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

Compare commits

...

1053 Commits

Author SHA1 Message Date
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
743 changed files with 52576 additions and 33817 deletions

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
.DS_Store
*.pyc
*.so
*.mo
*.pyd
*.waf*
.lock-waf*
build
dist
install
installer_tmp-cache
env
cocoa/autogen
/run.py
/conf.json
/cocoa/*/Info.plist
/cocoa/*/build
/qt/base/*_rc.py
/help/*/conf.py
/help/*/changelog.rst

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

View File

@@ -1,4 +1,4 @@
Copyright 2009 Hardcoded Software Inc. (http://www.hardcoded.net)
Copyright 2013 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:
@@ -6,6 +6,5 @@ Redistribution and use in source and binary forms, with or without modification,
* 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.

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
# dupeGuru
This package contains the source for dupeGuru. Its documentation is
[available online][documentation]. Here's how this source tree is organised:
* core: Contains the core logic code for dupeGuru. It's Python code.
* core_*: Edition-specific-cross-toolkit code written in Python.
* 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.
* debian: Skeleton files required to create a .deb package
* 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 subtrees:
* 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
## The very, very, very easy way
If you're on Linux or Mac, there's a bootstrap script that will make building very, 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. You can then ignore the rest of the build documentation.
## 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.2+][python] and [setuptools][setuptools]
* Mac OS X: The last XCode to have the 10.6 SDK included.
* Windows: Visual Studio 2008, [PyQt 4.7+][pyqt], [cx_Freeze][cxfreeze] and
[Advanced Installer][advinst] (you only need the last two if you want to create an installer)
On Ubuntu, the apt-get command to install all pre-requisites is:
$ apt-get install python3-dev python3-pyqt4 pyqt4-dev-tools python3-setuptools
## Virtualenv setup
First, you need `pip` and `virtualenv` in your system Python install:
$ sudo easy_install pip
$ sudo pip install virtualenv
Then, in dupeGuru's source folder, create a virtual environment and activate it:
$ virtualenv --system-site-packages env
$ source env/bin/activate
Then, you can install pip requirements in your virtualenv:
$ pip install -r requirements-[osx|win].txt
([osx|win] depends, of course, on your platform. On other platforms, just use requirements.txt).
## Alternative: pyvenv
If you're on Python 3.3+, you can use the built-in `pyvenv` instead of `virtualenv`. `pyvenv` is
pretty much the same thing as `virtualenv`, except that it doesn't install setuptools and pip, so it
has to be installed manually:
$ pyvenv --system-site-packages env
$ source env/bin/activate
$ wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python
$ easy_install pip
## Actual building and running
With your virtualenv activated, you can build and run dupeGuru with these commands:
$ python configure.py
$ python build.py
$ python run.py
You can also package dupeGuru into an installable package with:
$ python package.py
[documentation]: http://www.hardcoded.net/dupeguru/help/en/
[python]: http://www.python.org/
[setuptools]: https://pypi.python.org/pypi/setuptools
[pyqt]: http://www.riverbankcomputing.com
[cxfreeze]: http://cx-freeze.sourceforge.net/
[advinst]: http://www.advancedinstaller.com

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

26
bootstrap.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
command -v python3 -m venv >/dev/null 2>&1 || { echo >&2 "Python 3.3 required. Install it and try again. Aborting"; exit 1; }
if [ ! -d "env" ]; then
echo "No virtualenv. Creating one"
command -v curl >/dev/null 2>&1 || { echo >&2 "curl required. Install it and try again. Aborting"; exit 1; }
python3 -m venv --system-site-packages env
source env/bin/activate
curl https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py | python
easy_install pip
else
echo "There's already an env. Activating it"
source env/bin/activate
fi
echo "Installing pip requirements"
if [ "$(uname)" == "Darwin" ]; then
pip install -r requirements-osx.txt
else
python3 -c "import PyQt4" >/dev/null 2>&1 || { echo >&2 "PyQt 4.8+ required. Install it and try again. Aborting"; exit 1; }
pip install -r requirements.txt
fi
echo "Bootstrapping complete! You can now configure, build and run dupeGuru with:"
echo ". env/bin/activate && python configure.py && python build.py && python run.py"

367
build.py Normal file
View File

@@ -0,0 +1,367 @@
# Created By: Virgil Dupras
# Created On: 2009-12-30
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import sys
import os
import os.path as op
from optparse import OptionParser
import shutil
import json
import importlib
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_sysconfig_files_for_embed, copy_all, OSXAppStructure,
build_cocoalib_xibless, fix_qt_resource_file, build_cocoa_ext, copy_embeddable_python_dylib,
collect_stdlib_dependencies, copy)
from hscommon import loc
from hscommon.plat import ISOSX, ISLINUX
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('--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_compile_command(edition):
return '{0} waf configure --edition {1} && {0} waf'.format(sys.executable, edition)
def cocoa_app(edition):
app_path = {
'se': 'build/dupeGuru.app',
'me': 'build/dupeGuru ME.app',
'pe': 'build/dupeGuru PE.app',
}[edition]
return OSXAppStructure(app_path)
def build_xibless(edition, 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'),
('preferences_panel.py', 'PreferencesPanel_UI'),
]
for srcname, dstname in FNPAIRS:
xibless.generate(op.join('cocoa', 'base', 'ui', srcname), op.join(dest, dstname),
localizationTable='Localizable', args={'edition': edition})
if edition == 'pe':
xibless.generate('cocoa/pe/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable')
else:
xibless.generate('cocoa/base/ui/details_panel.py', op.join(dest, 'DetailsPanel_UI'), localizationTable='Localizable')
def build_cocoa(edition, dev):
print("Creating OS X app structure")
ed = lambda s: s.format(edition)
app = cocoa_app(edition)
app_version = get_module_version(ed('core_{}'))
cocoa_project_path = ed('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', edition)
print("Building xibless UIs")
build_cocoalib_xibless()
build_xibless(edition)
print("Building Python extensions")
build_cocoa_proxy_module()
build_cocoa_bridging_interfaces(edition)
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')
appscript_pkgs = ['appscript', 'aem', 'mactypes', 'osax']
specific_packages = {
'se': ['core_se'],
'me': ['core_me'] + appscript_pkgs + ['hsaudiotag'],
'pe': ['core_pe'] + appscript_pkgs,
}[edition]
tocopy = ['core', 'hscommon', 'cocoa/inter', 'cocoalib/cocoa', 'jobprogress', 'objp',
'send2trash'] + specific_packages
copy_packages(tocopy, pydep_folder, create_links=dev)
sys.path.insert(0, 'build')
extra_deps = None
if edition == 'pe':
# 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'))
copy_sysconfig_files_for_embed(pydep_folder)
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(cocoa_compile_command(edition))
os.chdir('..')
app.copy_executable('cocoa/build/dupeGuru')
print("Copying resources and frameworks")
image_path = ed('cocoa/{}/dupeguru.icns')
resources = [image_path, 'cocoa/base/dsa_pub.pem', 'build/dg_cocoa.py', 'build/help']
app.copy_resources(*resources, use_symlinks=dev)
app.copy_frameworks('build/Python', 'cocoalib/Sparkle.framework')
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(edition, dev, conf):
print("Building localizations")
build_localizations('qt', edition)
print("Building Qt stuff")
print_and_do("pyrcc4 -py3 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py')))
fix_qt_resource_file(op.join('qt', 'base', 'dg_rc.py'))
print("Creating the run.py file")
filereplace(op.join('qt', 'run_template.py'), 'run.py', edition=edition)
def build_help(edition):
print("Generating Help")
current_path = op.abspath('.')
help_basepath = op.join(current_path, 'help', 'en')
help_destpath = op.join(current_path, 'build', 'help'.format(edition))
changelog_path = op.join(current_path, 'help', 'changelog_{}'.format(edition))
tixurl = "https://github.com/hsoft/dupeguru/issues/{}"
appname = {'se': 'dupeGuru', 'me': 'dupeGuru Music Edition', 'pe': 'dupeGuru Picture Edition'}[edition]
homepage = 'http://www.hardcoded.net/dupeguru{}/'.format('_' + edition if edition != 'se' else '')
confrepl = {'edition': edition, 'appname': appname, 'homepage': homepage, '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_base_localizations():
loc.compile_all_po('locale')
loc.compile_all_po(op.join('hscommon', 'locale'))
loc.merge_locale_dir(op.join('hscommon', 'locale'), 'locale')
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, edition):
build_base_localizations()
if ui == 'cocoa':
app = cocoa_app(edition)
loc.build_cocoa_localizations(app, en_stringsfile=op.join('cocoa', 'base', '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'))
if ui == 'qt' and not ISLINUX:
print("Copying qt_*.qm files into the 'locale' folder")
from PyQt4.QtCore import QLibraryInfo
trfolder = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
for lang in loc.get_langs('locale'):
qmname = 'qt_%s.qm' % lang
src = op.join(trfolder, qmname)
if op.exists(src):
copy(src, op.join('build', 'locale', qmname))
def build_updatepot():
if ISOSX:
print("Updating Cocoa strings file.")
# We need to have strings from *all* editions in here, so we'll call xibless for all editions
# in dummy subfolders.
build_cocoalib_xibless('cocoalib/autogen')
loc.generate_cocoa_strings_from_code('cocoalib', 'cocoalib/en.lproj')
for edition in ('se', 'me', 'pe'):
build_xibless(edition, op.join('cocoa', 'autogen', edition))
loc.generate_cocoa_strings_from_code('cocoa', 'cocoa/base/en.lproj')
print("Building .pot files from source files")
print("Building core.pot")
all_cores = ['core', 'core_se', 'core_me', 'core_pe']
loc.generate_pot(all_cores, op.join('locale', 'core.pot'), ['tr'])
print("Building columns.pot")
loc.generate_pot(all_cores, 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 hscommon.pot")
loc.generate_pot(['hscommon'], op.join('hscommon', 'locale', 'hscommon.pot'), ['tr'])
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', 'base', '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('hscommon', '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('hscommon', '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(edition):
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,
PyFairware, 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 PyDupeGuruBase, DupeGuruView
appmod = importlib.import_module('inter.app_{}'.format(edition))
allclasses = [PyGUIObject, PyColumns, PyOutline, PySelectableList, PyTable, PyBaseApp, PyFairware,
PyDetailsPanel, PyDirectoryOutline, PyPrioritizeDialog, PyPrioritizeList, PyProblemDialog,
PyIgnoreListDialog, PyDeletionOptions, PyResultTable, PyStatsLabel, PyDupeGuruBase,
PyTextField, PyProgressWindow, appmod.PyDupeGuru]
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*', 'core_pe')
move_all('_cache*', 'core_pe')
def build_normal(edition, ui, dev, conf):
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
add_to_pythonpath('.')
build_help(edition)
print("Building dupeGuru")
if edition == 'pe':
build_pe_modules(ui)
if ui == 'cocoa':
build_cocoa(edition, dev)
elif ui == 'qt':
build_qt(edition, dev, conf)
def main():
options = parse_args()
conf = json.load(open('conf.json'))
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
if dev:
print("Building in Dev mode")
if options.clean:
if op.exists('build'):
shutil.rmtree('build')
if not op.exists('build'):
os.mkdir('build')
if options.doc:
build_help(edition)
elif options.loc:
build_localizations(ui, edition)
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(edition)
elif options.cocoa_compile:
os.chdir('cocoa')
print_and_do(cocoa_compile_command(edition))
os.chdir('..')
cocoa_app(edition).copy_executable('cocoa/build/dupeGuru')
elif options.xibless:
build_cocoalib_xibless()
build_xibless(edition)
else:
build_normal(edition, ui, dev, conf)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,78 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import <Cocoa/Cocoa.h>
#import <Sparkle/SUUpdater.h>
#import "PyDupeGuru.h"
#import "ResultWindow.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "IgnoreListDialog.h"
#import "HSFairwareAboutBox.h"
#import "HSRecentFiles.h"
#import "HSProgressWindow.h"
@interface AppDelegateBase : NSObject
{
NSMenu *recentResultsMenu;
NSMenu *columnsMenu;
SUUpdater *updater;
PyDupeGuru *model;
ResultWindowBase *_resultWindow;
DirectoryPanel *_directoryPanel;
DetailsPanel *_detailsPanel;
IgnoreListDialog *_ignoreListDialog;
HSProgressWindow *_progressWindow;
NSWindowController *_preferencesPanel;
HSFairwareAboutBox *_aboutBox;
HSRecentFiles *_recentResults;
}
@property (readwrite, retain) NSMenu *recentResultsMenu;
@property (readwrite, retain) NSMenu *columnsMenu;
@property (readwrite, retain) SUUpdater *updater;
/* Virtual */
+ (NSDictionary *)defaultPreferences;
- (PyDupeGuru *)model;
- (ResultWindowBase *)createResultWindow;
- (DirectoryPanel *)createDirectoryPanel;
- (DetailsPanel *)createDetailsPanel;
- (NSString *)homepageURL;
/* Public */
- (void)finalizeInit;
- (ResultWindowBase *)resultWindow;
- (DirectoryPanel *)directoryPanel;
- (DetailsPanel *)detailsPanel;
- (HSRecentFiles *)recentResults;
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (void)applicationWillBecomeActive:(NSNotification *)aNotification;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender;
- (void)applicationWillTerminate:(NSNotification *)aNotification;
- (void)recentFileClicked:(NSString *)path;
/* Actions */
- (void)loadResults;
- (void)openWebsite;
- (void)openHelp;
- (void)showAboutBox;
- (void)showDirectoryWindow;
- (void)showPreferencesPanel;
- (void)showResultWindow;
- (void)showIgnoreList;
- (void)startScanning;
/* model --> view */
- (void)showMessage:(NSString *)msg;
- (void)setupAsRegistered;
- (void)showDemoNagWithPrompt:(NSString *)prompt;
@end

