1
0
mirror of https://github.com/arsenetar/dupeguru.git synced 2026-01-25 16:11:39 +00:00

Compare commits

..

1308 Commits

Author SHA1 Message Date
Virgil Dupras
6a28017c49 v4.0.3 2016-11-25 01:04:31 +00:00
Virgil Dupras
dc6933c90c Fix crash when cleaning picture cache 2016-11-25 00:59:51 +00:00
Virgil Dupras
e0281dd740 Fix previous commit
I forgot to remove a sparkle reference in the build script.
2016-11-23 20:25:32 -05:00
Virgil Dupras
79e99db1d3 cocoa: remove Sparkle
It's a deployment headache. Old sparkle versions generate runtime warnings about security and up to date version requires me to compile on 10.10, but after many tries, it seems that I absolutely need to build on my minimum requirements version which is 10.8. So screw Sparkle.
2016-11-23 19:51:55 -05:00
Virgil Dupras
76cc2000ab Add UI preference to picture cache type under Qt 2016-11-22 02:41:43 +00:00
Virgil Dupras
e4b6e12d4c Update tox warning exception
E305 somehow popped up as a default warning which I don't care about.
2016-11-22 02:39:51 +00:00
Virgil Dupras
c58a4817ca Add shelve-based picture cache implementation
Hopefully, this will fix #394 for real this time, that is, without the
need for a messy python executable ship in the app.
2016-11-15 19:58:18 -05:00
Virgil Dupras
f7adb5f11e Whitespace normalization 2016-11-15 19:57:30 -05:00
Virgil Dupras
c43044ea4c Remove unused imports 2016-11-15 19:56:19 -05:00
Virgil Dupras
cc01e8eb09 Move pe.cache.Cache into its own unit, cache_sqlite
This prepares us for an upcoming alternative cache implementation.
2016-11-13 17:01:20 -05:00
Virgil Dupras
1c20e5c770 v4.0.2 2016-10-09 12:32:04 -04:00
Virgil Dupras
edcff588e2 Update po from code 2016-08-25 21:43:51 -04:00
Virgil Dupras
26aad6e948 Add DESTDIR variable to makefile
That allows us to install in a sandbox.
2016-08-24 22:22:20 -04:00
Virgil Dupras
c303a490ef Make 'make env' a bit more solid
In some context, we don't end up with a bin/pip executable in our venv.
It's better to call pip as a module.
2016-08-24 22:04:58 -04:00
Virgil Dupras
6ed4499a97 v4.0.1 2016-08-24 20:31:58 -04:00
Virgil Dupras
aa7499aa12 Add make install and make uninstall 2016-08-23 23:02:38 -04:00
Virgil Dupras
63558d647a Add 'make srcpkg' 2016-08-23 19:10:03 -04:00
Virgil Dupras
eb3f7d65de Adjust requirements to pytest 3.0 release
pytest-monkeyplus isn't pytest 3.0 compatible yet, so we have to
explicitly forbid pytest 3.0+ in requirements.
2016-08-22 22:16:45 -04:00
Virgil Dupras
ac8a336c4a Fix picture mode's fuzzy block scanner threshold
It was always wrongly set to it's weirld old default 75 threshold.

fixes #387
2016-08-22 21:35:46 -04:00
Virgil Dupras
0206f2fd15 makefile: compile PE extensions without build.py 2016-08-16 22:03:43 -04:00
Virgil Dupras
b41d3f7efc Improve makefile's i18n target
It now uses proper dependencies and directly calls msgfmt.
2016-08-16 20:59:05 -04:00
Virgil Dupras
c43d37582e Fix syntax error in greek po file 2016-08-16 20:55:59 -04:00
Virgil Dupras
30a278719b Moved credits to the root folder
It was a real pain to edit credits files in all languages.
2016-08-16 20:18:49 -04:00
Virgil Dupras
87ef46ca15 Update hscommon and qtlib subrepos
They contain our new greek translation.
2016-08-16 20:01:25 -04:00
1kakarot
9f3ec065ed Added 'el' locale (#382) 2016-08-16 19:59:04 -04:00
Virgil Dupras
e19056048c Clarify the Windows situation in README 2016-08-16 19:33:39 -04:00
Virgil Dupras
76e5817ff3 Add Makefile
I finally took the time to properly learn how to write makefiles. This
was long overdue, but here we go.

Much of the makefile wraps `build.py`, but gradually, we'll extract
stuff from there until the makefile is the main container for build
logic.
2016-08-15 22:54:22 -04:00
Virgil Dupras
20dc2d63fd qt: save prefs on close more predictably
Ticket #379 reports crashes on quit due to `willSavePrefs` being called
when result and details dialogs are already freed. I can't reproduce the
crash, but it's still a bad idea to rely on the timing of
`aboutToQuit()` to launch this process.

This commits uses a more predictable place to emit `willSavePrefs` and
I'm pretty sure it will fix the crash at #379.
2016-08-14 21:11:24 -04:00
Virgil Dupras
28d2aa8197 cocoa: fix crash on load results
During createResultsWindow(), we would initialize the details panel too late.

fixes #380
2016-08-14 20:31:14 -04:00
Virgil Dupras
5be9d537a5 qt: fix broken load results dialog
`QFileDialog.getOpenFileName`, under pyqt5, returns a tuple, not only a
file path.
2016-08-14 20:01:46 -04:00
Virgil Dupras
b97e89d4d8 package.py: use proper prefix for submodules archive file 2016-08-13 20:37:08 -04:00
Virgil Dupras
0f4992de47 package.py: include submodules src in tar.gz
Otherwise, that results in an incomplete source package!
2016-08-13 20:30:24 -04:00
Virgil Dupras
55ad9ef33a Fix qt.platform.BASE_PATH location
It was wrong since it was moved from qt/base.

fixes #378
2016-07-21 20:39:15 -04:00
Virgil Dupras
e69a1764a0 Fix cocoa build script
It wouldn't properly find python 3.5 dylib for linking.
2016-07-01 19:50:19 -04:00
Virgil Dupras
215307df93 Remove this dependency inclusion thing in src packages
It's pointless and wasteful.
2016-07-01 17:12:31 -04:00
Virgil Dupras
3aa99c396b Bump OS X requirements to 10.8 and update README
Because of Sparkle, it's now required to build dupeguru on 10.10+, but with MACOSX_DEPLOYMENT_TARGET, which we now properly set, the results properly runs on 10.8.

This requires a python that has also been compiled with  MACOSX_DEPLOYMENT_TARGET=10.8
2016-07-01 15:36:15 -04:00
Virgil Dupras
9f2c3e7732 Fix failing test on OS X / py35
A 100 recursion limit was too low in that environment.
2016-07-01 15:29:50 -04:00
Eugene San
d660cef245 Update packaging to conform with package unification and few fixes (#372)
* Rename package (dupeguru-se -> dupeguru)
* Update package name in .desktop files and scripts
* Add Ubuntu package building instructions
* Fix build_pe_modules.py
* Add description to package
* Add conflicts dependencies to replace previous versions
* Update python version
* Unify .json configs
* Few cosmetics changes (mainly missing end-lines and images permissions)
2016-06-28 22:39:23 -04:00
Virgil Dupras
bdd404ce0e Remove appscript from OS X requirements
It's not needed anymore.
2016-06-10 09:48:53 -04:00
Virgil Dupras
df9f72d9bf v4.0.0 2016-06-10 09:16:54 -04:00
Virgil Dupras
53bbc5901c Add xenial to the list of supported ubuntu distros 2016-06-10 09:15:20 -04:00
Virgil Dupras
0959f4581e Update to Sparkle 1.14 2016-06-08 13:28:52 -04:00
Virgil Dupras
b1ef3dc8fe Simplify progress report during scanning
We now get less progress feedback, but in exchange, our progress job is
simpler. Previously, our progress bar would often get wonky towards the
end of the scan and I didn't have the energy to debug that.

Besides, people don't care about that level of progress feedback.
2016-06-08 12:29:28 -04:00
Virgil Dupras
334f4dd2ae Increase md5 reading buffer to 1mb
This makes md5 computing faster without using too much memory.
2016-06-08 12:23:10 -04:00
Virgil Dupras
fbdd1d866e Simplify getmatches_by_contents() signature
partial and sizeattr attributes are not needed anymore.
2016-06-08 12:06:08 -04:00
Virgil Dupras
64e86c9ff9 Add ctags config 2016-06-07 21:36:25 -04:00
Virgil Dupras
80f659858c Fail with excplicit message when unable to load results file
Previously, we would simply show an empty results window. Not very
helpful.
2016-06-07 21:34:04 -04:00
Virgil Dupras
ef8f8f0e44 Fix broken tests 2016-06-07 21:32:30 -04:00
Virgil Dupras
b7a7282c2a Fix results loading
The merge operation broke it. It would try to access a result_table that
didn't exist yet.
2016-06-07 16:56:59 -04:00
Virgil Dupras
668821301c Update documentation 2016-06-06 20:48:26 -04:00
Virgil Dupras
13fb06a693 Remove ContentsAusio scan type
It had few uses and had a confusing name. People though it did fuzzy
audio data matching, which it does not.
2016-06-06 17:08:41 -04:00
Virgil Dupras
61b219ff43 flake8 fix 2016-06-06 17:05:48 -04:00
Virgil Dupras
c4aeda0bd0 Clean up README 2016-06-06 17:00:57 -04:00
Virgil Dupras
76f3332d36 Remove windows leftover file 2016-06-06 17:00:38 -04:00
Virgil Dupras
b47b1e11af cocoa: remove inter.app_se
The last remnant of the pre-merge era.
2016-06-06 11:20:45 -04:00
Virgil Dupras
168d94910b cocoa: fix image loading in picture mode details panel
I had broken it during the big merge.
2016-06-06 11:03:09 -04:00
Virgil Dupras
ca3172044f qt: move scan type and app mode selector to the top of the window 2016-06-06 10:29:02 -04:00
Virgil Dupras
f66849b09d Fix tox tests 2016-06-06 10:21:32 -04:00
Virgil Dupras
8c1078aa71 cocoa: merge se/me/pe into one single app
That merge has already been done in core and qt, we're following.

I broke picture scan details panel image loading. Will fix later.
2016-06-05 21:18:48 -04:00
Virgil Dupras
b780816e3c Merge commit 'a65077f871481ca98ce51810751e66f228cb096a'
# Conflicts:
#	build.py
#	core/pe/iphoto_plist.py
2016-06-05 13:18:33 -04:00
Virgil Dupras
fb8a384a6a cocoa: get rid of edition-specific ResultWindow overrides 2016-06-04 21:18:14 -04:00
Virgil Dupras
2be4ae8f65 cocoa: move scan type selector to directory window
Also, use dynamic scan type labels supplied by core.
2016-06-02 21:31:12 -04:00
Virgil Dupras
f8686ffb55 cocoa: remove iPhoto and Aperture support
These apps don't exist anymore.
2016-06-01 22:34:16 -04:00
Virgil Dupras
3093a42553 cocoa: remove iTunes support
It was an unmaintained feature that wasn't working well with recent OS X releases.
2016-06-01 22:12:27 -04:00
Virgil Dupras
83d934fd4f cocoa: make auto-update URLs HTTPS 2016-06-01 21:57:27 -04:00
Virgil Dupras
f3c09c7a8d cocoa: adjust to latest changes
...that is, scanner on-the-fly instantiation and fileclasses/folderclass config move.

We haven't moved the scan type selector in the UI yet.
2016-06-01 21:56:18 -04:00
Virgil Dupras
a65077f871 Merge core_{se,me,pe} into core.{se,me,pe} 2016-05-31 22:32:37 -04:00
Virgil Dupras
d4919054f9 qt: move qt.base units into qt root package 2016-05-31 21:59:31 -04:00
Virgil Dupras
773f6651e6 Merge core_se.app into core.app 2016-05-31 21:43:24 -04:00
Virgil Dupras
9a25670552 qt: merge se.app into base.app 2016-05-31 21:22:50 -04:00
Virgil Dupras
8c9ef3ea29 Re-add the Clear Picture Cache action 2016-05-31 20:55:32 -04:00
Virgil Dupras
7256adb4d4 qt: remove UI testapp 2016-05-31 20:23:09 -04:00
Virgil Dupras
ad45a6e16e Adapt build/package scripts to single-edition 2016-05-31 20:21:07 -04:00
Virgil Dupras
c865f84c16 Merge PE into SE 2016-05-30 22:27:59 -04:00
Virgil Dupras
7d749779f2 qt: merge ME edition into SE
(breaks PE temporarily)

Adds a Standard/Music Application Mode button to SE and thus adds the
ability to run ME scan types in SE. When in Music mode, the
Music-specific results window, details panel and preferences panel will
show up.

All preferences except scan_type become shared between app modes
(changing the pref in a mode changes it in the other mode).

Results Window and Details Panel are now re-created at each scan
operation because they could change their type between two runs.

Preferences panel is instantiated on the fly and discarded after close.

This is a very big merge operation and I'm trying to touch as little
code as possible, sometimes at the cost of elegance. I try to minimize
the breakage that this change brings.
2016-05-29 22:37:38 -04:00
Virgil Dupras
8b878b7b13 core_me: properly set scanner class
It was wrongly instatiating the scanner on startup (we now do it
on-the-fly).
2016-05-29 17:22:46 -04:00
Virgil Dupras
0056f696df refactoring: move fileclasses and folderclass options in app class
Previously, it was in `Directory`.

This will make our job easier for an upcoming SE/ME/PE merge.
2016-05-29 17:15:55 -04:00
Virgil Dupras
abd2f3a9d6 core_pe: fix missing scanner option refactoring 2016-05-29 17:08:54 -04:00
Virgil Dupras
5c57a2a8fc Instantiate Scanner on-the-fly
Previously, it would be instantiated on startup.

This will make our job easier for an upcoming SE/ME/PE merge.
2016-05-29 16:52:07 -04:00
Virgil Dupras
dc76f9744e Update qtlib subrepo 2016-05-29 16:48:44 -04:00
Virgil Dupras
130581db53 Apply flake8 checks to tests 2016-05-29 15:02:39 -04:00
Virgil Dupras
9ed4b7abf0 refactoring: take ignore_list out of Scanner class
It's now `DupeGuru` that holds it and passes it to `get_dupe_groups()`,
the only place where it's actually used in `Scanner`.

This will make the SE/ME/PE merge easier by allowing us to instantiate
the Scanner on-the-fly since it doesn't hold state anymore.
2016-05-29 14:13:19 -04:00
Virgil Dupras
a0a90e8ef8 Update qtlib subrepo 2016-05-28 22:20:42 -04:00
Virgil Dupras
197acbf5b3 qt: move scan_type preference to main window
It leads to better discoverability of dupeguru's options and will make
more sense after the big merge of all editions.
2016-05-28 21:54:25 -04:00
Virgil Dupras
09d5243648 Bump python requirement to v3.4 2016-05-27 19:28:19 -04:00
Virgil Dupras
10169bee9c Update qtlib
This updates `progress_window`, which fixes a bug where the progress
window would be mistakenly shown on starup.

fixes #357
2016-05-25 21:27:48 -04:00
Virgil Dupras
bb8a41f8c5 Do git submodule init/update in bootstrap script 2016-05-25 21:15:56 -04:00
Virgil Dupras
bb1f0f5be6 Convert hscommon, qtlib and cocoalib to submodules
... rather than subtrees. That also represents a small qtlib updates
which needed a code adjustment.
2016-05-25 21:07:30 -04:00
Virgil Dupras
4b6f8b45e2 Fix tox tests
Add new warning to ignores
2016-05-24 22:54:28 -04:00
Virgil Dupras
2ed1b82ecf Push edition-specific scan option listing down to the core
... rather than have each UI layer repeat them.

Did qt, but not cocoa yet.
2016-05-24 22:53:03 -04:00
Virgil Dupras
de9122c3cb Remove obsolete ABOUT_LICENSE
dupeGuru is GPL now
2016-05-24 22:36:37 -04:00
Yichi Zhang
632650b483 Fix some compiler warnings (Cocoa) 2016-04-04 22:08:58 -04:00
Virgil Dupras
c05f01853d Merge remote-tracking branch 'patrick/feature/tox-py35' 2016-01-05 17:21:44 -05:00
Virgil Dupras
15539eb3c5 flake8 fix 2016-01-05 17:16:39 -05:00
Virgil Dupras
b9874cc7ed Add tox instructions in README
Also, remove py33 from tox envlist
2016-01-05 17:14:05 -05:00
Patrick Atamaniuk
13a2868dd2 add py35 to tox environments 2016-01-05 22:15:42 +01:00
Virgil Dupras
abb1345c49 Update FAQ 2015-12-26 09:20:07 -05:00
Virgil Dupras
9c53b2218c Update waf to v1.8.17
This allows us to call it from Python 3.5 without the failures we previously had.
2015-12-25 10:26:56 -05:00
Virgil Dupras
4b3c1e2828 Add Windows/Mac maintainer notice in the README 2015-12-24 20:50:33 -05:00
Virgil Dupras
b64f9f5ec0 Add support for Python 3.5 and pyenv's pythons on OS X 2015-10-27 21:33:41 -04:00
Virgil Dupras
40d9a486e2 Add Spanish and Dutch localizations
Thanks Josep and Kees Duvekot!

Also, made the language selector sorted alphabetically. It was getting
confusing in there.
2015-07-20 13:18:14 -04:00
Virgil Dupras
6930e092e0 Document branching in the repo 2015-07-20 13:02:14 -04:00
Virgil Dupras
6b41223a22 Add missing pl_PL log in cocoalib/qtlib 2015-07-20 12:54:38 -04:00
Virgil Dupras
d15321a8e9 Update locales from transifex
Also, add missing Korean locales from cocoalib/qtlib, which prevented
proper build on OS X.
2015-07-20 12:50:58 -04:00
Virgil Dupras
d6533cbfa2 Update README 2015-07-03 19:41:10 -04:00
Virgil Dupras
43974f9ebd Update Russian localisation (from Igor Fokusov) 2015-04-14 19:07:08 -04:00
Virgil Dupras
0068e7b85a Add Korean localization (from woosuk park) 2015-04-12 22:22:00 -04:00
Virgil Dupras
23b29eb5c3 Add Polish localization (from mstefanski1987) 2015-04-12 21:53:45 -04:00
Virgil Dupras
dba231cf21 Fix broken link in README :( 2015-04-12 15:34:29 -04:00
Virgil Dupras
f25b1f9f46 Fix typo in README 2015-04-12 15:33:07 -04:00
Virgil Dupras
60dd73f634 Add Current Status section to README 2015-04-12 15:31:01 -04:00
Virgil Dupras
3b6fe992c0 Clarify documentation about results filtering
It wasn't clear that filtering was applied to whole paths.

ref #294
2015-04-05 16:19:03 -04:00
Virgil Dupras
6d263215ad Fix wrong use_regexp option propagation to core (qt)
We need to flip `use_regexp` before sending it down to
`escape_filter_regexp`!

fixes #295
2015-04-05 09:17:35 -04:00
Virgil Dupras
bba20f4218 Improve bootstrap script by working around some problems
... notably, Ubuntu 14.04's python and python v3.4.1, which is still the
newest python 3.4 on some systems.
2015-03-01 08:56:01 -05:00
Virgil Dupras
bb9908abb4 Change license from BSD to GPLv3
See http://www.hardcoded.net/archive2014#2014-12-28 for context
2015-01-04 09:59:08 -05:00
Virgil Dupras
e7076bc3bd Change license from BSD to GPLv3
See http://www.hardcoded.net/archive2014#2014-12-28 for context
2015-01-03 16:33:16 -05:00
Virgil Dupras
fc16ea8c49 Change copyright year to 2015 2015-01-03 16:30:57 -05:00
Virgil Dupras
0c07046ec4 cocoalib update 2015-01-03 16:29:36 -05:00
Virgil Dupras
943a6570d8 Added Utopic Unicorn to the list of supported Ubuntu dists 2014-10-26 12:18:49 -04:00
Virgil Dupras
854a253d9f me v6.8.1 2014-10-26 12:00:54 -04:00
Virgil Dupras
4e477104a6 Use --deep flag when code signing under OS X
It is now required in new versions of OS X that the embedded Python framework is signed separately.
2014-10-18 11:09:18 -04:00
Virgil Dupras
79800bc6ed Added --arch-pkg option to package.py
Otherwise, AUR packages don't work with Arch lookalikes like Manjaro.
2014-10-17 15:58:45 -04:00
Virgil Dupras
6e7b95b2cf se v3.9.1 2014-10-17 15:51:48 -04:00
Virgil Dupras
bf09c4ce8a Nicely wrap PermissionDenied errors on save
In fact, all `OSError`.

ref #266
2014-10-17 15:46:43 -04:00
Virgil Dupras
b4a73771c2 Fix iCCP: known incorrect sRGB profile warnings in stderr
I processed all images through `convert -strip`.

It's still possible, however, to get these error if PE tries to open an
image with an invalid profile.
2014-10-17 15:45:07 -04:00
Virgil Dupras
2166a0996c 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.
2014-10-13 15:08:59 -04:00
Virgil Dupras
24643a9b5d Updated copyright year to 2014 in Cocoa about boxes
Better late than never.
2014-10-12 13:19:55 -04:00
Virgil Dupras
045051ce06 Fixed formatting in changelog_pe 2014-10-12 10:52:41 -04:00
Virgil Dupras
7c3728ca47 Converted hscommon.jobprogress.qt to Qt5 2014-10-12 10:52:21 -04:00
Virgil Dupras
91be1c7336 pe v2.10.1 2014-10-12 10:47:18 -04:00
Virgil Dupras
162378bb0a Updated hscommon 2014-10-12 10:39:21 -04:00
Virgil Dupras
4e3cad5702 Fixed minor typo 2014-10-12 10:15:07 -04:00
Virgil Dupras
321f8ab406 Catch MemoryError better in PE's block matching algo
fixes #264 (for good this time, hopefully)
2014-10-05 22:22:59 -04:00
Virgil Dupras
5b3d5f5d1c Tweaked the main dev help page to have actual reflinks 2014-10-05 20:12:38 -04:00
Virgil Dupras
372a682610 Catch MemoryError in PE's block matching algo
fixes #264 (hopefully)
2014-10-05 17:13:36 -04:00
Virgil Dupras
44266273bf Included hscommon.jobprogress in the devdocs 2014-10-05 17:12:10 -04:00
Virgil Dupras
ac32305532 Integrated the jobprogress library into hscommon
I have a fix to make in it and it's really silly to pretend that this
lib is of any use to anybody outside HS apps. Bringing it back here will
make things more simple.
2014-10-05 16:31:16 -04:00
Virgil Dupras
87c2fa2573 Updated README which was a bit outdated 2014-10-04 17:01:22 -04:00
Virgil Dupras
db63b63cfd Fix crash in PE when reading some EXIF tags
The crash was caused by ObjP, which crashed when converting `NSDictionary` containing unsupported types.

Updating ObjP to v1.3.1 does the trick.

fixes #263
fixes #265
2014-10-04 16:35:26 -04:00
Virgil Dupras
6725b2bf0f Updated German localisation, by Frank Weber 2014-09-28 13:40:09 -04:00
Virgil Dupras
990e73c383 Catch Spinx SystemExit when building help
In a recent Sphinx release, it started calling `sys.exit()` and that
caused our whole build process to exit prematurely.
2014-09-13 16:05:40 -04:00
Virgil Dupras
9e9e73aa6b qtlib: Fix broken SelectableList
It was still using `.reset()`, which disappeared in Qt5.

Fixes #254.
2014-07-01 08:30:56 -04:00
Virgil Dupras
8434befe1f me v6.8.0 2014-05-11 09:26:55 -04:00
Virgil Dupras
1114ac5613 Fixed debian packaging 2014-05-11 09:11:38 -04:00
Virgil Dupras
f5f29d775c Adapt IPhotoPlistParser to Python 3.4
This also means that Python 3.3 isn't supported anymore for that part.
Updated README accordingly.
2014-05-03 15:12:13 -04:00
Virgil Dupras
ebd7f1b4ce pe v2.10.0 2014-05-03 13:57:00 -04:00
Virgil Dupras
279b7ad10c Fix typo in README 2014-05-03 13:53:16 -04:00
Virgil Dupras
878205fc49 Fix empty ignore List dialog bug in PE
Re-instantiating a new scanner for PE  made the ignore list dialog
target the wrong ignore list. We now only instantiate a scanner once.

Fixes #253
2014-05-03 13:44:38 -04:00
Virgil Dupras
b16df32150 I'm giving PyCharm a try 2014-05-03 13:39:39 -04:00
Virgil Dupras
04b06f7704 Removed the setNativeMenuBar() call under Qt
I put it there to make the menu usable under Ubuntu 13.10, but since
14.04, this line actually brakes it.
2014-05-03 09:34:41 -04:00
Virgil Dupras
c6ea1c62d4 Fixed Windows packaging 2014-04-21 10:00:53 -04:00
Virgil Dupras
6ce0f66601 Fixed debian packaging 2014-04-19 18:32:11 -04:00
Virgil Dupras
ac3a9e3ba8 Removed Qt's "Check for updates"
It only worked on 32bit Windows, and it's gone now.
2014-04-19 18:21:56 -04:00
Virgil Dupras
903d2f9183 Improved arch packaging
No need to bundle a .desktop file with arch source packages anymore.
dupeGuru's source package takes care of that.
2014-04-19 17:50:40 -04:00
Virgil Dupras
ca709a60cf Updated copyright year to 2014 2014-04-19 12:19:11 -04:00
Virgil Dupras
a9b4ce5529 se v3.9.0 2014-04-19 12:17:26 -04:00
Virgil Dupras
9b82ceca67 Updated windows packaging for Qt5
We now only support 64bit Windows.
2014-04-18 13:22:04 -04:00
Virgil Dupras
4c7c279dd2 Avoid crashes on quit under Windows 2014-04-18 10:55:01 -04:00
Virgil Dupras
79db31685e Fixed crash on results double-click
Introduced by the Qt5 move. Looks like passing `None` to
`doubleClicked.emit()` doesn't cut it anymore.
2014-04-18 10:44:59 -04:00
Virgil Dupras
ba13b700b0 Fixed crashing save dialogs under Qt5 2014-03-30 15:57:07 -04:00
Virgil Dupras
640561a534 Updated F.A.Q.
Fixes #252
2014-03-30 13:43:29 -04:00
Virgil Dupras
e4f81cbf04 Update loc 2014-03-30 10:47:37 -04:00
Virgil Dupras
4be4825112 Bootstrapping: don't use system-site-packages under OS X 2014-03-30 10:26:09 -04:00
Virgil Dupras
7d107d8efa Moved Cocoa error reporting to Github mode. 2014-03-30 10:07:01 -04:00
Virgil Dupras
10d1363334 Changed the error report so it brings the user to Github directly
Making error reporting too easy results in too much context-less
tracebacks which demand attention and, in the end, aren't of much use.

Requiring the user to report errors on Github will reduce the number of
reports, but hopefully make these reports have better context.
2014-03-29 17:42:23 -04:00
Virgil Dupras
b76820ebde Fixed bootstrapping under Python 3.3 2014-03-28 16:27:45 -04:00
Virgil Dupras
72b3cfb364 Adapted bootstrapping procedure to Python 3.4 2014-03-28 16:21:05 -04:00
Virgil Dupras
8b83ed0e5c Removed needless PyQt signal overloading
After a PyQt5 update, dupeGuru wouldn't run anymore because it choked on
signal overloading that weren't necessary.
2014-03-27 19:09:10 -04:00
Virgil Dupras
781f13ae1a Overwrite subfolders' state when setting states in folder dialog
Fixes #248
2014-03-15 17:31:33 -04:00
Virgil Dupras
8193bbae6e Fixed broken tests in core_me 2014-03-15 14:09:36 -04:00
Virgil Dupras
4cafeaff91 Don't crash on malformed integer in iPhoto plist
Simply default to 0. Fixes #214.
2014-03-15 14:06:20 -04:00
Virgil Dupras
95c6a7d41f Add debugging data to iPhoto plist parsing
Fixes #233.
2014-03-15 13:59:15 -04:00
Virgil Dupras
a29e007475 cocoalib: Replaced the "Relevant Console log" mechanism
The old grepping method wasn't reliable and now, we simply keep the last
20 logs in memory to place in that section of error reporting.
2014-03-15 13:57:34 -04:00
Virgil Dupras
d924d7797a Qt: Don't use a native menubar for the Result Window
Having two native menu bars in the app made the result window all
glitchy under Ubuntu 13.10.
2014-02-15 21:02:38 -05:00
Virgil Dupras
33c217ecc8 Straightened out Qt window parenting chain 2014-02-15 15:05:46 -05:00
Virgil Dupras
c9035046ae Updated cocoalib 2014-02-01 17:54:30 -05:00
Virgil Dupras
ad31016825 Updated qtlib 2014-02-01 17:17:15 -05:00
Virgil Dupras
c809066a93 Updated hscommon 2014-02-01 16:18:00 -05:00
Virgil Dupras
60ca27b5e1 Make Cocoa use the new FTP report-sender 2014-01-26 15:27:02 -05:00
Virgil Dupras
1104e24408 Error reports are now dropped by FTP on drop.hardcoded.net 2014-01-26 15:03:24 -05:00
Virgil Dupras
f66db94ffd Merge branch 'master' into develop
Conflicts:
	bootstrap.sh
2014-01-26 09:49:57 -05:00
Virgil Dupras
d98b5b22da polib is now on PyPI 2014-01-26 09:48:30 -05:00
Virgil Dupras
937748e838 Improved source packaging and bootstrapping 2014-01-26 09:41:15 -05:00
Virgil Dupras
37ebf36cee Merge branch 'master' into develop
Conflicts:
	bootstrap.sh
2014-01-11 13:30:30 -05:00
Virgil Dupras
1c84bdd198 Fixed bootstrapping and README for pip 1.5 2014-01-11 13:27:31 -05:00
Virgil Dupras
4a2fa7cd2c Updated FAQ 2014-01-10 15:05:45 -05:00
Virgil Dupras
7d4110f6d3 Merge branch 'master' into develop
Conflicts:
	README.md
2014-01-10 15:00:02 -05:00
Virgil Dupras
8497343d7f Updated FAQ in docs 2014-01-05 21:45:57 -05:00
Virgil Dupras
235a2c2904 Added a "contribute" page to the docs. 2014-01-05 21:44:56 -05:00
Virgil Dupras
25169cfc20 Create an empty site.py in collect_stdlib_dependencies()
Since we have Python 3.3 as a minimum requirement, we don't need to
patch our site.py with copy_sysconfig_files_for_embed() anymore, but we
still need a site.py file on startup. We create it when we collect
stdlib deps.
2013-12-22 12:13:39 -05:00
Virgil Dupras
152f5f37ce pe v2.9.0 2013-12-22 10:23:54 -05:00
Virgil Dupras
3e42ad8469 Minimum Python version is now 3.3 2013-12-22 09:52:19 -05:00
Virgil Dupras
7ba2e38cd6 Package PyPI dependencies right into our source package 2013-12-21 12:13:26 -05:00
Virgil Dupras
c7c7a73384 me v6.7.0 2013-12-08 10:34:04 -05:00
Virgil Dupras
46f8984bdc Merge branch 'qt5' into develop
Conflicts:
	README.md
	qtlib/about_box.py
	qtlib/reg.py
	qtlib/reg_demo_dialog.py
	qtlib/reg_submit_dialog.py
2013-12-07 19:49:27 -05:00
Virgil Dupras
c7d306b7d5 Minimum Python version is now 3.3 2013-12-07 17:23:18 -05:00
Virgil Dupras
47e636e949 Merge branch 'develop' 2013-12-07 11:15:39 -05:00
Virgil Dupras
0562729d8b Improved build script's --clean. 2013-12-07 11:14:59 -05:00
Virgil Dupras
4a36227a18 v3.8.0 2013-12-07 10:57:30 -05:00
Virgil Dupras
28b8b2e415 Sync locs with Transifex 2013-12-07 10:26:01 -05:00
Virgil Dupras
fd82464564 Removed .tx config in hscommon (useless now) 2013-12-07 10:20:13 -05:00
Virgil Dupras
418acf6e5e Merge branch 'regless' into develop
Conflicts:
	cocoa/inter/app.py
	core/app.py
	hscommon/reg.py
	locale/cs/LC_MESSAGES/ui.po
	locale/de/LC_MESSAGES/ui.po
	locale/fr/LC_MESSAGES/ui.po
	locale/hy/LC_MESSAGES/ui.po
	locale/it/LC_MESSAGES/ui.po
	locale/pt_BR/LC_MESSAGES/ui.po
	locale/ru/LC_MESSAGES/ui.po
	locale/ui.pot
	locale/uk/LC_MESSAGES/ui.po
	locale/vi/LC_MESSAGES/ui.po
	locale/zh_CN/LC_MESSAGES/ui.po
	qt/base/app.py
2013-12-07 10:19:31 -05:00
Virgil Dupras
d14d076989 Disable symlink/hardlink option when not relevant (Cocoa)
Fixes #247.
2013-12-06 16:17:04 -05:00
Virgil Dupras
cb8bb5a70e Disable symlink/hardlink option when not relevant (Qt)
When the "Replace with links" option is not enabled, the choice of
symlink or hardlink is irrelevant and causes confusion. Implemented core
mechanism for controlling the enabled state of that option. Also
implemented the Qt interface for it. Cocoa-part is still to be done.

I used this opportunity to greatly enhance documentation of this part of
the code. I'm beginning to like documenting...

Ref #247.
2013-12-06 15:48:01 -05:00
Virgil Dupras
563c9aeff3 Updated README 2013-12-01 11:26:30 -05:00
Virgil Dupras
a0cc1f2e03 Fixed regless cocoa and updated locs 2013-11-30 18:23:42 -05:00
Virgil Dupras
01403a3f92 Removed fairware 2013-11-30 17:54:40 -05:00
Virgil Dupras
7116674663 Improved hscommon docs 2013-11-30 16:13:12 -05:00
Virgil Dupras
b6bc5de79c Improved hscommon docs
TIL sphinx is rather smart about partial class refrences (starting with
a ".")
2013-11-30 12:29:25 -05:00
Virgil Dupras
5a275db67d Improved hscommon doc
* Completed hscommon.gui.table's doc
* Use sphinx.ext.autosummary.
* Moved attribute docstrings directly into properties.
2013-11-30 12:15:03 -05:00
Virgil Dupras
31395d8794 Fix typos in docs 2013-11-28 22:49:26 -05:00
Virgil Dupras
3734bd6f6c Improved hscommon.gui docs
Added docs for Table and Row in hscommon.gui.table.
2013-11-28 22:38:07 -05:00
Virgil Dupras
da06ef8cad Improved hscommon.gui docs 2013-11-24 13:53:52 -05:00
Virgil Dupras
0b00171655 pygettext: explicitly open files as utf-8
When running it through SSH, I couldn't open files with non-ascii chars.
2013-11-24 10:22:05 -05:00
Virgil Dupras
c1cfa86ad1 Make Cmd+A select all folders in the Folder Selection dialog (Cocoa)
Fixes #228.
2013-11-24 10:12:47 -05:00
Virgil Dupras
c34c9562d3 Make non-numeric delta comparison case insensitive
Fixes #239.
2013-11-23 15:31:20 -05:00
Virgil Dupras
0e542577b0 Merge branch 'master' into develop 2013-11-23 12:39:59 -05:00
Virgil Dupras
42be49da83 Fix surrogate-related UnicodeEncodeError on CSV export
Fixes #210.
2013-11-23 12:38:55 -05:00
Virgil Dupras
398ac9b7c6 Greatly improved docs
Added a new scan.rst page, laying out in much more details than before
the inner workings of the scanning process.

Fixes #208, but does much more than that.
2013-11-17 12:03:48 -05:00
Virgil Dupras
508e9a5d94 Reorganized hscommon documentation
Removed hscommon's "docs" folder and moved all documentation directly
into docstrings. Then, in dupeGuru's developer documentation, added
autodoc references to relevant modules.

The result is a much more usable hscommon documentation.
2013-11-16 14:46:34 -05:00
Virgil Dupras
cc5ea1dbc1 Fixed qt5 migration for ME and PE 2013-11-16 13:38:07 -05:00
Virgil Dupras
3b8d355b9e Merge branch 'develop' into qt5
Conflicts:
	hscommon/desktop.py
2013-11-16 12:11:32 -05:00
Virgil Dupras
10dbfa9b38 Refactoring: Path API compatibility with pathlib
Refactored dupeGuru to make hscommon.path's API a bit close to pathlib's
API. It's not 100% compatible yet, but it's much better than before.

This is more of a hscommon refactoring than a dupeguru one, but since
duepGuru is the main user of Path, it was the driver behind the
refactoring.

This refactoring also see the introduction of @pathify, which ensure
Path arguments. Previously, we were often unsure of whether the caller
of a function was passing a Path or a str. This problem is now solved
and this allows us to remove hscommon.io, an ill-conceived attempt to
solve that same ambiguity problem.

Fixes #235.
2013-11-16 12:06:16 -05:00
Virgil Dupras
e8c42740cf Fixed tests which were broken 2013-11-10 12:54:35 -05:00
Virgil Dupras
4b6c4f048d Fixed compilation warnings on OS X 2013-11-10 12:41:10 -05:00
Virgil Dupras
7594cccf8c Fixed build on OS X which was broken 2013-11-10 12:39:02 -05:00
Virgil Dupras
1d9573cf6f On OS X, read Exif tags using Cocoa's built-in functionality
This allows for RAW files Exif reading. Fixes #234.
2013-11-10 12:00:16 -05:00
Virgil Dupras
76f45fb5a6 Fixed appdata logic which was broken on OS X. 2013-11-10 11:05:03 -05:00
Virgil Dupras
12cf9b800b Merge branch 'master' into develop 2013-11-09 16:21:59 -05:00
Virgil Dupras
ba7e6494c6 Fixed crash on Dupe Count sorting with Delta + Dupes Only
Fixes #238
2013-11-09 16:20:33 -05:00
Virgil Dupras
72d8160b28 Fix boken tests 2013-11-08 16:45:14 -05:00
Virgil Dupras
6d53511cee Merge branch 'master' into develop 2013-11-08 16:03:35 -05:00
Virgil Dupras
64d3c211e6 Updated README 2013-10-20 16:26:16 -04:00
Virgil Dupras
fad112f554 Merge branch 'develop' into qt5 2013-10-20 16:02:36 -04:00
Virgil Dupras
a563327723 Updated cocoalib 2013-10-20 16:01:59 -04:00
Virgil Dupras
096e2bb78a Updated hscommon 2013-10-20 16:01:27 -04:00
Virgil Dupras
5a8cb6f5e3 Implemented super() inheritance style suggested by PyQt5 2013-10-20 15:53:59 -04:00
Virgil Dupras
664d630b96 Fixed occasional core dumps on exit 2013-10-20 15:38:24 -04:00
Virgil Dupras
a4256d3d2b First Qt5 conversion commit
Replaced PyQt4 with PyQt5 and made all adjustments necessary to make
dupeGuru start up.
2013-10-20 15:15:09 -04:00
Virgil Dupras
8e65f15e1a Fixed core.engine.Match docstring
The way it was set made dupeGuru crash under Python 3.2
2013-10-20 13:33:27 -04:00
Virgil Dupras
9ea9f60e92 Added packaging support for ubuntu 13.10 2013-10-19 14:37:01 -04:00
Virgil Dupras
8efefaf0bf Improved API docs 2013-10-12 13:55:36 -04:00
Virgil Dupras
33d9569427 Refactoring: Created hscommon.desktop
This unit hosts previously awkward UI view methods which weren't related
to the view itself, but to the current desktop environment. These
functions are now at their appropriate place.
2013-10-12 13:54:13 -04:00
Virgil Dupras
2fdfacb34e Docs: Fix ugly nulljob repr in method signatures 2013-10-11 12:15:02 -04:00
Virgil Dupras
97fcf1ffa8 Fixed debian packaging
.so files were included in the source package, which messed up builds
under archs that weren't the same as the srcpkg creator (namely, i386
builds).
2013-09-22 09:38:52 -04:00
Virgil Dupras
350b2c64e0 Fixed nasty crash during PE's Cocoa block scanning
Using PyUnicode_GET_SIZE was obviously wrong, but I'm guessing that the str changes in py3.3 made that wrongness significant...
2013-08-26 07:17:02 -04:00
Virgil Dupras
dcc57a7afb Ah crap, another Cocoa fatal mistake 2013-08-25 17:10:26 -04:00
Virgil Dupras
8b510994ad pe v2.8.0 2013-08-25 10:53:08 -04:00
Virgil Dupras
4a4d1bbfcd Eased "Clear Picture Cache" triggering under Qt
Added a keybinding and added the action to the directories dialog's menu
(it was previously only in the results window's menu). Fixes #230.
2013-08-25 10:47:10 -04:00
Virgil Dupras
78c3c8ec2d Improved dev docs 2013-08-20 22:52:43 -04:00
Virgil Dupras
e99e2b18e0 Call sphinx-build from withing Python instead of a subprocess 2013-08-19 17:43:32 -04:00
Virgil Dupras
ae1283f2e1 se v3.7.1 2013-08-19 16:48:07 -04:00
Virgil Dupras
cc76f3ca87 Fixed SE folder scanning under Cocoa 2013-08-18 21:07:33 -04:00
Virgil Dupras
be8efea081 Fixed folder scanning in SE, which was completely broken
Oops
2013-08-18 20:50:31 -04:00
Virgil Dupras
7e8f9036d8 Began serious code documentation effort
Enabled the autodoc Sphinx extension and started adding docstrings to
classes, methods, etc.. It's quickly becoming quite interesting...
2013-08-18 18:36:09 -04:00
Virgil Dupras
8a8ac027f5 Fixed ME's cocoa interface file, which was broken (again)
The Remove Dead Tracks didn't use the new job system and appscript wasn't properly packaged.
2013-08-18 11:23:20 -04:00
Virgil Dupras
1d9d09fdf7 Fixed ME's cocoa interface file, which was broken
It tried to update JOBID2TITLE from inter.app, but it has moved to core.app.
2013-08-18 10:48:02 -04:00
Virgil Dupras
5dc956870d me v6.6.0 2013-08-18 10:16:39 -04:00
Virgil Dupras
d8f48cbd42 Fixed 32bit Windows packaging for Python 3.3
Python 3.3 is compiled with VS2010, and the old VS2008 pre-requisite
scheme doesn't work anymore. We now do like with 64bit, include the DLLs
directly in the package.
2013-08-17 14:48:36 -04:00
Virgil Dupras
39d24817f6 Added packaging for Ubuntu 13.04 2013-08-17 11:42:19 -04:00
Virgil Dupras
2364e44707 Tweaked bootstrap script so it works on Ubuntu
Ubuntu 13.04 doesn't have the pyvenv command. Instead, it's pyvenv-3.3.
Replaced pyvenv with python3 -m venv.
2013-08-17 11:32:49 -04:00
Virgil Dupras
3e2249bf89 se v3.7.0 2013-08-17 11:13:16 -04:00
Virgil Dupras
38acb6f91c Updated french doc 2013-08-17 10:41:41 -04:00
Virgil Dupras
9bcb28d5e2 Fixed inaccuracies in docs 2013-08-16 18:06:58 -04:00
Virgil Dupras
d0a3f081da Tweaked bootstrap script to work on OS X 2013-08-04 16:18:31 -04:00
Virgil Dupras
d11ec557e7 Added bootstrap script for easy build setup 2013-08-04 15:57:39 -04:00
Virgil Dupras
b9124a497c Added Phan Anh to the credits for the vietnamese loc 2013-08-04 10:19:05 -04:00
Virgil Dupras
502715cfd6 Updated locs from Transifex 2013-08-04 10:18:38 -04:00
Virgil Dupras
e1f532e2fd Fixed broken tests 2013-08-04 09:26:18 -04:00
Virgil Dupras
a71033d9d6 Added a splitter control to the Re-Prioritize dialog
Fixes #224
2013-08-04 09:20:08 -04:00
Virgil Dupras
a15a62f55c Fixed progress under Cocoa which always cancelled the job
Yeah, it's funny, same problem as with Qt, but for different reasons.
2013-08-04 09:11:19 -04:00
Virgil Dupras
2fe5cdcf02 Fixed progress under Qt which always cancelled the job 2013-08-03 21:28:02 -04:00
Virgil Dupras
21c64545e5 Fixed job UI cancellation
It was broken since the modernization.
2013-08-03 18:33:35 -04:00
Virgil Dupras
c93a88f8b0 Fix startup crash with PE
Fixes #232
2013-08-03 18:01:28 -04:00
Virgil Dupras
86a81eab4e Added the Vietnamese language 2013-08-03 17:36:53 -04:00
Virgil Dupras
1c779cb3ec Removed spurious debug code 2013-08-03 17:36:12 -04:00
Virgil Dupras
04949c853d Pulled all locs from Transifex
Vietnamese was added.

There's also updated to Russian and Brazilian.
2013-08-03 17:34:02 -04:00
Virgil Dupras
ff782a09f5 Added the --normpo build option
This build command normalizes all PO so that I stop getting
spurious diffs whenever I pull from Transifex.
2013-08-03 17:13:24 -04:00
Virgil Dupras
e5ce6680ca Modernized progress window GUI
Following the refactoring that has been initiated in pdfmasher's
"vala" branch, I pushed more progress window logic into the
core.

The UI code is now a bit dumber than it used to be, and the core
now directly decides when the progress window is shown and
hidden. The "job finished" notification is also directly sent by the
core. Job description update logic is handled by a core gui
textfield.

Job description contsants also moved to the core, triggering
a localisation migration from "ui" to "core".
2013-08-03 16:27:36 -04:00
Virgil Dupras
8e15d89a2e Updated cocoalib subtree. 2013-08-03 11:12:31 -04:00
Virgil Dupras
d874f26f06 Updated qtlib subtree 2013-08-03 11:06:58 -04:00
Virgil Dupras
80a99ff29e Updated hscommon subtree 2013-08-03 10:59:44 -04:00
Virgil Dupras
b11b97dd7c Improved delta values to support non-numerical values
Delta values now work for non-numerical values. Any column,
when its value differs from its ref, becomes orange.

A column that was already a "delta column" keeps its previous
behavior (dupe cells for these columns are always displayed in
orange).

Sorting behavior, when Dupes Only and Delta Values are enabled
at the same time, has also been extended to non-numerical
values, making it easy to mass-mark dupe rows with orange
values.

Documentation was updated, unit tests were added.

Fixes #213
2013-07-28 17:45:23 -04:00
Virgil Dupras
386a5f2c64 Fixed results display bug under Mac OS X
Since the latest refactoring, results wouldn't display properly.
2013-07-28 16:41:07 -04:00
Virgil Dupras
c13a2f207c Dropped i386 support under Mac OS X. 2013-07-28 16:39:53 -04:00
Virgil Dupras
d36710ef38 Docs: Changelog issue now point to Github 2013-07-28 15:11:39 -04:00
Virgil Dupras
bbc9b003c6 Docs: Changed theme to haiku 2013-07-28 15:09:35 -04:00
Virgil Dupras
3edba28f0b Docs: Added a Developer Guide page 2013-07-28 15:09:17 -04:00
Virgil Dupras
9304f42f69 Use Send2Trash instead of the newly deprecated Send2Trash3k 2013-07-28 11:58:49 -04:00
Virgil Dupras
375963ebfd Docs: Updated F.A.Q 2013-07-28 11:29:36 -04:00
Virgil Dupras
7891fb5396 Refactoring: Moved some code from app.DupeGuru to fs.File.
Moved DupeGuru._get_display_info() to File.get_display_info().
This method used none of the app's global state or methods
and had nothing to do there.
2013-07-14 17:43:58 -04:00
Virgil Dupras
bdd5f0a515 Updated help about symlinks and hardlinks
It now mentions that Windows is supported and under which
conditions it is. Ref #220.
2013-07-14 14:06:01 -04:00
Virgil Dupras
db0901b1de Handle OSError during symlink support check
Under a windows that supports symlinks (Vista+), we still need
proper privileges. If we don't have it, OSError is raised and we
need to correctly handle this case. Ref #220.
2013-07-14 13:59:03 -04:00
Virgil Dupras
9225697053 Added hardlink/symlink support for Windows Vista+.
Fixes #220.
2013-07-14 11:58:49 -04:00
Virgil Dupras
097b949763 Tweaked README 2013-06-24 16:20:05 -04:00
Virgil Dupras
60701c2a5c Fixed package.py --src-pkg
Make it use "git archive" instead of "hg archive".
2013-06-23 09:36:44 -04:00
Virgil Dupras
3ef1281450 Updated README to include clearer build instructions 2013-06-22 21:43:24 -04:00
Virgil Dupras
af4e74a130 Added qtlib repo as a subtree 2013-06-22 21:34:41 -04:00
Virgil Dupras
422fb2670d Added cocoalib as a subtree. 2013-06-22 21:32:48 -04:00
Virgil Dupras
94a469205a Added hscommon repo as a subtree 2013-06-22 21:32:23 -04:00
Virgil Dupras
95623f9b47 Removed submodules. 2013-06-22 21:22:32 -04:00
Virgil Dupras
a65c246a2e Updated README so it talks about git submodules. 2013-06-21 21:37:20 -04:00
Virgil Dupras
045d496a98 Converted repo to Git. 2013-06-21 21:00:52 -04:00
Virgil Dupras
5ed98b3d92 Fixed Cocoa build for ME. 2013-05-18 13:14:04 -04:00
Virgil Dupras
5799e3548b me v6.5.1 2013-05-18 12:19:03 -04:00
Virgil Dupras
b01ed1e9f3 Added tag pe2.7.1 for changeset 3cfae8ce9120 2013-05-05 12:32:49 -04:00
Virgil Dupras
49839b8b8e Fix cocoa build for PE.
appscript and multiprocessing dependencies weren't properly packaged.
2013-05-05 11:37:28 -04:00
Virgil Dupras
500468ed1c Added the --src-pkg packaging option. 2013-05-05 10:47:43 -04:00
Virgil Dupras
de6f50ab12 pe v2.7.1 2013-05-05 10:17:24 -04:00
Virgil Dupras
2c6339f7a2 In PE's EXIF mode, don't match pictures without EXIF timestamp.
[#219 state:fixed]
2013-05-05 10:11:07 -04:00
Virgil Dupras
96c3d63557 Added tag se3.6.1 for changeset 810ab1e1324e 2013-04-28 17:46:59 -04:00
Virgil Dupras
e86b23259c Improved selection handling during ref-swapping under dupes-only mode.
Previously, our selection would simply be lost (because the dupes that were
just swapped wouldn't be present in the table). Now, instead of trying to
preserve our dupe-selection, we preserve our index-selection when in dupes-only
mode. [#222]
2013-04-28 16:27:45 -04:00
Virgil Dupras
89955aa96a Sync locs with Transifex (BR loc by Victor Figueiredo). 2013-04-28 16:15:58 -04:00
Virgil Dupras
39a601e393 Merge heads. 2013-04-28 14:57:16 -04:00
Virgil Dupras
3b9e11d14f se v3.6.1 2013-04-28 14:56:54 -04:00
Virgil Dupras
81a7c4d6a6 Sync locs with Transifex (BR loc by Victor Figueiredo). 2013-04-28 14:45:35 -04:00
Virgil Dupras
35a162faf4 Added "..." next to "Re-Prioritize Results" in cocoa's main menu.
[#225 state:fixed]
2013-04-28 14:32:57 -04:00
Virgil Dupras
70e505ad92 Tweaked Make Selected into Reference.
Having dupes from ref folders (which makes ref switching impossible) would make
the new feature glitchy (selection would be emptied). Now, in cases where the action
results in nothing being changed, the selection stays intact. [#222]
2013-04-28 14:12:08 -04:00
Virgil Dupras
aa3cf9700d Changed the shebang line to #!/usr/bin/python3 under Qt.
This way, the activity monitor correctly shows the app name instead of showing
"python3".
2013-04-28 11:38:41 -04:00
Virgil Dupras
e4b949abf6 README tweak again. 2013-04-28 11:06:48 -04:00
Virgil Dupras
2e23303d7e README tweak. 2013-04-28 11:04:34 -04:00
Virgil Dupras
2fcaacd285 Updated README which was somewhat outdated. 2013-04-28 11:03:38 -04:00
Virgil Dupras
6627f0dbea Renaming README to make Bitbucket correctly format it.
--HG--
rename : README => README.rst
2013-04-28 10:51:11 -04:00
Virgil Dupras
a64b42de65 Oops, there wasn't only one place to fix in [49fa0fc]. 2013-04-28 10:46:36 -04:00
Virgil Dupras
6dddcb1a47 Fixed scanner_test which was broken.
The monkeypatching made to hscommon.io wasn't correctly transferred to Path
after the migration at [dfc82cd].
2013-04-28 10:43:20 -04:00
Virgil Dupras
4a8ce9b6c4 Updated copyright year to 2013. 2013-04-28 10:35:51 -04:00
Virgil Dupras
d4e6632e7e Fixed typo in messages.
Sucessfully --> Successfully. [#216 state:fixed]
2013-04-28 10:20:59 -04:00
Virgil Dupras
0ced3e39c8 Allow "Open selected" to open more than one file at once.
When there's mpre than 10 selected dupes, a warning is shown, asking
confirmation. [#142 state:fixed]
2013-04-28 10:12:25 -04:00
Virgil Dupras
26d923e175 Updated localizations from Transifex. 2013-04-27 10:42:55 -04:00
Virgil Dupras
592eba9eaa [#222] Tweaked dupe selection after a ref-swapping action. 2013-04-27 10:08:38 -04:00
Virgil Dupras
11450ae56a [#222] Tweaked ref-swapping action text.
Make Selected Reference --> Make Selected into Reference.
2013-04-27 09:38:08 -04:00
Virgil Dupras
008bd1414e Added tag pe2.7.0-arch for changeset 286ba6959cd0 2013-04-14 18:13:38 -04:00
Virgil Dupras
62f6cc3705 Added tag me6.5.0-arch for changeset df6e045b9e76 2013-04-14 17:59:23 -04:00
Virgil Dupras
e16041c703 Added tag se3.6.0-arch for changeset 6b42e0d5628b 2013-04-14 17:46:54 -04:00
Virgil Dupras
1103b58ec5 Fixed Arch packaging. 2013-04-14 17:39:13 -04:00
Virgil Dupras
dc8b1b3b02 Fixed typo in packaging. 2013-04-14 17:37:01 -04:00
Virgil Dupras
36b214443a Added packaging support for Arch Linux. 2013-04-14 17:21:08 -04:00
Virgil Dupras
d11e20f6ba Adapted to fairware changes. 2013-03-24 18:53:41 -04:00
Virgil Dupras
477f73ffa4 Make build and package work with a 64bit Windows install. 2013-03-24 12:02:41 -04:00
Virgil Dupras
0b5cd61540 Updated subrepo. 2013-03-24 11:33:08 -04:00
Virgil Dupras
17b5703885 While keeping demo dialogs, removed fairware dialogs.
Npw, when in "fairware mode", we simply show no dialog at all.
2013-03-24 11:27:02 -04:00
Virgil Dupras
7cac0b5d6e Added missing import in build script. 2013-03-24 11:15:27 -04:00
Virgil Dupras
a4003b6072 Removed fairware dialogs under Linux. 2013-03-24 11:10:07 -04:00
Virgil Dupras
fb26d7d077 When under Linux, load qt_*.qm files from the system Qt.
We previously bundled up these files in the .deb, but this was unnecessary.
2013-03-24 11:06:08 -04:00
Virgil Dupras
f2cbb513d3 Fixed build.py --updatepot on non-OSX systems.
Previously, when running this command on non-OSX system, the ui.pot file would
lose all its previsouly cocoa-related strings.
2013-03-24 10:59:41 -04:00
Virgil Dupras
8c36218150 In addition to EnvironmentError, catch UnicodeEncodeError when performing actions on marked duplicates.
When running dupeGuru under Linux with a messed up locale, it's
UnicodeEncodeError we get. Instead of popping a reportable traceback, it's
better to just pop the Problems dialog up.
2012-10-28 11:06:09 -04:00
Virgil Dupras
7637e493a6 In Debian packaging, create package for both precise and quantal. 2012-10-26 10:23:50 -04:00
Virgil Dupras
62be8da6f9 Pulled localizations from Transifex. 2012-10-24 16:45:25 -04:00
Virgil Dupras
b1c2941616 Fixed cocoa dev builds which were badly broken. 2012-09-14 12:55:53 -04:00
Virgil Dupras
8efd3033a3 Removed pluginbuilder usage in build script and replaced it with hscommon functions. 2012-09-10 15:37:57 -04:00
Virgil Dupras
d417dbd2e3 Removed code duplication with hscommon in the build script. 2012-09-10 10:14:50 -04:00
Virgil Dupras
3a717a86d8 Added Transifex config file. 2012-09-05 08:52:04 -07:00
Virgil Dupras
e1f7260774 Removed duplicate entries in some PO files. 2012-09-05 11:29:09 -04:00
Virgil Dupras
bfc1ee90ec Added "Content-Transfer-Encoding: utf-8\n" to po files. 2012-09-05 11:18:06 -04:00
Virgil Dupras
93a5fd01b5 Added "Content-Transfer-Encoding: utf-8\n" to pot files. 2012-09-05 11:15:18 -04:00
Virgil Dupras
b028670250 Updated locs and improved brazilian loc with Victor Figueiredo's tips. 2012-08-30 16:09:33 -04:00
Virgil Dupras
dff141e800 Renamed cocoa/base/ResultWindow to ResultWindowBase to avoid ambiguities in the result_winodw cocoa UI script.
--HG--
rename : cocoa/base/ResultWindow.h => cocoa/base/ResultWindowBase.h
rename : cocoa/base/ResultWindow.m => cocoa/base/ResultWindowBase.m
2012-08-30 16:08:21 -04:00
Virgil Dupras
f49c7dee96 Fixed cocoa build script to work with Python 3.3. 2012-08-30 15:35:18 -04:00
Virgil Dupras
5da793b029 Fixed wrong assignment in result_window cocoa ui. 2012-08-15 15:16:29 -04:00
Virgil Dupras
a56258f8b3 Fixed debian packaging to not include the recently moved qt run template. 2012-08-15 08:07:32 -07:00
Virgil Dupras
ab6e0945a7 Added the 'ubuntu-store' configuration option to build a package that is already registered for the Ubuntu Store. 2012-08-15 08:01:29 -07:00
Virgil Dupras
8ac035c8a9 Moved run templates from root folder to cocoa/qt subfolders.
--HG--
rename : run_template_cocoa.py => cocoa/run_template.py
rename : run_template_qt.py => qt/run_template.py
2012-08-15 07:33:01 -07:00
Virgil Dupras
1c15b0114b Updated subrepo URLs to use HTTPS instead of HTTP. 2012-08-15 07:27:57 -07:00
Virgil Dupras
e78b14f9a2 Added tag pe2.7.0 for changeset d773721e6c32 2012-08-11 12:31:33 -04:00
Virgil Dupras
573d088088 pe v2.7.0 2012-08-11 12:00:16 -04:00
Virgil Dupras
75b08125c0 [#201 state:fixed] Added an EXIF Timestamp column in PE. 2012-08-10 16:34:27 -04:00
Virgil Dupras
20320f539f [#199 state:fixed] Added a mtime column to PE's cache DB so that we can purge outdated caches. 2012-08-10 15:58:37 -04:00
Virgil Dupras
24771af955 Removed obsolete hgignore entries. 2012-08-10 15:15:38 -04:00
Virgil Dupras
2bfe9960f1 Fixed typo in changelog. 2012-08-10 11:13:23 -04:00
Virgil Dupras
215bcb0d76 Added tag me6.5.0 for changeset 8f478379ec62 2012-08-10 11:07:03 -04:00
Virgil Dupras
2dbf8b80ae Updated hsaudiotag version req. 2012-08-10 10:30:50 -04:00
Virgil Dupras
470cd92030 me v6.5.0 2012-08-10 10:23:35 -04:00
Virgil Dupras
111edc3ce5 Fixed a bug causing groups with more than one ref file in it to appear (which looks weird and messes with selection).
Contents scans already weeded them out, bu t they were still possible with name-based scans. Now, the Scanner removes them all.
2012-08-09 11:16:06 -04:00
Virgil Dupras
df30a31782 Refactoring: Began to phase out to the use of hscommon.io in favor of Path methods. 2012-08-09 10:53:24 -04:00
Virgil Dupras
91f3a59523 Fixed add_directory() test which were broken. 2012-08-09 10:22:04 -04:00
Virgil Dupras
3441e51c0e [#200 state:fixed] Fixed a KeyError wihle parsing iTunes XML. 2012-08-09 10:01:44 -04:00
Virgil Dupras
a99c40b5d8 Updated PO files from POTs. 2012-08-09 09:58:14 -04:00
Virgil Dupras
5b4de58c38 Oops, I had forgot one of the credits file. 2012-08-08 16:53:08 -04:00
Virgil Dupras
cd83b16dbd Added Kyrill Detinov to credits for russian loc improvements. 2012-08-08 16:39:17 -04:00
Virgil Dupras
b67db988ab Improvement to the Russian loc by Kyrill Detinov. 2012-08-08 16:32:14 -04:00
Virgil Dupras
7ebea44cb0 Added tag se3.6.0 for changeset 0f18c4498a6c 2012-08-08 11:27:04 -04:00
Virgil Dupras
4de40af1b0 Copy "en.lproj" in cocoa app so that english is actually chosen when it's at the top of the list in the Cocoa language settings. 2012-08-08 10:26:33 -04:00
Virgil Dupras
54988650d7 Updated xibless requirements. 2012-08-08 10:05:44 -04:00
Virgil Dupras
a47c208f45 v3.6.0 2012-08-08 10:05:15 -04:00
Virgil Dupras
c321427a8f [#206 state:fixed] Moved stdout wrapping under cxfreeze sooner at startup. 2012-08-07 12:37:17 -04:00
Virgil Dupras
26c77a18fd Merge heads. 2012-08-07 12:08:28 -04:00
Virgil Dupras
9f19451ac7 Fixed build problem on windows when the locale is non-english. 2012-08-07 12:07:14 -04:00
Virgil Dupras
bcd9d7e7d0 Disable the Symlink/Hardlink radio buttons under Windows. 2012-08-02 15:42:02 -04:00
Virgil Dupras
b42b0be512 Fixed cocoa building process which didn't put the "locale" folder in the Resource folder anymore, causing core and hscommon locs not to be shown. 2012-08-02 15:30:20 -04:00
Virgil Dupras
d90764a9ea Removed a needless cocoa build step.
Previously, dependencies were collected in "build/py" and then copied into OS X app's Resources folder. Now, dependencies are collected direcly in the Resources folder.
2012-08-02 15:23:17 -04:00
Virgil Dupras
c5c4e02bf4 Improved cocoa build process.
Now, lproj folders are generated directly in the target app folder, meaning that a "build.py --loc" call will actually change the localization without a full rebuild.
2012-08-02 12:49:49 -04:00
Virgil Dupras
fae3a6ac3a Improved french loc. 2012-08-02 12:47:02 -04:00
Virgil Dupras
09b91aab66 Updated docs to explain the difference between hardlinks and symlinks. 2012-08-02 11:36:31 -04:00
Virgil Dupras
b5a219cc00 Pushed build_cocoalib_xibless() to hscommon.build 2012-08-02 11:27:19 -04:00
Virgil Dupras
9cb62e0544 Updated locs. 2012-08-01 16:42:35 -04:00
Virgil Dupras
653668dd96 Auto-generate Cocoa strings file from code (at last!).
Also, removed TR() macro usage (didn't work with the genstrings command) and tweaked a couple of localized strings.
2012-08-01 16:34:12 -04:00
Virgil Dupras
5a5a74d0e1 [#194 state:fixed] Added the "Replace with symlink" deletion option. 2012-08-01 12:36:23 -04:00
Virgil Dupras
5247ac8abd Brought back multiple selection in the folder selection outline.
It mistakenly went away in the xibless conversion.
2012-08-01 11:54:05 -04:00
Virgil Dupras
f992599beb Install cocoa logger on startup so that logged messages show up in the Console under OS X 10.8. 2012-08-01 11:52:59 -04:00
Virgil Dupras
51f8c51ef3 Updated locs. 2012-07-31 16:47:10 -04:00
Virgil Dupras
fcdc692b61 [#189 state:fixed] Added "Export to CSV" feature. 2012-07-31 16:46:51 -04:00
Virgil Dupras
deb5260c6a Pushed the error message logic in Cocoa' addDirectory into the core. 2012-07-31 15:33:44 -04:00
Virgil Dupras
81df280ea6 Update locs. 2012-07-31 11:39:22 -04:00
Virgil Dupras
4f097a3a89 [#204 state:fixed] Added a message after re-prioritization telling how many dupe groups were changed by it. 2012-07-31 11:37:51 -04:00
Virgil Dupras
8cd1e13814 Updated locs. 2012-07-31 11:21:54 -04:00
Virgil Dupras
7e81e6c93f Fixed build script's --updatepot command which was broken. 2012-07-31 11:19:07 -04:00
Virgil Dupras
b19d6c9a27 [#198 state:fixed] Added Longest/Shortest filename criteria in the re-prioritize dialog. 2012-07-31 11:18:39 -04:00
Virgil Dupras
977fb606eb [#202 state:fixed] [#203 state:fixed] Made results table column read-only (they were mistakenly editable) and fixed the column ID edited by the Rename Selected action. 2012-07-31 10:42:39 -04:00
Virgil Dupras
9e7d27dcda Fixed a bug where "Reset to Defaults" in the Columns menu wouldn't refresh menu items' marked state. 2012-07-31 10:35:19 -04:00
Virgil Dupras
caf04f0d3f Re-organized the definition of prefs default values.
By doing so, I also fixed a bug where DebugMode and CustomCommand prefs weren't correctly reset.
2012-07-31 10:27:36 -04:00
Virgil Dupras
a2553da578 Merge default with xibless. 2012-07-31 10:04:37 -04:00
Virgil Dupras
3cd44705f8 Updated README and pip requirements to include xibless (and remove Sparkle, which is now included in cocoalib).
--HG--
branch : xibless
2012-07-30 14:14:10 -04:00
Virgil Dupras
0cf6987083 Set shortcut for the "Rename Selected" menu item in Cocoa's main menu.
--HG--
branch : xibless
2012-07-30 13:13:27 -04:00
Virgil Dupras
a67f7e2c9e Added a formatter to the slider value indicator in Cocoa's pref panel.
--HG--
branch : xibless
2012-07-29 17:42:47 -04:00
Virgil Dupras
5a3b6883fa Fixed bezel style of + and - buttons in the Folder Selection Dialog.
--HG--
branch : xibless
2012-07-29 16:57:27 -04:00
Virgil Dupras
b0f9a94375 Add the ME-specific "Remove Dead Tracks" menu item in the UI script instead of in the objc code.
--HG--
branch : xibless
2012-07-29 16:07:17 -04:00
Virgil Dupras
ad5c4a954c Fixed action menu arrow which was misplaced under os x 10.6.
--HG--
branch : xibless
2012-07-29 13:42:41 -04:00
Virgil Dupras
292d993dce Fixed cocoa packaging.
--HG--
branch : xibless
2012-07-29 12:59:39 -04:00
Virgil Dupras
1fe42f673f Fixed warning during the compilation of PE's cocoa UI unit.
--HG--
branch : xibless
rename : cocoa/base/DetailsPanel.h => cocoa/base/DetailsPanelBase.h
rename : cocoa/base/DetailsPanel.m => cocoa/base/DetailsPanelBase.m
rename : cocoa/pe/DetailsPanelPE.h => cocoa/pe/DetailsPanel.h
rename : cocoa/pe/DetailsPanelPE.m => cocoa/pe/DetailsPanel.m
2012-07-29 11:57:46 -04:00
Virgil Dupras
244af5b652 Instead of "manually" added a "Clear Picture Cache" menu item in the objc code, added an "edition" argument to main_menu UI script and conditionally add it there.
--HG--
branch : xibless
2012-07-29 11:28:39 -04:00
Virgil Dupras
abe9041a67 De-IBAction-ified actions in ResultsWindow and fixed a few mis-connections in the main menu.
--HG--
branch : xibless
2012-07-29 11:16:04 -04:00
Virgil Dupras
a2d73b216c Added missing (for PE) cocoalib units in waf script.
--HG--
branch : xibless
2012-07-29 11:01:28 -04:00
Virgil Dupras
f08b593acb Converted PE's preference panel to xibless and thus completed its transition to waf-based building.
--HG--
branch : xibless
2012-07-29 10:38:29 -04:00
Virgil Dupras
cb35dc7897 Converted ME's preference panel to xibless and thus completed its transition to waf-based building.
--HG--
branch : xibless
rename : cocoa/se/ui/preferences_panel.py => cocoa/base/ui/preferences_panel.py
2012-07-28 19:07:37 -04:00
Virgil Dupras
903ecd9eae Added build support for ME in the new waf script (but I haven't converted ME's pref panel to xibless yet).
--HG--
branch : xibless
2012-07-28 18:10:05 -04:00
Virgil Dupras
79e9251511 Generate cocoalib stuff in the same autogen folder as the rest instead of in its own autogen folder.
--HG--
branch : xibless
2012-07-28 16:49:36 -04:00
Virgil Dupras
81daef6145 Replaced dupeGuru XCode project with a WAF build script.
--HG--
branch : xibless
2012-07-28 16:22:51 -04:00
Virgil Dupras
6a7af81685 xibless-ified MainMenu.
--HG--
branch : xibless
rename : cocoa/base/AppDelegate.h => cocoa/base/AppDelegateBase.h
rename : cocoa/base/AppDelegate.m => cocoa/base/AppDelegateBase.m
2012-07-27 18:30:34 -04:00
Virgil Dupras
b3db7c6842 Updated cocoalib subrepo.
--HG--
branch : xibless
2012-07-27 15:32:12 -04:00
Virgil Dupras
85e5b4cfa7 xibless-ified SE's preferences panel.
--HG--
branch : xibless
2012-07-27 15:21:35 -04:00
Virgil Dupras
79e6020982 Tweaked the result window xibless file and removed the old results XIB.
--HG--
branch : xibless
2012-07-27 09:02:03 -04:00
Virgil Dupras
b7e7e67c99 Tweaked match table settings and bindings in results_window xibless UI.
--HG--
branch : xibless
2012-07-26 16:33:12 -04:00
Virgil Dupras
1017e3c730 Fixed dupeGuru building under virtualenv, which mistakenly didn't include distutils.sysconfig during the dependencies collection phase.
--HG--
branch : xibless
2012-07-26 12:48:47 -04:00
Virgil Dupras
b74e33f4b0 Began designing a xibless result window. The basics are there, but there are still many things missing.
--HG--
branch : xibless
2012-07-26 12:18:39 -04:00
Virgil Dupras
2d0facdb14 Fixed a crash on fresh repo building.
--HG--
branch : xibless
2012-07-26 11:24:44 -04:00
Virgil Dupras
c34004ed94 xibless-ified PrioritizeDialog.
--HG--
branch : xibless
2012-07-24 11:31:18 -04:00
Virgil Dupras
4db5fae38b xibless-ified DirectoryPanel.
--HG--
branch : xibless
2012-07-23 17:46:01 -04:00
Virgil Dupras
5d5670d4be xibless-ified ProblemDialog.
Also, fixed a bug where the problems table wasn't read-only.

--HG--
branch : xibless
2012-07-23 11:04:13 -04:00
Virgil Dupras
e21a7e18b4 I had forgot to remove the DeletionOptions XIB and to move localized strings in my last commit.
--HG--
branch : xibless
2012-07-23 10:22:21 -04:00
Virgil Dupras
a29ed235f6 xibless-ified DeletionOptions.
--HG--
branch : xibless
2012-07-23 10:14:50 -04:00
Virgil Dupras
fd706e752f xibless-ified IgnoreListDialog.
--HG--
branch : xibless
2012-07-22 12:46:43 -04:00
Virgil Dupras
729db49183 xibless-ified PE\s details panel.
--HG--
branch : xibless
rename : cocoa/pe/DetailsPanel.h => cocoa/pe/DetailsPanelPE.h
rename : cocoa/pe/DetailsPanel.m => cocoa/pe/DetailsPanelPE.m
2012-07-21 16:13:16 -04:00
Virgil Dupras
a68e4310ee xibless-ified base DetailsPanel. Note that for now, PE's detail panel is broken.
--HG--
branch : xibless
2012-07-21 10:39:01 -04:00
Virgil Dupras
500314859d Switched to cocoalib's xibless branch and adapted build script and cocoa projects to that branch.
--HG--
branch : xibless
2012-07-20 17:09:43 -04:00
Virgil Dupras
b6d457f908 Added brazilian Localizable.strings to cocoa projects, something I had forgot to do earlier. 2012-07-20 13:51:06 -04:00
Virgil Dupras
6850c7e2f8 Updated Brazilian loc. 2012-07-20 13:44:06 -04:00
Virgil Dupras
9e5630fe99 Fixed debian packaging which was broken and produced (nearly) empty binary packages. 2012-07-11 14:48:42 -07:00
Virgil Dupras
f1a21e62cd Updated hscommon subrepo. 2012-07-11 13:23:45 -07:00
Virgil Dupras
7cc2defa35 Improved debian packaging. 2012-07-11 13:21:40 -07:00
Virgil Dupras
24a11ee4bd Modernized debian packaging. 2012-07-11 12:07:29 -07:00
Virgil Dupras
e2b23ca961 dupeGuru PE now has a functional debian source package (the source package compiles extensions). 2012-07-11 09:22:00 -07:00
Virgil Dupras
8fabb14b8b Fixed dupeGuru PE architecture for debian packaging. It was mistakenly set to 'all'. 2012-07-11 08:29:31 -07:00
Virgil Dupras
07de7d6f0e Fix test that became flaky under Python v3.3. 2012-07-08 09:03:05 -04:00
Virgil Dupras
e0b844f617 Added tag me6.4.2 for changeset a618e954f01e 2012-07-07 17:07:26 -04:00
Virgil Dupras
a7bc76bf7c Small tweaks to the Brazilian loc. 2012-07-07 16:08:30 -04:00
Virgil Dupras
89fb531f3d me v6.4.2 2012-07-07 15:57:05 -04:00
Virgil Dupras
f22baa8d5a Fixed iTunes intergration which was broken since iTunes 10.6.3.
More info at http://www.leancrew.com/all-this/2012/06/the-first-nail-in-the-coffin-of-python-appscript/

I'm a bit late to the party. I hadn't realised that these crash reports were caused by iTunes 10.6.3... Oops.
2012-07-07 15:44:13 -04:00
Virgil Dupras
7c2e601a30 Brazilian loc tweaks by Victor Figueiredo. 2012-07-07 11:34:58 -04:00
Virgil Dupras
16e4a5fddd Brazilian localization by Victor Figueiredo. 2012-07-03 12:42:23 -04:00
Virgil Dupras
4200f2a090 Moved cocoa packaging logic that is common to all HS apps into hscommon.build. 2012-06-20 13:09:42 -04:00
Virgil Dupras
45c8291645 Fixed a few inconsistencies in debian packaging that were left from the /usr/local/share --> /usr/share move. 2012-06-16 07:57:52 -07:00
Virgil Dupras
89f8214bce Improved debian packaging by reducing metadata duplication in the project.
--HG--
rename : debian_se/compat => debian/compat
rename : debian_se/control => debian/control
rename : debian_se/copyright => debian/copyright
rename : debian_se/dirs => debian/dirs
rename : debian_se/dupeguru_se.desktop => debian/dupeguru.desktop
rename : debian_se/rules => debian/rules
2012-06-16 07:35:23 -07:00
Virgil Dupras
16e1ee93d0 Fixed ME cocoa project which had duplicate items in resource copying build phase. 2012-06-13 13:12:22 -04:00
Virgil Dupras
222ae73590 Fix bug where invalid xml in iTunes library would make dgme crash. 2012-06-13 13:11:48 -04:00
Virgil Dupras
21c0292154 Added tag pe2.6.0 for changeset c8a9a4d35592 2012-06-06 18:27:08 -04:00
Virgil Dupras
a7eb90894a Updated changelog. 2012-06-06 17:35:08 -04:00
Virgil Dupras
64baf2a10c PE's block module: Use sip.voidptr's ascapsule() instead of __int__() to retrieve its pointer.
It caused crashes under 32 bits when pointers would flip our long's most significant bit. (Well, at least that's what I think was going on).
2012-06-06 17:18:53 -04:00
Virgil Dupras
392a802ef1 Updated Ubuntu package requirements in README. For PE, python3-dev is required. 2012-06-06 15:44:21 -04:00
Virgil Dupras
8efeab7b40 In Cocoa interface units, added missing @dontwrap decorators where appropriate. 2012-06-06 15:09:38 -04:00
Virgil Dupras
8a86ecee38 pe v2.6.0 2012-06-06 15:02:19 -04:00
Virgil Dupras
3e79b57409 Updated loc. 2012-06-06 14:59:23 -04:00
Virgil Dupras
362e020585 Changed "Quicklook" in the main menu to "Quick Look" to follow Finder's label. 2012-06-06 14:45:28 -04:00
Virgil Dupras
df5c8ddf22 Appended "..." to "Send to Trash|Recycle bin" to indicate that a dialog is triggered by this action. 2012-06-06 14:40:23 -04:00
Virgil Dupras
70cc48d51f Updated help file to include iPhoto/Aperture/iTunes explanations. 2012-06-06 11:41:14 -04:00
Virgil Dupras
dccffd9516 [#42 state:fixed] Added Aperture support in dupeGuru PE. 2012-06-05 13:56:28 -04:00
Virgil Dupras
04056c1597 Added tag me6.4.1 for changeset e772f1de8674 2012-06-04 10:42:23 -04:00
Virgil Dupras
69b2e37368 me v6.4.1 2012-06-04 10:11:00 -04:00
Virgil Dupras
05478591a4 Fixed stupid bug with _do_delete() ME/PE overrides not having the right arguments. 2012-06-04 10:08:51 -04:00
Virgil Dupras
ead3b1e651 Added tag me6.4.0 for changeset 254bce83ad6e 2012-06-02 12:02:06 -04:00
Virgil Dupras
27f4c290c4 me v6.4.0 2012-06-02 11:23:58 -04:00
Virgil Dupras
4f248ee981 Fixed SE changelog to include Ubuntu upgrade notice. 2012-06-02 11:23:49 -04:00
Virgil Dupras
d7397c0125 Added tag se3.5.0 for changeset c3d9f91dc9c9 2012-06-01 12:22:29 -04:00
Virgil Dupras
e4430168f7 se v3.5.0 2012-06-01 10:56:48 -04:00
Virgil Dupras
78c2ae150d Disable hardlink feature on Windows. 2012-06-01 10:30:17 -04:00
Virgil Dupras
806e3917e3 Merged heads. 2012-06-01 10:24:00 -04:00
Virgil Dupras
6c62c2d563 Updated help. 2012-06-01 10:23:37 -04:00
Virgil Dupras
7a768bd0e6 [#182 state:fixed] Added keyboards binding to focus on the filter field. 2012-06-01 10:00:05 -04:00
Virgil Dupras
a6c01f6868 Updated debian packaging for Ubuntu 12.04. 2012-05-30 13:53:09 -07:00
Virgil Dupras
721591d3e3 Added a Deletion Options dialog that pops up when Send to Trash is triggered.
It offers hardlink and direct deletion options. This new feature supersedes the old "Send to Trash and Hardlink" menu item, which was removed.
2012-05-30 12:10:56 -04:00
Virgil Dupras
1171705921 Made core.fs.File slotted to save a lot of memory usage. 2012-05-29 17:39:54 -04:00
Virgil Dupras
65d2581f74 Fix ME and PE cocoa projects which mistakenly weren't changed 2 commits ago. 2012-05-29 17:31:58 -04:00
Virgil Dupras
2128d1787a Fixed core_me.scanner_test 2012-05-29 17:05:39 -04:00
Virgil Dupras
cbf2ab82ce Added Nickolas Pohilets to credits. 2012-05-28 14:54:30 -04:00
Virgil Dupras
adef5a9dfa Merge dev head with Nickolas Pohilets UK loc updates. 2012-05-28 14:51:13 -04:00
Virgil Dupras
0c9e5c90a3 Updated subrepos to updated UK loc by Nickolas Pohilets. 2012-05-28 14:49:07 -04:00
Virgil Dupras
787f546c17 Adapted to HSPyUtil/HSFairwareProtocol split in cocoalib. 2012-05-28 14:45:13 -04:00
Nickolas Pohilets
2d4ecf1122 merge 2012-05-21 23:56:16 +03:00
Nickolas Pohilets
1836003506 Switched subrepos to forked clones 2012-05-21 23:50:38 +03:00
Virgil Dupras
44d34f56f7 Added tag me6.3.1 for changeset 71b7e18613f3 2012-05-15 08:33:35 -04:00
Virgil Dupras
24c3d7ed00 Update cocoalib subrepo. 2012-05-15 08:03:49 -04:00
Virgil Dupras
8f7657573d me v6.3.1 2012-05-15 07:49:25 -04:00
Virgil Dupras
c03c7f2be1 Added tag se3.4.1 for changeset c153aef25e5c 2012-05-14 09:41:53 -04:00
Virgil Dupras
91a157cb2b Fixed build bug where the qtlib po file wouldn't be copied to the build locale folder. 2012-05-14 08:55:23 -04:00
Virgil Dupras
7ced1e7b9d Disabled broken signature option in se installer project under qt. 2012-05-14 08:45:24 -04:00
Nickolas Pohilets
f4b8efff50 Fixed Ukrainian interface localization 2012-05-14 15:31:05 +03:00
Virgil Dupras
4c0545189e se v3.4.1 2012-05-14 08:26:32 -04:00
Virgil Dupras
21af7b2fcd Fixed the "Folders" mode in SE which was broken since the conversion to objp. 2012-05-13 12:45:22 -04:00
Virgil Dupras
df9af9a796 Fixed crash on iTunes library parsing when the "Location" element is missing in track data dictionary. 2012-04-13 15:21:48 -04:00
Virgil Dupras
933474400c Sign OS X app at package time rather than build time. 2012-04-07 11:21:38 -04:00
Virgil Dupras
b2c8e779bd Added tag pe2.5.0 for changeset 93ed33410df2 2012-03-31 12:14:53 -04:00
Virgil Dupras
e570a308bb Fixed PE's Clear Picture Cache action under Qt. 2012-03-31 10:56:14 -04:00
Virgil Dupras
69c01a658b pe v2.5.0 2012-03-31 10:39:18 -04:00
Virgil Dupras
0741cbe17b Toggled code signing in Cocoa apps. 2012-03-31 10:35:46 -04:00
Virgil Dupras
ad4ccfe347 Added tag se3.4.0 for changeset 90318f130385 2012-03-29 11:32:09 -04:00
Virgil Dupras
57375f83f6 activated "todo" extension in help conf file. 2012-03-29 11:26:42 -04:00
Virgil Dupras
2d75ffefa7 se v3.4.0 2012-03-29 10:48:48 -04:00
Virgil Dupras
87aa181e4b Added tag me6.3.0 for changeset 236cf9b690a1 2012-03-27 12:38:34 -04:00
Virgil Dupras
1f359e0391 Updated qtlib subrepo. 2012-03-27 11:07:43 -04:00
Virgil Dupras
c8af8ccc61 me v 6.3.0 2012-03-27 10:03:37 -04:00
Virgil Dupras
2daaaee893 Work around a crash in picture analysis where there's sometimes a null path and improved debug logging calls in the matchblock unit. 2012-03-23 16:10:58 -04:00
Virgil Dupras
03712860b1 Added debug message on sending to trash. 2012-03-23 15:51:39 -04:00
Virgil Dupras
9e1f79c152 Added runtime adjustments to UI elements depending on active localization. 2012-03-20 15:43:47 -04:00
Virgil Dupras
edc385cfbc Updated french loc. 2012-03-20 15:42:43 -04:00
Virgil Dupras
0f62917b55 Fixed folder sorting under ME when the folder is the iTunes library. 2012-03-20 11:01:25 -04:00
Virgil Dupras
515c195780 Updated jobprogress requirements. 2012-03-20 10:16:02 -04:00
Virgil Dupras
c63df97dd6 Fixed prioritize dialog which was broken. 2012-03-19 16:52:08 -04:00
Virgil Dupras
1f312d8532 Fixed renaming under Qt which was broken since the addition of the 'marked' column. 2012-03-19 16:20:51 -04:00
Virgil Dupras
fa547bb95e Fixed details window geometry save/restore which sometimes caused the dialog to appear in inconvenient places (partially out of the screen). 2012-03-19 15:09:44 -04:00
Virgil Dupras
b500b34ef1 Updated qtlib subrepo. 2012-03-19 11:09:57 -04:00
Virgil Dupras
23cb71b522 Fixed results loading for iTunes songs. 2012-03-17 12:13:58 -04:00
Virgil Dupras
720d2fce83 Added Move and iPhoto/iTunes warning in help file. 2012-03-17 10:59:50 -04:00
Virgil Dupras
97f447fc27 Activated the ability to sort by 'marked' status under Cocoa. 2012-03-16 15:10:33 -04:00
Virgil Dupras
6ee56dc55c Under Qt, added a 'marked' column thus uncoupling the marked' attribute from the 'filename' column.
Also, added the ability to sort by 'marked' status.
2012-03-16 15:05:52 -04:00
Virgil Dupras
771f22f208 Removed code duplication among editions in sorting algorithms. 2012-03-16 14:57:21 -04:00
Virgil Dupras
80ff5f64f7 Updated qtlib subrepo. 2012-03-16 14:20:23 -04:00
Virgil Dupras
d0bfa2a6ca Converted dupeguru to the new automated QVariant conversion protocol introduced in PyQt (which is the default protocol when running under python 3). 2012-03-16 14:07:29 -04:00
Virgil Dupras
04a547656e Modernized qt.base.results_model by leveraging more of qtlib's table. 2012-03-16 13:50:17 -04:00
Virgil Dupras
657f6743c2 Changed copyright year to 2012 2012-03-15 14:28:40 -04:00
Virgil Dupras
42b57431e0 Added a close button the the qt ignore list dialog. 2012-03-15 14:23:38 -04:00
Virgil Dupras
3fc83d6245 Added an Ignore List dialog. 2012-03-14 12:47:21 -04:00
Virgil Dupras
ae16845477 Removed old automatically generated strings files in Cocoa.
They were kept around to make sure that the new localization system behaved correctly, and it does. We can remove them now.
2012-03-14 12:20:29 -04:00
Virgil Dupras
49a7043b4d Updated hscommon to its tip.
Because the latest changes in hscommon include the introduction of a base GUIObject which significantly changes view setting mechanisms, significant adjustments had to be made in dupeGuru.
2012-03-13 14:27:08 -04:00
Virgil Dupras
878c744c21 Refactoring: de-notified problems_dialog. It simplifies things. 2012-03-13 11:58:07 -04:00
Virgil Dupras
b4b9393e14 Updated loc. 2012-03-13 11:56:49 -04:00
Virgil Dupras
bf17eb715a Pushed some action confirmation logic down from GUI layers to the core. 2012-03-10 14:32:56 -05:00
Virgil Dupras
cd9f54163b Pushed some action confirmation logic down from GUI layers to the core. 2012-03-10 10:58:08 -05:00
Virgil Dupras
ef0a66f794 Pushed job_finished logic down from GUI layers to the core. 2012-03-09 13:47:28 -05:00
Virgil Dupras
5fb7742cf4 [#188 state:fixed] Instead of showing a crash report on iTunes communication problems, show a friendlier message. 2012-03-09 11:34:08 -05:00
Virgil Dupras
22de2d803a [#196 state:fixed] [#188] Fixed a crash on crash reporting under Cocoa and improved job management to allow threaded error handling on _job_finished(). 2012-03-05 14:09:42 -05:00
Virgil Dupras
188aa4bf2e Removed code duplication in qt.base.util.
createActions was also in qtlib.util (it had been moved for reuse in moneyGuru). Also, updated qtlib subrepo which wasn't correctly updated in previous commits.
2012-03-04 11:14:59 -05:00
Virgil Dupras
05552c160c Updated loc. 2012-03-04 11:08:34 -05:00
Virgil Dupras
dd70bd6d41 Improved Qt UI by adding a couple of widgets at the top of the results window.
Theres; now an Actions button, delta/dupes only checkboxes and a search field. Apply/Cancel filter menu items are gone.
2012-03-04 11:04:47 -05:00
Virgil Dupras
70f88ba39c Modernized signal handling/emitting in result_window and results_model. 2012-03-04 09:55:21 -05:00
Virgil Dupras
496f29b5c3 Fixed localization for ME/PE specific messages under Cocoa.
Despite having localizations for these message, dupeGuru would previously display the english message.
2012-02-27 10:12:36 -05:00
Virgil Dupras
b0818f2bdf Added russian localization to PE's cocoa proj.
For some reason, it wasn't there.
2012-02-27 10:07:12 -05:00
Virgil Dupras
e0efd660f6 Updated localizations.
There was the removal of the extra fairware reminder and the addition of iTunes-related messages.
2012-02-27 09:52:40 -05:00
Virgil Dupras
4592000464 Removed extra_fairware_reminder.
With the introduction of dual-mode fairware, the presence of the extra fairware reminder has been made rather useless.
2012-02-27 09:44:51 -05:00
Virgil Dupras
93781a0f35 [#195 state:fixed] Fixed bug where there would be a false reporting of discarded matches. 2012-02-26 11:18:29 -05:00
Virgil Dupras
7dfb42fb41 [#190 state:fixed] Fixed "Open with default application" which didn't work. 2012-02-26 10:36:13 -05:00
Virgil Dupras
a1fc64cd36 Ignore files in the scanning list that point to the same path as another file in the scanning list. 2012-02-21 11:14:12 -05:00
Virgil Dupras
b12b70b0a1 Added real iTunes support in dgme (similar to iPhoto support in dgpe). 2012-02-21 10:23:23 -05:00
Virgil Dupras
9457a43993 Added tag se3.3.3 for changeset fad463ae749b 2012-02-01 11:03:54 -05:00
Virgil Dupras
ab201bfae0 se v3.3.3 2012-02-01 09:56:05 -05:00
Virgil Dupras
66204ff0b5 Added the language confreplace in help build. 2012-02-01 09:53:24 -05:00
Virgil Dupras
95f77aef3f Added cocoalib locale in updatepot/mergepot build actions.
Also, updated subrepos to include error window restart notice change.
2012-02-01 09:45:33 -05:00
Virgil Dupras
1e96b6f9c4 Updated objp requirement to fix crash on getUTI(). 2012-02-01 09:18:21 -05:00
Virgil Dupras
f813673d6c Moved help changelog and conf tmpl files to the root of the help folder instead of having one copy per language.
--HG--
rename : help/en/changelog.tmpl => help/changelog.tmpl
rename : help/en/conf.tmpl => help/conf.tmpl
2012-01-24 08:28:23 -05:00
Virgil Dupras
1da325a56b Added tag pe2.4.1 for changeset a619f313712e 2012-01-23 15:48:00 -05:00
Virgil Dupras
d885bf0b18 Fixed tests. 2012-01-23 15:09:13 -05:00
Virgil Dupras
5f7e7fbb13 pe v2.4.1 2012-01-23 14:39:41 -05:00
Virgil Dupras
757f8569e6 Added "crash" terminology notice in changelog. 2012-01-23 14:38:15 -05:00
Virgil Dupras
bf86f56371 Added tag me6.2.1 for changeset 6a08c1205dfe 2012-01-20 12:31:33 -05:00
Virgil Dupras
7d0948cd23 me v6.2.1 2012-01-20 11:42:28 -05:00
Virgil Dupras
c4824b0f16 [#185 state:fixed] Made an iPhoto-appscript error a bit friendlier. 2012-01-20 11:34:46 -05:00
Virgil Dupras
ea32af04fb Fixed cocoa dev build. 2012-01-18 12:33:54 -05:00
Virgil Dupras
e519e41e42 Ukrainian help file by Yuri Petrashko. 2012-01-17 20:51:16 -05:00
Virgil Dupras
c937f2b20f Ukrainian localization by Yuri Petrashko. 2012-01-17 20:15:09 -05:00
Virgil Dupras
fc477ab3b5 Added tag se3.3.2 for changeset 2a96f2fb3ddb 2012-01-16 14:41:00 -05:00
Virgil Dupras
c53ddcdee1 Fixed crash under Cocoa when dupeGuru was launched from a path containing non-ascii characters. 2012-01-16 14:20:51 -05:00
Virgil Dupras
9e668b11b8 se v3.3.2 2012-01-16 13:09:20 -05:00
Virgil Dupras
f53a4c261c Updated requirements for Cocoa. 2012-01-16 13:02:40 -05:00
Virgil Dupras
9a2554d24e Merge with objp branch. 2012-01-16 12:52:50 -05:00
Virgil Dupras
b9ac135b89 Fixed a crash on sorting by dimensions (in PE) while being in dupesonly/delta mode. 2012-01-16 12:00:14 -05:00
Virgil Dupras
235d3ae521 Fixed a crash under Qt on Export to HTML. 2012-01-16 11:52:51 -05:00
Virgil Dupras
a487e83f3c Converted ME and PE cocoa projects to objp.
--HG--
branch : objp
rename : cocoa/se/main.m => cocoa/base/main.m
2012-01-16 10:30:45 -05:00
Virgil Dupras
4aaff8ffb1 Adapted qt layer to recent changes to the core (core initialization and core connection).
--HG--
branch : objp
2012-01-16 09:29:57 -05:00
Virgil Dupras
302050b2d6 Delegate model instantiation to HSGUIController where possible.
--HG--
branch : objp
2012-01-15 18:07:32 -05:00
Virgil Dupras
1b571f6fd2 Create the extra fairware reminder on-the-fly rather than on launch time.
--HG--
branch : objp
2012-01-15 17:31:15 -05:00
Virgil Dupras
baa2177439 Fixed a bug where re-prioritization criteria list would initially be empty.
That was because the dialog was created on launch time rather than on-the-fly.

--HG--
branch : objp
2012-01-15 17:24:30 -05:00
Virgil Dupras
29796e87b7 Put all cocoa callback views in the same extension module.
--HG--
branch : objp
2012-01-15 11:49:04 -05:00
Virgil Dupras
017e483b5a Re-added error handling.
--HG--
branch : objp
2012-01-15 11:00:34 -05:00
Virgil Dupras
7a167208d0 Allow cocoa dev builds again.
--HG--
branch : objp
2012-01-13 17:03:00 -05:00
Virgil Dupras
475f2c7238 Fixed PyDupeGuru which didn't have its callback set.
--HG--
branch : objp
2012-01-13 17:02:41 -05:00
Virgil Dupras
9392f818cc Pushed connect() calls in objc into the core.
--HG--
branch : objp
2012-01-13 16:34:21 -05:00
Virgil Dupras
58347bc36f Removed view arguments from core.gui classes's init methods.
--HG--
branch : objp
2012-01-13 16:14:06 -05:00
Virgil Dupras
55db21f3e0 Removed temporary objp conversion hacks.
--HG--
branch : objp
2012-01-13 15:25:34 -05:00
Virgil Dupras
950cd0c341 Completed the conversion wo objp with the conversion of PyDupeGuru. I had to temporarily disable error handling though.
--HG--
branch : objp
2012-01-13 14:43:43 -05:00
Virgil Dupras
937ea73c87 Converted ProblemDialog to objp.
--HG--
branch : objp
2012-01-13 12:19:23 -05:00
Virgil Dupras
7bce70c128 Converted ResultsTable to objp.
--HG--
branch : objp
2012-01-13 11:49:34 -05:00
Virgil Dupras
f9ced08e6d Fixed the mess I had to introduce earlier to make selectable list work by using objp's new inheritance capabilities.
--HG--
branch : objp
2012-01-13 10:20:46 -05:00
Virgil Dupras
4c8ce4b52d Converted the prioritize dialog to objp. A bit hackish though. Refactorings required before going further.
--HG--
branch : objp
2012-01-12 17:40:23 -05:00
Virgil Dupras
0d78201548 Comverted DirectoryOutline to objp. I converted HSColumns and I realized at the end that I didn't need to do it yet, but well, it will be done for ResultsTable.
--HG--
branch : objp
2012-01-12 15:19:40 -05:00
Virgil Dupras
45d4915d88 Converted objp code to 2-steps instantiation.
--HG--
branch : objp
2012-01-12 14:23:10 -05:00
Virgil Dupras
a699a2ef45 Converted .h view objp bridge interfaces to python ones. It simplifies them and allows for subclassing.
--HG--
branch : objp
2012-01-12 12:34:20 -05:00
Virgil Dupras
f6dd1a6a42 Pushed common py wrapper creation code to cocoalib.
--HG--
branch : objp
2012-01-12 10:23:31 -05:00
Virgil Dupras
e6819781f6 Converted details_panel to objp.
--HG--
branch : objp
2012-01-12 09:41:03 -05:00
Virgil Dupras
12467c9493 Converted extra_fairware_reminder to objp.
--HG--
branch : objp
2012-01-11 15:55:25 -05:00
Virgil Dupras
0c7d73854d Converted stats_label to objp.
--HG--
branch : objp
2012-01-11 15:14:59 -05:00
Virgil Dupras
a7eeb7db89 Directly embed python rather than going through a plugin.
--HG--
branch : objp
2012-01-11 12:18:03 -05:00
Virgil Dupras
989026051c Converted PyStatsLabel to a core instance mode.
--HG--
branch : objp
2012-01-09 11:15:20 -05:00
Virgil Dupras
56fd94e205 Added tag pe2.4.0 for changeset 8178bda48324 2012-01-06 16:49:03 -05:00
Virgil Dupras
c8e00cf0f7 Tweaked localized help files. 2012-01-06 15:40:21 -05:00
Virgil Dupras
0cca745d0d pe v2.4.0 2012-01-06 12:29:40 -05:00
Virgil Dupras
80043ccbea Added link to russian help document in english help index page. 2012-01-06 12:25:32 -05:00
Virgil Dupras
d62bfac95e Merge with objp branch. 2012-01-05 17:20:02 -05:00
Virgil Dupras
f636333938 Simplified the dev cocoa build process.
--HG--
branch : objp
2012-01-05 17:08:56 -05:00
Virgil Dupras
01f1e5e46e Moved hscommon.cocoa to cocoalib.
--HG--
branch : objp
2012-01-05 16:57:31 -05:00
Virgil Dupras
7ce72b1998 Adapted build script to changes in hscommon's CocoaProxy and objp.
--HG--
branch : objp
2012-01-05 14:12:52 -05:00
Virgil Dupras
c4f95a4901 Pushed LANGNAMES (and their localizations) from dupeGuru to qtlib. 2012-01-03 17:03:53 -05:00
Virgil Dupras
5b0d9f311c Added Russian localization for the help file by Igor Pavlov. 2012-01-03 15:35:17 -05:00
Virgil Dupras
11d8f824e9 Added Russian localization by Igor Pavlov. 2012-01-03 15:03:10 -05:00
Virgil Dupras
ceaf2ee4ba Fixed bogus imports.
--HG--
branch : objp
2011-12-28 15:04:42 -05:00
Virgil Dupras
3b80de869a Replaced objcmin with objp-based proxy.
--HG--
branch : objp
2011-12-28 14:51:33 -05:00
Virgil Dupras
08813ce39c Added Armenian help file translation by Hrant Ohanyan. 2011-12-19 16:02:47 -05:00
Virgil Dupras
478f462ecc Updated subrepos 2011-12-12 14:14:13 -05:00
Virgil Dupras
be53b6de76 Fixed Clear Picture Cache action under Qt which was broken. 2011-12-12 13:37:45 -05:00
Virgil Dupras
ca602480d9 Added Armenian loc by Hrant Ohanyan. Also, fixed the dgpe Cocoa project which was broken. 2011-12-08 11:06:25 -05:00
Virgil Dupras
185cdbb6fa Added tag me6.2.0 for changeset 015ba7e2c10d 2011-12-07 16:48:51 -05:00
Virgil Dupras
2d4903da26 Fixed the Qt-marked-column-deletion hack on ME, it was performed on SE columns instead. 2011-12-07 16:03:14 -05:00
Virgil Dupras
98954bd582 Added CFBundleShortVersionString to Cocoa's Info.plist. 2011-12-07 15:26:24 -05:00
Virgil Dupras
f862f32fb4 me v6.2.0 2011-12-07 15:12:38 -05:00
Virgil Dupras
804a5a1bbf Fixed ME project and columns under Cocoa. 2011-12-07 15:09:29 -05:00
Virgil Dupras
f004535820 Merge heads. 2011-12-07 12:06:54 -05:00
Virgil Dupras
2abd932709 Updated jobprogress requirement. 2011-12-07 12:05:22 -05:00
Virgil Dupras
2a78b8ce41 Added a startup warning message about wrong locale (causing surrogate-type-thing crashes on Linux). 2011-12-07 12:04:02 -05:00
Virgil Dupras
2301082307 Added tag se3.3.1 for changeset 6e3379be6821 2011-12-02 11:41:34 -05:00
Virgil Dupras
168546608d se v3.3.1 2011-12-02 10:59:44 -05:00
Virgil Dupras
27c1a03496 Updated qtlib. 2011-12-02 10:54:40 -05:00
Virgil Dupras
d382cec0fe Added an install destination in Cocoa projects because the built product location changed with XCode 4.2 and it became complicated, in build/package scripts, to locate it. 2011-12-02 10:48:59 -05:00
Virgil Dupras
c5b7f6b3d5 Added tag se3.3.0 for changeset 9ea9af1b886c 2011-11-30 16:16:30 -05:00
Virgil Dupras
725f9d51db se v3.3.0 2011-11-30 14:30:02 -05:00
Virgil Dupras
b4815d91c7 Fixed debian packaging and locale folder under linux. 2011-11-30 12:13:02 -05:00
Virgil Dupras
28e5924633 Fixed help paths under a windows frozen environment. 2011-11-30 16:36:29 +00:00
Virgil Dupras
66303a2076 Fixed help.locale path references under Qt and fixed packaging to include the locale folder. 2011-11-30 11:06:08 -05:00
Virgil Dupras
d3918724c0 Merge with fix for #184. 2011-11-29 10:43:13 -05:00
Virgil Dupras
fa294ea142 [#184 state:fixed] Fixed a crash in HSRecentFiles when it was fed with invalid data by the prefs. 2011-11-29 10:36:28 -05:00
Virgil Dupras
565c58b3a9 Updated demo limitations FAQ entry. 2011-11-29 09:41:57 -05:00
Virgil Dupras
d8970ca6b4 [#179] Fixed a crash under Cocoa when deleting many folders at once. 2011-11-28 15:42:13 -05:00
Virgil Dupras
1b7068bfe9 [#179 state:fixed] Added multiple-selection to the folder selection dialog and thus added the ability to remove multiple folders at once. 2011-11-28 15:25:18 -05:00
Virgil Dupras
756190cb8e [#179] Pushed the delete-or-exclude folder down from GUI layers to the core. 2011-11-28 14:52:48 -05:00
Virgil Dupras
3342b32882 [#179] Refactored folder deletion so that it uses selection in the core's directory_tree instead of using the one from the GUI layer. 2011-11-28 14:21:10 -05:00
Virgil Dupras
561b469e41 Oops, I didn't edit qt.me.result_model correctly. 2011-11-28 14:19:21 -05:00
Virgil Dupras
69fbda5d2c Added qt.(se|me|pe).result_model which I had forgot to add in previous commit. 2011-11-28 14:07:09 -05:00
Virgil Dupras
63180eaa5b Merge dev branch with fix for #181 2011-11-28 13:40:19 -05:00
Virgil Dupras
25faa458b9 [#181 state:fixed] Fixed a bug where performing actions like Mark All under a filtered environment would mark dupes even if they weren't in the filter. 2011-11-28 13:39:37 -05:00
Virgil Dupras
760e4085fa Merge dev branch with fix for #178 2011-11-28 13:16:22 -05:00
Virgil Dupras
fbe66d27c9 [#178 state:fixed] Fixed a crash on adding a criterion with an empty selection. 2011-11-28 13:14:36 -05:00
Virgil Dupras
08fd17f208 (Correction: The previous commit wasn't about --updatepot, but rather --loc) Updated Cocoa localization which were outdated because the build script didn't include edition-specific strings files. 2011-11-28 13:06:14 -05:00
Virgil Dupras
f8af6dbd18 Added edition-specific .strings in the build's --updatepot action. 2011-11-28 13:00:36 -05:00
Virgil Dupras
54d6fb080c Updated cocoa's ResultWindow localization which were out of sync with the XIB. 2011-11-28 11:02:31 -05:00
Virgil Dupras
8409a01bcc Removed columns.strings from Cocoa now that it became useless. 2011-11-28 11:00:16 -05:00
Virgil Dupras
1e136d2703 Updated localization. 2011-11-28 10:55:10 -05:00
Virgil Dupras
25afe54be3 Column-ized problem_table under Cocoa. 2011-11-28 10:45:11 -05:00
Virgil Dupras
d9ae967439 Updated the qt part of dupeguru to adjust to the latest column reorganization. 2011-11-28 10:27:17 -05:00
Virgil Dupras
9226a4fb7c Fixed unittests. 2011-11-27 16:49:12 -05:00
Virgil Dupras
fc5a0d914b Moved DELTA_COLUMNS from app to result_table. 2011-11-27 13:10:35 -05:00
Virgil Dupras
fca66d5108 Fixed HTML exporting. 2011-11-27 13:02:59 -05:00
Virgil Dupras
0571151c5f Fixed delta columns. 2011-11-27 12:54:58 -05:00
Virgil Dupras
7e95404903 Moved column information in new edition-specific core result_table units. 2011-11-27 12:47:00 -05:00
Virgil Dupras
eb83b830df Began a long overdue overhaul of the columns system. Cocoa-only so far, but it will affect the Qt part. 2011-11-26 10:55:14 -05:00
Virgil Dupras
0b1bf79796 Updated credits for localizations and added polib to the list of dependencies. 2011-11-25 15:10:18 -05:00
Virgil Dupras
6ab074decb Added localizations for Yes/No/OK in Dialogs and straightened up ui po files which had (near) duplicate problems. 2011-11-09 11:49:54 -05:00
Virgil Dupras
b7462f1d17 Fixed localization function in cocoa.inter.app 2011-11-09 11:02:15 -05:00
Virgil Dupras
a2a8397e78 Cocoalib's strings are now autogenerated from .po files suring the build_localizations() phase. 2011-11-04 15:17:14 -04:00
Virgil Dupras
5c0d9411e5 Added a few missing italian translations and updated a couple of oudated french strings files. 2011-11-04 15:16:38 -04:00
Virgil Dupras
cd2afeb32b Fixed a mix up in the message translations. 2011-11-04 15:11:09 -04:00
Virgil Dupras
b2fd022d07 Converted missing strings from Localizable.strings to ui.po. 2011-11-04 15:05:13 -04:00
Virgil Dupras
878046b579 Removing english locale. Now that the project has been de-message-ified, it became useless. 2011-11-04 14:38:13 -04:00
Virgil Dupras
428a400848 De-message-ified the rest of the project. 2011-11-04 14:37:07 -04:00
Virgil Dupras
8aa5826080 De-message-ified all messages from Cocoa's message.strings files. 2011-11-04 13:10:11 -04:00
Virgil Dupras
6b5d1e9894 Tweaked italian loc and added italian support in Cocoa and Qt layers. 2011-11-04 12:07:45 -04:00
Virgil Dupras
07a6a37502 Added cocoa localization by Paolo Rossi and converted it to .po. 2011-11-04 11:42:43 -04:00
Virgil Dupras
dd0af2fe15 I had forgotten to add translatable strings from cocoa/inter into the .pot files. 2011-11-04 11:23:17 -04:00
Virgil Dupras
df6d7141f1 De-message-ified hscommon and updated pot/po files. 2011-11-03 11:12:29 -04:00
Virgil Dupras
81d4cdde33 Added Czech support on the Qt side. 2011-11-03 10:38:31 -04:00
Virgil Dupras
88a613268d Removed .ts localizations, they aren't needed anymore.
--HG--
rename : qt/lang/qt_zh.qm => qt/lang/qt_zh_CN.qm
2011-11-03 10:25:15 -04:00
Virgil Dupras
b7aa4a1ad8 Removed core.strings from cocoa projects. They're not needed anymore since the core takes care of its own translations now. 2011-11-03 10:03:41 -04:00
Virgil Dupras
6c75d3afdf Added support got gettext localizations under Cocoa. 2011-11-02 17:31:57 -04:00
Virgil Dupras
771f26ba0f Generate lproj folders from .po files. 2011-11-02 16:47:56 -04:00
Virgil Dupras
30676fd20b Migrated XIB strings translations to .pot and .po files. 2011-11-02 15:55:20 -04:00
Virgil Dupras
607433d918 Added gettext support for czech. 2011-11-01 17:49:25 -04:00
Virgil Dupras
29db39f144 Added gettext support for chinese. 2011-11-01 17:40:57 -04:00
Virgil Dupras
49e49d5e1a Added gettext support for german. 2011-11-01 17:28:44 -04:00
Virgil Dupras
b9b84c9b7d Added gettext support for english (for messages). 2011-11-01 16:01:34 -04:00
Virgil Dupras
d80a56db78 Started moving towards a gettext-based localization. 2011-11-01 15:44:18 -04:00
Virgil Dupras
036026d64a Sphinx 1.1 has been released, we can finally remove the notes about Sphinx and python 2.7. 2011-11-01 10:04:34 -04:00
Virgil Dupras
13ef2fae90 Finished integrating Czech localization under Cocoa. 2011-11-01 10:00:30 -04:00
Virgil Dupras
54731e4ba0 Integrated Cocoa Czech localization by Aleš Nehyba. 2011-10-31 11:23:23 -04:00
Virgil Dupras
aa341bc5ed Debian packaging: /usr/local/share --> /usr/share 2011-10-24 13:11:40 -04:00
Virgil Dupras
4426f924e2 Added tag pe2.3.0 for changeset 4e6cbef6bcdf 2011-10-04 10:29:06 -04:00
Virgil Dupras
a6deb04049 Fixed a PE build problem under linux where extensions would get complicated extensions, not found by the old move() scheme. 2011-10-04 09:45:55 -04:00
Virgil Dupras
806d6be36c pe v2.3.0 2011-10-04 09:17:08 -04:00
Virgil Dupras
152a8772da Added tag me6.1.1 for changeset 3dd08060135b 2011-10-03 11:27:36 -04:00
Virgil Dupras
5885ead5ab me v6.1.1 2011-10-03 09:45:24 -04:00
Virgil Dupras
6fc5ce4bad Added tag se3.2.1 for changeset 0fd77be57ff7 2011-10-02 12:18:20 -04:00
Virgil Dupras
a4ae503bd4 Fixed changelog typo. 2011-10-02 10:58:08 -04:00
Virgil Dupras
020746be10 se v3.2.1 2011-10-02 10:39:07 -04:00
Virgil Dupras
436a8e686d Fixed broken actions: reveal_path in problems dialog and copy/move under Qt. 2011-10-02 10:27:40 -04:00
Virgil Dupras
b6d66f6d0b Added tag me6.1.0 for changeset 5a5134a4fa9b 2011-09-29 15:38:42 -04:00
Virgil Dupras
5284decd67 Fixed open_url() under Qt. It didn't work under Linux. 2011-09-29 15:01:37 -04:00
Virgil Dupras
73d22de752 me v6.1.0 2011-09-29 14:39:59 -04:00
Virgil Dupras
26e496a051 Fixed failing tests on Windows. 2011-09-29 19:23:35 +01:00
Virgil Dupras
75f0ed14aa Added tag se3.2.0 for changeset a2f7b7302e17 2011-09-27 16:07:40 -04:00
Virgil Dupras
27cecb0dbc Change shebang line in run_qt template. 2011-09-27 15:24:55 -04:00
Virgil Dupras
094d6702ba Updated README and debian control files. 2011-09-27 13:50:52 -04:00
Virgil Dupras
f8750dd392 se3.2.0 2011-09-27 13:44:43 -04:00
Virgil Dupras
76b873a504 Adjusted the height of the pref dialog to look better under Linux. 2011-09-27 11:24:18 -04:00
Virgil Dupras
dd031ffa1d Updated qtlib subrepo. 2011-09-27 16:12:27 +01:00
Virgil Dupras
a0991745e2 Fixed syntax error in german trans (Qt). 2011-09-27 10:58:52 -04:00
Virgil Dupras
3553d1a458 Added demo limitations. 2011-09-26 14:55:03 -04:00
Virgil Dupras
1b855ad64b Adapted to recent fairware changes in hscommon. 2011-09-26 11:54:17 -04:00
Virgil Dupras
9a7a20472d Pushed some logic for fairware from gui layers to hscommon. 2011-09-24 16:21:20 -04:00
Virgil Dupras
9fac97c147 Don't allow dupes from ref folders to step down from their ref position during reprioritization. 2011-09-23 13:14:57 -04:00
Virgil Dupras
11aa2c147c [#82 state:fixed] Updated localizations with new strings. 2011-09-23 10:47:16 -04:00
Virgil Dupras
2c260742f6 [#82] Added table font size pref (Qt). 2011-09-23 10:29:25 -04:00
Virgil Dupras
b8ac192d2a [#82] Added table font size pref (Cocoa). 2011-09-23 09:14:16 -04:00
Virgil Dupras
ae21ff988a [#21 state:fixed] Added quicklook support (Cocoa). 2011-09-22 15:59:11 -04:00
Virgil Dupras
8102c89802 [#169 state:fixed] Ignore filename extension during filename sorting in results. 2011-09-22 12:29:56 -04:00
Virgil Dupras
48e2acf0a2 [#139 state:fixed] Make "mark selected" behave in a more standard way. 2011-09-22 12:12:43 -04:00
Virgil Dupras
01731a8277 Moved the 'markable' unit from hscommon to core. 2011-09-22 11:55:31 -04:00
Virgil Dupras
abe25d6967 Refactored the scan error message system which was too complicated, especially on cocoa. Replaced it with a simpler view.show_message(msg) call. 2011-09-22 10:35:17 -04:00
Virgil Dupras
669e4b390b [#173 state:fixed] Fixed debian desktop files which didn't have a proper format. 2011-09-22 09:48:20 -04:00
Virgil Dupras
1fafe04f19 Use the new hscommon.plat unit. 2011-09-22 09:32:09 -04:00
Virgil Dupras
43c4dcb267 Moved core*.app_cocoa to cocoa/inter.
--HG--
rename : core_me/app_cocoa.py => cocoa/inter/app_me.py
rename : core_pe/app_cocoa.py => cocoa/inter/app_pe.py
rename : core_se/app_cocoa.py => cocoa/inter/app_se.py
2011-09-21 16:02:13 -04:00
Virgil Dupras
b44e52689f Simplified model --> cocoa communication. 2011-09-21 15:24:26 -04:00
Virgil Dupras
f0441db88a Removed a little cross-platform code duplication in core_me and core_pe. 2011-09-21 14:04:41 -04:00
Virgil Dupras
0da1947902 Fixed broken dgme-cocoa and replaced JOB_* consts with JobType const class. 2011-09-21 13:55:26 -04:00
Virgil Dupras
3b4ea50119 (Qt) Check for fairware hours only once the app is launched. 2011-09-21 13:42:54 -04:00
Virgil Dupras
e21627bbde Got rid of some code duplication in Cocoa interface. 2011-09-21 10:35:34 -04:00
Virgil Dupras
70689ce057 Removed data modules and moved their functionalities to core_*.app. 2011-09-21 10:26:58 -04:00
Virgil Dupras
60462698ac Fixed unit tests. 2011-09-21 09:17:22 -04:00
Virgil Dupras
f2164924f7 Wrapped data modules around edition-specific core.app subclasses to prepare for data modules removal. 2011-09-20 18:40:27 -04:00
Virgil Dupras
f730f4f55a Decoupled app in core.app from apps in qt.app and core.app_cocoa. Instead of subclassing it, they now hold a reference to it while fulfilling the role of core.app's "view". 2011-09-20 15:06:29 -04:00
Virgil Dupras
841b249b67 Updated chinese trans. 2011-09-20 13:32:20 -04:00
Virgil Dupras
0f12103616 [#138 state:fixed] Pri-dialog: Fixed the Folder crit category which didn't work well and added a replacement string for empty kind crit. 2011-09-20 10:38:48 -04:00
Virgil Dupras
edac54c5e6 [#138] Added reprioritization help section. 2011-09-19 12:12:22 -04:00
Virgil Dupras
818bc908a0 [#138] Updated localizations so they include new strings from the pri-dialog. 2011-09-16 18:01:56 -04:00
Virgil Dupras
26e81a8cbf [#138] Added PE-specific prioritizations and adjusted PE cocoa proj to the pri dialog. 2011-09-16 12:44:20 -04:00
Virgil Dupras
664803c2ca [#138] Added ME-specific prioritizations and adjusted ME cocoa proj to the pri dialog. 2011-09-16 12:24:56 -04:00
Virgil Dupras
5a26f1c2ae [#138] Added drag & drop re-ordering capabilities to the pri-pist (Cocoa). 2011-09-16 11:08:24 -04:00
Virgil Dupras
880f0787ce [#138] Added label and ok/cancel buttons to pri-dialog (Cocoa). 2011-09-13 18:19:46 -04:00
Virgil Dupras
549e3e1f3b [#138] Began implementing the Cocoa interface of the prioritization dialog. 2011-09-13 16:31:25 -04:00
Virgil Dupras
cf606a494c [#138] Added a prompt label and a remove button to the reprioritization dialog (Qt). 2011-09-12 11:05:53 -04:00
Virgil Dupras
90f9493ccc [#138] Added the Filename reprioritization crit category. 2011-09-12 10:33:17 -04:00
Virgil Dupras
3ec2a3ef81 [#138] Added drag & drop criteria re-ordering capabilities to the re-prioritization panel. 2011-09-09 18:24:17 -04:00
Virgil Dupras
b65c9b8c9a [#138] Added a crude-but-functional reprioritization dialog in Qt. 2011-09-09 12:01:15 -04:00
Virgil Dupras
2dc588e0fd [#138] Added FolderCategory and SizeCategory. 2011-09-08 13:28:19 -04:00
Virgil Dupras
9c30486f14 [#138] Completed re-prioritization workflow, now all that is needed is to add more criteria, handle corner case and build the GUI for it. 2011-09-08 12:36:57 -04:00
Virgil Dupras
518228a368 [#138] Added a very preliminary version of the prioritization dialog. A big part of this commit is about refactoring the tests and introducing a TestApp for dupeGuru (in core.tests.base). 2011-09-07 15:46:41 -04:00
Virgil Dupras
ff228035a3 Fixed app_test which was broken. 2011-09-07 09:58:46 -04:00
Virgil Dupras
eeb7f84601 Changed the code for Chinese from zh to zh_CN to accomodate a possible zh_TW. 2011-09-06 15:04:06 -04:00
Virgil Dupras
ee24234156 Converted Qt chinese loc to Cocoa. 2011-09-06 14:44:05 -04:00
Virgil Dupras
d462fd44c4 Tweaked chinese loc (by Eric Dee). 2011-09-06 13:15:16 -04:00
Virgil Dupras
22fedc4ee4 Fixed all xcode4 build warnings. 2011-09-06 10:03:14 -04:00
Virgil Dupras
548bd84a4b Modernized xibs to 10.6. 2011-09-06 09:55:58 -04:00
Virgil Dupras
d66afca753 Dropped os x 10.5 (and xcode3) support.
--HG--
rename : cocoa/me/dupeguru_xcode4.xcodeproj/project.pbxproj => cocoa/me/dupeguru.xcodeproj/project.pbxproj
rename : cocoa/me/dupeguru_xcode4.xcodeproj/project.xcworkspace/contents.xcworkspacedata => cocoa/me/dupeguru.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename : cocoa/pe/dupeguru_xcode4.xcodeproj/project.pbxproj => cocoa/pe/dupeguru.xcodeproj/project.pbxproj
rename : cocoa/pe/dupeguru_xcode4.xcodeproj/project.xcworkspace/contents.xcworkspacedata => cocoa/pe/dupeguru.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename : cocoa/se/dupeguru_xcode4.xcodeproj/project.pbxproj => cocoa/se/dupeguru.xcodeproj/project.pbxproj
rename : cocoa/se/dupeguru_xcode4.xcodeproj/project.xcworkspace/contents.xcworkspacedata => cocoa/se/dupeguru.xcodeproj/project.xcworkspace/contents.xcworkspacedata
rename : cocoa/se/dupeguru_xcode4.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings => cocoa/se/dupeguru.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
2011-09-06 09:28:04 -04:00
Virgil Dupras
7222ec3f02 Added tag before-leopard-drop for changeset 383b14d6e855 2011-09-06 08:45:09 -04:00
Virgil Dupras
f2d77bb60b Updated hscommon. 2011-09-03 13:49:06 -04:00
Virgil Dupras
4b9cba4d7f Removed some code duplication. 2011-09-03 13:28:36 -04:00
Virgil Dupras
47c9d39150 Added loc entry for "Chinese (Simplified)". 2011-09-03 10:30:31 -04:00
Virgil Dupras
14aba2b507 Added Eric Dee to credits for the chinese localization. 2011-09-03 10:25:17 -04:00
Virgil Dupras
bb2faa27f9 Added Chinese (Simplified) localization for Qt. 2011-09-03 10:19:17 -04:00
Virgil Dupras
e10e9a6976 Fixed a double tr() call. It caused a crash when in Chinese mode. 2011-09-03 10:15:03 -04:00
Virgil Dupras
73ba4954c1 Today's not my day. 2011-08-27 16:54:44 -04:00
Virgil Dupras
2c8c077b82 Well, congrats to me: that was a really stupid bug. Scans in PE were impossible. That's a quick fix. 2011-08-27 15:33:27 -04:00
Virgil Dupras
f7a3e78870 Merged heads. 2011-08-27 12:12:11 -07:00
Virgil Dupras
38638a90f1 Fixed se's xcode3 project which didn't have a ref tothe german FairwareReminder.xib. 2011-08-27 12:11:42 -07:00
Virgil Dupras
11cff312f5 Added tag pe2.2.2 for changeset 925847384dce 2011-08-27 13:41:31 -04:00
Virgil Dupras
17656d8e7c pe v2.2.2 2011-08-27 12:56:51 -04:00
Virgil Dupras
42cb788d35 Added tag me6.0.2 for changeset 28ba95706dc5 2011-08-26 18:39:20 -04:00
Virgil Dupras
327fe0b660 Fixed cocoalib xib refs in me/pe's xcode3 project. 2011-08-26 13:54:51 -07:00
Virgil Dupras
2cad94941b se v6.0.2 2011-08-26 16:45:09 -04:00
Virgil Dupras
d93b5d65b9 Added missing german core and Localizable references in xcode3 project. 2011-08-25 12:21:03 -04:00
Virgil Dupras
9bf9dd330a Added missing german "core" and "Localizable" references in xcode4 project. 2011-08-25 12:14:32 -04:00
Virgil Dupras
4449831ace There's a bug in ResultWindow.xib and it makes it impossible to edit *or* localize it with XCode4. It fear that I'll have to entirely rebuild the file from scratch, but I'll only do it after I've dropped 10.5/XCode3 support. So, for now, to make it possible to compile dupeGuru with XCode4, I restore pre-localized ResultWindow.xib in fr.lproj and de.lproj. 2011-08-25 12:03:38 -04:00
Virgil Dupras
303cf52d6a Added tag se3.1.2 for changeset e62183e907d6 2011-08-25 10:45:49 -04:00
Virgil Dupras
21b1a63687 [#166 state:fixed] Added libqtgui4 to deps in debian packaging. 2011-08-25 10:08:13 -04:00
Virgil Dupras
b1dce31542 se v3.1.2 2011-08-25 09:45:49 -04:00
Virgil Dupras
a64ddcb804 [#172 state:fixed] Fixed the Folders scan type which didn't work at all. 2011-08-25 09:37:30 -04:00
Virgil Dupras
905c194cdd Updated qt/lang/de.qm 2011-08-25 09:36:06 -04:00
Virgil Dupras
e61c698b03 Added tag se3.1.1 for changeset db1f325c907f 2011-08-24 13:17:05 -04:00
Virgil Dupras
77f70d120a Fixed compilation error under Leopard. 2011-08-24 09:28:13 -07:00
Virgil Dupras
3df35be0cf se v3.1.1 2011-08-24 12:05:31 -04:00
Virgil Dupras
04938bb573 Removed XIB files in de.lproj and fr.lproj, as they're just by products of other files. 2011-08-24 11:58:28 -04:00
Virgil Dupras
df57d993f6 Updated credits (to add gregor for the german loc). 2011-08-22 16:30:54 -04:00
Virgil Dupras
b74984f3b6 Added german help translation. By Gregor Taetzner (tweaked a bit by me). 2011-08-22 16:10:47 -04:00
Virgil Dupras
0bbdeb0846 Tweaked german loc. By Gregor Taetzner. 2011-08-22 15:54:45 -04:00
Virgil Dupras
5441da4630 Added Xcode 4 support.
--HG--
rename : cocoa/me/dupeguru.xcodeproj/project.pbxproj => cocoa/me/dupeguru_xcode4.xcodeproj/project.pbxproj
rename : cocoa/pe/dupeguru.xcodeproj/project.pbxproj => cocoa/pe/dupeguru_xcode4.xcodeproj/project.pbxproj
2011-07-21 10:11:51 -04:00
Virgil Dupras
366a55b27d Converted german translation from Qt to Cocoa. 2011-07-13 12:20:51 -04:00
Virgil Dupras
34a1b5d9b5 Added german localization (by Gregor Taetzner) [Qt]. 2011-07-13 10:21:36 -04:00
Virgil Dupras
2ba3584b7e [#165 state:fixed] Don't collapse tree nodes on changing folder states. [Qt] 2011-07-12 12:51:11 -04:00
Virgil Dupras
99e3c34060 [#168 state:fixed] Made the file collection phase cancellable. 2011-07-11 14:18:55 -04:00
Virgil Dupras
577cee1a38 Added pip requirement files. 2011-07-11 14:00:03 -04:00
Virgil Dupras
734b790581 Added tag pe2.2.1 for changeset ecf9aaa56834 2011-06-15 14:54:21 -04:00
Virgil Dupras
526bcf2566 pe v2.2.1 2011-06-15 14:20:34 -04:00
Virgil Dupras
56207f4dbb [#161 state:fixed] Fixed folder sorting. 2011-06-15 11:58:33 -04:00
Virgil Dupras
cd9fd3a10b [#162 state:fixed] Apply the 'Match scaled pictures' option to exif timestamp scan type. 2011-06-15 10:13:03 -04:00
Virgil Dupras
4399fe9d17 Fixed a bug where corrupt exif tags would make the analysis process stall. 2011-06-14 15:05:33 -04:00
Virgil Dupras
2a6f524a5b [#159 state:fixed] Added 'orf' to the list of supported extensions under cocoa (they work fine, I tried em). 2011-06-14 13:54:50 -04:00
Virgil Dupras
caee5e37f0 [#164 state:fixed] When I added the scan type combobox in pe's pref panel under cocoa, I forgot the little label next to the hardness slider, so it ended up under the newly added combobox. fixed it. 2011-06-14 13:44:59 -04:00
Virgil Dupras
bbd9d68dfd Added a --loc option to the build script for times when you only want to refresh localizations. 2011-06-14 13:43:29 -04:00
Virgil Dupras
16b1b00906 Added tag pe2.2.0 for changeset e44d5127ed60 2011-06-02 14:21:49 -04:00
Virgil Dupras
7183408535 Oops, fixed release date in changelog. 2011-06-02 10:53:02 -04:00
Virgil Dupras
591b4c7c6a pe v2.2.0 2011-06-02 10:32:48 -04:00
Virgil Dupras
8b1170a82b When an exif tag can't be read, log the event as 'info' rather than 'warning'. We don't want to fill the user's console with these messages, which will be very common. 2011-06-02 10:14:08 -04:00
Virgil Dupras
1f26fbeacc [#154 state:fixed] Added exif orientation support. 2011-05-31 10:05:12 -04:00
Virgil Dupras
cc7ccff48e [#154] Created the cross-platform unit core_pe.photo in prep for rotation support. 2011-05-29 10:18:03 -04:00
Virgil Dupras
a0809333c1 [#157 state:fixed] Straightened up extension glitches during result load/save under Qt 2011-05-29 09:12:24 -04:00
Virgil Dupras
8975f78a5f updated jobprogress version in README 2011-05-29 08:51:19 -04:00
Virgil Dupras
56bc1c1373 Added gotchas section to README 2011-05-28 16:44:23 -04:00
Virgil Dupras
417233a47f [#155 state:fixed] Added dg edition name in results window. 2011-04-22 11:37:53 +02:00
Virgil Dupras
59eaf5305a [#156 state:fixed] Fixed a visual glitch in Cocoa's result window colors when a row is selected. 2011-04-22 11:07:54 +02:00
Virgil Dupras
275c6be108 Added the EXIF Timestamp scan type in dgpe.
--HG--
rename : core_pe/matchbase.py => core_pe/matchblock.py
2011-04-21 17:17:19 +02:00
Virgil Dupras
a0e2b11663 Added core_pe.exif to read exif data from pictures 2011-04-20 15:19:12 +02:00
Virgil Dupras
de23ce90d8 Deduplicated scan type combobox creation code between SE and ME (soon to be shared by PE) (Qt). 2011-04-20 15:18:21 +02:00
Virgil Dupras
9d5f3029d0 Added tag se3.1.0 for changeset 97893f37d7d0 2011-04-16 15:48:50 +02:00
Virgil Dupras
33ee220933 Updated cocoalib subrepo. 2011-04-16 15:15:59 +02:00
Virgil Dupras
23d36b58c8 UI tweaks for linux. 2011-04-16 04:06:08 -07:00
Virgil Dupras
39b895f01b UI tweaks for windows. 2011-04-16 11:46:24 +01:00
Virgil Dupras
5f4252cddc Updated a couple of remaning copyright years (in about boxes) from 2010 to 2011. 2011-04-16 03:27:10 -07:00
Virgil Dupras
fc54a1ea39 se v3.1.0 2011-04-16 12:20:46 +02:00
Virgil Dupras
285f338dce Added an option to register the contributor's OS on openhs when registering an app. 2011-04-15 14:42:14 +02:00
Virgil Dupras
379e420577 Fixed exclusion bug during folders scan. 2011-04-14 15:37:12 +02:00
Virgil Dupras
0b20b35ffb Fixed copying operations for folders which didn't work. 2011-04-14 12:55:50 +02:00
Virgil Dupras
d887cd118c Update cocoalib subrepo. 2011-04-14 11:51:29 +02:00
Virgil Dupras
54ffcfab79 [#149 state:fixed] Fixed crash on result saving. 2011-04-13 16:59:02 +02:00
Virgil Dupras
f28ffc680a [#140 state:fixed] Fixed a crash on dupe renaming. 2011-04-13 16:23:22 +02:00
Virgil Dupras
f33f30eabf Merged heads 2011-04-12 13:29:27 +02:00
Virgil Dupras
279d44b7f3 [#89 state:fixed] Added a Folders scan type in dgse.
--HG--
rename : core_se/tests/fs_test.py => core/tests/fs_test.py
2011-04-12 13:22:29 +02:00
Virgil Dupras
0fea59007c Updated copyright year to 2011. 2011-04-12 10:04:01 +02:00
Virgil Dupras
54720b15ca Added tkinter to cx_freeze explicit excludes because v4.2.3 started to falsely include it. 2011-03-20 11:28:53 +00:00
Virgil Dupras
39fc7a91d7 me v6.0.1 2011-03-18 09:11:11 +01:00
Virgil Dupras
7f9c322d48 Added tag se3.0.2 for changeset 77e169f75719 2011-03-16 15:30:07 +01:00
Virgil Dupras
b2ff02c773 Added missing entry in 3.0.2 changelog. 2011-03-16 14:38:17 +01:00
Virgil Dupras
2c242aedfd se v3.0.2 2011-03-16 14:36:46 +01:00
Virgil Dupras
70e4e6f5af Depend specifically on python 3.1 in deb packages instead of depending on python3 because PyQt modules are not binary compatiable with python 3.2 2011-03-16 04:07:22 -07:00
Virgil Dupras
ebeb068042 Removed an old workaround in the Qt version that doesn't seem to be needed with the current version of Qt. 2011-03-16 08:41:29 +00:00
Virgil Dupras
731e68f164 [#153 state:fixed] Fixed a refresh bug in directory panel. 2011-03-16 09:31:16 +01:00
Virgil Dupras
fa0c3aeb78 Added tag pe2.1.0 for changeset d274bcb98f2d 2011-03-07 15:59:13 +01:00
Virgil Dupras
5a36f15667 Fixed bug causing PE progress report to be (very) wrong during matching. 2011-03-07 11:55:37 +01:00
Virgil Dupras
b96fae74b6 pe v2.1.0 2011-03-07 11:33:40 +01:00
Virgil Dupras
404743a27f Merged heads. 2011-03-06 17:25:54 +01:00
Virgil Dupras
58c8fd0f09 Updated qtlib subrepo 2011-03-06 17:25:02 +01:00
Virgil Dupras
fb3d3a135d [#148 state:fixed] Fixed a crash on copy/move when windows drive letters were involved. 2011-03-06 17:21:42 +01:00
Virgil Dupras
96bddd1995 Adjusted extra fairware reminder according to platform. 2011-03-06 03:12:16 -08:00
Virgil Dupras
6e60ea6984 Added extra Fairware reminder. We'll see if that boosts dupeGuru contributions. 2011-03-05 13:03:23 +01:00
Virgil Dupras
e410f88926 Catch a random occurrence of an exception on multiprocessing.cpu_count() so that dupeGuru doesn't crash because of that. 2011-03-05 13:02:11 +01:00
Virgil Dupras
1b52feb8b8 Optimized the scanning process in PE. 2011-03-04 11:15:04 +01:00
Virgil Dupras
ec8e915830 Updated hscommon subrepo. 2011-02-22 15:59:34 +01:00
Virgil Dupras
0bdbbbdf16 Only import pluginbuilder when building the Cocoa version. 2011-02-22 10:04:54 +01:00
Virgil Dupras
92e0647f19 Use pluginbuilder instead of py2app. 2011-02-21 11:57:37 +01:00
Virgil Dupras
69498147a2 Added tag before-pluginbuilder for changeset ff43c6d9feb3 2011-02-21 11:56:28 +01:00
Virgil Dupras
4249c528e9 [#144 state:fixed] Fixed crash on error handling under windows when sys.stderr is None. 2011-02-18 10:10:11 +00:00
Virgil Dupras
084068852e [#140 state:fixed] Fixed a crash on showing details panel in PE right after a mark-based removal (selection wasn't refreshed). 2011-02-18 10:37:40 +01:00
Virgil Dupras
c524a85897 Greatly improved iPhoto deletion process. 2011-02-17 15:08:23 +01:00
Virgil Dupras
d39d46be5a Added tag me6.0.0 for changeset 2fd901a516f8 2011-02-01 12:23:44 +01:00
Virgil Dupras
b8980b4667 Fixed a glitch with initial window position (they'd sometimes end up in awkward places on the screen). 2011-02-01 11:43:16 +01:00
Virgil Dupras
e0adec7b2b me v6.0.0 2011-02-01 10:03:56 +01:00
Virgil Dupras
eb8b9d663f Fixed a mistake in french translation. 2011-02-01 10:01:57 +01:00
Virgil Dupras
fa4b0cf9ec Added tag pe2.0.0 for changeset f1d40b556c01 2011-01-29 14:56:15 +01:00
Virgil Dupras
f72db8dd1d pe v2.0.0 2011-01-29 11:39:51 +01:00
Virgil Dupras
c5bf0f228a Changed error logging in core_pe.cache because it would sometimes result in huge logs of no value. Also, added debug logging during the analysis of pictures. 2011-01-29 11:31:17 +01:00
Virgil Dupras
e150b26cab Forgot a litteral --> constant conversion in the last commit. 2011-01-29 11:20:19 +01:00
Virgil Dupras
da41d07dae [#115 state:fixed] Re-factored the data columns (and delta columns) and made the Dimensions column a delta one. 2011-01-29 11:07:33 +01:00
Virgil Dupras
c885cb35d8 Added tag se3.0.1 for changeset 778876a8a978 2011-01-27 12:11:11 +01:00
Virgil Dupras
7c38217308 Fixed pref dialog UI for Linux. 2011-01-27 02:22:10 -08:00
Virgil Dupras
a88519b814 Fixed pref window UI on Windows. 2011-01-27 10:11:23 +00:00
Virgil Dupras
0aa91b170c se v3.0.1 2011-01-27 10:53:13 +01:00
Virgil Dupras
e9bb1c01f7 [#136 state:fixed] Add dropped folders to recent added folders list in the folder selection window. 2011-01-27 10:27:17 +01:00
Virgil Dupras
883875e88e [#135 state:fixed] Removed focus from cancel button in progress dialog. 2011-01-27 09:58:43 +01:00
Virgil Dupras
4cab6b0ad2 Added a 'More Info' button to the Qt Fairware pop up. 2011-01-26 17:15:15 +01:00
Virgil Dupras
91a2664830 Internationalized (and localized to french) column names under Qt, which I had forgot to do. 2011-01-26 13:06:54 +01:00
Virgil Dupras
6abbeaf987 [#132 state:fixed] Added a debug mode preference as well as extra debug loggings. 2011-01-26 12:50:44 +01:00
Virgil Dupras
21efef42f7 [#134 state:fixed] Removing all dupes from the results sets it in 'not modified' state. 2011-01-26 11:49:30 +01:00
Virgil Dupras
9d0e8d94ca Fixed dummy DupeGuru app so it implements get/set defaults methods (their lack made all tests fail). 2011-01-26 11:48:48 +01:00
Virgil Dupras
0f57ca698c [#133 state:fixed] Restored the context menu in the results window. 2011-01-26 11:34:47 +01:00
Virgil Dupras
69c572e875 Added tag se3.0.0 for changeset 8d12cab3b12b 2011-01-24 17:22:17 +01:00
Virgil Dupras
4083b60ff3 se v3.0.0 2011-01-24 15:40:01 +01:00
Virgil Dupras
41fbeb7ec9 Fixed changelog formatting (again). 2011-01-24 14:54:29 +01:00
Virgil Dupras
1162357b9b Fixed changelog formatting. 2011-01-24 14:48:45 +01:00
Virgil Dupras
a2a526866f Don't show bundles as subfolders in Folder Selection dialog. 2011-01-24 12:35:07 +01:00
Virgil Dupras
d3338b699e Added a short timeout to fairware unpaid hours fetching to avoid delaying the app's launch too much. 2011-01-24 11:40:41 +01:00
Virgil Dupras
6c60e76b55 Localized Fairware dialogs to french and made a few fixes here and there. 2011-01-24 11:30:45 +01:00
Virgil Dupras
8a0d31f612 Fixed linux-specific crashes and glitches. 2011-01-23 07:09:47 -08:00
Virgil Dupras
6fc7e5ace1 Fixed windows-specific crash in pref panel under Qt. 2011-01-23 14:49:59 +00:00
Virgil Dupras
8175762e74 Fixed auto update checks in Cocoa which were broken. 2011-01-23 12:47:21 +01:00
Virgil Dupras
f48e14af8a Removed appname duplication between dg_cocoa and core_*.__init__. 2011-01-23 12:20:44 +01:00
Virgil Dupras
cd2a61d926 Removed py2app import workarounds, they're not needed anymore. 2011-01-23 12:12:28 +01:00
Virgil Dupras
f45997afe4 Added prompt in folders dialog under Qt. 2011-01-23 11:24:33 +01:00
Virgil Dupras
b6f56721cb Changed sphinxgen's mechanism so that we don't have to copy the whole sphinx source dir every time we generate help.
--HG--
rename : help/en/changelog.rst => help/en/changelog.tmpl
rename : help/en/conf.py => help/en/conf.tmpl
rename : help/fr/changelog.rst => help/fr/changelog.tmpl
rename : help/fr/conf.py => help/fr/conf.tmpl
2011-01-22 17:06:04 +01:00
Virgil Dupras
f9e7e82772 Fixed a few bugs here and there. 2011-01-22 16:12:18 +01:00
Virgil Dupras
dbcd7b63d8 Tweaked FAQ in help file to use topic directives instead of section. This way it doesn't bug when using 'only' directives (and it kind of looks cute). 2011-01-22 13:17:45 +01:00
Virgil Dupras
bf807684dd [#32] Translated the help file in french. 2011-01-22 12:12:08 +01:00
Virgil Dupras
f02fcb5e4b Added a --only-help option to build.py 2011-01-21 14:39:33 +01:00
Virgil Dupras
2c127adf59 [#32] Internationalized the qt layer and localized it to French.
In the process of doing so, I also added a new preferences_dialog base class to reduce code duplication in the three pref dialogs (I didn't want to copy/paste the language combobox addition three times).
2011-01-21 13:57:54 +01:00
Virgil Dupras
7f8a357019 (Cocoa) Converted .strings files from UTF-16 to UTF-8. 2011-01-21 13:49:39 +01:00
Virgil Dupras
99daf5b7b7 Added core translation to qt. 2011-01-19 09:47:00 +01:00
Virgil Dupras
42cff20710 [#32] Internationalized the core and localized it to french. 2011-01-18 17:33:33 +01:00
Virgil Dupras
04d7880a0c [#32] Internationalized the cocoa layer and localized it to french.
--HG--
rename : cocoa/base/xib/DetailsPanel.xib => cocoa/base/en.lproj/DetailsPanel.xib
rename : cocoa/base/xib/DirectoryPanel.xib => cocoa/base/en.lproj/DirectoryPanel.xib
rename : cocoa/base/xib/MainMenu.xib => cocoa/base/en.lproj/MainMenu.xib
rename : cocoa/base/xib/ProblemDialog.xib => cocoa/base/en.lproj/ProblemDialog.xib
rename : cocoa/base/xib/ResultWindow.xib => cocoa/base/en.lproj/ResultWindow.xib
rename : cocoa/me/xib/Preferences.xib => cocoa/me/en.lproj/Preferences.xib
rename : cocoa/pe/xib/DetailsPanel.xib => cocoa/pe/en.lproj/DetailsPanel.xib
rename : cocoa/pe/xib/Preferences.xib => cocoa/pe/en.lproj/Preferences.xib
rename : cocoa/se/xib/Preferences.xib => cocoa/se/en.lproj/Preferences.xib
2011-01-18 15:35:14 +01:00
Virgil Dupras
e7d26e3f82 Replaced 'Add' and 'Remove' by + and - icons in the directories dialog under Qt. 2011-01-18 11:07:56 +01:00
Virgil Dupras
19308bf686 Made a few wording fixes in the doc and in Qt. 2011-01-18 10:45:40 +01:00
Virgil Dupras
92970489c5 Straightened out actions pictures and keybindings, added a Recent Folders menu and a link to Recent Results menu in a dialog button. 2011-01-17 17:15:16 +01:00
Virgil Dupras
d51f5184d7 The directories dialog is now the main window. There's probably many glitches left to fix due to that change, but the basic functionalities are there.
--HG--
rename : qt/base/main_window.py => qt/base/result_window.py
rename : qt/pe/main_window.py => qt/pe/result_window.py
2011-01-15 16:29:35 +01:00
Virgil Dupras
30eb26af7d Fixed ambiguities in Directories/Folder vocabulary. 2011-01-15 14:14:30 +01:00
Virgil Dupras
3ea43f8213 Adapted help to recent UI changes.
--HG--
rename : help/en/directories.rst => help/en/folders.rst
2011-01-15 14:10:16 +01:00
Virgil Dupras
9833067ba7 Added a 'Load Results' button in the Directory window. 2011-01-15 12:08:10 +01:00
Virgil Dupras
ad3114c56b Cleaned ResultWindow's code up. 2011-01-15 11:38:59 +01:00
Virgil Dupras
9da9c269c1 Prettyfied the directories panel. 2011-01-14 15:51:19 +01:00
Virgil Dupras
0a22bb8469 Made a few UI fixes. Mostly, it's about main menu item not pointing the appropriate target. 2011-01-14 15:34:10 +01:00
Virgil Dupras
c9fd1b1a17 Don't consider results as modified if they're empty. 2011-01-14 15:12:02 +01:00
Virgil Dupras
19b40d45c0 Brought DirectoryPanel in ME and PE up to speed with latest developments and fixed ResultsWindow.awakeFromNib (in ME and PE also). 2011-01-14 15:07:11 +01:00
Virgil Dupras
90e2a1cda0 The main window of dupeGuru is now the directories window (and its Done button is replaced by a Start Scanning button). 2011-01-14 14:41:43 +01:00
Virgil Dupras
5e47b9f4a7 Transfered ownership of pref panel from result window to app delegate. 2011-01-14 14:06:54 +01:00
Virgil Dupras
50b6948250 Extracted ResultWindow.xib from MainMenu.xib. 2011-01-14 13:56:50 +01:00
Virgil Dupras
3ef118c9fa Results are not automatically saved/load anymore. There's a reminder on quitting if you haven't saved your results. Also, for easier re-loading, there's a 'open recent results' menu item. 2011-01-13 16:20:03 +01:00
Virgil Dupras
064707db43 Merged heads 2011-01-13 13:53:28 +01:00
Virgil Dupras
8f71a1318d [#129] Replaced details, power marker and delta values button by one segmented control on OS X. 2011-01-13 13:51:00 +01:00
Virgil Dupras
1b8ab35fdd Fixed debian packaging. 2011-01-13 03:23:21 -08:00
Virgil Dupras
4a1fe2f8ab Fixed packaging versioning and help building for Qt. 2011-01-13 10:56:46 +00:00
Virgil Dupras
e6e4e14781 Centralized version information in core_* package so that they only live at one place (instead of several).
--HG--
rename : cocoa/me/Info.plist => cocoa/me/InfoTemplate.plist
rename : cocoa/pe/Info.plist => cocoa/pe/InfoTemplate.plist
rename : cocoa/se/Info.plist => cocoa/se/InfoTemplate.plist
2011-01-13 11:29:01 +01:00
Virgil Dupras
d139157234 [#130 state:fixed] Converted help file to Sphinx.
--HG--
rename : help_me/CHANGELOG => help/changelog_me
rename : help_pe/CHANGELOG => help/changelog_pe
rename : help_se/CHANGELOG => help/changelog_se
2011-01-12 17:30:57 +01:00
Virgil Dupras
94104f4e03 Removed duplication among help files of the different editions.
--HG--
rename : help_se/en/credits.md => help_base/en/credits.md
rename : help_se/en/directories.md => help_base/en/directories.md
rename : help_se/en/power_marker.md => help_base/en/power_marker.md
rename : help_se/en/quick_start.md => help_base/en/quick_start.md
rename : help_se/en/results.md => help_base/en/results.md
rename : help_se/skeleton/hardcoded.css => help_base/skeleton/hardcoded.css
rename : help_se/skeleton/images/hs_title.png => help_base/skeleton/images/hs_title.png
2011-01-11 17:58:28 +01:00
Virgil Dupras
8bea978715 Removed dependencies for yaml everywhere except for the documentation generation (it's going to be converted to sphinx).
--HG--
rename : help_me/changelog.yaml => help_me/CHANGELOG
rename : help_pe/changelog.yaml => help_pe/CHANGELOG
2011-01-11 16:21:36 +01:00
Virgil Dupras
eefe464fba Replaced dependencies from hsutil to hscommon. 2011-01-11 13:36:05 +01:00
Virgil Dupras
33c0ba808c Changed references to what has already been moved from hsutil to hscommon (io, path, testutil). 2011-01-11 11:59:53 +01:00
Virgil Dupras
e0cc8ecda2 Stop using hsutil.testcase. 2011-01-05 11:11:21 +01:00
Virgil Dupras
2d423b2358 Added test skipping if os.link() is not available. 2011-01-01 16:22:38 +00:00
Virgil Dupras
b5b27b141c Modernized core_pe tests and added skipping when the modules haven't been compiled (rather than a hard crash). 2011-01-01 17:17:27 +01:00
Virgil Dupras
800a879927 Added tag se2.12.3 for changeset 0056293b0dad 2011-01-01 13:32:54 +01:00
Virgil Dupras
f6806e42db se v2.12.3 2011-01-01 12:45:39 +01:00
Virgil Dupras
3aae21b810 Added tag pe1.11.3 for changeset 3f71a8f5bf8f 2010-12-31 15:51:52 +01:00
Virgil Dupras
75239d6a64 pe v1.11.3 2010-12-31 14:43:00 +01:00
Virgil Dupras
09082955a3 [#125 state:fixed] Wrapped error message around a crash when the iPhoto app can't be found. 2010-12-31 12:10:44 +01:00
Virgil Dupras
6a6f2d51aa Added tag me5.10.4 for changeset 44f6ff67066c 2010-12-30 16:11:17 +01:00
Virgil Dupras
7b0d3ea8ac me v5.10.4 2010-12-30 14:55:13 +01:00
Virgil Dupras
1c88b6bb26 Tweaked the wording of the fairware pop up. 2010-12-30 13:07:04 +01:00
Virgil Dupras
e5e8e5d908 Replaced the about box with one that supports fairware registering. 2010-12-30 13:00:44 +01:00
Virgil Dupras
92fadd26b7 [#120 state:fixed] Fixed dangling bogus results after cancelled scan. 2010-12-30 10:24:37 +01:00
Virgil Dupras
45d783ac43 Removed CallLogger-related code in app_test. This code was duplicating the code that was recently added to hscommon.testutil. 2010-12-30 10:00:29 +01:00
Virgil Dupras
ea9e76e7ae Removed conftest.py modules in tests, which aren't required anymore with pytest v2.0 2010-12-30 09:47:22 +01:00
Virgil Dupras
28426c0e91 [#121 state:fixed] Catch HTTPException in Fairware fetching. 2010-12-30 09:35:45 +01:00
Virgil Dupras
3a9f51b600 [#122 state:fixed] Fixed crash on scanning when file is being deleted during the scan. 2010-12-29 15:41:12 +01:00
Virgil Dupras
f1b4db368e [#123 state:fixed] Updated codebase to use hsaudiotag v1.1.0 (which fixed the AIFF bug) and made it use the new auto.File wrapper. 2010-12-29 13:17:30 +01:00
Virgil Dupras
95efac187b Updated hscommon and adapted to changes in hscommon.gui.table.Table.refresh(). 2010-11-24 16:12:10 +01:00
Virgil Dupras
6770d22438 Updated hscommon subrepo. 2010-11-22 10:06:42 +01:00
Virgil Dupras
4ce97613c4 Added tag me5.10.3 for changeset ca93352ce351 2010-11-21 17:56:37 +01:00
Virgil Dupras
030eb8eb6e Fixed debian packaging 2010-11-21 07:49:38 -08:00
Virgil Dupras
c9da8e26e6 Fixed crash caused by outdated hsgui. Also, fixed app_test, which was also outdated. 2010-11-21 16:45:02 +01:00
Virgil Dupras
7ddf9772df v5.10.3 2010-11-21 16:25:16 +01:00
Virgil Dupras
0382ad1534 Adapted to the job-related code moving to the 'jobprogress' package. 2010-11-20 12:42:15 +01:00
Virgil Dupras
1b6e1369a0 Tranformed PyQt's license warning into a licensing note
--HG--
rename : qt/WARNING => qt/ABOUT_LICENSE
2010-11-13 14:37:20 +01:00
Virgil Dupras
835050c337 Added tag pe1.11.2 for changeset 154c8cb6f018 2010-10-07 12:44:04 +02:00
Virgil Dupras
ca6a42e6eb pe 1.11.2 2010-10-07 11:34:29 +02:00
Virgil Dupras
a2e4d893ac Added tag me5.10.2 for changeset f9cae82a0752 2010-10-06 12:44:43 +02:00
Virgil Dupras
657520b0b3 me5.10.2 2010-10-06 11:43:37 +02:00
Virgil Dupras
ea4b87895c Fixed typo in fairware reminder. 2010-10-06 11:37:26 +02:00
Virgil Dupras
19db500a19 Updated cocoalib subrepo. 2010-10-06 10:47:05 +02:00
Virgil Dupras
1366cfd478 Added a "Don't contribute" feedback dialog to fairware reminder. 2010-10-06 10:45:27 +02:00
Virgil Dupras
56a6df1f68 Added tag se2.12.2 for changeset 22239f94589b 2010-10-05 15:56:21 +02:00
Virgil Dupras
a1b35a8abf Fixed reg demo dialog for windows. 2010-10-05 13:55:26 +01:00
Virgil Dupras
8a8a181186 se 2.12.2 2010-10-05 03:50:28 -07:00
Virgil Dupras
463a551f7d Fixed debian packaging. 2010-10-05 02:02:02 -07:00
Virgil Dupras
fc613fb325 Fixed Qt packaging under Windows. 2010-10-05 09:48:07 +01:00
Virgil Dupras
4517bea664 Moved the start.py file directly in qt run template instead of using this subprocess thingy. Much easier for packaging. 2010-10-05 10:22:02 +02:00
Virgil Dupras
81dcfbe6ae Use sys.platform instead of platform.system(). The latter somes crashes with "Interrupted system call". 2010-10-05 10:03:56 +02:00
Virgil Dupras
fa8e64d04a Fixed delta values color which were mixed up. 2010-10-05 09:54:52 +02:00
Virgil Dupras
562123b219 Fixed qt run template so that the current environment is sent to the new python process. 2010-10-05 00:36:20 -07:00
Virgil Dupras
b217309618 Replaced the use of runpy for running Qt by a simple subprocess call. runpy would cause weird QTimer warnings. 2010-10-05 09:27:32 +02:00
Virgil Dupras
357a02c74b Use QTimer.singleShot() for nag window showing instead of a complicated scheme like the old one. 2010-10-05 08:44:32 +02:00
Virgil Dupras
508eeffa6e Fixed register button that linked to the wrong method. 2010-10-05 08:20:53 +02:00
Virgil Dupras
31555aa473 Rather than having a run.py file that checks build config at runtime, this file is generated at build time, making it easier to package it. 2010-10-04 15:42:38 +02:00
Virgil Dupras
d2f968def7 Removed .ui files and made the UI setup "by hand". ui files cause more problems than they solve (UI designer is limited in what it can do). 2010-10-04 15:29:00 +02:00
Virgil Dupras
d574bc611b [#108 state:fixed] Fixed column mess after the earlier removal of the ctime col. 2010-10-04 10:01:52 +02:00
Virgil Dupras
a50a3b0123 [#106 state:fixed] Wrapped getJobDesc in a try except in case it isn't defined yet. 2010-10-04 09:40:46 +02:00
Virgil Dupras
5b6891dd45 se v2.12.1 me v5.10.1 pe v1.11.1 2010-09-30 12:35:40 +02:00
Virgil Dupras
4886982d43 Re-licensed to BSD 2010-09-30 12:17:41 +02:00
Virgil Dupras
7360f57beb Converted registration system to Fairware. 2010-09-29 16:49:50 +02:00
Virgil Dupras
491279b7a8 Added tag before-fairware for changeset 96b6aee66839 2010-09-29 16:11:52 +02:00
Virgil Dupras
05b79f81af Added tag pe1.11.0 for changeset b07ac1398703 2010-09-27 15:13:42 +02:00
Virgil Dupras
96ef2f2dd3 Added tag me5.10.0 for changeset d3fe0d0dcda1 2010-09-27 15:13:36 +02:00
Virgil Dupras
2542af17b6 Adjusted default column widths so it fits better with the revamped UI. 2010-09-27 12:25:31 +02:00
Virgil Dupras
c86bc649ff pe 1.11.0 and me 5.10.0. 2010-09-27 11:56:02 +02:00
Virgil Dupras
4b8e48ed88 Added tag se2.12.0 for changeset dbfee3ee2fa5 2010-09-26 14:26:06 +02:00
Virgil Dupras
a1addfd416 Fixed typo in changelog. 2010-09-26 14:25:53 +02:00
Virgil Dupras
a1a57d8933 Adjusted default column widths to fit better with UI revamp. 2010-09-26 12:50:45 +02:00
Virgil Dupras
864970b860 se2.12.0 2010-09-26 12:33:39 +02:00
Virgil Dupras
a056be0842 Fixed UI glitch introduced by the move from outline to table for results (the selected row would not be kept visible after refreshes). 2010-09-26 12:09:50 +02:00
Virgil Dupras
c672e75739 Updated subrepo. 2010-09-26 02:20:18 -07:00
Virgil Dupras
7b5dd3f964 Adjusted the height of the pref pane in SE under Linux. 2010-09-26 02:17:29 -07:00
Virgil Dupras
a6072f608b [#105 state:fixed] Allow multiple selection in Add Directory. 2010-09-25 16:12:20 +02:00
Virgil Dupras
06462c65a5 Updated help files. 2010-09-25 15:49:19 +02:00
Virgil Dupras
359f9c0680 [#92 state:fixed] Added an action to delete duplicates and then create hardlinks to group ref. 2010-09-25 15:37:18 +02:00
Virgil Dupras
01db7c4948 Fixed a py3k-induced bug when drag & dropping directories in the directories panel. 2010-09-25 15:34:42 +02:00
Virgil Dupras
f67f14a78d Fixed compilation warning under cocoa. 2010-09-25 12:35:51 +02:00
Virgil Dupras
0a64d653e1 [#92 state:fixed] Added an option to ignore duplicates hardlinking to the same file. 2010-09-25 12:28:34 +02:00
Virgil Dupras
456a835285 Made the main window under cocoa a little cuter. 2010-09-24 16:01:38 +02:00
Virgil Dupras
0d8ed92a68 Converted the result tree into a result table.
--HG--
rename : cocoa/base/PyResultTree.h => cocoa/base/PyResultTable.h
rename : cocoa/base/ResultOutline.h => cocoa/base/ResultTable.h
rename : cocoa/base/ResultOutline.m => cocoa/base/ResultTable.m
rename : core/gui/result_tree.py => core/gui/result_table.py
2010-09-24 15:48:59 +02:00
Virgil Dupras
9bd093a03c [#106 state:fixed] I couldn't find the root cause of the problem, but I wrapped it anyway... 2010-09-24 09:56:08 +02:00
Virgil Dupras
361d4698a9 Added tag se2.11.1 for changeset 9735a5218d2b 2010-08-26 13:39:42 +02:00
Virgil Dupras
b342b15011 se v2.11.1 2010-08-26 13:03:14 +02:00
Virgil Dupras
95638a3a80 Added tag me5.9.1 for changeset 95b3a4b564c6 2010-08-26 12:58:43 +02:00
Virgil Dupras
2204fe3355 Fixed help file under Cocoa, which strangely stopped working. 2010-08-24 02:34:27 -07:00
Virgil Dupras
abcd774c9d me5.9.1 2010-08-24 10:46:47 +02:00
Virgil Dupras
ee209f8f88 Added tag pe1.10.0 for changeset 618a7365457d 2010-08-21 18:27:27 +02:00
Virgil Dupras
b1f2e1c191 Fixed debian packaging for PE. 2010-08-21 08:26:56 -07:00
Virgil Dupras
33f372f6c6 Fixed the building process of the block module for Qt. 2010-08-21 16:04:23 +01:00
Virgil Dupras
8e5c2a8875 pe v1.10.0 2010-08-21 16:44:50 +02:00
Virgil Dupras
36f3638ae4 [#104 state:fixed] Fixed str/bytes mixup in HTML export. 2010-08-21 16:34:35 +02:00
Virgil Dupras
d10210011f Added tag me5.9.0 for changeset b56fe4dd8c95 2010-08-20 12:19:06 +02:00
Virgil Dupras
e867840d81 Fixed debian packaging for ME. 2010-08-20 02:29:51 -07:00
Virgil Dupras
fb7e3189a8 me v5.9.0 2010-08-20 09:51:30 +02:00
Virgil Dupras
5733c0143b With PyQt 4.7.5's new from_imports option, sys.path hackage is not required anymore. 2010-08-20 09:48:16 +02:00
Virgil Dupras
ac4881f231 Updated py2app workarounds for ME again. 2010-08-18 12:01:10 +02:00
Virgil Dupras
939efd7dab Updated py2app workarounds for ME. 2010-08-18 11:37:18 +02:00
Virgil Dupras
a93d96d742 Added tag se2.11.0 for changeset 2b67955db2b0 2010-08-18 10:07:06 +02:00
Virgil Dupras
f21804c769 Fixed typo in changelog. 2010-08-18 08:56:22 +02:00
Virgil Dupras
4bc05a8d46 dgse v2.11.0 was delayed. 2010-08-18 07:59:26 +02:00
Virgil Dupras
eebe2b0e80 Fixed debian packaging some more. 2010-08-18 07:55:01 +02:00
Virgil Dupras
250a496a78 Fixed debian packaging for SE under Python 3. 2010-08-17 07:26:46 -07:00
Virgil Dupras
29163ed053 se v2.11.0 2010-08-17 11:32:20 +02:00
Virgil Dupras
cc05661f9e Qt: Fixed packaging which didn't work under py3k. 2010-08-17 09:27:38 +01:00
Virgil Dupras
89409c22d1 Removed dependencies on PIL. Man, I wish I had known about QImageReader sooner... That was a little stupid on my part not to look further than QImage. 2010-08-17 09:38:58 +02:00
Virgil Dupras
e2f240ebc9 Prettified the build system by getting rid of those "gen.py" files and hardcoded "python3" calls. Also, ported Qt's block.c to Python3, which hadn't been done yet. 2010-08-17 09:30:25 +02:00
Virgil Dupras
8d56f4c33b Fixed broken test. 2010-08-15 15:09:40 +02:00
Virgil Dupras
36eccb7122 Removed the "all files are refs" error message and made the "no files, can't scan" message quicker. That's because when scanning iPhoto libraries with big libraries, the GUI would hang because these checks would involve loading the whole library. 2010-08-15 15:07:44 +02:00
Virgil Dupras
c8827769b4 Removed dependency on lxml (it made the final package much bigger, and building it on windows is not fun). 2010-08-15 14:42:55 +02:00
Virgil Dupras
12e6c400b9 Fixes here and there to make dupeGuru PE run. 2010-08-15 14:23:16 +02:00
Virgil Dupras
4c273a7910 [#102 state:fixed] Remember the size/position of all window between launches. 2010-08-15 12:27:15 +02:00
Virgil Dupras
58da335b17 Enum-ified Scan Type constants, looks nicer. 2010-08-14 19:52:23 +02:00
Virgil Dupras
5b2d506462 [#15 state:fixed] Improved tie breaker in cases where filenames end with digits inside brackets. 2010-08-14 19:32:09 +02:00
Virgil Dupras
531430d44a Updated dependencies in the README file. 2010-08-14 13:20:57 +02:00
Virgil Dupras
7450eec7eb Added Load/Save Results menu items, allowing to save results at arbitrary places. 2010-08-13 13:06:18 +02:00
Virgil Dupras
3a5802435f Only save results on quit if the results are actually modified. 2010-08-13 11:48:05 +02:00
Virgil Dupras
1b6b058097 Added a is_modified flag to Results. 2010-08-13 11:37:45 +02:00
Virgil Dupras
a5797a2350 Semi-pytest-ified results_test. 2010-08-13 09:48:37 +02:00
Virgil Dupras
e81a5147c5 Adjusted details panel's eight in SE. 2010-08-13 09:32:05 +02:00
Virgil Dupras
565c990687 [#101 state:fixed] Remove the Creation Time column. 2010-08-13 09:26:38 +02:00
Virgil Dupras
0ccdfe0e26 Adjusted xcode project to registration.xib move due to localization. 2010-08-12 17:29:22 +02:00
Virgil Dupras
f8a558e3a7 Updated cocoalib subrepo. 2010-08-12 17:24:54 +02:00
Virgil Dupras
c5fa195cc6 [#103 state:fixed] Correctly hide progress dialog when a job was completed while dupeGuru was inactive. 2010-08-12 17:21:01 +02:00
Virgil Dupras
3a821edd45 Results loading now takes place in one shot (file locate and metadata read). It makes weeding out the bad files more convenient and fixes the Cancel loading glitch where we end up with "ghost" results. 2010-08-12 15:57:47 +02:00
Virgil Dupras
854d194f88 Converted to py3k. There's probably some bugs still. So far, I managed to run dupeGuru SE under pyobjc and qt. 2010-08-11 16:39:06 +02:00
Virgil Dupras
fb79daad6a Added tag before-py3k for changeset 634b66415c65 2010-08-11 16:15:32 +02:00
Virgil Dupras
b2ae0e8759 Added tag pe1.9.1 for changeset 724ff565dd78 2010-07-17 10:33:19 +02:00
Virgil Dupras
09f73988b3 pe v1.9.1 2010-07-17 07:14:39 +02:00
Virgil Dupras
9e6f289319 Added tag me5.8.1 for changeset 3742e83edd9e 2010-07-16 10:04:28 +02:00
Virgil Dupras
d2a55ffd31 me v5.8.1 2010-07-16 08:53:43 +02:00
Virgil Dupras
793c2aa423 Added tag se2.10.1 for changeset f71d405e62ba 2010-07-15 09:21:19 +02:00
Virgil Dupras
5daa332b6c Merged heads. 2010-07-15 09:20:52 +02:00
Virgil Dupras
d5511a857c Added tag se2.10.1 for changeset cb0a860430ba 2010-07-15 09:18:39 +02:00
Virgil Dupras
7fecd21331 Fixed typos in help. 2010-07-15 09:18:30 +02:00
Virgil Dupras
88b79e512f Updated hscommon subrepo. 2010-07-15 06:54:25 +01:00
Virgil Dupras
853bf63777 v2.10.1 2010-07-15 07:31:33 +02:00
Virgil Dupras
ff16fea54a Fixed debian packaging. 2010-07-14 02:40:46 -07:00
Virgil Dupras
a03e2a69d4 [#97 state:fixed] Fixed a crash on load. 2010-07-14 10:50:15 +02:00
Virgil Dupras
56a39df635 [#96 state:fixed] Fixed a hard crash on calling get_blocks() with an empty path. 2010-07-14 09:36:35 +02:00
Virgil Dupras
ac1593ff75 [#95 state:fixed] Fixed a crash on results save when it contained invalid characters. 2010-07-14 09:19:34 +02:00
Virgil Dupras
4d66b4667c Moved from nose to py.test (the former doesn't officially support py3k, which is limiting). 2010-07-13 11:10:45 +02:00
Virgil Dupras
fdde538b66 Converted help files to the new, simpler helpgen system in hscommon.
--HG--
rename : help_me/templates/credits.mako => help_me/en/credits.md
rename : help_me/templates/directories.mako => help_me/en/directories.md
rename : help_me/templates/faq.mako => help_me/en/faq.md
rename : help_me/templates/intro.mako => help_me/en/intro.md
rename : help_me/templates/power_marker.mako => help_me/en/power_marker.md
rename : help_me/templates/preferences.mako => help_me/en/preferences.md
rename : help_me/templates/quick_start.mako => help_me/en/quick_start.md
rename : help_me/templates/results.mako => help_me/en/results.md
rename : help_me/templates/versions.mako => help_me/en/versions.md
rename : help_pe/templates/credits.mako => help_pe/en/credits.md
rename : help_pe/templates/directories.mako => help_pe/en/directories.md
rename : help_pe/templates/faq.mako => help_pe/en/faq.md
rename : help_pe/templates/intro.mako => help_pe/en/intro.md
rename : help_pe/templates/power_marker.mako => help_pe/en/power_marker.md
rename : help_pe/templates/preferences.mako => help_pe/en/preferences.md
rename : help_pe/templates/quick_start.mako => help_pe/en/quick_start.md
rename : help_pe/templates/results.mako => help_pe/en/results.md
rename : help_pe/templates/versions.mako => help_pe/en/versions.md
rename : help_se/templates/credits.mako => help_se/en/credits.md
rename : help_se/templates/directories.mako => help_se/en/directories.md
rename : help_se/templates/faq.mako => help_se/en/faq.md
rename : help_se/templates/intro.mako => help_se/en/intro.md
rename : help_se/templates/power_marker.mako => help_se/en/power_marker.md
rename : help_se/templates/preferences.mako => help_se/en/preferences.md
rename : help_se/templates/quick_start.mako => help_se/en/quick_start.md
rename : help_se/templates/results.mako => help_se/en/results.md
rename : help_se/templates/versions.mako => help_se/en/versions.md
2010-07-13 11:03:20 +02:00
Virgil Dupras
de1147219c Adjusted a forgotten hsutil/hscommon reference. 2010-07-13 08:16:44 +02:00
Virgil Dupras
371426a08e Adapted codebase to the hsutil/hscommon split and the hsmedia --> hsaudiotag rename. 2010-07-13 08:08:18 +02:00
Virgil Dupras
75eb005ba0 Fixed a flaky test which was broken in python 2.7rc1. 2010-06-07 10:15:58 -04:00
Virgil Dupras
601b67145c Fixed a flaky test which was broken in python 2.7rc1. 2010-06-07 09:41:59 -04:00
Virgil Dupras
c65afbc057 Added tag pe1.9.0 for changeset 27501167e3b9 2010-04-15 17:17:08 +02:00
Virgil Dupras
378589a473 Brought dgpe qt up to speed for the 1.9.0 release. 2010-04-15 10:05:33 +01:00
Virgil Dupras
fa264972a4 pe v1.9.0 2010-04-15 10:41:36 +02:00
Virgil Dupras
6b10e01c03 Updated dgpe help to include custom command description. 2010-04-15 10:40:34 +02:00
Virgil Dupras
5a6d74ab37 Brought dgpe cocoa up to speed for the 1.9 release. 2010-04-15 10:38:53 +02:00
Virgil Dupras
73f1bb6968 Tweaked dgpe's matching to work better with huge scans. 2010-04-15 10:38:30 +02:00
Virgil Dupras
d1a7f51859 Added tag me5.8.0 for changeset 388a7e5aef63 2010-04-14 14:08:24 +02:00
Virgil Dupras
2ae16396a6 Updated dgme installer project to cope with cxFreeze inability to add version information to the exe. 2010-04-14 09:22:16 +01:00
Virgil Dupras
ef090a5dc5 Updated the dgme Qt pref dialog to include the custom command field and added cxFreeze workaround in dgme qt start script. 2010-04-14 09:10:57 +01:00
Virgil Dupras
5c0799e82b me v5.8.0 2010-04-14 09:37:36 +02:00
Virgil Dupras
fa2ee01d3f Updated the project file to include newly added units for 5.8, updated the preferences XIB to add the Custom Command field and updated the help file to include custom commands. 2010-04-14 09:33:42 +02:00
Virgil Dupras
d6ba80bd3f Added tag se2.10.0 for changeset 914902428395 2010-04-13 17:31:36 +02:00
Virgil Dupras
ee96d5f88c Fixed Windows packaging for dgse. 2010-04-13 14:04:15 +01:00
Virgil Dupras
e96a917bef Fixed the problem dialog under cocoa, which was visible at launch. 2010-04-13 14:22:24 +02:00
Virgil Dupras
769b816998 se v2.10.0 2010-04-13 11:58:53 +02:00
Virgil Dupras
ff891c210c [#4 state:fixed] Filters are now applied on the whole file path. 2010-04-13 11:40:20 +02:00
Virgil Dupras
3ed5e1bf95 [#12 state:fixed] Added custom command help. 2010-04-13 11:05:42 +02:00
Virgil Dupras
5bc8581389 [#12] Tweaked the custom command feature under Cocoa. 2010-04-13 10:52:44 +02:00
Virgil Dupras
7346b422d5 [#12] Added the Custom Command preference on the Qt side. 2010-04-13 09:02:09 +01:00
Virgil Dupras
5c80ac1c74 [#12] dgse cocoa: Added custom command invocation. 2010-04-12 17:43:24 +02:00
Virgil Dupras
699023992c Added the problem dialog to the Qt side. 2010-04-12 15:29:56 +02:00
Virgil Dupras
454ce604ad Merged hsgui heads. 2010-04-12 12:22:18 +02:00
Virgil Dupras
1e0f6bfecb Added a dialog giving more information about the causes of problems during operations. 2010-04-12 12:21:01 +02:00
Virgil Dupras
7f10aa3de2 Merged heads. 2010-04-08 07:07:45 -07:00
Virgil Dupras
f8764ab85e dgme qt: Fixed visual glitch in preference panel under Linux. 2010-04-08 07:06:32 -07:00
Virgil Dupras
aa8544308e Added tag pe1.8.6 for changeset 556baf4a4107 2010-04-08 15:01:27 +02:00
Virgil Dupras
31fc70e0f8 Updated dependencies to include cx_Freeze. 2010-04-08 15:01:21 +02:00
Virgil Dupras
a16af4560b dgse qt: fixed visual glitch in the preference dialog under linux. 2010-04-08 04:26:11 -07:00
Virgil Dupras
0782ba0dab Only do cxfreeze workarounds under Windows. 2010-04-08 04:12:29 -07:00
Virgil Dupras
83725667a4 Made the windows packaging copy qt plugins in the dist package. PyInstaller did this, but cxfreeze doesn't. 2010-04-08 11:17:03 +01:00
Virgil Dupras
f4b3163b04 Merged heads. 2010-04-08 11:14:42 +02:00
Virgil Dupras
6cd745f429 Added hsutil.files.find_in_path() 2010-04-08 11:14:01 +02:00
Virgil Dupras
6131f7f6bf Merge heads. 2010-04-08 07:55:03 +01:00
Virgil Dupras
dd4faa030f Changed the installer project so that we make sure that the executable is always overwritten.
Previously, (probably because the exe doesn't have version embedded in it anymore), we ended up, during upgrades, with executable-less installs.
2010-04-08 07:54:03 +01:00
Virgil Dupras
ab8691f5ac Changed the release date for pe 1.8.6, which has been delayed by packaging problems. 2010-04-08 08:35:17 +02:00
Virgil Dupras
77ab073cdb Added a missing python-lxml dep to the debian packages. 2010-04-07 09:32:49 -07:00
Virgil Dupras
87e0011525 Under Linux, don't show the "Check for Update" action and correctly open the help file. 2010-04-07 09:04:58 -07:00
Virgil Dupras
7af3bb7226 Merged heads. 2010-04-07 08:50:56 -07:00
Virgil Dupras
5573352ce6 PyInstaller is fucked up. Moved to cxFreeze. 2010-04-07 16:30:04 +01:00
Virgil Dupras
e6486e08ab Qt: fixed help packaging. 2010-04-07 15:04:09 +01:00
Virgil Dupras
48badaa927 pe v1.8.6 2010-04-07 13:59:40 +02:00
Virgil Dupras
2f13bf677e Adjusted details table height by 2 pixels so that it doesn't show a scrollbar under Linux. 2010-04-07 04:02:18 -07:00
Virgil Dupras
e63abc1b4b Added debian packaging support. 2010-04-07 03:56:43 -07:00
Virgil Dupras
88334acdef [#90 state:fixed] Fixed a rare crash on results loading. 2010-04-07 10:29:00 +02:00
Virgil Dupras
0491aa9f6e Updated dependencies in README. 2010-04-07 09:14:10 +02:00
Virgil Dupras
5be76d7c0f Use the send2trash lib in _do_delete_dupe(). 2010-04-07 09:11:36 +02:00
Virgil Dupras
3b510389fc cocoa: Removed obsolete refreshStats calls. 2010-04-07 09:09:19 +02:00
Virgil Dupras
32d88e9249 Limit the size of arguments sent to multiprocessing because it could cause crashes. 2010-04-05 10:15:33 +02:00
Virgil Dupras
7b1a1ff4bb Added tag pe1.8.5 for changeset 0a71306434bc 2010-03-01 17:15:23 +01:00
Virgil Dupras
19beb919d0 Fixed the automatic update check option on the Cocoa side. 2010-03-01 16:09:59 +01:00
Virgil Dupras
ba09e8bf4d Updated the readme file to add the lxml dependency. 2010-03-01 14:35:58 +01:00
Virgil Dupras
26dd2d0e8e Updated py2app workaround in dg_cocoa for lxml. 2010-03-01 04:15:27 -08:00
Virgil Dupras
69b15d58a2 Updated hsutil subrepo. 2010-03-01 12:33:16 +01:00
Virgil Dupras
ba68789fb9 pe v1.8.5 2010-03-01 12:31:34 +01:00
Virgil Dupras
47a6ceffbc Use lxml everywhere for xml save/load (instead of ElementTree and minidom). 2010-03-01 12:21:43 +01:00
Virgil Dupras
b17ca66f73 Fixed crashes when reading invalid iPhoto AlbumData file. This time, I used lxml's "recover" feature to filter out crap in the XML, so it should cover most cases of invalid stuff in iPhoto data files. 2010-03-01 12:20:21 +01:00
Virgil Dupras
93bc609026 Updated the SE cocoa project so that it includes the lastest changes in dgbase and cocoalib. 2010-03-01 12:14:49 +01:00
Virgil Dupras
3ea51c2e15 Added tag pe1.8.4 for changeset 4c3cb1e671a3 2010-02-18 15:31:59 +01:00
Virgil Dupras
1d9897ea60 (Forgot to commit). Updated the ME installer project for Advanced Installer 7.5. 2010-02-18 09:49:28 +00:00
Virgil Dupras
b6cb00bc79 pe 1.8.4 2010-02-18 10:31:24 +01:00
Virgil Dupras
6dd53c6bfd Removing duplicates now preserve selected paths. 2010-02-17 18:05:19 +01:00
Virgil Dupras
07df5126b3 Adapted the PE edition to the latest refactorings and fixed a (very) minor memory leak in ME. 2010-02-17 17:37:42 +01:00
Virgil Dupras
47b38c7d45 Preliminary linux support (it starts up, at least...). 2010-02-13 12:22:34 -08:00
Virgil Dupras
0e97bec7b2 Added tag me5.7.2 for changeset 90ed56ee6026 2010-02-13 18:36:54 +01:00
Virgil Dupras
b182585d46 Fixed column reloading which was broken since the mark-->marked rename. 2010-02-13 14:08:37 +01:00
Virgil Dupras
e8f92535d3 me v5.7.2 2010-02-13 13:00:41 +01:00
Virgil Dupras
d62c3663e9 qt: scroll to selection on results refresh. 2010-02-13 12:34:36 +01:00
Virgil Dupras
6b0bfda9fb During Make Selected Reference, it's now the selection *paths* that are restored rather than the selected *dupes* 2010-02-13 10:39:54 +01:00
Virgil Dupras
7477330961 Fixed ResultOutline.selectedDupeCount(). 2010-02-12 21:58:50 +01:00
Virgil Dupras
1f71157063 Updated cocoalib subrepo. 2010-02-12 20:10:50 +01:00
Virgil Dupras
905988c592 Removed MatchesView and took advantage of HSOutlineView's delete and space triggered delegate methods. 2010-02-12 17:15:48 +01:00
Virgil Dupras
310951bfa8 Removed getSelectedPaths() from ResultsWindow. 2010-02-12 16:30:32 +01:00
Virgil Dupras
64c1087856 Fixed app_test which was broken since connext() calls aren't made by the gui themselves. 2010-02-12 16:28:15 +01:00
Virgil Dupras
cab6d924aa Adapted the Qt codebase to the addition of core.gui.result_tree and core.gui.stats_label. 2010-02-12 15:39:29 +01:00
Virgil Dupras
c3a972d39b Fixed renaming in results. 2010-02-12 13:52:40 +01:00
Virgil Dupras
33d44d4d24 Remove Marked now correctly updates the results. 2010-02-12 13:39:50 +01:00
Virgil Dupras
fd89cf2482 Pushed some code down from app_cocoa to app and re-organized test units. 2010-02-12 12:43:50 +01:00
Virgil Dupras
112ffb981f Cleaned up some cruft. 2010-02-12 12:30:00 +01:00
Virgil Dupras
514426b980 Re-added the root children count optimization in the results outline. 2010-02-12 11:34:00 +01:00
Virgil Dupras
a4bf1c8be6 Made marking changes much faster and also made data fetching lazy in dupe nodes. 2010-02-12 11:21:39 +01:00
Virgil Dupras
9b82e1478f Re-added multiple selection support in the results. 2010-02-12 11:07:33 +01:00
Virgil Dupras
d5f145d57e Fixed sorting. 2010-02-11 21:03:22 +01:00
Virgil Dupras
bab891ee74 Added the StatsLabel. 2010-02-11 20:54:06 +01:00
Virgil Dupras
a65fd7d0d0 Brought back delta values. 2010-02-11 19:22:31 +01:00
Virgil Dupras
46836cc805 Pushed down some result refresh calls to the core code. 2010-02-11 18:47:45 +01:00
Virgil Dupras
42559f13d8 Began the transition to a HSOutline based result outline. There's still a lot of glitches, the most glaring one being the lack of support for multiple selection. 2010-02-11 17:52:18 +01:00
Virgil Dupras
87351b5920 Removed Table from cocoalib and fixed the license of the newly added units. 2010-02-11 13:38:34 +01:00
Virgil Dupras
e68dcf189c Adapted the ME project to the latest structural changes. 2010-02-11 13:35:14 +01:00
Virgil Dupras
5d62b8389c Added tag pe1.8.3 for changeset 1cef6d39855f 2010-02-11 12:37:15 +01:00
Virgil Dupras
c50aebe76d pe v1.8.3 2010-02-11 10:04:54 +01:00
Virgil Dupras
a610f3fde7 Adapted the PE project to the latest structural changes. 2010-02-10 12:07:31 +01:00
Virgil Dupras
626391a1d9 [#94 state:fixed] Fixed bug in block_osx causing blocks containing nil values to be created. 2010-02-10 11:58:05 +01:00
Virgil Dupras
1bedfe75ea Added tag se2.9.2 for changeset 7b7c5a66ebee 2010-02-10 10:50:05 +01:00
Virgil Dupras
86ecc8d4d5 Fixed build script 2010-02-10 00:18:25 -08:00
Virgil Dupras
9eca84efe1 se v2.9.2 2010-02-10 08:48:01 +01:00
Virgil Dupras
8a6fb6dcba Updated Andvanced Installer project file for 7.5. 2010-02-09 15:03:36 +00:00
Virgil Dupras
e3706fa923 Fixed qt packaging. 2010-02-09 14:52:09 +00:00
Virgil Dupras
8193bc5f60 build.add_to_pythonpath() now also adds the path to sys.path. 2010-02-09 15:47:22 +01:00
Virgil Dupras
504ecaee5e Straightened out qt's packaging process. 2010-02-09 15:42:48 +01:00
Virgil Dupras
7c9e836572 Straightened out qt's build process. 2010-02-09 15:32:52 +01:00
Virgil Dupras
5db0f09b43 Fixed Reveal File on Qt. 2010-02-09 15:24:57 +01:00
Virgil Dupras
195bc4ef21 Eliminated code duplication in ResultsWindow. 2010-02-09 14:59:35 +01:00
Virgil Dupras
6b190bc184 Fixed a bug where double clicking a column would open the selected file. 2010-02-09 14:55:51 +01:00
Virgil Dupras
39f1cac2c8 Eliminated code duplication in ResultsWindow's awakeFromNib. 2010-02-09 14:50:27 +01:00
Virgil Dupras
d193eed519 [#93 state:fixed] Straightened out selection and matches reloading. 2010-02-09 14:45:14 +01:00
Virgil Dupras
2d80b0e12f Updated hsutil subrepo. 2010-02-08 08:37:40 +01:00
Virgil Dupras
b50d99be9c Added the PyRegistrable cocoa interface. 2010-02-07 16:29:39 +01:00
Virgil Dupras
af41876a5e DetailsPanel is now a subclass of HGWindowController. 2010-02-07 16:19:14 +01:00
Virgil Dupras
76d351d8be Adapted th qt part to core.gui.directory_tree. 2010-02-07 16:00:58 +01:00
Virgil Dupras
b5dd9651c3 Huge refactoring. I moved MGOutline from moneyGuru (as well as everything that comes with it) and used it to create DirectoryOutline for the directories panel. 2010-02-07 15:26:50 +01:00
Virgil Dupras
3e34502014 Added the hsgui subrepo. 2010-02-06 15:35:51 +01:00
Virgil Dupras
5e57f9cbd6 Removed logic duplication across toolkit code in "Reveal Selected" action. 2010-02-06 15:31:35 +01:00
Virgil Dupras
8edb869fdc Removed logic duplication across toolkit code in "Remove Selected" action. 2010-02-06 12:44:21 +01:00
Virgil Dupras
37238c7f57 Removed logic duplication across toolkit code in "Open Selected" action. 2010-02-06 12:36:43 +01:00
Virgil Dupras
9edee82fa1 Removed logic duplication across toolkit code in "Make Reference" action. 2010-02-06 12:27:11 +01:00
Virgil Dupras
f7aaea79af Removed useless add_to_ignore_list() 2010-02-06 12:14:33 +01:00
Virgil Dupras
3c75d2f8b7 Removed logic duplication across toolkit code in "Add to Ignore List" action. 2010-02-06 12:12:20 +01:00
Virgil Dupras
64c67e19d2 Reduced code duplication among editions in ResultsWindow. 2010-02-06 11:40:10 +01:00
Virgil Dupras
d4db8faad8 Added tag pe1.8.2 for changeset 19e40bab2052 2010-02-06 10:49:13 +01:00
Virgil Dupras
7957b73b4a Tweaked PE installer project. 2010-02-06 09:30:33 +00:00
Virgil Dupras
69838c44af pe 1.8.2 2010-02-06 09:09:40 +01:00
Virgil Dupras
8e2953aef6 Updated PE installer for Advanced Installer 7.5 and changed build scripts so they use the Advanced Installer command present in the PATH. 2010-02-06 07:58:37 +00:00
Virgil Dupras
8dda616502 The Qt side now makes use of core.gui.details_panel. 2010-02-05 21:09:04 +01:00
Virgil Dupras
484512e35b Removed refreshDetailsWithSelected which wasn't needed anymore. 2010-02-05 20:32:57 +01:00
Virgil Dupras
c8cd05c07d Removed code duplication among editions in ResultWindow. 2010-02-05 20:16:56 +01:00
Virgil Dupras
7ffefe6259 Created gui.details_panel and moved all details panel related logic in there (cocoa only, for now). 2010-02-05 20:10:54 +01:00
Virgil Dupras
cd9b7f2f11 [#86 state:fixed] Fixed a crash in GetOutlineViewValues. 2010-02-05 18:16:05 +01:00
Virgil Dupras
b372974437 [#84 state:hold] Added debug logging to fs.get_files() to eventually figure out the cause of this bug. 2010-02-05 17:55:47 +01:00
Virgil Dupras
7464e0f799 [#85 state:fixed] Fixed crash when sorting by Words Used after a Contents scan. 2010-02-05 17:47:17 +01:00
Virgil Dupras
25e12f1775 [#83 state:fixed] Fixed crash on quitting if the appdata dir has been removed. 2010-02-05 17:24:20 +01:00
Virgil Dupras
6416469f78 Re-organized DetailsPanel across editions in a saner way. 2010-02-05 17:15:45 +01:00
Virgil Dupras
922ce5ae36 Re-organized DirectoryPanel across editions in a saner way. 2010-02-05 17:05:00 +01:00
Virgil Dupras
9ca8a199c0 Re-implemented the fix for utf-8 lookup error during auto-update in a more graceful way. 2010-02-05 16:51:00 +01:00
Virgil Dupras
a570406ac8 Reduced code duplication among editions in AppDelegate.m. 2010-02-05 16:31:09 +01:00
Virgil Dupras
719edb6b6e Use hsutil.cocoa.objcmin instead of Foundation and AppKit. 2010-02-04 17:12:58 +01:00
Virgil Dupras
d075218621 Removed a </li> tag in preferences help pages which had nothing to do there. 2010-02-04 15:25:29 +01:00
Virgil Dupras
7509943938 Added _block_osx to py2app workaround in dg_cocoa. 2010-02-04 06:13:40 -08:00
Virgil Dupras
774da9d2f8 Fixed XCode projects' target build settings. 2010-02-04 15:00:06 +01:00
Virgil Dupras
978fd383e8 Fixed package.py which was broken since the xcode template change. 2010-02-04 14:36:02 +01:00
Virgil Dupras
8551fc23fe Removed the 'build64' option and added a 'dev' configuration to all xcode projects. 2010-02-04 13:45:35 +01:00
Virgil Dupras
fb711edeeb Use hsutil.cocoa.signature instead of objc.signature in dgse.dg_cocoa. 2010-02-04 13:20:38 +01:00
Virgil Dupras
352a21acaa Converted PictureBlocks to a Python extension and created a "common" C unit for common code among extensions. 2010-02-04 13:13:08 +01:00
Virgil Dupras
0b9d936317 Optimized qt/pe/modules/block.c 2010-02-03 15:44:15 +01:00
Virgil Dupras
dc500243e9 Updated cocoalib subrepo. 2010-02-03 12:19:51 +01:00
Virgil Dupras
21203b8341 Adapted to NIB --> XIB conversion in cocoalib. 2010-02-03 12:17:39 +01:00
Virgil Dupras
5b03447640 Updated subrepos to improve the registration process. 2010-02-03 11:58:58 +01:00
Virgil Dupras
d34158db2c Merged dg_cocoa files from all editions into core/app_cocoa_inter to eliminate annoying code duplication. 2010-02-03 11:55:53 +01:00
Virgil Dupras
65a17390c7 Corrected grammatical mistake in preferences panels. 2010-02-02 11:50:47 +01:00
Virgil Dupras
0e96f0917c core_pe.modules.block: Converted inttuple() to a vararg based function. 2010-01-31 12:41:28 +01:00
Virgil Dupras
3d62a7e64a Reorganized qt/pe/modules
--HG--
rename : qt/pe/modules/block/block.c => qt/pe/modules/block.c
rename : qt/pe/modules/block/setup.py => qt/pe/modules/setup.py
2010-01-31 12:25:34 +01:00
Virgil Dupras
962805936e ifdef'd min/max functions when compiled under VC. It seems that VC already defines them. 2010-01-31 11:05:13 +00:00
Virgil Dupras
967aeecf5b Removed "inline" directive from C modules (doesn't work in VC). 2010-01-31 11:33:26 +01:00
Virgil Dupras
348b039fa3 Removed references to Cython. 2010-01-31 11:25:47 +01:00
Virgil Dupras
6e9b1f4fa3 Converted qt/modules/block from Cython to C. 2010-01-31 11:24:51 +01:00
Virgil Dupras
f1d447d1aa Fixed core_pe's c modules licence notices. 2010-01-31 11:23:23 +01:00
Virgil Dupras
a7c6f47dbe Reorganized core_pe's module folder.
--HG--
rename : core_pe/modules/block/block.c => core_pe/modules/block.c
rename : core_pe/modules/cache/cache.c => core_pe/modules/cache.c
rename : core_pe/modules/cache/setup.py => core_pe/modules/setup.py
2010-01-31 10:32:02 +01:00
Virgil Dupras
0446e89bfe Converted core_pe's "block" module from Cython to C. 2010-01-31 10:27:59 +01:00
Virgil Dupras
e41457913f Fixed a memory leak in the cache module. 2010-01-31 10:12:26 +01:00
Virgil Dupras
cea1ec7641 core_pe: Aah, got it. Performance from the new cache module are now comparable to the old Cython based one. 2010-01-30 17:19:40 +01:00
Virgil Dupras
cc362deb87 core_pe: Tried to improve speed of that newly converted cache module. 2010-01-30 16:50:49 +01:00
Virgil Dupras
7ec64e8a3d core_pe: Re-implemented the "cache" cython module in C. I like coding in C from time to time... 2010-01-30 16:29:18 +01:00
Virgil Dupras
ff2461df9d Fix the configure script so that the --64bit flag works. 2010-01-20 15:22:25 +01:00
Virgil Dupras
192cd2733c Added tag me5.7.1 for changeset 2c454eca9ebe 2010-01-19 17:59:11 +01:00
Virgil Dupras
ecef95469d me v5.7.1 2010-01-19 12:38:30 +01:00
Virgil Dupras
55d30d5e4b Use hsutil.cocoa.signature in me/dg_cocoa.py, Added the Remove Dead Tracks menu item which was lost since 5.7 (how did I not notice that?) 2010-01-19 12:28:15 +01:00
Virgil Dupras
2d5502cc2f Explicited pyobjc imports. 2010-01-18 08:48:44 +01:00
Virgil Dupras
5cda4a1eb4 Updated the cocoalib subrepo 2010-01-17 12:41:19 +01:00
Virgil Dupras
812b914b70 Added support for 64 bit in the build script. 2010-01-15 11:47:40 +01:00
Virgil Dupras
9b870ad863 Converted int to NSInteger 2010-01-15 11:19:24 +01:00
Virgil Dupras
0f250ac92d Added tag pe1.8.1 for changeset 0e923897a338 2010-01-15 09:00:36 +01:00
Virgil Dupras
552e6b7836 pe v1.8.1 2010-01-15 07:24:40 +01:00
Virgil Dupras
28f70b281b [#80 state:fixed] Removed some old references to the hsfs system. 2010-01-14 16:33:27 +01:00
Virgil Dupras
32d9b573c0 Removed the test for threading support in Cache. That feature has been removed in the previous commit. 2010-01-14 16:17:38 +01:00
Virgil Dupras
fc76a843d5 Straightened out the blocks cache. Instead of having a single global threaded block cache in the app, there's just a cache path, and non-threaded caches are created when needed. Also, made Cache.clear() more robust (it will clear the cache even if the db is corrupted). 2010-01-14 16:14:26 +01:00
Virgil Dupras
06607aabb2 Added tag se2.9.1 for changeset 61c4101851bd 2010-01-13 18:03:05 +01:00
Virgil Dupras
a1edc0e4f1 Fixed packaging on windows. It didn't correctly find help files. 2010-01-13 16:02:59 +00:00
Virgil Dupras
787c5d2189 Fixed the build script so it correctly calls HelpIndexer on OS X 10.5 2010-01-13 16:18:13 +01:00
Virgil Dupras
492c577184 se v2.9.1 2010-01-13 16:06:59 +01:00
Virgil Dupras
f5d0e22dc7 qt.base.preferences.Preferences now subclasses qtlib.preferences.Preferences. 2010-01-13 15:25:15 +01:00
Virgil Dupras
dc5ba01f1e [#74 state:fixed] The value for small files threshold is sent to preferences even if the field doesn't lose focus. 2010-01-13 14:53:41 +01:00
Virgil Dupras
a31f6e68aa Updated qtlib subrepo. 2010-01-13 10:41:22 +01:00
Virgil Dupras
c95b356a99 [#81 state:fixed] Show a message dialog when a duplicate scan yields no result. 2010-01-13 10:39:27 +01:00
Virgil Dupras
b5e645cb10 Removed redudant license files. 2010-01-13 10:07:14 +01:00
Virgil Dupras
627e638251 [#77 state:fixed] Don't spend time comparing 2 ref files together. 2010-01-13 10:04:53 +01:00
Virgil Dupras
d2e2f337f6 Fixed core_me.tests.scanner_test which was broken. 2010-01-13 09:35:37 +01:00
Virgil Dupras
e6d4d44f15 Removed APPNAME and LIMIT_DESC consts from cocoa. 2010-01-13 09:30:10 +01:00
Virgil Dupras
55f4df19a9 Updated cocoalib subrepo 2010-01-13 09:03:00 +01:00
Virgil Dupras
9f006ec08a [#75 state:fixed] md5 hashes are now computed incrementally. 2010-01-13 08:59:44 +01:00
Virgil Dupras
d62ff40bed Removed svn keywords. 2010-01-02 16:52:18 +01:00
Virgil Dupras
da194007fb Improved the cocoa build process.
--HG--
rename : cocoa/me/py/dg_cocoa.py => cocoa/me/dg_cocoa.py
rename : cocoa/pe/py/dg_cocoa.py => cocoa/pe/dg_cocoa.py
rename : cocoa/se/py/dg_cocoa.py => cocoa/se/dg_cocoa.py
2010-01-01 21:42:52 +01:00
Virgil Dupras
d06ce0c748 Updated subrepo ref. 2010-01-01 21:15:51 +01:00
Virgil Dupras
c14fecb415 Changed copyright year to 2010 2010-01-01 21:11:34 +01:00
Virgil Dupras
99f7308a67 Added tag pe1.8.0 for changeset cbcf9c80fee4 2009-12-31 14:14:10 +01:00
Virgil Dupras
99ee45ba2d Added tag se2.9.0 for changeset adc73ccd14b1 2009-12-31 14:13:22 +01:00
Virgil Dupras
615f3f77a6 Added tag me5.7.0 for changeset 321d15e818cf 2009-12-31 14:12:24 +01:00
Virgil Dupras
12e4c00c5d Added tag before-tiger-drop for changeset a8f232f880b6 2009-12-31 14:10:58 +01:00
Virgil Dupras
d954cb468f Added tag pe1.7.8 for changeset 0ef0ca83b49a 2009-12-31 14:10:31 +01:00
Virgil Dupras
e0fadc7af5 Added tag me5.6.6 for changeset 0ef0ca83b49a 2009-12-31 14:10:10 +01:00
Virgil Dupras
c5e9fd99b8 Added tag se2.8.2 for changeset 0ef0ca83b49a 2009-12-31 14:09:28 +01:00
Virgil Dupras
7efbbb2153 Added .hgignore 2009-12-31 11:01:55 +01:00
Virgil Dupras
4eb505e24a Oops, should have had updated before adding that .hgsub. Had to merge. Mercurial newbie mistake I guess. 2009-12-31 10:55:24 +01:00
Virgil Dupras
b6b08cccd7 Added .hgsub 2009-12-31 10:52:19 +01:00
hsoft
70af8541da Fixed packaging, which didn't work on windows.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40286
2009-12-30 16:52:46 +00:00
hsoft
838f8ae352 Changed the build system (that commit is *huge*)
--HG--
rename : base/cocoa/AppDelegate.h => cocoa/base/AppDelegate.h
rename : base/cocoa/AppDelegate.m => cocoa/base/AppDelegate.m
rename : base/cocoa/Consts.h => cocoa/base/Consts.h
rename : base/cocoa/DetailsPanel.h => cocoa/base/DetailsPanel.h
rename : base/cocoa/DetailsPanel.m => cocoa/base/DetailsPanel.m
rename : base/cocoa/DirectoryPanel.h => cocoa/base/DirectoryPanel.h
rename : base/cocoa/DirectoryPanel.m => cocoa/base/DirectoryPanel.m
rename : base/cocoa/PyDupeGuru.h => cocoa/base/PyDupeGuru.h
rename : base/cocoa/ResultWindow.h => cocoa/base/ResultWindow.h
rename : base/cocoa/ResultWindow.m => cocoa/base/ResultWindow.m
rename : base/cocoa/dsa_pub.pem => cocoa/base/dsa_pub.pem
rename : base/cocoa/xib/DetailsPanel.xib => cocoa/base/xib/DetailsPanel.xib
rename : base/cocoa/xib/DirectoryPanel.xib => cocoa/base/xib/DirectoryPanel.xib
rename : base/cocoa/xib/MainMenu.xib => cocoa/base/xib/MainMenu.xib
rename : me/cocoa/AppDelegate.h => cocoa/me/AppDelegate.h
rename : me/cocoa/AppDelegate.m => cocoa/me/AppDelegate.m
rename : me/cocoa/Consts.h => cocoa/me/Consts.h
rename : me/cocoa/DetailsPanel.h => cocoa/me/DetailsPanel.h
rename : me/cocoa/DetailsPanel.m => cocoa/me/DetailsPanel.m
rename : me/cocoa/DirectoryPanel.h => cocoa/me/DirectoryPanel.h
rename : me/cocoa/DirectoryPanel.m => cocoa/me/DirectoryPanel.m
rename : me/cocoa/Info.plist => cocoa/me/Info.plist
rename : me/cocoa/PyDupeGuru.h => cocoa/me/PyDupeGuru.h
rename : me/cocoa/ResultWindow.h => cocoa/me/ResultWindow.h
rename : me/cocoa/ResultWindow.m => cocoa/me/ResultWindow.m
rename : me/cocoa/dupeguru.icns => cocoa/me/dupeguru.icns
rename : me/cocoa/dupeguru.xcodeproj/project.pbxproj => cocoa/me/dupeguru.xcodeproj/project.pbxproj
rename : me/cocoa/gen.py => cocoa/me/gen.py
rename : me/cocoa/main.m => cocoa/me/main.m
rename : me/cocoa/py/dg_cocoa.py => cocoa/me/py/dg_cocoa.py
rename : me/cocoa/py/setup.py => cocoa/me/py/setup.py
rename : me/cocoa/xib/Preferences.xib => cocoa/me/xib/Preferences.xib
rename : pe/cocoa/AppDelegate.h => cocoa/pe/AppDelegate.h
rename : pe/cocoa/AppDelegate.m => cocoa/pe/AppDelegate.m
rename : pe/cocoa/Consts.h => cocoa/pe/Consts.h
rename : pe/cocoa/DetailsPanel.h => cocoa/pe/DetailsPanel.h
rename : pe/cocoa/DetailsPanel.m => cocoa/pe/DetailsPanel.m
rename : pe/cocoa/DirectoryPanel.h => cocoa/pe/DirectoryPanel.h
rename : pe/cocoa/DirectoryPanel.m => cocoa/pe/DirectoryPanel.m
rename : pe/cocoa/Info.plist => cocoa/pe/Info.plist
rename : pe/cocoa/PictureBlocks.h => cocoa/pe/PictureBlocks.h
rename : pe/cocoa/PictureBlocks.m => cocoa/pe/PictureBlocks.m
rename : pe/cocoa/PyDupeGuru.h => cocoa/pe/PyDupeGuru.h
rename : pe/cocoa/ResultWindow.h => cocoa/pe/ResultWindow.h
rename : pe/cocoa/ResultWindow.m => cocoa/pe/ResultWindow.m
rename : pe/cocoa/dupeguru.icns => cocoa/pe/dupeguru.icns
rename : pe/cocoa/dupeguru.xcodeproj/project.pbxproj => cocoa/pe/dupeguru.xcodeproj/project.pbxproj
rename : pe/cocoa/gen.py => cocoa/pe/gen.py
rename : pe/cocoa/main.m => cocoa/pe/main.m
rename : pe/cocoa/py/dg_cocoa.py => cocoa/pe/py/dg_cocoa.py
rename : pe/cocoa/py/setup.py => cocoa/pe/py/setup.py
rename : pe/cocoa/xib/DetailsPanel.xib => cocoa/pe/xib/DetailsPanel.xib
rename : pe/cocoa/xib/Preferences.xib => cocoa/pe/xib/Preferences.xib
rename : se/cocoa/AppDelegate.h => cocoa/se/AppDelegate.h
rename : se/cocoa/AppDelegate.m => cocoa/se/AppDelegate.m
rename : se/cocoa/Consts.h => cocoa/se/Consts.h
rename : se/cocoa/DetailsPanel.h => cocoa/se/DetailsPanel.h
rename : se/cocoa/DetailsPanel.m => cocoa/se/DetailsPanel.m
rename : se/cocoa/DirectoryPanel.h => cocoa/se/DirectoryPanel.h
rename : se/cocoa/DirectoryPanel.m => cocoa/se/DirectoryPanel.m
rename : se/cocoa/Info.plist => cocoa/se/Info.plist
rename : se/cocoa/PyDupeGuru.h => cocoa/se/PyDupeGuru.h
rename : se/cocoa/ResultWindow.h => cocoa/se/ResultWindow.h
rename : se/cocoa/ResultWindow.m => cocoa/se/ResultWindow.m
rename : se/cocoa/dupeguru.icns => cocoa/se/dupeguru.icns
rename : se/cocoa/dupeguru.xcodeproj/project.pbxproj => cocoa/se/dupeguru.xcodeproj/project.pbxproj
rename : se/cocoa/gen.py => cocoa/se/gen.py
rename : se/cocoa/main.m => cocoa/se/main.m
rename : se/cocoa/py/dg_cocoa.py => cocoa/se/py/dg_cocoa.py
rename : se/cocoa/py/setup.py => cocoa/se/py/setup.py
rename : se/cocoa/xib/Preferences.xib => cocoa/se/xib/Preferences.xib
rename : base/core/LICENSE => core/LICENSE
rename : base/core/__init__.py => core/__init__.py
rename : base/core/app.py => core/app.py
rename : base/core/app_cocoa.py => core/app_cocoa.py
rename : base/core/data.py => core/data.py
rename : base/core/directories.py => core/directories.py
rename : base/core/engine.py => core/engine.py
rename : base/core/export.py => core/export.py
rename : base/core/fs.py => core/fs.py
rename : base/core/ignore.py => core/ignore.py
rename : base/core/results.py => core/results.py
rename : base/core/scanner.py => core/scanner.py
rename : base/core/tests/__init__.py => core/tests/__init__.py
rename : base/core/tests/app_cocoa_test.py => core/tests/app_cocoa_test.py
rename : base/core/tests/app_test.py => core/tests/app_test.py
rename : base/core/tests/data.py => core/tests/data.py
rename : base/core/tests/directories_test.py => core/tests/directories_test.py
rename : base/core/tests/engine_test.py => core/tests/engine_test.py
rename : base/core/tests/ignore_test.py => core/tests/ignore_test.py
rename : base/core/tests/results_test.py => core/tests/results_test.py
rename : base/core/tests/scanner_test.py => core/tests/scanner_test.py
rename : me/core/__init__.py => core_me/__init__.py
rename : me/core/app_cocoa.py => core_me/app_cocoa.py
rename : me/core/data.py => core_me/data.py
rename : me/core/fs.py => core_me/fs.py
rename : me/core/scanner.py => core_me/scanner.py
rename : me/core/tests/__init__.py => core_me/tests/__init__.py
rename : me/core/tests/scanner_test.py => core_me/tests/scanner_test.py
rename : pe/core/LICENSE => core_pe/LICENSE
rename : pe/core/__init__.py => core_pe/__init__.py
rename : pe/core/app_cocoa.py => core_pe/app_cocoa.py
rename : pe/core/block.py => core_pe/block.py
rename : pe/core/cache.py => core_pe/cache.py
rename : pe/core/data.py => core_pe/data.py
rename : pe/core/gen.py => core_pe/gen.py
rename : pe/core/matchbase.py => core_pe/matchbase.py
rename : pe/core/modules/block/block.pyx => core_pe/modules/block/block.pyx
rename : pe/core/modules/block/setup.py => core_pe/modules/block/setup.py
rename : pe/core/modules/cache/cache.pyx => core_pe/modules/cache/cache.pyx
rename : pe/core/modules/cache/setup.py => core_pe/modules/cache/setup.py
rename : pe/core/scanner.py => core_pe/scanner.py
rename : pe/core/tests/__init__.py => core_pe/tests/__init__.py
rename : pe/core/tests/block_test.py => core_pe/tests/block_test.py
rename : pe/core/tests/cache_test.py => core_pe/tests/cache_test.py
rename : se/core/LICENSE => core_se/LICENSE
rename : se/core/__init__.py => core_se/__init__.py
rename : se/core/app_cocoa.py => core_se/app_cocoa.py
rename : se/core/data.py => core_se/data.py
rename : se/core/fs.py => core_se/fs.py
rename : se/core/tests/__init__.py => core_se/tests/__init__.py
rename : se/core/tests/fs_test.py => core_se/tests/fs_test.py
rename : me/help/LICENSE => help_me/LICENSE
rename : me/help/__init__.py => help_me/__init__.py
rename : me/help/changelog.yaml => help_me/changelog.yaml
rename : me/help/gen.py => help_me/gen.py
rename : me/help/skeleton/hardcoded.css => help_me/skeleton/hardcoded.css
rename : me/help/skeleton/images/hs_title.png => help_me/skeleton/images/hs_title.png
rename : me/help/templates/base_dg.mako => help_me/templates/base_dg.mako
rename : me/help/templates/credits.mako => help_me/templates/credits.mako
rename : me/help/templates/directories.mako => help_me/templates/directories.mako
rename : me/help/templates/faq.mako => help_me/templates/faq.mako
rename : me/help/templates/intro.mako => help_me/templates/intro.mako
rename : me/help/templates/power_marker.mako => help_me/templates/power_marker.mako
rename : me/help/templates/preferences.mako => help_me/templates/preferences.mako
rename : me/help/templates/quick_start.mako => help_me/templates/quick_start.mako
rename : me/help/templates/results.mako => help_me/templates/results.mako
rename : me/help/templates/versions.mako => help_me/templates/versions.mako
rename : pe/help/LICENSE => help_pe/LICENSE
rename : pe/help/__init__.py => help_pe/__init__.py
rename : pe/help/changelog.yaml => help_pe/changelog.yaml
rename : pe/help/gen.py => help_pe/gen.py
rename : pe/help/skeleton/hardcoded.css => help_pe/skeleton/hardcoded.css
rename : pe/help/skeleton/images/hs_title.png => help_pe/skeleton/images/hs_title.png
rename : pe/help/templates/base_dg.mako => help_pe/templates/base_dg.mako
rename : pe/help/templates/credits.mako => help_pe/templates/credits.mako
rename : pe/help/templates/directories.mako => help_pe/templates/directories.mako
rename : pe/help/templates/faq.mako => help_pe/templates/faq.mako
rename : pe/help/templates/intro.mako => help_pe/templates/intro.mako
rename : pe/help/templates/power_marker.mako => help_pe/templates/power_marker.mako
rename : pe/help/templates/preferences.mako => help_pe/templates/preferences.mako
rename : pe/help/templates/quick_start.mako => help_pe/templates/quick_start.mako
rename : pe/help/templates/results.mako => help_pe/templates/results.mako
rename : pe/help/templates/versions.mako => help_pe/templates/versions.mako
rename : se/help/LICENSE => help_se/LICENSE
rename : se/help/changelog.yaml => help_se/changelog.yaml
rename : se/help/gen.py => help_se/gen.py
rename : se/help/skeleton/hardcoded.css => help_se/skeleton/hardcoded.css
rename : se/help/skeleton/images/hs_title.png => help_se/skeleton/images/hs_title.png
rename : se/help/templates/base_dg.mako => help_se/templates/base_dg.mako
rename : se/help/templates/credits.mako => help_se/templates/credits.mako
rename : se/help/templates/directories.mako => help_se/templates/directories.mako
rename : se/help/templates/faq.mako => help_se/templates/faq.mako
rename : se/help/templates/intro.mako => help_se/templates/intro.mako
rename : se/help/templates/power_marker.mako => help_se/templates/power_marker.mako
rename : se/help/templates/preferences.mako => help_se/templates/preferences.mako
rename : se/help/templates/quick_start.mako => help_se/templates/quick_start.mako
rename : se/help/templates/results.mako => help_se/templates/results.mako
rename : se/help/templates/versions.mako => help_se/templates/versions.mako
rename : base/qt/WARNING => qt/WARNING
rename : base/qt/__init__.py => qt/base/__init__.py
rename : base/qt/app.py => qt/base/app.py
rename : base/qt/details_table.py => qt/base/details_table.py
rename : base/qt/dg.qrc => qt/base/dg.qrc
rename : base/qt/directories_dialog.py => qt/base/directories_dialog.py
rename : base/qt/directories_dialog.ui => qt/base/directories_dialog.ui
rename : base/qt/directories_model.py => qt/base/directories_model.py
rename : base/qt/main_window.py => qt/base/main_window.py
rename : base/qt/main_window.ui => qt/base/main_window.ui
rename : base/qt/platform.py => qt/base/platform.py
rename : base/qt/platform_osx.py => qt/base/platform_osx.py
rename : base/qt/platform_win.py => qt/base/platform_win.py
rename : base/qt/preferences.py => qt/base/preferences.py
rename : base/qt/results_model.py => qt/base/results_model.py
rename : me/qt/app.py => qt/me/app.py
rename : me/qt/build.py => qt/me/build.py
rename : me/qt/details_dialog.py => qt/me/details_dialog.py
rename : me/qt/details_dialog.ui => qt/me/details_dialog.ui
rename : me/qt/dgme.spec => qt/me/dgme.spec
rename : me/qt/gen.py => qt/me/gen.py
rename : me/qt/installer.aip => qt/me/installer.aip
rename : me/qt/preferences.py => qt/me/preferences.py
rename : me/qt/preferences_dialog.py => qt/me/preferences_dialog.py
rename : me/qt/preferences_dialog.ui => qt/me/preferences_dialog.ui
rename : me/qt/profile.py => qt/me/profile.py
rename : me/qt/start.py => qt/me/start.py
rename : me/qt/verinfo => qt/me/verinfo
rename : pe/qt/app.py => qt/pe/app.py
rename : pe/qt/block.py => qt/pe/block.py
rename : pe/qt/build.py => qt/pe/build.py
rename : pe/qt/details_dialog.py => qt/pe/details_dialog.py
rename : pe/qt/details_dialog.ui => qt/pe/details_dialog.ui
rename : pe/qt/dgpe.spec => qt/pe/dgpe.spec
rename : pe/qt/gen.py => qt/pe/gen.py
rename : pe/qt/installer.aip => qt/pe/installer.aip
rename : pe/qt/main_window.py => qt/pe/main_window.py
rename : pe/qt/modules/block/block.pyx => qt/pe/modules/block/block.pyx
rename : pe/qt/modules/block/setup.py => qt/pe/modules/block/setup.py
rename : pe/qt/preferences.py => qt/pe/preferences.py
rename : pe/qt/preferences_dialog.py => qt/pe/preferences_dialog.py
rename : pe/qt/preferences_dialog.ui => qt/pe/preferences_dialog.ui
rename : pe/qt/profile.py => qt/pe/profile.py
rename : pe/qt/start.py => qt/pe/start.py
rename : pe/qt/verinfo => qt/pe/verinfo
rename : se/qt/app.py => qt/se/app.py
rename : se/qt/build.py => qt/se/build.py
rename : se/qt/details_dialog.py => qt/se/details_dialog.py
rename : se/qt/details_dialog.ui => qt/se/details_dialog.ui
rename : se/qt/dgse.spec => qt/se/dgse.spec
rename : se/qt/gen.py => qt/se/gen.py
rename : se/qt/installer.aip => qt/se/installer.aip
rename : se/qt/preferences.py => qt/se/preferences.py
rename : se/qt/preferences_dialog.py => qt/se/preferences_dialog.py
rename : se/qt/preferences_dialog.ui => qt/se/preferences_dialog.ui
rename : se/qt/profile.py => qt/se/profile.py
rename : se/qt/start.py => qt/se/start.py
rename : se/qt/verinfo => qt/se/verinfo
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40285
2009-12-30 16:34:41 +00:00
hsoft
5645515d90 me cocoa: fixed dead reference in project and broken external ref.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40284
2009-12-30 10:52:23 +00:00
hsoft
d114ffb2c4 pe cocoa: Fixed gen script and project dead references.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40283
2009-12-30 10:48:01 +00:00
hsoft
a9f9534ce6 pe qt: Changed a reference to "dupeguru_pe" to "core_pe" in gen script.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40282
2009-12-30 10:46:08 +00:00
hsoft
7dee2c67c6 se qt: updates externals to reference to "core_se" instead of "dupeguru_se".
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40281
2009-12-30 10:43:33 +00:00
hsoft
e18f8ba6d4 se help: updated FAQ.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40280
2009-12-30 10:43:00 +00:00
hsoft
4d44753f6e cocoa se: updated the project for 10.5-updated cocoalib.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40279
2009-12-30 10:40:48 +00:00
hsoft
f5accbfaed Changed dupeguru and dupeguru_* external references to core and core_* references.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40278
2009-12-30 10:37:57 +00:00
hsoft
6eba99eba1 Adjusted externals to the py --> core renames.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40277
2009-12-30 10:26:50 +00:00
hsoft
1e18a08998 Renamed base/py to base/core
--HG--
rename : base/py/LICENSE => base/core/LICENSE
rename : base/py/__init__.py => base/core/__init__.py
rename : base/py/app.py => base/core/app.py
rename : base/py/app_cocoa.py => base/core/app_cocoa.py
rename : base/py/data.py => base/core/data.py
rename : base/py/directories.py => base/core/directories.py
rename : base/py/engine.py => base/core/engine.py
rename : base/py/export.py => base/core/export.py
rename : base/py/fs.py => base/core/fs.py
rename : base/py/ignore.py => base/core/ignore.py
rename : base/py/results.py => base/core/results.py
rename : base/py/scanner.py => base/core/scanner.py
rename : base/py/tests/__init__.py => base/core/tests/__init__.py
rename : base/py/tests/app_cocoa_test.py => base/core/tests/app_cocoa_test.py
rename : base/py/tests/app_test.py => base/core/tests/app_test.py
rename : base/py/tests/data.py => base/core/tests/data.py
rename : base/py/tests/directories_test.py => base/core/tests/directories_test.py
rename : base/py/tests/engine_test.py => base/core/tests/engine_test.py
rename : base/py/tests/ignore_test.py => base/core/tests/ignore_test.py
rename : base/py/tests/results_test.py => base/core/tests/results_test.py
rename : base/py/tests/scanner_test.py => base/core/tests/scanner_test.py
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40276
2009-12-30 10:23:59 +00:00
hsoft
2526380184 Renamed me/py to me/core
--HG--
rename : me/py/__init__.py => me/core/__init__.py
rename : me/py/app_cocoa.py => me/core/app_cocoa.py
rename : me/py/data.py => me/core/data.py
rename : me/py/fs.py => me/core/fs.py
rename : me/py/scanner.py => me/core/scanner.py
rename : me/py/tests/__init__.py => me/core/tests/__init__.py
rename : me/py/tests/scanner_test.py => me/core/tests/scanner_test.py
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40275
2009-12-30 10:23:31 +00:00
hsoft
74dba7cb6c Renamed pe/py to pe/core
--HG--
rename : pe/py/LICENSE => pe/core/LICENSE
rename : pe/py/__init__.py => pe/core/__init__.py
rename : pe/py/app_cocoa.py => pe/core/app_cocoa.py
rename : pe/py/block.py => pe/core/block.py
rename : pe/py/cache.py => pe/core/cache.py
rename : pe/py/data.py => pe/core/data.py
rename : pe/py/gen.py => pe/core/gen.py
rename : pe/py/matchbase.py => pe/core/matchbase.py
rename : pe/py/modules/block/block.pyx => pe/core/modules/block/block.pyx
rename : pe/py/modules/block/setup.py => pe/core/modules/block/setup.py
rename : pe/py/modules/cache/cache.pyx => pe/core/modules/cache/cache.pyx
rename : pe/py/modules/cache/setup.py => pe/core/modules/cache/setup.py
rename : pe/py/scanner.py => pe/core/scanner.py
rename : pe/py/tests/__init__.py => pe/core/tests/__init__.py
rename : pe/py/tests/block_test.py => pe/core/tests/block_test.py
rename : pe/py/tests/cache_test.py => pe/core/tests/cache_test.py
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40274
2009-12-30 10:23:02 +00:00
hsoft
63aad8ca84 Renamed se/py to se/core
--HG--
rename : se/py/LICENSE => se/core/LICENSE
rename : se/py/__init__.py => se/core/__init__.py
rename : se/py/app_cocoa.py => se/core/app_cocoa.py
rename : se/py/data.py => se/core/data.py
rename : se/py/fs.py => se/core/fs.py
rename : se/py/tests/__init__.py => se/core/tests/__init__.py
rename : se/py/tests/fs_test.py => se/core/tests/fs_test.py
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40273
2009-12-30 10:22:33 +00:00
hsoft
b8bb40de62 dgme cocoa: v5.7.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40271
2009-12-18 13:04:30 +00:00
hsoft
67dff7fbf2 dgme qt: v5.7.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40270
2009-12-18 12:58:14 +00:00
hsoft
6e226f32fd dgme help: "packagified" help and updated to 5.7.0.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40269
2009-12-18 12:57:47 +00:00
hsoft
cf819dc0a8 dgme qt: fixed gen script and updated FAQ.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40268
2009-12-18 12:21:33 +00:00
hsoft
4f6af6e4dc dgpe cocoa: ugh... fixed typo
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40266
2009-12-16 16:16:22 +00:00
hsoft
a6d2a9b7b3 dgpe cocoa: Fixed a crash happening when iPhoto was never launched.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40265
2009-12-16 15:51:26 +00:00
hsoft
cf34164191 dg cocoa: Since there are problems with the latest pyobjc + py2app + Snow Leopard, I've got to stick with pyobjc1.4 for a while after all...
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40264
2009-12-16 14:48:37 +00:00
hsoft
9a7bb30df4 dgse cocoa: Since there are problems with the latest pyobjc + py2app + Snow Leopard, I've got to stick with pyobjc1.4 for a while after all...
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40263
2009-12-16 14:48:18 +00:00
hsoft
5dc78809b6 dgpe: oops, wrong release date for 1.8.0.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40262
2009-12-16 10:29:02 +00:00
hsoft
2b53a6e7d6 dgpe cocoa: removed the forgotten "-A" flag in bundle generation script.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40261
2009-12-16 10:10:18 +00:00
hsoft
eb82a35e5b dgpe cocoa v1.8.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40260
2009-12-16 09:54:51 +00:00
hsoft
51b14435e0 dgpe qt v1.8.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40259
2009-12-16 09:54:00 +00:00
hsoft
59de033523 [#79 state:fixed] Wrapped PIL's IOError into a warning logging.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40258
2009-12-15 16:54:47 +00:00
hsoft
c9b0a278ca [#78 state:fixed] Wrapped appscript errors, updated error message and the F.A.Q. to give users a clue of what to do.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40257
2009-12-15 16:23:02 +00:00
hsoft
b487189742 [#76 state:fixed] Added combobox painting for the selected item in Directories' State column.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40256
2009-12-15 14:09:13 +00:00
hsoft
d5a60b1580 dgpe cocoa: Made the help file generation process independent from the current work directory. Reverted XCode version of the project to 3.0 so that it can be compiled on Leopard.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40255
2009-12-15 12:52:21 +00:00
hsoft
e2665610e9 dgpe qt: Packagified help.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40254
2009-12-15 11:35:30 +00:00
hsoft
3262ee9938 dgbase: Changed ask_for_reg_code() to askForRegCode() to adapt to change in qtlib.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40253
2009-12-15 11:35:08 +00:00
hsoft
2f153003b3 dgpe help: packagified the help folder.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40252
2009-12-15 11:34:21 +00:00
552 changed files with 41635 additions and 35693 deletions

5
.ctags Normal file
View File

@@ -0,0 +1,5 @@
-R
--exclude=build
--exclude=env
--exclude=.tox
--python-kinds=-i

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
.DS_Store
__pycache__
*.so
*.mo
*.waf*
.lock-waf*
.tox
/tags
build
dist
env
/deps
cocoa/autogen
/run.py
/cocoa/*/Info.plist
/cocoa/*/build
/qt/*_rc.py
/help/*/conf.py
/help/*/changelog.rst

9
.gitmodules vendored Normal file
View File

@@ -0,0 +1,9 @@
[submodule "qtlib"]
path = qtlib
url = https://github.com/hsoft/qtlib.git
[submodule "hscommon"]
path = hscommon
url = https://github.com/hsoft/hscommon.git
[submodule "cocoalib"]
path = cocoalib
url = https://github.com/hsoft/cocoalib.git

21
.tx/config Normal file
View File

@@ -0,0 +1,21 @@
[main]
host = https://www.transifex.com
[dupeguru.core]
file_filter = locale/<lang>/LC_MESSAGES/core.po
source_file = locale/core.pot
source_lang = en
type = PO
[dupeguru.columns]
file_filter = locale/<lang>/LC_MESSAGES/columns.po
source_file = locale/columns.pot
source_lang = en
type = PO
[dupeguru.ui]
file_filter = locale/<lang>/LC_MESSAGES/ui.po
source_file = locale/ui.pot
source_lang = en
type = PO

19
CREDITS Normal file
View File

@@ -0,0 +1,19 @@
To know who contributed to dupeGuru, you can look at the commit log, but not all contributions
result in a commit. This file lists contributors who don't necessarily appear in the commit log.
* Jérôme Cantin, Main icon
* Gregor Tätzner, German localization
* Frank Weber, German localization
* Eric Dee, Chinese localization
* Aleš Nehyba, Czech localization
* Paolo Rossi, Italian localization
* Hrant Ohanyan, Armenian localization
* Igor Pavlov, Russian localization
* Kyrill Detinov, Russian localization
* Yuri Petrashko, Ukrainian localization
* Nickolas Pohilets, Ukrainian localization
* Victor Figueiredo, Brazilian localization
* Phan Anh, Vietnamese localization
* Gabriel Koutilellis, Greek localization
Thanks!

622
LICENSE Normal file
View File

@@ -0,0 +1,622 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS

111
Makefile Normal file
View File

@@ -0,0 +1,111 @@
PYTHON ?= python3
REQ_MINOR_VERSION = 4
PREFIX ?= /usr/local
# If you're installing into a path that is not going to be the final path prefix (such as a
# sandbox), set DESTDIR to that path.
# Our build scripts are not very "make like" yet and perform their task in a bundle. For now, we
# use one of each file to act as a representative, a target, of these groups.
submodules_target = hscommon/__init__.py
packages = hscommon qtlib core qt
localedirs = $(wildcard locale/*/LC_MESSAGES)
pofiles = $(wildcard locale/*/LC_MESSAGES/*.po)
mofiles = $(patsubst %.po,%.mo,$(pofiles))
vpath %.po $(localedirs)
vpath %.mo $(localedirs)
all : | run.py
@echo "Build complete! You can run dupeGuru with 'make run'"
run.py : | env i18n modules qt/dg_rc.py
cp qt/run_template.py run.py
run: | run.py
./env/bin/python run.py
pyc:
${PYTHON} -m compileall ${packages}
reqs :
@ret=`${PYTHON} -c "import sys; print(int(sys.version_info[:2] >= (3, ${REQ_MINOR_VERSION})))"`; \
if [ $${ret} -ne 1 ]; then \
echo "Python 3.${REQ_MINOR_VERSION}+ required. Aborting."; \
exit 1; \
fi
@${PYTHON} -m venv -h > /dev/null || \
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
@${PYTHON} -c 'import PyQt5' >/dev/null 2>&1 || \
{ echo "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
# Ensure that submodules are initialized
$(submodules_target) :
git submodule init
git submodule update
env : | $(submodules_target) reqs
@echo "Creating our virtualenv"
${PYTHON} -m venv env --system-site-packages
./env/bin/python -m pip install -r requirements.txt
build/help : | env
./env/bin/python build.py --doc
qt/dg_rc.py : qt/dg.qrc
pyrcc5 qt/dg.qrc > qt/dg_rc.py
i18n: $(mofiles)
%.mo : %.po
msgfmt -o $@ $<
core/pe/_block.*.so : core/pe/modules/block.c core/pe/modules/common.c | env
./env/bin/python hscommon/build_ext.py $^ _block
mv _block.*.so core/pe
core/pe/_cache.*.so : core/pe/modules/cache.c core/pe/modules/common.c | env
./env/bin/python hscommon/build_ext.py $^ _cache
mv _cache.*.so core/pe
qt/pe/_block_qt.*.so : qt/pe/modules/block.c | env
./env/bin/python hscommon/build_ext.py $^ _block_qt
mv _block_qt.*.so qt/pe
modules : core/pe/_block.*.so core/pe/_cache.*.so qt/pe/_block_qt.*.so
mergepot :
./env/bin/python build.py --mergepot
normpo :
./env/bin/python build.py --normpo
srcpkg :
./scripts/srcpkg.sh
install: build/help | all pyc
mkdir -p ${DESTDIR}${PREFIX}/share/dupeguru
cp -rf ${packages} locale build/help ${DESTDIR}${PREFIX}/share/dupeguru
cp -f run.py ${DESTDIR}${PREFIX}/share/dupeguru/run.py
chmod 755 ${DESTDIR}${PREFIX}/share/dupeguru/run.py
mkdir -p ${DESTDIR}${PREFIX}/bin
ln -sf ${PREFIX}/share/dupeguru/run.py ${DESTDIR}${PREFIX}/bin/dupeguru
mkdir -p ${DESTDIR}${PREFIX}/share/applications
cp -f pkg/dupeguru.desktop ${DESTDIR}${PREFIX}/share/applications
mkdir -p ${DESTDIR}${PREFIX}/share/pixmaps
cp -f images/dgse_logo_128.png ${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png
uninstall :
rm -rf "${DESTDIR}${PREFIX}/share/dupeguru"
rm -f "${DESTDIR}${PREFIX}/bin/dupeguru"
rm -f "${DESTDIR}${PREFIX}/share/applications/dupeguru.desktop"
rm -f "${DESTDIR}${PREFIX}/share/pixmaps/dupeguru.png"
clean:
-rm run.py
-rm -rf build
-rm locale/*/LC_MESSAGES/*.mo
-rm core/pe/*.so qt/pe/*.so
.PHONY : clean srcpkg normpo mergepot modules i18n reqs run pyc install uninstall all

170
README.md Normal file
View File

@@ -0,0 +1,170 @@
# dupeGuru
[dupeGuru][dupeguru] is a cross-platform (Linux and OS X) GUI tool to find duplicate files in
a system. It's written mostly in Python 3 and has the peculiarity of using
[multiple GUI toolkits][cross-toolkit], all using the same core Python code. On OS X, the UI layer
is written in Objective-C and uses Cocoa. On Linux, it's written in Python and uses Qt5.
## Current status: People wanted
dupeGuru has currently only one maintainer, me. This is a dangerous situation that needs to be
corrected.
The goal is to eventually have another active maintainer, but before we can get there, the project
needs more contributors. It is very much lacking on that side right now.
Whatever your skills, if you are remotely interestested in being a contributor, I'm interested in
mentoring you. If that's the case, please refer to [the open ticket on the subject][contrib-issue]
and let's get started.
### Slowed development
Until I manage to find contributors, I'm slowing the development pace of dupeGuru. I'm not much
interested in maintaining it alone, I personally have no use for this app (it's been a *loooong*,
time since I had dupe problems :) )
I don't want to let it die, however, so I will still do normal maintainership, that is, issue
triaging, code review, critical bugfixes, releases management.
But anything non-critical, I'm not going to implement it myself because I see every issue as a
contribution opportunity.
### Windows maintainer wanted
As [described on my website][nowindows], dupeGuru v4.0 dropped Windows support
because there isn't anyone to bear the burden of Windows maintenance. If
you're a Windows developer and are interested in taking this task, [don't
hesitate to let me know][contrib-issue].
### OS X maintainer wanted
My Mac Mini is already a couple of years old and is likely to be my last Apple purchase. When it
dies, I will be unable maintain the OS X version of moneyGuru. I've already stopped paying for the
Mac Developer membership so I can't sign the apps anymore (in the "official way" I mean. The
download is still PGP signed) If you're a Mac developer and are interested in taking this task,
[don't hesitate to let me know][contrib-issue].
## Contents of this folder
This folder contains the source for dupeGuru. Its documentation is in `help`, but is also
[available online][documentation] in its built form. Here's how this source tree is organised:
* core: Contains the core logic code for dupeGuru. It's Python code.
* cocoa: UI code for the Cocoa toolkit. It's Objective-C code.
* qt: UI code for the Qt toolkit. It's written in Python and uses PyQt.
* images: Images used by the different UI codebases.
* pkg: Skeleton files required to create different packages
* help: Help document, written for Sphinx.
* locale: .po files for localisation.
There are also other sub-folder that comes from external repositories and are part of this repo as
git submodules:
* hscommon: A collection of helpers used across HS applications.
* cocoalib: A collection of helpers used across Cocoa UI codebases of HS applications.
* qtlib: A collection of helpers used across Qt UI codebases of HS applications.
## How to build dupeGuru from source
### make
If you're on linux, you can build the ap for local development with `make`:
$ make
$ make run
The `Makefile` is a recent addition, however. You might have to fallback to the legacy build
scripts.
### Legacy build
If you're on OS X or that if the `make` method didn't work, you can build dupeGuru with the
legacy scripts.
There's a bootstrap script that will make building very easy. There might be some things that you
need to install manually on your system, but the bootstrap script will tell you when what you need
to install. You can run the bootstrap with:
$ ./bootstrap.sh
and follow instructions from the script.
### Prerequisites installation
Prerequisites are installed through `pip`. However, some of them are not "pip installable" and have
to be installed manually.
* All systems: [Python 3.4+][python]
* Mac OS X: OS X 10.10+ with XCode command line tools.
* Linux: PyQt5
On Ubuntu (14.04+), the apt-get command to install all pre-requisites is:
$ apt-get install python3-dev python3-pyqt5 pyqt5-dev-tools python3-venv
### OS X and pyenv
[pyenv][pyenv] is a popular way to manage multiple python versions. However, be aware that dupeGuru
will not compile with a pyenv's python unless it's been built with `--enable-framework`. You can do
this with:
$ env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.4.3
### Setting up the virtual environment
*This is done automatically by the bootstrap script. This is a reference in case you need to do it
manually.*
Use Python's built-in `pyvenv` to create a virtual environment in which we're going to install our.
Python-related dependencies. In that environment, we then install our requirements with pip.
For Linux (`--system-site-packages` is to be able to import PyQt):
$ pyvenv --system-site-packages env
$ source env/bin/activate
$ pip install -r requirements.txt
For OS X:
$ pyvenv env
$ source env/bin/activate
$ pip install -r requirements-osx.txt
### Actual building and running
With your virtualenv activated, you can build and run dupeGuru with these commands:
$ python build.py
$ python run.py
You can also package dupeGuru into an installable package with:
$ python package.py
### Generate Ubuntu packages
$ bash -c "pyvenv --system-site-packages env && source env/bin/activate && pip install -r requirements.txt && python3 build.py --clean && python3 package.py"
### Running tests
The complete test suite is ran with [Tox 1.7+][tox]. If you have it installed system-wide, you
don't even need to set up a virtualenv. Just `cd` into the root project folder and run `tox`.
If you don't have Tox system-wide, install it in your virtualenv with `pip install tox` and then
run `tox`.
You can also run automated tests without Tox. Extra requirements for running tests are in
`requirements-extra.txt`. So, you can do `pip install -r requirements-extra.txt` inside your
virtualenv and then `py.test core hscommon`
[dupeguru]: http://www.hardcoded.net/dupeguru/
[cross-toolkit]: http://www.hardcoded.net/articles/cross-toolkit-software
[contrib-issue]: https://github.com/hsoft/dupeguru/issues/300
[nowindows]: https://www.hardcoded.net/archive2015#2015-11-01
[documentation]: http://www.hardcoded.net/dupeguru/help/en/
[python]: http://www.python.org/
[pyqt]: http://www.riverbankcomputing.com
[pyenv]: https://github.com/yyuu/pyenv
[tox]: https://tox.readthedocs.org/en/latest/

View File

@@ -1,30 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "RecentDirectories.h"
#import "PyDupeGuru.h"
#import "ResultWindow.h"
#import "DetailsPanel.h"
@interface AppDelegateBase : NSObject
{
IBOutlet PyDupeGuruBase *py;
IBOutlet RecentDirectories *recentDirectories;
IBOutlet NSMenuItem *unlockMenuItem;
IBOutlet ResultWindowBase *result;
NSString *_appName;
DetailsPanelBase *_detailsPanel;
}
- (IBAction)unlockApp:(id)sender;
- (PyDupeGuruBase *)py;
- (RecentDirectories *)recentDirectories;
- (DetailsPanelBase *)detailsPanel; // Virtual
@end

View File

@@ -1,59 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import "AppDelegate.h"
#import "ProgressController.h"
#import "RegistrationInterface.h"
#import "Utils.h"
#import "Consts.h"
@implementation AppDelegateBase
- (id)init
{
self = [super init];
_appName = @"";
return self;
}
- (IBAction)unlockApp:(id)sender
{
if ([[self py] isRegistered])
return;
RegistrationInterface *ri = [[RegistrationInterface alloc] initWithApp:[self py] name:_appName limitDescription:LIMIT_DESC];
if ([ri enterCode] == NSOKButton)
{
NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@!",_appName];
[unlockMenuItem setTitle:menuTitle];
}
[ri release];
}
- (PyDupeGuruBase *)py { return py; }
- (RecentDirectories *)recentDirectories { return recentDirectories; }
- (DetailsPanelBase *)detailsPanel { return nil; } // Virtual
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[ProgressController mainProgressController] setWorker:py];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
//Restore Columns
NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"];
NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"];
if ([columnsOrder count])
[result restoreColumnsPosition:columnsOrder widths:columnsWidth];
else
[result resetColumnsToDefault:nil];
//Reg stuff
if ([RegistrationInterface showNagWithApp:[self py] name:_appName limitDescription:LIMIT_DESC])
[unlockMenuItem setTitle:[NSString stringWithFormat:@"Thanks for buying %@!",_appName]];
//Restore results
[py loadIgnoreList];
[py loadResults];
}
@end

View File

@@ -1,28 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#define DuplicateSelectionChangedNotification @"DuplicateSelectionChangedNotification"
/* ResultsChangedNotification happens on major changes, which requires a complete reload of the data*/
#define ResultsChangedNotification @"ResultsChangedNotification"
/* ResultsChangedNotification happens on minor changes, which requires buffer flush*/
#define ResultsUpdatedNotification @"ResultsUpdatedNotification"
#define ResultsMarkingChangedNotification @"ResultsMarkingChangedNotification"
#define RegistrationRequired @"RegistrationRequired"
#define JobStarted @"JobStarted"
#define JobInProgress @"JobInProgress"
#define jobLoad @"job_load"
#define jobScan @"job_scan"
#define jobCopy @"job_copy"
#define jobMove @"job_move"
#define jobDelete @"job_delete"
#define DEMO_MAX_ACTION_COUNT 10
#define LIMIT_DESC @"In the demo version, only 10 duplicates per session can be sent to Trash, moved or copied."

View File

@@ -1,25 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "PyApp.h"
#import "Table.h"
@interface DetailsPanelBase : NSWindowController
{
IBOutlet TableView *detailsTable;
}
- (id)initWithPy:(PyApp *)aPy;
- (void)refresh;
- (void)toggleVisibility;
/* Notifications */
- (void)duplicateSelectionChanged:(NSNotification *)aNotification;
@end

View File

@@ -1,44 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import "DetailsPanel.h"
#import "Consts.h"
@implementation DetailsPanelBase
- (id)initWithPy:(PyApp *)aPy
{
self = [super initWithWindowNibName:@"DetailsPanel"];
[self window]; //So the detailsTable is initialized.
[detailsTable setPy:aPy];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil];
return self;
}
- (void)refresh
{
[detailsTable reloadData];
}
- (void)toggleVisibility
{
if ([[self window] isVisible])
[[self window] close];
else
{
[self refresh]; // selection might have changed since last time
[[self window] orderFront:nil];
}
}
/* Notifications */
- (void)duplicateSelectionChanged:(NSNotification *)aNotification
{
if ([[self window] isVisible])
[self refresh];
}
@end

View File

@@ -1,42 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "RecentDirectories.h"
#import "Outline.h"
#import "PyDupeGuru.h"
@interface DirectoryOutline : OutlineView
{
}
@end
@protocol DirectoryOutlineDelegate
- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory;
@end
@interface DirectoryPanelBase : NSWindowController
{
IBOutlet NSPopUpButton *addButtonPopUp;
IBOutlet DirectoryOutline *directories;
IBOutlet NSButton *removeButton;
PyDupeGuruBase *_py;
RecentDirectories *_recentDirectories;
}
- (id)initWithParentApp:(id)aParentApp;
- (IBAction)askForDirectory:(id)sender;
- (IBAction)changeDirectoryState:(id)sender;
- (IBAction)popupAddDirectoryMenu:(id)sender;
- (IBAction)removeSelectedDirectory:(id)sender;
- (IBAction)toggleVisible:(id)sender;
- (void)addDirectory:(NSString *)directory;
- (void)refreshRemoveButtonText;
@end

View File

@@ -1,231 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import "DirectoryPanel.h"
#import "Dialogs.h"
#import "Utils.h"
#import "AppDelegate.h"
@implementation DirectoryOutline
- (void)doInit
{
[super doInit];
[self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
}
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
{
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [info draggingSourceOperationMask];
pboard = [info draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType])
{
if (sourceDragMask & NSDragOperationLink)
return NSDragOperationLink;
}
return NSDragOperationNone;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index
{
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [info draggingSourceOperationMask];
pboard = [info draggingPasteboard];
if ( [[pboard types] containsObject:NSFilenamesPboardType] )
{
NSArray *filenames = [pboard propertyListForType:NSFilenamesPboardType];
if (!(sourceDragMask & NSDragOperationLink))
return NO;
if (([self delegate] == nil) || (![[self delegate] respondsToSelector:@selector(outlineView:addDirectory:)]))
return NO;
for (NSString *filename in filenames)
[[self delegate] outlineView:self addDirectory:filename];
}
return YES;
}
@end
@implementation DirectoryPanelBase
- (id)initWithParentApp:(id)aParentApp
{
self = [super initWithWindowNibName:@"DirectoryPanel"];
[self window];
AppDelegateBase *app = aParentApp;
_py = [app py];
_recentDirectories = [app recentDirectories];
[directories setPy:_py];
NSPopUpButtonCell *cell = [[directories tableColumnWithIdentifier:@"1"] dataCell];
[cell addItemWithTitle:@"Normal"];
[cell addItemWithTitle:@"Reference"];
[cell addItemWithTitle:@"Excluded"];
for (int i=0;i<[[cell itemArray] count];i++)
{
NSMenuItem *mi = [[cell itemArray] objectAtIndex:i];
[mi setTarget:self];
[mi setAction:@selector(changeDirectoryState:)];
[mi setTag:i];
}
[self refreshRemoveButtonText];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directorySelectionChanged:) name:NSOutlineViewSelectionDidChangeNotification object:directories];
return self;
}
/* Actions */
- (IBAction)askForDirectory:(id)sender
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:YES];
[op setAllowsMultipleSelection:NO];
[op setTitle:@"Select a directory to add to the scanning list"];
[op setDelegate:self];
if ([op runModalForTypes:nil] == NSOKButton)
{
NSString *directory = [[op filenames] objectAtIndex:0];
[self addDirectory:directory];
}
}
- (IBAction)changeDirectoryState:(id)sender
{
OVNode *node = [directories itemAtRow:[directories clickedRow]];
[_py setDirectory:p2a([node indexPath]) state:i2n([sender tag])];
[node resetAllBuffers];
[directories reloadItem:node reloadChildren:YES];
[directories display];
}
- (IBAction)popupAddDirectoryMenu:(id)sender
{
if ([[_recentDirectories directories] count] == 0)
{
[self askForDirectory:sender];
return;
}
NSMenu *m = [addButtonPopUp menu];
while ([m numberOfItems] > 0)
[m removeItemAtIndex:0];
NSMenuItem *mi = [m addItemWithTitle:@"Add New Directory..." action:@selector(askForDirectory:) keyEquivalent:@""];
[mi setTarget:self];
[m addItem:[NSMenuItem separatorItem]];
[_recentDirectories fillMenu:m];
[addButtonPopUp selectItem:nil];
[[addButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]];
}
- (IBAction)removeSelectedDirectory:(id)sender
{
[[self window] makeKeyAndOrderFront:nil];
if ([directories selectedRow] < 0)
return;
OVNode *node = [directories itemAtRow:[directories selectedRow]];
if ([node level] == 1)
{
[_py removeDirectory:i2n([node index])];
[directories reloadData];
}
else
{
int state = n2i([[node buffer] objectAtIndex:1]);
int newState = state == 2 ? 0 : 2; // If excluded, put it back
[_py setDirectory:p2a([node indexPath]) state:i2n(newState)];
[node resetAllBuffers];
[directories display];
}
[self refreshRemoveButtonText];
}
- (IBAction)toggleVisible:(id)sender
{
[[self window] makeKeyAndOrderFront:nil];
}
/* Public */
- (void)addDirectory:(NSString *)directory
{
int r = [[_py addDirectory:directory] intValue];
if (r)
{
NSString *m;
switch (r)
{
case 1:
{
m = @"This directory already is in the list.";
break;
}
case 2:
{
m = @"This directory does not exist.";
break;
}
}
[Dialogs showMessage:m];
}
[directories reloadData];
[_recentDirectories addDirectory:directory];
[[self window] makeKeyAndOrderFront:nil];
}
- (void)refreshRemoveButtonText
{
if ([directories selectedRow] < 0)
{
[removeButton setEnabled:NO];
return;
}
[removeButton setEnabled:YES];
OVNode *node = [directories itemAtRow:[directories selectedRow]];
int state = n2i([[node buffer] objectAtIndex:1]);
NSString *buttonText = state == 2 ? @"Put Back" : @"Remove";
[removeButton setTitle:buttonText];
}
/* Delegate */
- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory
{
[self addDirectory:directory];
}
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
OVNode *node = item;
int state = n2i([[node buffer] objectAtIndex:1]);
if ([cell isKindOfClass:[NSTextFieldCell class]])
{
NSTextFieldCell *textCell = cell;
if (state == 1)
[textCell setTextColor:[NSColor blueColor]];
else if (state == 2)
[textCell setTextColor:[NSColor redColor]];
else
[textCell setTextColor:[NSColor blackColor]];
}
}
- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path
{
BOOL isdir;
[[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isdir];
return isdir;
}
/* Notifications */
- (void)directorySelectionChanged:(NSNotification *)aNotification
{
[self refreshRemoveButtonText];
}
@end

View File

@@ -1,11 +0,0 @@
Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
* If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,66 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "PyApp.h"
@interface PyDupeGuruBase : PyApp
//Actions
- (NSNumber *)addDirectory:(NSString *)name;
- (void)removeDirectory:(NSNumber *)index;
- (void)setDirectory:(NSArray *)indexPath state:(NSNumber *)state;
- (void)loadResults;
- (void)saveResults;
- (void)loadIgnoreList;
- (void)saveIgnoreList;
- (void)clearIgnoreList;
- (void)purgeIgnoreList;
- (NSString *)exportToXHTMLwithColumns:(NSArray *)aColIds;
- (NSNumber *)doScan;
- (NSArray *)selectedPowerMarkerNodePaths;
- (void)selectPowerMarkerNodePaths:(NSArray *)aIndexPaths;
- (NSArray *)selectedResultNodePaths;
- (void)selectResultNodePaths:(NSArray *)aIndexPaths;
- (void)toggleSelectedMark;
- (void)markAll;
- (void)markInvert;
- (void)markNone;
- (void)addSelectedToIgnoreList;
- (void)refreshDetailsWithSelected;
- (void)removeSelected;
- (void)openSelected;
- (NSNumber *)renameSelected:(NSString *)aNewName;
- (void)revealSelected;
- (void)makeSelectedReference;
- (void)applyFilter:(NSString *)filter;
- (void)sortGroupsBy:(NSNumber *)aIdentifier ascending:(NSNumber *)aAscending;
- (void)sortDupesBy:(NSNumber *)aIdentifier ascending:(NSNumber *)aAscending;
- (void)copyOrMove:(NSNumber *)aCopy markedTo:(NSString *)destination recreatePath:(NSNumber *)aRecreateType;
- (void)deleteMarked;
- (void)removeMarked;
//Data
- (NSNumber *)getIgnoreListCount;
- (NSNumber *)getMarkCount;
- (NSString *)getStatLine;
- (NSNumber *)getOperationalErrorCount;
//Scanning options
- (void)setMinMatchPercentage:(NSNumber *)percentage;
- (void)setMixFileKind:(NSNumber *)mix_file_kind;
- (void)setDisplayDeltaValues:(NSNumber *)display_delta_values;
- (void)setEscapeFilterRegexp:(NSNumber *)escape_filter_regexp;
- (void)setRemoveEmptyFolders:(NSNumber *)remove_empty_folders;
- (void)setSizeThreshold:(int)size_threshold;
@end

View File

@@ -1,63 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "Outline.h"
#import "PyDupeGuru.h"
@interface MatchesView : OutlineView
- (void)keyDown:(NSEvent *)theEvent;
@end
@interface ResultWindowBase : NSWindowController
{
@protected
IBOutlet PyDupeGuruBase *py;
IBOutlet id app;
IBOutlet NSSegmentedControl *deltaSwitch;
IBOutlet MatchesView *matches;
IBOutlet NSSegmentedControl *pmSwitch;
IBOutlet NSTextField *stats;
IBOutlet NSMenu *columnsMenu;
BOOL _powerMode;
BOOL _displayDelta;
NSMutableArray *_resultColumns;
NSWindowController *preferencesPanel;
}
/* Helpers */
- (void)fillColumnsMenu;
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn;
- (NSArray *)getColumnsOrder;
- (NSDictionary *)getColumnsWidth;
- (NSArray *)getSelected:(BOOL)aDupesOnly;
- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly;
- (void)initResultColumns;
- (void)updatePySelection;
- (void)performPySelection:(NSArray *)aIndexPaths;
- (void)refreshStats;
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth;
/* Actions */
- (IBAction)changeDelta:(id)sender;
- (IBAction)changePowerMarker:(id)sender;
- (IBAction)copyMarked:(id)sender;
- (IBAction)deleteMarked:(id)sender;
- (IBAction)expandAll:(id)sender;
- (IBAction)exportToXHTML:(id)sender;
- (IBAction)moveMarked:(id)sender;
- (IBAction)resetColumnsToDefault:(id)sender;
- (IBAction)showPreferencesPanel:(id)sender;
- (IBAction)switchSelected:(id)sender;
- (IBAction)toggleColumn:(id)sender;
- (IBAction)toggleDetailsPanel:(id)sender;
- (IBAction)togglePowerMarker:(id)sender;
/* Notifications */
- (void)jobCompleted:(NSNotification *)aNotification;
@end

View File

@@ -1,460 +0,0 @@
/*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/hs_license
*/
#import "ResultWindow.h"
#import "Dialogs.h"
#import "ProgressController.h"
#import "Utils.h"
#import "RegistrationInterface.h"
#import "AppDelegate.h"
#import "Consts.h"
@implementation MatchesView
- (void)keyDown:(NSEvent *)theEvent
{
unichar key = [[theEvent charactersIgnoringModifiers] characterAtIndex:0];
// get flags and strip the lower 16 (device dependant) bits
unsigned int flags = ( [theEvent modifierFlags] & 0x00FF );
if (((key == NSDeleteFunctionKey) || (key == NSDeleteCharacter)) && (flags == 0))
[self sendAction:@selector(removeSelected:) to:[self delegate]];
else
if ((key == 0x20) && (flags == 0)) // Space
[self sendAction:@selector(markSelected:) to:[self delegate]];
else
[super keyDown:theEvent];
}
- (void)outlineView:(NSOutlineView *)outlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
{
if (![[tableColumn identifier] isEqual:@"0"])
return; //We only want to cover renames.
OVNode *node = item;
NSString *oldName = [[node buffer] objectAtIndex:0];
NSString *newName = object;
if (![newName isEqual:oldName])
{
BOOL renamed = n2b([(PyDupeGuruBase *)py renameSelected:newName]);
if (renamed)
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
else
[Dialogs showMessage:[NSString stringWithFormat:@"The name '%@' already exists.",newName]];
}
}
@end
@implementation ResultWindowBase
- (void)awakeFromNib
{
[self window];
preferencesPanel = [[NSWindowController alloc] initWithWindowNibName:@"Preferences"];
[self initResultColumns];
[self fillColumnsMenu];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registrationRequired:) name:RegistrationRequired object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobInProgress:) name:JobInProgress object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsUpdated:) name:ResultsUpdatedNotification object:nil];
}
- (void)dealloc
{
[preferencesPanel release];
[super dealloc];
}
/* Helpers */
- (void)fillColumnsMenu
{
// The columns menu is supposed to be empty and initResultColumns must have been called
for (NSTableColumn *col in _resultColumns)
{
NSMenuItem *mi = [columnsMenu addItemWithTitle:[[col headerCell] stringValue] action:@selector(toggleColumn:) keyEquivalent:@""];
[mi setTag:[[col identifier] integerValue]];
[mi setTarget:self];
if ([[matches tableColumns] containsObject:col])
[mi setState:NSOnState];
}
[columnsMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem *mi = [columnsMenu addItemWithTitle:@"Reset to Default" action:@selector(resetColumnsToDefault:) keyEquivalent:@""];
[mi setTarget:self];
}
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn
{
NSNumber *n = [NSNumber numberWithInt:aIdentifier];
NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]];
[col setWidth:aWidth];
[col setEditable:NO];
[[col dataCell] setFont:[[aColumn dataCell] font]];
[[col headerCell] setStringValue:aTitle];
[col setResizingMask:NSTableColumnUserResizingMask];
[col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]];
return col;
}
//Returns an array of identifiers, in order.
- (NSArray *)getColumnsOrder
{
NSTableColumn *col;
NSString *colId;
NSMutableArray *result = [NSMutableArray array];
NSEnumerator *e = [[matches tableColumns] objectEnumerator];
while (col = [e nextObject])
{
colId = [col identifier];
[result addObject:colId];
}
return result;
}
- (NSDictionary *)getColumnsWidth
{
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSTableColumn *col;
NSString *colId;
NSNumber *width;
NSEnumerator *e = [[matches tableColumns] objectEnumerator];
while (col = [e nextObject])
{
colId = [col identifier];
width = [NSNumber numberWithFloat:[col width]];
[result setObject:width forKey:colId];
}
return result;
}
- (NSArray *)getSelected:(BOOL)aDupesOnly
{
if (_powerMode)
aDupesOnly = NO;
NSIndexSet *indexes = [matches selectedRowIndexes];
NSMutableArray *nodeList = [NSMutableArray array];
OVNode *node;
int i = [indexes firstIndex];
while (i != NSNotFound)
{
node = [matches itemAtRow:i];
if (!aDupesOnly || ([node level] > 1))
[nodeList addObject:node];
i = [indexes indexGreaterThanIndex:i];
}
return nodeList;
}
- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly
{
NSMutableArray *r = [NSMutableArray array];
NSArray *selected = [self getSelected:aDupesOnly];
NSEnumerator *e = [selected objectEnumerator];
OVNode *node;
while (node = [e nextObject])
[r addObject:p2a([node indexPath])];
return r;
}
- (void)initResultColumns
{
// Virtual
}
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth
{
NSTableColumn *col;
NSString *colId;
NSNumber *width;
NSMenuItem *mi;
//Remove all columns
NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator];
while (mi = [e nextObject])
{
if ([mi state] == NSOnState)
[self toggleColumn:mi];
}
//Add columns and set widths
e = [aColumnsOrder objectEnumerator];
while (colId = [e nextObject])
{
if (![colId isEqual:@"mark"])
{
col = [_resultColumns objectAtIndex:[colId intValue]];
width = [aColumnsWidth objectForKey:[col identifier]];
mi = [columnsMenu itemWithTag:[colId intValue]];
if (width)
[col setWidth:[width floatValue]];
[self toggleColumn:mi];
}
}
}
- (void)updatePySelection
{
NSArray *selection;
if (_powerMode)
selection = [py selectedPowerMarkerNodePaths];
else
selection = [py selectedResultNodePaths];
[matches selectNodePaths:selection];
}
- (void)performPySelection:(NSArray *)aIndexPaths
{
if (_powerMode)
[py selectPowerMarkerNodePaths:aIndexPaths];
else
[py selectResultNodePaths:aIndexPaths];
}
- (void)refreshStats
{
[stats setStringValue:[py getStatLine]];
}
/* Actions */
- (IBAction)changeDelta:(id)sender
{
_displayDelta = [deltaSwitch selectedSegment] == 1;
[py setDisplayDeltaValues:b2n(_displayDelta)];
[matches reloadData];
[self expandAll:nil];
}
- (IBAction)changePowerMarker:(id)sender
{
_powerMode = [pmSwitch selectedSegment] == 1;
if (_powerMode)
[matches setTag:2];
else
[matches setTag:0];
[self expandAll:nil];
[self outlineView:matches didClickTableColumn:nil];
[self updatePySelection];
}
- (IBAction)copyMarked:(id)sender
{
int mark_count = [[py getMarkCount] intValue];
if (!mark_count)
return;
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:NO];
[op setCanChooseDirectories:YES];
[op setCanCreateDirectories:YES];
[op setAllowsMultipleSelection:NO];
[op setTitle:@"Select a directory to copy marked files to"];
if ([op runModalForTypes:nil] == NSOKButton)
{
NSString *directory = [[op filenames] objectAtIndex:0];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py copyOrMove:b2n(YES) markedTo:directory recreatePath:[ud objectForKey:@"recreatePathType"]];
}
}
- (IBAction)deleteMarked:(id)sender
{
int mark_count = [[py getMarkCount] intValue];
if (!mark_count)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to send %d files to Trash. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO
return;
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setRemoveEmptyFolders:[ud objectForKey:@"removeEmptyFolders"]];
[py deleteMarked];
}
- (IBAction)expandAll:(id)sender
{
for (int i=0;i < [matches numberOfRows];i++)
[matches expandItem:[matches itemAtRow:i]];
}
- (IBAction)exportToXHTML:(id)sender
{
NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder]];
[[NSWorkspace sharedWorkspace] openFile:exported];
}
- (IBAction)moveMarked:(id)sender
{
int mark_count = [[py getMarkCount] intValue];
if (!mark_count)
return;
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:NO];
[op setCanChooseDirectories:YES];
[op setCanCreateDirectories:YES];
[op setAllowsMultipleSelection:NO];
[op setTitle:@"Select a directory to move marked files to"];
if ([op runModalForTypes:nil] == NSOKButton)
{
NSString *directory = [[op filenames] objectAtIndex:0];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setRemoveEmptyFolders:[ud objectForKey:@"removeEmptyFolders"]];
[py copyOrMove:b2n(NO) markedTo:directory recreatePath:[ud objectForKey:@"recreatePathType"]];
}
}
- (IBAction)resetColumnsToDefault:(id)sender
{
// Virtual
}
- (IBAction)showPreferencesPanel:(id)sender
{
[preferencesPanel showWindow:sender];
}
- (IBAction)switchSelected:(id)sender
{
// It might look like a complicated way to get the length of the current dupe list on the py side
// but after a lot of fussing around, believe it or not, it actually is.
int matchesTag = _powerMode ? 2 : 0;
int startLen = [[py getOutlineView:matchesTag childCountsForPath:[NSArray array]] count];
[self performPySelection:[self getSelectedPaths:YES]];
[py makeSelectedReference];
// In some cases (when in a filtered view in Power Marker mode, it's possible that the demoted
// ref is not a part of the filter, making the table smaller. In those cases, we want to do a
// complete reload of the table to avoid a crash.
if ([[py getOutlineView:matchesTag childCountsForPath:[NSArray array]] count] == startLen)
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsUpdatedNotification object:self];
else
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)toggleColumn:(id)sender
{
NSMenuItem *mi = sender;
NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]];
NSTableColumn *col = [matches tableColumnWithIdentifier:colId];
if (col == nil)
{
//Add Column
col = [_resultColumns objectAtIndex:[mi tag]];
[matches addTableColumn:col];
[mi setState:NSOnState];
}
else
{
//Remove column
[matches removeTableColumn:col];
[mi setState:NSOffState];
}
}
- (IBAction)toggleDetailsPanel:(id)sender
{
[[(AppDelegateBase *)app detailsPanel] toggleVisibility];
}
- (IBAction)togglePowerMarker:(id)sender
{
if ([pmSwitch selectedSegment] == 1)
[pmSwitch setSelectedSegment:0];
else
[pmSwitch setSelectedSegment:1];
[self changePowerMarker:sender];
}
/* Delegate */
- (void)outlineView:(NSOutlineView *)outlineView didClickTableColumn:(NSTableColumn *)tableColumn
{
if ([[outlineView sortDescriptors] count] < 1)
return;
NSSortDescriptor *sd = [[outlineView sortDescriptors] objectAtIndex:0];
if (_powerMode)
[py sortDupesBy:i2n([[sd key] intValue]) ascending:b2n([sd ascending])];
else
[py sortGroupsBy:i2n([[sd key] intValue]) ascending:b2n([sd ascending])];
[matches reloadData];
[self expandAll:nil];
}
/* Notifications */
- (void)windowWillClose:(NSNotification *)aNotification
{
[NSApp hide:NSApp];
}
- (void)jobCompleted:(NSNotification *)aNotification
{
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
int r = n2i([py getOperationalErrorCount]);
id lastAction = [[ProgressController mainProgressController] jobId];
if ([lastAction isEqualTo:jobCopy])
{
if (r > 0)
[Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be copied.",r]];
else
[Dialogs showMessage:@"All marked files were copied sucessfully."];
}
if ([lastAction isEqualTo:jobMove])
{
if (r > 0)
[Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be moved. They were kept in the results, and still are marked.",r]];
else
[Dialogs showMessage:@"All marked files were moved sucessfully."];
}
if ([lastAction isEqualTo:jobDelete])
{
if (r > 0)
[Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be sent to Trash. They were kept in the results, and still are marked.",r]];
else
[Dialogs showMessage:@"All marked files were sucessfully sent to Trash."];
}
// Re-activate toolbar items right after the progress bar stops showing instead of waiting until
// a mouse-over is performed
[[[self window] toolbar] validateVisibleItems];
}
- (void)jobInProgress:(NSNotification *)aNotification
{
[Dialogs showMessage:@"A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."];
}
- (void)jobStarted:(NSNotification *)aNotification
{
NSDictionary *ui = [aNotification userInfo];
NSString *desc = [ui valueForKey:@"desc"];
[[ProgressController mainProgressController] setJobDesc:desc];
NSString *jobid = [ui valueForKey:@"jobid"];
// NSLog(jobid);
[[ProgressController mainProgressController] setJobId:jobid];
[[ProgressController mainProgressController] showSheetForParent:[self window]];
}
- (void)registrationRequired:(NSNotification *)aNotification
{
NSString *msg = @"This is a demo version, which only allows you 10 delete/copy/move actions per session. You cannot continue.";
[Dialogs showMessage:msg];
}
- (void)resultsChanged:(NSNotification *)aNotification
{
[matches reloadData];
[self expandAll:nil];
[self outlineViewSelectionDidChange:nil];
[self refreshStats];
}
- (void)resultsUpdated:(NSNotification *)aNotification
{
[matches invalidateBuffers];
[matches invalidateMarkings];
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
{
return ![[ProgressController mainProgressController] isShown];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
return ![[ProgressController mainProgressController] isShown];
}
@end

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@

View File

@@ -1,249 +0,0 @@
#!/usr/bin/env python
# Created By: Virgil Dupras
# Created On: 2006/11/11
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import unicode_literals
import os
import os.path as op
import logging
from hsutil import io, files
from hsutil.path import Path
from hsutil.reg import RegistrableApplication, RegistrationRequired
from hsutil.misc import flatten, first
from hsutil.str import escape
from . import directories, results, scanner, export, fs
JOB_SCAN = 'job_scan'
JOB_LOAD = 'job_load'
JOB_MOVE = 'job_move'
JOB_COPY = 'job_copy'
JOB_DELETE = 'job_delete'
class NoScannableFileError(Exception):
pass
class AllFilesAreRefError(Exception):
pass
class DupeGuru(RegistrableApplication):
def __init__(self, data_module, appdata, appid):
RegistrableApplication.__init__(self, appid)
self.appdata = appdata
if not op.exists(self.appdata):
os.makedirs(self.appdata)
self.data = data_module
self.directories = directories.Directories()
self.results = results.Results(data_module)
self.scanner = scanner.Scanner()
self.action_count = 0
self.last_op_error_count = 0
self.options = {
'escape_filter_regexp': True,
'clean_empty_dirs': False,
}
def _demo_check(self):
if self.registered:
return
count = self.results.mark_count
if count + self.action_count > 10:
raise RegistrationRequired()
else:
self.action_count += count
def _do_delete(self, j):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe)
j.start_job(self.results.mark_count)
self.last_op_error_count = self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe):
if not io.exists(dupe.path):
return True
self._recycle_dupe(dupe)
self.clean_empty_dirs(dupe.path[:-1])
if not io.exists(dupe.path):
return True
logging.warning("Could not send {0} to trash.".format(unicode(dupe.path)))
return False
def _do_load(self, j):
self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml'))
j = j.start_subjob([1, 9])
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
files = flatten(g[:] for g in self.results.groups)
for file in j.iter_with_progress(files, 'Reading metadata %d/%d'):
file._read_all_info(attrnames=self.data.METADATA_TO_READ)
def _get_display_info(self, dupe, group, delta=False):
if (dupe is None) or (group is None):
return ['---'] * len(self.data.COLUMNS)
try:
return self.data.GetDisplayInfo(dupe, group, delta)
except Exception as e:
logging.warning("Exception on GetDisplayInfo for %s: %s", unicode(dupe.path), unicode(e))
return ['---'] * len(self.data.COLUMNS)
def _get_file(self, str_path):
path = Path(str_path)
return fs.get_file(path, self.directories.fileclasses)
@staticmethod
def _recycle_dupe(dupe):
raise NotImplementedError()
def _start_job(self, jobid, func):
# func(j)
raise NotImplementedError()
def add_directory(self, d):
try:
self.directories.add_path(Path(d))
return 0
except directories.AlreadyThereError:
return 1
except directories.InvalidPathError:
return 2
def add_to_ignore_list(self, dupe):
g = self.results.get_group_of_duplicate(dupe)
for other in g:
if other is not dupe:
self.scanner.ignore_list.Ignore(unicode(other.path), unicode(dupe.path))
def apply_filter(self, filter):
self.results.apply_filter(None)
if self.options['escape_filter_regexp']:
filter = escape(filter, '()[]\\.|+?^')
filter = escape(filter, '*', '.')
self.results.apply_filter(filter)
def clean_empty_dirs(self, path):
if self.options['clean_empty_dirs']:
while files.delete_if_empty(path, ['.DS_Store']):
path = path[:-1]
def copy_or_move(self, dupe, copy, destination, dest_type):
"""
copy: True = Copy False = Move
destination: string.
dest_type: 0 = right in destination.
1 = relative re-creation.
2 = absolute re-creation.
"""
source_path = dupe.path
location_path = first(p for p in self.directories if dupe.path in p)
dest_path = Path(destination)
if dest_type == 2:
dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename
elif dest_type == 1:
dest_path = dest_path + source_path[location_path:-1]
try:
if not io.exists(dest_path):
io.makedirs(dest_path)
if copy:
files.copy(source_path, dest_path)
else:
files.move(source_path, dest_path)
self.clean_empty_dirs(source_path[:-1])
except EnvironmentError as e:
operation = 'Copy' if copy else 'Move'
logging.warning('%s operation failed on %s. Error: %s' % (operation, unicode(dupe.path), unicode(e)))
return False
return True
def copy_or_move_marked(self, copy, destination, recreate_path):
def do(j):
def op(dupe):
j.add_progress()
return self.copy_or_move(dupe, copy, destination, recreate_path)
j.start_job(self.results.mark_count)
self.last_op_error_count = self.results.perform_on_marked(op, not copy)
self._demo_check()
jobid = JOB_COPY if copy else JOB_MOVE
self._start_job(jobid, do)
def delete_marked(self):
self._demo_check()
self._start_job(JOB_DELETE, self._do_delete)
def export_to_xhtml(self, column_ids):
column_ids = [colid for colid in column_ids if colid.isdigit()]
column_ids = map(int, column_ids)
column_ids.sort()
colnames = [col['display'] for i, col in enumerate(self.data.COLUMNS) if i in column_ids]
rows = []
for group in self.results.groups:
for dupe in group:
data = self._get_display_info(dupe, group)
row = [data[colid] for colid in column_ids]
row.insert(0, dupe is not group.ref)
rows.append(row)
return export.export_to_xhtml(colnames, rows)
def load(self):
self._start_job(JOB_LOAD, self._do_load)
self.load_ignore_list()
def load_ignore_list(self):
p = op.join(self.appdata, 'ignore_list.xml')
self.scanner.ignore_list.load_from_xml(p)
def make_reference(self, duplicates):
changed_groups = set()
for dupe in duplicates:
g = self.results.get_group_of_duplicate(dupe)
if g not in changed_groups:
self.results.make_ref(dupe)
changed_groups.add(g)
def save(self):
try:
self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml'))
self.results.save_to_xml(op.join(self.appdata, 'last_results.xml'))
except LookupError:
# This is that weird issue from #39 that sometimes happens when auto-updating with
# Sparkle. Just ignore it.
pass
def save_ignore_list(self):
p = op.join(self.appdata, 'ignore_list.xml')
self.scanner.ignore_list.save_to_xml(p)
def start_scanning(self):
def do(j):
j.set_progress(0, 'Collecting files to scan')
files = list(self.directories.get_files())
logging.info('Scanning %d files' % len(files))
self.results.groups = self.scanner.GetDupeGroups(files, j)
files = self.directories.get_files()
first_file = first(files)
if first_file is None:
raise NoScannableFileError()
if first_file.is_ref and all(f.is_ref for f in files):
raise AllFilesAreRefError()
self.results.groups = []
self._start_job(JOB_SCAN, do)
#--- Properties
@property
def stat_line(self):
result = self.results.stat_line
if self.scanner.discarded_file_count:
result = '%s (%d discarded)' % (result, self.scanner.discarded_file_count)
return result

View File

@@ -1,302 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/11/11
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from Foundation import *
from AppKit import *
import logging
import os.path as op
from hsutil import io, cocoa, job
from hsutil.cocoa import install_exception_hook
from hsutil.misc import stripnone
from hsutil.reg import RegistrationRequired
from . import app, fs
JOBID2TITLE = {
app.JOB_SCAN: "Scanning for duplicates",
app.JOB_LOAD: "Loading",
app.JOB_MOVE: "Moving",
app.JOB_COPY: "Copying",
app.JOB_DELETE: "Sending to Trash",
}
def demo_method(method):
def wrapper(self, *args, **kwargs):
try:
return method(self, *args, **kwargs)
except RegistrationRequired:
NSNotificationCenter.defaultCenter().postNotificationName_object_('RegistrationRequired', self)
return wrapper
class DupeGuru(app.DupeGuru):
def __init__(self, data_module, appdata_subdir, appid):
LOGGING_LEVEL = logging.DEBUG if NSUserDefaults.standardUserDefaults().boolForKey_('debug') else logging.WARNING
logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s')
logging.debug('started in debug mode')
install_exception_hook()
appsupport = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0]
appdata = op.join(appsupport, appdata_subdir)
app.DupeGuru.__init__(self, data_module, appdata, appid)
self.progress = cocoa.ThreadedJobPerformer()
self.display_delta_values = False
self.selected_dupes = []
self.RefreshDetailsTable(None,None)
#--- Override
@staticmethod
def _recycle_dupe(dupe):
directory = unicode(dupe.path[:-1])
filename = dupe.name
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
NSWorkspaceRecycleOperation, directory, '', [filename], None)
def _start_job(self, jobid, func):
try:
j = self.progress.create_job()
self.progress.run_threaded(func, args=(j, ))
except job.JobInProgressError:
NSNotificationCenter.defaultCenter().postNotificationName_object_('JobInProgress', self)
else:
ud = {'desc': JOBID2TITLE[jobid], 'jobid':jobid}
NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_('JobStarted', self, ud)
#---Helpers
def GetObjects(self,node_path):
#returns a tuple g,d
try:
g = self.results.groups[node_path[0]]
if len(node_path) == 2:
return (g,self.results.groups[node_path[0]].dupes[node_path[1]])
else:
return (g,None)
except IndexError:
return (None,None)
def get_folder_path(self, node_path, curr_path=None):
if not node_path:
return curr_path
current_index = node_path[0]
if curr_path is None:
curr_path = self.directories[current_index]
else:
curr_path = self.directories.get_subfolders(curr_path)[current_index]
return self.get_folder_path(node_path[1:], curr_path)
def RefreshDetailsTable(self,dupe,group):
l1 = self._get_display_info(dupe, group, False)
# we don't want the two sides of the table to display the stats for the same file
ref = group.ref if group is not None and group.ref is not dupe else None
l2 = self._get_display_info(ref, group, False)
names = [c['display'] for c in self.data.COLUMNS]
self.details_table = zip(names,l1,l2)
#---Public
def AddSelectedToIgnoreList(self):
for dupe in self.selected_dupes:
self.add_to_ignore_list(dupe)
copy_or_move_marked = demo_method(app.DupeGuru.copy_or_move_marked)
delete_marked = demo_method(app.DupeGuru.delete_marked)
def MakeSelectedReference(self):
self.make_reference(self.selected_dupes)
def OpenSelected(self):
if self.selected_dupes:
path = unicode(self.selected_dupes[0].path)
NSWorkspace.sharedWorkspace().openFile_(path)
def PurgeIgnoreList(self):
self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s))
def RefreshDetailsWithSelected(self):
if self.selected_dupes:
self.RefreshDetailsTable(
self.selected_dupes[0],
self.results.get_group_of_duplicate(self.selected_dupes[0])
)
else:
self.RefreshDetailsTable(None,None)
def RemoveDirectory(self,index):
try:
del self.directories[index]
except IndexError:
pass
def RemoveSelected(self):
self.results.remove_duplicates(self.selected_dupes)
def RenameSelected(self, newname):
try:
d = self.selected_dupes[0]
d.rename(newname)
return True
except (IndexError, fs.FSError) as e:
logging.warning("dupeGuru Warning: %s" % unicode(e))
return False
def RevealSelected(self):
if self.selected_dupes:
path = unicode(self.selected_dupes[0].path)
NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path,'')
def start_scanning(self):
self.RefreshDetailsTable(None, None)
try:
app.DupeGuru.start_scanning(self)
return 0
except app.NoScannableFileError:
return 3
except app.AllFilesAreRefError:
return 1
def selected_result_node_paths(self):
def get_path(dupe):
try:
group = self.results.get_group_of_duplicate(dupe)
groupindex = self.results.groups.index(group)
if dupe is group.ref:
return [groupindex]
dupeindex = group.dupes.index(dupe)
return [groupindex, dupeindex]
except ValueError: # dupe not in there
return None
dupes = self.selected_dupes
return stripnone(get_path(dupe) for dupe in dupes)
def selected_powermarker_node_paths(self):
def get_path(dupe):
try:
dupeindex = self.results.dupes.index(dupe)
return [dupeindex]
except ValueError: # dupe not in there
return None
dupes = self.selected_dupes
return stripnone(get_path(dupe) for dupe in dupes)
def SelectResultNodePaths(self,node_paths):
def extract_dupe(t):
g,d = t
if d is not None:
return d
else:
if g is not None:
return g.ref
selected = [extract_dupe(self.GetObjects(p)) for p in node_paths]
self.selected_dupes = [dupe for dupe in selected if dupe is not None]
def SelectPowerMarkerNodePaths(self,node_paths):
rows = [p[0] for p in node_paths]
self.selected_dupes = [
self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes))
]
def SetDirectoryState(self, node_path, state):
p = self.get_folder_path(node_path)
self.directories.set_state(p, state)
def sort_dupes(self,key,asc):
self.results.sort_dupes(key,asc,self.display_delta_values)
def sort_groups(self,key,asc):
self.results.sort_groups(key,asc)
def ToggleSelectedMarkState(self):
for dupe in self.selected_dupes:
self.results.mark_toggle(dupe)
#---Data
def GetOutlineViewMaxLevel(self, tag):
if tag == 0:
return 2
elif tag == 1:
return 0
elif tag == 2:
return 1
def GetOutlineViewChildCounts(self, tag, node_path):
if self.progress._job_running:
return []
if tag == 0: #Normal results
assert not node_path # no other value is possible
return [len(g.dupes) for g in self.results.groups]
elif tag == 1: #Directories
try:
if node_path:
path = self.get_folder_path(node_path)
subfolders = self.directories.get_subfolders(path)
else:
subfolders = self.directories
return [len(self.directories.get_subfolders(path)) for path in subfolders]
except IndexError: # node_path out of range
return []
else: #Power Marker
assert not node_path # no other value is possible
return [0 for d in self.results.dupes]
def GetOutlineViewValues(self, tag, node_path):
if self.progress._job_running:
return
if not node_path:
return
if tag in (0,2): #Normal results / Power Marker
if tag == 0:
g, d = self.GetObjects(node_path)
if d is None:
d = g.ref
else:
d = self.results.dupes[node_path[0]]
g = self.results.get_group_of_duplicate(d)
result = self._get_display_info(d, g, self.display_delta_values)
return result
elif tag == 1: #Directories
try:
path = self.get_folder_path(node_path)
name = unicode(path) if len(node_path) == 1 else path[-1]
return [name, self.directories.get_state(path)]
except IndexError: # node_path out of range
return []
def GetOutlineViewMarked(self, tag, node_path):
# 0=unmarked 1=marked 2=unmarkable
if self.progress._job_running:
return
if not node_path:
return 2
if tag == 1: #Directories
return 2
if tag == 0: #Normal results
g, d = self.GetObjects(node_path)
else: #Power Marker
d = self.results.dupes[node_path[0]]
if (d is None) or (not self.results.is_markable(d)):
return 2
elif self.results.is_marked(d):
return 1
else:
return 0
def GetTableViewCount(self, tag):
if self.progress._job_running:
return 0
return len(self.details_table)
def GetTableViewMarkedIndexes(self,tag):
return []
def GetTableViewValues(self,tag,row):
return self.details_table[row]

View File

@@ -1,42 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/03/15
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsutil.str import format_time, FT_DECIMAL, format_size
import time
def format_path(p):
return unicode(p[:-1])
def format_timestamp(t, delta):
if delta:
return format_time(t, FT_DECIMAL)
else:
if t > 0:
return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(t))
else:
return '---'
def format_words(w):
def do_format(w):
if isinstance(w, list):
return '(%s)' % ', '.join(do_format(item) for item in w)
else:
return w.replace('\n', ' ')
return ', '.join(do_format(item) for item in w)
def format_perc(p):
return "%0.0f" % p
def format_dupe_count(c):
return str(c) if c else '---'
def cmp_value(value):
return value.lower() if isinstance(value, basestring) else value

View File

@@ -1,173 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/02/27
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import xml.dom.minidom
from hsutil import io
from hsutil.files import FileOrPath
from hsutil.path import Path
from . import fs
(STATE_NORMAL,
STATE_REFERENCE,
STATE_EXCLUDED) = range(3)
class AlreadyThereError(Exception):
"""The path being added is already in the directory list"""
class InvalidPathError(Exception):
"""The path being added is invalid"""
class Directories(object):
#---Override
def __init__(self, fileclasses=[fs.File]):
self._dirs = []
self.states = {}
self.fileclasses = fileclasses
def __contains__(self, path):
for p in self._dirs:
if path in p:
return True
return False
def __delitem__(self,key):
self._dirs.__delitem__(key)
def __getitem__(self,key):
return self._dirs.__getitem__(key)
def __len__(self):
return len(self._dirs)
#---Private
def _default_state_for_path(self, path):
# Override this in subclasses to specify the state of some special folders.
if path[-1].startswith('.'): # hidden
return STATE_EXCLUDED
def _get_files(self, from_path):
state = self.get_state(from_path)
if state == STATE_EXCLUDED:
# Recursively get files from folders with lots of subfolder is expensive. However, there
# might be a subfolder in this path that is not excluded. What we want to do is to skim
# through self.states and see if we must continue, or we can stop right here to save time
if not any(p[:len(from_path)] == from_path for p in self.states):
return
try:
filepaths = set()
if state != STATE_EXCLUDED:
for file in fs.get_files(from_path, fileclasses=self.fileclasses):
file.is_ref = state == STATE_REFERENCE
filepaths.add(file.path)
yield file
subpaths = [from_path + name for name in io.listdir(from_path)]
# 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 subpaths if not io.islink(p) and io.isdir(p) and p not in filepaths]
for subfolder in subfolders:
for file in self._get_files(subfolder):
yield file
except (EnvironmentError, fs.InvalidPath):
pass
#---Public
def add_path(self, path):
"""Adds 'path' to self, if not already there.
Raises AlreadyThereError if 'path' is already in self. If path is a directory containing
some of the directories already present in self, 'path' will be added, but all directories
under it will be removed. Can also raise InvalidPathError if 'path' does not exist.
"""
if path in self:
raise AlreadyThereError()
if not io.exists(path):
raise InvalidPathError()
self._dirs = [p for p in self._dirs if p not in path]
self._dirs.append(path)
@staticmethod
def get_subfolders(path):
"""returns a sorted list of paths corresponding to subfolders in `path`"""
try:
names = [name for name in io.listdir(path) if io.isdir(path + name)]
names.sort(key=lambda x:x.lower())
return [path + name for name in names]
except EnvironmentError:
return []
def get_files(self):
"""Returns a list of all files that are not excluded.
Returned files also have their 'is_ref' attr set.
"""
for path in self._dirs:
for file in self._get_files(path):
yield file
def get_state(self, path):
"""Returns the state of 'path' (One of the STATE_* const.)
"""
if path in self.states:
return self.states[path]
default_state = self._default_state_for_path(path)
if default_state is not None:
return default_state
parent = path[:-1]
if parent in self:
return self.get_state(parent)
else:
return STATE_NORMAL
def load_from_file(self, infile):
try:
doc = xml.dom.minidom.parse(infile)
except:
return
root_path_nodes = doc.getElementsByTagName('root_directory')
for rdn in root_path_nodes:
if not rdn.getAttributeNode('path'):
continue
path = rdn.getAttributeNode('path').nodeValue
try:
self.add_path(Path(path))
except (AlreadyThereError, InvalidPathError):
pass
state_nodes = doc.getElementsByTagName('state')
for sn in state_nodes:
if not (sn.getAttributeNode('path') and sn.getAttributeNode('value')):
continue
path = sn.getAttributeNode('path').nodeValue
state = sn.getAttributeNode('value').nodeValue
self.set_state(Path(path), int(state))
def save_to_file(self,outfile):
with FileOrPath(outfile, 'wb') as fp:
doc = xml.dom.minidom.Document()
root = doc.appendChild(doc.createElement('directories'))
for root_path in self:
root_path_node = root.appendChild(doc.createElement('root_directory'))
root_path_node.setAttribute('path', unicode(root_path).encode('utf-8'))
for path, state in self.states.iteritems():
state_node = root.appendChild(doc.createElement('state'))
state_node.setAttribute('path', unicode(path).encode('utf-8'))
state_node.setAttribute('value', str(state))
doc.writexml(fp, '\t', '\t', '\n', encoding='utf-8')
def set_state(self, path, state):
if self.get_state(path) == state:
return
# we don't want to needlessly fill self.states. if get_state returns the same thing
# without an explicit entry, remove that entry
if path in self.states:
del self.states[path]
if self.get_state(path) == state: # no need for an entry
return
self.states[path] = state

View File

@@ -1,138 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/09/16
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import tempfile
import os.path as op
from tempfile import mkdtemp
# Yes, this is a very low-tech solution, but at least it doesn't have all these annoying dependency
# and resource problems.
MAIN_TEMPLATE = u"""
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<title>dupeGuru Results</title>
<style type="text/css">
BODY
{
background-color:white;
}
BODY,A,P,UL,TABLE,TR,TD
{
font-family:Tahoma,Arial,sans-serif;
font-size:10pt;
color: #4477AA;
}
TABLE
{
background-color: #225588;
margin-left: auto;
margin-right: auto;
width: 90%;
}
TR
{
background-color: white;
}
TH
{
font-weight: bold;
color: black;
background-color: #C8D6E5;
}
TH TD
{
color:black;
}
TD
{
padding-left: 2pt;
}
TD.rightelem
{
text-align:right;
/*padding-left:0pt;*/
padding-right: 2pt;
width: 17%;
}
TD.indented
{
padding-left: 12pt;
}
H1
{
font-family:&quot;Courier New&quot;,monospace;
color:#6699CC;
font-size:18pt;
color:#6da500;
border-color: #70A0CF;
border-width: 1pt;
border-style: solid;
margin-top: 16pt;
margin-left: 5%;
margin-right: 5%;
padding-top: 2pt;
padding-bottom:2pt;
text-align: center;
}
</style>
</head>
<body>
<h1>dupeGuru Results</h1>
<table>
<tr>$colheaders</tr>
$rows
</table>
</body>
</html>
"""
COLHEADERS_TEMPLATE = u"<th>{name}</th>"
ROW_TEMPLATE = u"""
<tr>
<td class="{indented}">{filename}</td>{cells}
</tr>
"""
CELL_TEMPLATE = u"""<td>{value}</td>"""
def export_to_xhtml(colnames, rows):
# a row is a list of values with the first value being a flag indicating if the row should be indented
if rows:
assert len(rows[0]) == len(colnames) + 1 # + 1 is for the "indented" flag
colheaders = u''.join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames)
rendered_rows = []
for row in rows:
# [2:] is to remove the indented flag + filename
indented = u'indented' if row[0] else u''
filename = row[1]
cells = u''.join(CELL_TEMPLATE.format(value=value) for value in row[2:])
rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells))
rendered_rows = u''.join(rendered_rows)
# The main template can't use format because the css code uses {}
content = MAIN_TEMPLATE.replace('$colheaders', colheaders).replace('$rows', rendered_rows)
folder = mkdtemp()
destpath = op.join(folder, u'export.htm')
fp = open(destpath, 'w')
fp.write(content.encode('utf-8'))
fp.close()
return destpath

View File

@@ -1,178 +0,0 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-10-22
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
# This is a fork from hsfs. The reason for this fork is that hsfs has been designed for musicGuru
# and was re-used for dupeGuru. The problem is that hsfs is way over-engineered for dupeGuru,
# resulting needless complexity and memory usage. It's been a while since I wanted to do that fork,
# and I'm doing it now.
from __future__ import unicode_literals
import hashlib
import logging
from hsutil import io
from hsutil.misc import nonone, flatten
from hsutil.str import get_file_ext
class FSError(Exception):
cls_message = "An error has occured on '{name}' in '{parent}'"
def __init__(self, fsobject, parent=None):
message = self.cls_message
if isinstance(fsobject, basestring):
name = fsobject
elif isinstance(fsobject, File):
name = fsobject.name
else:
name = ''
parentname = unicode(parent) if parent is not None else ''
Exception.__init__(self, message.format(name=name, parent=parentname))
class AlreadyExistsError(FSError):
"The directory or file name we're trying to add already exists"
cls_message = "'{name}' already exists in '{parent}'"
class InvalidPath(FSError):
"The path of self is invalid, and cannot be worked with."
cls_message = "'{name}' is invalid."
class InvalidDestinationError(FSError):
"""A copy/move operation has been called, but the destination is invalid."""
cls_message = "'{name}' is an invalid destination for this operation."
class OperationError(FSError):
"""A copy/move/delete operation has been called, but the checkup after the
operation shows that it didn't work."""
cls_message = "Operation on '{name}' failed."
class File(object):
INITIAL_INFO = {
'size': 0,
'ctime': 0,
'mtime': 0,
'md5': '',
'md5partial': '',
}
def __init__(self, path):
self.path = path
#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
self._md5partial_offset = 0x4000 #16Kb
self._md5partial_size = 0x4000 #16Kb
def __getattr__(self, attrname):
# Only called when attr is not there
if attrname in self.INITIAL_INFO:
try:
self._read_info(attrname)
except Exception as e:
logging.warning("An error '%s' was raised while decoding '%s'", e, repr(self.path))
try:
return self.__dict__[attrname]
except KeyError:
return self.INITIAL_INFO[attrname]
raise AttributeError()
def _read_info(self, field):
if field in ('size', 'ctime', 'mtime'):
stats = io.stat(self.path)
self.size = nonone(stats.st_size, 0)
self.ctime = nonone(stats.st_ctime, 0)
self.mtime = nonone(stats.st_mtime, 0)
elif field == 'md5partial':
try:
fp = io.open(self.path, 'rb')
offset = self._md5partial_offset
size = self._md5partial_size
fp.seek(offset)
partialdata = fp.read(size)
md5 = hashlib.md5(partialdata)
self.md5partial = md5.digest()
fp.close()
except Exception:
pass
elif field == 'md5':
try:
fp = io.open(self.path, 'rb')
filedata = fp.read()
md5 = hashlib.md5(filedata)
self.md5 = md5.digest()
fp.close()
except Exception:
pass
def _read_all_info(self, attrnames=None):
"""Cache all possible info.
If `attrnames` is not None, caches only attrnames.
"""
if attrnames is None:
attrnames = self.INITIAL_INFO.keys()
for attrname in attrnames:
if attrname not in self.__dict__:
self._read_info(attrname)
#--- Public
@classmethod
def can_handle(cls, path):
return not io.islink(path) and io.isfile(path)
def rename(self, newname):
if newname == self.name:
return
destpath = self.path[:-1] + newname
if io.exists(destpath):
raise AlreadyExistsError(newname, self.path[:-1])
try:
io.rename(self.path, destpath)
except EnvironmentError:
raise OperationError(self)
if not io.exists(destpath):
raise OperationError(self)
self.path = destpath
#--- Properties
@property
def extension(self):
return get_file_ext(self.name)
@property
def name(self):
return self.path[-1]
def get_file(path, fileclasses=[File]):
for fileclass in fileclasses:
if fileclass.can_handle(path):
return fileclass(path)
def get_files(path, fileclasses=[File]):
assert all(issubclass(fileclass, File) for fileclass in fileclasses)
try:
paths = [path + name for name in io.listdir(path)]
result = []
for path in paths:
file = get_file(path, fileclasses=fileclasses)
if file is not None:
result.append(file)
return result
except EnvironmentError:
raise InvalidPath(path)
def get_all_files(path, fileclasses=[File]):
files = get_files(path, fileclasses=fileclasses)
filepaths = set(f.path for f in files)
subpaths = [path + name for name in io.listdir(path)]
# 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 subpaths if not io.islink(p) and io.isdir(p) and p not in filepaths]
subfiles = flatten(get_all_files(subpath, fileclasses=fileclasses) for subpath in subfolders)
return subfiles + files

View File

@@ -1,116 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/05/02
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsutil.files import FileOrPath
import xml.dom.minidom
class IgnoreList(object):
"""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.
When iterated, 2 sized tuples will be returned, the tuples containing 2 items ignored together.
"""
#---Override
def __init__(self):
self._ignored = {}
self._count = 0
def __iter__(self):
for first,seconds in self._ignored.iteritems():
for second in seconds:
yield (first,second)
def __len__(self):
return self._count
#---Public
def AreIgnored(self,first,second):
def do_check(first,second):
try:
matches = self._ignored[first]
return second in matches
except KeyError:
return False
return do_check(first,second) or do_check(second,first)
def Clear(self):
self._ignored = {}
self._count = 0
def Filter(self,func):
"""Applies a filter on all ignored items, and remove all matches where func(first,second)
doesn't return True.
"""
filtered = IgnoreList()
for first,second in self:
if func(first,second):
filtered.Ignore(first,second)
self._ignored = filtered._ignored
self._count = filtered._count
def Ignore(self,first,second):
if self.AreIgnored(first,second):
return
try:
matches = self._ignored[first]
matches.add(second)
except KeyError:
try:
matches = self._ignored[second]
matches.add(first)
except KeyError:
matches = set()
matches.add(second)
self._ignored[first] = matches
self._count += 1
def load_from_xml(self,infile):
"""Loads the ignore list from a XML created with save_to_xml.
infile can be a file object or a filename.
"""
try:
doc = xml.dom.minidom.parse(infile)
except Exception:
return
file_nodes = doc.getElementsByTagName('file')
for fn in file_nodes:
if not fn.getAttributeNode('path'):
continue
file_path = fn.getAttributeNode('path').nodeValue
subfile_nodes = fn.getElementsByTagName('file')
for sfn in subfile_nodes:
if not sfn.getAttributeNode('path'):
continue
subfile_path = sfn.getAttributeNode('path').nodeValue
self.Ignore(file_path,subfile_path)
def save_to_xml(self,outfile):
"""Create a XML file that can be used by load_from_xml.
outfile can be a file object or a filename.
"""
doc = xml.dom.minidom.Document()
root = doc.appendChild(doc.createElement('ignore_list'))
for file,subfiles in self._ignored.items():
file_node = root.appendChild(doc.createElement('file'))
if isinstance(file,unicode):
file = file.encode('utf-8')
file_node.setAttribute('path',file)
for subfile in subfiles:
subfile_node = file_node.appendChild(doc.createElement('file'))
if isinstance(subfile,unicode):
subfile = subfile.encode('utf-8')
subfile_node.setAttribute('path',subfile)
with FileOrPath(outfile, 'wb') as fp:
doc.writexml(fp,'\t','\t','\n',encoding='utf-8')

View File

@@ -1,370 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/02/23
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import logging
import re
from xml.sax import handler, make_parser, SAXException
from xml.sax.saxutils import XMLGenerator
from xml.sax.xmlreader import AttributesImpl
from . import engine
from hsutil.job import nulljob
from hsutil.markable import Markable
from hsutil.misc import flatten, cond, nonone
from hsutil.str import format_size
from hsutil.files import open_if_filename
class Results(Markable):
#---Override
def __init__(self, data_module):
super(Results, self).__init__()
self.__groups = []
self.__group_of_duplicate = {}
self.__groups_sort_descriptor = None # This is a tuple (key, asc)
self.__dupes = None
self.__dupes_sort_descriptor = None # This is a tuple (key, asc, delta)
self.__filters = None
self.__filtered_dupes = None
self.__filtered_groups = None
self.__recalculate_stats()
self.__marked_size = 0
self.data = data_module
def _did_mark(self, dupe):
self.__marked_size += dupe.size
def _did_unmark(self, dupe):
self.__marked_size -= dupe.size
def _get_markable_count(self):
return self.__total_count
def _is_markable(self, dupe):
if dupe.is_ref:
return False
g = self.get_group_of_duplicate(dupe)
if not g:
return False
if dupe is g.ref:
return False
if self.__filtered_dupes and dupe not in self.__filtered_dupes:
return False
return True
#---Private
def __get_dupe_list(self):
if self.__dupes is None:
self.__dupes = flatten(group.dupes for group in self.groups)
if None in self.__dupes:
# 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)
if self.__filtered_dupes:
self.__dupes = [dupe for dupe in self.__dupes if dupe in self.__filtered_dupes]
sd = self.__dupes_sort_descriptor
if sd:
self.sort_dupes(sd[0], sd[1], sd[2])
return self.__dupes
def __get_groups(self):
if self.__filtered_groups is None:
return self.__groups
else:
return self.__filtered_groups
def __get_stat_line(self):
if self.__filtered_dupes is None:
mark_count = self.mark_count
marked_size = self.__marked_size
total_count = self.__total_count
total_size = self.__total_size
else:
mark_count = len([dupe for dupe in self.__filtered_dupes if self.is_marked(dupe)])
marked_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_marked(dupe))
total_count = len([dupe for dupe in self.__filtered_dupes if self.is_markable(dupe)])
total_size = sum(dupe.size for dupe in self.__filtered_dupes if self.is_markable(dupe))
if self.mark_inverted:
marked_size = self.__total_size - marked_size
result = '%d / %d (%s / %s) duplicates marked.' % (
mark_count,
total_count,
format_size(marked_size, 2),
format_size(total_size, 2),
)
if self.__filters:
result += ' filter: %s' % ' --> '.join(self.__filters)
return result
def __recalculate_stats(self):
self.__total_size = 0
self.__total_count = 0
for group in self.groups:
markable = [dupe for dupe in group.dupes if self._is_markable(dupe)]
self.__total_count += len(markable)
self.__total_size += sum(dupe.size for dupe in markable)
def __set_groups(self, new_groups):
self.mark_none()
self.__groups = new_groups
self.__group_of_duplicate = {}
for g in self.__groups:
for dupe in g:
self.__group_of_duplicate[dupe] = g
if not hasattr(dupe, 'is_ref'):
dupe.is_ref = False
old_filters = nonone(self.__filters, [])
self.apply_filter(None)
for filter_str in old_filters:
self.apply_filter(filter_str)
#---Public
def apply_filter(self, filter_str):
''' Applies a filter 'filter_str' to self.groups
When you apply the filter, only dupes with the filename matching 'filter_str' will be in
in the results. To cancel the filter, just call apply_filter with 'filter_str' to None,
and the results will go back to normal.
If call apply_filter on a filtered results, the filter will be applied
*on the filtered results*.
'filter_str' is a string containing a regexp to filter dupes with.
'''
if not filter_str:
self.__filtered_dupes = None
self.__filtered_groups = None
self.__filters = None
else:
if not self.__filters:
self.__filters = []
try:
filter_re = re.compile(filter_str, re.IGNORECASE)
except re.error:
return # don't apply this filter.
self.__filters.append(filter_str)
if self.__filtered_dupes is None:
self.__filtered_dupes = flatten(g[:] for g in self.groups)
self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(dupe.name))
filtered_groups = set()
for dupe in self.__filtered_dupes:
filtered_groups.add(self.get_group_of_duplicate(dupe))
self.__filtered_groups = list(filtered_groups)
self.__recalculate_stats()
sd = self.__groups_sort_descriptor
if sd:
self.sort_groups(sd[0], sd[1])
self.__dupes = None
def get_group_of_duplicate(self, dupe):
try:
return self.__group_of_duplicate[dupe]
except (TypeError, KeyError):
return None
is_markable = _is_markable
def load_from_xml(self, infile, get_file, j=nulljob):
self.apply_filter(None)
handler = _ResultsHandler(get_file)
try:
parser = make_parser()
except Exception as e:
# This special handling is to try to figure out the cause of #47
# We don't silently return, because we want the user to send error report.
logging.exception(e)
try:
import xml.parsers.expat
logging.warning('importing xml.parsers.expat went ok, WTF?')
except Exception as e:
# This log should give a little more details about the cause of this all
logging.exception(e)
raise
raise
parser.setContentHandler(handler)
try:
infile, must_close = open_if_filename(infile)
except IOError:
return
BUFSIZE = 1024 * 1024 # 1mb buffer
infile.seek(0, 2)
j.start_job(infile.tell() // BUFSIZE)
infile.seek(0, 0)
try:
while True:
data = infile.read(BUFSIZE)
if not data:
break
parser.feed(data)
j.add_progress()
except SAXException:
return
self.groups = handler.groups
for dupe_file in handler.marked:
self.mark(dupe_file)
def make_ref(self, dupe):
g = self.get_group_of_duplicate(dupe)
r = g.ref
self._remove_mark_flag(dupe)
g.switch_ref(dupe);
if not r.is_ref:
self.__total_count += 1
self.__total_size += r.size
if not dupe.is_ref:
self.__total_count -= 1
self.__total_size -= dupe.size
self.__dupes = None
def perform_on_marked(self, func, remove_from_results):
problems = []
for d in self.dupes:
if self.is_marked(d) and (not func(d)):
problems.append(d)
if remove_from_results:
to_remove = [d for d in self.dupes if self.is_marked(d) and (d not in problems)]
self.remove_duplicates(to_remove)
self.mark_none()
for d in problems:
self.mark(d)
return len(problems)
def remove_duplicates(self, dupes):
'''Remove 'dupes' from their respective group, and remove the group is it ends up empty.
'''
affected_groups = set()
for dupe in dupes:
group = self.get_group_of_duplicate(dupe)
if dupe not in group.dupes:
return
group.remove_dupe(dupe, False)
self._remove_mark_flag(dupe)
self.__total_count -= 1
self.__total_size -= dupe.size
if not group:
self.__groups.remove(group)
if self.__filtered_groups:
self.__filtered_groups.remove(group)
else:
affected_groups.add(group)
for group in affected_groups:
group.discard_matches()
self.__dupes = None
def save_to_xml(self, outfile):
self.apply_filter(None)
outfile, must_close = open_if_filename(outfile, 'wb')
writer = XMLGenerator(outfile, 'utf-8')
writer.startDocument()
empty_attrs = AttributesImpl({})
writer.startElement('results', empty_attrs)
for g in self.groups:
writer.startElement('group', empty_attrs)
dupe2index = {}
for index, d in enumerate(g):
dupe2index[d] = index
try:
words = engine.unpack_fields(d.words)
except AttributeError:
words = ()
attrs = AttributesImpl({
'path': unicode(d.path),
'is_ref': cond(d.is_ref, 'y', 'n'),
'words': ','.join(words),
'marked': cond(self.is_marked(d), 'y', 'n')
})
writer.startElement('file', attrs)
writer.endElement('file')
for match in g.matches:
attrs = AttributesImpl({
'first': str(dupe2index[match.first]),
'second': str(dupe2index[match.second]),
'percentage': str(int(match.percentage)),
})
writer.startElement('match', attrs)
writer.endElement('match')
writer.endElement('group')
writer.endElement('results')
writer.endDocument()
if must_close:
outfile.close()
def sort_dupes(self, key, asc=True, delta=False):
if not self.__dupes:
self.__get_dupe_list()
self.__dupes.sort(key=lambda d: self.data.GetDupeSortKey(d, lambda: self.get_group_of_duplicate(d), key, delta))
if not asc:
self.__dupes.reverse()
self.__dupes_sort_descriptor = (key,asc,delta)
def sort_groups(self,key,asc=True):
self.groups.sort(key=lambda g: self.data.GetGroupSortKey(g, key))
if not asc:
self.groups.reverse()
self.__groups_sort_descriptor = (key,asc)
#---Properties
dupes = property(__get_dupe_list)
groups = property(__get_groups, __set_groups)
stat_line = property(__get_stat_line)
class _ResultsHandler(handler.ContentHandler):
def __init__(self, get_file):
self.group = None
self.dupes = None
self.marked = set()
self.groups = []
self.get_file = get_file
def startElement(self, name, attrs):
if name == 'group':
self.group = engine.Group()
self.dupes = []
return
if (name == 'file') and (self.group is not None):
if not (('path' in attrs) and ('words' in attrs)):
return
path = attrs['path']
file = self.get_file(path)
if file is None:
return
file.words = attrs['words'].split(',')
file.is_ref = attrs.get('is_ref') == 'y'
self.dupes.append(file)
if attrs.get('marked') == 'y':
self.marked.add(file)
if (name == 'match') and (self.group is not None):
try:
first_file = self.dupes[int(attrs['first'])]
second_file = self.dupes[int(attrs['second'])]
percentage = int(attrs['percentage'])
self.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
pass
def endElement(self, name):
def do_match(ref_file, other_files, group):
if not other_files:
return
for other_file in other_files:
group.add_match(engine.get_match(ref_file, other_file))
do_match(other_files[0], other_files[1:], group)
if name == 'group':
group = self.group
self.group = None
dupes = self.dupes
self.dupes = []
if group is None:
return
if len(dupes) < 2:
return
if not group.matches: # <match> elements not present, do it manually, without %
do_match(dupes[0], dupes[1:], group)
group.prioritize(lambda x: dupes.index(x))
self.groups.append(group)

View File

@@ -1,109 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/03/03
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import logging
from hsutil import job, io
from hsutil.misc import dedupe
from hsutil.str import get_file_ext, rem_file_ext
from . import engine
from .ignore import IgnoreList
(SCAN_TYPE_FILENAME,
SCAN_TYPE_FIELDS,
SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG,
UNUSED, # Must not be removed. Constants here are what scan_type in the prefs are.
SCAN_TYPE_CONTENT,
SCAN_TYPE_CONTENT_AUDIO) = range(7)
SCANNABLE_TAGS = ['track', 'artist', 'album', 'title', 'genre', 'year']
class Scanner(object):
def __init__(self):
self.ignore_list = IgnoreList()
self.discarded_file_count = 0
def _getmatches(self, files, j):
if self.size_threshold:
j = j.start_subjob([2, 8])
for f in j.iter_with_progress(files, 'Read size of %d/%d files'):
f.size # pre-read, makes a smoother progress if read here (especially for bundles)
files = [f for f in files if f.size >= self.size_threshold]
if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO):
sizeattr = 'size' if self.scan_type == SCAN_TYPE_CONTENT else 'audiosize'
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==SCAN_TYPE_CONTENT_AUDIO, j=j)
else:
j = j.start_subjob([2, 8])
kw = {}
kw['match_similar_words'] = self.match_similar_words
kw['weight_words'] = self.word_weighting
kw['min_match_percentage'] = self.min_match_percentage
if self.scan_type == SCAN_TYPE_FIELDS_NO_ORDER:
self.scan_type = SCAN_TYPE_FIELDS
kw['no_field_order'] = True
func = {
SCAN_TYPE_FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)),
SCAN_TYPE_FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)),
SCAN_TYPE_TAG: lambda f: [engine.getwords(unicode(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
}[self.scan_type]
for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'):
f.words = func(f)
return engine.getmatches(files, j=j, **kw)
@staticmethod
def _key_func(dupe):
return (not dupe.is_ref, -dupe.size)
@staticmethod
def _tie_breaker(ref, dupe):
refname = rem_file_ext(ref.name).lower()
dupename = rem_file_ext(dupe.name).lower()
if 'copy' in refname and 'copy' not in dupename:
return True
if refname.startswith(dupename) and (refname[len(dupename):].strip().isdigit()):
return True
return len(dupe.path) > len(ref.path)
def GetDupeGroups(self, files, j=job.nulljob):
j = j.start_subjob([8, 2])
for f in [f for f in files if not hasattr(f, 'is_ref')]:
f.is_ref = False
logging.info('Getting matches')
matches = self._getmatches(files, j)
logging.info('Found %d matches' % len(matches))
j.set_progress(100, 'Removing false matches')
if not self.mix_file_kind:
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
matches = [m for m in matches if io.exists(m.first.path) and io.exists(m.second.path)]
if self.ignore_list:
j = j.start_subjob(2)
iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list')
matches = [m for m in iter_matches
if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))]
logging.info('Grouping matches')
groups = engine.get_groups(matches, j)
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
self.discarded_file_count = len(matched_files) - sum(len(g) for g in groups)
groups = [g for g in groups if any(not f.is_ref for f in g)]
logging.info('Created %d groups' % len(groups))
j.set_progress(100, 'Doing group prioritization')
for g in groups:
g.prioritize(self._key_func, self._tie_breaker)
return groups
match_similar_words = False
min_match_percentage = 80
mix_file_kind = True
scan_type = SCAN_TYPE_FILENAME
scanned_tags = set(['artist', 'title'])
size_threshold = 0
word_weighting = False

View File

@@ -1,366 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/11/11
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import tempfile
import shutil
import logging
import os.path as op
from nose.tools import eq_
from hsutil.path import Path
from hsutil.testcase import TestCase
from hsutil.decorators import log_calls
from hsutil import io
from . import data
from .results_test import GetTestGroups
from .. import engine, fs
try:
from ..app_cocoa import DupeGuru as DupeGuruBase
except ImportError:
from nose.plugins.skip import SkipTest
raise SkipTest("These tests can only be run on OS X")
class DupeGuru(DupeGuruBase):
def __init__(self):
DupeGuruBase.__init__(self, data, '/tmp', appid=4)
def _start_job(self, jobid, func):
func(nulljob)
def r2np(rows):
#Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]]
return [[i] for i in rows]
class TCDupeGuru(TestCase):
def setUp(self):
self.app = DupeGuru()
self.objects,self.matches,self.groups = GetTestGroups()
self.app.results.groups = self.groups
tmppath = self.tmppath()
io.mkdir(tmppath + 'foo')
io.mkdir(tmppath + 'bar')
self.app.directories.add_path(tmppath)
def test_GetObjects(self):
app = self.app
objects = self.objects
groups = self.groups
g,d = app.GetObjects([0])
self.assert_(g is groups[0])
self.assert_(d is None)
g,d = app.GetObjects([0,0])
self.assert_(g is groups[0])
self.assert_(d is objects[1])
g,d = app.GetObjects([1,0])
self.assert_(g is groups[1])
self.assert_(d is objects[4])
def test_GetObjects_after_sort(self):
app = self.app
objects = self.objects
groups = self.groups[:] #To keep the old order in memory
app.sort_groups(0,False) #0 = Filename
#Now, the group order is supposed to be reversed
g,d = app.GetObjects([0,0])
self.assert_(g is groups[1])
self.assert_(d is objects[4])
def test_GetObjects_out_of_range(self):
app = self.app
self.assertEqual((None,None),app.GetObjects([2]))
self.assertEqual((None,None),app.GetObjects([]))
self.assertEqual((None,None),app.GetObjects([1,2]))
def test_selected_result_node_paths(self):
# app.selected_dupes is correctly converted into node paths
app = self.app
objects = self.objects
paths = [[0, 0], [0, 1], [1]]
app.SelectResultNodePaths(paths)
eq_(app.selected_result_node_paths(), paths)
def test_selected_result_node_paths_after_deletion(self):
# cases where the selected dupes aren't there are correctly handled
app = self.app
objects = self.objects
paths = [[0, 0], [0, 1], [1]]
app.SelectResultNodePaths(paths)
app.RemoveSelected()
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
eq_(app.selected_result_node_paths(), [[0]]) # no exception
def test_selectResultNodePaths(self):
app = self.app
objects = self.objects
app.SelectResultNodePaths([[0,0],[0,1]])
self.assertEqual(2,len(app.selected_dupes))
self.assert_(app.selected_dupes[0] is objects[1])
self.assert_(app.selected_dupes[1] is objects[2])
def test_selectResultNodePaths_with_ref(self):
app = self.app
objects = self.objects
app.SelectResultNodePaths([[0,0],[0,1],[1]])
self.assertEqual(3,len(app.selected_dupes))
self.assert_(app.selected_dupes[0] is objects[1])
self.assert_(app.selected_dupes[1] is objects[2])
self.assert_(app.selected_dupes[2] is self.groups[1].ref)
def test_selectResultNodePaths_empty(self):
self.app.SelectResultNodePaths([])
self.assertEqual(0,len(self.app.selected_dupes))
def test_selectResultNodePaths_after_sort(self):
app = self.app
objects = self.objects
groups = self.groups[:] #To keep the old order in memory
app.sort_groups(0,False) #0 = Filename
#Now, the group order is supposed to be reversed
app.SelectResultNodePaths([[0,0],[1],[1,0]])
self.assertEqual(3,len(app.selected_dupes))
self.assert_(app.selected_dupes[0] is objects[4])
self.assert_(app.selected_dupes[1] is groups[0].ref)
self.assert_(app.selected_dupes[2] is objects[1])
def test_selectResultNodePaths_out_of_range(self):
app = self.app
app.SelectResultNodePaths([[0,0],[0,1],[1],[1,1],[2]])
self.assertEqual(3,len(app.selected_dupes))
def test_selected_powermarker_node_paths(self):
# app.selected_dupes is correctly converted into paths
app = self.app
objects = self.objects
paths = r2np([0, 1, 2])
app.SelectPowerMarkerNodePaths(paths)
eq_(app.selected_powermarker_node_paths(), paths)
def test_selected_powermarker_node_paths_after_deletion(self):
# cases where the selected dupes aren't there are correctly handled
app = self.app
objects = self.objects
paths = r2np([0, 1, 2])
app.SelectPowerMarkerNodePaths(paths)
app.RemoveSelected()
eq_(app.selected_powermarker_node_paths(), []) # no exception
def test_selectPowerMarkerRows(self):
app = self.app
objects = self.objects
app.SelectPowerMarkerNodePaths(r2np([0,1,2]))
self.assertEqual(3,len(app.selected_dupes))
self.assert_(app.selected_dupes[0] is objects[1])
self.assert_(app.selected_dupes[1] is objects[2])
self.assert_(app.selected_dupes[2] is objects[4])
def test_selectPowerMarkerRows_empty(self):
self.app.SelectPowerMarkerNodePaths([])
self.assertEqual(0,len(self.app.selected_dupes))
def test_selectPowerMarkerRows_after_sort(self):
app = self.app
objects = self.objects
app.sort_dupes(0,False) #0 = Filename
app.SelectPowerMarkerNodePaths(r2np([0,1,2]))
self.assertEqual(3,len(app.selected_dupes))
self.assert_(app.selected_dupes[0] is objects[4])
self.assert_(app.selected_dupes[1] is objects[2])
self.assert_(app.selected_dupes[2] is objects[1])
def test_selectPowerMarkerRows_out_of_range(self):
app = self.app
app.SelectPowerMarkerNodePaths(r2np([0,1,2,3]))
self.assertEqual(3,len(app.selected_dupes))
def test_toggleSelectedMark(self):
app = self.app
objects = self.objects
app.ToggleSelectedMarkState()
self.assertEqual(0,app.results.mark_count)
app.SelectPowerMarkerNodePaths(r2np([0,2]))
app.ToggleSelectedMarkState()
self.assertEqual(2,app.results.mark_count)
self.assert_(not app.results.is_marked(objects[0]))
self.assert_(app.results.is_marked(objects[1]))
self.assert_(not app.results.is_marked(objects[2]))
self.assert_(not app.results.is_marked(objects[3]))
self.assert_(app.results.is_marked(objects[4]))
def test_refreshDetailsWithSelected(self):
def mock_refresh(dupe,group):
self.called = True
if self.app.selected_dupes:
self.assert_(dupe is self.app.selected_dupes[0])
self.assert_(group is self.app.results.get_group_of_duplicate(dupe))
else:
self.assert_(dupe is None)
self.assert_(group is None)
self.app.RefreshDetailsTable = mock_refresh
self.called = False
self.app.SelectPowerMarkerNodePaths(r2np([0,2]))
self.app.RefreshDetailsWithSelected()
self.assert_(self.called)
self.called = False
self.app.SelectPowerMarkerNodePaths([])
self.app.RefreshDetailsWithSelected()
self.assert_(self.called)
def test_makeSelectedReference(self):
app = self.app
objects = self.objects
groups = self.groups
app.SelectPowerMarkerNodePaths(r2np([0,2]))
app.MakeSelectedReference()
self.assert_(groups[0].ref is objects[1])
self.assert_(groups[1].ref is objects[4])
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self):
app = self.app
objects = self.objects
groups = self.groups
app.SelectPowerMarkerNodePaths(r2np([0,1,2]))
#Only 0 and 2 must go ref, not 1 because it is a part of the same group
app.MakeSelectedReference()
self.assert_(groups[0].ref is objects[1])
self.assert_(groups[1].ref is objects[4])
def test_removeSelected(self):
app = self.app
app.SelectPowerMarkerNodePaths(r2np([0,2]))
app.RemoveSelected()
self.assertEqual(1,len(app.results.dupes))
app.RemoveSelected()
self.assertEqual(1,len(app.results.dupes))
app.SelectPowerMarkerNodePaths(r2np([0,2]))
app.RemoveSelected()
self.assertEqual(0,len(app.results.dupes))
def test_addDirectory_simple(self):
# There's already a directory in self.app, so adding another once makes 2 of em
app = self.app
eq_(app.add_directory(self.datadirpath()), 0)
eq_(len(app.directories), 2)
def test_addDirectory_already_there(self):
app = self.app
self.assertEqual(0,app.add_directory(self.datadirpath()))
self.assertEqual(1,app.add_directory(self.datadirpath()))
def test_addDirectory_does_not_exist(self):
app = self.app
self.assertEqual(2,app.add_directory('/does_not_exist'))
def test_ignore(self):
app = self.app
app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group
app.AddSelectedToIgnoreList()
self.assertEqual(1,len(app.scanner.ignore_list))
app.SelectPowerMarkerNodePaths(r2np([0])) #first dupe of the 3 dupes group
app.AddSelectedToIgnoreList()
#BOTH the ref and the other dupe should have been added
self.assertEqual(3,len(app.scanner.ignore_list))
def test_purgeIgnoreList(self):
app = self.app
p1 = self.filepath('zerofile')
p2 = self.filepath('zerofill')
dne = '/does_not_exist'
app.scanner.ignore_list.Ignore(dne,p1)
app.scanner.ignore_list.Ignore(p2,dne)
app.scanner.ignore_list.Ignore(p1,p2)
app.PurgeIgnoreList()
self.assertEqual(1,len(app.scanner.ignore_list))
self.assert_(app.scanner.ignore_list.AreIgnored(p1,p2))
self.assert_(not app.scanner.ignore_list.AreIgnored(dne,p1))
def test_only_unicode_is_added_to_ignore_list(self):
def FakeIgnore(first,second):
if not isinstance(first,unicode):
self.fail()
if not isinstance(second,unicode):
self.fail()
app = self.app
app.scanner.ignore_list.Ignore = FakeIgnore
app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group
app.AddSelectedToIgnoreList()
def test_GetOutlineViewChildCounts_out_of_range(self):
# Out of range requests don't crash and return an empty value
app = self.app
# [0, 2] is out of range
eq_(app.GetOutlineViewChildCounts(1, [0, 2]), []) # no crash
def test_GetOutlineViewValues_out_of_range(self):
# Out of range requests don't crash and return an empty value
app = self.app
# [0, 2] is out of range
eq_(app.GetOutlineViewValues(1, [0, 2]), []) # no crash
class TCDupeGuru_renameSelected(TestCase):
def setUp(self):
p = self.tmppath()
fp = open(unicode(p + 'foo bar 1'),mode='w')
fp.close()
fp = open(unicode(p + 'foo bar 2'),mode='w')
fp.close()
fp = open(unicode(p + 'foo bar 3'),mode='w')
fp.close()
files = fs.get_files(p)
matches = engine.getmatches(files)
groups = engine.get_groups(matches)
g = groups[0]
g.prioritize(lambda x:x.name)
app = DupeGuru()
app.results.groups = groups
self.app = app
self.groups = groups
self.p = p
self.files = files
def test_simple(self):
app = self.app
g = self.groups[0]
app.SelectPowerMarkerNodePaths(r2np([0]))
assert app.RenameSelected('renamed')
names = io.listdir(self.p)
assert 'renamed' in names
assert 'foo bar 2' not in names
eq_(g.dupes[0].name, 'renamed')
def test_none_selected(self):
app = self.app
g = self.groups[0]
app.SelectPowerMarkerNodePaths([])
self.mock(logging, 'warning', log_calls(lambda msg: None))
assert not app.RenameSelected('renamed')
msg = logging.warning.calls[0]['msg']
eq_('dupeGuru Warning: list index out of range', msg)
names = io.listdir(self.p)
assert 'renamed' not in names
assert 'foo bar 2' in names
eq_(g.dupes[0].name, 'foo bar 2')
def test_name_already_exists(self):
app = self.app
g = self.groups[0]
app.SelectPowerMarkerNodePaths(r2np([0]))
self.mock(logging, 'warning', log_calls(lambda msg: None))
assert not app.RenameSelected('foo bar 1')
msg = logging.warning.calls[0]['msg']
assert msg.startswith('dupeGuru Warning: \'foo bar 1\' already exists in')
names = io.listdir(self.p)
assert 'foo bar 1' in names
assert 'foo bar 2' in names
eq_(g.dupes[0].name, 'foo bar 2')

View File

@@ -1,136 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2007-06-23
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import os
from hsutil.testcase import TestCase
from hsutil import io
from hsutil.path import Path
from hsutil.decorators import log_calls
import hsutil.files
from hsutil.job import nulljob
from . import data
from .. import app, fs
from ..app import DupeGuru as DupeGuruBase
class DupeGuru(DupeGuruBase):
def __init__(self):
DupeGuruBase.__init__(self, data, '/tmp', appid=4)
def _start_job(self, jobid, func):
func(nulljob)
class TCDupeGuru(TestCase):
cls_tested_module = app
def test_apply_filter_calls_results_apply_filter(self):
app = DupeGuru()
self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter))
app.apply_filter('foo')
self.assertEqual(2, len(app.results.apply_filter.calls))
call = app.results.apply_filter.calls[0]
self.assert_(call['filter_str'] is None)
call = app.results.apply_filter.calls[1]
self.assertEqual('foo', call['filter_str'])
def test_apply_filter_escapes_regexp(self):
app = DupeGuru()
self.mock(app.results, 'apply_filter', log_calls(app.results.apply_filter))
app.apply_filter('()[]\\.|+?^abc')
call = app.results.apply_filter.calls[1]
self.assertEqual('\\(\\)\\[\\]\\\\\\.\\|\\+\\?\\^abc', call['filter_str'])
app.apply_filter('(*)') # In "simple mode", we want the * to behave as a wilcard
call = app.results.apply_filter.calls[3]
self.assertEqual('\(.*\)', call['filter_str'])
app.options['escape_filter_regexp'] = False
app.apply_filter('(abc)')
call = app.results.apply_filter.calls[5]
self.assertEqual('(abc)', call['filter_str'])
def test_copy_or_move(self):
# The goal here is just to have a test for a previous blowup I had. I know my test coverage
# for this unit is pathetic. What's done is done. My approach now is to add tests for
# every change I want to make. The blowup was caused by a missing import.
p = self.tmppath()
io.open(p + 'foo', 'w').close()
self.mock(hsutil.files, 'copy', log_calls(lambda source_path, dest_path: None))
self.mock(os, 'makedirs', lambda path: None) # We don't want the test to create that fake directory
app = DupeGuru()
app.directories.add_path(p)
[f] = app.directories.get_files()
app.copy_or_move(f, True, 'some_destination', 0)
self.assertEqual(1, len(hsutil.files.copy.calls))
call = hsutil.files.copy.calls[0]
self.assertEqual('some_destination', call['dest_path'])
self.assertEqual(f.path, call['source_path'])
def test_copy_or_move_clean_empty_dirs(self):
tmppath = Path(self.tmpdir())
sourcepath = tmppath + 'source'
io.mkdir(sourcepath)
io.open(sourcepath + 'myfile', 'w')
app = DupeGuru()
app.directories.add_path(tmppath)
[myfile] = app.directories.get_files()
self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None))
app.copy_or_move(myfile, False, tmppath + 'dest', 0)
calls = app.clean_empty_dirs.calls
self.assertEqual(1, len(calls))
self.assertEqual(sourcepath, calls[0]['path'])
def test_Scan_with_objects_evaluating_to_false(self):
class FakeFile(fs.File):
def __nonzero__(self):
return False
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
app = DupeGuru()
f1, f2 = [FakeFile('foo') for i in range(2)]
f1.is_ref, f2.is_ref = (False, False)
assert not (bool(f1) and bool(f2))
app.directories.get_files = lambda: [f1, f2]
app.directories._dirs.append('this is just so Scan() doesnt return 3')
app.start_scanning() # no exception
class TCDupeGuru_clean_empty_dirs(TestCase):
cls_tested_module = app
def setUp(self):
self.mock(hsutil.files, 'delete_if_empty', log_calls(lambda path, files_to_delete=[]: None))
self.app = DupeGuru()
def test_option_off(self):
self.app.clean_empty_dirs(Path('/foo/bar'))
self.assertEqual(0, len(hsutil.files.delete_if_empty.calls))
def test_option_on(self):
self.app.options['clean_empty_dirs'] = True
self.app.clean_empty_dirs(Path('/foo/bar'))
calls = hsutil.files.delete_if_empty.calls
self.assertEqual(1, len(calls))
self.assertEqual(Path('/foo/bar'), calls[0]['path'])
self.assertEqual(['.DS_Store'], calls[0]['files_to_delete'])
def test_recurse_up(self):
# delete_if_empty must be recursively called up in the path until it returns False
@log_calls
def mock_delete_if_empty(path, files_to_delete=[]):
return len(path) > 1
self.mock(hsutil.files, 'delete_if_empty', mock_delete_if_empty)
self.app.options['clean_empty_dirs'] = True
self.app.clean_empty_dirs(Path('not-empty/empty/empty'))
calls = hsutil.files.delete_if_empty.calls
self.assertEqual(3, len(calls))
self.assertEqual(Path('not-empty/empty/empty'), calls[0]['path'])
self.assertEqual(Path('not-empty/empty'), calls[1]['path'])
self.assertEqual(Path('not-empty'), calls[2]['path'])

View File

@@ -1,45 +0,0 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-10-23
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
# data module for tests
from hsutil.str import format_size
from dupeguru.data import format_path, cmp_value
COLUMNS = [
{'attr':'name','display':'Filename'},
{'attr':'path','display':'Directory'},
{'attr':'size','display':'Size (KB)'},
{'attr':'extension','display':'Kind'},
]
METADATA_TO_READ = ['size']
def GetDisplayInfo(dupe, group, delta):
size = dupe.size
m = group.get_match_of(dupe)
if m and delta:
r = group.ref
size -= r.size
return [
dupe.name,
format_path(dupe.path),
format_size(size, 0, 1, False),
dupe.extension,
]
def GetDupeSortKey(dupe, get_group, key, delta):
r = cmp_value(getattr(dupe, COLUMNS[key]['attr']))
if delta and (key == 2):
r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr']))
return r
def GetGroupSortKey(group, key):
return cmp_value(getattr(group.ref, COLUMNS[key]['attr']))

View File

@@ -1,279 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/02/27
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import os.path as op
import os
import time
from nose.tools import eq_
from hsutil import io
from hsutil.path import Path
from hsutil.testcase import TestCase
from ..directories import *
testpath = Path(TestCase.datadirpath())
def create_fake_fs(rootpath):
rootpath = rootpath + 'fs'
io.mkdir(rootpath)
io.mkdir(rootpath + 'dir1')
io.mkdir(rootpath + 'dir2')
io.mkdir(rootpath + 'dir3')
fp = io.open(rootpath + 'file1.test', 'w')
fp.write('1')
fp.close()
fp = io.open(rootpath + 'file2.test', 'w')
fp.write('12')
fp.close()
fp = io.open(rootpath + 'file3.test', 'w')
fp.write('123')
fp.close()
fp = io.open(rootpath + ('dir1', 'file1.test'), 'w')
fp.write('1')
fp.close()
fp = io.open(rootpath + ('dir2', 'file2.test'), 'w')
fp.write('12')
fp.close()
fp = io.open(rootpath + ('dir3', 'file3.test'), 'w')
fp.write('123')
fp.close()
return rootpath
class TCDirectories(TestCase):
def test_empty(self):
d = Directories()
self.assertEqual(0,len(d))
self.assert_('foobar' not in d)
def test_add_path(self):
d = Directories()
p = testpath + 'utils'
d.add_path(p)
self.assertEqual(1,len(d))
self.assert_(p in d)
self.assert_((p + 'foobar') in d)
self.assert_(p[:-1] not in d)
p = self.tmppath()
d.add_path(p)
self.assertEqual(2,len(d))
self.assert_(p in d)
def test_AddPath_when_path_is_already_there(self):
d = Directories()
p = testpath + 'utils'
d.add_path(p)
self.assertRaises(AlreadyThereError, d.add_path, p)
self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar')
self.assertEqual(1, len(d))
def test_add_path_containing_paths_already_there(self):
d = Directories()
d.add_path(testpath + 'utils')
self.assertEqual(1, len(d))
d.add_path(testpath)
eq_(len(d), 1)
eq_(d[0], testpath)
def test_AddPath_non_latin(self):
p = Path(self.tmpdir())
to_add = p + u'unicode\u201a'
os.mkdir(unicode(to_add))
d = Directories()
try:
d.add_path(to_add)
except UnicodeDecodeError:
self.fail()
def test_del(self):
d = Directories()
d.add_path(testpath + 'utils')
try:
del d[1]
self.fail()
except IndexError:
pass
d.add_path(self.tmppath())
del d[1]
self.assertEqual(1, len(d))
def test_states(self):
d = Directories()
p = testpath + 'utils'
d.add_path(p)
self.assertEqual(STATE_NORMAL,d.get_state(p))
d.set_state(p,STATE_REFERENCE)
self.assertEqual(STATE_REFERENCE,d.get_state(p))
self.assertEqual(STATE_REFERENCE,d.get_state(p + 'dir1'))
self.assertEqual(1,len(d.states))
self.assertEqual(p,d.states.keys()[0])
self.assertEqual(STATE_REFERENCE,d.states[p])
def test_get_state_with_path_not_there(self):
# When the path's not there, just return STATE_NORMAL
d = Directories()
d.add_path(testpath + 'utils')
eq_(d.get_state(testpath), STATE_NORMAL)
def test_states_remain_when_larger_directory_eat_smaller_ones(self):
d = Directories()
p = testpath + 'utils'
d.add_path(p)
d.set_state(p,STATE_EXCLUDED)
d.add_path(testpath)
d.set_state(testpath,STATE_REFERENCE)
self.assertEqual(STATE_EXCLUDED,d.get_state(p))
self.assertEqual(STATE_EXCLUDED,d.get_state(p + 'dir1'))
self.assertEqual(STATE_REFERENCE,d.get_state(testpath))
def test_set_state_keep_state_dict_size_to_minimum(self):
d = Directories()
p = create_fake_fs(self.tmppath())
d.add_path(p)
d.set_state(p,STATE_REFERENCE)
d.set_state(p + 'dir1',STATE_REFERENCE)
self.assertEqual(1,len(d.states))
self.assertEqual(STATE_REFERENCE,d.get_state(p + 'dir1'))
d.set_state(p + 'dir1',STATE_NORMAL)
self.assertEqual(2,len(d.states))
self.assertEqual(STATE_NORMAL,d.get_state(p + 'dir1'))
d.set_state(p + 'dir1',STATE_REFERENCE)
self.assertEqual(1,len(d.states))
self.assertEqual(STATE_REFERENCE,d.get_state(p + 'dir1'))
def test_get_files(self):
d = Directories()
p = create_fake_fs(self.tmppath())
d.add_path(p)
d.set_state(p + 'dir1',STATE_REFERENCE)
d.set_state(p + 'dir2',STATE_EXCLUDED)
files = list(d.get_files())
self.assertEqual(5, len(files))
for f in files:
if f.path[:-1] == p + 'dir1':
assert f.is_ref
else:
assert not f.is_ref
def test_get_files_with_inherited_exclusion(self):
d = Directories()
p = testpath + 'utils'
d.add_path(p)
d.set_state(p,STATE_EXCLUDED)
self.assertEqual([], list(d.get_files()))
def test_save_and_load(self):
d1 = Directories()
d2 = Directories()
p1 = self.tmppath()
p2 = self.tmppath()
d1.add_path(p1)
d1.add_path(p2)
d1.set_state(p1, STATE_REFERENCE)
d1.set_state(p1 + 'dir1',STATE_EXCLUDED)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
d1.save_to_file(tmpxml)
d2.load_from_file(tmpxml)
self.assertEqual(2, len(d2))
self.assertEqual(STATE_REFERENCE,d2.get_state(p1))
self.assertEqual(STATE_EXCLUDED,d2.get_state(p1 + 'dir1'))
def test_invalid_path(self):
d = Directories()
p = Path('does_not_exist')
self.assertRaises(InvalidPathError, d.add_path, p)
self.assertEqual(0, len(d))
def test_set_state_on_invalid_path(self):
d = Directories()
try:
d.set_state(Path('foobar',),STATE_NORMAL)
except LookupError:
self.fail()
def test_load_from_file_with_invalid_path(self):
#This test simulates a load from file resulting in a
#InvalidPath raise. Other directories must be loaded.
d1 = Directories()
d1.add_path(testpath + 'utils')
#Will raise InvalidPath upon loading
p = self.tmppath()
d1.add_path(p)
io.rmdir(p)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
d1.save_to_file(tmpxml)
d2 = Directories()
d2.load_from_file(tmpxml)
self.assertEqual(1, len(d2))
def test_unicode_save(self):
d = Directories()
p1 = self.tmppath() + u'hello\xe9'
io.mkdir(p1)
io.mkdir(p1 + u'foo\xe9')
d.add_path(p1)
d.set_state(p1 + u'foo\xe9', STATE_EXCLUDED)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
try:
d.save_to_file(tmpxml)
except UnicodeDecodeError:
self.fail()
def test_get_files_refreshes_its_directories(self):
d = Directories()
p = create_fake_fs(self.tmppath())
d.add_path(p)
files = d.get_files()
self.assertEqual(6, len(list(files)))
time.sleep(1)
os.remove(str(p + ('dir1','file1.test')))
files = d.get_files()
self.assertEqual(5, len(list(files)))
def test_get_files_does_not_choke_on_non_existing_directories(self):
d = Directories()
p = Path(self.tmpdir())
d.add_path(p)
io.rmtree(p)
self.assertEqual([], list(d.get_files()))
def test_get_state_returns_excluded_by_default_for_hidden_directories(self):
d = Directories()
p = Path(self.tmpdir())
hidden_dir_path = p + '.foo'
io.mkdir(p + '.foo')
d.add_path(p)
self.assertEqual(d.get_state(hidden_dir_path), STATE_EXCLUDED)
# But it can be overriden
d.set_state(hidden_dir_path, STATE_NORMAL)
self.assertEqual(d.get_state(hidden_dir_path), STATE_NORMAL)
def test_default_path_state_override(self):
# It's possible for a subclass to override the default state of a path
class MyDirectories(Directories):
def _default_state_for_path(self, path):
if 'foobar' in path:
return STATE_EXCLUDED
d = MyDirectories()
p1 = self.tmppath()
io.mkdir(p1 + 'foobar')
io.open(p1 + 'foobar/somefile', 'w').close()
io.mkdir(p1 + 'foobaz')
io.open(p1 + 'foobaz/somefile', 'w').close()
d.add_path(p1)
eq_(d.get_state(p1 + 'foobaz'), STATE_NORMAL)
eq_(d.get_state(p1 + 'foobar'), STATE_EXCLUDED)
eq_(len(list(d.get_files())), 1) # only the 'foobaz' file is there
# However, the default state can be changed
d.set_state(p1 + 'foobar', STATE_NORMAL)
eq_(d.get_state(p1 + 'foobar'), STATE_NORMAL)
eq_(len(list(d.get_files())), 2)

View File

@@ -1,815 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/01/29
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import sys
from nose.tools import eq_
from hsutil import job
from hsutil.decorators import log_calls
from hsutil.testcase import TestCase
from .. import engine, fs
from ..engine import *
class NamedObject(object):
def __init__(self, name="foobar", with_words=False, size=1):
self.name = name
self.size = size
self.md5partial = name
self.md5 = name
if with_words:
self.words = getwords(name)
no = NamedObject
def get_match_triangle():
o1 = NamedObject(with_words=True)
o2 = NamedObject(with_words=True)
o3 = NamedObject(with_words=True)
m1 = get_match(o1,o2)
m2 = get_match(o1,o3)
m3 = get_match(o2,o3)
return [m1, m2, m3]
def get_test_group():
m1, m2, m3 = get_match_triangle()
result = Group()
result.add_match(m1)
result.add_match(m2)
result.add_match(m3)
return result
class TCgetwords(TestCase):
def test_spaces(self):
self.assertEqual(['a', 'b', 'c', 'd'], getwords("a b c d"))
self.assertEqual(['a', 'b', 'c', 'd'], getwords(" a b c d "))
def test_splitter_chars(self):
self.assertEqual(
[chr(i) for i in xrange(ord('a'),ord('z')+1)],
getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,p<q>r/s?t~u!v@w#x$y*z")
)
def test_joiner_chars(self):
self.assertEqual(["aec"], getwords(u"a'e\u0301c"))
def test_empty(self):
self.assertEqual([], getwords(''))
def test_returns_lowercase(self):
self.assertEqual(['foo', 'bar'], getwords('FOO BAR'))
def test_decompose_unicode(self):
self.assertEqual(getwords(u'foo\xe9bar'), ['fooebar'])
class TCgetfields(TestCase):
def test_simple(self):
self.assertEqual([['a', 'b'], ['c', 'd', 'e']], getfields('a b - c d e'))
def test_empty(self):
self.assertEqual([], getfields(''))
def test_cleans_empty_fields(self):
expected = [['a', 'bc', 'def']]
actual = getfields(' - a bc def')
self.assertEqual(expected, actual)
expected = [['bc', 'def']]
class TCunpack_fields(TestCase):
def test_with_fields(self):
expected = ['a', 'b', 'c', 'd', 'e', 'f']
actual = unpack_fields([['a'], ['b', 'c'], ['d', 'e', 'f']])
self.assertEqual(expected, actual)
def test_without_fields(self):
expected = ['a', 'b', 'c', 'd', 'e', 'f']
actual = unpack_fields(['a', 'b', 'c', 'd', 'e', 'f'])
self.assertEqual(expected, actual)
def test_empty(self):
self.assertEqual([], unpack_fields([]))
class TCWordCompare(TestCase):
def test_list(self):
self.assertEqual(100, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c', 'd']))
self.assertEqual(86, compare(['a', 'b', 'c', 'd'],['a', 'b', 'c']))
def test_unordered(self):
#Sometimes, users don't want fuzzy matching too much When they set the slider
#to 100, they don't expect a filename with the same words, but not the same order, to match.
#Thus, we want to return 99 in that case.
self.assertEqual(99, compare(['a', 'b', 'c', 'd'], ['d', 'b', 'c', 'a']))
def test_word_occurs_twice(self):
#if a word occurs twice in first, but once in second, we want the word to be only counted once
self.assertEqual(89, compare(['a', 'b', 'c', 'd', 'a'], ['d', 'b', 'c', 'a']))
def test_uses_copy_of_lists(self):
first = ['foo', 'bar']
second = ['bar', 'bleh']
compare(first, second)
self.assertEqual(['foo', 'bar'], first)
self.assertEqual(['bar', 'bleh'], second)
def test_word_weight(self):
self.assertEqual(int((6.0 / 13.0) * 100), compare(['foo', 'bar'], ['bar', 'bleh'], (WEIGHT_WORDS, )))
def test_similar_words(self):
self.assertEqual(100, compare(['the', 'white', 'stripes'],['the', 'whites', 'stripe'], (MATCH_SIMILAR_WORDS, )))
def test_empty(self):
self.assertEqual(0, compare([], []))
def test_with_fields(self):
self.assertEqual(67, compare([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']]))
def test_propagate_flags_with_fields(self):
def mock_compare(first, second, flags):
self.assertEqual((0, 1, 2, 3, 5), flags)
self.mock(engine, 'compare_fields', mock_compare)
compare([['a']], [['a']], (0, 1, 2, 3, 5))
class TCWordCompareWithFields(TestCase):
def test_simple(self):
self.assertEqual(67, compare_fields([['a', 'b'], ['c', 'd', 'e']], [['a', 'b'], ['c', 'd', 'f']]))
def test_empty(self):
self.assertEqual(0, compare_fields([], []))
def test_different_length(self):
self.assertEqual(0, compare_fields([['a'], ['b']], [['a'], ['b'], ['c']]))
def test_propagates_flags(self):
def mock_compare(first, second, flags):
self.assertEqual((0, 1, 2, 3, 5), flags)
self.mock(engine, 'compare_fields', mock_compare)
compare_fields([['a']], [['a']],(0, 1, 2, 3, 5))
def test_order(self):
first = [['a', 'b'], ['c', 'd', 'e']]
second = [['c', 'd', 'f'], ['a', 'b']]
self.assertEqual(0, compare_fields(first, second))
def test_no_order(self):
first = [['a','b'],['c','d','e']]
second = [['c','d','f'],['a','b']]
self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, )))
first = [['a','b'],['a','b']] #a field can only be matched once.
second = [['c','d','f'],['a','b']]
self.assertEqual(0, compare_fields(first, second, (NO_FIELD_ORDER, )))
first = [['a','b'],['a','b','c']]
second = [['c','d','f'],['a','b']]
self.assertEqual(33, compare_fields(first, second, (NO_FIELD_ORDER, )))
def test_compare_fields_without_order_doesnt_alter_fields(self):
#The NO_ORDER comp type altered the fields!
first = [['a','b'],['c','d','e']]
second = [['c','d','f'],['a','b']]
self.assertEqual(67, compare_fields(first, second, (NO_FIELD_ORDER, )))
self.assertEqual([['a','b'],['c','d','e']],first)
self.assertEqual([['c','d','f'],['a','b']],second)
class TCbuild_word_dict(TestCase):
def test_with_standard_words(self):
l = [NamedObject('foo bar',True)]
l.append(NamedObject('bar baz',True))
l.append(NamedObject('baz bleh foo',True))
d = build_word_dict(l)
self.assertEqual(4,len(d))
self.assertEqual(2,len(d['foo']))
self.assert_(l[0] in d['foo'])
self.assert_(l[2] in d['foo'])
self.assertEqual(2,len(d['bar']))
self.assert_(l[0] in d['bar'])
self.assert_(l[1] in d['bar'])
self.assertEqual(2,len(d['baz']))
self.assert_(l[1] in d['baz'])
self.assert_(l[2] in d['baz'])
self.assertEqual(1,len(d['bleh']))
self.assert_(l[2] in d['bleh'])
def test_unpack_fields(self):
o = NamedObject('')
o.words = [['foo','bar'],['baz']]
d = build_word_dict([o])
self.assertEqual(3,len(d))
self.assertEqual(1,len(d['foo']))
def test_words_are_unaltered(self):
o = NamedObject('')
o.words = [['foo','bar'],['baz']]
d = build_word_dict([o])
self.assertEqual([['foo','bar'],['baz']],o.words)
def test_object_instances_can_only_be_once_in_words_object_list(self):
o = NamedObject('foo foo',True)
d = build_word_dict([o])
self.assertEqual(1,len(d['foo']))
def test_job(self):
def do_progress(p,d=''):
self.log.append(p)
return True
j = job.Job(1,do_progress)
self.log = []
s = "foo bar"
build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j)
self.assertEqual(0,self.log[0])
self.assertEqual(33,self.log[1])
self.assertEqual(66,self.log[2])
self.assertEqual(100,self.log[3])
class TCmerge_similar_words(TestCase):
def test_some_similar_words(self):
d = {
'foobar':set([1]),
'foobar1':set([2]),
'foobar2':set([3]),
}
merge_similar_words(d)
self.assertEqual(1,len(d))
self.assertEqual(3,len(d['foobar']))
class TCreduce_common_words(TestCase):
def test_typical(self):
d = {
'foo': set([NamedObject('foo bar',True) for i in range(50)]),
'bar': set([NamedObject('foo bar',True) for i in range(49)])
}
reduce_common_words(d, 50)
self.assert_('foo' not in d)
self.assertEqual(49,len(d['bar']))
def test_dont_remove_objects_with_only_common_words(self):
d = {
'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]),
'uncommon': set([NamedObject("common uncommon",True)])
}
reduce_common_words(d, 50)
self.assertEqual(1,len(d['common']))
self.assertEqual(1,len(d['uncommon']))
def test_values_still_are_set_instances(self):
d = {
'common': set([NamedObject("common uncommon",True) for i in range(50)] + [NamedObject("common",True)]),
'uncommon': set([NamedObject("common uncommon",True)])
}
reduce_common_words(d, 50)
self.assert_(isinstance(d['common'],set))
self.assert_(isinstance(d['uncommon'],set))
def test_dont_raise_KeyError_when_a_word_has_been_removed(self):
#If a word has been removed by the reduce, an object in a subsequent common word that
#contains the word that has been removed would cause a KeyError.
d = {
'foo': set([NamedObject('foo bar baz',True) for i in range(50)]),
'bar': set([NamedObject('foo bar baz',True) for i in range(50)]),
'baz': set([NamedObject('foo bar baz',True) for i in range(49)])
}
try:
reduce_common_words(d, 50)
except KeyError:
self.fail()
def test_unpack_fields(self):
#object.words may be fields.
def create_it():
o = NamedObject('')
o.words = [['foo','bar'],['baz']]
return o
d = {
'foo': set([create_it() for i in range(50)])
}
try:
reduce_common_words(d, 50)
except TypeError:
self.fail("must support fields.")
def test_consider_a_reduced_common_word_common_even_after_reduction(self):
#There was a bug in the code that causeda word that has already been reduced not to
#be counted as a common word for subsequent words. For example, if 'foo' is processed
#as a common word, keeping a "foo bar" file in it, and the 'bar' is processed, "foo bar"
#would not stay in 'bar' because 'foo' is not a common word anymore.
only_common = NamedObject('foo bar',True)
d = {
'foo': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]),
'bar': set([NamedObject('foo bar baz',True) for i in range(49)] + [only_common]),
'baz': set([NamedObject('foo bar baz',True) for i in range(49)])
}
reduce_common_words(d, 50)
self.assertEqual(1,len(d['foo']))
self.assertEqual(1,len(d['bar']))
self.assertEqual(49,len(d['baz']))
class TCget_match(TestCase):
def test_simple(self):
o1 = NamedObject("foo bar",True)
o2 = NamedObject("bar bleh",True)
m = get_match(o1,o2)
self.assertEqual(50,m.percentage)
self.assertEqual(['foo','bar'],m.first.words)
self.assertEqual(['bar','bleh'],m.second.words)
self.assert_(m.first is o1)
self.assert_(m.second is o2)
def test_in(self):
o1 = NamedObject("foo",True)
o2 = NamedObject("bar",True)
m = get_match(o1,o2)
self.assert_(o1 in m)
self.assert_(o2 in m)
self.assert_(object() not in m)
def test_word_weight(self):
self.assertEqual(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage)
class GetMatches(TestCase):
def test_empty(self):
eq_(getmatches([]), [])
def test_simple(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
r = getmatches(l)
self.assertEqual(2,len(r))
seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh"
m = seek[0]
self.assertEqual(['foo','bar'],m.first.words)
self.assertEqual(['bar','bleh'],m.second.words)
seek = [m for m in r if m.percentage == 33] #"foo bar" and "a b c foo"
m = seek[0]
self.assertEqual(['foo','bar'],m.first.words)
self.assertEqual(['a','b','c','foo'],m.second.words)
def test_null_and_unrelated_objects(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")]
r = getmatches(l)
self.assertEqual(1,len(r))
m = r[0]
self.assertEqual(50,m.percentage)
self.assertEqual(['foo','bar'],m.first.words)
self.assertEqual(['bar','bleh'],m.second.words)
def test_twice_the_same_word(self):
l = [NamedObject("foo foo bar"),NamedObject("bar bleh")]
r = getmatches(l)
self.assertEqual(1,len(r))
def test_twice_the_same_word_when_preworded(self):
l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)]
r = getmatches(l)
self.assertEqual(1,len(r))
def test_two_words_match(self):
l = [NamedObject("foo bar"),NamedObject("foo bar bleh")]
r = getmatches(l)
self.assertEqual(1,len(r))
def test_match_files_with_only_common_words(self):
#If a word occurs more than 50 times, it is excluded from the matching process
#The problem with the common_word_threshold is that the files containing only common
#words will never be matched together. We *should* match them.
# This test assumes that the common word threashold const is 50
l = [NamedObject("foo") for i in range(50)]
r = getmatches(l)
self.assertEqual(1225,len(r))
def test_use_words_already_there_if_there(self):
o1 = NamedObject('foo')
o2 = NamedObject('bar')
o2.words = ['foo']
eq_(1, len(getmatches([o1,o2])))
def test_job(self):
def do_progress(p,d=''):
self.log.append(p)
return True
j = job.Job(1,do_progress)
self.log = []
s = "foo bar"
getmatches([NamedObject(s), NamedObject(s), NamedObject(s)], j=j)
self.assert_(len(self.log) > 2)
self.assertEqual(0,self.log[0])
self.assertEqual(100,self.log[-1])
def test_weight_words(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
m = getmatches(l, weight_words=True)[0]
self.assertEqual(int((6.0 / 13.0) * 100),m.percentage)
def test_similar_word(self):
l = [NamedObject("foobar"),NamedObject("foobars")]
eq_(len(getmatches(l, match_similar_words=True)), 1)
eq_(getmatches(l, match_similar_words=True)[0].percentage, 100)
l = [NamedObject("foobar"),NamedObject("foo")]
eq_(len(getmatches(l, match_similar_words=True)), 0) #too far
l = [NamedObject("bizkit"),NamedObject("bizket")]
eq_(len(getmatches(l, match_similar_words=True)), 1)
l = [NamedObject("foobar"),NamedObject("foosbar")]
eq_(len(getmatches(l, match_similar_words=True)), 1)
def test_single_object_with_similar_words(self):
l = [NamedObject("foo foos")]
eq_(len(getmatches(l, match_similar_words=True)), 0)
def test_double_words_get_counted_only_once(self):
l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")]
m = getmatches(l)[0]
self.assertEqual(75,m.percentage)
def test_with_fields(self):
o1 = NamedObject("foo bar - foo bleh")
o2 = NamedObject("foo bar - bleh bar")
o1.words = getfields(o1.name)
o2.words = getfields(o2.name)
m = getmatches([o1, o2])[0]
self.assertEqual(50, m.percentage)
def test_with_fields_no_order(self):
o1 = NamedObject("foo bar - foo bleh")
o2 = NamedObject("bleh bang - foo bar")
o1.words = getfields(o1.name)
o2.words = getfields(o2.name)
m = getmatches([o1, o2], no_field_order=True)[0]
eq_(m.percentage, 50)
def test_only_match_similar_when_the_option_is_set(self):
l = [NamedObject("foobar"),NamedObject("foobars")]
eq_(len(getmatches(l, match_similar_words=False)), 0)
def test_dont_recurse_do_match(self):
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
sys.setrecursionlimit(100)
files = [NamedObject('foo bar') for i in range(101)]
try:
getmatches(files)
except RuntimeError:
self.fail()
finally:
sys.setrecursionlimit(1000)
def test_min_match_percentage(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
r = getmatches(l, min_match_percentage=50)
self.assertEqual(1,len(r)) #Only "foo bar" / "bar bleh" should match
def test_MemoryError(self):
@log_calls
def mocked_match(first, second, flags):
if len(mocked_match.calls) > 42:
raise MemoryError()
return Match(first, second, 0)
objects = [NamedObject() for i in range(10)] # results in 45 matches
self.mock(engine, 'get_match', mocked_match)
try:
r = getmatches(objects)
except MemoryError:
self.fail('MemorryError must be handled')
self.assertEqual(42, len(r))
class GetMatchesByContents(TestCase):
def test_dont_compare_empty_files(self):
o1, o2 = no(size=0), no(size=0)
assert not getmatches_by_contents([o1, o2])
class TCGroup(TestCase):
def test_empy(self):
g = Group()
self.assertEqual(None,g.ref)
self.assertEqual([],g.dupes)
self.assertEqual(0,len(g.matches))
def test_add_match(self):
g = Group()
m = get_match(NamedObject("foo",True),NamedObject("bar",True))
g.add_match(m)
self.assert_(g.ref is m.first)
self.assertEqual([m.second],g.dupes)
self.assertEqual(1,len(g.matches))
self.assert_(m in g.matches)
def test_multiple_add_match(self):
g = Group()
o1 = NamedObject("a",True)
o2 = NamedObject("b",True)
o3 = NamedObject("c",True)
o4 = NamedObject("d",True)
g.add_match(get_match(o1,o2))
self.assert_(g.ref is o1)
self.assertEqual([o2],g.dupes)
self.assertEqual(1,len(g.matches))
g.add_match(get_match(o1,o3))
self.assertEqual([o2],g.dupes)
self.assertEqual(2,len(g.matches))
g.add_match(get_match(o2,o3))
self.assertEqual([o2,o3],g.dupes)
self.assertEqual(3,len(g.matches))
g.add_match(get_match(o1,o4))
self.assertEqual([o2,o3],g.dupes)
self.assertEqual(4,len(g.matches))
g.add_match(get_match(o2,o4))
self.assertEqual([o2,o3],g.dupes)
self.assertEqual(5,len(g.matches))
g.add_match(get_match(o3,o4))
self.assertEqual([o2,o3,o4],g.dupes)
self.assertEqual(6,len(g.matches))
def test_len(self):
g = Group()
self.assertEqual(0,len(g))
g.add_match(get_match(NamedObject("foo",True),NamedObject("bar",True)))
self.assertEqual(2,len(g))
def test_add_same_match_twice(self):
g = Group()
m = get_match(NamedObject("foo",True),NamedObject("foo",True))
g.add_match(m)
self.assertEqual(2,len(g))
self.assertEqual(1,len(g.matches))
g.add_match(m)
self.assertEqual(2,len(g))
self.assertEqual(1,len(g.matches))
def test_in(self):
g = Group()
o1 = NamedObject("foo",True)
o2 = NamedObject("bar",True)
self.assert_(o1 not in g)
g.add_match(get_match(o1,o2))
self.assert_(o1 in g)
self.assert_(o2 in g)
def test_remove(self):
g = Group()
o1 = NamedObject("foo",True)
o2 = NamedObject("bar",True)
o3 = NamedObject("bleh",True)
g.add_match(get_match(o1,o2))
g.add_match(get_match(o1,o3))
g.add_match(get_match(o2,o3))
self.assertEqual(3,len(g.matches))
self.assertEqual(3,len(g))
g.remove_dupe(o3)
self.assertEqual(1,len(g.matches))
self.assertEqual(2,len(g))
g.remove_dupe(o1)
self.assertEqual(0,len(g.matches))
self.assertEqual(0,len(g))
def test_remove_with_ref_dupes(self):
g = Group()
o1 = NamedObject("foo",True)
o2 = NamedObject("bar",True)
o3 = NamedObject("bleh",True)
g.add_match(get_match(o1,o2))
g.add_match(get_match(o1,o3))
g.add_match(get_match(o2,o3))
o1.is_ref = True
o2.is_ref = True
g.remove_dupe(o3)
self.assertEqual(0,len(g))
def test_switch_ref(self):
o1 = NamedObject(with_words=True)
o2 = NamedObject(with_words=True)
g = Group()
g.add_match(get_match(o1,o2))
self.assert_(o1 is g.ref)
g.switch_ref(o2)
self.assert_(o2 is g.ref)
self.assertEqual([o1],g.dupes)
g.switch_ref(o2)
self.assert_(o2 is g.ref)
g.switch_ref(NamedObject('',True))
self.assert_(o2 is g.ref)
def test_get_match_of(self):
g = Group()
for m in get_match_triangle():
g.add_match(m)
o = g.dupes[0]
m = g.get_match_of(o)
self.assert_(g.ref in m)
self.assert_(o in m)
self.assert_(g.get_match_of(NamedObject('',True)) is None)
self.assert_(g.get_match_of(g.ref) is None)
def test_percentage(self):
#percentage should return the avg percentage in relation to the ref
m1,m2,m3 = get_match_triangle()
m1 = Match(m1[0], m1[1], 100)
m2 = Match(m2[0], m2[1], 50)
m3 = Match(m3[0], m3[1], 33)
g = Group()
g.add_match(m1)
g.add_match(m2)
g.add_match(m3)
self.assertEqual(75,g.percentage)
g.switch_ref(g.dupes[0])
self.assertEqual(66,g.percentage)
g.remove_dupe(g.dupes[0])
self.assertEqual(33,g.percentage)
g.add_match(m1)
g.add_match(m2)
self.assertEqual(66,g.percentage)
def test_percentage_on_empty_group(self):
g = Group()
self.assertEqual(0,g.percentage)
def test_prioritize(self):
m1,m2,m3 = get_match_triangle()
o1 = m1.first
o2 = m1.second
o3 = m2.second
o1.name = 'c'
o2.name = 'b'
o3.name = 'a'
g = Group()
g.add_match(m1)
g.add_match(m2)
g.add_match(m3)
self.assert_(o1 is g.ref)
g.prioritize(lambda x:x.name)
self.assert_(o3 is g.ref)
def test_prioritize_with_tie_breaker(self):
# if the ref has the same key as one or more of the dupe, run the tie_breaker func among them
g = get_test_group()
o1, o2, o3 = g.ordered
tie_breaker = lambda ref, dupe: dupe is o3
g.prioritize(lambda x:0, tie_breaker)
self.assertTrue(g.ref is o3)
def test_prioritize_with_tie_breaker_runs_on_all_dupes(self):
# Even if a dupe is chosen to switch with ref with a tie breaker, we still run the tie breaker
# with other dupes and the newly chosen ref
g = get_test_group()
o1, o2, o3 = g.ordered
o1.foo = 1
o2.foo = 2
o3.foo = 3
tie_breaker = lambda ref, dupe: dupe.foo > ref.foo
g.prioritize(lambda x:0, tie_breaker)
self.assertTrue(g.ref is o3)
def test_prioritize_with_tie_breaker_runs_only_on_tie_dupes(self):
# The tie breaker only runs on dupes that had the same value for the key_func
g = get_test_group()
o1, o2, o3 = g.ordered
o1.foo = 2
o2.foo = 2
o3.foo = 1
o1.bar = 1
o2.bar = 2
o3.bar = 3
key_func = lambda x: -x.foo
tie_breaker = lambda ref, dupe: dupe.bar > ref.bar
g.prioritize(key_func, tie_breaker)
self.assertTrue(g.ref is o2)
def test_list_like(self):
g = Group()
o1,o2 = (NamedObject("foo",True),NamedObject("bar",True))
g.add_match(get_match(o1,o2))
self.assert_(g[0] is o1)
self.assert_(g[1] is o2)
def test_discard_matches(self):
g = Group()
o1,o2,o3 = (NamedObject("foo",True),NamedObject("bar",True),NamedObject("baz",True))
g.add_match(get_match(o1,o2))
g.add_match(get_match(o1,o3))
g.discard_matches()
self.assertEqual(1,len(g.matches))
self.assertEqual(0,len(g.candidates))
class TCget_groups(TestCase):
def test_empty(self):
r = get_groups([])
self.assertEqual([],r)
def test_simple(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
matches = getmatches(l)
m = matches[0]
r = get_groups(matches)
self.assertEqual(1,len(r))
g = r[0]
self.assert_(g.ref is m.first)
self.assertEqual([m.second],g.dupes)
def test_group_with_multiple_matches(self):
#This results in 3 matches
l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")]
matches = getmatches(l)
r = get_groups(matches)
self.assertEqual(1,len(r))
g = r[0]
self.assertEqual(3,len(g))
def test_must_choose_a_group(self):
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")]
#There will be 2 groups here: group "a b" and group "c d"
#"b c" can go either of them, but not both.
matches = getmatches(l)
r = get_groups(matches)
self.assertEqual(2,len(r))
self.assertEqual(5,len(r[0])+len(r[1]))
def test_should_all_go_in_the_same_group(self):
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")]
#There will be 2 groups here: group "a b" and group "c d"
#"b c" can fit in both, but it must be in only one of them
matches = getmatches(l)
r = get_groups(matches)
self.assertEqual(1,len(r))
def test_give_priority_to_matches_with_higher_percentage(self):
o1 = NamedObject(with_words=True)
o2 = NamedObject(with_words=True)
o3 = NamedObject(with_words=True)
m1 = Match(o1, o2, 1)
m2 = Match(o2, o3, 2)
r = get_groups([m1,m2])
self.assertEqual(1,len(r))
g = r[0]
self.assertEqual(2,len(g))
self.assert_(o1 not in g)
self.assert_(o2 in g)
self.assert_(o3 in g)
def test_four_sized_group(self):
l = [NamedObject("foobar") for i in xrange(4)]
m = getmatches(l)
r = get_groups(m)
self.assertEqual(1,len(r))
self.assertEqual(4,len(r[0]))
def test_referenced_by_ref2(self):
o1 = NamedObject(with_words=True)
o2 = NamedObject(with_words=True)
o3 = NamedObject(with_words=True)
m1 = get_match(o1,o2)
m2 = get_match(o3,o1)
m3 = get_match(o3,o2)
r = get_groups([m1,m2,m3])
self.assertEqual(3,len(r[0]))
def test_job(self):
def do_progress(p,d=''):
self.log.append(p)
return True
self.log = []
j = job.Job(1,do_progress)
m1,m2,m3 = get_match_triangle()
#101%: To make sure it is processed first so the job test works correctly
m4 = Match(NamedObject('a',True), NamedObject('a',True), 101)
get_groups([m1,m2,m3,m4],j)
self.assertEqual(0,self.log[0])
self.assertEqual(100,self.log[-1])
def test_group_admissible_discarded_dupes(self):
# If, with a (A, B, C, D) set, all match with A, but C and D don't match with B and that the
# (A, B) match is the highest (thus resulting in an (A, B) group), still match C and D
# in a separate group instead of discarding them.
A, B, C, D = [NamedObject() for _ in range(4)]
m1 = Match(A, B, 90) # This is the strongest "A" match
m2 = Match(A, C, 80) # Because C doesn't match with B, it won't be in the group
m3 = Match(A, D, 80) # Same thing for D
m4 = Match(C, D, 70) # However, because C and D match, they should have their own group.
groups = get_groups([m1, m2, m3, m4])
eq_(len(groups), 2)
g1, g2 = groups
assert A in g1
assert B in g1
assert C in g2
assert D in g2

View File

@@ -1,152 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/05/02
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import cStringIO
import xml.dom.minidom
from nose.tools import eq_
from ..ignore import *
def test_empty():
il = IgnoreList()
eq_(0,len(il))
assert not il.AreIgnored('foo','bar')
def test_simple():
il = IgnoreList()
il.Ignore('foo','bar')
assert il.AreIgnored('foo','bar')
assert il.AreIgnored('bar','foo')
assert not il.AreIgnored('foo','bleh')
assert not il.AreIgnored('bleh','bar')
eq_(1,len(il))
def test_multiple():
il = IgnoreList()
il.Ignore('foo','bar')
il.Ignore('foo','bleh')
il.Ignore('bleh','bar')
il.Ignore('aybabtu','bleh')
assert il.AreIgnored('foo','bar')
assert il.AreIgnored('bar','foo')
assert il.AreIgnored('foo','bleh')
assert il.AreIgnored('bleh','bar')
assert not il.AreIgnored('aybabtu','bar')
eq_(4,len(il))
def test_clear():
il = IgnoreList()
il.Ignore('foo','bar')
il.Clear()
assert not il.AreIgnored('foo','bar')
assert not il.AreIgnored('bar','foo')
eq_(0,len(il))
def test_add_same_twice():
il = IgnoreList()
il.Ignore('foo','bar')
il.Ignore('bar','foo')
eq_(1,len(il))
def test_save_to_xml():
il = IgnoreList()
il.Ignore('foo','bar')
il.Ignore('foo','bleh')
il.Ignore('bleh','bar')
f = cStringIO.StringIO()
il.save_to_xml(f)
f.seek(0)
doc = xml.dom.minidom.parse(f)
root = doc.documentElement
eq_('ignore_list',root.nodeName)
children = [c for c in root.childNodes if c.localName]
eq_(2,len(children))
eq_(2,len([c for c in children if c.nodeName == 'file']))
f1,f2 = children
subchildren = [c for c in f1.childNodes if c.localName == 'file'] +\
[c for c in f2.childNodes if c.localName == 'file']
eq_(3,len(subchildren))
def test_SaveThenLoad():
il = IgnoreList()
il.Ignore('foo','bar')
il.Ignore('foo','bleh')
il.Ignore('bleh','bar')
il.Ignore(u'\u00e9','bar')
f = cStringIO.StringIO()
il.save_to_xml(f)
f.seek(0)
il = IgnoreList()
il.load_from_xml(f)
eq_(4,len(il))
assert il.AreIgnored(u'\u00e9','bar')
def test_LoadXML_with_empty_file_tags():
f = cStringIO.StringIO()
f.write('<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
f.seek(0)
il = IgnoreList()
il.load_from_xml(f)
eq_(0,len(il))
def test_AreIgnore_works_when_a_child_is_a_key_somewhere_else():
il = IgnoreList()
il.Ignore('foo','bar')
il.Ignore('bar','baz')
assert il.AreIgnored('bar','foo')
def test_no_dupes_when_a_child_is_a_key_somewhere_else():
il = IgnoreList()
il.Ignore('foo','bar')
il.Ignore('bar','baz')
il.Ignore('bar','foo')
eq_(2,len(il))
def test_iterate():
#It must be possible to iterate through ignore list
il = IgnoreList()
expected = [('foo','bar'),('bar','baz'),('foo','baz')]
for i in expected:
il.Ignore(i[0],i[1])
for i in il:
expected.remove(i) #No exception should be raised
assert not expected #expected should be empty
def test_filter():
il = IgnoreList()
il.Ignore('foo','bar')
il.Ignore('bar','baz')
il.Ignore('foo','baz')
il.Filter(lambda f,s: f == 'bar')
eq_(1,len(il))
assert not il.AreIgnored('foo','bar')
assert il.AreIgnored('bar','baz')
def test_save_with_non_ascii_non_unicode_items():
il = IgnoreList()
il.Ignore('\xac','\xbf')
f = cStringIO.StringIO()
try:
il.save_to_xml(f)
except Exception as e:
raise AssertionError(unicode(e))
def test_len():
il = IgnoreList()
eq_(0,len(il))
il.Ignore('foo','bar')
eq_(1,len(il))
def test_nonzero():
il = IgnoreList()
assert not il
il.Ignore('foo','bar')
assert il

View File

@@ -1,717 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/02/23
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import unittest
import StringIO
import xml.dom.minidom
import os.path as op
from hsutil.path import Path
from hsutil.testcase import TestCase
from hsutil.misc import first
from . import engine_test, data
from .. import engine
from ..results import *
class NamedObject(engine_test.NamedObject):
path = property(lambda x:Path('basepath') + x.name)
is_ref = False
def __nonzero__(self):
return False #Make sure that operations are made correctly when the bool value of files is false.
# Returns a group set that looks like that:
# "foo bar" (1)
# "bar bleh" (1024)
# "foo bleh" (1)
# "ibabtu" (1)
# "ibabtu" (1)
def GetTestGroups():
objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")]
objects[1].size = 1024
matches = engine.getmatches(objects) #we should have 5 matches
groups = engine.get_groups(matches) #We should have 2 groups
for g in groups:
g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is
groups.sort(key=len, reverse=True) # We want the group with 3 members to be first.
return (objects,matches,groups)
class TCResultsEmpty(TestCase):
def setUp(self):
self.results = Results(data)
def test_apply_invalid_filter(self):
# If the applied filter is an invalid regexp, just ignore the filter.
self.results.apply_filter('[') # invalid
self.test_stat_line() # make sure that the stats line isn't saying we applied a '[' filter
def test_stat_line(self):
self.assertEqual("0 / 0 (0.00 B / 0.00 B) duplicates marked.",self.results.stat_line)
def test_groups(self):
self.assertEqual(0,len(self.results.groups))
def test_get_group_of_duplicate(self):
self.assert_(self.results.get_group_of_duplicate('foo') is None)
def test_save_to_xml(self):
f = StringIO.StringIO()
self.results.save_to_xml(f)
f.seek(0)
doc = xml.dom.minidom.parse(f)
root = doc.documentElement
self.assertEqual('results',root.nodeName)
class TCResultsWithSomeGroups(TestCase):
def setUp(self):
self.results = Results(data)
self.objects,self.matches,self.groups = GetTestGroups()
self.results.groups = self.groups
def test_stat_line(self):
self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
def test_groups(self):
self.assertEqual(2,len(self.results.groups))
def test_get_group_of_duplicate(self):
for o in self.objects:
g = self.results.get_group_of_duplicate(o)
self.assert_(isinstance(g, engine.Group))
self.assert_(o in g)
self.assert_(self.results.get_group_of_duplicate(self.groups[0]) is None)
def test_remove_duplicates(self):
g1,g2 = self.results.groups
self.results.remove_duplicates([g1.dupes[0]])
self.assertEqual(2,len(g1))
self.assert_(g1 in self.results.groups)
self.results.remove_duplicates([g1.ref])
self.assertEqual(2,len(g1))
self.assert_(g1 in self.results.groups)
self.results.remove_duplicates([g1.dupes[0]])
self.assertEqual(0,len(g1))
self.assert_(g1 not in self.results.groups)
self.results.remove_duplicates([g2.dupes[0]])
self.assertEqual(0,len(g2))
self.assert_(g2 not in self.results.groups)
self.assertEqual(0,len(self.results.groups))
def test_remove_duplicates_with_ref_files(self):
g1,g2 = self.results.groups
self.objects[0].is_ref = True
self.objects[1].is_ref = True
self.results.remove_duplicates([self.objects[2]])
self.assertEqual(0,len(g1))
self.assert_(g1 not in self.results.groups)
def test_make_ref(self):
g = self.results.groups[0]
d = g.dupes[0]
self.results.make_ref(d)
self.assert_(d is g.ref)
def test_sort_groups(self):
self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref.
g1,g2 = self.groups
self.results.sort_groups(2) #2 is the key for size
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
self.results.sort_groups(2,False)
self.assert_(self.results.groups[0] is g1)
self.assert_(self.results.groups[1] is g2)
def test_set_groups_when_sorted(self):
self.results.make_ref(self.objects[1]) #We want to make the 1024 sized object to go ref.
self.results.sort_groups(2)
objects,matches,groups = GetTestGroups()
g1,g2 = groups
g1.switch_ref(objects[1])
self.results.groups = groups
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
def test_get_dupe_list(self):
self.assertEqual([self.objects[1],self.objects[2],self.objects[4]],self.results.dupes)
def test_dupe_list_is_cached(self):
self.assert_(self.results.dupes is self.results.dupes)
def test_dupe_list_cache_is_invalidated_when_needed(self):
o1,o2,o3,o4,o5 = self.objects
self.assertEqual([o2,o3,o5],self.results.dupes)
self.results.make_ref(o2)
self.assertEqual([o1,o3,o5],self.results.dupes)
objects,matches,groups = GetTestGroups()
o1,o2,o3,o4,o5 = objects
self.results.groups = groups
self.assertEqual([o2,o3,o5],self.results.dupes)
def test_dupe_list_sort(self):
o1,o2,o3,o4,o5 = self.objects
o1.size = 5
o2.size = 4
o3.size = 3
o4.size = 2
o5.size = 1
self.results.sort_dupes(2)
self.assertEqual([o5,o3,o2],self.results.dupes)
self.results.sort_dupes(2,False)
self.assertEqual([o2,o3,o5],self.results.dupes)
def test_dupe_list_remember_sort(self):
o1,o2,o3,o4,o5 = self.objects
o1.size = 5
o2.size = 4
o3.size = 3
o4.size = 2
o5.size = 1
self.results.sort_dupes(2)
self.results.make_ref(o2)
self.assertEqual([o5,o3,o1],self.results.dupes)
def test_dupe_list_sort_delta_values(self):
o1,o2,o3,o4,o5 = self.objects
o1.size = 10
o2.size = 2 #-8
o3.size = 3 #-7
o4.size = 20
o5.size = 1 #-19
self.results.sort_dupes(2,delta=True)
self.assertEqual([o5,o2,o3],self.results.dupes)
def test_sort_empty_list(self):
#There was an infinite loop when sorting an empty list.
r = Results(data)
r.sort_dupes(0)
self.assertEqual([],r.dupes)
def test_dupe_list_update_on_remove_duplicates(self):
o1,o2,o3,o4,o5 = self.objects
self.assertEqual(3,len(self.results.dupes))
self.results.remove_duplicates([o2])
self.assertEqual(2,len(self.results.dupes))
class TCResultsMarkings(TestCase):
def setUp(self):
self.results = Results(data)
self.objects,self.matches,self.groups = GetTestGroups()
self.results.groups = self.groups
def test_stat_line(self):
self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark(self.objects[1])
self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark_invert()
self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark_invert()
self.results.unmark(self.objects[1])
self.results.mark(self.objects[2])
self.results.mark(self.objects[4])
self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.mark(self.objects[0]) #this is a ref, it can't be counted
self.assertEqual("2 / 3 (2.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.groups = self.groups
self.assertEqual("0 / 3 (0.00 B / 1.01 KB) duplicates marked.",self.results.stat_line)
def test_with_ref_duplicate(self):
self.objects[1].is_ref = True
self.results.groups = self.groups
self.assert_(not self.results.mark(self.objects[1]))
self.results.mark(self.objects[2])
self.assertEqual("1 / 2 (1.00 B / 2.00 B) duplicates marked.",self.results.stat_line)
def test_perform_on_marked(self):
def log_object(o):
log.append(o)
return True
log = []
self.results.mark_all()
self.results.perform_on_marked(log_object,False)
self.assert_(self.objects[1] in log)
self.assert_(self.objects[2] in log)
self.assert_(self.objects[4] in log)
self.assertEqual(3,len(log))
log = []
self.results.mark_none()
self.results.mark(self.objects[4])
self.results.perform_on_marked(log_object,True)
self.assertEqual(1,len(log))
self.assert_(self.objects[4] in log)
self.assertEqual(1,len(self.results.groups))
def test_perform_on_marked_with_problems(self):
def log_object(o):
log.append(o)
return o is not self.objects[1]
log = []
self.results.mark_all()
self.assert_(self.results.is_marked(self.objects[1]))
self.assertEqual(1,self.results.perform_on_marked(log_object, True))
self.assertEqual(3,len(log))
self.assertEqual(1,len(self.results.groups))
self.assertEqual(2,len(self.results.groups[0]))
self.assert_(self.objects[1] in self.results.groups[0])
self.assert_(not self.results.is_marked(self.objects[2]))
self.assert_(self.results.is_marked(self.objects[1]))
def test_perform_on_marked_with_ref(self):
def log_object(o):
log.append(o)
return True
log = []
self.objects[0].is_ref = True
self.objects[1].is_ref = True
self.results.mark_all()
self.results.perform_on_marked(log_object,True)
self.assert_(self.objects[1] not in log)
self.assert_(self.objects[2] in log)
self.assert_(self.objects[4] in log)
self.assertEqual(2,len(log))
self.assertEqual(0,len(self.results.groups))
def test_perform_on_marked_remove_objects_only_at_the_end(self):
def check_groups(o):
self.assertEqual(3,len(g1))
self.assertEqual(2,len(g2))
return True
g1,g2 = self.results.groups
self.results.mark_all()
self.results.perform_on_marked(check_groups,True)
self.assertEqual(0,len(g1))
self.assertEqual(0,len(g2))
self.assertEqual(0,len(self.results.groups))
def test_remove_duplicates(self):
g1 = self.results.groups[0]
g2 = self.results.groups[1]
self.results.mark(g1.dupes[0])
self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.remove_duplicates([g1.dupes[1]])
self.assertEqual("1 / 2 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.remove_duplicates([g1.dupes[0]])
self.assertEqual("0 / 1 (0.00 B / 1.00 B) duplicates marked.",self.results.stat_line)
def test_make_ref(self):
g = self.results.groups[0]
d = g.dupes[0]
self.results.mark(d)
self.assertEqual("1 / 3 (1.00 KB / 1.01 KB) duplicates marked.",self.results.stat_line)
self.results.make_ref(d)
self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line)
self.results.make_ref(d)
self.assertEqual("0 / 3 (0.00 B / 3.00 B) duplicates marked.",self.results.stat_line)
def test_SaveXML(self):
self.results.mark(self.objects[1])
self.results.mark_invert()
f = StringIO.StringIO()
self.results.save_to_xml(f)
f.seek(0)
doc = xml.dom.minidom.parse(f)
root = doc.documentElement
g1,g2 = root.getElementsByTagName('group')
d1,d2,d3 = g1.getElementsByTagName('file')
self.assertEqual('n',d1.getAttributeNode('marked').nodeValue)
self.assertEqual('n',d2.getAttributeNode('marked').nodeValue)
self.assertEqual('y',d3.getAttributeNode('marked').nodeValue)
d1,d2 = g2.getElementsByTagName('file')
self.assertEqual('n',d1.getAttributeNode('marked').nodeValue)
self.assertEqual('y',d2.getAttributeNode('marked').nodeValue)
def test_LoadXML(self):
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
self.results.mark(self.objects[1])
self.results.mark_invert()
f = StringIO.StringIO()
self.results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
self.assert_(not r.is_marked(self.objects[0]))
self.assert_(not r.is_marked(self.objects[1]))
self.assert_(r.is_marked(self.objects[2]))
self.assert_(not r.is_marked(self.objects[3]))
self.assert_(r.is_marked(self.objects[4]))
class TCResultsXML(TestCase):
def setUp(self):
self.results = Results(data)
self.objects, self.matches, self.groups = GetTestGroups()
self.results.groups = self.groups
def get_file(self, path): # use this as a callback for load_from_xml
return [o for o in self.objects if o.path == path][0]
def test_save_to_xml(self):
self.objects[0].is_ref = True
self.objects[0].words = [['foo','bar']]
f = StringIO.StringIO()
self.results.save_to_xml(f)
f.seek(0)
doc = xml.dom.minidom.parse(f)
root = doc.documentElement
self.assertEqual('results',root.nodeName)
children = [c for c in root.childNodes if c.localName]
self.assertEqual(2,len(children))
self.assertEqual(2,len([c for c in children if c.nodeName == 'group']))
g1,g2 = children
children = [c for c in g1.childNodes if c.localName]
self.assertEqual(6,len(children))
self.assertEqual(3,len([c for c in children if c.nodeName == 'file']))
self.assertEqual(3,len([c for c in children if c.nodeName == 'match']))
d1,d2,d3 = [c for c in children if c.nodeName == 'file']
self.assertEqual(op.join('basepath','foo bar'),d1.getAttributeNode('path').nodeValue)
self.assertEqual(op.join('basepath','bar bleh'),d2.getAttributeNode('path').nodeValue)
self.assertEqual(op.join('basepath','foo bleh'),d3.getAttributeNode('path').nodeValue)
self.assertEqual('y',d1.getAttributeNode('is_ref').nodeValue)
self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue)
self.assertEqual('n',d3.getAttributeNode('is_ref').nodeValue)
self.assertEqual('foo,bar',d1.getAttributeNode('words').nodeValue)
self.assertEqual('bar,bleh',d2.getAttributeNode('words').nodeValue)
self.assertEqual('foo,bleh',d3.getAttributeNode('words').nodeValue)
children = [c for c in g2.childNodes if c.localName]
self.assertEqual(3,len(children))
self.assertEqual(2,len([c for c in children if c.nodeName == 'file']))
self.assertEqual(1,len([c for c in children if c.nodeName == 'match']))
d1,d2 = [c for c in children if c.nodeName == 'file']
self.assertEqual(op.join('basepath','ibabtu'),d1.getAttributeNode('path').nodeValue)
self.assertEqual(op.join('basepath','ibabtu'),d2.getAttributeNode('path').nodeValue)
self.assertEqual('n',d1.getAttributeNode('is_ref').nodeValue)
self.assertEqual('n',d2.getAttributeNode('is_ref').nodeValue)
self.assertEqual('ibabtu',d1.getAttributeNode('words').nodeValue)
self.assertEqual('ibabtu',d2.getAttributeNode('words').nodeValue)
def test_LoadXML(self):
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
self.objects[0].is_ref = True
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
f = StringIO.StringIO()
self.results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
self.assertEqual(2,len(r.groups))
g1,g2 = r.groups
self.assertEqual(3,len(g1))
self.assert_(g1[0].is_ref)
self.assert_(not g1[1].is_ref)
self.assert_(not g1[2].is_ref)
self.assert_(g1[0] is self.objects[0])
self.assert_(g1[1] is self.objects[1])
self.assert_(g1[2] is self.objects[2])
self.assertEqual(['foo','bar'],g1[0].words)
self.assertEqual(['bar','bleh'],g1[1].words)
self.assertEqual(['foo','bleh'],g1[2].words)
self.assertEqual(2,len(g2))
self.assert_(not g2[0].is_ref)
self.assert_(not g2[1].is_ref)
self.assert_(g2[0] is self.objects[3])
self.assert_(g2[1] is self.objects[4])
self.assertEqual(['ibabtu'],g2[0].words)
self.assertEqual(['ibabtu'],g2[1].words)
def test_LoadXML_with_filename(self):
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
filename = op.join(self.tmpdir(), 'dupeguru_results.xml')
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
self.results.save_to_xml(filename)
r = Results(data)
r.load_from_xml(filename,get_file)
self.assertEqual(2,len(r.groups))
def test_LoadXML_with_some_files_that_dont_exist_anymore(self):
def get_file(path):
if path.endswith('ibabtu 2'):
return None
return [f for f in self.objects if str(f.path) == path][0]
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
f = StringIO.StringIO()
self.results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
self.assertEqual(1,len(r.groups))
self.assertEqual(3,len(r.groups[0]))
def test_LoadXML_missing_attributes_and_bogus_elements(self):
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
doc = xml.dom.minidom.Document()
root = doc.appendChild(doc.createElement('foobar')) #The root element shouldn't matter, really.
group_node = root.appendChild(doc.createElement('group'))
dupe_node = group_node.appendChild(doc.createElement('file')) #Perfectly correct file
dupe_node.setAttribute('path',op.join('basepath','foo bar'))
dupe_node.setAttribute('is_ref','y')
dupe_node.setAttribute('words','foo,bar')
dupe_node = group_node.appendChild(doc.createElement('file')) #is_ref missing, default to 'n'
dupe_node.setAttribute('path',op.join('basepath','foo bleh'))
dupe_node.setAttribute('words','foo,bleh')
dupe_node = group_node.appendChild(doc.createElement('file')) #words are missing, invalid.
dupe_node.setAttribute('path',op.join('basepath','bar bleh'))
dupe_node = group_node.appendChild(doc.createElement('file')) #path is missing, invalid.
dupe_node.setAttribute('words','foo,bleh')
dupe_node = group_node.appendChild(doc.createElement('foobar')) #Invalid element name
dupe_node.setAttribute('path',op.join('basepath','bar bleh'))
dupe_node.setAttribute('is_ref','y')
dupe_node.setAttribute('words','bar,bleh')
match_node = group_node.appendChild(doc.createElement('match')) # match pointing to a bad index
match_node.setAttribute('first', '42')
match_node.setAttribute('second', '45')
match_node = group_node.appendChild(doc.createElement('match')) # match with missing attrs
match_node = group_node.appendChild(doc.createElement('match')) # match with non-int values
match_node.setAttribute('first', 'foo')
match_node.setAttribute('second', 'bar')
match_node.setAttribute('percentage', 'baz')
group_node = root.appendChild(doc.createElement('foobar')) #invalid group
group_node = root.appendChild(doc.createElement('group')) #empty group
f = StringIO.StringIO()
doc.writexml(f,'\t','\t','\n',encoding='utf-8')
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
self.assertEqual(1,len(r.groups))
self.assertEqual(2,len(r.groups[0]))
def test_xml_non_ascii(self):
def get_file(path):
if path == op.join('basepath',u'\xe9foo bar'):
return objects[0]
if path == op.join('basepath',u'bar bleh'):
return objects[1]
objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)]
matches = engine.getmatches(objects) #we should have 5 matches
groups = engine.get_groups(matches) #We should have 2 groups
for g in groups:
g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is
results = Results(data)
results.groups = groups
f = StringIO.StringIO()
results.save_to_xml(f)
f.seek(0)
r = Results(data)
r.load_from_xml(f,get_file)
g = r.groups[0]
self.assertEqual(u"\xe9foo bar",g[0].name)
self.assertEqual(['efoo','bar'],g[0].words)
def test_load_invalid_xml(self):
f = StringIO.StringIO()
f.write('<this is invalid')
f.seek(0)
r = Results(data)
r.load_from_xml(f,None)
self.assertEqual(0,len(r.groups))
def test_load_non_existant_xml(self):
r = Results(data)
try:
r.load_from_xml('does_not_exist.xml', None)
except IOError:
self.fail()
self.assertEqual(0,len(r.groups))
def test_remember_match_percentage(self):
group = self.groups[0]
d1, d2, d3 = group
fake_matches = set()
fake_matches.add(engine.Match(d1, d2, 42))
fake_matches.add(engine.Match(d1, d3, 43))
fake_matches.add(engine.Match(d2, d3, 46))
group.matches = fake_matches
f = StringIO.StringIO()
results = self.results
results.save_to_xml(f)
f.seek(0)
results = Results(data)
results.load_from_xml(f, self.get_file)
group = results.groups[0]
d1, d2, d3 = group
match = group.get_match_of(d2) #d1 - d2
self.assertEqual(42, match[2])
match = group.get_match_of(d3) #d1 - d3
self.assertEqual(43, match[2])
group.switch_ref(d2)
match = group.get_match_of(d3) #d2 - d3
self.assertEqual(46, match[2])
def test_save_and_load(self):
# previously, when reloading matches, they wouldn't be reloaded as namedtuples
f = StringIO.StringIO()
self.results.save_to_xml(f)
f.seek(0)
self.results.load_from_xml(f, self.get_file)
first(self.results.groups[0].matches).percentage
class TCResultsFilter(TestCase):
def setUp(self):
self.results = Results(data)
self.objects, self.matches, self.groups = GetTestGroups()
self.results.groups = self.groups
self.results.apply_filter(r'foo')
def test_groups(self):
self.assertEqual(1, len(self.results.groups))
self.assert_(self.results.groups[0] is self.groups[0])
def test_dupes(self):
# There are 2 objects matching. The first one is ref. Only the 3rd one is supposed to be in dupes.
self.assertEqual(1, len(self.results.dupes))
self.assert_(self.results.dupes[0] is self.objects[2])
def test_cancel_filter(self):
self.results.apply_filter(None)
self.assertEqual(3, len(self.results.dupes))
self.assertEqual(2, len(self.results.groups))
def test_dupes_reconstructed_filtered(self):
# make_ref resets self.__dupes to None. When it's reconstructed, we want it filtered
dupe = self.results.dupes[0] #3rd object
self.results.make_ref(dupe)
self.assertEqual(1, len(self.results.dupes))
self.assert_(self.results.dupes[0] is self.objects[0])
def test_include_ref_dupes_in_filter(self):
# When only the ref of a group match the filter, include it in the group
self.results.apply_filter(None)
self.results.apply_filter(r'foo bar')
self.assertEqual(1, len(self.results.groups))
self.assertEqual(0, len(self.results.dupes))
def test_filters_build_on_one_another(self):
self.results.apply_filter(r'bar')
self.assertEqual(1, len(self.results.groups))
self.assertEqual(0, len(self.results.dupes))
def test_stat_line(self):
expected = '0 / 1 (0.00 B / 1.00 B) duplicates marked. filter: foo'
self.assertEqual(expected, self.results.stat_line)
self.results.apply_filter(r'bar')
expected = '0 / 0 (0.00 B / 0.00 B) duplicates marked. filter: foo --> bar'
self.assertEqual(expected, self.results.stat_line)
self.results.apply_filter(None)
expected = '0 / 3 (0.00 B / 1.01 KB) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
def test_mark_count_is_filtered_as_well(self):
self.results.apply_filter(None)
# We don't want to perform mark_all() because we want the mark list to contain objects
for dupe in self.results.dupes:
self.results.mark(dupe)
self.results.apply_filter(r'foo')
expected = '1 / 1 (1.00 B / 1.00 B) duplicates marked. filter: foo'
self.assertEqual(expected, self.results.stat_line)
def test_sort_groups(self):
self.results.apply_filter(None)
self.results.make_ref(self.objects[1]) # to have the 1024 b obkect as ref
g1,g2 = self.groups
self.results.apply_filter('a') # Matches both group
self.results.sort_groups(2) #2 is the key for size
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
self.results.apply_filter(None)
self.assert_(self.results.groups[0] is g2)
self.assert_(self.results.groups[1] is g1)
self.results.sort_groups(2, False)
self.results.apply_filter('a')
self.assert_(self.results.groups[1] is g2)
self.assert_(self.results.groups[0] is g1)
def test_set_group(self):
#We want the new group to be filtered
self.objects, self.matches, self.groups = GetTestGroups()
self.results.groups = self.groups
self.assertEqual(1, len(self.results.groups))
self.assert_(self.results.groups[0] is self.groups[0])
def test_load_cancels_filter(self):
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
filename = op.join(self.tmpdir(), 'dupeguru_results.xml')
self.objects[4].name = 'ibabtu 2' #we can't have 2 files with the same path
self.results.save_to_xml(filename)
r = Results(data)
r.apply_filter('foo')
r.load_from_xml(filename,get_file)
self.assertEqual(2,len(r.groups))
def test_remove_dupe(self):
self.results.remove_duplicates([self.results.dupes[0]])
self.results.apply_filter(None)
self.assertEqual(2,len(self.results.groups))
self.assertEqual(2,len(self.results.dupes))
self.results.apply_filter('ibabtu')
self.results.remove_duplicates([self.results.dupes[0]])
self.results.apply_filter(None)
self.assertEqual(1,len(self.results.groups))
self.assertEqual(1,len(self.results.dupes))
def test_filter_is_case_insensitive(self):
self.results.apply_filter(None)
self.results.apply_filter('FOO')
self.assertEqual(1, len(self.results.dupes))
def test_make_ref_on_filtered_out_doesnt_mess_stats(self):
# When filtered, a group containing filtered out dupes will display them as being reference.
# When calling make_ref on such a dupe, the total size and dupecount stats gets messed up
# because they are *not* counted in the stats in the first place.
g1, g2 = self.groups
bar_bleh = g1[1] # The "bar bleh" dupe is filtered out
self.results.make_ref(bar_bleh)
# Now the stats should display *2* markable dupes (instead of 1)
expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked. filter: foo'
self.assertEqual(expected, self.results.stat_line)
self.results.apply_filter(None) # Now let's make sure our unfiltered results aren't fucked up
expected = '0 / 3 (0.00 B / 3.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
class TCResultsRefFile(TestCase):
def setUp(self):
self.results = Results(data)
self.objects, self.matches, self.groups = GetTestGroups()
self.objects[0].is_ref = True
self.objects[1].is_ref = True
self.results.groups = self.groups
def test_stat_line(self):
expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
def test_make_ref(self):
d = self.results.groups[0].dupes[1] #non-ref
r = self.results.groups[0].ref
self.results.make_ref(d)
expected = '0 / 1 (0.00 B / 1.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
self.results.make_ref(r)
expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)

View File

@@ -1,467 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/03/03
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from nose.tools import eq_
from hsutil import job, io
from hsutil.path import Path
from hsutil.testcase import TestCase
from .. import fs
from ..engine import getwords, Match
from ..ignore import IgnoreList
from ..scanner import *
class NamedObject(object):
def __init__(self, name="foobar", size=1):
self.name = name
self.size = size
self.path = Path('')
self.words = getwords(name)
no = NamedObject
#--- Scanner
class ScannerTestFakeFiles(TestCase):
def setUp(self):
# This is a hack to avoid invalidating all previous tests since the scanner started to test
# for file existence before doing the match grouping.
self.mock(io, 'exists', lambda _: True)
def test_empty(self):
s = Scanner()
r = s.GetDupeGroups([])
eq_(r, [])
def test_default_settings(self):
s = Scanner()
eq_(s.min_match_percentage, 80)
eq_(s.scan_type, SCAN_TYPE_FILENAME)
eq_(s.mix_file_kind, True)
eq_(s.word_weighting, False)
eq_(s.match_similar_words, False)
assert isinstance(s.ignore_list, IgnoreList)
def test_simple_with_default_settings(self):
s = Scanner()
f = [no('foo bar'), no('foo bar'), no('foo bleh')]
r = s.GetDupeGroups(f)
eq_(len(r), 1)
g = r[0]
#'foo bleh' cannot be in the group because the default min match % is 80
eq_(len(g), 2)
assert g.ref in f[:2]
assert g.dupes[0] in f[:2]
def test_simple_with_lower_min_match(self):
s = Scanner()
s.min_match_percentage = 50
f = [no('foo bar'), no('foo bar'), no('foo bleh')]
r = s.GetDupeGroups(f)
eq_(len(r), 1)
g = r[0]
eq_(len(g), 3)
def test_trim_all_ref_groups(self):
# When all files of a group are ref, don't include that group in the results, but also don't
# count the files from that group as discarded.
s = Scanner()
f = [no('foo'), no('foo'), no('bar'), no('bar')]
f[2].is_ref = True
f[3].is_ref = True
r = s.GetDupeGroups(f)
eq_(len(r), 1)
eq_(s.discarded_file_count, 0)
def test_priorize(self):
s = Scanner()
f = [no('foo'), no('foo'), no('bar'), no('bar')]
f[1].size = 2
f[2].size = 3
f[3].is_ref = True
r = s.GetDupeGroups(f)
g1, g2 = r
assert f[1] in (g1.ref,g2.ref)
assert f[0] in (g1.dupes[0],g2.dupes[0])
assert f[3] in (g1.ref,g2.ref)
assert f[2] in (g1.dupes[0],g2.dupes[0])
def test_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = f[0].md5partial = 'foobar'
f[1].md5 = f[1].md5partial = 'foobar'
f[2].md5 = f[2].md5partial = 'bleh'
r = s.GetDupeGroups(f)
eq_(len(r), 1)
eq_(len(r[0]), 2)
eq_(s.discarded_file_count, 0) # don't count the different md5 as discarded!
def test_content_scan_compare_sizes_first(self):
class MyFile(no):
@property
def md5(file):
raise AssertionError()
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
f = [MyFile('foo', 1), MyFile('bar', 2)]
eq_(len(s.GetDupeGroups(f)), 0)
def test_min_match_perc_doesnt_matter_for_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = f[0].md5partial = 'foobar'
f[1].md5 = f[1].md5partial = 'foobar'
f[2].md5 = f[2].md5partial = 'bleh'
s.min_match_percentage = 101
r = s.GetDupeGroups(f)
eq_(len(r), 1)
eq_(len(r[0]), 2)
s.min_match_percentage = 0
r = s.GetDupeGroups(f)
eq_(len(r), 1)
eq_(len(r[0]), 2)
def test_content_scan_doesnt_put_md5_in_words_at_the_end(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
f = [no('foo'),no('bar')]
f[0].md5 = f[0].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'
f[1].md5 = f[1].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'
r = s.GetDupeGroups(f)
g = r[0]
def test_extension_is_not_counted_in_filename_scan(self):
s = Scanner()
s.min_match_percentage = 100
f = [no('foo.bar'), no('foo.bleh')]
r = s.GetDupeGroups(f)
eq_(len(r), 1)
eq_(len(r[0]), 2)
def test_job(self):
def do_progress(progress, desc=''):
log.append(progress)
return True
s = Scanner()
log = []
f = [no('foo bar'), no('foo bar'), no('foo bleh')]
r = s.GetDupeGroups(f, job.Job(1, do_progress))
eq_(log[0], 0)
eq_(log[-1], 100)
def test_mix_file_kind(self):
s = Scanner()
s.mix_file_kind = False
f = [no('foo.1'), no('foo.2')]
r = s.GetDupeGroups(f)
eq_(len(r), 0)
def test_word_weighting(self):
s = Scanner()
s.min_match_percentage = 75
s.word_weighting = True
f = [no('foo bar'), no('foo bar bleh')]
r = s.GetDupeGroups(f)
eq_(len(r), 1)
g = r[0]
m = g.get_match_of(g.dupes[0])
eq_(m.percentage, 75) # 16 letters, 12 matching
def test_similar_words(self):
s = Scanner()
s.match_similar_words = True
f = [no('The White Stripes'), no('The Whites Stripe'), no('Limp Bizkit'), no('Limp Bizkitt')]
r = s.GetDupeGroups(f)
eq_(len(r), 2)
def test_fields(self):
s = Scanner()
s.scan_type = SCAN_TYPE_FIELDS
f = [no('The White Stripes - Little Ghost'), no('The White Stripes - Little Acorn')]
r = s.GetDupeGroups(f)
eq_(len(r), 0)
def test_fields_no_order(self):
s = Scanner()
s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER
f = [no('The White Stripes - Little Ghost'), no('Little Ghost - The White Stripes')]
r = s.GetDupeGroups(f)
eq_(len(r), 1)
def test_tag_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
o1 = no('foo')
o2 = no('bar')
o1.artist = 'The White Stripes'
o1.title = 'The Air Near My Fingers'
o2.artist = 'The White Stripes'
o2.title = 'The Air Near My Fingers'
r = s.GetDupeGroups([o1,o2])
eq_(len(r), 1)
def test_tag_with_album_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scanned_tags = set(['artist', 'album', 'title'])
o1 = no('foo')
o2 = no('bar')
o3 = no('bleh')
o1.artist = 'The White Stripes'
o1.title = 'The Air Near My Fingers'
o1.album = 'Elephant'
o2.artist = 'The White Stripes'
o2.title = 'The Air Near My Fingers'
o2.album = 'Elephant'
o3.artist = 'The White Stripes'
o3.title = 'The Air Near My Fingers'
o3.album = 'foobar'
r = s.GetDupeGroups([o1,o2,o3])
eq_(len(r), 1)
def test_that_dash_in_tags_dont_create_new_fields(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scanned_tags = set(['artist', 'album', 'title'])
s.min_match_percentage = 50
o1 = no('foo')
o2 = no('bar')
o1.artist = 'The White Stripes - a'
o1.title = 'The Air Near My Fingers - a'
o1.album = 'Elephant - a'
o2.artist = 'The White Stripes - b'
o2.title = 'The Air Near My Fingers - b'
o2.album = 'Elephant - b'
r = s.GetDupeGroups([o1,o2])
eq_(len(r), 1)
def test_tag_scan_with_different_scanned(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scanned_tags = set(['track', 'year'])
o1 = no('foo')
o2 = no('bar')
o1.artist = 'The White Stripes'
o1.title = 'some title'
o1.track = 'foo'
o1.year = 'bar'
o2.artist = 'The White Stripes'
o2.title = 'another title'
o2.track = 'foo'
o2.year = 'bar'
r = s.GetDupeGroups([o1, o2])
eq_(len(r), 1)
def test_tag_scan_only_scans_existing_tags(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scanned_tags = set(['artist', 'foo'])
o1 = no('foo')
o2 = no('bar')
o1.artist = 'The White Stripes'
o1.foo = 'foo'
o2.artist = 'The White Stripes'
o2.foo = 'bar'
r = s.GetDupeGroups([o1, o2])
eq_(len(r), 1) # Because 'foo' is not scanned, they match
def test_tag_scan_converts_to_str(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scanned_tags = set(['track'])
o1 = no('foo')
o2 = no('bar')
o1.track = 42
o2.track = 42
try:
r = s.GetDupeGroups([o1, o2])
except TypeError:
raise AssertionError()
eq_(len(r), 1)
def test_tag_scan_non_ascii(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scanned_tags = set(['title'])
o1 = no('foo')
o2 = no('bar')
o1.title = u'foobar\u00e9'
o2.title = u'foobar\u00e9'
try:
r = s.GetDupeGroups([o1, o2])
except UnicodeEncodeError:
raise AssertionError()
eq_(len(r), 1)
def test_audio_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = 'foo'
f[1].md5 = 'bar'
f[2].md5 = 'bleh'
f[0].md5partial = 'foo'
f[1].md5partial = 'foo'
f[2].md5partial = 'bleh'
f[0].audiosize = 1
f[1].audiosize = 1
f[2].audiosize = 1
r = s.GetDupeGroups(f)
eq_(len(r), 1)
eq_(len(r[0]), 2)
def test_audio_content_scan_compare_sizes_first(self):
class MyFile(no):
@property
def md5partial(file):
raise AssertionError()
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
f = [MyFile('foo'), MyFile('bar')]
f[0].audiosize = 1
f[1].audiosize = 2
eq_(len(s.GetDupeGroups(f)), 0)
def test_ignore_list(self):
s = Scanner()
f1 = no('foobar')
f2 = no('foobar')
f3 = no('foobar')
f1.path = Path('dir1/foobar')
f2.path = Path('dir2/foobar')
f3.path = Path('dir3/foobar')
s.ignore_list.Ignore(str(f1.path),str(f2.path))
s.ignore_list.Ignore(str(f1.path),str(f3.path))
r = s.GetDupeGroups([f1,f2,f3])
eq_(len(r), 1)
g = r[0]
eq_(len(g.dupes), 1)
assert f1 not in g
assert f2 in g
assert f3 in g
# Ignored matches are not counted as discarded
eq_(s.discarded_file_count, 0)
def test_ignore_list_checks_for_unicode(self):
#scanner was calling path_str for ignore list checks. Since the Path changes, it must
#be unicode(path)
s = Scanner()
f1 = no('foobar')
f2 = no('foobar')
f3 = no('foobar')
f1.path = Path(u'foo1\u00e9')
f2.path = Path(u'foo2\u00e9')
f3.path = Path(u'foo3\u00e9')
s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path))
s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path))
r = s.GetDupeGroups([f1,f2,f3])
eq_(len(r), 1)
g = r[0]
eq_(len(g.dupes), 1)
assert f1 not in g
assert f2 in g
assert f3 in g
def test_file_evaluates_to_false(self):
# A very wrong way to use any() was added at some point, causing resulting group list
# to be empty.
class FalseNamedObject(NamedObject):
def __nonzero__(self):
return False
s = Scanner()
f1 = FalseNamedObject('foobar')
f2 = FalseNamedObject('foobar')
r = s.GetDupeGroups([f1, f2])
eq_(len(r), 1)
def test_size_threshold(self):
# Only file equal or higher than the size_threshold in size are scanned
s = Scanner()
f1 = no('foo', 1)
f2 = no('foo', 2)
f3 = no('foo', 3)
s.size_threshold = 2
groups = s.GetDupeGroups([f1,f2,f3])
eq_(len(groups), 1)
[group] = groups
eq_(len(group), 2)
assert f1 not in group
assert f2 in group
assert f3 in group
def test_tie_breaker_path_deepness(self):
# If there is a tie in prioritization, path deepness is used as a tie breaker
s = Scanner()
o1, o2 = no('foo'), no('foo')
o1.path = Path('foo')
o2.path = Path('foo/bar')
[group] = s.GetDupeGroups([o1, o2])
assert group.ref is o2
def test_tie_breaker_copy(self):
# if copy is in the words used (even if it has a deeper path), it becomes a dupe
s = Scanner()
o1, o2 = no('foo bar Copy'), no('foo bar')
o1.path = Path('deeper/path')
o2.path = Path('foo')
[group] = s.GetDupeGroups([o1, o2])
assert group.ref is o2
def test_tie_breaker_same_name_plus_digit(self):
# if ref has the same words as dupe, but has some just one extra word which is a digit, it
# becomes a dupe
s = Scanner()
o1, o2 = no('foo bar 42'), no('foo bar')
o1.path = Path('deeper/path')
o2.path = Path('foo')
[group] = s.GetDupeGroups([o1, o2])
assert group.ref is o2
def test_partial_group_match(self):
# Count the number od discarded matches (when a file doesn't match all other dupes of the
# group) in Scanner.discarded_file_count
s = Scanner()
o1, o2, o3 = no('a b'), no('a'), no('b')
s.min_match_percentage = 50
[group] = s.GetDupeGroups([o1, o2, o3])
eq_(len(group), 2)
assert o1 in group
assert o2 in group
assert o3 not in group
eq_(s.discarded_file_count, 1)
class ScannerTest(TestCase):
def test_dont_group_files_that_dont_exist(self):
# when creating groups, check that files exist first. It's possible that these files have
# been moved during the scan by the user.
# In this test, we have to delete one of the files between the get_matches() part and the
# get_groups() part.
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
p = self.tmppath()
io.open(p + 'file1', 'w').write('foo')
io.open(p + 'file2', 'w').write('foo')
file1, file2 = fs.get_files(p)
def getmatches(*args, **kw):
io.remove(file2.path)
return [Match(file1, file2, 100)]
s._getmatches = getmatches
assert not s.GetDupeGroups([file1, file2])

View File

@@ -1,11 +0,0 @@
Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
* If the source code has been published less than two years ago, any redistribution, in whole or in part, must retain full licensing functionality, without any attempt to change, obscure or in other ways circumvent its intent.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,11 +0,0 @@
WARNING ABOUT THE HS LICENSE AND PyQt
Although Qt is now LGPL licensed, PyQt still is dual licensed. Until Nokia buys Riverbank and
releases PyQt as LGPL, users of this part of the code (The PyQt-based GUI code) have to use the
GPL version of PyQt, unless they possess a commercial license to it.
There is no problem to this AS LONG AS YOU DON'T REDISTRIBUTE HS LICENSED CODE. The GPL license, from the point of view of the user, is very permissive. You can do WHATEVER you want with the GPLed version of PyQt, as long as you don't redistribute any of the code, or code dependent on it. When you do, the code you distribute has to be GPL compliant. The HS license is NOT, I repeat, NOT compliant with the GPL.
So, what does it all mean? You have no restriction on the usage of the PyQt-dependent-HS-licensed code, but unless you possess a commercial PyQt license, Hardcoded Software (or anyone) cannot accept any contribution from you for this part of the code.
Note that this only affects the PyQt dependent code, and not any other part of HS licensed code (if it has "import PyQt4" in it, it's PyQt dependent code). For the rest of the code, the only restrictions that apply are the ones from the HS license.

View File

@@ -1,258 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-04-25
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import unicode_literals
import logging
import os
import os.path as op
from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL
from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox
from hsutil import job
from hsutil.reg import RegistrationRequired
from dupeguru import fs
from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY,
JOB_DELETE)
from qtlib.about_box import AboutBox
from qtlib.progress import Progress
from qtlib.reg import Registration
from . import platform
from .main_window import MainWindow
from .directories_dialog import DirectoriesDialog
JOBID2TITLE = {
JOB_SCAN: "Scanning for duplicates",
JOB_LOAD: "Loading",
JOB_MOVE: "Moving",
JOB_COPY: "Copying",
JOB_DELETE: "Sending files to the recycle bin",
}
def demo_method(method):
def wrapper(self, *args, **kwargs):
try:
return method(self, *args, **kwargs)
except RegistrationRequired:
msg = "The demo version of dupeGuru only allows 10 actions (delete/move/copy) per session."
QMessageBox.information(self.main_window, 'Demo', msg)
return wrapper
class DupeGuru(DupeGuruBase, QObject):
LOGO_NAME = '<replace this>'
NAME = '<replace this>'
DELTA_COLUMNS = frozenset()
DEMO_LIMIT_DESC = "In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied."
def __init__(self, data_module, appid):
appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
if not op.exists(appdata):
os.makedirs(appdata)
# For basicConfig() to work, we have to be sure that no logging has taken place before this call.
logging.basicConfig(filename=op.join(appdata, 'debug.log'), level=logging.WARNING)
DupeGuruBase.__init__(self, data_module, appdata, appid)
QObject.__init__(self)
self._setup()
#--- Private
def _setup(self):
self.selected_dupe = None
self.prefs = self._create_preferences()
self.prefs.load()
self._update_options()
self.main_window = self._create_main_window()
self._progress = Progress(self.main_window)
self.directories_dialog = DirectoriesDialog(self.main_window, self)
self.details_dialog = self._create_details_dialog(self.main_window)
self.preferences_dialog = self._create_preferences_dialog(self.main_window)
self.about_box = AboutBox(self.main_window, self)
self.reg = Registration(self)
self.set_registration(self.prefs.registration_code, self.prefs.registration_email)
if not self.registered:
# 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
# In some circumstances, the nag is hidden by other window, which may make the user think
# that the application haven't launched.
self._nagTimer = QTimer()
self.connect(self._nagTimer, SIGNAL('timeout()'), self.mustShowNag)
self._nagTimer.start(0)
self.main_window.show()
self.load()
self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate)
self.connect(self._progress, SIGNAL('finished(QString)'), self.job_finished)
def _setup_as_registered(self):
self.prefs.registration_code = self.registration_code
self.prefs.registration_email = self.registration_email
self.main_window.actionRegister.setVisible(False)
self.about_box.registerButton.hide()
self.about_box.registeredEmailLabel.setText(self.prefs.registration_email)
def _update_options(self):
self.scanner.mix_file_kind = self.prefs.mix_file_kind
self.options['escape_filter_regexp'] = self.prefs.use_regexp
self.options['clean_empty_dirs'] = self.prefs.remove_empty_folders
#--- Virtual
def _create_details_dialog(self, parent):
raise NotImplementedError()
def _create_main_window(self):
return MainWindow(app=self)
def _create_preferences(self):
raise NotImplementedError()
def _create_preferences_dialog(self, parent):
raise NotImplementedError()
#--- Override
@staticmethod
def _recycle_dupe(dupe):
platform.recycle_file(dupe.path)
def _start_job(self, jobid, func):
title = JOBID2TITLE[jobid]
try:
j = self._progress.create_job()
self._progress.run(jobid, title, func, args=(j, ))
except job.JobInProgressError:
msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
QMessageBox.information(self.main_window, 'Action in progress', msg)
#--- Public
def add_dupes_to_ignore_list(self, duplicates):
for dupe in duplicates:
self.add_to_ignore_list(dupe)
self.remove_duplicates(duplicates)
def apply_filter(self, filter):
DupeGuruBase.apply_filter(self, filter)
self.emit(SIGNAL('resultsChanged()'))
def ask_for_reg_code(self):
self.reg.ask_for_code()
@demo_method
def copy_or_move_marked(self, copy):
opname = 'copy' if copy else 'move'
title = "Select a directory to {0} marked files to".format(opname)
flags = QFileDialog.ShowDirsOnly
destination = unicode(QFileDialog.getExistingDirectory(self.main_window, title, '', flags))
if not destination:
return
recreate_path = self.prefs.destination_type
DupeGuruBase.copy_or_move_marked(self, copy, destination, recreate_path)
delete_marked = demo_method(DupeGuruBase.delete_marked)
def make_reference(self, duplicates):
DupeGuruBase.make_reference(self, duplicates)
self.emit(SIGNAL('resultsChanged()'))
def mark_all(self):
self.results.mark_all()
self.emit(SIGNAL('dupeMarkingChanged()'))
def mark_invert(self):
self.results.mark_invert()
self.emit(SIGNAL('dupeMarkingChanged()'))
def mark_none(self):
self.results.mark_none()
self.emit(SIGNAL('dupeMarkingChanged()'))
def openDebugLog(self):
debugLogPath = op.join(self.appdata, 'debug.log')
url = QUrl.fromLocalFile(debugLogPath)
QDesktopServices.openUrl(url)
def open_selected(self):
if self.selected_dupe is None:
return
url = QUrl.fromLocalFile(unicode(self.selected_dupe.path))
QDesktopServices.openUrl(url)
def remove_duplicates(self, duplicates):
self.results.remove_duplicates(duplicates)
self.emit(SIGNAL('resultsChanged()'))
def remove_marked_duplicates(self):
marked = [d for d in self.results.dupes if self.results.is_marked(d)]
self.remove_duplicates(marked)
def rename_dupe(self, dupe, newname):
try:
dupe.rename(newname)
return True
except (IndexError, fs.FSError) as e:
logging.warning("dupeGuru Warning: %s" % unicode(e))
return False
def reveal_selected(self):
if self.selected_dupe is None:
return
url = QUrl.fromLocalFile(unicode(self.selected_dupe.path[:-1]))
QDesktopServices.openUrl(url)
def select_duplicate(self, dupe):
self.selected_dupe = dupe
self.emit(SIGNAL('duplicateSelected()'))
def show_about_box(self):
self.about_box.show()
def show_details(self):
self.details_dialog.show()
def show_directories(self):
self.directories_dialog.show()
def show_help(self):
url = QUrl.fromLocalFile(op.abspath('help/intro.htm'))
QDesktopServices.openUrl(url)
def show_preferences(self):
self.preferences_dialog.load()
result = self.preferences_dialog.exec_()
if result == QDialog.Accepted:
self.preferences_dialog.save()
self.prefs.save()
self._update_options()
def toggle_marking_for_dupes(self, dupes):
for dupe in dupes:
self.results.mark_toggle(dupe)
self.emit(SIGNAL('dupeMarkingChanged()'))
#--- Events
def application_will_terminate(self):
self.save()
self.save_ignore_list()
def mustShowNag(self):
self._nagTimer.stop() # must be shown only once
self.reg.show_nag()
def job_finished(self, jobid):
self.emit(SIGNAL('resultsChanged()'))
if jobid == JOB_LOAD:
self.emit(SIGNAL('directoriesChanged()'))
if jobid in (JOB_MOVE, JOB_COPY, JOB_DELETE) and self.last_op_error_count > 0:
msg = "{0} files could not be processed.".format(self.results.mark_count)
QMessageBox.warning(self.main_window, 'Warning', msg)

View File

@@ -1,83 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-17
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant
from PyQt4.QtGui import QHeaderView, QTableView
HEADER = ['Attribute', 'Selected', 'Reference']
class DetailsModel(QAbstractTableModel):
def __init__(self, app):
QAbstractTableModel.__init__(self)
self._app = app
self._dupe_data = None
self._ref_data = None
self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected)
def columnCount(self, parent):
return len(HEADER)
def data(self, index, role):
if not index.isValid():
return QVariant()
if role != Qt.DisplayRole:
return QVariant()
column = index.column()
row = index.row()
if column == 0:
return QVariant(self._app.data.COLUMNS[row]['display'])
elif column == 1 and self._dupe_data:
return QVariant(self._dupe_data[row])
elif column == 2 and self._ref_data:
return QVariant(self._ref_data[row])
return QVariant()
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER):
return QVariant(HEADER[section])
return QVariant()
def rowCount(self, parent):
return len(self._app.data.COLUMNS)
#--- Events
def duplicateSelected(self):
dupe = self._app.selected_dupe
if dupe is None:
group = None
ref = None
else:
group = self._app.results.get_group_of_duplicate(dupe)
ref = group.ref if group.ref is not dupe else None
self._dupe_data = self._app._get_display_info(dupe, group)
self._ref_data = self._app._get_display_info(ref, group)
self.reset()
class DetailsTable(QTableView):
def __init__(self, *args):
QTableView.__init__(self, *args)
self.setAlternatingRowColors(True)
self.setSelectionBehavior(QTableView.SelectRows)
self.setShowGrid(False)
def setModel(self, model):
QTableView.setModel(self, model)
# The model needs to be set to set header stuff
hheader = self.horizontalHeader()
hheader.setHighlightSections(False)
hheader.setStretchLastSection(False)
hheader.resizeSection(0, 100)
hheader.setResizeMode(0, QHeaderView.Fixed)
hheader.setResizeMode(1, QHeaderView.Stretch)
hheader.setResizeMode(2, QHeaderView.Stretch)
vheader = self.verticalHeader()
vheader.setVisible(False)
vheader.setDefaultSectionSize(18)

View File

@@ -1,16 +0,0 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource>
<file alias="details">images/details32.png</file>
<file alias="logo_pe">images/dgpe_logo_32.png</file>
<file alias="logo_pe_big">images/dgpe_logo_128.png</file>
<file alias="logo_me">images/dgme_logo_32.png</file>
<file alias="logo_me_big">images/dgme_logo_128.png</file>
<file alias="logo_se">images/dgse_logo_32.png</file>
<file alias="logo_se_big">images/dgse_logo_128.png</file>
<file alias="folder">images/folderwin32.png</file>
<file alias="preferences">images/preferences32.png</file>
<file alias="actions">images/actions32.png</file>
<file alias="delta">images/delta32.png</file>
<file alias="power_marker">images/power_marker32.png</file>
</qresource>
</RCC>

View File

@@ -1,85 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-04-25
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QFileDialog, QHeaderView
from . import platform
from .directories_dialog_ui import Ui_DirectoriesDialog
from .directories_model import DirectoriesModel, DirectoriesDelegate
class DirectoriesDialog(QDialog, Ui_DirectoriesDialog):
def __init__(self, parent, app):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
QDialog.__init__(self, parent, flags)
self.app = app
self.lastAddedFolder = platform.INITIAL_FOLDER_IN_DIALOGS
self._setupUi()
self._updateRemoveButton()
self.connect(self.doneButton, SIGNAL('clicked()'), self.doneButtonClicked)
self.connect(self.addButton, SIGNAL('clicked()'), self.addButtonClicked)
self.connect(self.removeButton, SIGNAL('clicked()'), self.removeButtonClicked)
self.connect(self.treeView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged)
self.connect(self.app, SIGNAL('directoriesChanged()'), self.directoriesChanged)
def _setupUi(self):
self.setupUi(self)
# Stuff that can't be done in the Designer
self.directoriesModel = DirectoriesModel(self.app)
self.directoriesDelegate = DirectoriesDelegate()
self.treeView.setItemDelegate(self.directoriesDelegate)
self.treeView.setModel(self.directoriesModel)
header = self.treeView.header()
header.setStretchLastSection(False)
header.setResizeMode(0, QHeaderView.Stretch)
header.setResizeMode(1, QHeaderView.Fixed)
header.resizeSection(1, 100)
def _updateRemoveButton(self):
indexes = self.treeView.selectedIndexes()
if not indexes:
self.removeButton.setEnabled(False)
return
self.removeButton.setEnabled(True)
index = indexes[0]
node = index.internalPointer()
# label = 'Remove' if node.parent is None else 'Exclude'
def addButtonClicked(self):
title = u"Select a directory to add to the scanning list"
flags = QFileDialog.ShowDirsOnly
dirpath = unicode(QFileDialog.getExistingDirectory(self, title, self.lastAddedFolder, flags))
if not dirpath:
return
self.lastAddedFolder = dirpath
self.app.add_directory(dirpath)
self.directoriesModel.reset()
def directoriesChanged(self):
self.directoriesModel.reset()
def doneButtonClicked(self):
self.hide()
def removeButtonClicked(self):
indexes = self.treeView.selectedIndexes()
if not indexes:
return
index = indexes[0]
node = index.internalPointer()
if node.parent is None:
row = index.row()
del self.app.directories[row]
self.directoriesModel.reset()
def selectionChanged(self, selected, deselected):
self._updateRemoveButton()

View File

@@ -1,145 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DirectoriesDialog</class>
<widget class="QDialog" name="DirectoriesDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>420</width>
<height>338</height>
</rect>
</property>
<property name="windowTitle">
<string>Directories</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="treeView">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="editTriggers">
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
</property>
<property name="dragDropOverwriteMode">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="removeButton">
<property name="minimumSize">
<size>
<width>91</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>32</height>
</size>
</property>
<property name="text">
<string>Remove</string>
</property>
<property name="shortcut">
<string>Del</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="addButton">
<property name="minimumSize">
<size>
<width>91</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>32</height>
</size>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="doneButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>91</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>32</height>
</size>
</property>
<property name="text">
<string>Done</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,138 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-04-25
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import urllib
from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint, QUrl
from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush
from qtlib.tree_model import TreeNode, TreeModel
HEADERS = ['Name', 'State']
STATES = ['Normal', 'Reference', 'Excluded']
class DirectoriesDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QComboBox(parent);
editor.addItems(STATES)
return editor
def setEditorData(self, editor, index):
value = index.model().data(index, Qt.EditRole)
editor.setCurrentIndex(value);
press = QMouseEvent(QEvent.MouseButtonPress, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
release = QMouseEvent(QEvent.MouseButtonRelease, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
QApplication.sendEvent(editor, press)
QApplication.sendEvent(editor, release)
# editor.showPopup() # this causes a weird glitch. the ugly workaround is above.
def setModelData(self, editor, model, index):
value = editor.currentIndex()
model.setData(index, value, Qt.EditRole)
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
class DirectoryNode(TreeNode):
def __init__(self, model, parent, ref, row):
TreeNode.__init__(self, model, parent, row)
self.ref = ref
def _createNode(self, ref, row):
return DirectoryNode(self.model, self, ref, row)
def _getChildren(self):
return self.model.dirs.get_subfolders(self.ref)
@property
def name(self):
if self.parent is not None:
return self.ref[-1]
else:
return unicode(self.ref)
class DirectoriesModel(TreeModel):
def __init__(self, app):
self.app = app
self.dirs = app.directories
TreeModel.__init__(self)
def _createNode(self, ref, row):
return DirectoryNode(self, None, ref, row)
def _getChildren(self):
return self.dirs
def columnCount(self, parent):
return 2
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == Qt.DisplayRole:
if index.column() == 0:
return node.name
else:
return STATES[self.dirs.get_state(node.ref)]
elif role == Qt.EditRole and index.column() == 1:
return self.dirs.get_state(node.ref)
elif role == Qt.ForegroundRole:
state = self.dirs.get_state(node.ref)
if state == 1:
return QBrush(Qt.blue)
elif state == 2:
return QBrush(Qt.red)
return None
def dropMimeData(self, mimeData, action, row, column, parentIndex):
# the data in mimeData is urlencoded **in utf-8**!!! which means that urllib.unquote has
# to be called on the utf-8 encoded string, and *only then*, decoded to unicode.
if not mimeData.hasFormat('text/uri-list'):
return False
data = str(mimeData.data('text/uri-list'))
unquoted = urllib.unquote(data)
urls = unicode(unquoted, 'utf-8').split('\r\n')
paths = [unicode(QUrl(url).toLocalFile()) for url in urls if url]
for path in paths:
self.app.add_directory(path)
self.reset()
return True
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
result = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDropEnabled
if index.column() == 1:
result |= Qt.ItemIsEditable
return result
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal:
if role == Qt.DisplayRole and section < len(HEADERS):
return HEADERS[section]
return None
def mimeTypes(self):
return ['text/uri-list']
def setData(self, index, value, role):
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
return False
node = index.internalPointer()
self.dirs.set_state(node.ref, value)
return True
def supportedDropActions(self):
# 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.
return Qt.ActionMask

View File

@@ -1,332 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-04-25
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL, QUrl
from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView,
QMessageBox, QInputDialog, QLineEdit, QItemSelectionModel, QDesktopServices)
from hsutil.misc import nonone
from dupeguru.app import NoScannableFileError, AllFilesAreRefError
import dg_rc
from main_window_ui import Ui_MainWindow
from results_model import ResultsDelegate, ResultsModel
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, app):
QMainWindow.__init__(self, None)
self.app = app
self._last_filter = None
self._setupUi()
self.resultsDelegate = ResultsDelegate()
self.resultsModel = ResultsModel(self.app)
self.resultsView.setModel(self.resultsModel)
self.resultsView.setItemDelegate(self.resultsDelegate)
self._load_columns()
self._update_column_actions_status()
self.resultsView.expandAll()
self._update_status_line()
self.connect(self.app, SIGNAL('resultsChanged()'), self.resultsChanged)
self.connect(self.app, SIGNAL('dupeMarkingChanged()'), self.dupeMarkingChanged)
self.connect(self.actionQuit, SIGNAL('triggered()'), QCoreApplication.instance().quit)
self.connect(self.resultsView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged)
self.connect(self.menuColumns, SIGNAL('triggered(QAction*)'), self.columnToggled)
self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate)
self.connect(self.resultsModel, SIGNAL('modelReset()'), self.resultsReset)
self.connect(self.resultsView, SIGNAL('doubleClicked()'), self.resultsDoubleClicked)
def _setupUi(self):
self.setupUi(self)
# Stuff that can't be setup in the Designer
h = self.resultsView.header()
h.setHighlightSections(False)
h.setMovable(True)
h.setStretchLastSection(False)
h.setDefaultAlignment(Qt.AlignLeft)
self.setWindowTitle(QCoreApplication.instance().applicationName())
self.actionScan.setIcon(QIcon(QPixmap(':/%s' % self.app.LOGO_NAME)))
# Columns menu
menu = self.menuColumns
self._column_actions = []
for index, column in enumerate(self.app.data.COLUMNS):
action = menu.addAction(column['display'])
action.setCheckable(True)
action.column_index = index
self._column_actions.append(action)
menu.addSeparator()
action = menu.addAction("Reset to Defaults")
action.column_index = -1
# Action menu
actionMenu = QMenu('Actions', self.toolBar)
actionMenu.setIcon(QIcon(QPixmap(":/actions")))
actionMenu.addAction(self.actionDeleteMarked)
actionMenu.addAction(self.actionMoveMarked)
actionMenu.addAction(self.actionCopyMarked)
actionMenu.addAction(self.actionRemoveMarked)
actionMenu.addSeparator()
actionMenu.addAction(self.actionRemoveSelected)
actionMenu.addAction(self.actionIgnoreSelected)
actionMenu.addAction(self.actionMakeSelectedReference)
actionMenu.addSeparator()
actionMenu.addAction(self.actionOpenSelected)
actionMenu.addAction(self.actionRevealSelected)
actionMenu.addAction(self.actionRenameSelected)
self.actionActions.setMenu(actionMenu)
button = QToolButton(self.toolBar)
button.setDefaultAction(actionMenu.menuAction())
button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.actionsButton = button
self.toolBar.insertWidget(self.actionActions, button) # the action is a placeholder
self.toolBar.removeAction(self.actionActions)
self.statusLabel = QLabel(self)
self.statusbar.addPermanentWidget(self.statusLabel, 1)
#--- Private
def _confirm(self, title, msg, default_button=QMessageBox.Yes):
buttons = QMessageBox.Yes | QMessageBox.No
answer = QMessageBox.question(self, title, msg, buttons, default_button)
return answer == QMessageBox.Yes
def _load_columns(self):
h = self.resultsView.header()
h.setResizeMode(QHeaderView.Interactive)
prefs = self.app.prefs
attrs = zip(prefs.columns_width, prefs.columns_visible)
for index, (width, visible) in enumerate(attrs):
h.resizeSection(index, width)
h.setSectionHidden(index, not visible)
h.setResizeMode(0, QHeaderView.Stretch)
def _redraw_results(self):
# HACK. this is the only way I found to update the widget without reseting everything
self.resultsView.scroll(0, 1)
self.resultsView.scroll(0, -1)
def _save_columns(self):
h = self.resultsView.header()
widths = []
visible = []
for i in range(len(self.app.data.COLUMNS)):
widths.append(h.sectionSize(i))
visible.append(not h.isSectionHidden(i))
prefs = self.app.prefs
prefs.columns_width = widths
prefs.columns_visible = visible
prefs.save()
def _update_column_actions_status(self):
h = self.resultsView.header()
for action in self._column_actions:
colid = action.column_index
action.setChecked(not h.isSectionHidden(colid))
def _update_status_line(self):
self.statusLabel.setText(self.app.stat_line)
#--- Actions
def aboutTriggered(self):
self.app.show_about_box()
def actionsTriggered(self):
self.actionsButton.showMenu()
def addToIgnoreListTriggered(self):
dupes = self.resultsView.selectedDupes()
if not dupes:
return
title = "Add to Ignore List"
msg = "All selected {0} matches are going to be ignored in all subsequent scans. Continue?".format(len(dupes))
if self._confirm(title, msg):
self.app.add_dupes_to_ignore_list(dupes)
def applyFilterTriggered(self):
title = "Apply Filter"
msg = "Type the filter you want to apply on your results. See help for details."
text = nonone(self._last_filter, '[*]')
answer, ok = QInputDialog.getText(self, title, msg, QLineEdit.Normal, text)
if not ok:
return
answer = unicode(answer)
self.app.apply_filter(answer)
self._last_filter = answer
def cancelFilterTriggered(self):
self.app.apply_filter('')
def checkForUpdateTriggered(self):
QProcess.execute('updater.exe', ['/checknow'])
def clearIgnoreListTriggered(self):
title = "Clear Ignore List"
count = len(self.app.scanner.ignore_list)
if not count:
QMessageBox.information(self, title, "Nothing to clear.")
return
msg = "Do you really want to remove all {0} items from the ignore list?".format(count)
if self._confirm(title, msg, QMessageBox.No):
self.app.scanner.ignore_list.Clear()
QMessageBox.information(self, title, "Ignore list cleared.")
def copyTriggered(self):
self.app.copy_or_move_marked(True)
def deleteTriggered(self):
count = self.app.results.mark_count
if not count:
return
title = "Delete duplicates"
msg = "You are about to send {0} files to the recycle bin. Continue?".format(count)
if self._confirm(title, msg):
self.app.delete_marked()
def deltaTriggered(self):
self.resultsModel.delta = self.actionDelta.isChecked()
self._redraw_results()
def detailsTriggered(self):
self.app.show_details()
def directoriesTriggered(self):
self.app.show_directories()
def exportTriggered(self):
h = self.resultsView.header()
column_ids = []
for i in range(len(self.app.data.COLUMNS)):
if not h.isSectionHidden(i):
column_ids.append(str(i))
exported_path = self.app.export_to_xhtml(column_ids)
url = QUrl.fromLocalFile(exported_path)
QDesktopServices.openUrl(url)
def makeReferenceTriggered(self):
self.app.make_reference(self.resultsView.selectedDupes())
def markAllTriggered(self):
self.app.mark_all()
def markInvertTriggered(self):
self.app.mark_invert()
def markNoneTriggered(self):
self.app.mark_none()
def markSelectedTriggered(self):
dupes = self.resultsView.selectedDupes()
self.app.toggle_marking_for_dupes(dupes)
def moveTriggered(self):
self.app.copy_or_move_marked(False)
def openDebugLogTriggered(self):
self.app.openDebugLog()
def openTriggered(self):
self.app.open_selected()
def powerMarkerTriggered(self):
self.resultsModel.power_marker = self.actionPowerMarker.isChecked()
def preferencesTriggered(self):
self.app.show_preferences()
def registerTrigerred(self):
self.app.ask_for_reg_code()
def removeMarkedTriggered(self):
count = self.app.results.mark_count
if not count:
return
title = "Remove duplicates"
msg = "You are about to remove {0} files from results. Continue?".format(count)
if self._confirm(title, msg):
self.app.remove_marked_duplicates()
def removeSelectedTriggered(self):
dupes = self.resultsView.selectedDupes()
if not dupes:
return
title = "Remove duplicates"
msg = "You are about to remove {0} files from results. Continue?".format(len(dupes))
if self._confirm(title, msg):
self.app.remove_duplicates(dupes)
def renameTriggered(self):
self.resultsView.edit(self.resultsView.selectionModel().currentIndex())
def revealTriggered(self):
self.app.reveal_selected()
def scanTriggered(self):
title = "Start a new scan"
if len(self.app.results.groups) > 0:
msg = "Are you sure you want to start a new duplicate scan?"
if not self._confirm(title, msg):
return
try:
self.app.start_scanning()
except NoScannableFileError:
msg = "The selected directories contain no scannable file."
QMessageBox.warning(self, title, msg)
self.app.show_directories()
except AllFilesAreRefError:
msg = "You cannot make a duplicate scan with only reference directories."
QMessageBox.warning(self, title, msg)
def showHelpTriggered(self):
self.app.show_help()
#--- Events
def application_will_terminate(self):
self._save_columns()
def columnToggled(self, action):
colid = action.column_index
if colid == -1:
self.app.prefs.reset_columns()
self._load_columns()
else:
h = self.resultsView.header()
h.setSectionHidden(colid, not h.isSectionHidden(colid))
self._update_column_actions_status()
def contextMenuEvent(self, event):
self.actionActions.menu().exec_(event.globalPos())
def dupeMarkingChanged(self):
self._redraw_results()
self._update_status_line()
def resultsChanged(self):
self.resultsView.model().reset()
def resultsDoubleClicked(self):
self.app.open_selected()
def resultsReset(self):
self.resultsView.expandAll()
dupe = self.app.selected_dupe
if dupe is not None:
[modelIndex] = self.resultsModel.indexesForDupes([dupe])
if modelIndex.isValid():
flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows
self.resultsView.selectionModel().setCurrentIndex(modelIndex, flags)
self._update_status_line()
def selectionChanged(self, selected, deselected):
index = self.resultsView.selectionModel().currentIndex()
dupe = index.internalPointer().dupe if index.isValid() else None
self.app.select_duplicate(dupe)

View File

@@ -1,957 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>630</width>
<height>514</height>
</rect>
</property>
<property name="windowTitle">
<string>dupeGuru</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="ResultsView" name="resultsView">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="expandsOnDoubleClick">
<bool>false</bool>
</property>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>630</width>
<height>22</height>
</rect>
</property>
<widget class="QMenu" name="menuColumns">
<property name="title">
<string>Columns</string>
</property>
</widget>
<widget class="QMenu" name="menuActions">
<property name="title">
<string>Actions</string>
</property>
<addaction name="actionDeleteMarked"/>
<addaction name="actionMoveMarked"/>
<addaction name="actionCopyMarked"/>
<addaction name="actionRemoveMarked"/>
<addaction name="separator"/>
<addaction name="actionRemoveSelected"/>
<addaction name="actionIgnoreSelected"/>
<addaction name="actionMakeSelectedReference"/>
<addaction name="separator"/>
<addaction name="actionOpenSelected"/>
<addaction name="actionRevealSelected"/>
<addaction name="actionRenameSelected"/>
<addaction name="separator"/>
<addaction name="actionApplyFilter"/>
<addaction name="actionCancelFilter"/>
</widget>
<widget class="QMenu" name="menuMark">
<property name="title">
<string>Mark</string>
</property>
<addaction name="actionMarkAll"/>
<addaction name="actionMarkNone"/>
<addaction name="actionInvertMarking"/>
<addaction name="actionMarkSelected"/>
</widget>
<widget class="QMenu" name="menuModes">
<property name="title">
<string>Modes</string>
</property>
<addaction name="actionPowerMarker"/>
<addaction name="actionDelta"/>
</widget>
<widget class="QMenu" name="menuWindow">
<property name="title">
<string>Windows</string>
</property>
<addaction name="actionDetails"/>
<addaction name="actionDirectories"/>
<addaction name="actionPreferences"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
</property>
<addaction name="actionShowHelp"/>
<addaction name="actionRegister"/>
<addaction name="actionCheckForUpdate"/>
<addaction name="actionOpenDebugLog"/>
<addaction name="actionAbout"/>
</widget>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionScan"/>
<addaction name="separator"/>
<addaction name="actionExport"/>
<addaction name="actionClearIgnoreList"/>
<addaction name="separator"/>
<addaction name="actionQuit"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuMark"/>
<addaction name="menuActions"/>
<addaction name="menuColumns"/>
<addaction name="menuModes"/>
<addaction name="menuWindow"/>
<addaction name="menuHelp"/>
</widget>
<widget class="QToolBar" name="toolBar">
<property name="windowTitle">
<string>toolBar</string>
</property>
<property name="movable">
<bool>false</bool>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
<property name="floatable">
<bool>false</bool>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
</attribute>
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="actionScan"/>
<addaction name="actionActions"/>
<addaction name="actionDirectories"/>
<addaction name="actionDetails"/>
<addaction name="actionPreferences"/>
<addaction name="actionDelta"/>
<addaction name="actionPowerMarker"/>
</widget>
<widget class="QStatusBar" name="statusbar">
<property name="sizeGripEnabled">
<bool>true</bool>
</property>
</widget>
<action name="actionScan">
<property name="icon">
<iconset resource="dg.qrc">
<normaloff>:/logo_pe</normaloff>:/logo_pe</iconset>
</property>
<property name="text">
<string>Start Scan</string>
</property>
<property name="toolTip">
<string>Start scanning for duplicates</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionDirectories">
<property name="icon">
<iconset resource="dg.qrc">
<normaloff>:/folder</normaloff>:/folder</iconset>
</property>
<property name="text">
<string>Directories</string>
</property>
<property name="shortcut">
<string>Ctrl+4</string>
</property>
</action>
<action name="actionDetails">
<property name="icon">
<iconset resource="dg.qrc">
<normaloff>:/details</normaloff>:/details</iconset>
</property>
<property name="text">
<string>Details</string>
</property>
<property name="shortcut">
<string>Ctrl+3</string>
</property>
</action>
<action name="actionActions">
<property name="icon">
<iconset resource="dg.qrc">
<normaloff>:/actions</normaloff>:/actions</iconset>
</property>
<property name="text">
<string>Actions</string>
</property>
</action>
<action name="actionPreferences">
<property name="icon">
<iconset resource="dg.qrc">
<normaloff>:/preferences</normaloff>:/preferences</iconset>
</property>
<property name="text">
<string>Preferences</string>
</property>
<property name="shortcut">
<string>Ctrl+5</string>
</property>
</action>
<action name="actionDelta">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="dg.qrc">
<normaloff>:/delta</normaloff>:/delta</iconset>
</property>
<property name="text">
<string>Delta Values</string>
</property>
<property name="shortcut">
<string>Ctrl+2</string>
</property>
</action>
<action name="actionPowerMarker">
<property name="checkable">
<bool>true</bool>
</property>
<property name="icon">
<iconset resource="dg.qrc">
<normaloff>:/power_marker</normaloff>:/power_marker</iconset>
</property>
<property name="text">
<string>Power Marker</string>
</property>
<property name="shortcut">
<string>Ctrl+1</string>
</property>
</action>
<action name="actionDeleteMarked">
<property name="text">
<string>Send Marked to Recycle Bin</string>
</property>
<property name="shortcut">
<string>Ctrl+D</string>
</property>
</action>
<action name="actionMoveMarked">
<property name="text">
<string>Move Marked to...</string>
</property>
<property name="shortcut">
<string>Ctrl+M</string>
</property>
</action>
<action name="actionCopyMarked">
<property name="text">
<string>Copy Marked to...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+M</string>
</property>
</action>
<action name="actionRemoveMarked">
<property name="text">
<string>Remove Marked from Results</string>
</property>
<property name="shortcut">
<string>Ctrl+R</string>
</property>
</action>
<action name="actionRemoveSelected">
<property name="text">
<string>Remove Selected from Results</string>
</property>
<property name="shortcut">
<string>Ctrl+Del</string>
</property>
</action>
<action name="actionIgnoreSelected">
<property name="text">
<string>Add Selected to Ignore List</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+Del</string>
</property>
</action>
<action name="actionMakeSelectedReference">
<property name="text">
<string>Make Selected Reference</string>
</property>
<property name="shortcut">
<string>Ctrl+Space</string>
</property>
</action>
<action name="actionOpenSelected">
<property name="text">
<string>Open Selected with Default Application</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="actionRevealSelected">
<property name="text">
<string>Open Containing Folder of Selected</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+O</string>
</property>
</action>
<action name="actionRenameSelected">
<property name="text">
<string>Rename Selected</string>
</property>
<property name="shortcut">
<string>F2</string>
</property>
</action>
<action name="actionMarkAll">
<property name="text">
<string>Mark All</string>
</property>
<property name="shortcut">
<string>Ctrl+A</string>
</property>
</action>
<action name="actionMarkNone">
<property name="text">
<string>Mark None</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+A</string>
</property>
</action>
<action name="actionInvertMarking">
<property name="text">
<string>Invert Marking</string>
</property>
<property name="shortcut">
<string>Ctrl+Alt+A</string>
</property>
</action>
<action name="actionMarkSelected">
<property name="text">
<string>Mark Selected</string>
</property>
</action>
<action name="actionClearIgnoreList">
<property name="text">
<string>Clear Ignore List</string>
</property>
</action>
<action name="actionQuit">
<property name="text">
<string>Quit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionApplyFilter">
<property name="text">
<string>Apply Filter</string>
</property>
<property name="shortcut">
<string>Ctrl+F</string>
</property>
</action>
<action name="actionCancelFilter">
<property name="text">
<string>Cancel Filter</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+F</string>
</property>
</action>
<action name="actionShowHelp">
<property name="text">
<string>dupeGuru Help</string>
</property>
<property name="shortcut">
<string>F1</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>About dupeGuru</string>
</property>
</action>
<action name="actionRegister">
<property name="text">
<string>Register dupeGuru</string>
</property>
</action>
<action name="actionCheckForUpdate">
<property name="text">
<string>Check for Update</string>
</property>
</action>
<action name="actionExport">
<property name="text">
<string>Export To XHTML</string>
</property>
</action>
<action name="actionOpenDebugLog">
<property name="text">
<string>Open Debug Log</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>ResultsView</class>
<extends>QTreeView</extends>
<header>results_model</header>
</customwidget>
</customwidgets>
<resources>
<include location="dg.qrc"/>
</resources>
<connections>
<connection>
<sender>actionDirectories</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>directoriesTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionActions</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>actionsTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionCopyMarked</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>copyTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionDeleteMarked</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>deleteTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionDelta</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>deltaTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionDetails</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>detailsTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionIgnoreSelected</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>addToIgnoreListTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionMakeSelectedReference</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>makeReferenceTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionMoveMarked</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>moveTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionOpenSelected</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>openTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionPowerMarker</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>powerMarkerTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionPreferences</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>preferencesTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionRemoveMarked</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>removeMarkedTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionRemoveSelected</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>removeSelectedTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionRevealSelected</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>revealTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionRenameSelected</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>renameTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionScan</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>scanTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionClearIgnoreList</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>clearIgnoreListTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionMarkAll</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>markAllTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionMarkNone</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>markNoneTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionMarkSelected</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>markSelectedTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionInvertMarking</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>markInvertTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionApplyFilter</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>applyFilterTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionCancelFilter</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>cancelFilterTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionShowHelp</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>showHelpTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionAbout</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>aboutTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionRegister</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>registerTrigerred()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionCheckForUpdate</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>checkForUpdateTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionExport</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>exportTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
<connection>
<sender>actionOpenDebugLog</sender>
<signal>triggered()</signal>
<receiver>MainWindow</receiver>
<slot>openDebugLogTriggered()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>314</x>
<y>256</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>directoriesTriggered()</slot>
<slot>scanTriggered()</slot>
<slot>actionsTriggered()</slot>
<slot>detailsTriggered()</slot>
<slot>preferencesTriggered()</slot>
<slot>deltaTriggered()</slot>
<slot>powerMarkerTriggered()</slot>
<slot>deleteTriggered()</slot>
<slot>moveTriggered()</slot>
<slot>copyTriggered()</slot>
<slot>removeMarkedTriggered()</slot>
<slot>removeSelectedTriggered()</slot>
<slot>addToIgnoreListTriggered()</slot>
<slot>makeReferenceTriggered()</slot>
<slot>openTriggered()</slot>
<slot>revealTriggered()</slot>
<slot>renameTriggered()</slot>
<slot>clearIgnoreListTriggered()</slot>
<slot>clearPictureCacheTriggered()</slot>
<slot>markAllTriggered()</slot>
<slot>markNoneTriggered()</slot>
<slot>markInvertTriggered()</slot>
<slot>markSelectedTriggered()</slot>
<slot>applyFilterTriggered()</slot>
<slot>cancelFilterTriggered()</slot>
<slot>showHelpTriggered()</slot>
<slot>aboutTriggered()</slot>
<slot>registerTrigerred()</slot>
<slot>checkForUpdateTriggered()</slot>
<slot>exportTriggered()</slot>
<slot>openDebugLogTriggered()</slot>
</slots>
</ui>

View File

@@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-09-27
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import logging
import sys
if sys.platform == 'win32':
from platform_win import *
elif sys.platform == 'darwin':
from platform_osx import *
else:
pass # unsupported platform

View File

@@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-10-14
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
# dummy unit to allow the app to run under OSX during development
INITIAL_FOLDER_IN_DIALOGS = '/'
def recycle_file(path):
pass

View File

@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-08-31
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import unicode_literals
import logging
import winshell
INITIAL_FOLDER_IN_DIALOGS = 'C:\\'
def recycle_file(path):
try:
winshell.delete_file(unicode(path), no_confirm=True, silent=True)
except winshell.x_winshell as e:
logging.warning("winshell error: %s", e)

View File

@@ -1,116 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-03
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import QSettings, QVariant
from hsutil.misc import tryint
def variant_to_py(v):
value = None
ok = False
t = v.type()
if t == QVariant.String:
value = unicode(v.toString())
ok = True # anyway
# might be bool or int, try them
if v == 'true':
value = True
elif value == 'false':
value = False
else:
value = tryint(value, value)
elif t == QVariant.Int:
value, ok = v.toInt()
elif t == QVariant.Bool:
value, ok = v.toBool(), True
elif t in (QVariant.List, QVariant.StringList):
value, ok = map(variant_to_py, v.toList()), True
if not ok:
raise TypeError(u"Can't convert {0} of type {1}".format(repr(v), v.type()))
return value
def py_to_variant(v):
if isinstance(v, (list, tuple)):
return QVariant(map(py_to_variant, v))
return QVariant(v)
class Preferences(object):
# (width, is_visible)
COLUMNS_DEFAULT_ATTRS = []
def __init__(self):
self.reset()
self.reset_columns()
def _load_specific(self, settings, get):
# load prefs specific to the dg edition
pass
def load(self):
self.reset()
settings = QSettings()
def get(name, default):
if settings.contains(name):
return variant_to_py(settings.value(name))
else:
return default
self.filter_hardness = get('FilterHardness', self.filter_hardness)
self.mix_file_kind = get('MixFileKind', self.mix_file_kind)
self.use_regexp = get('UseRegexp', self.use_regexp)
self.remove_empty_folders = get('RemoveEmptyFolders', self.remove_empty_folders)
self.destination_type = get('DestinationType', self.destination_type)
widths = get('ColumnsWidth', self.columns_width)
# only set nonzero values
for index, width in enumerate(widths[:len(self.columns_width)]):
if width > 0:
self.columns_width[index] = width
self.columns_visible = get('ColumnsVisible', self.columns_visible)
self.registration_code = get('RegistrationCode', self.registration_code)
self.registration_email = get('RegistrationEmail', self.registration_email)
self._load_specific(settings, get)
def _reset_specific(self):
# reset prefs specific to the dg edition
pass
def reset(self):
self.filter_hardness = 95
self.mix_file_kind = True
self.use_regexp = False
self.remove_empty_folders = False
self.destination_type = 1
self.registration_code = ''
self.registration_email = ''
self._reset_specific()
def reset_columns(self):
self.columns_width = [width for width, _ in self.COLUMNS_DEFAULT_ATTRS]
self.columns_visible = [visible for _, visible in self.COLUMNS_DEFAULT_ATTRS]
def _save_specific(self, settings, set_):
# save prefs specific to the dg edition
pass
def save(self):
settings = QSettings()
def set_(name, value):
settings.setValue(name, py_to_variant(value))
set_('FilterHardness', self.filter_hardness)
set_('MixFileKind', self.mix_file_kind)
set_('UseRegexp', self.use_regexp)
set_('RemoveEmptyFolders', self.remove_empty_folders)
set_('DestinationType', self.destination_type)
set_('ColumnsWidth', self.columns_width)
set_('ColumnsVisible', self.columns_visible)
set_('RegistrationCode', self.registration_code)
set_('RegistrationEmail', self.registration_email)
self._save_specific(settings, set_)

View File

@@ -1,204 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-04-23
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QModelIndex, QRect
from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor
from qtlib.tree_model import TreeNode, TreeModel
class ResultNode(TreeNode):
def __init__(self, model, parent, row, dupe, group):
TreeNode.__init__(self, model, parent, row)
self.dupe = dupe
self.group = group
self._normalData = None
self._deltaData = None
def _createNode(self, ref, row):
return ResultNode(self.model, self, row, ref, self.group)
def _getChildren(self):
return self.group.dupes if self.dupe is self.group.ref else []
def invalidate(self):
self._normalData = None
self._deltaData = None
TreeNode.invalidate(self)
@property
def normalData(self):
if self._normalData is None:
self._normalData = self.model._app._get_display_info(self.dupe, self.group, delta=False)
return self._normalData
@property
def deltaData(self):
if self._deltaData is None:
self._deltaData = self.model._app._get_display_info(self.dupe, self.group, delta=True)
return self._deltaData
class ResultsDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
QStyledItemDelegate.initStyleOption(self, option, index)
node = index.internalPointer()
if node.group.ref is node.dupe:
newfont = QFont(option.font)
newfont.setBold(True)
option.font = newfont
class ResultsModel(TreeModel):
def __init__(self, app):
self._app = app
self._results = app.results
self._data = app.data
self._delta_columns = app.DELTA_COLUMNS
self.delta = False
self._power_marker = False
TreeModel.__init__(self)
def _createNode(self, ref, row):
if self.power_marker:
# ref is a dupe
group = self._results.get_group_of_duplicate(ref)
return ResultNode(self, None, row, ref, group)
else:
# ref is a group
return ResultNode(self, None, row, ref.ref, ref)
def _getChildren(self):
return self._results.dupes if self.power_marker else self._results.groups
def columnCount(self, parent):
return len(self._data.COLUMNS)
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == Qt.DisplayRole:
data = node.deltaData if self.delta else node.normalData
return data[index.column()]
elif role == Qt.CheckStateRole:
if index.column() == 0 and node.dupe is not node.group.ref:
state = Qt.Checked if self._results.is_marked(node.dupe) else Qt.Unchecked
return state
elif role == Qt.ForegroundRole:
if node.dupe is node.group.ref or node.dupe.is_ref:
return QBrush(Qt.blue)
elif self.delta and index.column() in self._delta_columns:
return QBrush(QColor(255, 142, 40)) # orange
elif role == Qt.EditRole:
if index.column() == 0:
return node.normalData[index.column()]
return None
def dupesForIndexes(self, indexes):
nodes = [index.internalPointer() for index in indexes]
return [node.dupe for node in nodes]
def indexesForDupes(self, dupes):
def index(dupe):
try:
if self.power_marker:
row = self._results.dupes.index(dupe)
node = self.subnodes[row]
assert node.dupe is dupe
return self.createIndex(row, 0, node)
else:
group = self._results.get_group_of_duplicate(dupe)
row = self._results.groups.index(group)
node = self.subnodes[row]
if dupe is group.ref:
assert node.dupe is dupe
return self.createIndex(row, 0, node)
subrow = group.dupes.index(dupe)
subnode = node.subnodes[subrow]
assert subnode.dupe is dupe
return self.createIndex(subrow, 0, subnode)
except ValueError: # the dupe is not there anymore
return QModelIndex()
return map(index, dupes)
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if index.column() == 0:
flags |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable
return flags
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS):
return self._data.COLUMNS[section]['display']
return None
def setData(self, index, value, role):
if not index.isValid():
return False
node = index.internalPointer()
if role == Qt.CheckStateRole:
if index.column() == 0:
self._app.toggle_marking_for_dupes([node.dupe])
return True
if role == Qt.EditRole:
if index.column() == 0:
value = unicode(value.toString())
if self._app.rename_dupe(node.dupe, value):
node.invalidate()
return True
return False
def sort(self, column, order):
if self.power_marker:
self._results.sort_dupes(column, order == Qt.AscendingOrder, self.delta)
else:
self._results.sort_groups(column, order == Qt.AscendingOrder)
self.reset()
def toggleMarked(self, indexes):
assert indexes
dupes = self.dupesForIndexes(indexes)
self._app.toggle_marking_for_dupes(dupes)
#--- Properties
@property
def power_marker(self):
return self._power_marker
@power_marker.setter
def power_marker(self, value):
if value == self._power_marker:
return
self._power_marker = value
self.reset()
class ResultsView(QTreeView):
#--- Override
def keyPressEvent(self, event):
if event.text() == ' ':
self.model().toggleMarked(self.selectionModel().selectedRows())
return
QTreeView.keyPressEvent(self, event)
def mouseDoubleClickEvent(self, event):
self.emit(SIGNAL('doubleClicked()'))
# We don't call the superclass' method because the default behavior is to rename the cell.
def setModel(self, model):
assert isinstance(model, ResultsModel)
QTreeView.setModel(self, model)
#--- Public
def selectedDupes(self):
return self.model().dupesForIndexes(self.selectionModel().selectedRows())

41
bootstrap.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
PYTHON=python3
ret=`$PYTHON -c "import sys; print(int(sys.version_info[:2] >= (3, 4)))"`
if [ $ret -ne 1 ]; then
echo "Python 3.4+ required. Aborting."
exit 1
fi
if [ -d ".git" ]; then
git submodule init
git submodule update
fi
if [ ! -d "env" ]; then
echo "No virtualenv. Creating one"
# We need a "system-site-packages" env to have PyQt, but we also need to ensure a local pip
# install. To achieve our latter goal, we start with a normal venv, which we later upgrade to
# a system-site-packages once pip is installed.
if ! $PYTHON -m venv env ; then
echo "Creation of our virtualenv failed. If you're on Ubuntu, you probably need python3-venv."
exit 1
fi
if [ "$(uname)" != "Darwin" ]; then
$PYTHON -m venv env --upgrade --system-site-packages
fi
fi
source env/bin/activate
echo "Installing pip requirements"
if [ "$(uname)" == "Darwin" ]; then
./env/bin/pip install -r requirements-osx.txt
else
./env/bin/python -c "import PyQt5" >/dev/null 2>&1 || { echo >&2 "PyQt 5.4+ required. Install it and try again. Aborting"; exit 1; }
./env/bin/pip install -r requirements.txt
fi
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
echo ". env/bin/activate && python build.py && python run.py"

376
build.py Normal file
View File

@@ -0,0 +1,376 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
import sys
import os
import os.path as op
from optparse import OptionParser
import shutil
import compileall
from setuptools import setup, Extension
from hscommon import sphinxgen
from hscommon.build import (
add_to_pythonpath, print_and_do, copy_packages, filereplace,
get_module_version, move_all, copy_all, OSXAppStructure,
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
collect_stdlib_dependencies
)
from hscommon import loc
from hscommon.plat import ISOSX
from hscommon.util import ensure_folder, delete_files_with_pattern
def parse_args():
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage)
parser.add_option(
'--clean', action='store_true', dest='clean',
help="Clean build folder before building"
)
parser.add_option(
'--doc', action='store_true', dest='doc',
help="Build only the help file"
)
parser.add_option(
'--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."
)
parser.add_option(
'--loc', action='store_true', dest='loc',
help="Build only localization"
)
parser.add_option(
'--cocoa-ext', action='store_true', dest='cocoa_ext',
help="Build only Cocoa extensions"
)
parser.add_option(
'--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()
return options
def cocoa_app():
app_path = 'build/dupeGuru.app'
return OSXAppStructure(app_path)
def build_xibless(dest='cocoa/autogen'):
import xibless
ensure_folder(dest)
FNPAIRS = [
('ignore_list_dialog.py', 'IgnoreListDialog_UI'),
('deletion_options.py', 'DeletionOptions_UI'),
('problem_dialog.py', 'ProblemDialog_UI'),
('directory_panel.py', 'DirectoryPanel_UI'),
('prioritize_dialog.py', 'PrioritizeDialog_UI'),
('result_window.py', 'ResultWindow_UI'),
('main_menu.py', 'MainMenu_UI'),
('details_panel.py', 'DetailsPanel_UI'),
('details_panel_picture.py', 'DetailsPanelPicture_UI'),
]
for srcname, dstname in FNPAIRS:
xibless.generate(
op.join('cocoa', 'ui', srcname), op.join(dest, dstname),
localizationTable='Localizable'
)
for appmode in ('standard', 'music', 'picture'):
xibless.generate(
op.join('cocoa', 'ui', 'preferences_panel.py'),
op.join(dest, 'PreferencesPanel%s_UI' % appmode.capitalize()),
localizationTable='Localizable',
args={'appmode': appmode},
)
def build_cocoa(dev):
print("Creating OS X app structure")
app = cocoa_app()
app_version = get_module_version('core')
cocoa_project_path = 'cocoa'
filereplace(op.join(cocoa_project_path, 'InfoTemplate.plist'), op.join('build', 'Info.plist'), version=app_version)
app.create(op.join('build', 'Info.plist'))
print("Building localizations")
build_localizations('cocoa')
print("Building xibless UIs")
build_cocoalib_xibless()
build_xibless()
print("Building Python extensions")
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces()
print("Building the cocoa layer")
copy_embeddable_python_dylib('build')
pydep_folder = op.join(app.resources, 'py')
if not op.exists(pydep_folder):
os.mkdir(pydep_folder)
shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build')
tocopy = [
'core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'objp', 'send2trash', 'hsaudiotag',
]
copy_packages(tocopy, pydep_folder, create_links=dev)
sys.path.insert(0, 'build')
# ModuleFinder can't seem to correctly detect the multiprocessing dependency, so we have
# to manually specify it.
extra_deps = ['multiprocessing']
collect_stdlib_dependencies('build/dg_cocoa.py', pydep_folder, extra_deps=extra_deps)
del sys.path[0]
# Views are not referenced by python code, so they're not found by the collector.
copy_all('build/inter/*.so', op.join(pydep_folder, 'inter'))
if not dev:
# Important: Don't ever run delete_files_with_pattern('*.py') on dev builds because you'll
# be deleting all py files in symlinked folders.
compileall.compile_dir(pydep_folder, force=True, legacy=True)
delete_files_with_pattern(pydep_folder, '*.py')
delete_files_with_pattern(pydep_folder, '__pycache__')
print("Compiling with WAF")
os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..')
app.copy_executable('cocoa/build/dupeGuru')
build_help()
print("Copying resources and frameworks")
image_path = 'cocoa/dupeguru.icns'
resources = [image_path, 'cocoa/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
app.copy_resources(*resources, use_symlinks=dev)
app.copy_frameworks('build/Python')
print("Creating the run.py file")
tmpl = open('cocoa/run_template.py', 'rt').read()
run_contents = tmpl.replace('{{app_path}}', app.dest)
open('run.py', 'wt').write(run_contents)
def build_qt(dev):
print("Building localizations")
build_localizations('qt')
print("Building Qt stuff")
print_and_do("pyrcc5 {0} > {1}".format(op.join('qt', 'dg.qrc'), op.join('qt', 'dg_rc.py')))
fix_qt_resource_file(op.join('qt', 'dg_rc.py'))
build_help()
print("Creating the run.py file")
shutil.copy(op.join('qt', 'run_template.py'), 'run.py')
def build_help():
print("Generating Help")
current_path = op.abspath('.')
help_basepath = op.join(current_path, 'help', 'en')
help_destpath = op.join(current_path, 'build', 'help')
changelog_path = op.join(current_path, 'help', 'changelog')
tixurl = "https://github.com/hsoft/dupeguru/issues/{}"
confrepl = {'language': 'en'}
changelogtmpl = op.join(current_path, 'help', 'changelog.tmpl')
conftmpl = op.join(current_path, 'help', 'conf.tmpl')
sphinxgen.gen(help_basepath, help_destpath, changelog_path, tixurl, confrepl, conftmpl, changelogtmpl)
def build_qt_localizations():
loc.compile_all_po(op.join('qtlib', 'locale'))
loc.merge_locale_dir(op.join('qtlib', 'locale'), 'locale')
def build_localizations(ui):
loc.compile_all_po('locale')
if ui == 'cocoa':
app = cocoa_app()
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'en.lproj', 'Localizable.strings'))
locale_dest = op.join(app.resources, 'locale')
elif ui == 'qt':
build_qt_localizations()
locale_dest = op.join('build', 'locale')
if op.exists(locale_dest):
shutil.rmtree(locale_dest)
shutil.copytree('locale', locale_dest, ignore=shutil.ignore_patterns('*.po', '*.pot'))
def build_updatepot():
if ISOSX:
print("Updating Cocoa strings file.")
build_cocoalib_xibless('cocoalib/autogen')
loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj')
build_xibless()
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/en.lproj')
print("Building .pot files from source files")
print("Building core.pot")
loc.generate_pot(['core'], op.join('locale', 'core.pot'), ['tr'])
print("Building columns.pot")
loc.generate_pot(['core'], op.join('locale', 'columns.pot'), ['coltr'])
print("Building ui.pot")
# When we're not under OS X, we don't want to overwrite ui.pot because it contains Cocoa locs
# We want to merge the generated pot with the old pot in the most preserving way possible.
ui_packages = ['qt', op.join('cocoa', 'inter')]
loc.generate_pot(ui_packages, op.join('locale', 'ui.pot'), ['tr'], merge=(not ISOSX))
print("Building qtlib.pot")
loc.generate_pot(['qtlib'], op.join('qtlib', 'locale', 'qtlib.pot'), ['tr'])
if ISOSX:
print("Building cocoalib.pot")
cocoalib_pot = op.join('cocoalib', 'locale', 'cocoalib.pot')
os.remove(cocoalib_pot)
loc.strings2pot(op.join('cocoalib', 'en.lproj', 'cocoalib.strings'), cocoalib_pot)
print("Enhancing ui.pot with Cocoa's strings files")
loc.strings2pot(
op.join('cocoa', 'en.lproj', 'Localizable.strings'),
op.join('locale', 'ui.pot')
)
def build_mergepot():
print("Updating .po files using .pot files")
loc.merge_pots_into_pos('locale')
loc.merge_pots_into_pos(op.join('qtlib', 'locale'))
loc.merge_pots_into_pos(op.join('cocoalib', 'locale'))
def build_normpo():
loc.normalize_all_pos('locale')
loc.normalize_all_pos(op.join('qtlib', 'locale'))
loc.normalize_all_pos(op.join('cocoalib', 'locale'))
def build_cocoa_proxy_module():
print("Building Cocoa Proxy")
import objp.p2o
objp.p2o.generate_python_proxy_code('cocoalib/cocoa/CocoaProxy.h', 'build/CocoaProxy.m')
build_cocoa_ext(
"CocoaProxy", 'cocoalib/cocoa',
[
'cocoalib/cocoa/CocoaProxy.m', 'build/CocoaProxy.m', 'build/ObjP.m',
'cocoalib/HSErrorReportWindow.m', 'cocoa/autogen/HSErrorReportWindow_UI.m'
],
['AppKit', 'CoreServices'],
['cocoalib', 'cocoa/autogen']
)
def build_cocoa_bridging_interfaces():
print("Building Cocoa Bridging Interfaces")
import objp.o2p
import objp.p2o
add_to_pythonpath('cocoa')
add_to_pythonpath('cocoalib')
from cocoa.inter import (
PyGUIObject, GUIObjectView, PyColumns, ColumnsView, PyOutline,
OutlineView, PySelectableList, SelectableListView, PyTable, TableView, PyBaseApp,
PyTextField, ProgressWindowView, PyProgressWindow
)
from inter.deletion_options import PyDeletionOptions, DeletionOptionsView
from inter.details_panel import PyDetailsPanel, DetailsPanelView
from inter.directory_outline import PyDirectoryOutline, DirectoryOutlineView
from inter.prioritize_dialog import PyPrioritizeDialog, PrioritizeDialogView
from inter.prioritize_list import PyPrioritizeList, PrioritizeListView
from inter.problem_dialog import PyProblemDialog
from inter.ignore_list_dialog import PyIgnoreListDialog, IgnoreListDialogView
from inter.result_table import PyResultTable, ResultTableView
from inter.stats_label import PyStatsLabel, StatsLabelView
from inter.app import PyDupeGuru, DupeGuruView
allclasses = [
PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp,
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuru,
PyTextField, PyProgressWindow
]
for class_ in allclasses:
objp.o2p.generate_objc_code(class_, 'cocoa/autogen', inherit=True)
allclasses = [
GUIObjectView, ColumnsView, OutlineView, SelectableListView, TableView,
DetailsPanelView, DirectoryOutlineView, PrioritizeDialogView, PrioritizeListView,
IgnoreListDialogView, DeletionOptionsView, ResultTableView, StatsLabelView,
ProgressWindowView, DupeGuruView
]
clsspecs = [objp.o2p.spec_from_python_class(class_) for class_ in allclasses]
objp.p2o.generate_python_proxy_code_from_clsspec(clsspecs, 'build/CocoaViews.m')
build_cocoa_ext('CocoaViews', 'cocoa/inter', ['build/CocoaViews.m', 'build/ObjP.m'])
def build_pe_modules(ui):
print("Building PE Modules")
exts = [
Extension(
"_block",
[op.join('core', 'pe', 'modules', 'block.c'), op.join('core', 'pe', 'modules', 'common.c')]
),
Extension(
"_cache",
[op.join('core', 'pe', 'modules', 'cache.c'), op.join('core', 'pe', 'modules', 'common.c')]
),
]
if ui == 'qt':
exts.append(Extension("_block_qt", [op.join('qt', 'pe', 'modules', 'block.c')]))
elif ui == 'cocoa':
exts.append(Extension(
"_block_osx",
[op.join('core', 'pe', 'modules', 'block_osx.m'), op.join('core', 'pe', 'modules', 'common.c')],
extra_link_args=[
"-framework", "CoreFoundation",
"-framework", "Foundation",
"-framework", "ApplicationServices",
]
))
setup(
script_args=['build_ext', '--inplace'],
ext_modules=exts,
)
move_all('_block_qt*', op.join('qt', 'pe'))
move_all('_block*', op.join('core', 'pe'))
move_all('_cache*', op.join('core', 'pe'))
def build_normal(ui, dev):
print("Building dupeGuru with UI {}".format(ui))
add_to_pythonpath('.')
print("Building dupeGuru")
build_pe_modules(ui)
if ui == 'cocoa':
build_cocoa(dev)
elif ui == 'qt':
build_qt(dev)
def main():
options = parse_args()
ui = options.ui
if ui not in ('cocoa', 'qt'):
ui = 'cocoa' if ISOSX else 'qt'
if options.dev:
print("Building in Dev mode")
if options.clean:
for path in ['build', op.join('cocoa', 'build'), op.join('cocoa', 'autogen')]:
if op.exists(path):
shutil.rmtree(path)
if not op.exists('build'):
os.mkdir('build')
if options.doc:
build_help()
elif options.loc:
build_localizations(ui)
elif options.updatepot:
build_updatepot()
elif options.mergepot:
build_mergepot()
elif options.normpo:
build_normpo()
elif options.cocoa_ext:
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces()
elif options.cocoa_compile:
os.chdir('cocoa')
print_and_do('{0} waf configure && {0} waf'.format(sys.executable))
os.chdir('..')
cocoa_app().copy_executable('cocoa/build/dupeGuru')
elif options.xibless:
build_cocoalib_xibless()
build_xibless()
else:
build_normal(ui, options.dev)
if __name__ == '__main__':
main()

79
cocoa/AppDelegate.h Normal file
View File

@@ -0,0 +1,79 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyDupeGuru.h"
#import "ResultWindow.h"
#import "ResultTable.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "IgnoreListDialog.h"
#import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSAboutBox.h"
#import "HSRecentFiles.h"
#import "HSProgressWindow.h"
@interface AppDelegate : NSObject <NSFileManagerDelegate>
{
NSMenu *recentResultsMenu;
NSMenu *columnsMenu;
PyDupeGuru *model;
ResultWindow *_resultWindow;
DirectoryPanel *_directoryPanel;
DetailsPanel *_detailsPanel;
IgnoreListDialog *_ignoreListDialog;
ProblemDialog *_problemDialog;
DeletionOptions *_deletionOptions;
HSProgressWindow *_progressWindow;
NSWindowController *_preferencesPanel;
HSAboutBox *_aboutBox;
HSRecentFiles *_recentResults;
}
@property (readwrite, retain) NSMenu *recentResultsMenu;
@property (readwrite, retain) NSMenu *columnsMenu;
/* Virtual */
+ (NSDictionary *)defaultPreferences;
- (PyDupeGuru *)model;
- (DetailsPanel *)createDetailsPanel;
- (void)setScanOptions;
/* Public */
- (void)finalizeInit;
- (ResultWindow *)resultWindow;
- (DirectoryPanel *)directoryPanel;
- (DetailsPanel *)detailsPanel;
- (HSRecentFiles *)recentResults;
- (NSInteger)getAppMode;
- (void)setAppMode:(NSInteger)appMode;
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (void)applicationWillBecomeActive:(NSNotification *)aNotification;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender;
- (void)applicationWillTerminate:(NSNotification *)aNotification;
- (void)recentFileClicked:(NSString *)path;
/* Actions */
- (void)clearPictureCache;
- (void)loadResults;
- (void)openWebsite;
- (void)openHelp;
- (void)showAboutBox;
- (void)showDirectoryWindow;
- (void)showPreferencesPanel;
- (void)showResultWindow;
- (void)showIgnoreList;
- (void)startScanning;
/* model --> view */
- (void)showMessage:(NSString *)msg;
@end

394
cocoa/AppDelegate.m Normal file
View File

@@ -0,0 +1,394 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "AppDelegate.h"
#import "ProgressController.h"
#import "HSPyUtil.h"
#import "Consts.h"
#import "Dialogs.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "DetailsPanelPicture.h"
#import "PreferencesPanelStandard_UI.h"
#import "PreferencesPanelMusic_UI.h"
#import "PreferencesPanelPicture_UI.h"
@implementation AppDelegate
@synthesize recentResultsMenu;
@synthesize columnsMenu;
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionary];
[d setObject:i2n(1) forKey:@"scanTypeStandard"];
[d setObject:i2n(3) forKey:@"scanTypeMusic"];
[d setObject:i2n(0) forKey:@"scanTypePicture"];
[d setObject:i2n(95) forKey:@"minMatchPercentage"];
[d setObject:i2n(30) forKey:@"smallFileThreshold"];
[d setObject:b2n(YES) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[d setObject:b2n(YES) forKey:@"ignoreSmallFiles"];
[d setObject:b2n(NO) forKey:@"scanTagTrack"];
[d setObject:b2n(YES) forKey:@"scanTagArtist"];
[d setObject:b2n(YES) forKey:@"scanTagAlbum"];
[d setObject:b2n(YES) forKey:@"scanTagTitle"];
[d setObject:b2n(NO) forKey:@"scanTagGenre"];
[d setObject:b2n(NO) forKey:@"scanTagYear"];
[d setObject:b2n(NO) forKey:@"matchScaled"];
[d setObject:i2n(1) forKey:@"recreatePathType"];
[d setObject:i2n(11) forKey:TableFontSize];
[d setObject:b2n(YES) forKey:@"mixFileKind"];
[d setObject:b2n(NO) forKey:@"useRegexpFilter"];
[d setObject:b2n(NO) forKey:@"ignoreHardlinkMatches"];
[d setObject:b2n(NO) forKey:@"removeEmptyFolders"];
[d setObject:b2n(NO) forKey:@"DebugMode"];
[d setObject:@"" forKey:@"CustomCommand"];
[d setObject:[NSArray array] forKey:@"recentDirectories"];
[d setObject:[NSArray array] forKey:@"columnsOrder"];
[d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"];
return d;
}
+ (void)initialize
{
HSVTAdd *vt = [[[HSVTAdd alloc] initWithValue:4] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtRowHeightOffset"];
NSDictionary *d = [self defaultPreferences];
[[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d];
[[NSUserDefaults standardUserDefaults] registerDefaults:d];
}
- (id)init
{
self = [super init];
model = [[PyDupeGuru alloc] init];
[model bindCallback:createCallback(@"DupeGuruView", self)];
NSMutableIndexSet *contentsIndexes = [NSMutableIndexSet indexSet];
[contentsIndexes addIndex:1];
[contentsIndexes addIndex:2];
VTIsIntIn *vt = [[[VTIsIntIn alloc] initWithValues:contentsIndexes reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vt forName:@"vtScanTypeIsNotContent"];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:0];
VTIsIntIn *vtScanTypeIsFuzzy = [[[VTIsIntIn alloc] initWithValues:i reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsFuzzy forName:@"vtScanTypeIsFuzzy"];
i = [NSMutableIndexSet indexSetWithIndex:4];
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeMusicIsNotContent"];
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
return self;
}
- (void)finalizeInit
{
// We can only finalize initialization once the main menu has been created, which cannot happen
// before AppDelegate is created.
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
[_recentResults setDelegate:self];
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
_ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]];
_problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
_deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
[_progressWindow setParentWindow:[_directoryPanel window]];
// Lazily loaded
_aboutBox = nil;
_preferencesPanel = nil;
_resultWindow = nil;
_detailsPanel = nil;
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
}
/* Virtual */
- (PyDupeGuru *)model
{
return model;
}
- (DetailsPanel *)createDetailsPanel
{
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
return [[DetailsPanelPicture alloc] initWithApp:model];
}
else {
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
}
}
- (void)setScanOptions
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *scanTypeOptionName;
NSInteger appMode = [self getAppMode];
if (appMode == AppModePicture) {
scanTypeOptionName = @"scanTypePicture";
}
else if (appMode == AppModeMusic) {
scanTypeOptionName = @"scanTypeMusic";
}
else {
scanTypeOptionName = @"scanTypeStandard";
}
[model setScanType:n2i([ud objectForKey:scanTypeOptionName])];
[model setMinMatchPercentage:n2i([ud objectForKey:@"minMatchPercentage"])];
[model setWordWeighting:n2b([ud objectForKey:@"wordWeighting"])];
[model setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[model setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[model setMatchSimilarWords:n2b([ud objectForKey:@"matchSimilarWords"])];
int smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
int sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
[model setSizeThreshold:sizeThreshold];
[model enable:n2b([ud objectForKey:@"scanTagTrack"]) scanForTag:@"track"];
[model enable:n2b([ud objectForKey:@"scanTagArtist"]) scanForTag:@"artist"];
[model enable:n2b([ud objectForKey:@"scanTagAlbum"]) scanForTag:@"album"];
[model enable:n2b([ud objectForKey:@"scanTagTitle"]) scanForTag:@"title"];
[model enable:n2b([ud objectForKey:@"scanTagGenre"]) scanForTag:@"genre"];
[model enable:n2b([ud objectForKey:@"scanTagYear"]) scanForTag:@"year"];
[model setMatchScaled:n2b([ud objectForKey:@"matchScaled"])];
}
/* Public */
- (ResultWindow *)resultWindow
{
return _resultWindow;
}
- (DirectoryPanel *)directoryPanel
{
return _directoryPanel;
}
- (DetailsPanel *)detailsPanel
{
return _detailsPanel;
}
- (HSRecentFiles *)recentResults
{
return _recentResults;
}
- (NSInteger)getAppMode
{
return [model getAppMode];
}
- (void)setAppMode:(NSInteger)appMode
{
[model setAppMode:appMode];
if (_preferencesPanel != nil) {
[_preferencesPanel release];
_preferencesPanel = nil;
}
}
/* Actions */
- (void)clearPictureCache
{
NSString *msg = NSLocalizedString(@"Do you really want to remove all your cached picture analysis?", @"");
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
return;
[model clearPictureCache];
}
- (void)loadResults
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:NO];
[op setCanCreateDirectories:NO];
[op setAllowsMultipleSelection:NO];
[op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[op setTitle:NSLocalizedString(@"Select a results file to load", @"")];
if ([op runModal] == NSOKButton) {
NSString *filename = [[[op URLs] objectAtIndex:0] path];
[model loadResultsFrom:filename];
[[self recentResults] addFile:filename];
}
}
- (void)openWebsite
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru/"]];
}
- (void)openHelp
{
NSBundle *b = [NSBundle mainBundle];
NSString *p = [b pathForResource:@"index" ofType:@"html" inDirectory:@"help"];
NSURL *u = [NSURL fileURLWithPath:p];
[[NSWorkspace sharedWorkspace] openURL:u];
}
- (void)showAboutBox
{
if (_aboutBox == nil) {
_aboutBox = [[HSAboutBox alloc] initWithApp:model];
}
[[_aboutBox window] makeKeyAndOrderFront:nil];
}
- (void)showDirectoryWindow
{
[[[self directoryPanel] window] makeKeyAndOrderFront:nil];
}
- (void)showPreferencesPanel
{
if (_preferencesPanel == nil) {
NSWindow *window;
NSInteger appMode = [model getAppMode];
if (appMode == AppModePicture) {
window = createPreferencesPanelPicture_UI(nil);
}
else if (appMode == AppModeMusic) {
window = createPreferencesPanelMusic_UI(nil);
}
else {
window = createPreferencesPanelStandard_UI(nil);
}
_preferencesPanel = [[NSWindowController alloc] initWithWindow:window];
}
[_preferencesPanel showWindow:nil];
}
- (void)showResultWindow
{
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
}
- (void)showIgnoreList
{
[model showIgnoreList];
}
- (void)startScanning
{
[[self directoryPanel] startDuplicateScan];
}
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[model loadSession];
}
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
{
if (![[[self directoryPanel] window] isVisible]) {
[[self directoryPanel] showWindow:NSApp];
}
}
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
if ([model resultsAreModified]) {
NSString *msg = NSLocalizedString(@"You have unsaved results, do you really want to quit?", @"");
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) { // NO
return NSTerminateCancel;
}
}
return NSTerminateNow;
}
- (void)applicationWillTerminate:(NSNotification *)aNotification
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSInteger sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"];
if (sc >= 10) {
sc = -1;
[model purgeIgnoreList];
}
sc++;
[model saveSession];
[ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"];
// NSApplication does not release nib instances objects, we must do it manually
// Well, it isn't needed because the memory is freed anyway (we are quitting the application
// But I need to release HSRecentFiles so it saves the user defaults
[_directoryPanel release];
[_recentResults release];
}
- (void)recentFileClicked:(NSString *)path
{
[model loadResultsFrom:path];
}
/* model --> view */
- (void)showMessage:(NSString *)msg
{
[Dialogs showMessage:msg];
}
- (BOOL)askYesNoWithPrompt:(NSString *)prompt
{
return [Dialogs askYesNo:prompt] == NSAlertFirstButtonReturn;
}
- (void)createResultsWindow
{
if (_resultWindow != nil) {
[_resultWindow release];
}
if (_detailsPanel != nil) {
[_detailsPanel release];
}
// Warning: creation order is important
// If the details panel is not created first and that there are some results in the model
// (happens if we load results), a dupe selection event triggers a details refresh in the
// core before we have the chance to initialize it, and then we crash.
_detailsPanel = [self createDetailsPanel];
_resultWindow = [[ResultWindow alloc] initWithParentApp:self];
}
- (void)showResultsWindow
{
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
}
- (void)showProblemDialog
{
[_problemDialog showWindow:self];
}
- (NSString *)selectDestFolderWithPrompt:(NSString *)prompt
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:NO];
[op setCanChooseDirectories:YES];
[op setCanCreateDirectories:YES];
[op setAllowsMultipleSelection:NO];
[op setTitle:prompt];
if ([op runModal] == NSOKButton) {
return [[[op URLs] objectAtIndex:0] path];
}
else {
return nil;
}
}
- (NSString *)selectDestFileWithPrompt:(NSString *)prompt extension:(NSString *)extension
{
NSSavePanel *sp = [NSSavePanel savePanel];
[sp setCanCreateDirectories:YES];
[sp setAllowedFileTypes:[NSArray arrayWithObject:extension]];
[sp setTitle:prompt];
if ([sp runModal] == NSOKButton) {
return [[sp URL] path];
}
else {
return nil;
}
}
@end

24
cocoa/Consts.h Normal file
View File

@@ -0,0 +1,24 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#define JobStarted @"JobStarted"
#define JobInProgress @"JobInProgress"
#define TableFontSize @"TableFontSize"
#define jobLoad @"job_load"
#define jobScan @"job_scan"
#define jobCopy @"job_copy"
#define jobMove @"job_move"
#define jobDelete @"job_delete"
#define DGPrioritizeIndexPasteboardType @"DGPrioritizeIndexPasteboardType"
#define ImageLoadedNotification @"ImageLoadedNotification"
#define AppModeStandard 0
#define AppModeMusic 1
#define AppModePicture 2

33
cocoa/DeletionOptions.h Normal file
View File

@@ -0,0 +1,33 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyDeletionOptions.h"
@interface DeletionOptions : NSWindowController
{
PyDeletionOptions *model;
NSTextField *messageTextField;
NSButton *linkButton;
NSMatrix *linkTypeRadio;
NSButton *directButton;
}
@property (readwrite, retain) NSTextField *messageTextField;
@property (readwrite, retain) NSButton *linkButton;
@property (readwrite, retain) NSMatrix *linkTypeRadio;
@property (readwrite, retain) NSButton *directButton;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (void)updateOptions;
- (void)proceed;
- (void)cancel;
@end

72
cocoa/DeletionOptions.m Normal file
View File

@@ -0,0 +1,72 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DeletionOptions.h"
#import "DeletionOptions_UI.h"
#import "HSPyUtil.h"
@implementation DeletionOptions
@synthesize messageTextField;
@synthesize linkButton;
@synthesize linkTypeRadio;
@synthesize directButton;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
model = [[PyDeletionOptions alloc] initWithModel:aPyRef];
[self setWindow:createDeletionOptions_UI(self)];
[model bindCallback:createCallback(@"DeletionOptionsView", self)];
return self;
}
- (void)dealloc
{
[model release];
[super dealloc];
}
- (void)updateOptions
{
[model setLinkDeleted:[linkButton state] == NSOnState];
[model setUseHardlinks:[linkTypeRadio selectedColumn] == 1];
[model setDirect:[directButton state] == NSOnState];
}
- (void)proceed
{
[NSApp stopModalWithCode:NSOKButton];
}
- (void)cancel
{
[NSApp stopModalWithCode:NSCancelButton];
}
/* model --> view */
- (void)updateMsg:(NSString *)msg
{
[messageTextField setStringValue:msg];
}
- (BOOL)show
{
[linkButton setState:NSOffState];
[directButton setState:NSOffState];
[linkTypeRadio selectCellAtRow:0 column:0];
NSInteger r = [NSApp runModalForWindow:[self window]];
[[self window] close];
return r == NSOKButton;
}
- (void)setHardlinkOptionEnabled:(BOOL)enabled
{
[linkTypeRadio setEnabled:enabled];
}
@end

31
cocoa/DetailsPanel.h Normal file
View File

@@ -0,0 +1,31 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Python.h>
#import "PyDetailsPanel.h"
@interface DetailsPanel : NSWindowController <NSTableViewDataSource>
{
NSTableView *detailsTable;
PyDetailsPanel *model;
}
@property (readwrite, retain) NSTableView *detailsTable;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (PyDetailsPanel *)model;
- (NSWindow *)createWindow;
- (BOOL)isVisible;
- (void)toggleVisibility;
/* Python --> Cocoa */
- (void)refresh;
@end

81
cocoa/DetailsPanel.m Normal file
View File

@@ -0,0 +1,81 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DetailsPanel.h"
#import "HSPyUtil.h"
#import "DetailsPanel_UI.h"
@implementation DetailsPanel
@synthesize detailsTable;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
[self setWindow:[self createWindow]];
model = [[PyDetailsPanel alloc] initWithModel:aPyRef];
[model bindCallback:createCallback(@"DetailsPanelView", self)];
return self;
}
- (void)dealloc
{
[model release];
[super dealloc];
}
- (PyDetailsPanel *)model
{
return (PyDetailsPanel *)model;
}
- (NSWindow *)createWindow
{
return createDetailsPanel_UI(self);
}
- (void)refreshDetails
{
[detailsTable reloadData];
}
- (BOOL)isVisible
{
return [[self window] isVisible];
}
- (void)toggleVisibility
{
if ([self isVisible]) {
[[self window] close];
}
else {
[self refreshDetails]; // selection might have changed since last time
[[self window] orderFront:nil];
}
}
/* NSTableView Delegate */
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [[self model] numberOfRows];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
return [[self model] valueForColumn:[column identifier] row:row];
}
/* Python --> Cocoa */
- (void)refresh
{
if ([[self window] isVisible]) {
[self refreshDetails];
}
}
@end

View File

@@ -0,0 +1,32 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "DetailsPanel.h"
#import "PyDupeGuru.h"
@interface DetailsPanelPicture : DetailsPanel
{
NSImageView *dupeImage;
NSProgressIndicator *dupeProgressIndicator;
NSImageView *refImage;
NSProgressIndicator *refProgressIndicator;
PyDupeGuru *pyApp;
BOOL _needsRefresh;
NSString *_dupePath;
NSString *_refPath;
}
@property (readwrite, retain) NSImageView *dupeImage;
@property (readwrite, retain) NSProgressIndicator *dupeProgressIndicator;
@property (readwrite, retain) NSImageView *refImage;
@property (readwrite, retain) NSProgressIndicator *refProgressIndicator;
- (id)initWithApp:(PyDupeGuru *)aApp;
@end

View File

@@ -1,28 +1,40 @@
/* /*
Copyright 2009 Hardcoded Software (http://www.hardcoded.net) Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "HS" License as described in the "LICENSE" file, This software is licensed under the "GPLv3" 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/hs_license http://www.gnu.org/licenses/gpl-3.0.html
*/ */
#import "Utils.h" #import "Utils.h"
#import "NSNotificationAdditions.h" #import "NSNotificationAdditions.h"
#import "NSImageAdditions.h" #import "NSImageAdditions.h"
#import "PyDupeGuru.h" #import "PyDupeGuru.h"
#import "DetailsPanel.h" #import "DetailsPanelPicture.h"
#import "Consts.h" #import "Consts.h"
#import "DetailsPanelPicture_UI.h"
@implementation DetailsPanel @implementation DetailsPanelPicture
- (id)initWithPy:(PyApp *)aPy
@synthesize dupeImage;
@synthesize dupeProgressIndicator;
@synthesize refImage;
@synthesize refProgressIndicator;
- (id)initWithApp:(PyDupeGuru *)aApp
{ {
self = [super initWithPy:aPy]; self = [super initWithPyRef:[aApp detailsPanel]];
py = aPy; pyApp = aApp;
_needsRefresh = YES; _needsRefresh = YES;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageLoaded:) name:ImageLoadedNotification object:self]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageLoaded:) name:ImageLoadedNotification object:self];
return self; return self;
} }
- (NSWindow *)createWindow
{
return createDetailsPanelPicture_UI(self);
}
- (void)loadImageAsync:(NSString *)imagePath - (void)loadImageAsync:(NSString *)imagePath
{ {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
@@ -36,18 +48,18 @@ http://www.hardcoded.net/licenses/hs_license
[pool release]; [pool release];
} }
- (void)refresh - (void)refreshDetails
{ {
if (!_needsRefresh) if (!_needsRefresh)
return; return;
[detailsTable reloadData]; [detailsTable reloadData];
NSString *refPath = [(PyDupeGuru *)py getSelectedDupeRefPath]; NSString *refPath = [pyApp getSelectedDupeRefPath];
if (_refPath != nil) if (_refPath != nil)
[_refPath autorelease]; [_refPath autorelease];
_refPath = [refPath retain]; _refPath = [refPath retain];
[NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:refPath]; [NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:refPath];
NSString *dupePath = [(PyDupeGuru *)py getSelectedDupePath]; NSString *dupePath = [pyApp getSelectedDupePath];
if (_dupePath != nil) if (_dupePath != nil)
[_dupePath autorelease]; [_dupePath autorelease];
_dupePath = [dupePath retain]; _dupePath = [dupePath retain];
@@ -59,12 +71,6 @@ http://www.hardcoded.net/licenses/hs_license
} }
/* Notifications */ /* Notifications */
- (void)duplicateSelectionChanged:(NSNotification *)aNotification
{
_needsRefresh = YES;
[super duplicateSelectionChanged:aNotification];
}
- (void)imageLoaded:(NSNotification *)aNotification - (void)imageLoaded:(NSNotification *)aNotification
{ {
NSString *imagePath = [[aNotification userInfo] valueForKey:@"imagePath"]; NSString *imagePath = [[aNotification userInfo] valueForKey:@"imagePath"];
@@ -80,4 +86,11 @@ http://www.hardcoded.net/licenses/hs_license
[dupeProgressIndicator stopAnimation:nil]; [dupeProgressIndicator stopAnimation:nil];
} }
} }
/* Python --> Cocoa */
- (void)refresh
{
_needsRefresh = YES;
[super refresh];
}
@end @end

21
cocoa/DirectoryOutline.h Normal file
View File

@@ -0,0 +1,21 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Python.h>
#import "HSOutline.h"
#import "PyDirectoryOutline.h"
#define DGAddedFoldersNotification @"DGAddedFoldersNotification"
@interface DirectoryOutline : HSOutline {}
- (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView;
- (PyDirectoryOutline *)model;
- (void)selectAll;
@end;

87
cocoa/DirectoryOutline.m Normal file
View File

@@ -0,0 +1,87 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DirectoryOutline.h"
@implementation DirectoryOutline
- (id)initWithPyRef:(PyObject *)aPyRef outlineView:(HSOutlineView *)aOutlineView
{
self = [super initWithPyRef:aPyRef wrapperClass:[PyDirectoryOutline class]
callbackClassName:@"DirectoryOutlineView" view:aOutlineView];
[[self view] registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
return self;
}
- (PyDirectoryOutline *)model
{
return (PyDirectoryOutline *)model;
}
/* Public */
- (void)selectAll
{
[[self model] selectAll];
}
/* Delegate */
- (NSDragOperation)outlineView:(NSOutlineView *)outlineView validateDrop:(id < NSDraggingInfo >)info proposedItem:(id)item proposedChildIndex:(NSInteger)index
{
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [info draggingSourceOperationMask];
pboard = [info draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
if (sourceDragMask & NSDragOperationLink)
return NSDragOperationLink;
}
return NSDragOperationNone;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id < NSDraggingInfo >)info item:(id)item childIndex:(NSInteger)index
{
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [info draggingSourceOperationMask];
pboard = [info draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
NSArray *foldernames = [pboard propertyListForType:NSFilenamesPboardType];
if (!(sourceDragMask & NSDragOperationLink))
return NO;
for (NSString *foldername in foldernames) {
[[self model] addDirectory:foldername];
}
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:foldernames forKey:@"foldernames"];
[[NSNotificationCenter defaultCenter] postNotificationName:DGAddedFoldersNotification
object:self userInfo:userInfo];
}
return YES;
}
- (void)outlineView:(NSOutlineView *)aOutlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
if ([cell isKindOfClass:[NSTextFieldCell class]]) {
NSTextFieldCell *textCell = cell;
NSIndexPath *path = item;
BOOL selected = [path isEqualTo:[[self view] selectedPath]];
if (selected) {
[textCell setTextColor:[NSColor blackColor]];
return;
}
NSInteger state = [self intProperty:@"state" valueAtPath:path];
if (state == 1) {
[textCell setTextColor:[NSColor blueColor]];
}
else if (state == 2) {
[textCell setTextColor:[NSColor redColor]];
}
else {
[textCell setTextColor:[NSColor blackColor]];
}
}
}
@end

57
cocoa/DirectoryPanel.h Normal file
View File

@@ -0,0 +1,57 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "HSOutlineView.h"
#import "HSRecentFiles.h"
#import "DirectoryOutline.h"
#import "PyDupeGuru.h"
@class AppDelegate;
@interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate>
{
AppDelegate *_app;
PyDupeGuru *model;
HSRecentFiles *_recentDirectories;
DirectoryOutline *outline;
BOOL _alwaysShowPopUp;
NSSegmentedControl *appModeSelector;
NSPopUpButton *scanTypePopup;
NSPopUpButton *addButtonPopUp;
NSPopUpButton *loadRecentButtonPopUp;
HSOutlineView *outlineView;
NSButton *removeButton;
NSButton *loadResultsButton;
}
@property (readwrite, retain) NSSegmentedControl *appModeSelector;
@property (readwrite, retain) NSPopUpButton *scanTypePopup;
@property (readwrite, retain) NSPopUpButton *addButtonPopUp;
@property (readwrite, retain) NSPopUpButton *loadRecentButtonPopUp;
@property (readwrite, retain) HSOutlineView *outlineView;
@property (readwrite, retain) NSButton *removeButton;
@property (readwrite, retain) NSButton *loadResultsButton;
- (id)initWithParentApp:(AppDelegate *)aParentApp;
- (void)fillPopUpMenu;
- (void)fillScanTypeMenu;
- (void)adjustUIToLocalization;
- (void)askForDirectory;
- (void)popupAddDirectoryMenu:(id)sender;
- (void)popupLoadRecentMenu:(id)sender;
- (void)removeSelectedDirectory;
- (void)startDuplicateScan;
- (void)addDirectory:(NSString *)directory;
- (void)refreshRemoveButtonText;
- (void)markAll;
@end

256
cocoa/DirectoryPanel.m Normal file
View File

@@ -0,0 +1,256 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "DirectoryPanel.h"
#import "DirectoryPanel_UI.h"
#import "Dialogs.h"
#import "Utils.h"
#import "AppDelegate.h"
#import "Consts.h"
@implementation DirectoryPanel
@synthesize appModeSelector;
@synthesize scanTypePopup;
@synthesize addButtonPopUp;
@synthesize loadRecentButtonPopUp;
@synthesize outlineView;
@synthesize removeButton;
@synthesize loadResultsButton;
- (id)initWithParentApp:(AppDelegate *)aParentApp
{
self = [super initWithWindow:nil];
[self setWindow:createDirectoryPanel_UI(self)];
_app = aParentApp;
model = [_app model];
[[self window] setTitle:[model appName]];
self.appModeSelector.selectedSegment = 0;
[self fillScanTypeMenu];
_alwaysShowPopUp = NO;
[self fillPopUpMenu];
_recentDirectories = [[HSRecentFiles alloc] initWithName:@"recentDirectories" menu:[addButtonPopUp menu]];
[_recentDirectories setDelegate:self];
outline = [[DirectoryOutline alloc] initWithPyRef:[model directoryTree] outlineView:outlineView];
[self refreshRemoveButtonText];
[self adjustUIToLocalization];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directorySelectionChanged:)
name:NSOutlineViewSelectionDidChangeNotification object:outlineView];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(outlineAddedFolders:)
name:DGAddedFoldersNotification object:outline];
return self;
}
- (void)dealloc
{
[outline release];
[_recentDirectories release];
[super dealloc];
}
/* Private */
- (void)fillPopUpMenu
{
NSMenu *m = [addButtonPopUp menu];
NSMenuItem *mi = [m addItemWithTitle:NSLocalizedString(@"Add New Folder...", @"") action:@selector(askForDirectory) keyEquivalent:@""];
[mi setTarget:self];
[m addItem:[NSMenuItem separatorItem]];
}
- (void)fillScanTypeMenu
{
[[self scanTypePopup] unbind:@"selectedIndex"];
[[self scanTypePopup] removeAllItems];
[[self scanTypePopup] addItemsWithTitles:[[_app model] getScanOptions]];
NSString *keypath;
NSInteger appMode = [_app getAppMode];
if (appMode == AppModePicture) {
keypath = @"values.scanTypePicture";
}
else if (appMode == AppModeMusic) {
keypath = @"values.scanTypeMusic";
}
else {
keypath = @"values.scanTypeStandard";
}
[[self scanTypePopup] bind:@"selectedIndex" toObject:[NSUserDefaultsController sharedUserDefaultsController] withKeyPath:keypath options:nil];
}
- (void)adjustUIToLocalization
{
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
NSInteger loadResultsWidthDelta = 0;
if ([lang isEqual:@"ru"]) {
loadResultsWidthDelta = 50;
}
else if ([lang isEqual:@"uk"]) {
loadResultsWidthDelta = 70;
}
else if ([lang isEqual:@"hy"]) {
loadResultsWidthDelta = 30;
}
if (loadResultsWidthDelta) {
NSRect r = [loadResultsButton frame];
r.size.width += loadResultsWidthDelta;
r.origin.x -= loadResultsWidthDelta;
[loadResultsButton setFrame:r];
}
}
/* Actions */
- (void)askForDirectory
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:YES];
[op setAllowsMultipleSelection:YES];
[op setTitle:NSLocalizedString(@"Select a folder to add to the scanning list", @"")];
[op setDelegate:self];
if ([op runModal] == NSOKButton) {
for (NSURL *directoryURL in [op URLs]) {
[self addDirectory:[directoryURL path]];
}
}
}
- (void)changeAppMode:(id)sender
{
NSInteger appMode;
NSUInteger selectedSegment = self.appModeSelector.selectedSegment;
if (selectedSegment == 2) {
appMode = AppModePicture;
}
else if (selectedSegment == 1) {
appMode = AppModeMusic;
}
else {
appMode = AppModeStandard;
}
[_app setAppMode:appMode];
[self fillScanTypeMenu];
}
- (void)popupAddDirectoryMenu:(id)sender
{
if ((!_alwaysShowPopUp) && ([[_recentDirectories filepaths] count] == 0)) {
[self askForDirectory];
}
else {
[addButtonPopUp selectItem:nil];
[[addButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]];
}
}
- (void)popupLoadRecentMenu:(id)sender
{
if ([[[_app recentResults] filepaths] count] > 0) {
NSMenu *m = [loadRecentButtonPopUp menu];
while ([m numberOfItems] > 0) {
[m removeItemAtIndex:0];
}
NSMenuItem *mi = [m addItemWithTitle:NSLocalizedString(@"Load from file...", @"") action:@selector(loadResults) keyEquivalent:@""];
[mi setTarget:_app];
[m addItem:[NSMenuItem separatorItem]];
[[_app recentResults] fillMenu:m];
[loadRecentButtonPopUp selectItem:nil];
[[loadRecentButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]];
}
else {
[_app loadResults];
}
}
- (void)removeSelectedDirectory
{
[[self window] makeKeyAndOrderFront:nil];
[[outline model] removeSelectedDirectory];
[self refreshRemoveButtonText];
}
- (void)startDuplicateScan
{
if ([model resultsAreModified]) {
if ([Dialogs askYesNo:NSLocalizedString(@"You have unsaved results, do you really want to continue?", @"")] == NSAlertSecondButtonReturn) // NO
return;
}
[_app setScanOptions];
[model doScan];
}
/* Public */
- (void)addDirectory:(NSString *)directory
{
[model addDirectory:directory];
[_recentDirectories addFile:directory];
[[self window] makeKeyAndOrderFront:nil];
}
- (void)refreshRemoveButtonText
{
if ([outlineView selectedRow] < 0) {
[removeButton setEnabled:NO];
return;
}
[removeButton setEnabled:YES];
NSIndexPath *path = [outline selectedIndexPath];
if (path != nil) {
NSInteger state = [outline intProperty:@"state" valueAtPath:path];
BOOL shouldDisplayArrow = ([path length] > 1) && (state == 2);
NSString *imgName = shouldDisplayArrow ? @"NSGoLeftTemplate" : @"NSRemoveTemplate";
[removeButton setImage:[NSImage imageNamed:imgName]];
}
}
- (void)markAll
{
/* markAll isn't very descriptive of what we do, but since we re-use the Mark All button from
the result window, we don't have much choice.
*/
[outline selectAll];
}
/* Delegate */
- (BOOL)panel:(id)sender shouldShowFilename:(NSString *)path
{
BOOL isdir;
[[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isdir];
return isdir;
}
- (void)recentFileClicked:(NSString *)path
{
[self addDirectory:path];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
if ([item action] == @selector(markAll)) {
[item setTitle:NSLocalizedString(@"Select All", @"")];
}
return YES;
}
/* Notifications */
- (void)directorySelectionChanged:(NSNotification *)aNotification
{
[self refreshRemoveButtonText];
}
- (void)outlineAddedFolders:(NSNotification *)aNotification
{
NSArray *foldernames = [[aNotification userInfo] objectForKey:@"foldernames"];
for (NSString *foldername in foldernames) {
[_recentDirectories addFile:foldername];
}
}
@end

25
cocoa/IgnoreListDialog.h Normal file
View File

@@ -0,0 +1,25 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyIgnoreListDialog.h"
#import "HSTable.h"
@interface IgnoreListDialog : NSWindowController
{
PyIgnoreListDialog *model;
HSTable *ignoreListTable;
NSTableView *ignoreListTableView;
}
@property (readwrite, retain) PyIgnoreListDialog *model;
@property (readwrite, retain) NSTableView *ignoreListTableView;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (void)initializeColumns;
@end

51
cocoa/IgnoreListDialog.m Normal file
View File

@@ -0,0 +1,51 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "IgnoreListDialog.h"
#import "IgnoreListDialog_UI.h"
#import "HSPyUtil.h"
@implementation IgnoreListDialog
@synthesize model;
@synthesize ignoreListTableView;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
self.model = [[[PyIgnoreListDialog alloc] initWithModel:aPyRef] autorelease];
[self.model bindCallback:createCallback(@"IgnoreListDialogView", self)];
[self setWindow:createIgnoreListDialog_UI(self)];
ignoreListTable = [[HSTable alloc] initWithPyRef:[model ignoreListTable] tableView:ignoreListTableView];
[self initializeColumns];
return self;
}
- (void)dealloc
{
[ignoreListTable release];
[super dealloc];
}
- (void)initializeColumns
{
HSColumnDef defs[] = {
{@"path1", 240, 40, 0, NO, nil},
{@"path2", 240, 40, 0, NO, nil},
nil
};
[[ignoreListTable columns] initializeColumns:defs];
[[ignoreListTable columns] setColumnsAsReadOnly];
}
/* model --> view */
- (void)show
{
[self showWindow:self];
}
@end

View File

@@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>English</string> <string>English</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string> <string>dupeGuru</string>
<key>CFBundleHelpBookFolder</key> <key>CFBundleHelpBookFolder</key>
<string>dupeguru_help</string> <string>dupeguru_help</string>
<key>CFBundleHelpBookName</key> <key>CFBundleHelpBookName</key>
@@ -13,25 +13,25 @@
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
<string>dupeguru</string> <string>dupeguru</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>com.hardcoded_software.dupeguru</string> <string>com.hardcoded-software.dupeguru</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>${PRODUCT_NAME}</string> <string>dupeGuru</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>hsft</string> <string>hsft</string>
<key>CFBundleShortVersionString</key>
<string>{version}</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>2.9.0</string> <string>{version}</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2009</string> <string>© Hardcoded Software, 2016</string>
<key>SUFeedURL</key> <key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru.appcast</string> <string>https://www.hardcoded.net/updates/dupeguru.appcast</string>
<key>SUPublicDSAKeyFile</key> <key>SUPublicDSAKeyFile</key>
<string>dsa_pub.pem</string> <string>dsa_pub.pem</string>
</dict> </dict>

37
cocoa/PrioritizeDialog.h Normal file
View File

@@ -0,0 +1,37 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyPrioritizeDialog.h"
#import "HSPopUpList.h"
#import "HSSelectableList.h"
#import "PrioritizeList.h"
#import "PyDupeGuru.h"
@interface PrioritizeDialog : NSWindowController
{
NSPopUpButton *categoryPopUpView;
NSTableView *criteriaTableView;
NSTableView *prioritizationTableView;
PyPrioritizeDialog *model;
HSPopUpList *categoryPopUp;
HSSelectableList *criteriaList;
PrioritizeList *prioritizationList;
}
@property (readwrite, retain) NSPopUpButton *categoryPopUpView;
@property (readwrite, retain) NSTableView *criteriaTableView;
@property (readwrite, retain) NSTableView *prioritizationTableView;
- (id)initWithApp:(PyDupeGuru *)aApp;
- (PyPrioritizeDialog *)model;
- (void)ok;
- (void)cancel;
@end;

56
cocoa/PrioritizeDialog.m Normal file
View File

@@ -0,0 +1,56 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "PrioritizeDialog.h"
#import "PrioritizeDialog_UI.h"
#import "HSPyUtil.h"
@implementation PrioritizeDialog
@synthesize categoryPopUpView;
@synthesize criteriaTableView;
@synthesize prioritizationTableView;
- (id)initWithApp:(PyDupeGuru *)aApp
{
self = [super initWithWindowNibName:@"PrioritizeDialog"];
model = [[PyPrioritizeDialog alloc] initWithApp:[aApp pyRef]];
[self setWindow:createPrioritizeDialog_UI(self)];
categoryPopUp = [[HSPopUpList alloc] initWithPyRef:[[self model] categoryList] popupView:categoryPopUpView];
criteriaList = [[HSSelectableList alloc] initWithPyRef:[[self model] criteriaList] tableView:criteriaTableView];
prioritizationList = [[PrioritizeList alloc] initWithPyRef:[[self model] prioritizationList] tableView:prioritizationTableView];
[model bindCallback:createCallback(@"PrioritizeDialogView", self)];
return self;
}
- (void)dealloc
{
[categoryPopUp release];
[criteriaList release];
[prioritizationList release];
[model release];
[super dealloc];
}
- (PyPrioritizeDialog *)model
{
return (PyPrioritizeDialog *)model;
}
- (void)ok
{
[NSApp stopModal];
[self close];
}
- (void)cancel
{
[NSApp abortModal];
[self close];
}
@end

16
cocoa/PrioritizeList.h Normal file
View File

@@ -0,0 +1,16 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "HSSelectableList.h"
#import "PyPrioritizeList.h"
@interface PrioritizeList : HSSelectableList {}
- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView;
- (PyPrioritizeList *)model;
@end

58
cocoa/PrioritizeList.m Normal file
View File

@@ -0,0 +1,58 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "PrioritizeList.h"
#import "Utils.h"
#import "Consts.h"
@implementation PrioritizeList
- (id)initWithPyRef:(PyObject *)aPyRef tableView:(NSTableView *)aTableView
{
self = [super initWithPyRef:aPyRef wrapperClass:[PyPrioritizeList class]
callbackClassName:@"PrioritizeListView" view:aTableView];
return self;
}
- (PyPrioritizeList *)model
{
return (PyPrioritizeList *)model;
}
- (void)setView:(NSTableView *)aTableView
{
[super setView:aTableView];
[[self view] registerForDraggedTypes:[NSArray arrayWithObject:DGPrioritizeIndexPasteboardType]];
}
- (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard*)pboard
{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:rowIndexes];
[pboard declareTypes:[NSArray arrayWithObject:DGPrioritizeIndexPasteboardType] owner:self];
[pboard setData:data forType:DGPrioritizeIndexPasteboardType];
return YES;
}
- (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(NSInteger)row
proposedDropOperation:(NSTableViewDropOperation)op
{
if (op == NSTableViewDropAbove) {
return NSDragOperationMove;
}
return NSDragOperationNone;
}
- (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id <NSDraggingInfo>)info
row:(NSInteger)row dropOperation:(NSTableViewDropOperation)operation
{
NSPasteboard* pboard = [info draggingPasteboard];
NSData* rowData = [pboard dataForType:DGPrioritizeIndexPasteboardType];
NSIndexSet* rowIndexes = [NSKeyedUnarchiver unarchiveObjectWithData:rowData];
[[self model] moveIndexes:[Utils indexSet2Array:rowIndexes] toIndex:row];
return YES;
}
@end

26
cocoa/ProblemDialog.h Normal file
View File

@@ -0,0 +1,26 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "PyProblemDialog.h"
#import "HSTable.h"
@interface ProblemDialog : NSWindowController
{
PyProblemDialog *model;
HSTable *problemTable;
NSTableView *problemTableView;
}
@property (readwrite, retain) PyProblemDialog *model;
@property (readwrite, retain) NSTableView *problemTableView;
- (id)initWithPyRef:(PyObject *)aPyRef;
- (void)initializeColumns;
@end

44
cocoa/ProblemDialog.m Normal file
View File

@@ -0,0 +1,44 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "ProblemDialog.h"
#import "ProblemDialog_UI.h"
#import "Utils.h"
@implementation ProblemDialog
@synthesize model;
@synthesize problemTableView;
- (id)initWithPyRef:(PyObject *)aPyRef
{
self = [super initWithWindow:nil];
self.model = [[PyProblemDialog alloc] initWithModel:aPyRef];
[self setWindow:createProblemDialog_UI(self)];
problemTable = [[HSTable alloc] initWithPyRef:[self.model problemTable] tableView:problemTableView];
[self initializeColumns];
return self;
}
- (void)dealloc
{
[problemTable release];
[super dealloc];
}
- (void)initializeColumns
{
HSColumnDef defs[] = {
{@"path", 202, 40, 0, NO, nil},
{@"msg", 228, 40, 0, NO, nil},
nil
};
[[problemTable columns] initializeColumns:defs];
[[problemTable columns] setColumnsAsReadOnly];
}
@end

23
cocoa/ResultTable.h Normal file
View File

@@ -0,0 +1,23 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Quartz/Quartz.h>
#import "HSTable.h"
#import "PyResultTable.h"
@interface ResultTable : HSTable <QLPreviewPanelDataSource, QLPreviewPanelDelegate>
{
}
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView;
- (PyResultTable *)model;
- (BOOL)powerMarkerMode;
- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode;
- (BOOL)deltaValuesMode;
- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode;
@end;

180
cocoa/ResultTable.m Normal file
View File

@@ -0,0 +1,180 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "ResultTable.h"
#import "Dialogs.h"
#import "Utils.h"
#import "HSQuicklook.h"
@interface HSTable (private)
- (void)setPySelection;
- (void)setViewSelection;
@end
@implementation ResultTable
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTableView *)aTableView
{
self = [super initWithPyRef:aPyRef wrapperClass:[PyResultTable class] callbackClassName:@"ResultTableView" view:aTableView];
return self;
}
- (PyResultTable *)model
{
return (PyResultTable *)model;
}
/* Private */
- (void)updateQuicklookIfNeeded
{
if ([[QLPreviewPanel sharedPreviewPanel] dataSource] == self) {
[[QLPreviewPanel sharedPreviewPanel] reloadData];
}
}
- (void)setPySelection
{
[super setPySelection];
[self updateQuicklookIfNeeded];
}
- (void)setViewSelection
{
[super setViewSelection];
[self updateQuicklookIfNeeded];
}
/* Public */
- (BOOL)powerMarkerMode
{
return [[self model] powerMarkerMode];
}
- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode
{
[[self model] setPowerMarkerMode:aPowerMarkerMode];
}
- (BOOL)deltaValuesMode
{
return [[self model] deltaValuesMode];
}
- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode
{
[[self model] setDeltaValuesMode:aDeltaValuesMode];
}
/* Datasource */
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
NSString *identifier = [column identifier];
if ([identifier isEqual:@"marked"]) {
return [[self model] valueForColumn:@"marked" row:row];
}
return [[self model] valueForRow:row column:identifier];
}
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
NSString *identifier = [column identifier];
if ([identifier isEqual:@"marked"]) {
[[self model] setValue:object forColumn:identifier row:row];
}
else if ([identifier isEqual:@"name"]) {
NSString *oldName = [[self model] valueForRow:row column:identifier];
NSString *newName = object;
if (![newName isEqual:oldName]) {
BOOL renamed = [[self model] renameSelected:newName];
if (!renamed) {
[Dialogs showMessage:[NSString stringWithFormat:NSLocalizedString(@"The name '%@' already exists.", @""), newName]];
}
else {
[[self view] setNeedsDisplay:YES];
}
}
}
}
/* Delegate */
- (void)tableView:(NSTableView *)aTableView didClickTableColumn:(NSTableColumn *)tableColumn
{
if ([[[self view] sortDescriptors] count] < 1)
return;
NSSortDescriptor *sd = [[[self view] sortDescriptors] objectAtIndex:0];
[[self model] sortBy:[sd key] ascending:[sd ascending]];
}
- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
BOOL isSelected = [[self view] isRowSelected:row];
BOOL isMarkable = n2b([[self model] valueForColumn:@"markable" row:row]);
if ([[column identifier] isEqual:@"marked"]) {
[cell setEnabled:isMarkable];
// Low-tech solution, for indentation, but it works...
NSCellImagePosition pos = isMarkable ? NSImageRight : NSImageLeft;
[cell setImagePosition:pos];
}
if ([cell isKindOfClass:[NSTextFieldCell class]]) {
NSColor *color = [NSColor textColor];
if (isSelected) {
color = [NSColor selectedTextColor];
}
else if (isMarkable) {
if ([[self model] isDeltaAtRow:row column:[column identifier]]) {
color = [NSColor orangeColor];
}
}
else {
color = [NSColor blueColor];
}
[(NSTextFieldCell *)cell setTextColor:color];
}
}
- (BOOL)tableViewHadDeletePressed:(NSTableView *)tableView
{
[[self model] removeSelected];
return YES;
}
- (BOOL)tableViewHadSpacePressed:(NSTableView *)tableView
{
[[self model] markSelected];
return YES;
}
/* Quicklook */
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel *)panel
{
return [[[self model] selectedRows] count];
}
- (id <QLPreviewItem>)previewPanel:(QLPreviewPanel *)panel previewItemAtIndex:(NSInteger)index
{
NSArray *selectedRows = [[self model] selectedRows];
NSInteger absIndex = n2i([selectedRows objectAtIndex:index]);
NSString *path = [[self model] pathAtIndex:absIndex];
return [[HSQLPreviewItem alloc] initWithUrl:[NSURL fileURLWithPath:path] title:path];
}
- (BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event
{
// redirect all key down events to the table view
if ([event type] == NSKeyDown) {
[[self view] keyDown:event];
return YES;
}
return NO;
}
/* Python --> Cocoa */
- (void)invalidateMarkings
{
[[self view] setNeedsDisplay:YES];
}
@end

76
cocoa/ResultWindow.h Normal file
View File

@@ -0,0 +1,76 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import <Quartz/Quartz.h>
#import "StatsLabel.h"
#import "ResultTable.h"
#import "HSTableView.h"
#import "PyDupeGuru.h"
@class AppDelegate;
@interface ResultWindow : NSWindowController
{
@protected
NSSegmentedControl *optionsSwitch;
NSToolbarItem *optionsToolbarItem;
HSTableView *matches;
NSTextField *stats;
NSSearchField *filterField;
AppDelegate *app;
PyDupeGuru *model;
ResultTable *table;
StatsLabel *statsLabel;
QLPreviewPanel* previewPanel;
}
@property (readwrite, retain) NSSegmentedControl *optionsSwitch;
@property (readwrite, retain) NSToolbarItem *optionsToolbarItem;
@property (readwrite, retain) HSTableView *matches;
@property (readwrite, retain) NSTextField *stats;
@property (readwrite, retain) NSSearchField *filterField;
- (id)initWithParentApp:(AppDelegate *)app;
/* Helpers */
- (void)fillColumnsMenu;
- (void)updateOptionSegments;
- (void)adjustUIToLocalization;
- (void)initResultColumns:(ResultTable *)aTable;
/* Actions */
- (void)changeOptions;
- (void)copyMarked;
- (void)trashMarked;
- (void)filter;
- (void)focusOnFilterField;
- (void)ignoreSelected;
- (void)invokeCustomCommand;
- (void)markAll;
- (void)markInvert;
- (void)markNone;
- (void)markSelected;
- (void)moveMarked;
- (void)openClicked;
- (void)openSelected;
- (void)removeMarked;
- (void)removeSelected;
- (void)renameSelected;
- (void)reprioritizeResults;
- (void)resetColumnsToDefault;
- (void)revealSelected;
- (void)saveResults;
- (void)switchSelected;
- (void)toggleColumn:(id)sender;
- (void)toggleDelta;
- (void)toggleDetailsPanel;
- (void)togglePowerMarker;
- (void)toggleQuicklookPanel;
@end

406
cocoa/ResultWindow.m Normal file
View File

@@ -0,0 +1,406 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "ResultWindow.h"
#import "ResultWindow_UI.h"
#import "Dialogs.h"
#import "ProgressController.h"
#import "Utils.h"
#import "AppDelegate.h"
#import "Consts.h"
#import "PrioritizeDialog.h"
@implementation ResultWindow
@synthesize optionsSwitch;
@synthesize optionsToolbarItem;
@synthesize matches;
@synthesize stats;
@synthesize filterField;
- (id)initWithParentApp:(AppDelegate *)aApp;
{
self = [super initWithWindow:nil];
app = aApp;
model = [app model];
[self setWindow:createResultWindow_UI(self)];
[[self window] setTitle:fmt(NSLocalizedString(@"%@ Results", @""), [model appName])];
/* Put a cute iTunes-like bottom bar */
[[self window] setContentBorderThickness:28 forEdge:NSMinYEdge];
table = [[ResultTable alloc] initWithPyRef:[model resultTable] view:matches];
statsLabel = [[StatsLabel alloc] initWithPyRef:[model statsLabel] view:stats];
[self initResultColumns:table];
[[table columns] setColumnsAsReadOnly];
[self fillColumnsMenu];
[matches setTarget:self];
[matches setDoubleAction:@selector(openClicked)];
[self adjustUIToLocalization];
return self;
}
- (void)dealloc
{
[table release];
[statsLabel release];
[super dealloc];
}
/* Helpers */
- (void)fillColumnsMenu
{
[[app columnsMenu] removeAllItems];
NSArray *menuItems = [[[table columns] model] menuItems];
for (NSInteger i=0; i < [menuItems count]; i++) {
NSArray *pair = [menuItems objectAtIndex:i];
NSString *display = [pair objectAtIndex:0];
BOOL marked = n2b([pair objectAtIndex:1]);
NSMenuItem *mi = [[app columnsMenu] addItemWithTitle:display action:@selector(toggleColumn:) keyEquivalent:@""];
[mi setTarget:self];
[mi setState:marked ? NSOnState : NSOffState];
[mi setTag:i];
}
[[app columnsMenu] addItem:[NSMenuItem separatorItem]];
NSMenuItem *mi = [[app columnsMenu] addItemWithTitle:NSLocalizedString(@"Reset to Default", @"")
action:@selector(resetColumnsToDefault) keyEquivalent:@""];
[mi setTarget:self];
}
- (void)updateOptionSegments
{
[optionsSwitch setSelected:[[app detailsPanel] isVisible] forSegment:0];
[optionsSwitch setSelected:[table powerMarkerMode] forSegment:1];
[optionsSwitch setSelected:[table deltaValuesMode] forSegment:2];
}
- (void)adjustUIToLocalization
{
NSString *lang = [[NSBundle preferredLocalizationsFromArray:[[NSBundle mainBundle] localizations]] objectAtIndex:0];
NSInteger seg1delta = 0;
NSInteger seg2delta = 0;
if ([lang isEqual:@"ru"]) {
seg2delta = 20;
}
else if ([lang isEqual:@"uk"]) {
seg2delta = 20;
}
else if ([lang isEqual:@"hy"]) {
seg1delta = 20;
}
if (seg1delta || seg2delta) {
[optionsSwitch setWidth:[optionsSwitch widthForSegment:0]+seg1delta forSegment:0];
[optionsSwitch setWidth:[optionsSwitch widthForSegment:1]+seg2delta forSegment:1];
NSSize s = [optionsToolbarItem maxSize];
s.width += seg1delta + seg2delta;
[optionsToolbarItem setMaxSize:s];
[optionsToolbarItem setMinSize:s];
}
}
- (void)initResultColumns:(ResultTable *)aTable
{
NSInteger appMode = [app getAppMode];
if (appMode == AppModePicture) {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 162, 16, 0, YES, nil},
{@"folder_path", 142, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"dimensions", 73, 16, 0, YES, nil},
{@"exif_timestamp", 120, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 58, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
else if (appMode == AppModeMusic) {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 235, 16, 0, YES, nil},
{@"folder_path", 120, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"duration", 50, 16, 0, YES, nil},
{@"bitrate", 50, 16, 0, YES, nil},
{@"samplerate", 60, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"title", 120, 16, 0, YES, nil},
{@"artist", 120, 16, 0, YES, nil},
{@"album", 120, 16, 0, YES, nil},
{@"genre", 80, 16, 0, YES, nil},
{@"year", 40, 16, 0, YES, nil},
{@"track", 40, 16, 0, YES, nil},
{@"comment", 120, 16, 0, YES, nil},
{@"percentage", 57, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [[aTable view] tableColumnWithIdentifier:@"duration"];
[[c dataCell] setAlignment:NSRightTextAlignment];
c = [[aTable view] tableColumnWithIdentifier:@"bitrate"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
else {
HSColumnDef defs[] = {
{@"marked", 26, 26, 26, YES, [NSButtonCell class]},
{@"name", 195, 16, 0, YES, nil},
{@"folder_path", 183, 16, 0, YES, nil},
{@"size", 63, 16, 0, YES, nil},
{@"extension", 40, 16, 0, YES, nil},
{@"mtime", 120, 16, 0, YES, nil},
{@"percentage", 60, 16, 0, YES, nil},
{@"words", 120, 16, 0, YES, nil},
{@"dupe_count", 80, 16, 0, YES, nil},
nil
};
[[aTable columns] initializeColumns:defs];
NSTableColumn *c = [[aTable view] tableColumnWithIdentifier:@"marked"];
[[c dataCell] setButtonType:NSSwitchButton];
[[c dataCell] setControlSize:NSSmallControlSize];
c = [[aTable view] tableColumnWithIdentifier:@"size"];
[[c dataCell] setAlignment:NSRightTextAlignment];
}
[[aTable columns] restoreColumns];
}
/* Actions */
- (void)changeOptions
{
NSInteger seg = [optionsSwitch selectedSegment];
if (seg == 0) {
[self toggleDetailsPanel];
}
else if (seg == 1) {
[self togglePowerMarker];
}
else if (seg == 2) {
[self toggleDelta];
}
}
- (void)copyMarked
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
[model setCopyMoveDestType:n2i([ud objectForKey:@"recreatePathType"])];
[model copyMarked];
}
- (void)trashMarked
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
[model deleteMarked];
}
- (void)filter
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setEscapeFilterRegexp:!n2b([ud objectForKey:@"useRegexpFilter"])];
[model applyFilter:[filterField stringValue]];
}
- (void)focusOnFilterField
{
[[self window] makeFirstResponder:filterField];
}
- (void)ignoreSelected
{
[model addSelectedToIgnoreList];
}
- (void)invokeCustomCommand
{
[model invokeCustomCommand];
}
- (void)markAll
{
[model markAll];
}
- (void)markInvert
{
[model markInvert];
}
- (void)markNone
{
[model markNone];
}
- (void)markSelected
{
[model toggleSelectedMark];
}
- (void)moveMarked
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[model setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
[model setCopyMoveDestType:n2i([ud objectForKey:@"recreatePathType"])];
[model moveMarked];
}
- (void)openClicked
{
if ([matches clickedRow] < 0) {
return;
}
[matches selectRowIndexes:[NSIndexSet indexSetWithIndex:[matches clickedRow]] byExtendingSelection:NO];
[model openSelected];
}
- (void)openSelected
{
[model openSelected];
}
- (void)removeMarked
{
[model removeMarked];
}
- (void)removeSelected
{
[model removeSelected];
}
- (void)renameSelected
{
NSInteger col = [matches columnWithIdentifier:@"name"];
NSInteger row = [matches selectedRow];
[matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES];
}
- (void)reprioritizeResults
{
PrioritizeDialog *dlg = [[PrioritizeDialog alloc] initWithApp:model];
NSInteger result = [NSApp runModalForWindow:[dlg window]];
if (result == NSRunStoppedResponse) {
[[dlg model] performReprioritization];
}
[dlg release];
[[self window] makeKeyAndOrderFront:nil];
}
- (void)resetColumnsToDefault
{
[[[table columns] model] resetToDefaults];
[self fillColumnsMenu];
}
- (void)revealSelected
{
[model revealSelected];
}
- (void)saveResults
{
NSSavePanel *sp = [NSSavePanel savePanel];
[sp setCanCreateDirectories:YES];
[sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[sp setTitle:NSLocalizedString(@"Select a file to save your results to", @"")];
if ([sp runModal] == NSOKButton) {
[model saveResultsAs:[[sp URL] path]];
[[app recentResults] addFile:[[sp URL] path]];
}
}
- (void)switchSelected
{
[model makeSelectedReference];
}
- (void)toggleColumn:(id)sender
{
NSMenuItem *mi = sender;
BOOL checked = [[[table columns] model] toggleMenuItem:[mi tag]];
[mi setState:checked ? NSOnState : NSOffState];
}
- (void)toggleDetailsPanel
{
[[app detailsPanel] toggleVisibility];
[self updateOptionSegments];
}
- (void)toggleDelta
{
[table setDeltaValuesMode:![table deltaValuesMode]];
[self updateOptionSegments];
}
- (void)togglePowerMarker
{
[table setPowerMarkerMode:![table powerMarkerMode]];
[self updateOptionSegments];
}
- (void)toggleQuicklookPanel
{
if ([QLPreviewPanel sharedPreviewPanelExists] && [[QLPreviewPanel sharedPreviewPanel] isVisible]) {
[[QLPreviewPanel sharedPreviewPanel] orderOut:nil];
}
else {
[[QLPreviewPanel sharedPreviewPanel] makeKeyAndOrderFront:nil];
}
}
/* Quicklook */
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel *)panel;
{
return YES;
}
- (void)beginPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document is now responsible of the preview panel
// It is allowed to set the delegate, data source and refresh panel.
previewPanel = [panel retain];
panel.delegate = table;
panel.dataSource = table;
}
- (void)endPreviewPanelControl:(QLPreviewPanel *)panel
{
// This document loses its responsisibility on the preview panel
// Until the next call to -beginPreviewPanelControl: it must not
// change the panel's delegate, data source or refresh it.
[previewPanel release];
previewPanel = nil;
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
{
return ![[ProgressController mainProgressController] isShown];
}
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
if ([item action] == @selector(markAll)) {
[item setTitle:NSLocalizedString(@"Mark All", @"")];
}
return ![[ProgressController mainProgressController] isShown];
}
@end

17
cocoa/StatsLabel.h Normal file
View File

@@ -0,0 +1,17 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import <Cocoa/Cocoa.h>
#import "HSGUIController.h"
#import "PyStatsLabel.h"
@interface StatsLabel : HSGUIController {}
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTextField *)aLabelView;
- (PyStatsLabel *)model;
- (NSTextField *)labelView;
@end

34
cocoa/StatsLabel.m Normal file
View File

@@ -0,0 +1,34 @@
/*
Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.gnu.org/licenses/gpl-3.0.html
*/
#import "StatsLabel.h"
#import "Utils.h"
@implementation StatsLabel
- (id)initWithPyRef:(PyObject *)aPyRef view:(NSTextField *)aLabelView
{
return [super initWithPyRef:aPyRef wrapperClass:[PyStatsLabel class]
callbackClassName:@"StatsLabelView" view:aLabelView];
}
- (PyStatsLabel *)model
{
return (PyStatsLabel *)model;
}
- (NSTextField *)labelView
{
return (NSTextField *)view;
}
/* Python --> Cocoa */
- (void)refresh
{
[[self labelView] setStringValue:[[self model] display]];
}
@end

17
cocoa/dg_cocoa.py Normal file
View File

@@ -0,0 +1,17 @@
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from hscommon.trans import install_gettext_trans_under_cocoa
install_gettext_trans_under_cocoa()
from cocoa.inter import PySelectableList, PyColumns, PyTable
from inter.all import *
from inter.app import PyDupeGuru
# When built under virtualenv, the dependency collector misses this module, so we have to force it
# to see the module.
import distutils.sysconfig

View File

@@ -0,0 +1,140 @@
"%@ Results" = "%@ Results";
"About dupeGuru" = "About dupeGuru";
"Action" = "Action";
"Actions" = "Actions";
"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 reference position. Read the help file for more information." = "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 reference position. Read the help file for more information.";
"Add New Folder..." = "Add New Folder...";
"Add Selected to Ignore List" = "Add Selected to Ignore List";
"Advanced" = "Advanced";
"After having deleted a duplicate, place a link targeting the reference file to replace the deleted file." = "After having deleted a duplicate, place a link targeting the reference file to replace the deleted file.";
"Album" = "Album";
"Application Mode:" = "Application Mode:";
"Artist" = "Artist";
"Attribute" = "Attribute";
"Automatically check for updates" = "Automatically check for updates";
"Basic" = "Basic";
"Bring All to Front" = "Bring All to Front";
"Can mix file kind" = "Can mix file kind";
"Cancel" = "Cancel";
"Check for update..." = "Check for update...";
"Clear" = "Clear";
"Clear Picture Cache" = "Clear Picture Cache";
"Close" = "Close";
"Close Window" = "Close Window";
"Columns" = "Columns";
"Copy" = "Copy";
"Copy and Move:" = "Copy and Move:";
"Copy Marked to..." = "Copy Marked to...";
"Custom command (arguments: %d for dupe, %r for ref):" = "Custom command (arguments: %d for dupe, %r for ref):";
"Cut" = "Cut";
"Debug mode (restart required)" = "Debug mode (restart required)";
"Deletion Options" = "Deletion Options";
"Delta" = "Delta";
"Details" = "Details";
"Details of Selected File" = "Details of Selected File";
"Details Panel" = "Details Panel";
"Directly delete files" = "Directly delete files";
"Directories" = "Directories";
"Do you really want to remove all your cached picture analysis?" = "Do you really want to remove all your cached picture analysis?";
"dupeGuru" = "dupeGuru";
"dupeGuru Help" = "dupeGuru Help";
"dupeGuru Preferences" = "dupeGuru Preferences";
"dupeGuru Results" = "dupeGuru Results";
"dupeGuru Website" = "dupeGuru Website";
"Dupes Only" = "Dupes Only";
"Edit" = "Edit";
"Excluded" = "Excluded";
"Export Results to CSV" = "Export Results to CSV";
"Export Results to XHTML" = "Export Results to XHTML";
"Fewer results" = "Fewer results";
"File" = "File";
"Filter" = "Filter";
"Filter hardness:" = "Filter hardness:";
"Filter Results..." = "Filter Results...";
"Folder Selection Window" = "Folder Selection Window";
"Font Size:" = "Font Size:";
"Genre" = "Genre";
"Help" = "Help";
"Hide dupeGuru" = "Hide dupeGuru";
"Hide Others" = "Hide Others";
"Ignore duplicates hardlinking to the same file" = "Ignore duplicates hardlinking to the same file";
"Ignore files smaller than:" = "Ignore files smaller than:";
"Ignore List" = "Ignore List";
"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." = "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.";
"Invert Marking" = "Invert Marking";
"Invoke Custom Command" = "Invoke Custom Command";
"KB" = "KB";
"Link deleted files" = "Link deleted files";
"Load from file..." = "Load from file...";
"Load Recent Results" = "Load Recent Results";
"Load Results" = "Load Results";
"Load Results..." = "Load Results...";
"Make Selected into Reference" = "Make Selected into Reference";
"Mark All" = "Mark All";
"Mark None" = "Mark None";
"Mark Selected" = "Mark Selected";
"Match pictures of different dimensions" = "Match pictures of different dimensions";
"Match similar words" = "Match similar words";
"Minimize" = "Minimize";
"Mode" = "Mode";
"More results" = "More results";
"Move Marked to..." = "Move Marked to...";
"Music" = "Music";
"Name" = "Name";
"Normal" = "Normal";
"Ok" = "Ok";
"Open Selected with Default Application" = "Open Selected with Default Application";
"Options" = "Options";
"Paste" = "Paste";
"Picture" = "Picture";
"Preferences..." = "Preferences...";
"Problems!" = "Problems!";
"Proceed" = "Proceed";
"Quick Look" = "Quick Look";
"Quit dupeGuru" = "Quit dupeGuru";
"Re-Prioritize duplicates" = "Re-Prioritize duplicates";
"Re-Prioritize Results..." = "Re-Prioritize Results...";
"Recreate absolute path" = "Recreate absolute path";
"Recreate relative path" = "Recreate relative path";
"Reference" = "Reference";
"Remove empty folders on delete or move" = "Remove empty folders on delete or move";
"Remove Marked from Results" = "Remove Marked from Results";
"Remove Selected" = "Remove Selected";
"Remove Selected from Results" = "Remove Selected from Results";
"Rename Selected" = "Rename Selected";
"Reset to Default" = "Reset to Default";
"Reset To Defaults" = "Reset To Defaults";
"Results Window" = "Results Window";
"Reveal" = "Reveal";
"Reveal Selected in Finder" = "Reveal Selected in Finder";
"Right in destination" = "Right in destination";
"Save Results..." = "Save Results...";
"Scan" = "Scan";
"Scan Type:" = "Scan Type:";
"Select a file to save your results to" = "Select a file to save your results to";
"Select a folder to add to the scanning list" = "Select a folder to add to the scanning list";
"Select a results file to load" = "Select a results file to load";
"Select All" = "Select All";
"Select folders to scan and press \"Scan\"." = "Select folders to scan and press \"Scan\".";
"Selected" = "Selected";
"Send Marked to Trash..." = "Send Marked to Trash...";
"Services" = "Services";
"Show All" = "Show All";
"Show Delta Values" = "Show Delta Values";
"Show Dupes Only" = "Show Dupes Only";
"Standard" = "Standard";
"Start Duplicate Scan" = "Start Duplicate Scan";
"State" = "State";
"Tags to scan:" = "Tags to scan:";
"The name '%@' already exists." = "The name '%@' already exists.";
"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 removed from your results." = "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 removed from your results.";
"Title" = "Title";
"Track" = "Track";
"Use regular expressions when filtering" = "Use regular expressions when filtering";
"Window" = "Window";
"Word weighting" = "Word weighting";
"Year" = "Year";
"You have unsaved results, do you really want to continue?" = "You have unsaved results, do you really want to continue?";
"You have unsaved results, do you really want to quit?" = "You have unsaved results, do you really want to quit?";
"Zoom" = "Zoom";

10
cocoa/inter/all.py Normal file
View File

@@ -0,0 +1,10 @@
from cocoa.inter import PyTextField, PyProgressWindow
from .deletion_options import PyDeletionOptions
from .details_panel import PyDetailsPanel
from .directory_outline import PyDirectoryOutline
from .prioritize_dialog import PyPrioritizeDialog
from .prioritize_list import PyPrioritizeList
from .problem_dialog import PyProblemDialog
from .ignore_list_dialog import PyIgnoreListDialog
from .result_table import PyResultTable
from .stats_label import PyStatsLabel

252
cocoa/inter/app.py Normal file
View File

@@ -0,0 +1,252 @@
import logging
from objp.util import pyref, dontwrap
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer
from cocoa.inter import PyBaseApp, BaseAppView
import core.pe.photo
from core.app import DupeGuru as DupeGuruBase, AppMode
from .directories import Directories, Bundle
from .photo import Photo
class DupeGuru(DupeGuruBase):
PICTURE_CACHE_TYPE = 'shelve'
def __init__(self, view):
DupeGuruBase.__init__(self, view)
self.directories = Directories()
def selected_dupe_path(self):
if not self.selected_dupes:
return None
return self.selected_dupes[0].path
def selected_dupe_ref_path(self):
if not self.selected_dupes:
return None
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
if ref is self.selected_dupes[0]: # we don't want the same pic to be displayed on both sides
return None
return ref.path
def _get_fileclasses(self):
result = DupeGuruBase._get_fileclasses(self)
if self.app_mode == AppMode.Standard:
result = [Bundle] + result
return result
class DupeGuruView(BaseAppView):
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
def createResultsWindow(self): pass
def showResultsWindow(self): pass
def showProblemDialog(self): pass
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
class PyDupeGuru(PyBaseApp):
@dontwrap
def __init__(self):
core.pe.photo.PLAT_SPECIFIC_PHOTO_CLASS = Photo
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
install_exception_hook('https://github.com/hsoft/dupeguru/issues')
install_cocoa_logger()
patch_threaded_job_performer()
self.model = DupeGuru(self)
#---Sub-proxies
def detailsPanel(self) -> pyref:
return self.model.details_panel
def directoryTree(self) -> pyref:
return self.model.directory_tree
def problemDialog(self) -> pyref:
return self.model.problem_dialog
def statsLabel(self) -> pyref:
return self.model.stats_label
def resultTable(self) -> pyref:
return self.model.result_table
def ignoreListDialog(self) -> pyref:
return self.model.ignore_list_dialog
def progressWindow(self) -> pyref:
return self.model.progress_window
def deletionOptions(self) -> pyref:
return self.model.deletion_options
#---Directories
def addDirectory_(self, directory: str):
self.model.add_directory(directory)
#---Results
def doScan(self):
self.model.start_scanning()
def exportToXHTML(self):
self.model.export_to_xhtml()
def exportToCSV(self):
self.model.export_to_csv()
def loadSession(self):
self.model.load()
def loadResultsFrom_(self, filename: str):
self.model.load_from(filename)
def markAll(self):
self.model.mark_all()
def markNone(self):
self.model.mark_none()
def markInvert(self):
self.model.mark_invert()
def purgeIgnoreList(self):
self.model.purge_ignore_list()
def toggleSelectedMark(self):
self.model.toggle_selected_mark_state()
def saveSession(self):
self.model.save()
def saveResultsAs_(self, filename: str):
self.model.save_as(filename)
#---Actions
def addSelectedToIgnoreList(self):
self.model.add_selected_to_ignore_list()
def deleteMarked(self):
self.model.delete_marked()
def applyFilter_(self, filter: str):
self.model.apply_filter(filter)
def makeSelectedReference(self):
self.model.make_selected_reference()
def copyMarked(self):
self.model.copy_or_move_marked(copy=True)
def moveMarked(self):
self.model.copy_or_move_marked(copy=False)
def openSelected(self):
self.model.open_selected()
def removeMarked(self):
self.model.remove_marked()
def removeSelected(self):
self.model.remove_selected()
def revealSelected(self):
self.model.reveal_selected()
def invokeCustomCommand(self):
self.model.invoke_custom_command()
def showIgnoreList(self):
self.model.ignore_list_dialog.show()
def clearPictureCache(self):
self.model.clear_picture_cache()
#---Information
def getScanOptions(self) -> list:
return [o.label for o in self.model.SCANNER_CLASS.get_scan_options()]
def resultsAreModified(self) -> bool:
return self.model.results.is_modified
def getSelectedDupePath(self) -> str:
return str(self.model.selected_dupe_path())
def getSelectedDupeRefPath(self) -> str:
return str(self.model.selected_dupe_ref_path())
#---Properties
def getAppMode(self) -> int:
return self.model.app_mode
def setAppMode_(self, app_mode: int):
self.model.app_mode = app_mode
def setScanType_(self, scan_type_index: int):
scan_options = self.model.SCANNER_CLASS.get_scan_options()
try:
so = scan_options[scan_type_index]
self.model.options['scan_type'] = so.scan_type
except IndexError:
pass
def setMinMatchPercentage_(self, percentage: int):
self.model.options['min_match_percentage'] = int(percentage)
def setWordWeighting_(self, words_are_weighted: bool):
self.model.options['word_weighting'] = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.options['match_similar_words'] = match_similar_words
def setSizeThreshold_(self, size_threshold: int):
self.model.options['size_threshold'] = size_threshold
def enable_scanForTag_(self, enable: bool, scan_tag: str):
if 'scanned_tags' not in self.model.options:
self.model.options['scanned_tags'] = set()
if enable:
self.model.options['scanned_tags'].add(scan_tag)
else:
self.model.options['scanned_tags'].discard(scan_tag)
def setMatchScaled_(self, match_scaled: bool):
self.model.options['match_scaled'] = match_scaled
def setMixFileKind_(self, mix_file_kind: bool):
self.model.options['mix_file_kind'] = mix_file_kind
def setEscapeFilterRegexp_(self, escape_filter_regexp: bool):
self.model.options['escape_filter_regexp'] = escape_filter_regexp
def setRemoveEmptyFolders_(self, remove_empty_folders: bool):
self.model.options['clean_empty_dirs'] = remove_empty_folders
def setIgnoreHardlinkMatches_(self, ignore_hardlink_matches: bool):
self.model.options['ignore_hardlink_matches'] = ignore_hardlink_matches
def setCopyMoveDestType_(self, copymove_dest_type: int):
self.model.options['copymove_dest_type'] = copymove_dest_type
#--- model --> view
@dontwrap
def ask_yes_no(self, prompt):
return self.callback.askYesNoWithPrompt_(prompt)
@dontwrap
def create_results_window(self):
self.callback.createResultsWindow()
@dontwrap
def show_results_window(self):
self.callback.showResultsWindow()
@dontwrap
def show_problem_dialog(self):
self.callback.showProblemDialog()
@dontwrap
def select_dest_folder(self, prompt):
return self.callback.selectDestFolderWithPrompt_(prompt)
@dontwrap
def select_dest_file(self, prompt, extension):
return self.callback.selectDestFileWithPrompt_extension_(prompt, extension)

View File

@@ -0,0 +1,37 @@
# Created On: 2012-05-30
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from objp.util import dontwrap
from cocoa.inter import PyGUIObject, GUIObjectView
class DeletionOptionsView(GUIObjectView):
def updateMsg_(self, msg: str): pass
def show(self) -> bool: pass
def setHardlinkOptionEnabled_(self, enabled: bool): pass
class PyDeletionOptions(PyGUIObject):
def setLinkDeleted_(self, link_deleted: bool):
self.model.link_deleted = link_deleted
def setUseHardlinks_(self, use_hardlinks: bool):
self.model.use_hardlinks = use_hardlinks
def setDirect_(self, direct: bool):
self.model.direct = direct
#--- model --> view
@dontwrap
def update_msg(self, msg):
self.callback.updateMsg_(msg)
@dontwrap
def show(self):
return self.callback.show()
@dontwrap
def set_hardlink_option_enabled(self, enabled):
self.callback.setHardlinkOptionEnabled_(enabled)

View File

@@ -0,0 +1,11 @@
from cocoa.inter import PyGUIObject, GUIObjectView
class DetailsPanelView(GUIObjectView):
pass
class PyDetailsPanel(PyGUIObject):
def numberOfRows(self) -> int:
return self.model.row_count()
def valueForColumn_row_(self, column: str, row: int) -> object:
return self.model.row(row)[int(column)]

View File

@@ -0,0 +1,53 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from cocoa import proxy
from hscommon.path import Path, pathify
from core.se import fs
from core.directories import Directories as DirectoriesBase, DirectoryState
def is_bundle(str_path):
uti = proxy.getUTI_(str_path)
if uti is None:
logging.warning('There was an error trying to detect the UTI of %s', str_path)
return proxy.type_conformsToType_(uti, 'com.apple.bundle') or proxy.type_conformsToType_(uti, 'com.apple.package')
class Bundle(fs.Folder):
@classmethod
@pathify
def can_handle(cls, path: Path):
return not path.islink() and path.isdir() and is_bundle(str(path))
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = list(map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev']))
HOME_PATH_TO_EXCLUDE = [Path('Library')]
def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path)
if result is not None:
return result
if path in self.ROOT_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
if path[:2] == Path('/Users') and path[3:] in self.HOME_PATH_TO_EXCLUDE:
return DirectoryState.Excluded
def _get_folders(self, from_folder, j):
# We don't want to scan bundle's subfolder even in Folders mode. Bundle's integrity has to
# stay intact.
if is_bundle(str(from_folder.path)):
# just yield the current folder and bail
state = self.get_state(from_folder.path)
if state != DirectoryState.Excluded:
from_folder.is_ref = state == DirectoryState.Reference
yield from_folder
return
else:
yield from DirectoriesBase._get_folders(self, from_folder, j)
@staticmethod
def get_subfolders(path):
result = DirectoriesBase.get_subfolders(path)
return [p for p in result if not is_bundle(str(p))]

View File

@@ -0,0 +1,21 @@
from objp.util import dontwrap
from cocoa.inter import PyOutline, GUIObjectView
class DirectoryOutlineView(GUIObjectView):
pass
class PyDirectoryOutline(PyOutline):
def addDirectory_(self, path: str):
self.model.add_directory(path)
def removeSelectedDirectory(self):
self.model.remove_selected()
def selectAll(self):
self.model.select_all()
# python --> cocoa
@dontwrap
def refresh_states(self):
# Under cocoa, both refresh() and refresh_states() do the same thing.
self.callback.refresh()

View File

@@ -0,0 +1,21 @@
from objp.util import pyref, dontwrap
from cocoa.inter import PyGUIObject, GUIObjectView
class IgnoreListDialogView(GUIObjectView):
def show(self): pass
class PyIgnoreListDialog(PyGUIObject):
def ignoreListTable(self) -> pyref:
return self.model.ignore_list_table
def removeSelected(self):
self.model.remove_selected()
def clear(self):
self.model.clear()
#--- model --> view
@dontwrap
def show(self):
self.callback.show()

35
cocoa/inter/photo.py Normal file
View File

@@ -0,0 +1,35 @@
# Copyright 2016 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.gnu.org/licenses/gpl-3.0.html
from cocoa import proxy
from core.pe import _block_osx
from core.pe.photo import Photo as PhotoBase
class Photo(PhotoBase):
HANDLED_EXTS = PhotoBase.HANDLED_EXTS.copy()
HANDLED_EXTS.update({'psd', 'nef', 'cr2', 'orf'})
def _plat_get_dimensions(self):
return _block_osx.get_image_size(str(self.path))
def _plat_get_blocks(self, block_count_per_side, orientation):
try:
blocks = _block_osx.getblocks(str(self.path), block_count_per_side, orientation)
except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (str(self.path), str(e)))
if not blocks:
raise IOError('The picture %s could not be read' % str(self.path))
return blocks
def _get_exif_timestamp(self):
exifdata = proxy.readExifData_(str(self.path))
if exifdata:
try:
return exifdata['{Exif}']['DateTimeOriginal']
except KeyError:
return ''
else:
return ''

View File

@@ -0,0 +1,29 @@
from objp.util import pyref
from cocoa.inter import PyGUIObject, GUIObjectView
from core.gui.prioritize_dialog import PrioritizeDialog
class PrioritizeDialogView(GUIObjectView):
pass
class PyPrioritizeDialog(PyGUIObject):
def __init__(self, app: pyref):
model = PrioritizeDialog(app.model)
PyGUIObject.__init__(self, model)
def categoryList(self) -> pyref:
return self.model.category_list
def criteriaList(self) -> pyref:
return self.model.criteria_list
def prioritizationList(self) -> pyref:
return self.model.prioritization_list
def addSelected(self):
self.model.add_selected()
def removeSelected(self):
self.model.remove_selected()
def performReprioritization(self):
self.model.perform_reprioritization()

View File

@@ -0,0 +1,8 @@
from cocoa.inter import PySelectableList, SelectableListView
class PrioritizeListView(SelectableListView):
pass
class PyPrioritizeList(PySelectableList):
def moveIndexes_toIndex_(self, indexes: list, dest_index: int):
self.model.move_indexes(indexes, dest_index)

Some files were not shown because too many files have changed in this diff Show More