View File

@@ -0,0 +1,304 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import "AppDelegateBase.h"
#import "ProgressController.h"
#import "HSFairwareReminder.h"
#import "HSPyUtil.h"
#import "Consts.h"
#import "Dialogs.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "PreferencesPanel_UI.h"
@implementation AppDelegateBase
@synthesize recentResultsMenu;
@synthesize columnsMenu;
@synthesize updater;
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionary];
[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)];
[self setUpdater:[SUUpdater sharedUpdater]];
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];
/* Because the pref pane is lazily loaded, we have to manually do the update check if the
preference is set.
*/
if ([ud boolForKey:@"SUEnableAutomaticChecks"]) {
[[SUUpdater sharedUpdater] checkForUpdatesInBackground];
}
_recentResults = [[HSRecentFiles alloc] initWithName:@"recentResults" menu:recentResultsMenu];
[_recentResults setDelegate:self];
_resultWindow = [self createResultWindow];
_directoryPanel = [self createDirectoryPanel];
_detailsPanel = [self createDetailsPanel];
_ignoreListDialog = [[IgnoreListDialog alloc] initWithPyRef:[model ignoreListDialog]];
_progressWindow = [[HSProgressWindow alloc] initWithPyRef:[[self model] progressWindow] view:nil];
[_progressWindow setParentWindow:[_resultWindow window]];
_aboutBox = nil; // Lazily loaded
_preferencesPanel = nil; // Lazily loaded
[[[self directoryPanel] window] makeKeyAndOrderFront:self];
}
/* Virtual */
- (PyDupeGuru *)model
{
return model;
}
- (ResultWindowBase *)createResultWindow
{
return nil; // must be overriden by all editions
}
- (DirectoryPanel *)createDirectoryPanel
{
return [[DirectoryPanel alloc] initWithParentApp:self];
}
- (DetailsPanel *)createDetailsPanel
{
return [[DetailsPanel alloc] initWithPyRef:[model detailsPanel]];
}
- (NSString *)homepageURL
{
return @""; // must be overriden by all editions
}
/* Public */
- (ResultWindowBase *)resultWindow
{
return _resultWindow;
}
- (DirectoryPanel *)directoryPanel
{
return _directoryPanel;
}
- (DetailsPanel *)detailsPanel
{
return _detailsPanel;
}
- (HSRecentFiles *)recentResults
{
return _recentResults;
}
/* Actions */
- (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 filenames] objectAtIndex:0];
[model loadResultsFrom:filename];
[[self recentResults] addFile:filename];
}
}
- (void)openWebsite
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[self homepageURL]]];
}
- (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 = [[HSFairwareAboutBox alloc] initWithApp:model];
}
[[_aboutBox window] makeKeyAndOrderFront:nil];
}
- (void)showDirectoryWindow
{
[[[self directoryPanel] window] makeKeyAndOrderFront:nil];
}
- (void)showPreferencesPanel
{
if (_preferencesPanel == nil) {
_preferencesPanel = [[NSWindowController alloc] initWithWindow:createPreferencesPanel_UI(nil)];
}
[_preferencesPanel showWindow:nil];
}
- (void)showResultWindow
{
[[[self resultWindow] window] makeKeyAndOrderFront:nil];
}
- (void)showIgnoreList
{
[model showIgnoreList];
}
- (void)startScanning
{
[[self resultWindow] startDuplicateScan];
}
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[model initialRegistrationSetup];
[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)showProblemDialog
{
[[self resultWindow] showProblemDialog];
}
- (void)setupAsRegistered
{
// Nothing to do.
}
- (void)showDemoNagWithPrompt:(NSString *)prompt
{
[HSFairwareReminder showDemoNagWithApp:[self model] prompt:prompt];
}
- (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 filenames] objectAtIndex:0];
}
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 filename];
}
else {
return nil;
}
}
@end

19
cocoa/base/Consts.h Normal file
View File

@@ -0,0 +1,19 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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"

View File

@@ -0,0 +1,33 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

View File

@@ -0,0 +1,67 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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;
}
@end

View File

@@ -0,0 +1,31 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import <Cocoa/Cocoa.h>
#import <Python.h>
#import "PyDetailsPanel.h"
@interface DetailsPanelBase : 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

View File

@@ -0,0 +1,80 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import "DetailsPanelBase.h"
#import "HSPyUtil.h"
@implementation DetailsPanelBase
@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 nil; // Virtual
}
- (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,19 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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;
@end;

View File

@@ -0,0 +1,81 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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;
}
/* 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

View File

@@ -0,0 +1,49 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import <Cocoa/Cocoa.h>
#import "HSOutlineView.h"
#import "HSRecentFiles.h"
#import "DirectoryOutline.h"
#import "PyDupeGuru.h"
@class AppDelegateBase;
@interface DirectoryPanel : NSWindowController <NSOpenSavePanelDelegate>
{
AppDelegateBase *_app;
PyDupeGuru *model;
HSRecentFiles *_recentDirectories;
DirectoryOutline *outline;
BOOL _alwaysShowPopUp;
NSPopUpButton *addButtonPopUp;
NSPopUpButton *loadRecentButtonPopUp;
HSOutlineView *outlineView;
NSButton *removeButton;
NSButton *loadResultsButton;
}
@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:(AppDelegateBase *)aParentApp;
- (void)fillPopUpMenu; // Virtual
- (void)adjustUIToLocalization;
- (void)askForDirectory;
- (void)popupAddDirectoryMenu:(id)sender;
- (void)popupLoadRecentMenu:(id)sender;
- (void)removeSelectedDirectory;
- (void)addDirectory:(NSString *)directory;
- (void)refreshRemoveButtonText;
@end

189
cocoa/base/DirectoryPanel.m Normal file
View File

@@ -0,0 +1,189 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import "DirectoryPanel.h"
#import "DirectoryPanel_UI.h"
#import "Dialogs.h"
#import "Utils.h"
#import "AppDelegate.h"
@implementation DirectoryPanel
@synthesize addButtonPopUp;
@synthesize loadRecentButtonPopUp;
@synthesize outlineView;
@synthesize removeButton;
@synthesize loadResultsButton;
- (id)initWithParentApp:(AppDelegateBase *)aParentApp
{
self = [super initWithWindow:nil];
[self setWindow:createDirectoryPanel_UI(self)];
_app = aParentApp;
model = [_app model];
[[self window] setTitle:[model appName]];
_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)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 (NSString *directory in [op filenames]) {
[self addDirectory:directory];
}
}
}
- (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];
}
/* 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]];
}
}
/* 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];
}
/* 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

View File

@@ -0,0 +1,25 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

View File

@@ -0,0 +1,51 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

@@ -0,0 +1,37 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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;

View File

@@ -0,0 +1,56 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

View File

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

View File

@@ -0,0 +1,58 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

View File

@@ -0,0 +1,26 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

View File

@@ -0,0 +1,44 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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/base/ResultTable.h Normal file
View File

@@ -0,0 +1,23 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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/base/ResultTable.m Normal file
View File

@@ -0,0 +1,180 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

View File

@@ -0,0 +1,85 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import <Cocoa/Cocoa.h>
#import <Quartz/Quartz.h>
#import "StatsLabel.h"
#import "ResultTable.h"
#import "ProblemDialog.h"
#import "DeletionOptions.h"
#import "HSTableView.h"
#import "PyDupeGuru.h"
@class AppDelegateBase;
@interface ResultWindowBase : NSWindowController
{
@protected
NSSegmentedControl *optionsSwitch;
NSToolbarItem *optionsToolbarItem;
HSTableView *matches;
NSTextField *stats;
NSSearchField *filterField;
AppDelegateBase *app;
PyDupeGuru *model;
ResultTable *table;
StatsLabel *statsLabel;
ProblemDialog *problemDialog;
DeletionOptions *deletionOptions;
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:(AppDelegateBase *)app;
/* Virtual */
- (void)initResultColumns;
- (void)setScanOptions;
/* Helpers */
- (void)fillColumnsMenu;
- (void)updateOptionSegments;
- (void)showProblemDialog;
- (void)adjustUIToLocalization;
/* 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)startDuplicateScan;
- (void)switchSelected;
- (void)toggleColumn:(id)sender;
- (void)toggleDelta;
- (void)toggleDetailsPanel;
- (void)togglePowerMarker;
- (void)toggleQuicklookPanel;
@end

View File

@@ -0,0 +1,349 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import "ResultWindowBase.h"
#import "ResultWindow_UI.h"
#import "Dialogs.h"
#import "ProgressController.h"
#import "Utils.h"
#import "AppDelegate.h"
#import "Consts.h"
#import "PrioritizeDialog.h"
@implementation ResultWindowBase
@synthesize optionsSwitch;
@synthesize optionsToolbarItem;
@synthesize matches;
@synthesize stats;
@synthesize filterField;
- (id)initWithParentApp:(AppDelegateBase *)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];
problemDialog = [[ProblemDialog alloc] initWithPyRef:[model problemDialog]];
deletionOptions = [[DeletionOptions alloc] initWithPyRef:[model deletionOptions]];
[self initResultColumns];
[[table columns] setColumnsAsReadOnly];
[self fillColumnsMenu];
[matches setTarget:self];
[matches setDoubleAction:@selector(openClicked)];
[self adjustUIToLocalization];
return self;
}
- (void)dealloc
{
[table release];
[statsLabel release];
[problemDialog release];
[super dealloc];
}
/* Virtual */
- (void)initResultColumns
{
}
- (void)setScanOptions
{
}
/* 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)showProblemDialog
{
[problemDialog showWindow:self];
}
- (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];
}
}
/* 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 filename]];
[[app recentResults] addFile:[sp filename]];
}
}
- (void)startDuplicateScan
{
if ([model resultsAreModified]) {
if ([Dialogs askYesNo:NSLocalizedString(@"You have unsaved results, do you really want to continue?", @"")] == NSAlertSecondButtonReturn) // NO
return;
}
[self setScanOptions];
[model doScan];
}
- (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
{
return ![[ProgressController mainProgressController] isShown];
}
@end

17
cocoa/base/StatsLabel.h Normal file
View File

@@ -0,0 +1,17 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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/base/StatsLabel.m Normal file
View File

@@ -0,0 +1,34 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#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

View File

@@ -0,0 +1,151 @@
"%@ Results" = "%@ Results";
"A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again." = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again.";
"About dupeGuru" = "About dupeGuru";
"Action" = "Action";
"Actions" = "Actions";
"Add Aperture Library" = "Add Aperture Library";
"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 iPhoto Library" = "Add iPhoto Library";
"Add iTunes Library" = "Add iTunes Library";
"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";
"Artist" = "Artist";
"Attribute" = "Attribute";
"Audio Content" = "Audio Content";
"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";
"Content" = "Content";
"Contents" = "Contents";
"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 ME Preferences" = "dupeGuru ME Preferences";
"dupeGuru PE Preferences" = "dupeGuru PE Preferences";
"dupeGuru Preferences" = "dupeGuru Preferences";
"dupeGuru Results" = "dupeGuru Results";
"dupeGuru Website" = "dupeGuru Website";
"Dupes Only" = "Dupes Only";
"Edit" = "Edit";
"Excluded" = "Excluded";
"EXIF Timestamp" = "EXIF Timestamp";
"Export Results to CSV" = "Export Results to CSV";
"Export Results to XHTML" = "Export Results to XHTML";
"Fewer results" = "Fewer results";
"File" = "File";
"Filename" = "Filename";
"Filename - Fields" = "Filename - Fields";
"Filename - Fields (No Order)" = "Filename - Fields (No Order)";
"Filter" = "Filter";
"Filter hardness:" = "Filter hardness:";
"Filter Results..." = "Filter Results...";
"Folder Selection Window" = "Folder Selection Window";
"Folders" = "Folders";
"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...";
"Name" = "Name";
"Normal" = "Normal";
"Ok" = "Ok";
"Open Selected with Default Application" = "Open Selected with Default Application";
"Options" = "Options";
"Paste" = "Paste";
"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 Dead Tracks in iTunes" = "Remove Dead Tracks in iTunes";
"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 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";
"Start Duplicate Scan" = "Start Duplicate Scan";
"State" = "State";
"Tags" = "Tags";
"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";

49
cocoa/base/main.m Normal file
View File

@@ -0,0 +1,49 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import <Cocoa/Cocoa.h>
#import <Python.h>
#import <wchar.h>
#import <locale.h>
#import "AppDelegate.h"
#import "MainMenu_UI.h"
int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
/* We have to set the locate to UTF8 for mbstowcs() to correctly convert non-ascii chars in paths */
setlocale(LC_ALL, "en_US.UTF-8");
NSString *respath = [[NSBundle mainBundle] resourcePath];
NSString *mainpy = [respath stringByAppendingPathComponent:@"dg_cocoa.py"];
wchar_t wPythonPath[PATH_MAX+1];
NSString *pypath = [respath stringByAppendingPathComponent:@"py"];
mbstowcs(wPythonPath, [pypath fileSystemRepresentation], PATH_MAX+1);
Py_SetPath(wPythonPath);
Py_SetPythonHome(wPythonPath);
Py_Initialize();
PyEval_InitThreads();
PyGILState_STATE gilState = PyGILState_Ensure();
FILE* fp = fopen([mainpy UTF8String], "r");
PyRun_SimpleFile(fp, [mainpy UTF8String]);
fclose(fp);
PyGILState_Release(gilState);
if (gilState == PyGILState_LOCKED) {
PyThreadState_Swap(NULL);
PyEval_ReleaseLock();
}
[NSApplication sharedApplication];
AppDelegate *appDelegate = [[AppDelegate alloc] init];
[NSApp setDelegate:appDelegate];
[NSApp setMainMenu:createMainMenu_UI(appDelegate)];
[appDelegate finalizeInit];
[pool release];
[NSApp run];
Py_Finalize();
return 0;
}

View File

@@ -0,0 +1,49 @@
ownerclass = 'DeletionOptions'
ownerimport = 'DeletionOptions.h'
result = Window(450, 240, "Deletion Options")
messageLabel = Label(result, "")
linkCheckbox = Checkbox(result, "Link deleted files")
linkLabel = Label(result, "After having deleted a duplicate, place a link targeting the "
"reference file to replace the deleted file.")
linkTypeChoice = RadioButtons(result, ["Symlink", "Hardlink"], columns=2)
directCheckbox = Checkbox(result, "Directly delete files")
directLabel = Label(result, "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.")
proceedButton = Button(result, "Proceed")
cancelButton = Button(result, "Cancel")
owner.linkButton = linkCheckbox
owner.linkTypeRadio = linkTypeChoice
owner.directButton = directCheckbox
owner.messageTextField = messageLabel
result.canMinimize = False
result.canResize = False
linkLabel.controlSize = ControlSize.Small
directLabel.controlSize = ControlSize.Small
linkTypeChoice.controlSize = ControlSize.Small
proceedButton.keyEquivalent = '\\r'
cancelButton.keyEquivalent = '\\e'
linkCheckbox.action = directCheckbox.action = linkTypeChoice.action = Action(owner, 'updateOptions')
proceedButton.action = Action(owner, 'proceed')
cancelButton.action = Action(owner, 'cancel')
linkLabel.height *= 2 # 2 lines
directLabel.height *= 3 # 3 lines
proceedButton.width = 92
cancelButton.width = 92
mainLayout = VLayout([messageLabel, linkCheckbox, linkLabel, linkTypeChoice, directCheckbox,
directLabel])
mainLayout.packToCorner(Pack.UpperLeft)
mainLayout.fill(Pack.Right)
buttonLayout = HLayout([cancelButton, proceedButton])
buttonLayout.packToCorner(Pack.LowerRight)
# indent the labels under checkboxes a little bit to the right
for indentedView in (linkLabel, directLabel, linkTypeChoice):
indentedView.x += 20
indentedView.width -= 20
# We actually don't want the link choice radio buttons to take all the width, it looks weird.
linkTypeChoice.width = 170

View File

@@ -0,0 +1,32 @@
ownerclass = 'DetailsPanel'
ownerimport = 'DetailsPanel.h'
result = Panel(451, 146, "Details of Selected File")
table = TableView(result)
owner.detailsTable = table
result.style = PanelStyle.Utility
result.xProportion = 0.2
result.yProportion = 0.4
result.canMinimize = False
result.autosaveName = 'DetailsPanel'
result.minSize = Size(result.width, result.height)
table.dataSource = owner
table.allowsColumnReordering = False
table.allowsColumnSelection = False
table.allowsMultipleSelection = False
table.font = Font(FontFamily.System, FontSize.SmallSystem)
table.rowHeight = 14
table.editable = False
col = table.addColumn('0', "Attribute", 70)
col.autoResizable = True
col = table.addColumn('1', "Selected", 198)
col.autoResizable = True
col = table.addColumn('2', "Reference", 172)
col.autoResizable = True
table.packToCorner(Pack.UpperLeft, margin=0)
table.fill(Pack.LowerRight, margin=0)
table.setAnchor(Pack.UpperLeft, growX=True, growY=True)

View File

@@ -0,0 +1,64 @@
ownerclass = 'DirectoryPanel'
ownerimport = 'DirectoryPanel.h'
result = Window(425, 300, "dupeGuru")
promptLabel = Label(result, "Select folders to scan and press \"Scan\".")
directoryOutline = OutlineView(result)
directoryOutline.OBJC_CLASS = 'HSOutlineView'
addButton = Button(result, "")
removeButton = Button(result, "")
loadResultsButton = Button(result, "Load Results")
scanButton = Button(result, "Scan")
addPopup = Popup(None)
loadRecentPopup = Popup(None)
owner.outlineView = directoryOutline
owner.removeButton = removeButton
owner.loadResultsButton = loadResultsButton
owner.addButtonPopUp = addPopup
owner.loadRecentButtonPopUp = loadRecentPopup
result.autosaveName = 'DirectoryPanel'
result.canMinimize = False
result.minSize = Size(370, 270)
addButton.bezelStyle = removeButton.bezelStyle = const.NSTexturedRoundedBezelStyle
addButton.image = 'NSAddTemplate'
removeButton.image = 'NSRemoveTemplate'
for button in (addButton, removeButton):
button.style = const.NSTexturedRoundedBezelStyle
button.imagePosition = const.NSImageOnly
scanButton.keyEquivalent = '\\r'
addButton.action = Action(owner, 'popupAddDirectoryMenu:')
removeButton.action = Action(owner, 'removeSelectedDirectory')
loadResultsButton.action = Action(owner, 'popupLoadRecentMenu:')
scanButton.action = Action(None, 'startScanning')
directoryOutline.font = Font(FontFamily.System, FontSize.SmallSystem)
col = directoryOutline.addColumn('name', "Name", 100)
col.editable = False
col.autoResizable = True
col = directoryOutline.addColumn('state', "State", 85)
col.editable = True
col.autoResizable = False
col.dataCell = Popup(None, ["Normal", "Reference", "Excluded"])
col.dataCell.controlSize = const.NSSmallControlSize
directoryOutline.allowsColumnReordering = False
directoryOutline.allowsColumnSelection = False
directoryOutline.allowsMultipleSelection = True
for button in (addButton, removeButton):
button.width = 28
for button in (loadResultsButton, scanButton):
button.width = 118
buttonLayout = HLayout([addButton, removeButton, None, loadResultsButton, scanButton])
promptLabel.packToCorner(Pack.UpperLeft)
promptLabel.fill(Pack.Right)
directoryOutline.packRelativeTo(promptLabel, Pack.Below)
buttonLayout.packRelativeTo(directoryOutline, Pack.Below, margin=8)
directoryOutline.fill(Pack.LowerRight)
buttonLayout.fill(Pack.Right)
promptLabel.setAnchor(Pack.UpperLeft, growX=True)
directoryOutline.setAnchor(Pack.UpperLeft, growX=True, growY=True)
buttonLayout.setAnchor(Pack.Below)

View File

@@ -0,0 +1,30 @@
ownerclass = 'IgnoreListDialog'
ownerimport = 'IgnoreListDialog.h'
result = Window(550, 350, "Ignore List")
table = TableView(result)
removeSelectedButton = Button(result, "Remove Selected")
clearButton = Button(result, "Clear")
closeButton = Button(result, "Close")
owner.ignoreListTableView = table
result.canMinimize = False
removeSelectedButton.action = Action(owner.model, 'removeSelected')
clearButton.action = Action(owner.model, 'clear')
closeButton.action = Action(result, 'performClose:')
closeButton.keyEquivalent = '\\r'
table.allowsColumnReordering = False
table.allowsColumnSelection = False
table.allowsMultipleSelection = True
removeSelectedButton.width = 142
clearButton.width = 142
closeButton.width = 84
buttonLayout = HLayout([removeSelectedButton, clearButton, None, closeButton])
buttonLayout.packToCorner(Pack.LowerLeft)
buttonLayout.fill(Pack.Right)
buttonLayout.setAnchor(Pack.Below)
table.packRelativeTo(buttonLayout, Pack.Above)
table.fill(Pack.UpperRight)
table.setAnchor(Pack.UpperLeft, growX=True, growY=True)

View File

@@ -0,0 +1,82 @@
ownerclass = 'AppDelegateBase'
ownerimport = 'AppDelegateBase.h'
edition = args.get('edition', 'se')
result = Menu("")
appMenu = result.addMenu("dupeGuru")
fileMenu = result.addMenu("File")
editMenu = result.addMenu("Edit")
actionMenu = result.addMenu("Actions")
owner.columnsMenu = result.addMenu("Columns")
modeMenu = result.addMenu("Mode")
windowMenu = result.addMenu("Window")
helpMenu = result.addMenu("Help")
appMenu.addItem("About dupeGuru", Action(owner, 'showAboutBox'))
appMenu.addItem("Check for update...", Action(owner.updater, 'checkForUpdates:'))
appMenu.addSeparator()
appMenu.addItem("Preferences...", Action(owner, 'showPreferencesPanel'), 'cmd+,')
appMenu.addSeparator()
NSApp.servicesMenu = appMenu.addMenu("Services")
appMenu.addSeparator()
appMenu.addItem("Hide dupeGuru", Action(NSApp, 'hide:'), 'cmd+h')
appMenu.addItem("Hide Others", Action(NSApp, 'hideOtherApplications:'), 'cmd+alt+h')
appMenu.addItem("Show All", Action(NSApp, 'unhideAllApplications:'))
appMenu.addSeparator()
appMenu.addItem("Quit dupeGuru", Action(NSApp, 'terminate:'), 'cmd+q')
fileMenu.addItem("Load Results...", Action(None, 'loadResults'), 'cmd+o')
owner.recentResultsMenu = fileMenu.addMenu("Load Recent Results")
fileMenu.addItem("Save Results...", Action(None, 'saveResults'), 'cmd+s')
fileMenu.addItem("Export Results to XHTML", Action(owner.model, 'exportToXHTML'), 'cmd+shift+e')
fileMenu.addItem("Export Results to CSV", Action(owner.model, 'exportToCSV'))
if edition == 'pe':
fileMenu.addItem("Clear Picture Cache", Action(owner, 'clearPictureCache'), 'cmd+shift+p')
elif edition == 'me':
fileMenu.addItem("Remove Dead Tracks in iTunes", Action(owner, 'removeDeadTracks'))
editMenu.addItem("Mark All", Action(None, 'markAll'), 'cmd+a')
editMenu.addItem("Mark None", Action(None, 'markNone'), 'cmd+shift+a')
editMenu.addItem("Invert Marking", Action(None, 'markInvert'), 'cmd+alt+a')
editMenu.addItem("Mark Selected", Action(None, 'markSelected'), 'ctrl+cmd+a')
editMenu.addSeparator()
editMenu.addItem("Cut", Action(None, 'cut:'), 'cmd+x')
editMenu.addItem("Copy", Action(None, 'copy:'), 'cmd+c')
editMenu.addItem("Paste", Action(None, 'paste:'), 'cmd+v')
editMenu.addSeparator()
editMenu.addItem("Filter Results...", Action(None, 'focusOnFilterField'), 'cmd+alt+f')
actionMenu.addItem("Start Duplicate Scan", Action(owner, 'startScanning'), 'cmd+d')
actionMenu.addSeparator()
actionMenu.addItem("Send Marked to Trash...", Action(None, 'trashMarked'), 'cmd+t')
actionMenu.addItem("Move Marked to...", Action(None, 'moveMarked'), 'cmd+m')
actionMenu.addItem("Copy Marked to...", Action(None, 'copyMarked'), 'cmd+alt+m')
actionMenu.addItem("Remove Marked from Results", Action(None, 'removeMarked'), 'cmd+r')
actionMenu.addItem("Re-Prioritize Results...", Action(None, 'reprioritizeResults'))
actionMenu.addSeparator()
actionMenu.addItem("Remove Selected from Results", Action(None, 'removeSelected'), 'cmd+backspace')
actionMenu.addItem("Add Selected to Ignore List", Action(None, 'ignoreSelected'), 'cmd+g')
actionMenu.addItem("Make Selected into Reference", Action(None, 'switchSelected'), 'cmd+arrowup')
actionMenu.addSeparator()
actionMenu.addItem("Open Selected with Default Application", Action(None, 'openSelected'), 'cmd+return')
actionMenu.addItem("Reveal Selected in Finder", Action(None, 'revealSelected'), 'cmd+alt+return')
actionMenu.addItem("Invoke Custom Command", Action(None, 'invokeCustomCommand'), 'cmd+shift+c')
actionMenu.addItem("Rename Selected", Action(None, 'renameSelected'), 'enter')
modeMenu.addItem("Show Dupes Only", Action(None, 'togglePowerMarker'), 'cmd+1')
modeMenu.addItem("Show Delta Values", Action(None, 'toggleDelta'), 'cmd+2')
windowMenu.addItem("Results Window", Action(owner, 'showResultWindow'))
windowMenu.addItem("Folder Selection Window", Action(owner, 'showDirectoryWindow'))
windowMenu.addItem("Ignore List", Action(owner, 'showIgnoreList'))
windowMenu.addItem("Details Panel", Action(None, 'toggleDetailsPanel'), 'cmd+i')
windowMenu.addItem("Quick Look", Action(None, 'toggleQuicklookPanel'), 'cmd+l')
windowMenu.addSeparator()
windowMenu.addItem("Minimize", Action(None, 'performMinimize:'))
windowMenu.addItem("Zoom", Action(None, 'performZoom:'))
windowMenu.addItem("Close Window", Action(None, 'performClose:'), 'cmd+w')
windowMenu.addSeparator()
windowMenu.addItem("Bring All to Front", Action(None, 'arrangeInFront:'))
helpMenu.addItem("dupeGuru Help", Action(owner, 'openHelp'), 'cmd+?')
helpMenu.addItem("dupeGuru Website", Action(owner, 'openWebsite'))

View File

@@ -0,0 +1,189 @@
edition = args.get('edition', 'se')
dialogTitles = {
'se': "dupeGuru Preferences",
'me': "dupeGuru ME Preferences",
'pe': "dupeGuru PE Preferences",
}
dialogHeights = {
'se': 345,
'me': 365,
'pe': 275,
}
scanTypeNames = {
'se': ["Filename", "Content", "Folders"],
'me': ["Filename", "Filename - Fields", "Filename - Fields (No Order)", "Tags", "Content", "Audio Content"],
'pe': ["Contents", "EXIF Timestamp"],
}
result = Window(410, dialogHeights[edition], dialogTitles[edition])
tabView = TabView(result)
basicTab = tabView.addTab("Basic")
advancedTab = tabView.addTab("Advanced")
scanTypePopup = Popup(basicTab.view, scanTypeNames[edition])
scanTypeLabel = Label(basicTab.view, "Scan Type:")
thresholdSlider = Slider(basicTab.view, 1, 100, 80)
thresholdLabel = Label(basicTab.view, "Filter hardness:")
moreResultsLabel = Label(basicTab.view, "More results")
fewerResultsLabel = Label(basicTab.view, "Fewer results")
thresholdValueLabel = Label(basicTab.view, "")
fontSizeCombo = Combobox(basicTab.view, ["11", "12", "13", "14", "18", "24"])
fontSizeLabel = Label(basicTab.view, "Font Size:")
if edition in ('se', 'me'):
wordWeightingBox = Checkbox(basicTab.view, "Word weighting")
matchSimilarWordsBox = Checkbox(basicTab.view, "Match similar words")
elif edition == 'pe':
matchDifferentDimensionsBox = Checkbox(basicTab.view, "Match pictures of different dimensions")
mixKindBox = Checkbox(basicTab.view, "Can mix file kind")
removeEmptyFoldersBox = Checkbox(basicTab.view, "Remove empty folders on delete or move")
checkForUpdatesBox = Checkbox(basicTab.view, "Automatically check for updates")
if edition == 'se':
ignoreSmallFilesBox = Checkbox(basicTab.view, "Ignore files smaller than:")
smallFilesThresholdText = TextField(basicTab.view, "")
smallFilesThresholdSuffixLabel = Label(basicTab.view, "KB")
elif edition == 'me':
tagsToScanLabel = Label(basicTab.view, "Tags to scan:")
trackBox = Checkbox(basicTab.view, "Track")
artistBox = Checkbox(basicTab.view, "Artist")
albumBox = Checkbox(basicTab.view, "Album")
titleBox = Checkbox(basicTab.view, "Title")
genreBox = Checkbox(basicTab.view, "Genre")
yearBox = Checkbox(basicTab.view, "Year")
tagBoxes = [trackBox, artistBox, albumBox, titleBox, genreBox, yearBox]
regexpCheckbox = Checkbox(advancedTab.view, "Use regular expressions when filtering")
ignoreHardlinksBox = Checkbox(advancedTab.view, "Ignore duplicates hardlinking to the same file")
debugModeCheckbox = Checkbox(advancedTab.view, "Debug mode (restart required)")
customCommandLabel = Label(advancedTab.view, "Custom command (arguments: %d for dupe, %r for ref):")
customCommandText = TextField(advancedTab.view, "")
copyMoveLabel = Label(advancedTab.view, "Copy and Move:")
copyMovePopup = Popup(advancedTab.view, ["Right in destination", "Recreate relative path", "Recreate absolute path"])
resetToDefaultsButton = Button(result, "Reset To Defaults")
scanTypePopup.bind('selectedIndex', defaults, 'values.scanType')
thresholdSlider.bind('value', defaults, 'values.minMatchPercentage')
thresholdValueLabel.bind('value', defaults, 'values.minMatchPercentage')
fontSizeCombo.bind('value', defaults, 'values.TableFontSize')
mixKindBox.bind('value', defaults, 'values.mixFileKind')
removeEmptyFoldersBox.bind('value', defaults, 'values.removeEmptyFolders')
checkForUpdatesBox.bind('value', defaults, 'values.SUEnableAutomaticChecks')
regexpCheckbox.bind('value', defaults, 'values.useRegexpFilter')
ignoreHardlinksBox.bind('value', defaults, 'values.ignoreHardlinkMatches')
debugModeCheckbox.bind('value', defaults, 'values.DebugMode')
customCommandText.bind('value', defaults, 'values.CustomCommand')
copyMovePopup.bind('selectedIndex', defaults, 'values.recreatePathType')
if edition in ('se', 'me'):
wordWeightingBox.bind('value', defaults, 'values.wordWeighting')
matchSimilarWordsBox.bind('value', defaults, 'values.matchSimilarWords')
disableWhenContentScan = [thresholdSlider, wordWeightingBox, matchSimilarWordsBox]
for control in disableWhenContentScan:
control.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsNotContent')
if edition == 'se':
ignoreSmallFilesBox.bind('value', defaults, 'values.ignoreSmallFiles')
smallFilesThresholdText.bind('value', defaults, 'values.smallFileThreshold')
elif edition == 'me':
for box in tagBoxes:
box.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsTag')
trackBox.bind('value', defaults, 'values.scanTagTrack')
artistBox.bind('value', defaults, 'values.scanTagArtist')
albumBox.bind('value', defaults, 'values.scanTagAlbum')
titleBox.bind('value', defaults, 'values.scanTagTitle')
genreBox.bind('value', defaults, 'values.scanTagGenre')
yearBox.bind('value', defaults, 'values.scanTagYear')
elif edition == 'pe':
matchDifferentDimensionsBox.bind('value', defaults, 'values.matchScaled')
thresholdSlider.bind('enabled', defaults, 'values.scanType', valueTransformer='vtScanTypeIsFuzzy')
result.canResize = False
result.canMinimize = False
thresholdValueLabel.formatter = NumberFormatter(NumberStyle.Decimal)
thresholdValueLabel.formatter.maximumFractionDigits = 0
allLabels = [scanTypeLabel, thresholdValueLabel, moreResultsLabel, fewerResultsLabel,
thresholdLabel, fontSizeLabel, customCommandLabel, copyMoveLabel]
allCheckboxes = [mixKindBox, removeEmptyFoldersBox, checkForUpdatesBox, regexpCheckbox,
ignoreHardlinksBox, debugModeCheckbox]
if edition == 'se':
allLabels += [smallFilesThresholdSuffixLabel]
allCheckboxes += [ignoreSmallFilesBox, wordWeightingBox, matchSimilarWordsBox]
elif edition == 'me':
allLabels += [tagsToScanLabel]
allCheckboxes += tagBoxes + [wordWeightingBox, matchSimilarWordsBox]
elif edition == 'pe':
allCheckboxes += [matchDifferentDimensionsBox]
for label in allLabels:
label.controlSize = ControlSize.Small
fewerResultsLabel.alignment = TextAlignment.Right
for checkbox in allCheckboxes:
checkbox.font = scanTypeLabel.font
resetToDefaultsButton.action = Action(defaults, 'revertToInitialValues:')
scanTypeLabel.width = thresholdLabel.width = fontSizeLabel.width = 94
fontSizeCombo.width = 66
thresholdValueLabel.width = 25
resetToDefaultsButton.width = 136
if edition == 'se':
smallFilesThresholdText.width = 60
smallFilesThresholdSuffixLabel.width = 40
elif edition == 'me':
for box in tagBoxes:
box.width = 70
tabView.packToCorner(Pack.UpperLeft)
tabView.fill(Pack.Right)
resetToDefaultsButton.packRelativeTo(tabView, Pack.Below, align=Pack.Right)
tabView.fill(Pack.Below, margin=14)
tabView.setAnchor(Pack.UpperLeft, growX=True, growY=True)
scanTypePopup.packToCorner(Pack.UpperRight)
scanTypeLabel.packRelativeTo(scanTypePopup, Pack.Left)
scanTypePopup.fill(Pack.Left)
thresholdSlider.packRelativeTo(scanTypePopup, Pack.Below)
thresholdValueLabel.packRelativeTo(thresholdSlider, Pack.Right)
thresholdSlider.fill(Pack.Right)
# We want to give the labels as much space as possible, and we only "know" how much is available
# after the slider's fill operation.
moreResultsLabel.width = fewerResultsLabel.width = thresholdSlider.width // 2
moreResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Left, margin=6)
fewerResultsLabel.packRelativeTo(thresholdSlider, Pack.Below, align=Pack.Right, margin=6)
thresholdLabel.packRelativeTo(thresholdSlider, Pack.Left)
fontSizeCombo.packRelativeTo(moreResultsLabel, Pack.Below)
fontSizeLabel.packRelativeTo(fontSizeCombo, Pack.Left)
if edition == 'me':
tagsToScanLabel.packRelativeTo(fontSizeCombo, Pack.Below)
tagsToScanLabel.fill(Pack.Left)
tagsToScanLabel.fill(Pack.Right)
trackBox.packRelativeTo(tagsToScanLabel, Pack.Below)
trackBox.x += 10
artistBox.packRelativeTo(trackBox, Pack.Right)
albumBox.packRelativeTo(artistBox, Pack.Right)
titleBox.packRelativeTo(trackBox, Pack.Below)
genreBox.packRelativeTo(titleBox, Pack.Right)
yearBox.packRelativeTo(genreBox, Pack.Right)
viewToPackCheckboxesUnder = titleBox
else:
viewToPackCheckboxesUnder = fontSizeCombo
if edition == 'se':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
ignoreSmallFilesBox]
elif edition == 'me':
checkboxesToLayout = [wordWeightingBox, matchSimilarWordsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox]
elif edition == 'pe':
checkboxesToLayout = [matchDifferentDimensionsBox, mixKindBox, removeEmptyFoldersBox,
checkForUpdatesBox]
checkboxLayout = VLayout(checkboxesToLayout)
checkboxLayout.packRelativeTo(viewToPackCheckboxesUnder, Pack.Below)
checkboxLayout.fill(Pack.Left)
checkboxLayout.fill(Pack.Right)
if edition == 'se':
smallFilesThresholdText.packRelativeTo(ignoreSmallFilesBox, Pack.Below, margin=4)
checkForUpdatesBox.packRelativeTo(smallFilesThresholdText, Pack.Below, margin=4)
checkForUpdatesBox.fill(Pack.Right)
smallFilesThresholdText.x += 20
smallFilesThresholdSuffixLabel.packRelativeTo(smallFilesThresholdText, Pack.Right)
advancedLayout = VLayout(advancedTab.view.subviews[:])
advancedLayout.packToCorner(Pack.UpperLeft)
advancedLayout.fill(Pack.Right)

View File

@@ -0,0 +1,65 @@
ownerclass = 'PrioritizeDialog'
ownerimport = 'PrioritizeDialog.h'
result = Window(610, 400, "Re-Prioritize duplicates")
promptLabel = Label(result, "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.")
split = SplitView(result, 2, vertical=True)
categoryPopup = Popup(split.subviews[0])
criteriaTable = ListView(split.subviews[0])
prioritizationTable = ListView(split.subviews[1])
addButton = Button(split.subviews[1], NLSTR("-->"))
removeButton = Button(split.subviews[1], NLSTR("<--"))
okButton = Button(result, "Ok")
cancelButton = Button(result, "Cancel")
owner.categoryPopUpView = categoryPopup
owner.criteriaTableView = criteriaTable
owner.prioritizationTableView = prioritizationTable
result.canMinimize = False
result.canClose = False
result.minSize = Size(result.width, result.height)
addButton.action = Action(owner.model, 'addSelected')
removeButton.action = Action(owner.model, 'removeSelected')
okButton.action = Action(owner, 'ok')
cancelButton.action = Action(owner, 'cancel')
okButton.keyEquivalent = '\\r'
cancelButton.keyEquivalent = '\\e'
# For layouts to correctly work, subviews need to have the dimensions they'll approximately have
# at runtime.
split.subviews[0].width = 260
split.subviews[0].height = 260
split.subviews[1].width = 340
split.subviews[1].height = 260
promptLabel.height *= 3 # 3 lines
leftLayout = VLayout([categoryPopup, criteriaTable], filler=criteriaTable)
middleLayout = VLayout([addButton, removeButton], width=41)
buttonLayout = HLayout([None, cancelButton, okButton])
#pack split subview 0
leftLayout.fillAll()
#pack split subview 1
prioritizationTable.fillAll()
prioritizationTable.width -= 48
prioritizationTable.moveTo(Pack.Right)
middleLayout.moveNextTo(prioritizationTable, Pack.Left, align=Pack.Middle)
# Main layout
promptLabel.packToCorner(Pack.UpperLeft)
promptLabel.fill(Pack.Right)
split.moveNextTo(promptLabel, Pack.Below)
buttonLayout.moveNextTo(split, Pack.Below)
buttonLayout.fill(Pack.Right)
split.fill(Pack.LowerRight)
promptLabel.setAnchor(Pack.UpperLeft, growX=True)
prioritizationTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
categoryPopup.setAnchor(Pack.UpperLeft, growX=True)
criteriaTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
split.setAnchor(Pack.UpperLeft, growX=True, growY=True)
buttonLayout.setAnchor(Pack.Below)

View File

@@ -0,0 +1,35 @@
ownerclass = 'ProblemDialog'
ownerimport = 'ProblemDialog.h'
result = Window(480, 310, "Problems!")
messageLabel = Label(result, "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.")
problemTable = TableView(result)
revealButton = Button(result, "Reveal")
closeButton = Button(result, "Close")
owner.problemTableView = problemTable
result.canMinimize = False
result.minSize = Size(300, 300)
closeButton.keyEquivalent = '\\r'
revealButton.action = Action(owner.model, 'revealSelected')
closeButton.action = Action(result, 'performClose:')
messageLabel.height *= 3 # 3 lines
revealButton.width = 150
closeButton.width = 98
messageLabel.packToCorner(Pack.UpperLeft)
messageLabel.fill(Pack.Right)
problemTable.packRelativeTo(messageLabel, Pack.Below)
problemTable.fill(Pack.Right)
revealButton.packRelativeTo(problemTable, Pack.Below)
closeButton.packRelativeTo(problemTable, Pack.Below, align=Pack.Right)
problemTable.fill(Pack.Below)
messageLabel.setAnchor(Pack.UpperLeft, growX=True)
problemTable.setAnchor(Pack.UpperLeft, growX=True, growY=True)
revealButton.setAnchor(Pack.LowerLeft)
closeButton.setAnchor(Pack.LowerRight)

View File

@@ -0,0 +1,97 @@
ownerclass = 'ResultWindowBase'
ownerimport = 'ResultWindowBase.h'
result = Window(557, 400, "dupeGuru Results")
toolbar = result.createToolbar('ResultsToolbar')
table = TableView(result)
table.OBJC_CLASS = 'HSTableView'
statsLabel = Label(result, "")
contextMenu = Menu("")
#Setup toolbar items
toolbar.displayMode = const.NSToolbarDisplayModeIconOnly
directoriesToolItem = toolbar.addItem('Directories', "Directories", image='folder32')
actionToolItem = toolbar.addItem('Action', "Action")
filterToolItem = toolbar.addItem('Filter', "Filter")
optionsToolItem = toolbar.addItem('Options', "Options")
quicklookToolItem = toolbar.addItem('QuickLook', "Quick Look")
toolbar.defaultItems = [actionToolItem, optionsToolItem, quicklookToolItem, directoriesToolItem,
toolbar.flexibleSpace(), filterToolItem]
actionPopup = Popup(None)
actionPopup.pullsdown = True
actionPopup.bezelStyle = const.NSTexturedRoundedBezelStyle
actionPopup.arrowPosition = const.NSPopUpArrowAtBottom
item = actionPopup.menu.addItem("") # First item is invisible
item.hidden = True
item.image = 'NSActionTemplate'
actionPopup.width = 44
actionToolItem.view = actionPopup
filterField = SearchField(None, "Filter")
filterField.action = Action(owner, 'filter')
filterField.sendsWholeSearchString = True
filterToolItem.view = filterField
filterToolItem.minSize = Size(80, 22)
filterToolItem.maxSize = Size(300, 22)
quickLookButton = Button(None, "")
quickLookButton.bezelStyle = const.NSTexturedRoundedBezelStyle
quickLookButton.image = 'NSQuickLookTemplate'
quickLookButton.width = 44
quickLookButton.action = Action(owner, 'toggleQuicklookPanel')
quicklookToolItem.view = quickLookButton
optionsSegments = SegmentedControl(None)
optionsSegments.segmentStyle = const.NSSegmentStyleCapsule
optionsSegments.trackingMode = const.NSSegmentSwitchTrackingSelectAny
optionsSegments.font = Font(FontFamily.System, 11)
optionsSegments.addSegment("Details", 57)
optionsSegments.addSegment("Dupes Only", 82)
optionsSegments.addSegment("Delta", 48)
optionsSegments.action = Action(owner, 'changeOptions')
optionsToolItem.view = optionsSegments
# Popuplate menus
actionPopup.menu.addItem("Send Marked to Trash...", action=Action(owner, 'trashMarked'))
actionPopup.menu.addItem("Move Marked to...", action=Action(owner, 'moveMarked'))
actionPopup.menu.addItem("Copy Marked to...", action=Action(owner, 'copyMarked'))
actionPopup.menu.addItem("Remove Marked from Results", action=Action(owner, 'removeMarked'))
actionPopup.menu.addSeparator()
for menu in (actionPopup.menu, contextMenu):
menu.addItem("Remove Selected from Results", action=Action(owner, 'removeSelected'))
menu.addItem("Add Selected to Ignore List", action=Action(owner, 'ignoreSelected'))
menu.addItem("Make Selected into Reference", action=Action(owner, 'switchSelected'))
menu.addSeparator()
menu.addItem("Open Selected with Default Application", action=Action(owner, 'openSelected'))
menu.addItem("Reveal Selected in Finder", action=Action(owner, 'revealSelected'))
menu.addItem("Rename Selected", action=Action(owner, 'renameSelected'))
# Doing connections
owner.filterField = filterField
owner.matches = table
owner.optionsSwitch = optionsSegments
owner.optionsToolbarItem = optionsToolItem
owner.stats = statsLabel
table.bind('rowHeight', defaults, 'values.TableFontSize', valueTransformer='vtRowHeightOffset')
# Rest of the setup
result.minSize = Size(340, 340)
result.autosaveName = 'MainWindow'
statsLabel.alignment = TextAlignment.Center
table.alternatingRows = True
table.menu = contextMenu
table.allowsColumnReordering = True
table.allowsColumnResizing = True
table.allowsColumnSelection = False
table.allowsEmptySelection = False
table.allowsMultipleSelection = True
table.allowsTypeSelect = True
table.gridStyleMask = const.NSTableViewSolidHorizontalGridLineMask
table.setAnchor(Pack.UpperLeft, growX=True, growY=True)
statsLabel.setAnchor(Pack.LowerLeft, growX=True)
# Layout
# It's a little weird to pack with a margin of -1, but if I don't do that, I get too thick of a
# border on the upper side of the table.
table.packToCorner(Pack.UpperLeft, margin=-1)
table.fill(Pack.Right, margin=0)
statsLabel.packRelativeTo(table, Pack.Below, margin=6)
statsLabel.fill(Pack.Right, margin=0)
table.fill(Pack.Below, margin=5)

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

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

@@ -0,0 +1,177 @@
import logging
from objp.util import pyref, dontwrap
from cocoa import install_exception_hook, install_cocoa_logger, patch_threaded_job_performer, proxy
from cocoa.inter import PyFairware, FairwareView
class DupeGuruView(FairwareView):
def askYesNoWithPrompt_(self, prompt: str) -> bool: pass
def showProblemDialog(self): pass
def selectDestFolderWithPrompt_(self, prompt: str) -> str: pass
def selectDestFileWithPrompt_extension_(self, prompt: str, extension: str) -> str: pass
class PyDupeGuruBase(PyFairware):
@dontwrap
def _init(self, modelclass):
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(message)s')
install_exception_hook()
install_cocoa_logger()
patch_threaded_job_performer()
appdata = proxy.getAppdataPath()
self.model = modelclass(self, appdata)
#---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()
#---Information
def resultsAreModified(self) -> bool:
return self.model.results.is_modified
#---Properties
def setMixFileKind_(self, mix_file_kind: bool):
self.model.scanner.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 open_path(self, path):
proxy.openPath_(str(path))
@dontwrap
def reveal_path(self, path):
proxy.revealPath_(str(path))
@dontwrap
def ask_yes_no(self, prompt):
return self.callback.askYesNoWithPrompt_(prompt)
@dontwrap
def show_results_window(self):
# Not needed yet because our progress dialog is shown as a sheet of the results window,
# which causes it to be already visible when the scan/load ends.
# XXX Make progress sheet be a child of the folder selection window.
pass
@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)

290
cocoa/inter/app_me.py Normal file
View File

@@ -0,0 +1,290 @@
# Created By: Virgil Dupras
# Created On: 2006/11/16
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import logging
import plistlib
import time
import os.path as op
from appscript import app, its, k, CommandError, ApplicationNotFoundError
from . import tunes
from cocoa import as_fetch, proxy
from hscommon.trans import trget
from hscommon.path import Path
from hscommon.util import remove_invalid_xml
from core import directories
from core.app import JobType, JOBID2TITLE
from core.scanner import ScanType
from core_me.app import DupeGuru as DupeGuruBase
from core_me import fs
from .app import PyDupeGuruBase
tr = trget('ui')
JobType.RemoveDeadTracks = 'jobRemoveDeadTracks'
JobType.ScanDeadTracks = 'jobScanDeadTracks'
JOBID2TITLE.update({
JobType.RemoveDeadTracks: tr("Removing dead tracks from your iTunes Library"),
JobType.ScanDeadTracks: tr("Scanning the iTunes Library"),
})
ITUNES = 'iTunes'
ITUNES_PATH = Path('iTunes Library')
def get_itunes_library(a):
try:
[source] = [s for s in a.sources(timeout=0) if s.kind(timeout=0) == k.library]
[library] = source.library_playlists(timeout=0)
return library
except ValueError:
logging.warning('Some unexpected iTunes configuration encountered')
return None
class ITunesSong(fs.MusicFile):
def __init__(self, song_data):
path = Path(proxy.url2path_(song_data['Location']))
fs.MusicFile.__init__(self, path)
self.id = song_data['Track ID']
def remove_from_library(self):
try:
a = app(ITUNES, terms=tunes)
library = get_itunes_library(a)
if library is None:
return
[song] = library.file_tracks[its.database_ID == self.id]()
a.delete(song, timeout=0)
except ValueError:
msg = "Could not find song '{}' (trackid: {}) in iTunes Library".format(str(self.path), self.id)
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
display_folder_path = ITUNES_PATH
def get_itunes_database_path():
plisturls = proxy.prefValue_inDomain_('iTunesRecentDatabases', 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_itunes_songs(plistpath):
if not plistpath.exists():
return []
s = plistpath.open('rt', encoding='utf-8').read()
# iTunes sometimes produces XML files with invalid characters in it.
s = remove_invalid_xml(s, replace_with='')
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = []
for song_data in plist['Tracks'].values():
try:
if song_data['Track Type'] != 'File':
continue
song = ITunesSong(song_data)
except KeyError: # No "Track Type", "Location" or "Track ID" key in track
continue
if song.path.exists():
result.append(song)
return result
class Directories(directories.Directories):
def __init__(self, fileclasses):
directories.Directories.__init__(self, fileclasses)
try:
self.itunes_libpath = get_itunes_database_path()
except directories.InvalidPathError:
self.itunes_libpath = None
def _get_files(self, from_path, j):
if from_path == ITUNES_PATH:
if self.itunes_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
songs = get_itunes_songs(self.itunes_libpath)
for song in songs:
song.is_ref = is_ref
return songs
else:
return directories.Directories._get_files(self, from_path, j)
@staticmethod
def get_subfolders(path):
if path == ITUNES_PATH:
return []
else:
return directories.Directories.get_subfolders(path)
def add_path(self, path):
if path == ITUNES_PATH:
if path not in self:
self._dirs.append(path)
else:
directories.Directories.add_path(self, path)
def has_itunes_path(self):
return any(path == ITUNES_PATH for path in self._dirs)
def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
# checking if there's any file to scan involves reading the whole library. If we have the
# iTunes library, we assume we have at least one file.
if self.has_itunes_path():
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruME(DupeGuruBase):
def __init__(self, view, appdata):
appdata = op.join(appdata, 'dupeGuru Music Edition')
DupeGuruBase.__init__(self, view, appdata)
# Use fileclasses set in DupeGuruBase.__init__()
self.directories = Directories(fileclasses=self.directories.fileclasses)
self.dead_tracks = []
def _do_delete(self, j, *args):
# XXX If I read correctly, Python 3.3 will allow us to go fetch inner function easily, so
# we'll be able to replace "op" below with DupeGuruBase._do_delete.op.
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, *args)
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
if any(isinstance(dupe, ITunesSong) for dupe in marked):
j.add_progress(0, desc=tr("Talking to iTunes. Don't touch it!"))
try:
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, ITunesSong):
dupe.remove_from_library()
DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path):
if (self.directories.itunes_libpath is not None) and (path in self.directories.itunes_libpath[:-1]):
if not hasattr(self, 'itunes_songs'):
songs = get_itunes_songs(self.directories.itunes_libpath)
self.itunes_songs = {song.path: song for song in songs}
if path in self.itunes_songs:
return self.itunes_songs[path]
else:
pass # We'll return the default file type, as per the last line of this method
return DupeGuruBase._create_file(self, path)
def _job_completed(self, jobid):
# XXX Just before release, I'm realizing that this piece of code below is why I was passing
# job exception as an argument to _job_completed(). I have to comment it for now. It's not
# the end of the world, but I should find an elegant solution to this at some point.
# if (jobid in {JobType.RemoveDeadTracks, JobType.ScanDeadTracks}) and (exc is not None):
# msg = tr("There were communication problems with iTunes. The operation couldn't be completed.")
# self.view.show_message(msg)
# return True
if jobid == JobType.ScanDeadTracks:
dead_tracks_count = len(self.dead_tracks)
if dead_tracks_count > 0:
msg = tr("Your iTunes Library contains %d dead tracks ready to be removed. Continue?")
if self.view.ask_yes_no(msg % dead_tracks_count):
self.remove_dead_tracks()
else:
msg = tr("You have no dead tracks in your iTunes Library")
self.view.show_message(msg)
if jobid == JobType.Load:
if hasattr(self, 'itunes_songs'):
# If we load another file, we want a refresh song list
del self.itunes_songs
DupeGuruBase._job_completed(self, jobid)
def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, ITunesSong):
copy = True
return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type)
def start_scanning(self):
if self.directories.has_itunes_path():
try:
app(ITUNES, terms=tunes)
except ApplicationNotFoundError:
self.view.show_message(tr("The iTunes application couldn't be found."))
return
DupeGuruBase.start_scanning(self)
def remove_dead_tracks(self):
def do(j):
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
for index, track in enumerate(j.iter_with_progress(self.dead_tracks)):
if index % 100 == 0:
time.sleep(.1)
try:
track.delete(timeout=0)
except CommandError as e:
logging.warning('Error while trying to remove a track from iTunes: %s' % str(e))
self._start_job(JobType.RemoveDeadTracks, do)
def scan_dead_tracks(self):
def do(j):
a = app(ITUNES, terms=tunes)
a.activate(timeout=0)
library = get_itunes_library(a)
if library is None:
return
self.dead_tracks = []
tracks = as_fetch(library.file_tracks, k.file_track)
for index, track in enumerate(j.iter_with_progress(tracks)):
if index % 100 == 0:
time.sleep(.1)
if track.location(timeout=0) == k.missing_value:
self.dead_tracks.append(track)
logging.info('Found %d dead tracks' % len(self.dead_tracks))
self._start_job(JobType.ScanDeadTracks, do)
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuruME)
def scanDeadTracks(self):
self.model.scan_dead_tracks()
#---Properties
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.min_match_percentage = percentage
def setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.Filename,
ScanType.Fields,
ScanType.FieldsNoOrder,
ScanType.Tag,
ScanType.Contents,
ScanType.ContentsAudio,
][scan_type]
except IndexError:
pass
def setWordWeighting_(self, words_are_weighted: bool):
self.model.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.scanner.match_similar_words = match_similar_words
def enable_scanForTag_(self, enable: bool, scan_tag: str):
if enable:
self.model.scanner.scanned_tags.add(scan_tag)
else:
self.model.scanner.scanned_tags.discard(scan_tag)

328
cocoa/inter/app_pe.py Normal file
View File

@@ -0,0 +1,328 @@
# Created By: Virgil Dupras
# Created On: 2006/11/13
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import os.path as op
import plistlib
import logging
import re
from appscript import app, its, k, CommandError, ApplicationNotFoundError
from hscommon import io
from hscommon.util import remove_invalid_xml, first
from hscommon.path import Path
from hscommon.trans import trget
from cocoa import proxy
from core.scanner import ScanType
from core import directories
from core.app import JobType
from core_pe import _block_osx
from core_pe.photo import Photo as PhotoBase
from core_pe.app import DupeGuru as DupeGuruBase
from .app import PyDupeGuruBase
tr = trget('ui')
IPHOTO_PATH = Path('iPhoto Library')
APERTURE_PATH = Path('Aperture Library')
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
class IPhoto(Photo):
def __init__(self, path, db_id):
# In IPhoto, we don't care about the db_id, we find photos by path.
Photo.__init__(self, path)
@property
def display_folder_path(self):
return IPHOTO_PATH
class AperturePhoto(Photo):
def __init__(self, path, db_id):
Photo.__init__(self, path)
self.db_id = db_id
@property
def display_folder_path(self):
return APERTURE_PATH
def get_iphoto_or_aperture_pictures(plistpath, photo_class):
# The structure of iPhoto and Aperture libraries for the base photo list are excactly the same.
if not io.exists(plistpath):
return []
s = io.open(plistpath, 'rt', encoding='utf-8').read()
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
s = remove_invalid_xml(s, replace_with='')
# It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find
# any & char that is not a &-based entity (&amp;, &quot;, etc.). based on TextMate's XML
# bundle's regexp
s, count = re.subn(r'&(?![a-zA-Z0-9_-]+|#[0-9]+|#x[0-9a-fA-F]+;)', '', s)
if count:
logging.warning("%d invalid XML entities replacement made", count)
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = []
for key, photo_data in plist['Master Image List'].items():
if photo_data['MediaType'] != 'Image':
continue
photo_path = Path(photo_data['ImagePath'])
photo = photo_class(photo_path, key)
result.append(photo)
return result
def get_iphoto_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, IPhoto)
def get_aperture_pictures(plistpath):
return get_iphoto_or_aperture_pictures(plistpath, AperturePhoto)
def get_iapps_database_path(prefname):
plisturls = proxy.prefValue_inDomain_(prefname, 'com.apple.iApps')
if not plisturls:
raise directories.InvalidPathError()
plistpath = proxy.url2path_(plisturls[0])
return Path(plistpath)
def get_iphoto_database_path():
return get_iapps_database_path('iPhotoRecentDatabases')
def get_aperture_database_path():
return get_iapps_database_path('ApertureLibraries')
class Directories(directories.Directories):
def __init__(self):
directories.Directories.__init__(self, fileclasses=[Photo])
try:
self.iphoto_libpath = get_iphoto_database_path()
self.set_state(self.iphoto_libpath[:-1], directories.DirectoryState.Excluded)
except directories.InvalidPathError:
self.iphoto_libpath = None
try:
self.aperture_libpath = get_aperture_database_path()
self.set_state(self.aperture_libpath[:-1], directories.DirectoryState.Excluded)
except directories.InvalidPathError:
self.aperture_libpath = None
def _get_files(self, from_path, j):
if from_path == IPHOTO_PATH:
if self.iphoto_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
photos = get_iphoto_pictures(self.iphoto_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
elif from_path == APERTURE_PATH:
if self.aperture_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.DirectoryState.Reference
photos = get_aperture_pictures(self.aperture_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
else:
return directories.Directories._get_files(self, from_path, j)
@staticmethod
def get_subfolders(path):
if path in {IPHOTO_PATH, APERTURE_PATH}:
return []
else:
return directories.Directories.get_subfolders(path)
def add_path(self, path):
if path in {IPHOTO_PATH, APERTURE_PATH}:
if path not in self:
self._dirs.append(path)
else:
directories.Directories.add_path(self, path)
def has_iphoto_path(self):
return any(path in {IPHOTO_PATH, APERTURE_PATH} for path in self._dirs)
def has_any_file(self):
# If we don't do that, it causes a hangup in the GUI when we click Start Scanning because
# checking if there's any file to scan involves reading the whole library. If we have the
# iPhoto library, we assume we have at least one file.
if self.has_iphoto_path():
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruPE(DupeGuruBase):
def __init__(self, view, appdata):
appdata = op.join(appdata, 'dupeGuru Picture Edition')
DupeGuruBase.__init__(self, view, appdata)
self.directories = Directories()
def _do_delete(self, j, *args):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe, *args)
self.deleted_aperture_photos = False
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
j.start_job(self.results.mark_count, tr("Sending dupes to the Trash"))
if any(isinstance(dupe, IPhoto) for dupe in marked):
j.add_progress(0, desc=tr("Talking to iPhoto. Don't touch it!"))
try:
a = app('iPhoto')
a.activate(timeout=0)
a.select(a.photo_library_album(timeout=0), timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
if any(isinstance(dupe, AperturePhoto) for dupe in marked):
self.deleted_aperture_photos = True
j.add_progress(0, desc=tr("Talking to Aperture. Don't touch it!"))
try:
a = app('Aperture')
a.activate(timeout=0)
except (CommandError, RuntimeError, ApplicationNotFoundError):
pass
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe, *args):
if isinstance(dupe, IPhoto):
try:
a = app('iPhoto')
album = a.photo_library_album()
if album is None:
msg = "There are communication problems with iPhoto. Try opening iPhoto first, it might solve it."
raise EnvironmentError(msg)
[photo] = album.photos[its.image_path == str(dupe.path)]()
a.remove(photo, timeout=0)
except ValueError:
msg = "Could not find photo '{}' in iPhoto Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
if isinstance(dupe, AperturePhoto):
try:
a = app('Aperture')
# I'm flying blind here. In my own test library, all photos are in an album with the
# id "LibraryFolder", so I'm going to guess that it's the case at least most of the
# time. As a safeguard, if we don't find any library with that id, we'll use the
# first album.
# Now, about deleting: All attempts I've made at sending photos to trash failed,
# even with normal applescript. So, what we're going to do here is to create a
# "dupeGuru Trash" project and tell the user to manually send those photos to trash.
libraries = a.libraries()
library = first(l for l in libraries if l.id == 'LibraryFolder')
if library is None:
library = libraries[0]
trash_project = a.projects["dupeGuru Trash"]
if trash_project.exists():
trash_project = trash_project()
else:
trash_project = library.make(new=k.project, with_properties={k.name: "dupeGuru Trash"})
[photo] = library.image_versions[its.id == dupe.db_id]()
photo.move(to=trash_project)
except (IndexError, ValueError):
msg = "Could not find photo '{}' in Aperture Library".format(str(dupe.path))
raise EnvironmentError(msg)
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
else:
DupeGuruBase._do_delete_dupe(self, dupe, *args)
def _create_file(self, path):
if (self.directories.iphoto_libpath is not None) and (path in self.directories.iphoto_libpath[:-1]):
if not hasattr(self, 'path2iphoto'):
photos = get_iphoto_pictures(self.directories.iphoto_libpath)
self.path2iphoto = {p.path: p for p in photos}
return self.path2iphoto.get(path)
if (self.directories.aperture_libpath is not None) and (path in self.directories.aperture_libpath[:-1]):
if not hasattr(self, 'path2aperture'):
photos = get_aperture_pictures(self.directories.aperture_libpath)
self.path2aperture = {p.path: p for p in photos}
return self.path2aperture.get(path)
return DupeGuruBase._create_file(self, path)
def _job_completed(self, jobid, exc):
DupeGuruBase._job_completed(self, jobid, exc)
if jobid == JobType.Load:
if hasattr(self, 'path2iphoto'):
del self.path2iphoto
if hasattr(self, 'path2aperture'):
del self.path2aperture
if jobid == JobType.Delete and self.deleted_aperture_photos:
msg = tr("Deleted Aperture photos were sent to a project called \"dupeGuru Trash\".")
self.view.show_message(msg)
def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, (IPhoto, AperturePhoto)):
copy = True
return DupeGuruBase.copy_or_move(self, dupe, copy, destination, dest_type)
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 start_scanning(self):
if self.directories.has_iphoto_path():
try:
app('iPhoto')
except ApplicationNotFoundError:
self.view.show_message(tr("The iPhoto application couldn't be found."))
return
DupeGuruBase.start_scanning(self)
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuruPE)
def clearPictureCache(self):
self.model.scanner.clear_picture_cache()
#---Information
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 setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.FuzzyBlock,
ScanType.ExifTimestamp,
][scan_type]
except IndexError:
pass
def setMatchScaled_(self, match_scaled: bool):
self.model.scanner.match_scaled = match_scaled
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.threshold = percentage

103
cocoa/inter/app_se.py Normal file
View File

@@ -0,0 +1,103 @@
# Created By: Virgil Dupras
# Created On: 2009-05-24
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
import logging
import os.path as op
from hscommon import io
from hscommon.path import Path
from cocoa import proxy
from core.scanner import ScanType
from core import fs
from core.directories import Directories as DirectoriesBase, DirectoryState
from core_se.app import DupeGuru as DupeGuruBase
from core_se.fs import File
from .app import PyDupeGuruBase
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
def can_handle(cls, path):
return not io.islink(path) and io.isdir(path) 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 __init__(self):
DirectoriesBase.__init__(self, fileclasses=[Bundle, File])
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:
for folder in DirectoriesBase._get_folders(self, from_folder, j):
yield folder
@staticmethod
def get_subfolders(path):
result = DirectoriesBase.get_subfolders(path)
return [p for p in result if not is_bundle(str(p))]
class DupeGuru(DupeGuruBase):
def __init__(self, view, appdata):
appdata = op.join(appdata, 'dupeGuru')
DupeGuruBase.__init__(self, view, appdata)
self.directories = Directories()
class PyDupeGuru(PyDupeGuruBase):
def __init__(self):
self._init(DupeGuru)
#---Properties
def setMinMatchPercentage_(self, percentage: int):
self.model.scanner.min_match_percentage = int(percentage)
def setScanType_(self, scan_type: int):
try:
self.model.scanner.scan_type = [
ScanType.Filename,
ScanType.Contents,
ScanType.Folders,
][scan_type]
except IndexError:
pass
def setWordWeighting_(self, words_are_weighted: bool):
self.model.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words: bool):
self.model.scanner.match_similar_words = match_similar_words
def setSizeThreshold_(self, size_threshold: int):
self.model.scanner.size_threshold = size_threshold

View File

@@ -0,0 +1,33 @@
# Created On: 2012-05-30
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
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
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()

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,18 @@
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()
# 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()

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)

View File

@@ -0,0 +1,9 @@
from objp.util import pyref
from cocoa.inter import PyGUIObject
class PyProblemDialog(PyGUIObject):
def problemTable(self) -> pyref:
return self.model.problem_table
def revealSelected(self):
self.model.reveal_selected_dupe()

View File

@@ -0,0 +1,50 @@
from objp.util import dontwrap
from cocoa.inter import PyTable, TableView
class ResultTableView(TableView):
def invalidateMarkings(self): pass
class PyResultTable(PyTable):
def powerMarkerMode(self) -> bool:
return self.model.power_marker
def setPowerMarkerMode_(self, value: bool):
self.model.power_marker = value
def deltaValuesMode(self) -> bool:
return self.model.delta_values
def setDeltaValuesMode_(self, value: bool):
self.model.delta_values = value
def valueForRow_column_(self, row_index: int, column: str) -> object:
return self.model.get_row_value(row_index, column)
def isDeltaAtRow_column_(self, row_index: int, column: str) -> bool:
row = self.model[row_index]
return row.is_cell_delta(column)
def renameSelected_(self, newname: str) -> bool:
return self.model.rename_selected(newname)
def sortBy_ascending_(self, key: str, asc: bool):
self.model.sort(key, asc)
def markSelected(self):
self.model.app.toggle_selected_mark_state()
def removeSelected(self):
self.model.app.remove_selected()
def selectedDupeCount(self) -> int:
return self.model.selected_dupe_count
def pathAtIndex_(self, index: int) -> str:
row = self.model[index]
return str(row._dupe.path)
# python --> cocoa
@dontwrap
def invalidate_markings(self):
self.callback.invalidateMarkings()

View File

@@ -0,0 +1,9 @@
from cocoa.inter import PyGUIObject, GUIObjectView
class StatsLabelView(GUIObjectView):
pass
class PyStatsLabel(PyGUIObject):
def display(self) -> str:
return self.model.display

282
cocoa/inter/tunes.py Normal file
View File

@@ -0,0 +1,282 @@
# Taken from https://github.com/abarnert/itunesterms
version = 1.1
path = '/Applications/iTunes.app'
classes = \
[('print_settings', b'pset'),
('application', b'capp'),
('artwork', b'cArt'),
('audio_CD_playlist', b'cCDP'),
('audio_CD_track', b'cCDT'),
('browser_window', b'cBrW'),
('device_playlist', b'cDvP'),
('device_track', b'cDvT'),
('encoder', b'cEnc'),
('EQ_preset', b'cEQP'),
('EQ_window', b'cEQW'),
('file_track', b'cFlT'),
('folder_playlist', b'cFoP'),
('item', b'cobj'),
('library_playlist', b'cLiP'),
('playlist', b'cPly'),
('playlist_window', b'cPlW'),
('radio_tuner_playlist', b'cRTP'),
('shared_track', b'cShT'),
('source', b'cSrc'),
('track', b'cTrk'),
('URL_track', b'cURT'),
('user_playlist', b'cUsP'),
('visual', b'cVis'),
('window', b'cwin')]
enums = \
[('track_listing', b'kTrk'),
('album_listing', b'kAlb'),
('cd_insert', b'kCDi'),
('standard', b'lwst'),
('detailed', b'lwdt'),
('stopped', b'kPSS'),
('playing', b'kPSP'),
('paused', b'kPSp'),
('fast_forwarding', b'kPSF'),
('rewinding', b'kPSR'),
('off', b'kRpO'),
('one', b'kRp1'),
('all', b'kAll'),
('small', b'kVSS'),
('medium', b'kVSM'),
('large', b'kVSL'),
('library', b'kLib'),
('iPod', b'kPod'),
('audio_CD', b'kACD'),
('MP3_CD', b'kMCD'),
('device', b'kDev'),
('radio_tuner', b'kTun'),
('shared_library', b'kShd'),
('unknown', b'kUnk'),
('albums', b'kSrL'),
('artists', b'kSrR'),
('composers', b'kSrC'),
('displayed', b'kSrV'),
('songs', b'kSrS'),
('none', b'kNon'),
('Books', b'kSpA'),
('folder', b'kSpF'),
('Genius', b'kSpG'),
('iTunes_U', b'kSpU'),
('Library', b'kSpL'),
('Movies', b'kSpI'),
('Music', b'kSpZ'),
('Party_Shuffle', b'kSpS'),
('Podcasts', b'kSpP'),
('Purchased_Music', b'kSpM'),
('TV_Shows', b'kSpT'),
('movie', b'kVdM'),
('music_video', b'kVdV'),
('TV_show', b'kVdT'),
('user', b'kRtU'),
('computed', b'kRtC')]
properties = \
[('copies', b'lwcp'),
('collating', b'lwcl'),
('starting_page', b'lwfp'),
('ending_page', b'lwlp'),
('pages_across', b'lwla'),
('pages_down', b'lwld'),
('error_handling', b'lweh'),
('requested_print_time', b'lwqt'),
('printer_features', b'lwpf'),
('fax_number', b'faxn'),
('target_printer', b'trpr'),
('current_encoder', b'pEnc'),
('current_EQ_preset', b'pEQP'),
('current_playlist', b'pPla'),
('current_stream_title', b'pStT'),
('current_stream_URL', b'pStU'),
('current_track', b'pTrk'),
('current_visual', b'pVis'),
('EQ_enabled', b'pEQ '),
('fixed_indexing', b'pFix'),
('frontmost', b'pisf'),
('full_screen', b'pFSc'),
('name', b'pnam'),
('mute', b'pMut'),
('player_position', b'pPos'),
('player_state', b'pPlS'),
('selection', b'sele'),
('sound_volume', b'pVol'),
('version', b'vers'),
('visuals_enabled', b'pVsE'),
('visual_size', b'pVSz'),
('data', b'pPCT'),
('description', b'pDes'),
('downloaded', b'pDlA'),
('format', b'pFmt'),
('kind', b'pKnd'),
('raw_data', b'pRaw'),
('artist', b'pArt'),
('compilation', b'pAnt'),
('composer', b'pCmp'),
('disc_count', b'pDsC'),
('disc_number', b'pDsN'),
('genre', b'pGen'),
('year', b'pYr '),
('location', b'pLoc'),
('minimized', b'pMin'),
('view', b'pPly'),
('band_1', b'pEQ1'),
('band_2', b'pEQ2'),
('band_3', b'pEQ3'),
('band_4', b'pEQ4'),
('band_5', b'pEQ5'),
('band_6', b'pEQ6'),
('band_7', b'pEQ7'),
('band_8', b'pEQ8'),
('band_9', b'pEQ9'),
('band_10', b'pEQ0'),
('modifiable', b'pMod'),
('preamp', b'pEQA'),
('update_tracks', b'pUTC'),
('container', b'ctnr'),
('id', b'ID '),
('index', b'pidx'),
('persistent_ID', b'pPIS'),
('duration', b'pDur'),
('parent', b'pPlP'),
('shuffle', b'pShf'),
('size', b'pSiz'),
('song_repeat', b'pRpt'),
('special_kind', b'pSpK'),
('time', b'pTim'),
('visible', b'pvis'),
('capacity', b'capa'),
('free_space', b'frsp'),
('album', b'pAlb'),
('album_artist', b'pAlA'),
('album_rating', b'pAlR'),
('album_rating_kind', b'pARk'),
('bit_rate', b'pBRt'),
('bookmark', b'pBkt'),
('bookmarkable', b'pBkm'),
('bpm', b'pBPM'),
('category', b'pCat'),
('comment', b'pCmt'),
('database_ID', b'pDID'),
('date_added', b'pAdd'),
('enabled', b'enbl'),
('episode_ID', b'pEpD'),
('episode_number', b'pEpN'),
('EQ', b'pEQp'),
('finish', b'pStp'),
('gapless', b'pGpl'),
('grouping', b'pGrp'),
('long_description', b'pLds'),
('lyrics', b'pLyr'),
('modification_date', b'asmo'),
('played_count', b'pPlC'),
('played_date', b'pPlD'),
('podcast', b'pTPc'),
('rating', b'pRte'),
('rating_kind', b'pRtk'),
('release_date', b'pRlD'),
('sample_rate', b'pSRt'),
('season_number', b'pSeN'),
('shufflable', b'pSfa'),
('skipped_count', b'pSkC'),
('skipped_date', b'pSkD'),
('show', b'pShw'),
('sort_album', b'pSAl'),
('sort_artist', b'pSAr'),
('sort_album_artist', b'pSAA'),
('sort_name', b'pSNm'),
('sort_composer', b'pSCm'),
('sort_show', b'pSSN'),
('start', b'pStr'),
('track_count', b'pTrC'),
('track_number', b'pTrN'),
('unplayed', b'pUnp'),
('video_kind', b'pVdK'),
('volume_adjustment', b'pAdj'),
('address', b'pURL'),
('shared', b'pShr'),
('smart', b'pSmt'),
('bounds', b'pbnd'),
('closeable', b'hclb'),
('collapseable', b'pWSh'),
('collapsed', b'wshd'),
('position', b'ppos'),
('resizable', b'prsz'),
('zoomable', b'iszm'),
('zoomed', b'pzum')]
elements = \
[('artworks', b'cArt'),
('audio_CD_playlists', b'cCDP'),
('audio_CD_tracks', b'cCDT'),
('browser_windows', b'cBrW'),
('device_playlists', b'cDvP'),
('device_tracks', b'cDvT'),
('encoders', b'cEnc'),
('EQ_presets', b'cEQP'),
('EQ_windows', b'cEQW'),
('file_tracks', b'cFlT'),
('folder_playlists', b'cFoP'),
('items', b'cobj'),
('library_playlists', b'cLiP'),
('playlists', b'cPly'),
('playlist_windows', b'cPlW'),
('radio_tuner_playlists', b'cRTP'),
('shared_tracks', b'cShT'),
('sources', b'cSrc'),
('tracks', b'cTrk'),
('URL_tracks', b'cURT'),
('user_playlists', b'cUsP'),
('visuals', b'cVis'),
('windows', b'cwin'),
('application', b'capp'),
('print_settings', b'pset')]
commands = \
[('set', b'coresetd', [('to', b'data')]),
('exists', b'coredoex', []),
('move', b'coremove', [('to', b'insh')]),
('subscribe', b'hookpSub', []),
('playpause', b'hookPlPs', []),
('download', b'hookDwnl', []),
('close', b'coreclos', []),
('open', b'aevtodoc', []),
('open_location', b'GURLGURL', []),
('quit', b'aevtquit', []),
('pause', b'hookPaus', []),
('make',
'corecrel',
[('new', b'kocl'), ('at', b'insh'), ('with_properties', b'prdt')]),
('duplicate', b'coreclon', [('to', b'insh')]),
('print_',
'aevtpdoc',
[('print_dialog', b'pdlg'),
('with_properties', b'prdt'),
('kind', b'pKnd'),
('theme', b'pThm')]),
('add', b'hookAdd ', [('to', b'insh')]),
('rewind', b'hookRwnd', []),
('play', b'hookPlay', [('once', b'POne')]),
('run', b'aevtoapp', []),
('resume', b'hookResu', []),
('updatePodcast', b'hookUpd1', []),
('next_track', b'hookNext', []),
('stop', b'hookStop', []),
('search', b'hookSrch', [('for_', b'pTrm'), ('only', b'pAre')]),
('updateAllPodcasts', b'hookUpdp', []),
('update', b'hookUpdt', []),
('previous_track', b'hookPrev', []),
('fast_forward', b'hookFast', []),
('count', b'corecnte', [('each', b'kocl')]),
('reveal', b'hookRevl', []),
('convert', b'hookConv', []),
('eject', b'hookEjct', []),
('back_track', b'hookBack', []),
('refresh', b'hookRfrs', []),
('delete', b'coredelo', [])]

16
cocoa/me/AppDelegate.h Normal file
View File

@@ -0,0 +1,16 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import <Cocoa/Cocoa.h>
#import "AppDelegateBase.h"
#import "ResultWindow.h"
#import "PyDupeGuru.h"
@interface AppDelegate : AppDelegateBase {}
- (void)removeDeadTracks;
@end

68
cocoa/me/AppDelegate.m Normal file
View File

@@ -0,0 +1,68 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import "AppDelegate.h"
#import "ProgressController.h"
#import "Utils.h"
#import "ValueTransformers.h"
#import "Dialogs.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
#import "ResultWindow.h"
#import "Consts.h"
@implementation AppDelegate
+ (NSDictionary *)defaultPreferences
{
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithDictionary:[super defaultPreferences]];
[d setObject:i2n(3) forKey:@"scanType"];
[d setObject:i2n(80) forKey:@"minMatchPercentage"];
[d setObject:b2n(NO) forKey:@"wordWeighting"];
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[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"];
return d;
}
- (id)init
{
self = [super init];
NSMutableIndexSet *i = [NSMutableIndexSet indexSetWithIndex:4];
[i addIndex:5];
VTIsIntIn *vtScanTypeIsNotContent = [[[VTIsIntIn alloc] initWithValues:i reverse:YES] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsNotContent forName:@"vtScanTypeIsNotContent"];
VTIsIntIn *vtScanTypeIsTag = [[[VTIsIntIn alloc] initWithValues:[NSIndexSet indexSetWithIndex:3] reverse:NO] autorelease];
[NSValueTransformer setValueTransformer:vtScanTypeIsTag forName:@"vtScanTypeIsTag"];
_directoryPanel = nil;
return self;
}
- (NSString *)homepageURL
{
return @"http://www.hardcoded.net/dupeguru_me/";
}
- (ResultWindowBase *)createResultWindow
{
return [[ResultWindow alloc] initWithParentApp:self];
}
- (DirectoryPanel *)createDirectoryPanel
{
return [[DirectoryPanelME alloc] initWithParentApp:self];
}
- (void)removeDeadTracks
{
[(ResultWindow *)[self resultWindow] removeDeadTracks];
}
@end

11
cocoa/me/Consts.h Normal file
View File

@@ -0,0 +1,11 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import "../base/Consts.h"
#define jobScanDeadTracks @"jobScanDeadTracks"

13
cocoa/me/DetailsPanel.h Normal file
View File

@@ -0,0 +1,13 @@
/*
Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
This software is licensed under the "BSD" License as described in the "LICENSE" file,
which should be included with this package. The terms are also available at
http://www.hardcoded.net/licenses/bsd_license
*/
#import <Cocoa/Cocoa.h>
#import "DetailsPanelBase.h"
@interface DetailsPanel : DetailsPanelBase
@end

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