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

Compare commits

..

203 Commits

Author SHA1 Message Date
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
216 changed files with 11247 additions and 7465 deletions

23
.hgtags
View File

@@ -8,3 +8,26 @@ cbcf9c80fee4c908ef2efbf1c143c9e47676c9b2 pe1.8.0
61c4101851bdea3cb37dfb76f0d404c78c7c594c se2.9.1
0e923897a3389331d4ab3debbc40b8dd616199d9 pe1.8.1
2c454eca9ebe93b6cf34916068f828a6a39e3eaf me5.7.1
19e40bab20521d4256acf325dba9b32e95e135c5 pe1.8.2
7b7c5a66ebee4e4b8125330d24fe9ce1a070ff25 se2.9.2
1cef6d39855f85d4be728646bc78b860e6d4e398 pe1.8.3
90ed56ee602666db2f267f73eac6f824347039b5 me5.7.2
4c3cb1e671a333eabde1151c7c6ffb3609cab025 pe1.8.4
0a71306434bca51bea9a5d5ae54fe1bf0e4900d8 pe1.8.5
556baf4a410779e9bbf43129de133e4c4b26d679 pe1.8.6
9149024283959a50fe9a47a5f175b905d1672c19 se2.10.0
388a7e5aef6385e515189f4a15b4c4fed3ae2fcf me5.8.0
27501167e3b9262ecb60c967941294f36d77eb25 pe1.9.0
cb0a860430bacd712820bce426bcf47a4135efe1 se2.10.1
cb0a860430bacd712820bce426bcf47a4135efe1 se2.10.1
f71d405e62badcfdc1b037facaac043cece40ee5 se2.10.1
3742e83edd9eadf44e1a501859f5e2462b1ef6fd me5.8.1
724ff565dd785fb739774588c6ee652cfc0612d5 pe1.9.1
634b66415c6529f46ae4f837318027cc9d70c3b5 before-py3k
2b67955db2b0580a8b0854dc918b6ab0d1fa3b88 se2.11.0
b56fe4dd8c95bca270b078a09e86848df77e2b2d me5.9.0
618a7365457d56fdc6920c70843a244762e2ea00 pe1.10.0
95b3a4b564c6222b414f2b40182dde2bd6d0e8a4 me5.9.1
9735a5218d2b5b3b1e1dfe17f2f874177cf8f61c se2.11.1
dbfee3ee2fa5cbb9e7ab36570659c17cd5b8561f se2.12.0
d3fe0d0dcda1e0bf1100d02f117503d3bf6baacf me5.10.0

27
README
View File

@@ -12,9 +12,7 @@ This package contains the source for dupeGuru. To learns how to build it, refer
There are also other sub-folder that comes from external repositories (automatically checked out
with svn:externals):
- hsutil: A collection of helpers used across HS applications.
- hsdocgen: An ad-hoc document generation used across HS project (used for help files)
- hsmedia: A library to read audio file metadata, used in dupeGuru ME.
- 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.
@@ -26,26 +24,28 @@ Before being able to build dupeGuru, a few dependencies have to be installed:
General dependencies
-----
- Python 2.6 (http://www.python.org)
- Mako, to generate help files. (http://www.makotemplates.org/)
- Python 3.1 (http://www.python.org)
- Send2Trash3k (http://hg.hardcoded.net/send2trash3k)
- hsutil3k (http://hg.hardcoded.net/hsutil3k)
- hsaudiotag3k (for ME) (http://hg.hardcoded.net/hsaudiotag3k)
- Markdown, to generate help files. (http://pypi.python.org/pypi/Markdown)
- PyYaml, for help files and the build system. (http://pyyaml.org/)
- Nose, to run unit tests. (http://somethingaboutorange.com/mrl/projects/nose/)
- py.test, to run unit tests. (http://codespeak.net/py/dist/test/)
OS X prerequisites
-----
- XCode 3.1 (http://developer.apple.com/TOOLS/xcode/)
- Sparkle (http://sparkle.andymatuschak.org/)
- PyObjC 2.2. (http://pyobjc.sourceforge.net/)
- py2app (http://svn.pythonmac.org/py2app/py2app/trunk/doc/index.html)
- PyObjC 2.3. (http://pyobjc.sourceforge.net/)
- py2app 0.5.4 (http://svn.pythonmac.org/py2app/py2app/trunk/doc/index.html)
Windows prerequisites
---
- Visual Studio 2008 (Express is enough) is needed to build C extensions. (http://www.microsoft.com/Express/)
- PyQt 4.6 (http://www.riverbankcomputing.co.uk/news)
- Python Imaging Library for dupeGuru PE. (http://www.pythonware.com/products/pil/)
- PyInstaller, if you want to build a exe. You don't need it if you just want to run dupeGuru. (http://www.pyinstaller.org/)
- PyQt 4.7.5 (http://www.riverbankcomputing.co.uk/news)
- cx_Freeze, if you want to build a exe. You don't need it if you just want to run dupeGuru. (http://cx-freeze.sourceforge.net/)
- Advanced Installer, if you want to build the installer file. (http://www.advancedinstaller.com/)
Building dupeGuru
@@ -65,8 +65,3 @@ Then, just build the thing and then run it with:
If you want to create ready-to-upload package, run:
python package.py
64-bit on OS X
---
The "release" configuration of dupeGuru's XCode project build with archs "i386 x86_64 ppc". However there are currently problems with py2app and 64 bit. If you want to correctly build 64-bit apps, refer to http://www.hardcoded.net/articles/building-64-bit-pyobjc-applications-with-py2app.htm .

110
build.py
View File

@@ -13,50 +13,28 @@ import os.path as op
import shutil
from setuptools import setup
from distutils.extension import Extension
import yaml
from hsdocgen import generate_help, filters
from hsutil.build import add_to_pythonpath, print_and_do, build_all_qt_ui, copy_packages
from hscommon import helpgen
from hscommon.build import add_to_pythonpath, print_and_do, build_all_qt_ui, copy_packages
def main():
conf = yaml.load(open('conf.yaml'))
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
print "Building dupeGuru {0} with UI {1}".format(edition.upper(), ui)
if dev:
print "Building in Dev mode"
add_to_pythonpath('.')
print "Generating Help"
windows = sys.platform == 'win32'
tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}")
help_dir = 'help_{0}'.format(edition)
dest_dir = 'dupeguru_{0}_help'.format(edition) if edition != 'se' else 'dupeguru_help'
help_basepath = op.abspath(help_dir)
help_destpath = op.abspath(op.join(help_dir, dest_dir))
generate_help.main(help_basepath, help_destpath, force_render=not dev, tix=tix, windows=windows)
print "Building dupeGuru"
if edition == 'pe':
os.chdir('core_pe')
os.system('python gen.py')
os.chdir('..')
if ui == 'cocoa':
def build_cocoa(edition, dev, help_destpath):
if not dev:
print "Building help index"
print("Building help index")
os.system('open -a /Developer/Applications/Utilities/Help\\ Indexer.app {0}'.format(help_destpath))
print "Building dg_cocoa.plugin"
print("Building dg_cocoa.plugin")
if op.exists('build'):
shutil.rmtree('build')
os.mkdir('build')
if not dev:
specific_packages = {
'se': ['core_se'],
'me': ['core_me', 'hsmedia'],
'me': ['core_me'],
'pe': ['core_pe'],
}[edition]
copy_packages(['core', 'hsutil'] + specific_packages, 'build')
copy_packages(['core', 'hscommon'] + specific_packages, 'build')
cocoa_project_path = 'cocoa/{0}'.format(edition)
shutil.copy(op.join(cocoa_project_path, 'dg_cocoa.py'), 'build')
os.chdir('build')
@@ -77,7 +55,7 @@ def main():
pthpath = op.join(pluginpath, 'Contents/Resources/dev.pth')
open(pthpath, 'w').write(op.abspath('.'))
os.chdir(cocoa_project_path)
print "Building the XCode project"
print("Building the XCode project")
args = []
if dev:
args.append('-configuration dev')
@@ -86,10 +64,74 @@ def main():
args = ' '.join(args)
os.system('xcodebuild {0}'.format(args))
os.chdir('..')
def build_qt(edition, dev):
print("Building Qt stuff")
build_all_qt_ui(op.join('qtlib', 'ui'))
build_all_qt_ui(op.join('qt', 'base'))
build_all_qt_ui(op.join('qt', edition))
print_and_do("pyrcc4 -py3 {0} > {1}".format(op.join('qt', 'base', 'dg.qrc'), op.join('qt', 'base', 'dg_rc.py')))
def build_pe_modules(ui):
def move(src, dst):
if not op.exists(src):
return
if op.exists(dst):
os.remove(dst)
print('Moving %s --> %s' % (src, dst))
os.rename(src, dst)
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('_block.so', op.join('core_pe', '_block.so'))
move('_block.pyd', op.join('core_pe', '_block.pyd'))
move('_block_osx.so', op.join('core_pe', '_block_osx.so'))
move('_cache.so', op.join('core_pe', '_cache.so'))
move('_cache.pyd', op.join('core_pe', '_cache.pyd'))
move('_block_qt.so', op.join('qt', 'pe', '_block_qt.so'))
move('_block_qt.pyd', op.join('qt', 'pe', '_block_qt.pyd'))
def main():
conf = yaml.load(open('conf.yaml'))
edition = conf['edition']
ui = conf['ui']
dev = conf['dev']
print("Building dupeGuru {0} with UI {1}".format(edition.upper(), ui))
if dev:
print("Building in Dev mode")
add_to_pythonpath('.')
print("Generating Help")
windows = sys.platform == 'win32'
profile = 'win_en' if windows else 'osx_en'
help_dir = 'help_{0}'.format(edition)
dest_dir = 'dupeguru_{0}_help'.format(edition) if edition != 'se' else 'dupeguru_help'
help_basepath = op.abspath(help_dir)
help_destpath = op.abspath(op.join(help_dir, dest_dir))
helpgen.gen(help_basepath, help_destpath, profile=profile)
print("Building dupeGuru")
if edition == 'pe':
build_pe_modules(ui)
if ui == 'cocoa':
build_cocoa(edition, dev, help_destpath)
elif ui == 'qt':
os.chdir(op.join('qt', edition))
os.system('python gen.py')
os.chdir(op.join('..', '..'))
build_qt(edition, dev)
if __name__ == '__main__':
main()

View File

@@ -8,11 +8,6 @@ http://www.hardcoded.net/licenses/hs_license
#import <Cocoa/Cocoa.h>
/* 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"
@@ -22,5 +17,3 @@ http://www.hardcoded.net/licenses/hs_license
#define jobCopy @"job_copy"
#define jobMove @"job_move"
#define jobDelete @"job_delete"
#define DEMO_MAX_ACTION_COUNT 10

View File

@@ -7,16 +7,16 @@ http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "HSWindowController.h"
#import "PyApp.h"
#import "PyDetailsPanel.h"
@interface DetailsPanel : NSWindowController
@interface DetailsPanel : HSWindowController
{
IBOutlet NSTableView *detailsTable;
PyDetailsPanel *py;
}
- (id)initWithPy:(PyApp *)aPy;
- (PyDetailsPanel *)py;
- (void)toggleVisibility;

View File

@@ -12,19 +12,23 @@ http://www.hardcoded.net/licenses/hs_license
@implementation DetailsPanel
- (id)initWithPy:(PyApp *)aPy
{
self = [super initWithWindowNibName:@"DetailsPanel"];
self = [super initWithNibName:@"DetailsPanel" pyClassName:@"PyDetailsPanel" pyParent:aPy];
[self window]; //So the detailsTable is initialized.
Class pyClass = [Utils classNamed:@"PyDetailsPanel"];
py = [[pyClass alloc] initWithCocoa:self pyParent:aPy];
[self connect];
return self;
}
- (void)dealloc
{
[py release];
[self disconnect];
[super dealloc];
}
- (PyDetailsPanel *)py
{
return (PyDetailsPanel *)py;
}
- (void)refreshDetails
{
[detailsTable reloadData];
@@ -44,12 +48,12 @@ http://www.hardcoded.net/licenses/hs_license
/* NSTableView Delegate */
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [py numberOfRows];
return [[self py] numberOfRows];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
return [py valueForColumn:[column identifier] row:row];
return [[self py] valueForColumn:[column identifier] row:row];
}
/* Python --> Cocoa */

View File

@@ -0,0 +1,16 @@
/*
Copyright 2010 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 "HSOutline.h"
#import "PyDirectoryOutline.h"
@interface DirectoryOutline : HSOutline {}
- (id)initWithPyParent:(id)aPyParent view:(HSOutlineView *)aOutlineView;
- (PyDirectoryOutline *)py;
@end;

View File

@@ -0,0 +1,84 @@
/*
Copyright 2010 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 "DirectoryOutline.h"
@implementation DirectoryOutline
- (id)initWithPyParent:(id)aPyParent view:(HSOutlineView *)aOutlineView
{
self = [super initWithPyClassName:@"PyDirectoryOutline" pyParent:aPyParent view:aOutlineView];
[outlineView registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
[self connect];
return self;
}
- (void)dealloc
{
[self disconnect];
[super dealloc];
}
- (PyDirectoryOutline *)py
{
return (PyDirectoryOutline *)py;
}
/* 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 *filenames = [pboard propertyListForType:NSFilenamesPboardType];
if (!(sourceDragMask & NSDragOperationLink))
return NO;
for (NSString *filename in filenames) {
[[self py] addDirectory:filename];
}
}
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:[outlineView 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

@@ -8,31 +8,23 @@ http://www.hardcoded.net/licenses/hs_license
#import <Cocoa/Cocoa.h>
#import "RecentDirectories.h"
#import "Outline.h"
#import "HSOutlineView.h"
#import "DirectoryOutline.h"
#import "PyDupeGuru.h"
@interface DirectoryOutline : OutlineView
{
}
@end
@protocol DirectoryOutlineDelegate
- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory;
@end
@interface DirectoryPanel : NSWindowController
{
IBOutlet NSPopUpButton *addButtonPopUp;
IBOutlet DirectoryOutline *directories;
IBOutlet HSOutlineView *outlineView;
IBOutlet NSButton *removeButton;
PyDupeGuruBase *_py;
RecentDirectories *_recentDirectories;
DirectoryOutline *outline;
}
- (id)initWithParentApp:(id)aParentApp;
- (IBAction)askForDirectory:(id)sender;
- (IBAction)changeDirectoryState:(id)sender;
- (IBAction)popupAddDirectoryMenu:(id)sender;
- (IBAction)removeSelectedDirectory:(id)sender;
- (IBAction)toggleVisible:(id)sender;

View File

@@ -11,48 +11,6 @@ http://www.hardcoded.net/licenses/hs_license
#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 DirectoryPanel
- (id)initWithParentApp:(id)aParentApp
{
@@ -61,23 +19,19 @@ http://www.hardcoded.net/licenses/hs_license
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 (NSInteger i=0;i<[[cell itemArray] count];i++)
{
NSMenuItem *mi = [[cell itemArray] objectAtIndex:i];
[mi setTarget:self];
[mi setAction:@selector(changeDirectoryState:)];
[mi setTag:i];
}
outline = [[DirectoryOutline alloc] initWithPyParent:_py view:outlineView];
[self refreshRemoveButtonText];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directorySelectionChanged:) name:NSOutlineViewSelectionDidChangeNotification object:directories];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(directorySelectionChanged:)
name:NSOutlineViewSelectionDidChangeNotification object:outlineView];
return self;
}
- (void)dealloc
{
[outline release];
[super dealloc];
}
/* Actions */
- (IBAction)askForDirectory:(id)sender
@@ -85,23 +39,14 @@ http://www.hardcoded.net/licenses/hs_license
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:YES];
[op setAllowsMultipleSelection:NO];
[op setAllowsMultipleSelection:YES];
[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];
if ([op runModal] == NSOKButton) {
for (NSString *directory in [op filenames]) {
[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
@@ -125,21 +70,17 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)removeSelectedDirectory:(id)sender
{
[[self window] makeKeyAndOrderFront:nil];
if ([directories selectedRow] < 0)
if ([outlineView selectedRow] < 0)
return;
OVNode *node = [directories itemAtRow:[directories selectedRow]];
if ([node level] == 1)
{
[_py removeDirectory:i2n([node index])];
[directories reloadData];
NSIndexPath *path = [outline selectedIndexPath];
NSInteger state = [outline intProperty:@"state" valueAtPath:path];
if (([path length] == 1) && (state != 2)) {
[_py removeDirectory:i2n([path indexAtPosition:0])];
}
else
{
NSInteger state = n2i([[node buffer] objectAtIndex:1]);
else {
NSInteger newState = state == 2 ? 0 : 2; // If excluded, put it back
[_py setDirectory:p2a([node indexPath]) state:i2n(newState)];
[node resetAllBuffers];
[directories display];
[outline setIntProperty:@"state" value:newState atPath:path];
[outlineView display];
}
[self refreshRemoveButtonText];
}
@@ -150,70 +91,36 @@ http://www.hardcoded.net/licenses/hs_license
}
/* Public */
- (void)addDirectory:(NSString *)directory
{
NSInteger r = [[_py addDirectory:directory] intValue];
if (r)
{
NSString *m;
switch (r)
{
case 1:
{
m = @"This directory already is in the list.";
break;
if (r) {
NSString *m = @"";
if (r == 1) {
m = @"'%@' already is in the list.";
}
case 2:
{
m = @"This directory does not exist.";
break;
else if (r == 2) {
m = @"'%@' does not exist.";
}
[Dialogs showMessage:[NSString stringWithFormat:m,directory]];
}
[Dialogs showMessage:m];
}
[directories reloadData];
[_recentDirectories addDirectory:directory];
[[self window] makeKeyAndOrderFront:nil];
}
- (void)refreshRemoveButtonText
{
if ([directories selectedRow] < 0)
{
if ([outlineView selectedRow] < 0) {
[removeButton setEnabled:NO];
return;
}
[removeButton setEnabled:YES];
OVNode *node = [directories itemAtRow:[directories selectedRow]];
NSInteger state = n2i([[node buffer] objectAtIndex:1]);
NSInteger state = [outline intProperty:@"state" valueAtPath:[outline selectedIndexPath]];
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;
NSInteger 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;

View File

@@ -0,0 +1,25 @@
/*
Copyright 2010 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 "HSWindowController.h"
#import "PyApp.h"
#import "PyProblemDialog.h"
#import "HSTable.h"
@interface ProblemDialog : HSWindowController
{
IBOutlet NSTableView *problemTableView;
HSTable *problemTable;
}
- (id)initWithPy:(PyApp *)aPy;
- (PyProblemDialog *)py;
- (IBAction)revealSelected:(id)sender;
@end

View File

@@ -0,0 +1,40 @@
/*
Copyright 2010 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 "ProblemDialog.h"
#import "Utils.h"
@implementation ProblemDialog
- (id)initWithPy:(PyApp *)aPy
{
self = [super initWithNibName:@"ProblemDialog" pyClassName:@"PyProblemDialog" pyParent:aPy];
[self window]; //So the detailsTable is initialized.
problemTable = [[HSTable alloc] initWithPyClassName:@"PyProblemTable" pyParent:[self py] view:problemTableView];
[self connect];
[problemTable connect];
return self;
}
- (void)dealloc
{
[problemTable disconnect];
[self disconnect];
[problemTable release];
[super dealloc];
}
- (PyProblemDialog *)py
{
return (PyProblemDialog *)py;
}
- (IBAction)revealSelected:(id)sender
{
[[self py] revealSelected];
}
@end

View File

@@ -7,9 +7,9 @@ http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "PyGUI.h"
@interface PyDetailsPanel : NSObject
- (id)initWithCocoa:(id)cocoa pyParent:(id)pyParent;
@interface PyDetailsPanel : PyGUI
- (NSInteger)numberOfRows;
- (id)valueForColumn:(NSString *)column row:(NSInteger)row;
@end

View File

@@ -0,0 +1,14 @@
/*
Copyright 2010 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 "PyOutline.h"
@interface PyDirectoryOutline : PyOutline
- (void)addDirectory:(NSString *)directoryPath;
@end

View File

@@ -13,53 +13,45 @@ http://www.hardcoded.net/licenses/hs_license
//Actions
- (NSNumber *)addDirectory:(NSString *)name;
- (void)removeDirectory:(NSNumber *)index;
- (void)setDirectory:(NSArray *)indexPath state:(NSNumber *)state;
- (void)loadResults;
- (void)loadResultsFrom:(NSString *)filename;
- (void)saveResults;
- (void)saveResultsAs:(NSString *)filename;
- (void)loadIgnoreList;
- (void)saveIgnoreList;
- (void)clearIgnoreList;
- (void)purgeIgnoreList;
- (NSString *)exportToXHTMLwithColumns:(NSArray *)aColIds;
- (void)invokeCommand:(NSString *)cmd;
- (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)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)hardlinkMarked;
- (void)removeMarked;
//Data
- (NSNumber *)getIgnoreListCount;
- (NSNumber *)getMarkCount;
- (NSString *)getStatLine;
- (NSNumber *)getOperationalErrorCount;
- (BOOL)scanWasProblematic;
//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)setMixFileKind:(BOOL)mix_file_kind;
- (void)setEscapeFilterRegexp:(BOOL)escape_filter_regexp;
- (void)setRemoveEmptyFolders:(BOOL)remove_empty_folders;
- (void)setIgnoreHardlinkMatches:(BOOL)ignore_hardlink_matches;
- (void)setSizeThreshold:(NSInteger)size_threshold;
@end

View File

@@ -0,0 +1,14 @@
/*
Copyright 2010 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 "PyGUI.h"
@interface PyProblemDialog : PyGUI
- (void)revealSelected;
@end

View File

@@ -0,0 +1,24 @@
/*
Copyright 2010 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 "PyTable.h"
@interface PyResultTable : PyTable
- (BOOL)powerMarkerMode;
- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode;
- (BOOL)deltaValuesMode;
- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode;
- (NSString *)valueForRow:(NSInteger)rowIndex column:(NSInteger)aColumn;
- (BOOL)renameSelected:(NSString *)aNewName;
- (void)sortBy:(NSInteger)aIdentifier ascending:(BOOL)aAscending;
- (void)markSelected;
- (void)removeSelected;
- (NSInteger)selectedDupeCount;
@end

14
cocoa/base/PyStatsLabel.h Normal file
View File

@@ -0,0 +1,14 @@
/*
Copyright 2010 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 "PyGUI.h"
@interface PyStatsLabel : PyGUI
- (NSString *)display;
@end

26
cocoa/base/ResultTable.h Normal file
View File

@@ -0,0 +1,26 @@
/*
Copyright 2010 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 "HSTable.h"
#import "PyResultTable.h"
@interface ResultTable : HSTable
{
NSIndexSet *_deltaColumns;
}
- (id)initWithPyParent:(id)aPyParent view:(NSTableView *)aTableView;
- (PyResultTable *)py;
- (BOOL)powerMarkerMode;
- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode;
- (BOOL)deltaValuesMode;
- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode;
- (void)setDeltaColumns:(NSIndexSet *)aDeltaColumns;
- (NSInteger)selectedDupeCount;
- (void)removeSelected;
@end;

162
cocoa/base/ResultTable.m Normal file
View File

@@ -0,0 +1,162 @@
/*
Copyright 2010 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 "ResultTable.h"
#import "Dialogs.h"
#import "Utils.h"
#import "Consts.h"
@implementation ResultTable
- (id)initWithPyParent:(id)aPyParent view:(NSTableView *)aTableView
{
self = [super initWithPyClassName:@"PyResultTable" pyParent:aPyParent view:aTableView];
[self connect];
return self;
}
- (void)dealloc
{
[self disconnect];
[_deltaColumns release];
[super dealloc];
}
- (PyResultTable *)py
{
return (PyResultTable *)py;
}
/* Public */
- (BOOL)powerMarkerMode
{
return [[self py] powerMarkerMode];
}
- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode
{
[[self py] setPowerMarkerMode:aPowerMarkerMode];
}
- (BOOL)deltaValuesMode
{
return [[self py] deltaValuesMode];
}
- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode
{
[[self py] setDeltaValuesMode:aDeltaValuesMode];
}
- (void)setDeltaColumns:(NSIndexSet *)aDeltaColumns
{
[_deltaColumns release];
_deltaColumns = [aDeltaColumns retain];
}
- (NSInteger)selectedDupeCount
{
return [[self py] selectedDupeCount];
}
- (void)removeSelected
{
NSInteger selectedDupeCount = [self selectedDupeCount];
if (!selectedDupeCount)
return;
NSString *msg = [NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",selectedDupeCount];
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
return;
[[self py] removeSelected];
}
/* Datasource */
- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
NSString *identifier = [column identifier];
if ([identifier isEqual:@"marked"]) {
return [[self py] valueForColumn:@"marked" row:row];
}
NSInteger columnId = [identifier integerValue];
return [[self py] valueForRow:row column:columnId];
}
- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
NSString *identifier = [column identifier];
if ([identifier isEqual:@"marked"]) {
[[self py] setValue:object forColumn:identifier row:row];
}
else if ([identifier isEqual:@"0"]) {
NSString *oldName = [[self py] valueForRow:row column:0];
NSString *newName = object;
if (![newName isEqual:oldName]) {
BOOL renamed = [[self py] renameSelected:newName];
if (!renamed) {
[Dialogs showMessage:[NSString stringWithFormat:@"The name '%@' already exists.", newName]];
}
else {
[tableView setNeedsDisplay:YES];
}
}
}
}
/* Delegate */
- (void)tableView:(NSTableView *)aTableView didClickTableColumn:(NSTableColumn *)tableColumn
{
if ([[tableView sortDescriptors] count] < 1)
return;
NSSortDescriptor *sd = [[tableView sortDescriptors] objectAtIndex:0];
[[self py] sortBy:[[sd key] integerValue] ascending:[sd ascending]];
}
- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)column row:(NSInteger)row
{
BOOL isMarkable = n2b([[self py] 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]]) {
// Determine if the text color will be blue due to directory being reference.
NSTextFieldCell *textCell = cell;
if (isMarkable) {
[textCell setTextColor:[NSColor blackColor]];
}
else {
[textCell setTextColor:[NSColor blueColor]];
if ([self deltaValuesMode]) {
NSInteger i = [[column identifier] integerValue];
if ([_deltaColumns containsIndex:i]) {
[textCell setTextColor:[NSColor orangeColor]];
}
}
}
}
}
- (BOOL)tableViewHadDeletePressed:(NSTableView *)tableView
{
[self removeSelected];
return YES;
}
- (BOOL)tableViewHadSpacePressed:(NSTableView *)tableView
{
[[self py] markSelected];
return YES;
}
/* Python --> Cocoa */
- (void)invalidateMarkings
{
[tableView setNeedsDisplay:YES];
}
@end

View File

@@ -7,54 +7,69 @@ http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "Outline.h"
#import "StatsLabel.h"
#import "ResultTable.h"
#import "ProblemDialog.h"
#import "HSTableView.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 HSTableView *matches;
IBOutlet NSSegmentedControl *pmSwitch;
IBOutlet NSTextField *stats;
IBOutlet NSMenu *columnsMenu;
IBOutlet NSSearchField *filterField;
BOOL _powerMode;
BOOL _displayDelta;
NSMutableArray *_resultColumns;
NSWindowController *preferencesPanel;
ResultTable *table;
StatsLabel *statsLabel;
ProblemDialog *problemDialog;
}
/* Helpers */
- (void)fillColumnsMenu;
- (NSTableColumn *)getColumnForIdentifier:(NSInteger)aIdentifier title:(NSString *)aTitle width:(NSInteger)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;
- (void)sendMarkedToTrash:(BOOL)hardlinkDeleted;
/* Actions */
- (IBAction)clearIgnoreList:(id)sender;
- (IBAction)changeDelta:(id)sender;
- (IBAction)changePowerMarker:(id)sender;
- (IBAction)copyMarked:(id)sender;
- (IBAction)deleteMarked:(id)sender;
- (IBAction)expandAll:(id)sender;
- (IBAction)hardlinkMarked:(id)sender;
- (IBAction)exportToXHTML:(id)sender;
- (IBAction)filter:(id)sender;
- (IBAction)ignoreSelected:(id)sender;
- (IBAction)invokeCustomCommand:(id)sender;
- (IBAction)loadResults:(id)sender;
- (IBAction)markAll:(id)sender;
- (IBAction)markInvert:(id)sender;
- (IBAction)markNone:(id)sender;
- (IBAction)markSelected:(id)sender;
- (IBAction)moveMarked:(id)sender;
- (IBAction)openClicked:(id)sender;
- (IBAction)openSelected:(id)sender;
- (IBAction)removeMarked:(id)sender;
- (IBAction)removeSelected:(id)sender;
- (IBAction)renameSelected:(id)sender;
- (IBAction)resetColumnsToDefault:(id)sender;
- (IBAction)revealSelected:(id)sender;
- (IBAction)saveResults:(id)sender;
- (IBAction)showPreferencesPanel:(id)sender;
- (IBAction)startDuplicateScan:(id)sender;
- (IBAction)switchSelected:(id)sender;
- (IBAction)toggleColumn:(id)sender;
- (IBAction)toggleDelta:(id)sender;
- (IBAction)toggleDetailsPanel:(id)sender;
- (IBAction)togglePowerMarker:(id)sender;

View File

@@ -14,57 +14,35 @@ http://www.hardcoded.net/licenses/hs_license
#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
NSUInteger 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];
/* Put a cute iTunes-like bottom bar */
[[self window] setContentBorderThickness:28 forEdge:NSMinYEdge];
preferencesPanel = [[NSWindowController alloc] initWithWindowNibName:@"Preferences"];
table = [[ResultTable alloc] initWithPyParent:py view:matches];
statsLabel = [[StatsLabel alloc] initWithPyParent:py labelView:stats];
problemDialog = [[ProblemDialog alloc] initWithPy:py];
[self initResultColumns];
[self fillColumnsMenu];
[deltaSwitch setSelectedSegment:0];
[pmSwitch setSelectedSegment:0];
[matches setTarget:self];
[matches setDoubleAction:@selector(openClicked:)];
[[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
{
[table release];
[preferencesPanel release];
[statsLabel release];
[problemDialog release];
[super dealloc];
}
@@ -101,13 +79,9 @@ http://www.hardcoded.net/licenses/hs_license
//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];
for (NSTableColumn *col in [matches tableColumns]) {
NSString *colId = [col identifier];
[result addObject:colId];
}
return result;
@@ -116,48 +90,14 @@ http://www.hardcoded.net/licenses/hs_license
- (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 numberWithDouble:[col width]];
for (NSTableColumn *col in [matches tableColumns]) {
NSString *colId = [col identifier];
NSNumber *width = [NSNumber numberWithDouble:[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;
NSInteger 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
@@ -165,75 +105,69 @@ http://www.hardcoded.net/licenses/hs_license
- (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)
for (NSMenuItem *mi in [columnsMenu itemArray]) {
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)
for (NSString *colId in aColumnsOrder) {
NSInteger colIndex = [colId integerValue];
if ((colIndex == 0) && (![colId isEqual:@"0"])) {
continue;
}
NSTableColumn *col = [_resultColumns objectAtIndex:colIndex];
NSNumber *width = [aColumnsWidth objectForKey:[col identifier]];
NSMenuItem *mi = [columnsMenu itemWithTag:colIndex];
if (width) {
[col setWidth:[width floatValue]];
}
[self toggleColumn:mi];
}
}
}
- (void)updatePySelection
- (void)sendMarkedToTrash:(BOOL)hardlinkDeleted
{
NSArray *selection;
if (_powerMode)
selection = [py selectedPowerMarkerNodePaths];
else
selection = [py selectedResultNodePaths];
[matches selectNodePaths:selection];
NSInteger mark_count = [[py getMarkCount] intValue];
if (!mark_count) {
return;
}
- (void)performPySelection:(NSArray *)aIndexPaths
{
if (_powerMode)
[py selectPowerMarkerNodePaths:aIndexPaths];
else
[py selectResultNodePaths:aIndexPaths];
NSString *msg = @"You are about to send %d files to Trash. Continue?";
if (hardlinkDeleted) {
msg = @"You are about to send %d files to Trash (and hardlink them afterwards). Continue?";
}
if ([Dialogs askYesNo:[NSString stringWithFormat:msg,mark_count]] == NSAlertSecondButtonReturn) { // NO
return;
}
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
if (hardlinkDeleted) {
[py hardlinkMarked];
}
else {
[py deleteMarked];
}
- (void)refreshStats
{
[stats setStringValue:[py getStatLine]];
}
/* Actions */
- (IBAction)clearIgnoreList:(id)sender
{
NSInteger i = n2i([py getIgnoreListCount]);
if (!i)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO
return;
[py clearIgnoreList];
}
- (IBAction)changeDelta:(id)sender
{
_displayDelta = [deltaSwitch selectedSegment] == 1;
[py setDisplayDeltaValues:b2n(_displayDelta)];
[matches reloadData];
[self expandAll:nil];
[table setDeltaValuesMode:[deltaSwitch selectedSegment] == 1];
}
- (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];
[table setPowerMarkerMode:[pmSwitch selectedSegment] == 1];
}
- (IBAction)copyMarked:(id)sender
@@ -257,20 +191,12 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)deleteMarked:(id)sender
{
NSInteger 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];
[self sendMarkedToTrash:NO];
}
- (IBAction)expandAll:(id)sender
- (IBAction)hardlinkMarked:(id)sender
{
for (NSInteger i=0;i < [matches numberOfRows];i++)
[matches expandItem:[matches itemAtRow:i]];
[self sendMarkedToTrash:YES];
}
- (IBAction)exportToXHTML:(id)sender
@@ -279,6 +205,71 @@ http://www.hardcoded.net/licenses/hs_license
[[NSWorkspace sharedWorkspace] openFile:exported];
}
- (IBAction)filter:(id)sender
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setEscapeFilterRegexp:!n2b([ud objectForKey:@"useRegexpFilter"])];
[py applyFilter:[filterField stringValue]];
}
- (IBAction)ignoreSelected:(id)sender
{
NSInteger selectedDupeCount = [table selectedDupeCount];
if (!selectedDupeCount)
return;
NSString *msg = [NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",selectedDupeCount];
if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO
return;
[py addSelectedToIgnoreList];
}
- (IBAction)invokeCustomCommand:(id)sender
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
NSString *cmd = [ud stringForKey:@"CustomCommand"];
if ((cmd != nil) && ([cmd length] > 0)) {
[py invokeCommand:cmd];
}
else {
[Dialogs showMessage:@"You have no custom command set up. Set it up in your preferences."];
}
}
- (IBAction)loadResults:(id)sender
{
NSOpenPanel *op = [NSOpenPanel openPanel];
[op setCanChooseFiles:YES];
[op setCanChooseDirectories:NO];
[op setCanCreateDirectories:NO];
[op setAllowsMultipleSelection:NO];
[op setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[op setTitle:@"Select a results file to load"];
if ([op runModal] == NSOKButton) {
NSString *filename = [[op filenames] objectAtIndex:0];
[py loadResultsFrom:filename];
}
}
- (IBAction)markAll:(id)sender
{
[py markAll];
}
- (IBAction)markInvert:(id)sender
{
[py markInvert];
}
- (IBAction)markNone:(id)sender
{
[py markNone];
}
- (IBAction)markSelected:(id)sender
{
[py toggleSelectedMark];
}
- (IBAction)moveMarked:(id)sender
{
NSInteger mark_count = [[py getMarkCount] intValue];
@@ -294,37 +285,81 @@ http://www.hardcoded.net/licenses/hs_license
{
NSString *directory = [[op filenames] objectAtIndex:0];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setRemoveEmptyFolders:[ud objectForKey:@"removeEmptyFolders"]];
[py setRemoveEmptyFolders:n2b([ud objectForKey:@"removeEmptyFolders"])];
[py copyOrMove:b2n(NO) markedTo:directory recreatePath:[ud objectForKey:@"recreatePathType"]];
}
}
- (IBAction)openClicked:(id)sender
{
if ([matches clickedRow] < 0) {
return;
}
[matches selectRowIndexes:[NSIndexSet indexSetWithIndex:[matches clickedRow]] byExtendingSelection:NO];
[py openSelected];
}
- (IBAction)openSelected:(id)sender
{
[py openSelected];
}
- (IBAction)removeMarked:(id)sender
{
int mark_count = [[py getMarkCount] intValue];
if (!mark_count)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO
return;
[py removeMarked];
}
- (IBAction)removeSelected:(id)sender
{
[table removeSelected];
}
- (IBAction)renameSelected:(id)sender
{
NSInteger col = [matches columnWithIdentifier:@"0"];
NSInteger row = [matches selectedRow];
[matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES];
}
- (IBAction)resetColumnsToDefault:(id)sender
{
// Virtual
}
- (IBAction)revealSelected:(id)sender
{
[py revealSelected];
}
- (IBAction)showPreferencesPanel:(id)sender
{
[preferencesPanel showWindow:sender];
}
- (IBAction)saveResults:(id)sender
{
NSSavePanel *sp = [NSSavePanel savePanel];
[sp setCanCreateDirectories:YES];
[sp setAllowedFileTypes:[NSArray arrayWithObject:@"dupeguru"]];
[sp setTitle:@"Select a file to save your results to"];
if ([sp runModal] == NSOKButton) {
[py saveResultsAs:[sp filename]];
}
}
- (IBAction)startDuplicateScan:(id)sender
{
// Virtual
}
- (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.
NSInteger matchesTag = _powerMode ? 2 : 0;
NSInteger startLen = [[py getOutlineView:matchesTag childCountsForPath:[NSArray array]] count];
[self performPySelection:[self getSelectedPaths:YES]];
[py makeSelectedReference];
[self performPySelection:[self getSelectedPaths:NO]];
// 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
@@ -347,6 +382,15 @@ http://www.hardcoded.net/licenses/hs_license
}
}
- (IBAction)toggleDelta:(id)sender
{
if ([deltaSwitch selectedSegment] == 1)
[deltaSwitch setSelectedSegment:0];
else
[deltaSwitch setSelectedSegment:1];
[self changeDelta:sender];
}
- (IBAction)toggleDetailsPanel:(id)sender
{
[[(AppDelegateBase *)app detailsPanel] toggleVisibility];
@@ -361,21 +405,6 @@ http://www.hardcoded.net/licenses/hs_license
[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
{
@@ -384,33 +413,34 @@ http://www.hardcoded.net/licenses/hs_license
- (void)jobCompleted:(NSNotification *)aNotification
{
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
NSInteger 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
if ([py scanWasProblematic]) {
[problemDialog showWindow:self];
}
else {
[Dialogs showMessage:@"All marked files were copied sucessfully."];
}
}
else 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
if ([py scanWasProblematic]) {
[problemDialog showWindow:self];
}
else {
[Dialogs showMessage:@"All marked files were moved sucessfully."];
}
else if ([lastAction isEqualTo:jobDelete]) {
if (r > 0) {
NSString *msg = @"%d file(s) couldn't be sent to Trash. They were kept in the results, "\
"and still are marked. See the F.A.Q. section in the help file for details.";
[Dialogs showMessage:[NSString stringWithFormat:msg,r]];
}
else
else if ([lastAction isEqualTo:jobDelete]) {
if ([py scanWasProblematic]) {
[problemDialog showWindow:self];
}
else {
[Dialogs showMessage:@"All marked files were sucessfully sent to Trash."];
}
}
else if ([lastAction isEqualTo:jobScan]) {
NSInteger groupCount = [[py getOutlineView:0 childCountsForPath:[NSArray array]] count];
if (groupCount == 0)
NSInteger rowCount = [[table py] numberOfRows];
if (rowCount == 0)
[Dialogs showMessage:@"No duplicates found."];
}
@@ -430,7 +460,6 @@ http://www.hardcoded.net/licenses/hs_license
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]];
}
@@ -441,31 +470,6 @@ http://www.hardcoded.net/licenses/hs_license
[Dialogs showMessage:msg];
}
- (void)outlineViewSelectionDidChange:(NSNotification *)notification
{
[self performPySelection:[self getSelectedPaths:NO]];
}
- (void)resultsChanged:(NSNotification *)aNotification
{
[matches reloadData];
[self expandAll:nil];
[self outlineViewSelectionDidChange:nil];
[self refreshStats];
}
- (void)resultsMarkingChanged:(NSNotification *)aNotification
{
[matches invalidateMarkings];
[self refreshStats];
}
- (void)resultsUpdated:(NSNotification *)aNotification
{
[matches invalidateBuffers];
[matches invalidateMarkings];
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
{
return ![[ProgressController mainProgressController] isShown];

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

@@ -0,0 +1,19 @@
/*
Copyright 2010 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 "HSGUIController.h"
#import "PyStatsLabel.h"
@interface StatsLabel : HSGUIController
{
NSTextField *labelView;
}
- (id)initWithPyParent:(id)aPyParent labelView:(NSTextField *)aLabelView;
- (PyStatsLabel *)py;
@end

38
cocoa/base/StatsLabel.m Normal file
View File

@@ -0,0 +1,38 @@
/*
Copyright 2010 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 "StatsLabel.h"
#import "Utils.h"
@implementation StatsLabel
- (id)initWithPyParent:(id)aPyParent labelView:(NSTextField *)aLabelView
{
self = [super initWithPyClassName:@"PyStatsLabel" pyParent:aPyParent];
labelView = [aLabelView retain];
[self connect];
return self;
}
- (void)dealloc
{
[self disconnect];
[labelView release];
[super dealloc];
}
- (PyStatsLabel *)py
{
return (PyStatsLabel *)py;
}
/* Python --> Cocoa */
- (void)refresh
{
[labelView setStringValue:[[self py] display]];
}
@end

View File

@@ -2,13 +2,13 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10C540</string>
<string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.AppKitVersion">1038.25</string>
<string key="IBDocument.HIToolboxVersion">458.00</string>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">740</string>
<string key="NS.object.0">788</string>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -41,7 +41,7 @@
<object class="NSWindowTemplate" id="476890502">
<int key="NSWindowStyleMask">155</int>
<int key="NSWindowBacking">2</int>
<string key="NSWindowRect">{{33, 261}, {451, 161}}</string>
<string key="NSWindowRect">{{33, 261}, {451, 146}}</string>
<int key="NSWTFlags">-260571136</int>
<string key="NSWindowTitle">Details of Selected File</string>
<object class="NSMutableString" key="NSWindowClass">
@@ -51,7 +51,7 @@
<characters key="NS.bytes">View</characters>
</object>
<string key="NSWindowContentMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSWindowContentMinSize">{451, 161}</string>
<string key="NSWindowContentMinSize">{451, 146}</string>
<object class="NSView" key="NSWindowView" id="1027711962">
<reference key="NSNextResponder"/>
<int key="NSvFlags">256</int>
@@ -70,7 +70,7 @@
<object class="NSTableView" id="251969872">
<reference key="NSNextResponder" ref="488480549"/>
<int key="NSvFlags">256</int>
<string key="NSFrameSize">{449, 143}</string>
<string key="NSFrameSize">{449, 128}</string>
<reference key="NSSuperview" ref="488480549"/>
<int key="NSTag">2</int>
<bool key="NSEnabled">YES</bool>
@@ -224,7 +224,7 @@
<int key="NSTableViewDraggingDestinationStyle">0</int>
</object>
</object>
<string key="NSFrame">{{1, 17}, {449, 143}}</string>
<string key="NSFrame">{{1, 17}, {449, 128}}</string>
<reference key="NSSuperview" ref="362108788"/>
<reference key="NSNextKeyView" ref="251969872"/>
<reference key="NSDocView" ref="251969872"/>
@@ -266,7 +266,7 @@
</object>
<reference ref="717380566"/>
</object>
<string key="NSFrameSize">{451, 161}</string>
<string key="NSFrameSize">{451, 146}</string>
<reference key="NSSuperview" ref="1027711962"/>
<reference key="NSNextKeyView" ref="488480549"/>
<int key="NSsFlags">530</int>
@@ -278,12 +278,13 @@
<bytes key="NSScrollAmts">QSAAAEEgAABBgAAAQYAAAA</bytes>
</object>
</object>
<string key="NSFrameSize">{451, 161}</string>
<string key="NSFrameSize">{451, 146}</string>
<reference key="NSSuperview"/>
</object>
<string key="NSScreenRect">{{0, 0}, {1024, 746}}</string>
<string key="NSMinSize">{451, 177}</string>
<string key="NSMinSize">{451, 162}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSFrameAutosaveName">DetailsPanel</string>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
@@ -497,12 +498,12 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>{{109, 656}, {451, 161}}</string>
<string>{{109, 671}, {451, 146}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>{{109, 656}, {451, 161}}</string>
<string>{{109, 671}, {451, 146}}</string>
<boolean value="YES"/>
<boolean value="YES"/>
<string>{451, 161}</string>
<string>{451, 146}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -536,11 +537,18 @@
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">DetailsPanel</string>
<string key="superclassName">NSWindowController</string>
<string key="superclassName">HSWindowController</string>
<object class="NSMutableDictionary" key="outlets">
<string key="NS.key.0">detailsTable</string>
<string key="NS.object.0">NSTableView</string>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<string key="NS.key.0">detailsTable</string>
<object class="IBToOneOutletInfo" key="NS.object.0">
<string key="name">detailsTable</string>
<string key="candidateClassName">NSTableView</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/DetailsPanel.h</string>
@@ -548,7 +556,7 @@
</object>
<object class="IBPartialClassDescription">
<string key="className">DetailsPanel</string>
<string key="superclassName">NSWindowController</string>
<string key="superclassName">HSWindowController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
@@ -562,6 +570,32 @@
<string key="minorKey"/>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">HSWindowController</string>
<string key="superclassName">NSWindowController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../controllers/HSWindowController.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NSObject</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../views/HSOutlineView.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NSObject</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier" id="111130406">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../views/NSTableViewAdditions.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">NSTableView</string>
<reference key="sourceIdentifier" ref="111130406"/>
</object>
</object>
<object class="NSMutableArray" key="referencedPartialClassDescriptionsV3.2+">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -1029,6 +1063,13 @@
<string key="NS.key.0">showWindow:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">showWindow:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">showWindow:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBFrameworkSource</string>
<string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string>
@@ -1037,6 +1078,7 @@
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>

View File

@@ -2,17 +2,17 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10B504</string>
<string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.AppKitVersion">1038.2</string>
<string key="IBDocument.HIToolboxVersion">437.00</string>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">740</string>
<string key="NS.object.0">788</string>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
<integer value="6"/>
<integer value="50"/>
</object>
<object class="NSArray" key="IBDocument.PluginDependencies">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -70,7 +70,6 @@
<int key="NSvFlags">256</int>
<string key="NSFrameSize">{327, 165}</string>
<reference key="NSSuperview" ref="514281221"/>
<int key="NSTag">1</int>
<bool key="NSEnabled">YES</bool>
<object class="NSTableHeaderView" key="NSHeaderView" id="885660940">
<reference key="NSNextResponder" ref="395832192"/>
@@ -88,7 +87,7 @@
<object class="NSMutableArray" key="NSTableColumns">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSTableColumn" id="547470852">
<string key="NSIdentifier">0</string>
<string key="NSIdentifier">name</string>
<double key="NSWidth">236</double>
<double key="NSMinWidth">16</double>
<double key="NSMaxWidth">1000</double>
@@ -142,7 +141,7 @@
<reference key="NSTableView" ref="10140319"/>
</object>
<object class="NSTableColumn" id="50798966">
<string key="NSIdentifier">1</string>
<string key="NSIdentifier">state</string>
<double key="NSWidth">85.35595703125</double>
<double key="NSMinWidth">30.35595703125</double>
<double key="NSMaxWidth">1000</double>
@@ -173,15 +172,41 @@
<string key="NSKeyEquivalent"/>
<int key="NSPeriodicDelay">400</int>
<int key="NSPeriodicInterval">75</int>
<nil key="NSMenuItem"/>
<object class="NSMenuItem" key="NSMenuItem" id="71151438">
<reference key="NSMenu" ref="104112446"/>
<string key="NSTitle">Normal</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<int key="NSState">1</int>
<string key="NSAction">_popUpItemAction:</string>
<reference key="NSTarget" ref="867721721"/>
</object>
<bool key="NSMenuItemRespectAlignment">YES</bool>
<object class="NSMenu" key="NSMenu" id="104112446">
<string key="NSTitle"/>
<string key="NSTitle">Normal</string>
<object class="NSMutableArray" key="NSMenuItems">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="71151438"/>
<object class="NSMenuItem" id="828402206">
<reference key="NSMenu" ref="104112446"/>
<string key="NSTitle">Reference</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<string key="NSAction">_popUpItemAction:</string>
<reference key="NSTarget" ref="867721721"/>
</object>
<object class="NSMenuItem" id="142495353">
<reference key="NSMenu" ref="104112446"/>
<string key="NSTitle">Excluded</string>
<string key="NSKeyEquiv"/>
<int key="NSMnemonicLoc">2147483647</int>
<string key="NSAction">_popUpItemAction:</string>
<reference key="NSTarget" ref="867721721"/>
</object>
</object>
<int key="NSSelectedIndex">-1</int>
<bool key="NSNoAutoenable">YES</bool>
<bool key="NSMenuExcludeMarkColumn">YES</bool>
</object>
<int key="NSPreferredEdge">3</int>
<bool key="NSUsesItemFromMenu">YES</bool>
<bool key="NSAltersState">YES</bool>
@@ -189,6 +214,7 @@
</object>
<int key="NSResizingMask">2</int>
<bool key="NSIsResizeable">YES</bool>
<bool key="NSIsEditable">YES</bool>
<reference key="NSTableView" ref="10140319"/>
</object>
</object>
@@ -402,6 +428,7 @@
<string key="NSScreenRect">{{0, 0}, {1440, 878}}</string>
<string key="NSMinSize">{369, 291}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSFrameAutosaveName">DirectoryPanel</string>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
@@ -415,30 +442,6 @@
</object>
<int key="connectionID">19</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">nextKeyView</string>
<reference key="source" ref="963602908"/>
<reference key="destination" ref="10140319"/>
</object>
<int key="connectionID">20</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">nextKeyView</string>
<reference key="source" ref="10140319"/>
<reference key="destination" ref="630693842"/>
</object>
<int key="connectionID">21</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">nextKeyView</string>
<reference key="source" ref="630693842"/>
<reference key="destination" ref="963602908"/>
</object>
<int key="connectionID">22</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">popupAddDirectoryMenu:</string>
@@ -471,22 +474,6 @@
</object>
<int key="connectionID">26</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">directories</string>
<reference key="source" ref="566600593"/>
<reference key="destination" ref="10140319"/>
</object>
<int key="connectionID">27</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">delegate</string>
<reference key="source" ref="10140319"/>
<reference key="destination" ref="566600593"/>
</object>
<int key="connectionID">29</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">performClose:</string>
@@ -503,6 +490,14 @@
</object>
<int key="connectionID">43</int>
</object>
<object class="IBConnectionRecord">
<object class="IBOutletConnection" key="connection">
<string key="label">outlineView</string>
<reference key="source" ref="566600593"/>
<reference key="destination" ref="10140319"/>
</object>
<int key="connectionID">54</int>
</object>
</object>
<object class="IBMutableOrderedSet" key="objectRecords">
<object class="NSArray" key="orderedObjects">
@@ -685,6 +680,12 @@
<object class="IBObjectRecord">
<int key="objectID">50</int>
<reference key="object" ref="104112446"/>
<object class="NSMutableArray" key="children">
<bool key="EncodedWithXMLCoder">YES</bool>
<reference ref="71151438"/>
<reference ref="828402206"/>
<reference ref="142495353"/>
</object>
<reference key="parent" ref="867721721"/>
</object>
<object class="IBObjectRecord">
@@ -702,6 +703,21 @@
<reference key="object" ref="885660940"/>
<reference key="parent" ref="242279311"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">55</int>
<reference key="object" ref="71151438"/>
<reference key="parent" ref="104112446"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">56</int>
<reference key="object" ref="828402206"/>
<reference key="parent" ref="104112446"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">57</int>
<reference key="object" ref="142495353"/>
<reference key="parent" ref="104112446"/>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="flattenedProperties">
@@ -739,6 +755,7 @@
<string>5.ImportedFromIB2</string>
<string>5.windowTemplate.hasMinSize</string>
<string>5.windowTemplate.minSize</string>
<string>50.IBEditorWindowLastContentRect</string>
<string>50.IBPluginDependency</string>
<string>51.IBPluginDependency</string>
<string>51.IBShouldRemoveOnLegacySave</string>
@@ -746,6 +763,9 @@
<string>52.IBShouldRemoveOnLegacySave</string>
<string>53.IBPluginDependency</string>
<string>53.IBShouldRemoveOnLegacySave</string>
<string>55.IBPluginDependency</string>
<string>56.IBPluginDependency</string>
<string>57.IBPluginDependency</string>
<string>6.IBPluginDependency</string>
<string>6.ImportedFromIB2</string>
<string>7.IBPluginDependency</string>
@@ -761,7 +781,7 @@
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>DirectoryOutline</string>
<string>HSOutlineView</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
@@ -788,6 +808,7 @@
<boolean value="YES"/>
<boolean value="YES"/>
<string>{369, 269}</string>
<string>{{98, 740}, {327, 63}}</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
@@ -796,6 +817,9 @@
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
<string>com.apple.InterfaceBuilder.CocoaPlugin</string>
<boolean value="YES"/>
@@ -821,27 +845,11 @@
</object>
</object>
<nil key="sourceID"/>
<int key="maxID">53</int>
<int key="maxID">57</int>
</object>
<object class="IBClassDescriber" key="IBDocument.Classes">
<object class="NSMutableArray" key="referencedPartialClassDescriptions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBPartialClassDescription">
<string key="className">DirectoryOutline</string>
<string key="superclassName">OutlineView</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier" id="462913745">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">dgbase/DirectoryPanel.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">DirectoryPanel</string>
<string key="superclassName">NSWindowController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">DirectoryPanel.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">DirectoryPanel</string>
<string key="superclassName">NSWindowController</string>
@@ -850,7 +858,6 @@
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>askForDirectory:</string>
<string>changeDirectoryState:</string>
<string>popupAddDirectoryMenu:</string>
<string>removeSelectedDirectory:</string>
<string>toggleVisible:</string>
@@ -861,7 +868,35 @@
<string>id</string>
<string>id</string>
<string>id</string>
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>askForDirectory:</string>
<string>popupAddDirectoryMenu:</string>
<string>removeSelectedDirectory:</string>
<string>toggleVisible:</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBActionInfo">
<string key="name">askForDirectory:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">popupAddDirectoryMenu:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">removeSelectedDirectory:</string>
<string key="candidateClassName">id</string>
</object>
<object class="IBActionInfo">
<string key="name">toggleVisible:</string>
<string key="candidateClassName">id</string>
</object>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
@@ -869,60 +904,53 @@
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addButtonPopUp</string>
<string>directories</string>
<string>outlineView</string>
<string>removeButton</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSPopUpButton</string>
<string>NSOutlineView</string>
<string>HSOutlineView</string>
<string>NSButton</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addButtonPopUp</string>
<string>outlineView</string>
<string>removeButton</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">addButtonPopUp</string>
<string key="candidateClassName">NSPopUpButton</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">outlineView</string>
<string key="candidateClassName">HSOutlineView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">removeButton</string>
<string key="candidateClassName">NSButton</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/DirectoryPanel.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">DirectoryPanel</string>
<string key="superclassName">NSWindowController</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">DirectoryPanel</string>
<string key="superclassName">NSWindowController</string>
<object class="NSMutableDictionary" key="actions">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>askForDirectory:</string>
<string>changeDirectoryState:</string>
<string>popupAddDirectoryMenu:</string>
<string>removeSelectedDirectory:</string>
<string>toggleVisible:</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>id</string>
<string>id</string>
<string>id</string>
<string>id</string>
<string>id</string>
</object>
</object>
<object class="NSMutableDictionary" key="outlets">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>addButtonPopUp</string>
<string>directories</string>
<string>removeButton</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSPopUpButton</string>
<string>DirectoryOutline</string>
<string>NSButton</string>
</object>
</object>
<reference key="sourceIdentifier" ref="462913745"/>
</object>
<object class="IBPartialClassDescription">
<string key="className">FirstResponder</string>
<string key="superclassName">NSObject</string>
@@ -932,40 +960,27 @@
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">OutlineView</string>
<string key="className">HSOutlineView</string>
<string key="superclassName">NSOutlineView</string>
<object class="NSMutableDictionary" key="outlets">
<string key="NS.key.0">py</string>
<string key="NS.object.0">PyApp</string>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<object class="IBClassDescriptionSource" key="sourceIdentifier" id="53364925">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">cocoalib/Outline.h</string>
<string key="minorKey">../views/HSOutlineView.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">OutlineView</string>
<string key="superclassName">NSOutlineView</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
<string key="className">NSObject</string>
<reference key="sourceIdentifier" ref="53364925"/>
</object>
<object class="IBPartialClassDescription">
<string key="className">NSObject</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier" id="42597526">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../views/NSTableViewAdditions.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">PyApp</string>
<string key="superclassName">PyRegistrable</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">cocoalib/PyApp.h</string>
</object>
</object>
<object class="IBPartialClassDescription">
<string key="className">PyRegistrable</string>
<string key="superclassName">NSObject</string>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">cocoalib/PyRegistrable.h</string>
</object>
<string key="className">NSTableView</string>
<reference key="sourceIdentifier" ref="42597526"/>
</object>
</object>
<object class="NSMutableArray" key="referencedPartialClassDescriptionsV3.2+">
@@ -1476,6 +1491,13 @@
<string key="NS.key.0">showWindow:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">showWindow:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">showWindow:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBFrameworkSource</string>
<string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string>
@@ -1484,6 +1506,7 @@
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
@@ -1497,7 +1520,20 @@
<integer value="3000" key="NS.object.0"/>
</object>
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<string key="IBDocument.LastKnownRelativeProjectPath">../../dupeguru.xcodeproj</string>
<string key="IBDocument.LastKnownRelativeProjectPath">../../se/dupeguru.xcodeproj</string>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>NSMenuCheckmark</string>
<string>NSMenuMixedState</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>{9, 8}</string>
<string>{7, 2}</string>
</object>
</object>
</data>
</archive>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@ http://www.hardcoded.net/licenses/hs_license
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[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:@"debug"];
[d setObject:b2n(NO) forKey:@"scanTagTrack"];

View File

@@ -23,7 +23,7 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>5.7.1</string>
<string>5.10.0</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@@ -7,32 +7,9 @@ http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "../../cocoalib/Outline.h"
#import "../base/ResultWindow.h"
#import "DirectoryPanel.h"
@interface ResultWindow : ResultWindowBase
{
IBOutlet NSSearchField *filterField;
NSString *_lastAction;
NSMutableIndexSet *_deltaColumns;
}
- (IBAction)clearIgnoreList:(id)sender;
- (IBAction)filter:(id)sender;
- (IBAction)ignoreSelected:(id)sender;
- (IBAction)markAll:(id)sender;
- (IBAction)markInvert:(id)sender;
- (IBAction)markNone:(id)sender;
- (IBAction)markSelected:(id)sender;
- (IBAction)markToggle:(id)sender;
- (IBAction)openSelected:(id)sender;
- (IBAction)refresh:(id)sender;
@interface ResultWindow : ResultWindowBase {}
- (IBAction)removeDeadTracks:(id)sender;
- (IBAction)removeMarked:(id)sender;
- (IBAction)removeSelected:(id)sender;
- (IBAction)renameSelected:(id)sender;
- (IBAction)revealSelected:(id)sender;
- (IBAction)startDuplicateScan:(id)sender;
- (IBAction)toggleDelta:(id)sender;
@end

View File

@@ -20,130 +20,17 @@ http://www.hardcoded.net/licenses/hs_license
{
[super awakeFromNib];
[[self window] setTitle:@"dupeGuru Music Edition"];
_displayDelta = NO;
_powerMode = NO;
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,7)] retain];
[_deltaColumns removeIndex:6];
[deltaSwitch setSelectedSegment:0];
[pmSwitch setSelectedSegment:0];
[py setDisplayDeltaValues:b2n(_displayDelta)];
[matches setTarget:self];
[matches setDoubleAction:@selector(openSelected:)];
[self refreshStats];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,6)];
[deltaColumns removeIndex:6];
[table setDeltaColumns:deltaColumns];
}
/* Actions */
- (IBAction)clearIgnoreList:(id)sender
{
NSInteger i = n2i([py getIgnoreListCount]);
if (!i)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO
return;
[py clearIgnoreList];
}
- (IBAction)filter:(id)sender
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setEscapeFilterRegexp:b2n(!n2b([ud objectForKey:@"useRegexpFilter"]))];
[py applyFilter:[filterField stringValue]];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)ignoreSelected:(id)sender
{
NSArray *nodeList = [self getSelected:YES];
if (![nodeList count])
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
return;
[self performPySelection:[self getSelectedPaths:YES]];
[py addSelectedToIgnoreList];
[py removeSelected];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)markAll:(id)sender
{
[py markAll];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markInvert:(id)sender
{
[py markInvert];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markNone:(id)sender
{
[py markNone];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:YES]];
[py toggleSelectedMark];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markToggle:(id)sender
{
OVNode *node = [matches itemAtRow:[matches clickedRow]];
[self performPySelection:[NSArray arrayWithObject:p2a([node indexPath])]];
[py toggleSelectedMark];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)openSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:NO]];
[py openSelected];
}
- (IBAction)refresh:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)removeDeadTracks:(id)sender
{
[(PyDupeGuru *)py scanDeadTracks];
}
- (IBAction)removeMarked:(id)sender
{
int mark_count = [[py getMarkCount] intValue];
if (!mark_count)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO
return;
[py removeMarked];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)removeSelected:(id)sender
{
NSArray *nodeList = [self getSelected:YES];
if (![nodeList count])
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
return;
[self performPySelection:[self getSelectedPaths:YES]];
[py removeSelected];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)renameSelected:(id)sender
{
NSInteger col = [matches columnWithIdentifier:@"0"];
NSInteger row = [matches selectedRow];
[matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES];
}
- (IBAction)resetColumnsToDefault:(id)sender
{
NSMutableArray *columnsOrder = [NSMutableArray array];
@@ -151,22 +38,18 @@ http://www.hardcoded.net/licenses/hs_license
[columnsOrder addObject:@"2"];
[columnsOrder addObject:@"3"];
[columnsOrder addObject:@"4"];
[columnsOrder addObject:@"16"];
[columnsOrder addObject:@"6"];
[columnsOrder addObject:@"15"];
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
[columnsWidth setObject:i2n(214) forKey:@"0"];
[columnsWidth setObject:i2n(235) forKey:@"0"];
[columnsWidth setObject:i2n(63) forKey:@"2"];
[columnsWidth setObject:i2n(50) forKey:@"3"];
[columnsWidth setObject:i2n(50) forKey:@"4"];
[columnsWidth setObject:i2n(57) forKey:@"16"];
[columnsWidth setObject:i2n(40) forKey:@"6"];
[columnsWidth setObject:i2n(57) forKey:@"15"];
[self restoreColumnsPosition:columnsOrder widths:columnsWidth];
}
- (IBAction)revealSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:NO]];
[py revealSelected];
}
- (IBAction)startDuplicateScan:(id)sender
{
if ([matches numberOfRows] > 0)
@@ -185,13 +68,10 @@ http://www.hardcoded.net/licenses/hs_license
[_py enable:[ud objectForKey:@"scanTagYear"] scanForTag:@"year"];
[_py setMinMatchPercentage:[ud objectForKey:@"minMatchPercentage"]];
[_py setWordWeighting:[ud objectForKey:@"wordWeighting"]];
[_py setMixFileKind:[ud objectForKey:@"mixFileKind"]];
[_py setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[_py setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[_py setMatchSimilarWords:[ud objectForKey:@"matchSimilarWords"]];
NSInteger r = n2i([py doScan]);
[matches reloadData];
[self refreshStats];
if (r == 1)
[Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."];
if (r == 3)
{
[Dialogs showMessage:@"The selected directories contain no scannable file."];
@@ -199,15 +79,6 @@ http://www.hardcoded.net/licenses/hs_license
}
}
- (IBAction)toggleDelta:(id)sender
{
if ([deltaSwitch selectedSegment] == 1)
[deltaSwitch setSelectedSegment:0];
else
[deltaSwitch setSelectedSegment:1];
[self changeDelta:sender];
}
/* Public */
- (void)initResultColumns
{
@@ -226,43 +97,17 @@ http://www.hardcoded.net/licenses/hs_license
[_resultColumns addObject:brCol];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Sample Rate" width:60 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Kind" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Creation" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:9 title:@"Title" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:10 title:@"Artist" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:11 title:@"Album" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:12 title:@"Genre" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:13 title:@"Year" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Track Number" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:15 title:@"Comment" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:16 title:@"Match %" width:57 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:17 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:18 title:@"Dupe Count" width:80 refCol:refCol]];
}
/* Delegate */
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
OVNode *node = item;
if ([[tableColumn identifier] isEqual:@"mark"])
{
[cell setEnabled: [node isMarkable]];
}
if ([cell isKindOfClass:[NSTextFieldCell class]])
{
// Determine if the text color will be blue due to directory being reference.
NSTextFieldCell *textCell = cell;
if ([node isMarkable])
[textCell setTextColor:[NSColor blackColor]];
else
[textCell setTextColor:[NSColor blueColor]];
if ((_displayDelta) && (_powerMode || ([node level] > 1)))
{
int i = [[tableColumn identifier] intValue];
if ([_deltaColumns containsIndex:i])
[textCell setTextColor:[NSColor orangeColor]];
}
}
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Title" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:9 title:@"Artist" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:10 title:@"Album" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:11 title:@"Genre" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:12 title:@"Year" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:13 title:@"Track Number" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Comment" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:15 title:@"Match %" width:57 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:16 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:17 title:@"Dupe Count" width:80 refCol:refCol]];
}
/* Notifications */

View File

@@ -4,62 +4,69 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsutil.cocoa import signature
from hscommon.cocoa import signature
from core.app_cocoa_inter import PyDupeGuruBase, PyDetailsPanel
from core_me.app_cocoa import DupeGuruME
from core.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO)
from core.scanner import ScanType
# Fix py2app imports which chokes on relative imports
from core_me import app_cocoa, data, fs, scanner
from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma
# Fix py2app imports which chokes on relative imports and other stuff
import core_me.app_cocoa, core_me.data, core_me.fs, core_me.scanner
import hsaudiotag.aiff, hsaudiotag.flac, hsaudiotag.genres, hsaudiotag.id3v1,\
hsaudiotag.id3v2, hsaudiotag.mp4, hsaudiotag.mpeg, hsaudiotag.ogg, hsaudiotag.wma
from hsaudiotag import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma
import hsutil.conflict
import core.engine, core.fs, core.app
import xml.etree.ElementPath
import gzip
import aem.kae
import appscript.defaultterminology
class PyDupeGuru(PyDupeGuruBase):
def init(self):
self = super(PyDupeGuru,self).init()
self.app = DupeGuruME()
self.py = DupeGuruME()
return self
def removeDeadTracks(self):
self.app.remove_dead_tracks()
self.py.remove_dead_tracks()
def scanDeadTracks(self):
self.app.scan_dead_tracks()
self.py.scan_dead_tracks()
#---Information
@signature('i@:')
def deadTrackCount(self):
return len(self.app.dead_tracks)
return len(self.py.dead_tracks)
#---Properties
def setMinMatchPercentage_(self, percentage):
self.app.scanner.min_match_percentage = int(percentage)
self.py.scanner.min_match_percentage = int(percentage)
def setScanType_(self, scan_type):
try:
self.app.scanner.scan_type = [
SCAN_TYPE_FILENAME,
SCAN_TYPE_FIELDS,
SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG,
SCAN_TYPE_CONTENT,
SCAN_TYPE_CONTENT_AUDIO
self.py.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):
self.app.scanner.word_weighting = words_are_weighted
self.py.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self, match_similar_words):
self.app.scanner.match_similar_words = match_similar_words
self.py.scanner.match_similar_words = match_similar_words
def enable_scanForTag_(self, enable, scan_tag):
if enable:
self.app.scanner.scanned_tags.add(scan_tag)
self.py.scanner.scanned_tags.add(scan_tag)
else:
self.app.scanner.scanned_tags.discard(scan_tag)
self.py.scanner.scanned_tags.discard(scan_tag)
#---Registration
def appName(self):

View File

@@ -21,7 +21,18 @@
/* Begin PBXBuildFile section */
8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
CE003CC611242D00004B0AA7 /* HSGUIController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CB411242D00004B0AA7 /* HSGUIController.m */; };
CE003CC711242D00004B0AA7 /* HSOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CB611242D00004B0AA7 /* HSOutline.m */; };
CE003CC811242D00004B0AA7 /* HSWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CB811242D00004B0AA7 /* HSWindowController.m */; };
CE003CC911242D00004B0AA7 /* NSEventAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CBA11242D00004B0AA7 /* NSEventAdditions.m */; };
CE003CCA11242D00004B0AA7 /* HSOutlineView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CC111242D00004B0AA7 /* HSOutlineView.m */; };
CE003CCB11242D00004B0AA7 /* NSIndexPathAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CC311242D00004B0AA7 /* NSIndexPathAdditions.m */; };
CE003CCC11242D00004B0AA7 /* NSTableViewAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CC511242D00004B0AA7 /* NSTableViewAdditions.m */; };
CE003CD011242D2C004B0AA7 /* DirectoryOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE003CCE11242D2C004B0AA7 /* DirectoryOutline.m */; };
CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */; };
CE0A0C001175A1C000DCA3C6 /* HSTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0A0BFF1175A1C000DCA3C6 /* HSTable.m */; };
CE0A0C041175A1DE00DCA3C6 /* ProblemDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0A0C021175A1DE00DCA3C6 /* ProblemDialog.m */; };
CE0A0C061175A24800DCA3C6 /* ProblemDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE0A0C051175A24800DCA3C6 /* ProblemDialog.xib */; };
CE1425890AFB718500BD5167 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE1425880AFB718500BD5167 /* Sparkle.framework */; };
CE14259F0AFB719300BD5167 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE1425880AFB718500BD5167 /* Sparkle.framework */; };
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; };
@@ -32,10 +43,8 @@
CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; };
CE4B59C81119919700C06C9E /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE4B59C51119919700C06C9E /* ErrorReportWindow.xib */; };
CE4B59C91119919700C06C9E /* progress.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE4B59C61119919700C06C9E /* progress.xib */; };
CE4B59CA1119919700C06C9E /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE4B59C71119919700C06C9E /* registration.xib */; };
CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; };
CE515DF40FC6C12E00EC695D /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */; };
CE515DF50FC6C12E00EC695D /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE50FC6C12E00EC695D /* Outline.m */; };
CE515DF60FC6C12E00EC695D /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE70FC6C12E00EC695D /* ProgressController.m */; };
CE515DF70FC6C12E00EC695D /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DEA0FC6C12E00EC695D /* RecentDirectories.m */; };
CE515DF80FC6C12E00EC695D /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DEC0FC6C12E00EC695D /* RegistrationInterface.m */; };
@@ -44,12 +53,16 @@
CE515E1D0FC6C19300EC695D /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E160FC6C19300EC695D /* AppDelegate.m */; };
CE515E1E0FC6C19300EC695D /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E190FC6C19300EC695D /* DirectoryPanel.m */; };
CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515E1C0FC6C19300EC695D /* ResultWindow.m */; };
CE578303124DFC660004769C /* HSTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE578302124DFC660004769C /* HSTableView.m */; };
CE6032C00FE6784C007E33FF /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6032BF0FE6784C007E33FF /* DetailsPanel.m */; };
CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; };
CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */; };
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
CE900AD2109B238600754048 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD1109B238600754048 /* Preferences.xib */; };
CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD6109B2A9B00754048 /* MainMenu.xib */; };
CEB14D29124DFC2800FA7481 /* ResultTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CEB14D28124DFC2800FA7481 /* ResultTable.m */; };
CECC563B12144A9000ABF262 /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CECC563912144A9000ABF262 /* registration.xib */; };
CEDF07A3112493B200EE5BC0 /* StatsLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDF07A2112493B200EE5BC0 /* StatsLabel.m */; };
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; };
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; };
CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; };
@@ -78,7 +91,34 @@
29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; };
8D1107320486CEB800E47090 /* dupeGuru ME.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru ME.app"; sourceTree = BUILT_PRODUCTS_DIR; };
CE003CB311242D00004B0AA7 /* HSGUIController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSGUIController.h; sourceTree = "<group>"; };
CE003CB411242D00004B0AA7 /* HSGUIController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSGUIController.m; sourceTree = "<group>"; };
CE003CB511242D00004B0AA7 /* HSOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSOutline.h; sourceTree = "<group>"; };
CE003CB611242D00004B0AA7 /* HSOutline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSOutline.m; sourceTree = "<group>"; };
CE003CB711242D00004B0AA7 /* HSWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSWindowController.h; sourceTree = "<group>"; };
CE003CB811242D00004B0AA7 /* HSWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSWindowController.m; sourceTree = "<group>"; };
CE003CB911242D00004B0AA7 /* NSEventAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSEventAdditions.h; path = ../../cocoalib/NSEventAdditions.h; sourceTree = SOURCE_ROOT; };
CE003CBA11242D00004B0AA7 /* NSEventAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSEventAdditions.m; path = ../../cocoalib/NSEventAdditions.m; sourceTree = SOURCE_ROOT; };
CE003CBC11242D00004B0AA7 /* PyGUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyGUI.h; sourceTree = "<group>"; };
CE003CBD11242D00004B0AA7 /* PyOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyOutline.h; sourceTree = "<group>"; };
CE003CBE11242D00004B0AA7 /* PyRegistrable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyRegistrable.h; path = ../../cocoalib/PyRegistrable.h; sourceTree = SOURCE_ROOT; };
CE003CC011242D00004B0AA7 /* HSOutlineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSOutlineView.h; sourceTree = "<group>"; };
CE003CC111242D00004B0AA7 /* HSOutlineView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSOutlineView.m; sourceTree = "<group>"; };
CE003CC211242D00004B0AA7 /* NSIndexPathAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSIndexPathAdditions.h; sourceTree = "<group>"; };
CE003CC311242D00004B0AA7 /* NSIndexPathAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSIndexPathAdditions.m; sourceTree = "<group>"; };
CE003CC411242D00004B0AA7 /* NSTableViewAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSTableViewAdditions.h; sourceTree = "<group>"; };
CE003CC511242D00004B0AA7 /* NSTableViewAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSTableViewAdditions.m; sourceTree = "<group>"; };
CE003CCD11242D2C004B0AA7 /* DirectoryOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryOutline.h; path = ../base/DirectoryOutline.h; sourceTree = SOURCE_ROOT; };
CE003CCE11242D2C004B0AA7 /* DirectoryOutline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryOutline.m; path = ../base/DirectoryOutline.m; sourceTree = SOURCE_ROOT; };
CE003CCF11242D2C004B0AA7 /* PyDirectoryOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDirectoryOutline.h; path = ../base/PyDirectoryOutline.h; sourceTree = SOURCE_ROOT; };
CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_me_help; path = ../../help_me/dupeguru_me_help; sourceTree = "<group>"; };
CE0A0BFE1175A1C000DCA3C6 /* HSTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSTable.h; sourceTree = "<group>"; };
CE0A0BFF1175A1C000DCA3C6 /* HSTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSTable.m; sourceTree = "<group>"; };
CE0A0C011175A1DE00DCA3C6 /* ProblemDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProblemDialog.h; path = ../base/ProblemDialog.h; sourceTree = SOURCE_ROOT; };
CE0A0C021175A1DE00DCA3C6 /* ProblemDialog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProblemDialog.m; path = ../base/ProblemDialog.m; sourceTree = SOURCE_ROOT; };
CE0A0C031175A1DE00DCA3C6 /* PyProblemDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyProblemDialog.h; path = ../base/PyProblemDialog.h; sourceTree = SOURCE_ROOT; };
CE0A0C051175A24800DCA3C6 /* ProblemDialog.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = ProblemDialog.xib; path = ../base/xib/ProblemDialog.xib; sourceTree = SOURCE_ROOT; };
CE0A0C131175A28100DCA3C6 /* PyTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyTable.h; sourceTree = "<group>"; };
CE1425880AFB718500BD5167 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = "<absolute>"; };
CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; };
CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; };
@@ -91,13 +131,10 @@
CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; };
CE4B59C51119919700C06C9E /* ErrorReportWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ErrorReportWindow.xib; sourceTree = "<group>"; };
CE4B59C61119919700C06C9E /* progress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = progress.xib; sourceTree = "<group>"; };
CE4B59C71119919700C06C9E /* registration.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = registration.xib; sourceTree = "<group>"; };
CE515DE00FC6C12E00EC695D /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; };
CE515DE10FC6C12E00EC695D /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; };
CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; };
CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = ../../cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; };
CE515DE40FC6C12E00EC695D /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; };
CE515DE50FC6C12E00EC695D /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; };
CE515DE60FC6C12E00EC695D /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; };
CE515DE70FC6C12E00EC695D /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; };
CE515DE80FC6C12E00EC695D /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; };
@@ -117,6 +154,8 @@
CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = ../base/PyDupeGuru.h; sourceTree = SOURCE_ROOT; };
CE515E1B0FC6C19300EC695D /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = ../base/ResultWindow.h; sourceTree = SOURCE_ROOT; };
CE515E1C0FC6C19300EC695D /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = ../base/ResultWindow.m; sourceTree = SOURCE_ROOT; };
CE578301124DFC660004769C /* HSTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSTableView.h; path = ../../cocoalib/views/HSTableView.h; sourceTree = SOURCE_ROOT; };
CE578302124DFC660004769C /* HSTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSTableView.m; path = ../../cocoalib/views/HSTableView.m; sourceTree = SOURCE_ROOT; };
CE6032BE0FE6784C007E33FF /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = ../base/DetailsPanel.h; sourceTree = SOURCE_ROOT; };
CE6032BF0FE6784C007E33FF /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = ../base/DetailsPanel.m; sourceTree = SOURCE_ROOT; };
CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; };
@@ -125,9 +164,14 @@
CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = "<group>"; };
CE900AD1109B238600754048 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = "<group>"; };
CE900AD6109B2A9B00754048 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../base/xib/MainMenu.xib; sourceTree = "<group>"; };
CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = "<group>"; };
CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = "<group>"; };
CEB14D26124DFC2800FA7481 /* PyResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyResultTable.h; path = ../base/PyResultTable.h; sourceTree = SOURCE_ROOT; };
CEB14D27124DFC2800FA7481 /* ResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultTable.h; path = ../base/ResultTable.h; sourceTree = SOURCE_ROOT; };
CEB14D28124DFC2800FA7481 /* ResultTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultTable.m; path = ../base/ResultTable.m; sourceTree = SOURCE_ROOT; };
CECC563A12144A9000ABF262 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = ../../cocoalib/en.lproj/registration.xib; sourceTree = SOURCE_ROOT; };
CED0A591111C9FD10020AD7D /* PyDetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDetailsPanel.h; path = ../base/PyDetailsPanel.h; sourceTree = SOURCE_ROOT; };
CEDF07A0112493B200EE5BC0 /* PyStatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyStatsLabel.h; path = ../base/PyStatsLabel.h; sourceTree = SOURCE_ROOT; };
CEDF07A1112493B200EE5BC0 /* StatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StatsLabel.h; path = ../base/StatsLabel.h; sourceTree = SOURCE_ROOT; };
CEDF07A2112493B200EE5BC0 /* StatsLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = StatsLabel.m; path = ../base/StatsLabel.m; sourceTree = SOURCE_ROOT; };
CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = "<group>"; };
CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = ../../images/folder32.png; sourceTree = SOURCE_ROOT; };
CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = ../../images/details32.png; sourceTree = SOURCE_ROOT; };
@@ -148,21 +192,19 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
080E96DDFE201D6D7F000001 /* Classes */ = {
080E96DDFE201D6D7F000001 /* DGME */ = {
isa = PBXGroup;
children = (
CE381C9509914ACE003581CE /* AppDelegate.h */,
CE381C9409914ACE003581CE /* AppDelegate.m */,
CE848A1809DD85810004CB44 /* Consts.h */,
CECA899A09DB132E00A3D774 /* DetailsPanel.h */,
CECA899B09DB132E00A3D774 /* DetailsPanel.m */,
CE68EE6509ABC48000971085 /* DirectoryPanel.h */,
CE68EE6609ABC48000971085 /* DirectoryPanel.m */,
CEFF18A009A4D387005E6321 /* PyDupeGuru.h */,
CE381C9B09914ADF003581CE /* ResultWindow.h */,
CE381C9A09914ADF003581CE /* ResultWindow.m */,
);
name = Classes;
name = DGME;
sourceTree = "<group>";
};
1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = {
@@ -195,7 +237,7 @@
29B97314FDCFA39411CA2CEA /* dupeguru */ = {
isa = PBXGroup;
children = (
080E96DDFE201D6D7F000001 /* Classes */,
080E96DDFE201D6D7F000001 /* DGME */,
CE515E140FC6C17900EC695D /* dgbase */,
CE515DDD0FC6C09400EC695D /* cocoalib */,
29B97315FDCFA39411CA2CEA /* Other Sources */,
@@ -237,6 +279,49 @@
name = Frameworks;
sourceTree = "<group>";
};
CE003CB211242D00004B0AA7 /* controllers */ = {
isa = PBXGroup;
children = (
CE003CB311242D00004B0AA7 /* HSGUIController.h */,
CE003CB411242D00004B0AA7 /* HSGUIController.m */,
CE003CB511242D00004B0AA7 /* HSOutline.h */,
CE003CB611242D00004B0AA7 /* HSOutline.m */,
CE0A0BFE1175A1C000DCA3C6 /* HSTable.h */,
CE0A0BFF1175A1C000DCA3C6 /* HSTable.m */,
CE003CB711242D00004B0AA7 /* HSWindowController.h */,
CE003CB811242D00004B0AA7 /* HSWindowController.m */,
);
name = controllers;
path = ../../cocoalib/controllers;
sourceTree = SOURCE_ROOT;
};
CE003CBB11242D00004B0AA7 /* proxies */ = {
isa = PBXGroup;
children = (
CE003CBC11242D00004B0AA7 /* PyGUI.h */,
CE003CBD11242D00004B0AA7 /* PyOutline.h */,
CE0A0C131175A28100DCA3C6 /* PyTable.h */,
);
name = proxies;
path = ../../cocoalib/proxies;
sourceTree = SOURCE_ROOT;
};
CE003CBF11242D00004B0AA7 /* views */ = {
isa = PBXGroup;
children = (
CE578301124DFC660004769C /* HSTableView.h */,
CE578302124DFC660004769C /* HSTableView.m */,
CE003CC011242D00004B0AA7 /* HSOutlineView.h */,
CE003CC111242D00004B0AA7 /* HSOutlineView.m */,
CE003CC211242D00004B0AA7 /* NSIndexPathAdditions.h */,
CE003CC311242D00004B0AA7 /* NSIndexPathAdditions.m */,
CE003CC411242D00004B0AA7 /* NSTableViewAdditions.h */,
CE003CC511242D00004B0AA7 /* NSTableViewAdditions.m */,
);
name = views;
path = ../../cocoalib/views;
sourceTree = SOURCE_ROOT;
};
CE3FBDD01094637800B72D77 /* xib */ = {
isa = PBXGroup;
children = (
@@ -244,6 +329,7 @@
CE3FBDD11094637800B72D77 /* DetailsPanel.xib */,
CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */,
CE900AD1109B238600754048 /* Preferences.xib */,
CE0A0C051175A24800DCA3C6 /* ProblemDialog.xib */,
);
path = xib;
sourceTree = "<group>";
@@ -261,9 +347,9 @@
CE4B59C41119919700C06C9E /* xib */ = {
isa = PBXGroup;
children = (
CECC563912144A9000ABF262 /* registration.xib */,
CE4B59C51119919700C06C9E /* ErrorReportWindow.xib */,
CE4B59C61119919700C06C9E /* progress.xib */,
CE4B59C71119919700C06C9E /* registration.xib */,
);
name = xib;
path = ../../cocoalib/xib;
@@ -272,17 +358,21 @@
CE515DDD0FC6C09400EC695D /* cocoalib */ = {
isa = PBXGroup;
children = (
CE003CB211242D00004B0AA7 /* controllers */,
CE003CBB11242D00004B0AA7 /* proxies */,
CE003CBF11242D00004B0AA7 /* views */,
CE4B59C41119919700C06C9E /* xib */,
CE49DEF10FDFEB810098617B /* brsinglelineformatter */,
CE515DE00FC6C12E00EC695D /* Dialogs.h */,
CE515DE10FC6C12E00EC695D /* Dialogs.m */,
CE515DE20FC6C12E00EC695D /* HSErrorReportWindow.h */,
CE515DE30FC6C12E00EC695D /* HSErrorReportWindow.m */,
CE515DE40FC6C12E00EC695D /* Outline.h */,
CE515DE50FC6C12E00EC695D /* Outline.m */,
CE003CB911242D00004B0AA7 /* NSEventAdditions.h */,
CE003CBA11242D00004B0AA7 /* NSEventAdditions.m */,
CE515DE60FC6C12E00EC695D /* ProgressController.h */,
CE515DE70FC6C12E00EC695D /* ProgressController.m */,
CE515DE80FC6C12E00EC695D /* PyApp.h */,
CE003CBE11242D00004B0AA7 /* PyRegistrable.h */,
CE515DE90FC6C12E00EC695D /* RecentDirectories.h */,
CE515DEA0FC6C12E00EC695D /* RecentDirectories.m */,
CE515DEB0FC6C12E00EC695D /* RegistrationInterface.h */,
@@ -298,6 +388,12 @@
CE515E140FC6C17900EC695D /* dgbase */ = {
isa = PBXGroup;
children = (
CEB14D26124DFC2800FA7481 /* PyResultTable.h */,
CEB14D27124DFC2800FA7481 /* ResultTable.h */,
CEB14D28124DFC2800FA7481 /* ResultTable.m */,
CE003CCD11242D2C004B0AA7 /* DirectoryOutline.h */,
CE003CCE11242D2C004B0AA7 /* DirectoryOutline.m */,
CE003CCF11242D2C004B0AA7 /* PyDirectoryOutline.h */,
CE515E150FC6C19300EC695D /* AppDelegate.h */,
CE515E160FC6C19300EC695D /* AppDelegate.m */,
CE515E170FC6C19300EC695D /* Consts.h */,
@@ -305,10 +401,16 @@
CE6032BF0FE6784C007E33FF /* DetailsPanel.m */,
CE515E180FC6C19300EC695D /* DirectoryPanel.h */,
CE515E190FC6C19300EC695D /* DirectoryPanel.m */,
CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */,
CED0A591111C9FD10020AD7D /* PyDetailsPanel.h */,
CE0A0C011175A1DE00DCA3C6 /* ProblemDialog.h */,
CE0A0C021175A1DE00DCA3C6 /* ProblemDialog.m */,
CE515E1B0FC6C19300EC695D /* ResultWindow.h */,
CE515E1C0FC6C19300EC695D /* ResultWindow.m */,
CEDF07A1112493B200EE5BC0 /* StatsLabel.h */,
CEDF07A2112493B200EE5BC0 /* StatsLabel.m */,
CE515E1A0FC6C19300EC695D /* PyDupeGuru.h */,
CED0A591111C9FD10020AD7D /* PyDetailsPanel.h */,
CE0A0C031175A1DE00DCA3C6 /* PyProblemDialog.h */,
CEDF07A0112493B200EE5BC0 /* PyStatsLabel.h */,
);
name = dgbase;
sourceTree = "<group>";
@@ -354,6 +456,13 @@
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 3.0";
hasScannedForEncodings = 1;
knownRegions = (
English,
Japanese,
French,
German,
en,
);
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
projectDirPath = "";
projectRoot = "";
@@ -381,7 +490,8 @@
CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */,
CE4B59C81119919700C06C9E /* ErrorReportWindow.xib in Resources */,
CE4B59C91119919700C06C9E /* progress.xib in Resources */,
CE4B59CA1119919700C06C9E /* registration.xib in Resources */,
CE0A0C061175A24800DCA3C6 /* ProblemDialog.xib in Resources */,
CECC563B12144A9000ABF262 /* registration.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -398,7 +508,6 @@
CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */,
CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */,
CE515DF40FC6C12E00EC695D /* HSErrorReportWindow.m in Sources */,
CE515DF50FC6C12E00EC695D /* Outline.m in Sources */,
CE515DF60FC6C12E00EC695D /* ProgressController.m in Sources */,
CE515DF70FC6C12E00EC695D /* RecentDirectories.m in Sources */,
CE515DF80FC6C12E00EC695D /* RegistrationInterface.m in Sources */,
@@ -409,11 +518,36 @@
CE515E1F0FC6C19300EC695D /* ResultWindow.m in Sources */,
CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */,
CE6032C00FE6784C007E33FF /* DetailsPanel.m in Sources */,
CE003CC611242D00004B0AA7 /* HSGUIController.m in Sources */,
CE003CC711242D00004B0AA7 /* HSOutline.m in Sources */,
CE003CC811242D00004B0AA7 /* HSWindowController.m in Sources */,
CE003CC911242D00004B0AA7 /* NSEventAdditions.m in Sources */,
CE003CCA11242D00004B0AA7 /* HSOutlineView.m in Sources */,
CE003CCB11242D00004B0AA7 /* NSIndexPathAdditions.m in Sources */,
CE003CCC11242D00004B0AA7 /* NSTableViewAdditions.m in Sources */,
CE003CD011242D2C004B0AA7 /* DirectoryOutline.m in Sources */,
CEDF07A3112493B200EE5BC0 /* StatsLabel.m in Sources */,
CE0A0C001175A1C000DCA3C6 /* HSTable.m in Sources */,
CE0A0C041175A1DE00DCA3C6 /* ProblemDialog.m in Sources */,
CEB14D29124DFC2800FA7481 /* ResultTable.m in Sources */,
CE578303124DFC660004769C /* HSTableView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
CECC563912144A9000ABF262 /* registration.xib */ = {
isa = PBXVariantGroup;
children = (
CECC563A12144A9000ABF262 /* en */,
);
name = registration.xib;
path = ../../cocoalib/xib;
sourceTree = SOURCE_ROOT;
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4C08A954540054247B /* release */ = {
isa = XCBuildConfiguration;

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ http://www.hardcoded.net/licenses/hs_license
[d setObject:[NSNumber numberWithBool:NO] forKey:@"matchScaled"];
[d setObject:[NSNumber numberWithBool:YES] forKey:@"mixFileKind"];
[d setObject:[NSNumber numberWithBool:NO] forKey:@"useRegexpFilter"];
[d setObject:[NSNumber numberWithBool:NO] forKey:@"ignoreHardlinkMatches"];
[d setObject:[NSNumber numberWithBool:NO] forKey:@"removeEmptyFolders"];
[d setObject:[NSNumber numberWithBool:NO] forKey:@"debug"];
[d setObject:[NSArray array] forKey:@"recentDirectories"];

View File

@@ -44,9 +44,8 @@ static NSString* jobAddIPhoto = @"jobAddIPhoto";
- (void)jobCompleted:(NSNotification *)aNotification
{
if ([[ProgressController mainProgressController] jobId] == jobAddIPhoto)
{
[directories reloadData];
if ([[ProgressController mainProgressController] jobId] == jobAddIPhoto) {
[outlineView reloadData];
}
}
@end

View File

@@ -23,7 +23,7 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>1.8.2</string>
<string>1.11.0</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@@ -7,31 +7,10 @@ http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "Outline.h"
#import "../base/ResultWindow.h"
@interface ResultWindow : ResultWindowBase
{
IBOutlet NSSearchField *filterField;
NSMutableIndexSet *_deltaColumns;
}
- (IBAction)clearIgnoreList:(id)sender;
@interface ResultWindow : ResultWindowBase {}
- (IBAction)clearPictureCache:(id)sender;
- (IBAction)filter:(id)sender;
- (IBAction)ignoreSelected:(id)sender;
- (IBAction)markAll:(id)sender;
- (IBAction)markInvert:(id)sender;
- (IBAction)markNone:(id)sender;
- (IBAction)markSelected:(id)sender;
- (IBAction)markToggle:(id)sender;
- (IBAction)openSelected:(id)sender;
- (IBAction)refresh:(id)sender;
- (IBAction)removeMarked:(id)sender;
- (IBAction)removeSelected:(id)sender;
- (IBAction)renameSelected:(id)sender;
- (IBAction)revealSelected:(id)sender;
- (IBAction)startDuplicateScan:(id)sender;
- (IBAction)toggleDelta:(id)sender;
- (IBAction)toggleDirectories:(id)sender;
@end

View File

@@ -20,31 +20,12 @@ http://www.hardcoded.net/licenses/hs_license
{
[super awakeFromNib];
[[self window] setTitle:@"dupeGuru Picture Edition"];
_displayDelta = NO;
_powerMode = NO;
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)] retain];
[_deltaColumns removeIndex:3];
[_deltaColumns removeIndex:4];
[deltaSwitch setSelectedSegment:0];
[pmSwitch setSelectedSegment:0];
[py setDisplayDeltaValues:b2n(_displayDelta)];
[matches setTarget:self];
[matches setDoubleAction:@selector(openSelected:)];
[self refreshStats];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndex:2];
[deltaColumns addIndex:5];
[table setDeltaColumns:deltaColumns];
}
/* Actions */
- (IBAction)clearIgnoreList:(id)sender
{
int i = n2i([py getIgnoreListCount]);
if (!i)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO
return;
[py clearIgnoreList];
}
- (IBAction)clearPictureCache:(id)sender
{
if ([Dialogs askYesNo:@"Do you really want to remove all your cached picture analysis?"] == NSAlertSecondButtonReturn) // NO
@@ -52,101 +33,6 @@ http://www.hardcoded.net/licenses/hs_license
[(PyDupeGuru *)py clearPictureCache];
}
- (IBAction)filter:(id)sender
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setEscapeFilterRegexp:b2n(!n2b([ud objectForKey:@"useRegexpFilter"]))];
[py applyFilter:[filterField stringValue]];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)ignoreSelected:(id)sender
{
NSArray *nodeList = [self getSelected:YES];
if (![nodeList count])
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
return;
[self performPySelection:[self getSelectedPaths:YES]];
[py addSelectedToIgnoreList];
[py removeSelected];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)markAll:(id)sender
{
[py markAll];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markInvert:(id)sender
{
[py markInvert];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markNone:(id)sender
{
[py markNone];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:YES]];
[py toggleSelectedMark];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markToggle:(id)sender
{
OVNode *node = [matches itemAtRow:[matches clickedRow]];
[self performPySelection:[NSArray arrayWithObject:p2a([node indexPath])]];
[py toggleSelectedMark];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)openSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:NO]];
[py openSelected];
}
- (IBAction)refresh:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)removeMarked:(id)sender
{
int mark_count = [[py getMarkCount] intValue];
if (!mark_count)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO
return;
[py removeMarked];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)removeSelected:(id)sender
{
NSArray *nodeList = [self getSelected:YES];
if (![nodeList count])
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
return;
[self performPySelection:[self getSelectedPaths:YES]];
[py removeSelected];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)renameSelected:(id)sender
{
int col = [matches columnWithIdentifier:@"0"];
int row = [matches selectedRow];
[matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES];
}
- (IBAction)resetColumnsToDefault:(id)sender
{
NSMutableArray *columnsOrder = [NSMutableArray array];
@@ -154,22 +40,16 @@ http://www.hardcoded.net/licenses/hs_license
[columnsOrder addObject:@"1"];
[columnsOrder addObject:@"2"];
[columnsOrder addObject:@"4"];
[columnsOrder addObject:@"7"];
[columnsOrder addObject:@"6"];
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
[columnsWidth setObject:i2n(121) forKey:@"0"];
[columnsWidth setObject:i2n(120) forKey:@"1"];
[columnsWidth setObject:i2n(162) forKey:@"0"];
[columnsWidth setObject:i2n(142) forKey:@"1"];
[columnsWidth setObject:i2n(63) forKey:@"2"];
[columnsWidth setObject:i2n(73) forKey:@"4"];
[columnsWidth setObject:i2n(58) forKey:@"7"];
[columnsWidth setObject:i2n(58) forKey:@"6"];
[self restoreColumnsPosition:columnsOrder widths:columnsWidth];
}
- (IBAction)revealSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:NO]];
[py revealSelected];
}
- (IBAction)startDuplicateScan:(id)sender
{
if ([matches numberOfRows] > 0)
@@ -180,15 +60,12 @@ http://www.hardcoded.net/licenses/hs_license
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
PyDupeGuru *_py = (PyDupeGuru *)py;
[_py setMinMatchPercentage:[ud objectForKey:@"minMatchPercentage"]];
[_py setMixFileKind:[ud objectForKey:@"mixFileKind"]];
[_py setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[_py setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[_py setMatchScaled:[ud objectForKey:@"matchScaled"]];
int r = n2i([py doScan]);
[matches reloadData];
[self refreshStats];
if (r != 0)
[[ProgressController mainProgressController] hide];
if (r == 1)
[Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."];
if (r == 3)
{
[Dialogs showMessage:@"The selected directories contain no scannable file."];
@@ -196,15 +73,6 @@ http://www.hardcoded.net/licenses/hs_license
}
}
- (IBAction)toggleDelta:(id)sender
{
if ([deltaSwitch selectedSegment] == 1)
[deltaSwitch setSelectedSegment:0];
else
[deltaSwitch setSelectedSegment:1];
[self changeDelta:sender];
}
- (IBAction)toggleDirectories:(id)sender
{
[(AppDelegate *)app toggleDirectories:sender];
@@ -222,34 +90,8 @@ http://www.hardcoded.net/licenses/hs_license
[_resultColumns addObject:sizeCol];
[_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Dimensions" width:80 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Creation" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Match %" width:58 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]];
}
/* Delegate */
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
OVNode *node = item;
if ([[tableColumn identifier] isEqual:@"mark"])
{
[cell setEnabled: [node isMarkable]];
}
if ([cell isKindOfClass:[NSTextFieldCell class]])
{
// Determine if the text color will be blue due to directory being reference.
NSTextFieldCell *textCell = cell;
if ([node isMarkable])
[textCell setTextColor:[NSColor blackColor]];
else
[textCell setTextColor:[NSColor blueColor]];
if ((_displayDelta) && (_powerMode || ([node level] > 1)))
{
int i = [[tableColumn identifier] intValue];
if ([_deltaColumns containsIndex:i])
[textCell setTextColor:[NSColor orangeColor]];
}
}
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Match %" width:58 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Dupe Count" width:80 refCol:refCol]];
}
@end

View File

@@ -7,31 +7,37 @@
from core.app_cocoa_inter import PyDupeGuruBase, PyDetailsPanel
from core_pe import app_cocoa as app_pe_cocoa
# Fix py2app imports which chokes on relative imports
from core_pe import block, cache, matchbase, data, _block_osx
# Fix py2app imports which chokes on relative imports and other stuff
import hsutil.conflict
import core.engine, core.fs, core.app
import core_pe.block, core_pe.cache, core_pe.matchbase, core_pe.data, core_pe._block_osx
import xml.etree.ElementPath
import gzip
import aem.kae
import appscript.defaultterminology
class PyDupeGuru(PyDupeGuruBase):
def init(self):
self = super(PyDupeGuru, self).init()
self.app = app_pe_cocoa.DupeGuruPE()
self.py = app_pe_cocoa.DupeGuruPE()
return self
def clearPictureCache(self):
self.app.scanner.clear_picture_cache()
self.py.scanner.clear_picture_cache()
#---Information
def getSelectedDupePath(self):
return unicode(self.app.selected_dupe_path())
return str(self.py.selected_dupe_path())
def getSelectedDupeRefPath(self):
return unicode(self.app.selected_dupe_ref_path())
return str(self.py.selected_dupe_ref_path())
#---Properties
def setMatchScaled_(self,match_scaled):
self.app.scanner.match_scaled = match_scaled
self.py.scanner.match_scaled = match_scaled
def setMinMatchPercentage_(self,percentage):
self.app.scanner.threshold = int(percentage)
self.py.scanner.threshold = int(percentage)
#---Registration
def appName(self):

View File

@@ -12,6 +12,9 @@
CE031751109B340A00517EE6 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE031750109B340A00517EE6 /* Preferences.xib */; };
CE031754109B345200517EE6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE031753109B345200517EE6 /* MainMenu.xib */; };
CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */; };
CE0C2AB61177011000BC749F /* HSTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C2AB51177011000BC749F /* HSTable.m */; };
CE0C2ABD1177014200BC749F /* ProblemDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C2ABB1177014200BC749F /* ProblemDialog.m */; };
CE0C2AC81177021600BC749F /* ProblemDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE0C2AC71177021600BC749F /* ProblemDialog.xib */; };
CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; };
CE15C8C00ADEB8D40061D4A5 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; };
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; };
@@ -24,10 +27,8 @@
CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C8A710946CE20078B0DB /* DetailsPanel.xib */; };
CE7AC9181119911200D02F6C /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE7AC9151119911200D02F6C /* ErrorReportWindow.xib */; };
CE7AC9191119911200D02F6C /* progress.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE7AC9161119911200D02F6C /* progress.xib */; };
CE7AC91A1119911200D02F6C /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE7AC9171119911200D02F6C /* registration.xib */; };
CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; };
CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; };
CE80DB300FC192D60086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB200FC192D60086DCA6 /* Outline.m */; };
CE80DB310FC192D60086DCA6 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB220FC192D60086DCA6 /* ProgressController.m */; };
CE80DB320FC192D60086DCA6 /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB250FC192D60086DCA6 /* RecentDirectories.m */; };
CE80DB330FC192D60086DCA6 /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */; };
@@ -39,10 +40,22 @@
CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */; };
CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB890FC1951C0086DCA6 /* ResultWindow.m */; };
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
CE895D7B12144A7800E74705 /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE895D7912144A7800E74705 /* registration.xib */; };
CE95865F112C516400F95FD2 /* StatsLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE95865D112C516400F95FD2 /* StatsLabel.m */; };
CE9EA7561122C96C008CD2BC /* HSGUIController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7441122C96C008CD2BC /* HSGUIController.m */; };
CE9EA7571122C96C008CD2BC /* HSOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7461122C96C008CD2BC /* HSOutline.m */; };
CE9EA7581122C96C008CD2BC /* HSWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7481122C96C008CD2BC /* HSWindowController.m */; };
CE9EA7591122C96C008CD2BC /* NSEventAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA74A1122C96C008CD2BC /* NSEventAdditions.m */; };
CE9EA75A1122C96C008CD2BC /* HSOutlineView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7511122C96C008CD2BC /* HSOutlineView.m */; };
CE9EA75B1122C96C008CD2BC /* NSIndexPathAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7531122C96C008CD2BC /* NSIndexPathAdditions.m */; };
CE9EA75C1122C96C008CD2BC /* NSTableViewAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7551122C96C008CD2BC /* NSTableViewAdditions.m */; };
CE9EA7721122CA0B008CD2BC /* DirectoryOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE9EA7701122CA0B008CD2BC /* DirectoryOutline.m */; };
CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */; };
CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; };
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; };
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; };
CEF12A7E124DFD400087B51D /* HSTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF12A7D124DFD400087B51D /* HSTableView.m */; };
CEF12A84124DFD620087B51D /* ResultTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF12A83124DFD620087B51D /* ResultTable.m */; };
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; };
CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; };
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; };
@@ -75,6 +88,13 @@
CE031750109B340A00517EE6 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = "<group>"; };
CE031753109B345200517EE6 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../../base/xib/MainMenu.xib; sourceTree = "<group>"; };
CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_pe_help; path = ../../help_pe/dupeguru_pe_help; sourceTree = SOURCE_ROOT; };
CE0C2AAA117700E700BC749F /* PyTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyTable.h; sourceTree = "<group>"; };
CE0C2AB41177011000BC749F /* HSTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSTable.h; sourceTree = "<group>"; };
CE0C2AB51177011000BC749F /* HSTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSTable.m; sourceTree = "<group>"; };
CE0C2ABA1177014200BC749F /* ProblemDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProblemDialog.h; path = ../base/ProblemDialog.h; sourceTree = SOURCE_ROOT; };
CE0C2ABB1177014200BC749F /* ProblemDialog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProblemDialog.m; path = ../base/ProblemDialog.m; sourceTree = SOURCE_ROOT; };
CE0C2ABC1177014200BC749F /* PyProblemDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyProblemDialog.h; path = ../base/PyProblemDialog.h; sourceTree = SOURCE_ROOT; };
CE0C2AC71177021600BC749F /* ProblemDialog.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = ProblemDialog.xib; path = ../base/xib/ProblemDialog.xib; sourceTree = SOURCE_ROOT; };
CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = "<absolute>"; };
CE18126F111C9D5100E49FCE /* PyDetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDetailsPanel.h; path = ../base/PyDetailsPanel.h; sourceTree = SOURCE_ROOT; };
CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; };
@@ -91,13 +111,10 @@
CE77C8A710946CE20078B0DB /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsPanel.xib; sourceTree = "<group>"; };
CE7AC9151119911200D02F6C /* ErrorReportWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ErrorReportWindow.xib; sourceTree = "<group>"; };
CE7AC9161119911200D02F6C /* progress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = progress.xib; sourceTree = "<group>"; };
CE7AC9171119911200D02F6C /* registration.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = registration.xib; sourceTree = "<group>"; };
CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = ../../cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; };
CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; };
CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; };
CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = ../../cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; };
CE80DB1F0FC192D60086DCA6 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; };
CE80DB200FC192D60086DCA6 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; };
CE80DB210FC192D60086DCA6 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; };
CE80DB220FC192D60086DCA6 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; };
CE80DB230FC192D60086DCA6 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; };
@@ -122,11 +139,40 @@
CE80DB880FC1951C0086DCA6 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = ../base/ResultWindow.h; sourceTree = SOURCE_ROOT; };
CE80DB890FC1951C0086DCA6 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = ../base/ResultWindow.m; sourceTree = SOURCE_ROOT; };
CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = "<group>"; };
CE895D7A12144A7800E74705 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = ../../cocoalib/en.lproj/registration.xib; sourceTree = SOURCE_ROOT; };
CE958659112C516400F95FD2 /* PyStatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyStatsLabel.h; path = ../base/PyStatsLabel.h; sourceTree = SOURCE_ROOT; };
CE95865C112C516400F95FD2 /* StatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StatsLabel.h; path = ../base/StatsLabel.h; sourceTree = SOURCE_ROOT; };
CE95865D112C516400F95FD2 /* StatsLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = StatsLabel.m; path = ../base/StatsLabel.m; sourceTree = SOURCE_ROOT; };
CE9EA7431122C96C008CD2BC /* HSGUIController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSGUIController.h; sourceTree = "<group>"; };
CE9EA7441122C96C008CD2BC /* HSGUIController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSGUIController.m; sourceTree = "<group>"; };
CE9EA7451122C96C008CD2BC /* HSOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSOutline.h; sourceTree = "<group>"; };
CE9EA7461122C96C008CD2BC /* HSOutline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSOutline.m; sourceTree = "<group>"; };
CE9EA7471122C96C008CD2BC /* HSWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSWindowController.h; sourceTree = "<group>"; };
CE9EA7481122C96C008CD2BC /* HSWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSWindowController.m; sourceTree = "<group>"; };
CE9EA7491122C96C008CD2BC /* NSEventAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSEventAdditions.h; path = ../../cocoalib/NSEventAdditions.h; sourceTree = SOURCE_ROOT; };
CE9EA74A1122C96C008CD2BC /* NSEventAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSEventAdditions.m; path = ../../cocoalib/NSEventAdditions.m; sourceTree = SOURCE_ROOT; };
CE9EA74C1122C96C008CD2BC /* PyGUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyGUI.h; sourceTree = "<group>"; };
CE9EA74D1122C96C008CD2BC /* PyOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyOutline.h; sourceTree = "<group>"; };
CE9EA74E1122C96C008CD2BC /* PyRegistrable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyRegistrable.h; path = ../../cocoalib/PyRegistrable.h; sourceTree = SOURCE_ROOT; };
CE9EA7501122C96C008CD2BC /* HSOutlineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSOutlineView.h; sourceTree = "<group>"; };
CE9EA7511122C96C008CD2BC /* HSOutlineView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSOutlineView.m; sourceTree = "<group>"; };
CE9EA7521122C96C008CD2BC /* NSIndexPathAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSIndexPathAdditions.h; sourceTree = "<group>"; };
CE9EA7531122C96C008CD2BC /* NSIndexPathAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSIndexPathAdditions.m; sourceTree = "<group>"; };
CE9EA7541122C96C008CD2BC /* NSTableViewAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSTableViewAdditions.h; sourceTree = "<group>"; };
CE9EA7551122C96C008CD2BC /* NSTableViewAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSTableViewAdditions.m; sourceTree = "<group>"; };
CE9EA76F1122CA0B008CD2BC /* DirectoryOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryOutline.h; path = ../base/DirectoryOutline.h; sourceTree = SOURCE_ROOT; };
CE9EA7701122CA0B008CD2BC /* DirectoryOutline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryOutline.m; path = ../base/DirectoryOutline.m; sourceTree = SOURCE_ROOT; };
CE9EA7711122CA0B008CD2BC /* PyDirectoryOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDirectoryOutline.h; path = ../base/PyDirectoryOutline.h; sourceTree = SOURCE_ROOT; };
CEBAE4230FDA97E000B7887D /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; };
CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; };
CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = "<group>"; };
CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = "<group>"; };
CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = "<group>"; };
CEF12A7C124DFD400087B51D /* HSTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSTableView.h; path = ../../cocoalib/views/HSTableView.h; sourceTree = SOURCE_ROOT; };
CEF12A7D124DFD400087B51D /* HSTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSTableView.m; path = ../../cocoalib/views/HSTableView.m; sourceTree = SOURCE_ROOT; };
CEF12A81124DFD620087B51D /* PyResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyResultTable.h; path = ../base/PyResultTable.h; sourceTree = SOURCE_ROOT; };
CEF12A82124DFD620087B51D /* ResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultTable.h; path = ../base/ResultTable.h; sourceTree = SOURCE_ROOT; };
CEF12A83124DFD620087B51D /* ResultTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultTable.m; path = ../base/ResultTable.m; sourceTree = SOURCE_ROOT; };
CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = ../../images/folder32.png; sourceTree = SOURCE_ROOT; };
CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = ../../images/details32.png; sourceTree = SOURCE_ROOT; };
CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = ../../images/preferences32.png; sourceTree = SOURCE_ROOT; };
@@ -147,7 +193,7 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
080E96DDFE201D6D7F000001 /* Classes */ = {
080E96DDFE201D6D7F000001 /* DGPE */ = {
isa = PBXGroup;
children = (
CE381C9509914ACE003581CE /* AppDelegate.h */,
@@ -161,7 +207,7 @@
CE381C9B09914ADF003581CE /* ResultWindow.h */,
CE381C9A09914ADF003581CE /* ResultWindow.m */,
);
name = Classes;
name = DGPE;
sourceTree = "<group>";
};
1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = {
@@ -194,7 +240,7 @@
29B97314FDCFA39411CA2CEA /* dupeguru */ = {
isa = PBXGroup;
children = (
080E96DDFE201D6D7F000001 /* Classes */,
080E96DDFE201D6D7F000001 /* DGPE */,
CE80DB1A0FC192AB0086DCA6 /* cocoalib */,
CE80DB810FC194BD0086DCA6 /* dgbase */,
29B97315FDCFA39411CA2CEA /* Other Sources */,
@@ -243,6 +289,7 @@
CE77C8A710946CE20078B0DB /* DetailsPanel.xib */,
CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */,
CE031750109B340A00517EE6 /* Preferences.xib */,
CE0C2AC71177021600BC749F /* ProblemDialog.xib */,
);
path = xib;
sourceTree = "<group>";
@@ -250,9 +297,9 @@
CE7AC9141119911200D02F6C /* xib */ = {
isa = PBXGroup;
children = (
CE895D7912144A7800E74705 /* registration.xib */,
CE7AC9151119911200D02F6C /* ErrorReportWindow.xib */,
CE7AC9161119911200D02F6C /* progress.xib */,
CE7AC9171119911200D02F6C /* registration.xib */,
);
name = xib;
path = ../../cocoalib/xib;
@@ -261,21 +308,25 @@
CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = {
isa = PBXGroup;
children = (
CE9EA7421122C96C008CD2BC /* controllers */,
CE9EA74B1122C96C008CD2BC /* proxies */,
CE9EA74F1122C96C008CD2BC /* views */,
CE7AC9141119911200D02F6C /* xib */,
CEBAE4220FDA97E000B7887D /* brsinglelineformatter */,
CE80DB480FC193770086DCA6 /* NSImageAdditions.h */,
CE80DB490FC193770086DCA6 /* NSImageAdditions.m */,
CE80DB450FC193650086DCA6 /* NSNotificationAdditions.h */,
CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */,
CE9EA7491122C96C008CD2BC /* NSEventAdditions.h */,
CE9EA74A1122C96C008CD2BC /* NSEventAdditions.m */,
CE80DB1B0FC192D60086DCA6 /* Dialogs.h */,
CE80DB1C0FC192D60086DCA6 /* Dialogs.m */,
CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */,
CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */,
CE80DB1F0FC192D60086DCA6 /* Outline.h */,
CE80DB200FC192D60086DCA6 /* Outline.m */,
CE80DB210FC192D60086DCA6 /* ProgressController.h */,
CE80DB220FC192D60086DCA6 /* ProgressController.m */,
CE80DB230FC192D60086DCA6 /* PyApp.h */,
CE9EA74E1122C96C008CD2BC /* PyRegistrable.h */,
CE80DB240FC192D60086DCA6 /* RecentDirectories.h */,
CE80DB250FC192D60086DCA6 /* RecentDirectories.m */,
CE80DB260FC192D60086DCA6 /* RegistrationInterface.h */,
@@ -291,6 +342,9 @@
CE80DB810FC194BD0086DCA6 /* dgbase */ = {
isa = PBXGroup;
children = (
CEF12A81124DFD620087B51D /* PyResultTable.h */,
CEF12A82124DFD620087B51D /* ResultTable.h */,
CEF12A83124DFD620087B51D /* ResultTable.m */,
CE80DB820FC1951C0086DCA6 /* AppDelegate.h */,
CE80DB830FC1951C0086DCA6 /* AppDelegate.m */,
CE80DB840FC1951C0086DCA6 /* Consts.h */,
@@ -298,14 +352,66 @@
CE6044EB0FE6796200B71262 /* DetailsPanel.m */,
CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */,
CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */,
CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */,
CE18126F111C9D5100E49FCE /* PyDetailsPanel.h */,
CE9EA76F1122CA0B008CD2BC /* DirectoryOutline.h */,
CE9EA7701122CA0B008CD2BC /* DirectoryOutline.m */,
CE0C2ABA1177014200BC749F /* ProblemDialog.h */,
CE0C2ABB1177014200BC749F /* ProblemDialog.m */,
CE80DB880FC1951C0086DCA6 /* ResultWindow.h */,
CE80DB890FC1951C0086DCA6 /* ResultWindow.m */,
CE95865C112C516400F95FD2 /* StatsLabel.h */,
CE95865D112C516400F95FD2 /* StatsLabel.m */,
CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */,
CE18126F111C9D5100E49FCE /* PyDetailsPanel.h */,
CE9EA7711122CA0B008CD2BC /* PyDirectoryOutline.h */,
CE0C2ABC1177014200BC749F /* PyProblemDialog.h */,
CE958659112C516400F95FD2 /* PyStatsLabel.h */,
);
name = dgbase;
sourceTree = "<group>";
};
CE9EA7421122C96C008CD2BC /* controllers */ = {
isa = PBXGroup;
children = (
CE9EA7431122C96C008CD2BC /* HSGUIController.h */,
CE9EA7441122C96C008CD2BC /* HSGUIController.m */,
CE9EA7451122C96C008CD2BC /* HSOutline.h */,
CE9EA7461122C96C008CD2BC /* HSOutline.m */,
CE0C2AB41177011000BC749F /* HSTable.h */,
CE0C2AB51177011000BC749F /* HSTable.m */,
CE9EA7471122C96C008CD2BC /* HSWindowController.h */,
CE9EA7481122C96C008CD2BC /* HSWindowController.m */,
);
name = controllers;
path = ../../cocoalib/controllers;
sourceTree = SOURCE_ROOT;
};
CE9EA74B1122C96C008CD2BC /* proxies */ = {
isa = PBXGroup;
children = (
CE9EA74C1122C96C008CD2BC /* PyGUI.h */,
CE9EA74D1122C96C008CD2BC /* PyOutline.h */,
CE0C2AAA117700E700BC749F /* PyTable.h */,
);
name = proxies;
path = ../../cocoalib/proxies;
sourceTree = SOURCE_ROOT;
};
CE9EA74F1122C96C008CD2BC /* views */ = {
isa = PBXGroup;
children = (
CEF12A7C124DFD400087B51D /* HSTableView.h */,
CEF12A7D124DFD400087B51D /* HSTableView.m */,
CE9EA7501122C96C008CD2BC /* HSOutlineView.h */,
CE9EA7511122C96C008CD2BC /* HSOutlineView.m */,
CE9EA7521122C96C008CD2BC /* NSIndexPathAdditions.h */,
CE9EA7531122C96C008CD2BC /* NSIndexPathAdditions.m */,
CE9EA7541122C96C008CD2BC /* NSTableViewAdditions.h */,
CE9EA7551122C96C008CD2BC /* NSTableViewAdditions.m */,
);
name = views;
path = ../../cocoalib/views;
sourceTree = SOURCE_ROOT;
};
CEBAE4220FDA97E000B7887D /* brsinglelineformatter */ = {
isa = PBXGroup;
children = (
@@ -357,6 +463,13 @@
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 3.0";
hasScannedForEncodings = 1;
knownRegions = (
English,
Japanese,
French,
German,
en,
);
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
projectDirPath = "";
projectRoot = "";
@@ -385,7 +498,8 @@
CE031754109B345200517EE6 /* MainMenu.xib in Resources */,
CE7AC9181119911200D02F6C /* ErrorReportWindow.xib in Resources */,
CE7AC9191119911200D02F6C /* progress.xib in Resources */,
CE7AC91A1119911200D02F6C /* registration.xib in Resources */,
CE0C2AC81177021600BC749F /* ProblemDialog.xib in Resources */,
CE895D7B12144A7800E74705 /* registration.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -403,7 +517,6 @@
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */,
CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */,
CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */,
CE80DB300FC192D60086DCA6 /* Outline.m in Sources */,
CE80DB310FC192D60086DCA6 /* ProgressController.m in Sources */,
CE80DB320FC192D60086DCA6 /* RecentDirectories.m in Sources */,
CE80DB330FC192D60086DCA6 /* RegistrationInterface.m in Sources */,
@@ -416,11 +529,36 @@
CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */,
CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */,
CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */,
CE9EA7561122C96C008CD2BC /* HSGUIController.m in Sources */,
CE9EA7571122C96C008CD2BC /* HSOutline.m in Sources */,
CE9EA7581122C96C008CD2BC /* HSWindowController.m in Sources */,
CE9EA7591122C96C008CD2BC /* NSEventAdditions.m in Sources */,
CE9EA75A1122C96C008CD2BC /* HSOutlineView.m in Sources */,
CE9EA75B1122C96C008CD2BC /* NSIndexPathAdditions.m in Sources */,
CE9EA75C1122C96C008CD2BC /* NSTableViewAdditions.m in Sources */,
CE9EA7721122CA0B008CD2BC /* DirectoryOutline.m in Sources */,
CE95865F112C516400F95FD2 /* StatsLabel.m in Sources */,
CE0C2AB61177011000BC749F /* HSTable.m in Sources */,
CE0C2ABD1177014200BC749F /* ProblemDialog.m in Sources */,
CEF12A7E124DFD400087B51D /* HSTableView.m in Sources */,
CEF12A84124DFD620087B51D /* ResultTable.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
CE895D7912144A7800E74705 /* registration.xib */ = {
isa = PBXVariantGroup;
children = (
CE895D7A12144A7800E74705 /* en */,
);
name = registration.xib;
path = ../../cocoalib/xib;
sourceTree = SOURCE_ROOT;
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4C08A954540054247B /* release */ = {
isa = XCBuildConfiguration;

View File

@@ -2,13 +2,13 @@
<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="7.10">
<data>
<int key="IBDocument.SystemTarget">1050</int>
<string key="IBDocument.SystemVersion">10C540</string>
<string key="IBDocument.InterfaceBuilderVersion">740</string>
<string key="IBDocument.AppKitVersion">1038.25</string>
<string key="IBDocument.HIToolboxVersion">458.00</string>
<string key="IBDocument.SystemVersion">10F569</string>
<string key="IBDocument.InterfaceBuilderVersion">788</string>
<string key="IBDocument.AppKitVersion">1038.29</string>
<string key="IBDocument.HIToolboxVersion">461.00</string>
<object class="NSMutableDictionary" key="IBDocument.PluginVersions">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="NS.object.0">740</string>
<string key="NS.object.0">788</string>
</object>
<object class="NSMutableArray" key="IBDocument.EditedObjectIDs">
<bool key="EncodedWithXMLCoder">YES</bool>
@@ -434,6 +434,7 @@
<string key="NSScreenRect">{{0, 0}, {1440, 878}}</string>
<string key="NSMinSize">{451, 177}</string>
<string key="NSMaxSize">{1.79769e+308, 1.79769e+308}</string>
<string key="NSFrameAutosaveName">DetailsPanel</string>
</object>
</object>
<object class="IBObjectContainer" key="IBDocument.Objects">
@@ -866,6 +867,13 @@
<string key="NS.key.0">detailsTable</string>
<string key="NS.object.0">NSTableView</string>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<string key="NS.key.0">detailsTable</string>
<object class="IBToOneOutletInfo" key="NS.object.0">
<string key="name">detailsTable</string>
<string key="candidateClassName">NSTableView</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">../base/DetailsPanel.h</string>
@@ -891,6 +899,35 @@
<string>NSProgressIndicator</string>
</object>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="NSArray" key="dict.sortedKeys">
<bool key="EncodedWithXMLCoder">YES</bool>
<string>dupeImage</string>
<string>dupeProgressIndicator</string>
<string>refImage</string>
<string>refProgressIndicator</string>
</object>
<object class="NSMutableArray" key="dict.values">
<bool key="EncodedWithXMLCoder">YES</bool>
<object class="IBToOneOutletInfo">
<string key="name">dupeImage</string>
<string key="candidateClassName">NSImageView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">dupeProgressIndicator</string>
<string key="candidateClassName">NSProgressIndicator</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">refImage</string>
<string key="candidateClassName">NSImageView</string>
</object>
<object class="IBToOneOutletInfo">
<string key="name">refProgressIndicator</string>
<string key="candidateClassName">NSProgressIndicator</string>
</object>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBProjectSource</string>
<string key="minorKey">DetailsPanel.h</string>
@@ -903,6 +940,13 @@
<string key="NS.key.0">detailsTable</string>
<string key="NS.object.0">NSTableView</string>
</object>
<object class="NSMutableDictionary" key="toOneOutletInfosByName">
<string key="NS.key.0">detailsTable</string>
<object class="IBToOneOutletInfo" key="NS.object.0">
<string key="name">detailsTable</string>
<string key="candidateClassName">NSTableView</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBUserSource</string>
<string key="minorKey"/>
@@ -1423,6 +1467,13 @@
<string key="NS.key.0">showWindow:</string>
<string key="NS.object.0">id</string>
</object>
<object class="NSMutableDictionary" key="actionInfosByName">
<string key="NS.key.0">showWindow:</string>
<object class="IBActionInfo" key="NS.object.0">
<string key="name">showWindow:</string>
<string key="candidateClassName">id</string>
</object>
</object>
<object class="IBClassDescriptionSource" key="sourceIdentifier">
<string key="majorKey">IBFrameworkSource</string>
<string key="minorKey">AppKit.framework/Headers/NSWindowController.h</string>
@@ -1431,6 +1482,7 @@
</object>
</object>
<int key="IBDocument.localizationMode">0</int>
<string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
<object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
<string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
<integer value="1050" key="NS.object.0"/>
@@ -1446,5 +1498,9 @@
<bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
<nil key="IBDocument.LastKnownRelativeProjectPath"/>
<int key="IBDocument.defaultPropertyAccessControl">3</int>
<object class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
<string key="NS.key.0">NSApplicationIcon</string>
<string key="NS.object.0">{128, 128}</string>
</object>
</data>
</archive>

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,7 @@ http://www.hardcoded.net/licenses/hs_license
[d setObject:b2n(NO) forKey:@"matchSimilarWords"];
[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(YES) forKey:@"ignoreSmallFiles"];
[d setObject:b2n(NO) forKey:@"debug"];

View File

@@ -23,7 +23,7 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>2.9.1</string>
<string>2.12.0</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>

View File

@@ -7,32 +7,13 @@ http://www.hardcoded.net/licenses/hs_license
*/
#import <Cocoa/Cocoa.h>
#import "../../cocoalib/Outline.h"
#import "../base/ResultWindow.h"
#import "DirectoryPanel.h"
@interface ResultWindow : ResultWindowBase
{
IBOutlet NSSearchField *filterField;
NSString *_lastAction;
NSMutableIndexSet *_deltaColumns;
}
- (IBAction)clearIgnoreList:(id)sender;
- (IBAction)filter:(id)sender;
- (IBAction)ignoreSelected:(id)sender;
- (IBAction)markAll:(id)sender;
- (IBAction)markInvert:(id)sender;
- (IBAction)markNone:(id)sender;
- (IBAction)markSelected:(id)sender;
- (IBAction)markToggle:(id)sender;
- (IBAction)openSelected:(id)sender;
- (IBAction)refresh:(id)sender;
- (IBAction)removeMarked:(id)sender;
- (IBAction)removeSelected:(id)sender;
- (IBAction)renameSelected:(id)sender;
- (IBAction)resetColumnsToDefault:(id)sender;
- (IBAction)revealSelected:(id)sender;
- (IBAction)startDuplicateScan:(id)sender;
- (IBAction)toggleDelta:(id)sender;
@end

View File

@@ -18,146 +18,27 @@ http://www.hardcoded.net/licenses/hs_license
- (void)awakeFromNib
{
[super awakeFromNib];
_displayDelta = NO;
_powerMode = NO;
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,4)] retain];
[_deltaColumns removeIndex:3];
[deltaSwitch setSelectedSegment:0];
[pmSwitch setSelectedSegment:0];
[py setDisplayDeltaValues:b2n(_displayDelta)];
[matches setTarget:self];
[matches setDoubleAction:@selector(openSelected:)];
[self refreshStats];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndex:2];
[deltaColumns addIndex:4];
[table setDeltaColumns:deltaColumns];
}
/* Actions */
- (IBAction)clearIgnoreList:(id)sender
{
int i = n2i([py getIgnoreListCount]);
if (!i)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO
return;
[py clearIgnoreList];
}
- (IBAction)filter:(id)sender
{
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
[py setEscapeFilterRegexp:b2n(!n2b([ud objectForKey:@"useRegexpFilter"]))];
[py applyFilter:[filterField stringValue]];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)ignoreSelected:(id)sender
{
NSArray *nodeList = [self getSelected:YES];
if (![nodeList count])
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
return;
[self performPySelection:[self getSelectedPaths:YES]];
[py addSelectedToIgnoreList];
[py removeSelected];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)markAll:(id)sender
{
[py markAll];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markInvert:(id)sender
{
[py markInvert];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markNone:(id)sender
{
[py markNone];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:YES]];
[py toggleSelectedMark];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)markToggle:(id)sender
{
OVNode *node = [matches itemAtRow:[matches clickedRow]];
[self performPySelection:[NSArray arrayWithObject:p2a([node indexPath])]];
[py toggleSelectedMark];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
}
- (IBAction)openSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:NO]];
[py openSelected];
}
- (IBAction)refresh:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)removeMarked:(id)sender
{
int mark_count = [[py getMarkCount] intValue];
if (!mark_count)
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO
return;
[py removeMarked];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)removeSelected:(id)sender
{
NSArray *nodeList = [self getSelected:YES];
if (![nodeList count])
return;
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
return;
[self performPySelection:[self getSelectedPaths:YES]];
[py removeSelected];
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)renameSelected:(id)sender
{
int col = [matches columnWithIdentifier:@"0"];
int row = [matches selectedRow];
[matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES];
}
- (IBAction)resetColumnsToDefault:(id)sender
{
NSMutableArray *columnsOrder = [NSMutableArray array];
[columnsOrder addObject:@"0"];
[columnsOrder addObject:@"1"];
[columnsOrder addObject:@"2"];
[columnsOrder addObject:@"6"];
[columnsOrder addObject:@"5"];
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
[columnsWidth setObject:i2n(195) forKey:@"0"];
[columnsWidth setObject:i2n(120) forKey:@"1"];
[columnsWidth setObject:i2n(183) forKey:@"1"];
[columnsWidth setObject:i2n(63) forKey:@"2"];
[columnsWidth setObject:i2n(60) forKey:@"6"];
[columnsWidth setObject:i2n(60) forKey:@"5"];
[self restoreColumnsPosition:columnsOrder widths:columnsWidth];
}
- (IBAction)revealSelected:(id)sender
{
[self performPySelection:[self getSelectedPaths:NO]];
[py revealSelected];
}
- (IBAction)startDuplicateScan:(id)sender
{
if ([matches numberOfRows] > 0)
@@ -170,18 +51,15 @@ http://www.hardcoded.net/licenses/hs_license
[_py setScanType:[ud objectForKey:@"scanType"]];
[_py setMinMatchPercentage:[ud objectForKey:@"minMatchPercentage"]];
[_py setWordWeighting:[ud objectForKey:@"wordWeighting"]];
[_py setMixFileKind:[ud objectForKey:@"mixFileKind"]];
[_py setMixFileKind:n2b([ud objectForKey:@"mixFileKind"])];
[_py setIgnoreHardlinkMatches:n2b([ud objectForKey:@"ignoreHardlinkMatches"])];
[_py setMatchSimilarWords:[ud objectForKey:@"matchSimilarWords"]];
int smallFileThreshold = [ud integerForKey:@"smallFileThreshold"]; // In KB
int sizeThreshold = [ud boolForKey:@"ignoreSmallFiles"] ? smallFileThreshold * 1024 : 0; // The py side wants bytes
[_py setSizeThreshold:sizeThreshold];
int r = n2i([py doScan]);
[matches reloadData];
[self refreshStats];
if (r != 0)
[[ProgressController mainProgressController] hide];
if (r == 1)
[Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."];
if (r == 3)
{
[Dialogs showMessage:@"The selected directories contain no scannable file."];
@@ -190,15 +68,6 @@ http://www.hardcoded.net/licenses/hs_license
}
- (IBAction)toggleDelta:(id)sender
{
if ([deltaSwitch selectedSegment] == 1)
[deltaSwitch setSelectedSegment:0];
else
[deltaSwitch setSelectedSegment:1];
[self changeDelta:sender];
}
/* Public */
- (void)initResultColumns
{
@@ -210,35 +79,9 @@ http://www.hardcoded.net/licenses/hs_license
[[sizeCol dataCell] setAlignment:NSRightTextAlignment];
[_resultColumns addObject:sizeCol];
[_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Creation" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Match %" width:60 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]];
}
/* Delegate */
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
OVNode *node = item;
if ([[tableColumn identifier] isEqual:@"mark"])
{
[cell setEnabled: [node isMarkable]];
}
if ([cell isKindOfClass:[NSTextFieldCell class]])
{
// Determine if the text color will be blue due to directory being reference.
NSTextFieldCell *textCell = cell;
if ([node isMarkable])
[textCell setTextColor:[NSColor blackColor]];
else
[textCell setTextColor:[NSColor blueColor]];
if ((_displayDelta) && (_powerMode || ([node level] > 1)))
{
int i = [[tableColumn identifier] intValue];
if ([_deltaColumns containsIndex:i])
[textCell setTextColor:[NSColor orangeColor]];
}
}
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Modification" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Match %" width:60 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Words Used" width:120 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Dupe Count" width:80 refCol:refCol]];
}
@end

View File

@@ -4,43 +4,47 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsutil.cocoa import signature
from hscommon.cocoa import signature
from core import scanner
from core.scanner import ScanType
from core.app_cocoa_inter import PyDupeGuruBase, PyDetailsPanel
from core_se.app_cocoa import DupeGuru
# Fix py2app imports with chokes on relative imports
from core_se import fs, data
# Fix py2app imports with chokes on relative imports and other stuff
import hsutil.conflict
import core.engine, core.fs, core.app
import core_se.fs, core_se.data
import xml.etree.ElementPath
import gzip
class PyDupeGuru(PyDupeGuruBase):
def init(self):
self = super(PyDupeGuru,self).init()
self.app = DupeGuru()
self.py = DupeGuru()
return self
#---Properties
def setMinMatchPercentage_(self,percentage):
self.app.scanner.min_match_percentage = int(percentage)
self.py.scanner.min_match_percentage = int(percentage)
def setScanType_(self,scan_type):
try:
self.app.scanner.scan_type = [
scanner.SCAN_TYPE_FILENAME,
scanner.SCAN_TYPE_CONTENT
self.py.scanner.scan_type = [
ScanType.Filename,
ScanType.Contents,
][scan_type]
except IndexError:
pass
def setWordWeighting_(self,words_are_weighted):
self.app.scanner.word_weighting = words_are_weighted
self.py.scanner.word_weighting = words_are_weighted
def setMatchSimilarWords_(self,match_similar_words):
self.app.scanner.match_similar_words = match_similar_words
self.py.scanner.match_similar_words = match_similar_words
@signature('v@:i')
def setSizeThreshold_(self, size_threshold):
self.app.scanner.size_threshold = size_threshold
self.py.scanner.size_threshold = size_threshold
#---Registration
def appName(self):

View File

@@ -12,15 +12,29 @@
CE073F6309CAE1A3005C1D2F /* dupeguru_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_help */; };
CE19BC6311199231007CCEB0 /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE19BC6011199231007CCEB0 /* ErrorReportWindow.xib */; };
CE19BC6411199231007CCEB0 /* progress.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE19BC6111199231007CCEB0 /* progress.xib */; };
CE19BC6511199231007CCEB0 /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE19BC6211199231007CCEB0 /* registration.xib */; };
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; };
CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; };
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; };
CE3A46FA109B212E002ABFD5 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3A46F9109B212E002ABFD5 /* MainMenu.xib */; };
CE45579B0AE3BC2B005A9546 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; };
CE4557B40AE3BC50005A9546 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; };
CE647E571173024A006D28BA /* ProblemDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = CE647E551173024A006D28BA /* ProblemDialog.m */; };
CE647E591173026F006D28BA /* ProblemDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE647E581173026F006D28BA /* ProblemDialog.xib */; };
CE6DD4E7124CA3070089A48D /* ResultTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6DD4E6124CA3070089A48D /* ResultTable.m */; };
CE6DD547124CAF1F0089A48D /* HSTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6DD546124CAF1F0089A48D /* HSTableView.m */; };
CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */; };
CE76FDC4111EE37C006618EA /* HSOutlineView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDBF111EE37C006618EA /* HSOutlineView.m */; };
CE76FDC5111EE37C006618EA /* NSIndexPathAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDC1111EE37C006618EA /* NSIndexPathAdditions.m */; };
CE76FDC6111EE37C006618EA /* NSTableViewAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDC3111EE37C006618EA /* NSTableViewAdditions.m */; };
CE76FDCF111EE38E006618EA /* HSGUIController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDC9111EE38E006618EA /* HSGUIController.m */; };
CE76FDD4111EE3A7006618EA /* DirectoryOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDD2111EE3A7006618EA /* DirectoryOutline.m */; };
CE76FDDF111EE42F006618EA /* HSOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDDE111EE42F006618EA /* HSOutline.m */; };
CE76FDF7111EE561006618EA /* NSEventAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDF6111EE561006618EA /* NSEventAdditions.m */; };
CE8C53BC117324CE0011B41F /* HSTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CE8C53BB117324CE0011B41F /* HSTable.m */; };
CE91F216113BC22D0010360B /* StatsLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE91F214113BC22D0010360B /* StatsLabel.m */; };
CEAC6811109B0B7E00B43C85 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEAC6810109B0B7E00B43C85 /* Preferences.xib */; };
CEBC6C3912144A4B007B43AE /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEBC6C3712144A4B007B43AE /* registration.xib */; };
CEBE4D74111F0EE1009AAC6D /* HSWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBE4D73111F0EE1009AAC6D /* HSWindowController.m */; };
CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */; };
CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7EA120FE675C80004E467 /* DetailsPanel.m */; };
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; };
@@ -31,7 +45,6 @@
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; };
CEFC7F9E0FC9517500CD5728 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8B0FC9517500CD5728 /* Dialogs.m */; };
CEFC7F9F0FC9517500CD5728 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */; };
CEFC7FA00FC9517500CD5728 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F8F0FC9517500CD5728 /* Outline.m */; };
CEFC7FA10FC9517500CD5728 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F910FC9517500CD5728 /* ProgressController.m */; };
CEFC7FA20FC9517500CD5728 /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F950FC9517500CD5728 /* RecentDirectories.m */; };
CEFC7FA30FC9517500CD5728 /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CEFC7F970FC9517500CD5728 /* RegistrationInterface.m */; };
@@ -66,7 +79,6 @@
CE073F5409CAE1A3005C1D2F /* dupeguru_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_help; path = ../../help_se/dupeguru_help; sourceTree = "<group>"; };
CE19BC6011199231007CCEB0 /* ErrorReportWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ErrorReportWindow.xib; sourceTree = "<group>"; };
CE19BC6111199231007CCEB0 /* progress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = progress.xib; sourceTree = "<group>"; };
CE19BC6211199231007CCEB0 /* registration.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = registration.xib; sourceTree = "<group>"; };
CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; };
CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; };
CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; };
@@ -74,9 +86,43 @@
CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dg_cocoa.plugin; sourceTree = SOURCE_ROOT; };
CE3A46F9109B212E002ABFD5 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = MainMenu.xib; path = ../base/xib/MainMenu.xib; sourceTree = "<group>"; };
CE45579A0AE3BC2B005A9546 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = "<absolute>"; };
CE647E541173024A006D28BA /* ProblemDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProblemDialog.h; path = ../base/ProblemDialog.h; sourceTree = SOURCE_ROOT; };
CE647E551173024A006D28BA /* ProblemDialog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProblemDialog.m; path = ../base/ProblemDialog.m; sourceTree = SOURCE_ROOT; };
CE647E561173024A006D28BA /* PyProblemDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyProblemDialog.h; path = ../base/PyProblemDialog.h; sourceTree = SOURCE_ROOT; };
CE647E581173026F006D28BA /* ProblemDialog.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = ProblemDialog.xib; path = ../base/xib/ProblemDialog.xib; sourceTree = SOURCE_ROOT; };
CE6DD4E4124CA3070089A48D /* PyResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyResultTable.h; path = ../base/PyResultTable.h; sourceTree = SOURCE_ROOT; };
CE6DD4E5124CA3070089A48D /* ResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultTable.h; path = ../base/ResultTable.h; sourceTree = SOURCE_ROOT; };
CE6DD4E6124CA3070089A48D /* ResultTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultTable.m; path = ../base/ResultTable.m; sourceTree = SOURCE_ROOT; };
CE6DD545124CAF1F0089A48D /* HSTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSTableView.h; path = ../../cocoalib/views/HSTableView.h; sourceTree = SOURCE_ROOT; };
CE6DD546124CAF1F0089A48D /* HSTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSTableView.m; path = ../../cocoalib/views/HSTableView.m; sourceTree = SOURCE_ROOT; };
CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = ../base/dsa_pub.pem; sourceTree = "<group>"; };
CE6E7407111C997500C350E3 /* PyDetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDetailsPanel.h; path = ../base/PyDetailsPanel.h; sourceTree = SOURCE_ROOT; };
CE76FDBE111EE37C006618EA /* HSOutlineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSOutlineView.h; sourceTree = "<group>"; };
CE76FDBF111EE37C006618EA /* HSOutlineView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSOutlineView.m; sourceTree = "<group>"; };
CE76FDC0111EE37C006618EA /* NSIndexPathAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSIndexPathAdditions.h; sourceTree = "<group>"; };
CE76FDC1111EE37C006618EA /* NSIndexPathAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSIndexPathAdditions.m; sourceTree = "<group>"; };
CE76FDC2111EE37C006618EA /* NSTableViewAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSTableViewAdditions.h; sourceTree = "<group>"; };
CE76FDC3111EE37C006618EA /* NSTableViewAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSTableViewAdditions.m; sourceTree = "<group>"; };
CE76FDC8111EE38E006618EA /* HSGUIController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSGUIController.h; sourceTree = "<group>"; };
CE76FDC9111EE38E006618EA /* HSGUIController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSGUIController.m; sourceTree = "<group>"; };
CE76FDCD111EE38E006618EA /* PyGUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyGUI.h; sourceTree = "<group>"; };
CE76FDCE111EE38E006618EA /* PyOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyOutline.h; sourceTree = "<group>"; };
CE76FDD1111EE3A7006618EA /* DirectoryOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryOutline.h; path = ../base/DirectoryOutline.h; sourceTree = SOURCE_ROOT; };
CE76FDD2111EE3A7006618EA /* DirectoryOutline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryOutline.m; path = ../base/DirectoryOutline.m; sourceTree = SOURCE_ROOT; };
CE76FDD3111EE3A7006618EA /* PyDirectoryOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDirectoryOutline.h; path = ../base/PyDirectoryOutline.h; sourceTree = SOURCE_ROOT; };
CE76FDDD111EE42F006618EA /* HSOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSOutline.h; sourceTree = "<group>"; };
CE76FDDE111EE42F006618EA /* HSOutline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSOutline.m; sourceTree = "<group>"; };
CE76FDF5111EE561006618EA /* NSEventAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSEventAdditions.h; path = ../../cocoalib/NSEventAdditions.h; sourceTree = SOURCE_ROOT; };
CE76FDF6111EE561006618EA /* NSEventAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSEventAdditions.m; path = ../../cocoalib/NSEventAdditions.m; sourceTree = SOURCE_ROOT; };
CE8C53B61173248F0011B41F /* PyTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyTable.h; sourceTree = "<group>"; };
CE8C53BB117324CE0011B41F /* HSTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSTable.m; sourceTree = "<group>"; };
CE91F210113BC22D0010360B /* PyStatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyStatsLabel.h; path = ../base/PyStatsLabel.h; sourceTree = SOURCE_ROOT; };
CE91F213113BC22D0010360B /* StatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StatsLabel.h; path = ../base/StatsLabel.h; sourceTree = SOURCE_ROOT; };
CE91F214113BC22D0010360B /* StatsLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = StatsLabel.m; path = ../base/StatsLabel.m; sourceTree = SOURCE_ROOT; };
CEAC6810109B0B7E00B43C85 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = xib/Preferences.xib; sourceTree = "<group>"; };
CEBC6C3812144A4B007B43AE /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = ../../cocoalib/en.lproj/registration.xib; sourceTree = SOURCE_ROOT; };
CEBE4D72111F0EE1009AAC6D /* HSWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSWindowController.h; sourceTree = "<group>"; };
CEBE4D73111F0EE1009AAC6D /* HSWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSWindowController.m; sourceTree = "<group>"; };
CEDD92D60FDD01640031C7B7 /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; };
CEDD92D70FDD01640031C7B7 /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = ../../cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; };
CEE7EA110FE675C80004E467 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = ../base/DetailsPanel.h; sourceTree = SOURCE_ROOT; };
@@ -91,8 +137,6 @@
CEFC7F8B0FC9517500CD5728 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = ../../cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; };
CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = ../../cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; };
CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = ../../cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; };
CEFC7F8E0FC9517500CD5728 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = ../../cocoalib/Outline.h; sourceTree = SOURCE_ROOT; };
CEFC7F8F0FC9517500CD5728 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = ../../cocoalib/Outline.m; sourceTree = SOURCE_ROOT; };
CEFC7F900FC9517500CD5728 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = ../../cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; };
CEFC7F910FC9517500CD5728 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = ../../cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; };
CEFC7F920FC9517500CD5728 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = ../../cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; };
@@ -208,14 +252,56 @@
CE19BC5F11199231007CCEB0 /* xib */ = {
isa = PBXGroup;
children = (
CEBC6C3712144A4B007B43AE /* registration.xib */,
CE19BC6011199231007CCEB0 /* ErrorReportWindow.xib */,
CE19BC6111199231007CCEB0 /* progress.xib */,
CE19BC6211199231007CCEB0 /* registration.xib */,
);
name = xib;
path = ../../cocoalib/xib;
sourceTree = SOURCE_ROOT;
};
CE76FDBD111EE37C006618EA /* views */ = {
isa = PBXGroup;
children = (
CE6DD545124CAF1F0089A48D /* HSTableView.h */,
CE6DD546124CAF1F0089A48D /* HSTableView.m */,
CE76FDBE111EE37C006618EA /* HSOutlineView.h */,
CE76FDBF111EE37C006618EA /* HSOutlineView.m */,
CE76FDC0111EE37C006618EA /* NSIndexPathAdditions.h */,
CE76FDC1111EE37C006618EA /* NSIndexPathAdditions.m */,
CE76FDC2111EE37C006618EA /* NSTableViewAdditions.h */,
CE76FDC3111EE37C006618EA /* NSTableViewAdditions.m */,
);
name = views;
path = ../../cocoalib/views;
sourceTree = SOURCE_ROOT;
};
CE76FDC7111EE38E006618EA /* controllers */ = {
isa = PBXGroup;
children = (
CEBE4D72111F0EE1009AAC6D /* HSWindowController.h */,
CEBE4D73111F0EE1009AAC6D /* HSWindowController.m */,
CE76FDDD111EE42F006618EA /* HSOutline.h */,
CE76FDDE111EE42F006618EA /* HSOutline.m */,
CE76FDC8111EE38E006618EA /* HSGUIController.h */,
CE76FDC9111EE38E006618EA /* HSGUIController.m */,
CE8C53BB117324CE0011B41F /* HSTable.m */,
);
name = controllers;
path = ../../cocoalib/controllers;
sourceTree = SOURCE_ROOT;
};
CE76FDCC111EE38E006618EA /* proxies */ = {
isa = PBXGroup;
children = (
CE76FDCD111EE38E006618EA /* PyGUI.h */,
CE76FDCE111EE38E006618EA /* PyOutline.h */,
CE8C53B61173248F0011B41F /* PyTable.h */,
);
name = proxies;
path = ../../cocoalib/proxies;
sourceTree = SOURCE_ROOT;
};
CEDD92D50FDD01640031C7B7 /* brsinglelineformatter */ = {
isa = PBXGroup;
children = (
@@ -229,6 +315,7 @@
CEEFC0CA10943849001F3A39 /* xib */ = {
isa = PBXGroup;
children = (
CE647E581173026F006D28BA /* ProblemDialog.xib */,
CE3A46F9109B212E002ABFD5 /* MainMenu.xib */,
CEAC6810109B0B7E00B43C85 /* Preferences.xib */,
CEEFC0F710945D9F001F3A39 /* DirectoryPanel.xib */,
@@ -250,14 +337,17 @@
CEFC7F890FC9513600CD5728 /* cocoalib */ = {
isa = PBXGroup;
children = (
CE76FDF5111EE561006618EA /* NSEventAdditions.h */,
CE76FDF6111EE561006618EA /* NSEventAdditions.m */,
CE76FDC7111EE38E006618EA /* controllers */,
CE76FDCC111EE38E006618EA /* proxies */,
CE76FDBD111EE37C006618EA /* views */,
CE19BC5F11199231007CCEB0 /* xib */,
CEDD92D50FDD01640031C7B7 /* brsinglelineformatter */,
CEFC7F8A0FC9517500CD5728 /* Dialogs.h */,
CEFC7F8B0FC9517500CD5728 /* Dialogs.m */,
CEFC7F8C0FC9517500CD5728 /* HSErrorReportWindow.h */,
CEFC7F8D0FC9517500CD5728 /* HSErrorReportWindow.m */,
CEFC7F8E0FC9517500CD5728 /* Outline.h */,
CEFC7F8F0FC9517500CD5728 /* Outline.m */,
CEFC7F900FC9517500CD5728 /* ProgressController.h */,
CEFC7F910FC9517500CD5728 /* ProgressController.m */,
CEFC7F920FC9517500CD5728 /* PyApp.h */,
@@ -277,6 +367,15 @@
CEFC7FB00FC9518F00CD5728 /* dgbase */ = {
isa = PBXGroup;
children = (
CE6DD4E4124CA3070089A48D /* PyResultTable.h */,
CE6DD4E5124CA3070089A48D /* ResultTable.h */,
CE6DD4E6124CA3070089A48D /* ResultTable.m */,
CE91F210113BC22D0010360B /* PyStatsLabel.h */,
CE91F213113BC22D0010360B /* StatsLabel.h */,
CE91F214113BC22D0010360B /* StatsLabel.m */,
CE76FDD1111EE3A7006618EA /* DirectoryOutline.h */,
CE76FDD2111EE3A7006618EA /* DirectoryOutline.m */,
CE76FDD3111EE3A7006618EA /* PyDirectoryOutline.h */,
CEFC7FB10FC951A700CD5728 /* AppDelegate.h */,
CEFC7FB20FC951A700CD5728 /* AppDelegate.m */,
CEFC7FB30FC951A700CD5728 /* Consts.h */,
@@ -288,6 +387,9 @@
CE6E7407111C997500C350E3 /* PyDetailsPanel.h */,
CEFC7FB70FC951A700CD5728 /* ResultWindow.h */,
CEFC7FB80FC951A700CD5728 /* ResultWindow.m */,
CE647E541173024A006D28BA /* ProblemDialog.h */,
CE647E551173024A006D28BA /* ProblemDialog.m */,
CE647E561173024A006D28BA /* PyProblemDialog.h */,
);
name = dgbase;
sourceTree = "<group>";
@@ -325,6 +427,13 @@
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 3.0";
hasScannedForEncodings = 1;
knownRegions = (
English,
Japanese,
French,
German,
en,
);
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
projectDirPath = "";
projectRoot = "";
@@ -352,7 +461,8 @@
CE3A46FA109B212E002ABFD5 /* MainMenu.xib in Resources */,
CE19BC6311199231007CCEB0 /* ErrorReportWindow.xib in Resources */,
CE19BC6411199231007CCEB0 /* progress.xib in Resources */,
CE19BC6511199231007CCEB0 /* registration.xib in Resources */,
CE647E591173026F006D28BA /* ProblemDialog.xib in Resources */,
CEBC6C3912144A4B007B43AE /* registration.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -368,7 +478,6 @@
CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */,
CEFC7F9E0FC9517500CD5728 /* Dialogs.m in Sources */,
CEFC7F9F0FC9517500CD5728 /* HSErrorReportWindow.m in Sources */,
CEFC7FA00FC9517500CD5728 /* Outline.m in Sources */,
CEFC7FA10FC9517500CD5728 /* ProgressController.m in Sources */,
CEFC7FA20FC9517500CD5728 /* RecentDirectories.m in Sources */,
CEFC7FA30FC9517500CD5728 /* RegistrationInterface.m in Sources */,
@@ -379,11 +488,36 @@
CEFC7FBB0FC951A700CD5728 /* ResultWindow.m in Sources */,
CEDD92DA0FDD01640031C7B7 /* BRSingleLineFormatter.m in Sources */,
CEE7EA130FE675C80004E467 /* DetailsPanel.m in Sources */,
CE76FDC4111EE37C006618EA /* HSOutlineView.m in Sources */,
CE76FDC5111EE37C006618EA /* NSIndexPathAdditions.m in Sources */,
CE76FDC6111EE37C006618EA /* NSTableViewAdditions.m in Sources */,
CE76FDCF111EE38E006618EA /* HSGUIController.m in Sources */,
CE76FDD4111EE3A7006618EA /* DirectoryOutline.m in Sources */,
CE76FDDF111EE42F006618EA /* HSOutline.m in Sources */,
CE76FDF7111EE561006618EA /* NSEventAdditions.m in Sources */,
CEBE4D74111F0EE1009AAC6D /* HSWindowController.m in Sources */,
CE91F216113BC22D0010360B /* StatsLabel.m in Sources */,
CE647E571173024A006D28BA /* ProblemDialog.m in Sources */,
CE8C53BC117324CE0011B41F /* HSTable.m in Sources */,
CE6DD4E7124CA3070089A48D /* ResultTable.m in Sources */,
CE6DD547124CAF1F0089A48D /* HSTableView.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
CEBC6C3712144A4B007B43AE /* registration.xib */ = {
isa = PBXVariantGroup;
children = (
CEBC6C3812144A4B007B43AE /* en */,
);
name = registration.xib;
path = ../../cocoalib/xib;
sourceTree = SOURCE_ROOT;
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4C08A954540054247B /* release */ = {
isa = XCBuildConfiguration;

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@ def main(edition, ui, dev):
if ui not in ('cocoa', 'qt'):
ui = 'cocoa' if sys.platform == 'darwin' else 'qt'
build_type = 'Dev' if dev else 'Release'
print "Configuring dupeGuru {0} for UI {1} ({2})".format(edition.upper(), ui, build_type)
print("Configuring dupeGuru {0} for UI {1} ({2})".format(edition.upper(), ui, build_type))
conf = {
'edition': edition,
'ui': ui,

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python
# Created By: Virgil Dupras
# Created On: 2006/11/11
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
@@ -7,17 +6,18 @@
# 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
import subprocess
import re
from send2trash import send2trash
from hscommon.reg import RegistrableApplication, RegistrationRequired
from hscommon.notify import Broadcaster
from hsutil import io, files
from hsutil.path import Path
from hsutil.reg import RegistrableApplication, RegistrationRequired
from hsutil.misc import flatten, first
from hsutil.notify import Broadcaster
from hsutil.str import escape
from . import directories, results, scanner, export, fs
@@ -31,9 +31,6 @@ JOB_DELETE = 'job_delete'
class NoScannableFileError(Exception):
pass
class AllFilesAreRefError(Exception):
pass
class DupeGuru(RegistrableApplication, Broadcaster):
DEMO_LIMIT_DESC = "In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied."
@@ -48,10 +45,10 @@ class DupeGuru(RegistrableApplication, Broadcaster):
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,
'ignore_hardlink_matches': False,
}
self.selected_dupes = []
@@ -64,31 +61,28 @@ class DupeGuru(RegistrableApplication, Broadcaster):
else:
self.action_count += count
def _do_delete(self, j):
def _do_delete(self, j, replace_with_hardlinks):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe)
return self._do_delete_dupe(dupe, replace_with_hardlinks)
j.start_job(self.results.mark_count)
self.last_op_error_count = self.results.perform_on_marked(op, True)
self.results.perform_on_marked(op, True)
def _do_delete_dupe(self, dupe):
def _do_delete_dupe(self, dupe, replace_with_hardlinks):
if not io.exists(dupe.path):
return True
self._recycle_dupe(dupe)
return
send2trash(str(dupe.path)) # Raises OSError when there's a problem
if replace_with_hardlinks:
group = self.results.get_group_of_duplicate(dupe)
ref = group.ref
os.link(str(ref.path), str(dupe.path))
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.notify('directories_changed')
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):
@@ -96,41 +90,75 @@ class DupeGuru(RegistrableApplication, Broadcaster):
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))
logging.warning("Exception on GetDisplayInfo for %s: %s", str(dupe.path), str(e))
return ['---'] * len(self.data.COLUMNS)
def _get_file(self, str_path):
path = Path(str_path)
return fs.get_file(path, self.directories.fileclasses)
f = fs.get_file(path, self.directories.fileclasses)
if f is None:
return None
try:
f._read_all_info(attrnames=self.data.METADATA_TO_READ)
return f
except EnvironmentError:
return None
def _job_completed(self, jobid):
# Must be called by subclasses when they detect that an async job is completed.
if jobid == JOB_SCAN:
self.notify('results_changed')
elif jobid in (JOB_LOAD, JOB_MOVE, JOB_DELETE):
self.notify('results_changed')
self.notify('problems_changed')
@staticmethod
def _recycle_dupe(dupe):
def _open_path(path):
raise NotImplementedError()
@staticmethod
def _reveal_path(path):
raise NotImplementedError()
@staticmethod
def _remove_hardlink_dupes(files):
seen_inodes = set()
result = []
for file in files:
inode = io.stat(file.path).st_ino
if inode not in seen_inodes:
seen_inodes.add(inode)
result.append(file)
return result
def _select_dupes(self, dupes):
if dupes == self.selected_dupes:
return
self.selected_dupes = dupes
self.notify('dupes_selected')
def _start_job(self, jobid, func):
# func(j)
def _start_job(self, jobid, func, *args):
# func(j, *args)
raise NotImplementedError()
def add_directory(self, d):
try:
self.directories.add_path(Path(d))
self.notify('directories_changed')
return 0
except directories.AlreadyThereError:
return 1
except directories.InvalidPathError:
return 2
def add_to_ignore_list(self, dupe):
def add_selected_to_ignore_list(self):
dupes = self.without_ref(self.selected_dupes)
for dupe in dupes:
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))
self.scanner.ignore_list.Ignore(str(other.path), str(dupe.path))
self.remove_duplicates(dupes)
def apply_filter(self, filter):
self.results.apply_filter(None)
@@ -138,6 +166,7 @@ class DupeGuru(RegistrableApplication, Broadcaster):
filter = escape(filter, '()[]\\.|+?^')
filter = escape(filter, '*', '.')
self.results.apply_filter(filter)
self.notify('results_changed')
def clean_empty_dirs(self, path):
if self.options['clean_empty_dirs']:
@@ -159,40 +188,35 @@ class DupeGuru(RegistrableApplication, Broadcaster):
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)
# Raises an EnvironmentError if there's a problem
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)
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.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):
def delete_marked(self, replace_with_hardlinks=False):
self._demo_check()
self._start_job(JOB_DELETE, self._do_delete)
self._start_job(JOB_DELETE, self._do_delete, replace_with_hardlinks)
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 = list(map(int, column_ids))
column_ids.sort()
colnames = [col['display'] for i, col in enumerate(self.data.COLUMNS) if i in column_ids]
rows = []
@@ -204,28 +228,123 @@ class DupeGuru(RegistrableApplication, Broadcaster):
rows.append(row)
return export.export_to_xhtml(colnames, rows)
def invoke_command(self, cmd):
"""Calls command `cmd` with %d and %r placeholders replaced.
Using the current selection, %d is replaced with the currently selected dupe and %r is
replaced with that dupe's ref file. If there's no selection, the command is not invoked.
If the dupe is a ref, %d and %r will be the same.
"""
if not self.selected_dupes:
return
dupe = self.selected_dupes[0]
group = self.results.get_group_of_duplicate(dupe)
ref = group.ref
cmd = cmd.replace('%d', str(dupe.path))
cmd = cmd.replace('%r', str(ref.path))
match = re.match(r'"([^"]+)"(.*)', cmd)
if match is not None:
# This code here is because subprocess. Popen doesn't seem to accept, under Windows,
# executable paths with spaces in it, *even* when they're enclosed in "". So this is
# a workaround to make the damn thing work.
exepath, args = match.groups()
path, exename = op.split(exepath)
subprocess.Popen(exename + args, shell=True, cwd=path)
else:
subprocess.Popen(cmd, shell=True)
def load(self):
self._start_job(JOB_LOAD, self._do_load)
self.load_ignore_list()
def load_from(self, filename):
def do(j):
self.results.load_from_xml(filename, self._get_file, j)
self._start_job(JOB_LOAD, do)
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):
def make_selected_reference(self):
dupes = self.without_ref(self.selected_dupes)
changed_groups = set()
for dupe in duplicates:
for dupe in dupes:
g = self.results.get_group_of_duplicate(dupe)
if g not in changed_groups:
self.results.make_ref(dupe)
changed_groups.add(g)
self.notify('results_changed_but_keep_selection')
def mark_all(self):
self.results.mark_all()
self.notify('marking_changed')
def mark_none(self):
self.results.mark_none()
self.notify('marking_changed')
def mark_invert(self):
self.results.mark_invert()
self.notify('marking_changed')
def mark_dupe(self, dupe, marked):
if marked:
self.results.mark(dupe)
else:
self.results.unmark(dupe)
self.notify('marking_changed')
def open_selected(self):
if self.selected_dupes:
self._open_path(self.selected_dupes[0].path)
def purge_ignore_list(self):
self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s))
def remove_directory(self,index):
try:
del self.directories[index]
self.notify('directories_changed')
except IndexError:
pass
def remove_duplicates(self, duplicates):
self.results.remove_duplicates(self.without_ref(duplicates))
self.notify('results_changed_but_keep_selection')
def remove_marked(self):
self.results.perform_on_marked(lambda x:None, True)
self.notify('results_changed')
def remove_selected(self):
self.remove_duplicates(self.selected_dupes)
def rename_selected(self, newname):
try:
d = self.selected_dupes[0]
d.rename(newname)
return True
except (IndexError, fs.FSError) as e:
logging.warning("dupeGuru Warning: %s" % str(e))
return False
def reveal_selected(self):
if self.selected_dupes:
self._reveal_path(self.selected_dupes[0].path)
def save(self):
if not op.exists(self.appdata):
os.makedirs(self.appdata)
self.directories.save_to_file(op.join(self.appdata, 'last_directories.xml'))
if self.results.is_modified:
self.results.save_to_xml(op.join(self.appdata, 'last_results.xml'))
def save_as(self, filename):
self.results.save_to_xml(filename)
# It's not because we saved it here that we don't want to save it in appdata when we quit
self.results.is_modified = True
def save_ignore_list(self):
if not op.exists(self.appdata):
os.makedirs(self.appdata)
@@ -236,18 +355,24 @@ class DupeGuru(RegistrableApplication, Broadcaster):
def do(j):
j.set_progress(0, 'Collecting files to scan')
files = list(self.directories.get_files())
if self.options['ignore_hardlink_matches']:
files = self._remove_hardlink_dupes(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:
if not self.directories.has_any_file():
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)
def toggle_selected_mark_state(self):
for dupe in self.selected_dupes:
self.results.mark_toggle(dupe)
self.notify('marking_changed')
def without_ref(self, dupes):
return [dupe for dupe in dupes if self.results.get_group_of_duplicate(dupe).ref is not dupe]
#--- Properties
@property
def stat_line(self):

View File

@@ -9,15 +9,14 @@
import logging
import os.path as op
from hsutil import cocoa, job
from hsutil.cocoa import install_exception_hook
from hsutil.cocoa.objcmin import (NSNotificationCenter, NSUserDefaults,
from hscommon import cocoa, job
from hscommon.cocoa import install_exception_hook
from hscommon.cocoa.objcmin import (NSNotificationCenter, NSUserDefaults,
NSSearchPathForDirectoriesInDomains, NSApplicationSupportDirectory, NSUserDomainMask,
NSWorkspace, NSWorkspaceRecycleOperation)
from hsutil.misc import stripnone
from hsutil.reg import RegistrationRequired
NSWorkspace)
from hscommon.reg import RegistrationRequired
from . import app, fs
from . import app
JOBID2TITLE = {
app.JOB_SCAN: "Scanning for duplicates",
@@ -46,93 +45,31 @@ class DupeGuru(app.DupeGuru):
appdata = op.join(appsupport, appdata_subdir)
app.DupeGuru.__init__(self, data_module, appdata, appid)
self.progress = cocoa.ThreadedJobPerformer()
self.display_delta_values = False
#--- Override
@staticmethod
def _recycle_dupe(dupe):
# local import because first appkit import takes a lot of memory. we want to avoid it.
directory = unicode(dupe.path[:-1])
filename = dupe.name
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
NSWorkspaceRecycleOperation, directory, '', [filename], None)
def _open_path(path):
NSWorkspace.sharedWorkspace().openFile_(str(path))
def _start_job(self, jobid, func):
@staticmethod
def _reveal_path(path):
NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(str(path), '')
def _start_job(self, jobid, func, *args):
try:
j = self.progress.create_job()
self.progress.run_threaded(func, args=(j, ))
args = tuple([j] + list(args))
self.progress.run_threaded(func, args=args)
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)
#---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):
# local import because first appkit import takes a lot of memory. we want to avoid it.
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 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):
# local import because first appkit import takes a lot of memory. we want to avoid it.
if self.selected_dupes:
path = unicode(self.selected_dupes[0].path)
NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path,'')
def start_scanning(self):
self._select_dupes([])
try:
@@ -140,134 +77,4 @@ class DupeGuru(app.DupeGuru):
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._select_dupes([dupe for dupe in selected if dupe is not None])
def SelectPowerMarkerNodePaths(self,node_paths):
rows = [p[0] for p in node_paths]
dupes = [self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes))]
self._select_dupes(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) and (g is not 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

View File

@@ -9,204 +9,154 @@
# Common interface for all editions' dg_cocoa unit.
from hsutil.cocoa.objcmin import NSObject
from hsutil.cocoa import signature
from hsutil.reg import InvalidCodeError
from hscommon.cocoa.inter import signature, PyTable, PyOutline, PyGUIObject, PyRegistrable
from .gui.details_panel import DetailsPanel
from .gui.directory_tree import DirectoryTree
from .gui.problem_dialog import ProblemDialog
from .gui.problem_table import ProblemTable
from .gui.result_table import ResultTable
from .gui.stats_label import StatsLabel
# Fix py2app's problems on relative imports
from core import app, app_cocoa, data, directories, engine, export, ignore, results, fs, scanner
from hsutil import conflict
class PyApp(NSObject):
pass #fake class
class PyDupeGuruBase(PyApp):
class PyDupeGuruBase(PyRegistrable):
#---Directories
def addDirectory_(self, directory):
return self.app.add_directory(directory)
return self.py.add_directory(directory)
def removeDirectory_(self, index):
self.app.RemoveDirectory(index)
def setDirectory_state_(self, node_path, state):
self.app.SetDirectoryState(node_path, state)
self.py.remove_directory(index)
#---Results
def clearIgnoreList(self):
self.app.scanner.ignore_list.Clear()
self.py.scanner.ignore_list.Clear()
def doScan(self):
return self.app.start_scanning()
return self.py.start_scanning()
def exportToXHTMLwithColumns_(self, column_ids):
return self.app.export_to_xhtml(column_ids)
return self.py.export_to_xhtml(column_ids)
def loadIgnoreList(self):
self.app.load_ignore_list()
self.py.load_ignore_list()
def loadResults(self):
self.app.load()
self.py.load()
def loadResultsFrom_(self, filename):
self.py.load_from(filename)
def markAll(self):
self.app.results.mark_all()
self.py.mark_all()
def markNone(self):
self.app.results.mark_none()
self.py.mark_none()
def markInvert(self):
self.app.results.mark_invert()
self.py.mark_invert()
def purgeIgnoreList(self):
self.app.PurgeIgnoreList()
self.py.purge_ignore_list()
def toggleSelectedMark(self):
self.app.ToggleSelectedMarkState()
self.py.toggle_selected_mark_state()
def saveIgnoreList(self):
self.app.save_ignore_list()
self.py.save_ignore_list()
def saveResults(self):
self.app.save()
self.py.save()
def selectedResultNodePaths(self):
return self.app.selected_result_node_paths()
def selectResultNodePaths_(self,node_paths):
self.app.SelectResultNodePaths(node_paths)
def selectedPowerMarkerNodePaths(self):
return self.app.selected_powermarker_node_paths()
def selectPowerMarkerNodePaths_(self,node_paths):
self.app.SelectPowerMarkerNodePaths(node_paths)
def saveResultsAs_(self, filename):
self.py.save_as(filename)
#---Actions
def addSelectedToIgnoreList(self):
self.app.AddSelectedToIgnoreList()
self.py.add_selected_to_ignore_list()
def deleteMarked(self):
self.app.delete_marked()
self.py.delete_marked()
def hardlinkMarked(self):
self.py.delete_marked(replace_with_hardlinks=True)
def applyFilter_(self, filter):
self.app.apply_filter(filter)
self.py.apply_filter(filter)
def makeSelectedReference(self):
self.app.MakeSelectedReference()
self.py.make_selected_reference()
def copyOrMove_markedTo_recreatePath_(self, copy, destination, recreate_path):
self.app.copy_or_move_marked(copy, destination, recreate_path)
self.py.copy_or_move_marked(copy, destination, recreate_path)
def openSelected(self):
self.app.OpenSelected()
self.py.open_selected()
def removeMarked(self):
self.app.results.perform_on_marked(lambda x:True, True)
def removeSelected(self):
self.app.RemoveSelected()
self.py.remove_marked()
def renameSelected_(self,newname):
return self.app.RenameSelected(newname)
return self.py.rename_selected(newname)
def revealSelected(self):
self.app.RevealSelected()
self.py.reveal_selected()
#---Misc
def sortDupesBy_ascending_(self, key, asc):
self.app.sort_dupes(key, asc)
def sortGroupsBy_ascending_(self, key, asc):
self.app.sort_groups(key, asc)
def invokeCommand_(self, cmd):
self.py.invoke_command(cmd)
#---Information
def getIgnoreListCount(self):
return len(self.app.scanner.ignore_list)
return len(self.py.scanner.ignore_list)
def getMarkCount(self):
return self.app.results.mark_count
return self.py.results.mark_count
def getStatLine(self):
return self.app.stat_line
def getOperationalErrorCount(self):
return self.app.last_op_error_count
#---Data
@signature('i@:i')
def getOutlineViewMaxLevel_(self, tag):
return self.app.GetOutlineViewMaxLevel(tag)
@signature('@@:i@')
def getOutlineView_childCountsForPath_(self, tag, node_path):
return self.app.GetOutlineViewChildCounts(tag, node_path)
def getOutlineView_valuesForIndexes_(self, tag, node_path):
return self.app.GetOutlineViewValues(tag, node_path)
def getOutlineView_markedAtIndexes_(self, tag, node_path):
return self.app.GetOutlineViewMarked(tag, node_path)
def getTableViewCount_(self, tag):
return self.app.GetTableViewCount(tag)
def getTableViewMarkedIndexes_(self, tag):
return self.app.GetTableViewMarkedIndexes(tag)
def getTableView_valuesForRow_(self, tag, row):
return self.app.GetTableViewValues(tag, row)
@signature('i@:')
def scanWasProblematic(self):
return bool(self.py.results.problems)
#---Properties
@signature('v@:c')
def setMixFileKind_(self, mix_file_kind):
self.app.scanner.mix_file_kind = mix_file_kind
def setDisplayDeltaValues_(self, display_delta_values):
self.app.display_delta_values= display_delta_values
self.py.scanner.mix_file_kind = mix_file_kind
@signature('v@:c')
def setEscapeFilterRegexp_(self, escape_filter_regexp):
self.app.options['escape_filter_regexp'] = escape_filter_regexp
self.py.options['escape_filter_regexp'] = escape_filter_regexp
@signature('v@:c')
def setRemoveEmptyFolders_(self, remove_empty_folders):
self.app.options['clean_empty_dirs'] = remove_empty_folders
self.py.options['clean_empty_dirs'] = remove_empty_folders
@signature('v@:c')
def setIgnoreHardlinkMatches_(self, ignore_hardlink_matches):
self.py.options['ignore_hardlink_matches'] = ignore_hardlink_matches
#---Worker
def getJobProgress(self):
return self.app.progress.last_progress
try:
return self.py.progress.last_progress
except AttributeError:
# I have *no idea* why this can possible happen (last_progress is always set by
# create_job() *before* any threaded job notification, which shows the progress panel,
# is sent), but it happens anyway, so there we go.
return -1
def getJobDesc(self):
return self.app.progress.last_desc
return self.py.progress.last_desc
def cancelJob(self):
self.app.progress.job_cancelled = True
self.py.progress.job_cancelled = True
#---Registration
def demoLimitDescription(self):
return self.app.DEMO_LIMIT_DESC
@signature('i@:')
def isRegistered(self):
return self.app.registered
def isCodeValid_withEmail_(self, code, email):
try:
self.app.validate_code(code, email)
return None
except InvalidCodeError as e:
return unicode(e)
def setRegisteredCode_andEmail_(self, code, email):
self.app.set_registration(code, email)
def jobCompleted_(self, jobid):
self.py._job_completed(jobid)
class PyDetailsPanel(NSObject):
def initWithCocoa_pyParent_(self, cocoa, pyparent):
super(PyDetailsPanel, self).init()
self.cocoa = cocoa
self.py = DetailsPanel(self, pyparent.app)
return self
class PyDetailsPanel(PyGUIObject):
py_class = DetailsPanel
@signature('i@:')
def numberOfRows(self):
return self.py.row_count()
@@ -215,7 +165,73 @@ class PyDetailsPanel(NSObject):
def valueForColumn_row_(self, column, row):
return self.py.row(row)[int(column)]
# python --> cocoa
def refresh(self):
self.cocoa.refresh()
class PyDirectoryOutline(PyOutline):
py_class = DirectoryTree
def addDirectory_(self, path):
self.py.add_directory(path)
class PyResultTable(PyTable):
py_class = ResultTable
@signature('c@:')
def powerMarkerMode(self):
return self.py.power_marker
@signature('v@:c')
def setPowerMarkerMode_(self, value):
self.py.power_marker = value
@signature('c@:')
def deltaValuesMode(self):
return self.py.delta_values
@signature('v@:c')
def setDeltaValuesMode_(self, value):
self.py.delta_values = value
@signature('@@:ii')
def valueForRow_column_(self, row_index, column):
return self.py.get_row_value(row_index, column)
@signature('c@:@')
def renameSelected_(self, newname):
return self.py.rename_selected(newname)
@signature('v@:ic')
def sortBy_ascending_(self, key, asc):
self.py.sort(key, asc)
def markSelected(self):
self.py.app.toggle_selected_mark_state()
def removeSelected(self):
self.py.app.remove_selected()
@signature('i@:')
def selectedDupeCount(self):
return self.py.selected_dupe_count
# python --> cocoa
def invalidate_markings(self):
self.cocoa.invalidateMarkings()
class PyStatsLabel(PyGUIObject):
py_class = StatsLabel
def display(self):
return self.py.display
class PyProblemDialog(PyGUIObject):
py_class = ProblemDialog
def revealSelected(self):
self.py.reveal_selected_dupe()
class PyProblemTable(PyTable):
py_class = ProblemTable

View File

@@ -11,7 +11,7 @@ from hsutil.str import format_time, FT_DECIMAL, format_size
import time
def format_path(p):
return unicode(p[:-1])
return str(p[:-1])
def format_timestamp(t, delta):
if delta:
@@ -38,4 +38,4 @@ def format_dupe_count(c):
return str(c) if c else '---'
def cmp_value(value):
return value.lower() if isinstance(value, basestring) else value
return value.lower() if isinstance(value, str) else value

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import xml.dom.minidom
from xml.etree import ElementTree as ET
from hsutil import io
from hsutil.files import FileOrPath
@@ -124,40 +124,47 @@ class Directories(object):
else:
return STATE_NORMAL
def has_any_file(self):
try:
next(self.get_files())
return True
except StopIteration:
return False
def load_from_file(self, infile):
try:
doc = xml.dom.minidom.parse(infile)
except:
root = ET.parse(infile).getroot()
except Exception:
return
root_path_nodes = doc.getElementsByTagName('root_directory')
for rdn in root_path_nodes:
if not rdn.getAttributeNode('path'):
for rdn in root.getiterator('root_directory'):
attrib = rdn.attrib
if 'path' not in attrib:
continue
path = rdn.getAttributeNode('path').nodeValue
path = attrib['path']
try:
self.add_path(Path(path))
except (AlreadyThereError, InvalidPathError):
pass
state_nodes = doc.getElementsByTagName('state')
for sn in state_nodes:
if not (sn.getAttributeNode('path') and sn.getAttributeNode('value')):
for sn in root.getiterator('state'):
attrib = sn.attrib
if not ('path' in attrib and 'value' in attrib):
continue
path = sn.getAttributeNode('path').nodeValue
state = sn.getAttributeNode('value').nodeValue
path = attrib['path']
state = attrib['value']
self.set_state(Path(path), int(state))
def save_to_file(self, outfile):
with FileOrPath(outfile, 'wb') as fp:
doc = xml.dom.minidom.Document()
root = doc.appendChild(doc.createElement('directories'))
root = ET.Element('directories')
for root_path in self:
root_path_node = root.appendChild(doc.createElement('root_directory'))
root_path_node.setAttribute('path', unicode(root_path).encode('utf-8'))
for path, state in self.states.iteritems():
state_node = root.appendChild(doc.createElement('state'))
state_node.setAttribute('path', unicode(path).encode('utf-8'))
state_node.setAttribute('value', str(state))
doc.writexml(fp, '\t', '\t', '\n', encoding='utf-8')
root_path_node = ET.SubElement(root, 'root_directory')
root_path_node.set('path', str(root_path))
for path, state in self.states.items():
state_node = ET.SubElement(root, 'state')
state_node.set('path', str(path))
state_node.set('value', str(state))
tree = ET.ElementTree(root)
tree.write(fp, encoding='utf-8')
def set_state(self, path, state):
if self.get_state(path) == state:

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from __future__ import division
import difflib
import itertools
import logging
@@ -16,7 +16,7 @@ from unicodedata import normalize
from hsutil.misc import flatten
from hsutil.str import multi_replace
from hsutil import job
from hscommon import job
(WEIGHT_WORDS,
MATCH_SIMILAR_WORDS,
@@ -25,15 +25,15 @@ NO_FIELD_ORDER) = range(3)
JOB_REFRESH_RATE = 100
def getwords(s):
if isinstance(s, unicode):
if isinstance(s, str):
s = normalize('NFD', s)
s = multi_replace(s, "-_&+():;\\[]{}.,<>/?~!@#$*", ' ').lower()
s = ''.join(c for c in s if c in string.ascii_letters + string.digits + string.whitespace)
return filter(None, s.split(' ')) # filter() is to remove empty elements
return [_f for _f in s.split(' ') if _f] # remove empty elements
def getfields(s):
fields = [getwords(field) for field in s.split(' - ')]
return filter(None, fields)
return [_f for _f in fields if _f]
def unpack_fields(fields):
result = []
@@ -118,7 +118,7 @@ def build_word_dict(objects, j=job.nulljob):
def merge_similar_words(word_dict):
"""Take all keys in word_dict that are similar, and merge them together.
"""
keys = word_dict.keys()
keys = list(word_dict.keys())
keys.sort(key=len)# we want the shortest word to stay
while keys:
key = keys.pop(0)
@@ -138,7 +138,7 @@ def reduce_common_words(word_dict, threshold):
Because if we remove them, we will miss some duplicates!
"""
uncommon_words = set(word for word, objects in word_dict.items() if len(objects) < threshold)
for word, objects in word_dict.items():
for word, objects in list(word_dict.items()):
if len(objects) < threshold:
continue
reduced = set()

View File

@@ -6,14 +6,13 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import tempfile
import os.path as op
from tempfile import mkdtemp
# Yes, this is a very low-tech solution, but at least it doesn't have all these annoying dependency
# and resource problems.
MAIN_TEMPLATE = u"""
MAIN_TEMPLATE = """
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>
<html xmlns="http://www.w3.org/1999/xhtml">
@@ -104,34 +103,34 @@ $rows
</html>
"""
COLHEADERS_TEMPLATE = u"<th>{name}</th>"
COLHEADERS_TEMPLATE = "<th>{name}</th>"
ROW_TEMPLATE = u"""
ROW_TEMPLATE = """
<tr>
<td class="{indented}">{filename}</td>{cells}
</tr>
"""
CELL_TEMPLATE = u"""<td>{value}</td>"""
CELL_TEMPLATE = """<td>{value}</td>"""
def export_to_xhtml(colnames, rows):
# a row is a list of values with the first value being a flag indicating if the row should be indented
if rows:
assert len(rows[0]) == len(colnames) + 1 # + 1 is for the "indented" flag
colheaders = u''.join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames)
colheaders = ''.join(COLHEADERS_TEMPLATE.format(name=name) for name in colnames)
rendered_rows = []
for row in rows:
# [2:] is to remove the indented flag + filename
indented = u'indented' if row[0] else u''
indented = 'indented' if row[0] else ''
filename = row[1]
cells = u''.join(CELL_TEMPLATE.format(value=value) for value in row[2:])
cells = ''.join(CELL_TEMPLATE.format(value=value) for value in row[2:])
rendered_rows.append(ROW_TEMPLATE.format(indented=indented, filename=filename, cells=cells))
rendered_rows = u''.join(rendered_rows)
rendered_rows = ''.join(rendered_rows)
# The main template can't use format because the css code uses {}
content = MAIN_TEMPLATE.replace('$colheaders', colheaders).replace('$rows', rendered_rows)
folder = mkdtemp()
destpath = op.join(folder, u'export.htm')
fp = open(destpath, 'w')
fp.write(content.encode('utf-8'))
destpath = op.join(folder, 'export.htm')
fp = open(destpath, 'wt', encoding='utf-8')
fp.write(content)
fp.close()
return destpath

View File

@@ -12,7 +12,7 @@
# 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
@@ -25,13 +25,13 @@ 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):
if isinstance(fsobject, str):
name = fsobject
elif isinstance(fsobject, File):
name = fsobject.name
else:
name = ''
parentname = unicode(parent) if parent is not None else ''
parentname = str(parent) if parent is not None else ''
Exception.__init__(self, message.format(name=name, parent=parentname))
@@ -55,7 +55,6 @@ class OperationError(FSError):
class File(object):
INITIAL_INFO = {
'size': 0,
'ctime': 0,
'mtime': 0,
'md5': '',
'md5partial': '',
@@ -82,10 +81,9 @@ class File(object):
raise AttributeError()
def _read_info(self, field):
if field in ('size', 'ctime', 'mtime'):
if field in ('size', '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:
@@ -119,7 +117,7 @@ class File(object):
If `attrnames` is not None, caches only attrnames.
"""
if attrnames is None:
attrnames = self.INITIAL_INFO.keys()
attrnames = list(self.INITIAL_INFO.keys())
for attrname in attrnames:
if attrname not in self.__dict__:
self._read_info(attrname)

35
core/gui/base.py Normal file
View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-02-06
# Copyright 2010 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 hscommon.notify import Listener
class GUIObject(Listener):
def __init__(self, view, app):
Listener.__init__(self, app)
self.view = view
self.app = app
def directories_changed(self):
pass
def dupes_selected(self):
pass
def marking_changed(self):
pass
def problems_changed(self):
pass
def results_changed(self):
pass
def results_changed_but_keep_selection(self):
pass

View File

@@ -7,16 +7,17 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsutil.notify import Listener
from .base import GUIObject
class DetailsPanel(Listener):
class DetailsPanel(GUIObject):
def __init__(self, view, app):
Listener.__init__(self, app)
self.app = app
self.view = view
GUIObject.__init__(self, view, app)
self._table = []
def connect(self):
GUIObject.connect(self)
self._refresh()
self.connect()
self.view.refresh()
#--- Private
def _refresh(self):
@@ -31,7 +32,7 @@ class DetailsPanel(Listener):
ref = group.ref if group is not None and group.ref is not dupe else None
l2 = self.app._get_display_info(ref, group, False)
names = [c['display'] for c in self.app.data.COLUMNS]
self._table = zip(names, l1, l2)
self._table = list(zip(names, l1, l2))
#--- Public
def row_count(self):

View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-02-06
# Copyright 2010 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 hsgui.tree import Tree, Node
from ..directories import STATE_NORMAL, STATE_REFERENCE, STATE_EXCLUDED
from .base import GUIObject
STATE_ORDER = [STATE_NORMAL, STATE_REFERENCE, STATE_EXCLUDED]
# Lazily loads children
class DirectoryNode(Node):
def __init__(self, app, path, name):
Node.__init__(self, name)
self._app = app
self._directory_path = path
self._loaded = False
self._state = STATE_ORDER.index(self._app.directories.get_state(path))
def __len__(self):
if not self._loaded:
self._load()
return Node.__len__(self)
def _load(self):
self.clear()
subpaths = self._app.directories.get_subfolders(self._directory_path)
for path in subpaths:
self.append(DirectoryNode(self._app, path, path[-1]))
self._loaded = True
# The state propery is an index to the combobox
@property
def state(self):
return self._state
@state.setter
def state(self, value):
if value == self._state:
return
self._state = value
state = STATE_ORDER[value]
self._app.directories.set_state(self._directory_path, state)
class DirectoryTree(GUIObject, Tree):
def __init__(self, view, app):
GUIObject.__init__(self, view, app)
Tree.__init__(self)
def connect(self):
GUIObject.connect(self)
self._refresh()
self.view.refresh()
def _refresh(self):
self.clear()
for path in self.app.directories:
self.append(DirectoryNode(self.app, path, str(path)))
def add_directory(self, path):
self.app.add_directory(path)
#--- Event Handlers
def directories_changed(self):
self._refresh()
self.view.refresh()

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-04-12
# Copyright 2010 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 hscommon.notify import Broadcaster
from .base import GUIObject
class ProblemDialog(GUIObject, Broadcaster):
def __init__(self, view, app):
GUIObject.__init__(self, view, app)
Broadcaster.__init__(self)
self._selected_dupe = None
def reveal_selected_dupe(self):
if self._selected_dupe is not None:
self.app._reveal_path(self._selected_dupe.path)
def select_dupe(self, dupe):
self._selected_dupe = dupe
#--- Event Handlers
def problems_changed(self):
self._selected_dupe = None
self.notify('problems_changed')

43
core/gui/problem_table.py Normal file
View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-04-12
# Copyright 2010 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 hscommon.notify import Listener
from hsgui.table import GUITable, Row
class ProblemTable(GUITable, Listener):
def __init__(self, view, problem_dialog):
GUITable.__init__(self)
Listener.__init__(self, problem_dialog)
self.view = view
self.dialog = problem_dialog
#--- Override
def _update_selection(self):
row = self.selected_row
dupe = row.dupe if row is not None else None
self.dialog.select_dupe(dupe)
def _fill(self):
problems = self.dialog.app.results.problems
for dupe, msg in problems:
self.append(ProblemRow(self, dupe, msg))
#--- Event handlers
def problems_changed(self):
self.refresh()
self.view.refresh()
class ProblemRow(Row):
def __init__(self, table, dupe, msg):
Row.__init__(self, table)
self.dupe = dupe
self.msg = msg
self.path = str(dupe.path)

162
core/gui/result_table.py Normal file
View File

@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-02-11
# Copyright 2010 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 operator import attrgetter
from hsgui.table import GUITable, Row
from .base import GUIObject
class DupeRow(Row):
def __init__(self, table, group, dupe):
Row.__init__(self, table)
self._app = table.app
self._group = group
self._dupe = dupe
self._data = None
self._data_delta = None
@property
def data(self):
if self._data is None:
self._data = self._app._get_display_info(self._dupe, self._group, False)
return self._data
@property
def data_delta(self):
if self._data_delta is None:
self._data_delta = self._app._get_display_info(self._dupe, self._group, True)
return self._data_delta
@property
def isref(self):
return self._dupe is self._group.ref
@property
def markable(self):
return self._app.results.is_markable(self._dupe)
@property
def marked(self):
return self._app.results.is_marked(self._dupe)
@marked.setter
def marked(self, value):
self._app.mark_dupe(self._dupe, value)
class ResultTable(GUIObject, GUITable):
def __init__(self, view, app):
GUIObject.__init__(self, view, app)
GUITable.__init__(self)
self._power_marker = False
self._delta_values = False
self._sort_descriptors = (0, True)
#--- Override
def connect(self):
GUIObject.connect(self)
self._refresh_with_view()
def _restore_selection(self, previous_selection):
if self.app.selected_dupes:
to_find = set(self.app.selected_dupes)
indexes = [i for i, r in enumerate(self) if r._dupe in to_find]
self.selected_indexes = indexes
def _update_selection(self):
rows = self.selected_rows
self.app._select_dupes(list(map(attrgetter('_dupe'), rows)))
def _fill(self):
if not self.power_marker:
for group in self.app.results.groups:
self.append(DupeRow(self, group, group.ref))
for dupe in group.dupes:
self.append(DupeRow(self, group, dupe))
else:
for dupe in self.app.results.dupes:
group = self.app.results.get_group_of_duplicate(dupe)
self.append(DupeRow(self, group, dupe))
def _refresh_with_view(self):
self.refresh()
self.view.refresh()
self.view.show_selected_row()
#--- Public
def get_row_value(self, index, column):
try:
row = self[index]
except IndexError:
return '---'
if self.delta_values:
return row.data_delta[column]
else:
return row.data[column]
def rename_selected(self, newname):
row = self.selected_row
row._data = None
row._data_delta = None
return self.app.rename_selected(newname)
def sort(self, key, asc):
if self.power_marker:
self.app.results.sort_dupes(key, asc, self.delta_values)
else:
self.app.results.sort_groups(key, asc)
self._sort_descriptors = (key, asc)
self._refresh_with_view()
#--- 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
key, asc = self._sort_descriptors
self.sort(key, asc)
# no need to refresh, it has happened in sort()
@property
def delta_values(self):
return self._delta_values
@delta_values.setter
def delta_values(self, value):
if value == self._delta_values:
return
self._delta_values = value
self.refresh()
self.view.refresh()
@property
def selected_dupe_count(self):
return sum(1 for row in self.selected_rows if not row.isref)
#--- Event Handlers
def marking_changed(self):
self.view.invalidate_markings()
def results_changed(self):
self._refresh_with_view()
def results_changed_but_keep_selection(self):
# What we want to to here is that instead of restoring selected *dupes* after refresh, we
# restore selected *paths*.
indexes = self.selected_indexes
self.refresh()
self.select(indexes)
self.view.refresh()

23
core/gui/stats_label.py Normal file
View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-02-11
# Copyright 2010 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 .base import GUIObject
class StatsLabel(GUIObject):
def connect(self):
GUIObject.connect(self)
self.view.refresh()
@property
def display(self):
return self.app.stat_line
def results_changed(self):
self.view.refresh()
marking_changed = results_changed

View File

@@ -6,9 +6,9 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsutil.files import FileOrPath
from xml.etree import ElementTree as ET
import xml.dom.minidom
from hsutil.files import FileOrPath
class IgnoreList(object):
"""An ignore list implementation that is iterable, filterable and exportable to XML.
@@ -22,7 +22,7 @@ class IgnoreList(object):
self._count = 0
def __iter__(self):
for first,seconds in self._ignored.iteritems():
for first,seconds in self._ignored.items():
for second in seconds:
yield (first,second)
@@ -77,19 +77,18 @@ class IgnoreList(object):
infile can be a file object or a filename.
"""
try:
doc = xml.dom.minidom.parse(infile)
root = ET.parse(infile).getroot()
except Exception:
return
file_nodes = doc.getElementsByTagName('file')
for fn in file_nodes:
if not fn.getAttributeNode('path'):
file_elems = (e for e in root if e.tag == 'file')
for fn in file_elems:
file_path = fn.get('path')
if not file_path:
continue
file_path = fn.getAttributeNode('path').nodeValue
subfile_nodes = fn.getElementsByTagName('file')
for sfn in subfile_nodes:
if not sfn.getAttributeNode('path'):
continue
subfile_path = sfn.getAttributeNode('path').nodeValue
subfile_elems = (e for e in fn if e.tag == 'file')
for sfn in subfile_elems:
subfile_path = sfn.get('path')
if subfile_path:
self.Ignore(file_path, subfile_path)
def save_to_xml(self, outfile):
@@ -97,19 +96,15 @@ class IgnoreList(object):
outfile can be a file object or a filename.
"""
doc = xml.dom.minidom.Document()
root = doc.appendChild(doc.createElement('ignore_list'))
for file,subfiles in self._ignored.items():
file_node = root.appendChild(doc.createElement('file'))
if isinstance(file,unicode):
file = file.encode('utf-8')
file_node.setAttribute('path',file)
for subfile in subfiles:
subfile_node = file_node.appendChild(doc.createElement('file'))
if isinstance(subfile,unicode):
subfile = subfile.encode('utf-8')
subfile_node.setAttribute('path',subfile)
root = ET.Element('ignore_list')
for filename, subfiles in self._ignored.items():
file_node = ET.SubElement(root, 'file')
file_node.set('path', filename)
for subfilename in subfiles:
subfile_node = ET.SubElement(file_node, 'file')
subfile_node.set('path', subfilename)
tree = ET.ElementTree(root)
with FileOrPath(outfile, 'wb') as fp:
doc.writexml(fp,'\t','\t','\n',encoding='utf-8')
tree.write(fp, encoding='utf-8')

View File

@@ -8,16 +8,14 @@
import logging
import re
from xml.sax import handler, make_parser, SAXException
from xml.sax.saxutils import XMLGenerator
from xml.sax.xmlreader import AttributesImpl
from xml.etree import ElementTree as ET
from . import engine
from hsutil.job import nulljob
from hsutil.markable import Markable
from hsutil.misc import flatten, cond, nonone
from hscommon.job import nulljob
from hscommon.markable import Markable
from hsutil.misc import flatten, nonone
from hsutil.str import format_size
from hsutil.files import open_if_filename
from hsutil.files import FileOrPath
class Results(Markable):
#---Override
@@ -34,6 +32,8 @@ class Results(Markable):
self.__recalculate_stats()
self.__marked_size = 0
self.data = data_module
self.problems = [] # (dupe, error_msg)
self.is_modified = False
def _did_mark(self, dupe):
self.__marked_size += dupe.size
@@ -116,6 +116,7 @@ class Results(Markable):
self.__group_of_duplicate[dupe] = g
if not hasattr(dupe, 'is_ref'):
dupe.is_ref = False
self.is_modified = True
old_filters = nonone(self.__filters, [])
self.apply_filter(None)
for filter_str in old_filters:
@@ -148,7 +149,7 @@ class Results(Markable):
self.__filters.append(filter_str)
if self.__filtered_dupes is None:
self.__filtered_dupes = flatten(g[:] for g in self.groups)
self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(dupe.name))
self.__filtered_dupes = set(dupe for dupe in self.__filtered_dupes if filter_re.search(str(dupe.path)))
filtered_groups = set()
for dupe in self.__filtered_dupes:
filtered_groups.add(self.get_group_of_duplicate(dupe))
@@ -168,43 +169,56 @@ class Results(Markable):
is_markable = _is_markable
def load_from_xml(self, infile, get_file, j=nulljob):
def do_match(ref_file, other_files, group):
if not other_files:
return
for other_file in other_files:
group.add_match(engine.get_match(ref_file, other_file))
do_match(other_files[0], other_files[1:], group)
self.apply_filter(None)
handler = _ResultsHandler(get_file)
try:
parser = make_parser()
except Exception as e:
# This special handling is to try to figure out the cause of #47
# We don't silently return, because we want the user to send error report.
logging.exception(e)
try:
import xml.parsers.expat
logging.warning('importing xml.parsers.expat went ok, WTF?')
except Exception as e:
# This log should give a little more details about the cause of this all
logging.exception(e)
raise
raise
parser.setContentHandler(handler)
try:
infile, must_close = open_if_filename(infile)
except IOError:
root = ET.parse(infile).getroot()
except Exception:
return
BUFSIZE = 1024 * 1024 # 1mb buffer
infile.seek(0, 2)
j.start_job(infile.tell() // BUFSIZE)
infile.seek(0, 0)
group_elems = list(root.getiterator('group'))
groups = []
marked = set()
for group_elem in j.iter_with_progress(group_elems, every=100):
group = engine.Group()
dupes = []
for file_elem in group_elem.getiterator('file'):
path = file_elem.get('path')
words = file_elem.get('words', '')
if not path:
continue
file = get_file(path)
if file is None:
continue
file.words = words.split(',')
file.is_ref = file_elem.get('is_ref') == 'y'
dupes.append(file)
if file_elem.get('marked') == 'y':
marked.add(file)
for match_elem in group_elem.getiterator('match'):
try:
while True:
data = infile.read(BUFSIZE)
if not data:
break
parser.feed(data)
attrs = match_elem.attrib
first_file = dupes[int(attrs['first'])]
second_file = dupes[int(attrs['second'])]
percentage = int(attrs['percentage'])
group.add_match(engine.Match(first_file, second_file, percentage))
except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds
pass
if (not group.matches) and (len(dupes) >= 2):
do_match(dupes[0], dupes[1:], group)
group.prioritize(lambda x: dupes.index(x))
if len(group):
groups.append(group)
j.add_progress()
except SAXException:
return
self.groups = handler.groups
for dupe_file in handler.marked:
self.groups = groups
for dupe_file in marked:
self.mark(dupe_file)
self.is_modified = False
def make_ref(self, dupe):
g = self.get_group_of_duplicate(dupe)
@@ -218,19 +232,25 @@ class Results(Markable):
self.__total_count -= 1
self.__total_size -= dupe.size
self.__dupes = None
self.is_modified = True
def perform_on_marked(self, func, remove_from_results):
problems = []
for d in self.dupes:
if self.is_marked(d) and (not func(d)):
problems.append(d)
# Performs `func` on all marked dupes. If an EnvironmentError is raised during the call,
# the problematic dupe is added to self.problems.
self.problems = []
to_remove = []
marked = (dupe for dupe in self.dupes if self.is_marked(dupe))
for dupe in marked:
try:
func(dupe)
to_remove.append(dupe)
except EnvironmentError as e:
self.problems.append((dupe, str(e)))
if remove_from_results:
to_remove = [d for d in self.dupes if self.is_marked(d) and (d not in problems)]
self.remove_duplicates(to_remove)
self.mark_none()
for d in problems:
self.mark(d)
return len(problems)
for dupe, _ in self.problems:
self.mark(dupe)
def remove_duplicates(self, dupes):
'''Remove 'dupes' from their respective group, and remove the group is it ends up empty.
@@ -253,16 +273,14 @@ class Results(Markable):
for group in affected_groups:
group.discard_matches()
self.__dupes = None
self.is_modified = True
def save_to_xml(self, outfile):
self.apply_filter(None)
outfile, must_close = open_if_filename(outfile, 'wb')
writer = XMLGenerator(outfile, 'utf-8')
writer.startDocument()
empty_attrs = AttributesImpl({})
writer.startElement('results', empty_attrs)
root = ET.Element('results')
# writer = XMLGenerator(outfile, 'utf-8')
for g in self.groups:
writer.startElement('group', empty_attrs)
group_elem = ET.SubElement(root, 'group')
dupe2index = {}
for index, d in enumerate(g):
dupe2index[d] = index
@@ -270,27 +288,23 @@ class Results(Markable):
words = engine.unpack_fields(d.words)
except AttributeError:
words = ()
attrs = AttributesImpl({
'path': unicode(d.path),
'is_ref': cond(d.is_ref, 'y', 'n'),
'words': ','.join(words),
'marked': cond(self.is_marked(d), 'y', 'n')
})
writer.startElement('file', attrs)
writer.endElement('file')
file_elem = ET.SubElement(group_elem, 'file')
try:
file_elem.set('path', str(d.path))
file_elem.set('words', ','.join(words))
except ValueError: # If there's an invalid character, just skip the file
file_elem.set('path', '')
file_elem.set('is_ref', ('y' if d.is_ref else 'n'))
file_elem.set('marked', ('y' if self.is_marked(d) else 'n'))
for match in g.matches:
attrs = AttributesImpl({
'first': str(dupe2index[match.first]),
'second': str(dupe2index[match.second]),
'percentage': str(int(match.percentage)),
})
writer.startElement('match', attrs)
writer.endElement('match')
writer.endElement('group')
writer.endElement('results')
writer.endDocument()
if must_close:
outfile.close()
match_elem = ET.SubElement(group_elem, 'match')
match_elem.set('first', str(dupe2index[match.first]))
match_elem.set('second', str(dupe2index[match.second]))
match_elem.set('percentage', str(int(match.percentage)))
tree = ET.ElementTree(root)
with FileOrPath(outfile, 'wb') as fp:
tree.write(fp, encoding='utf-8')
self.is_modified = False
def sort_dupes(self, key, asc=True, delta=False):
if not self.__dupes:
@@ -310,60 +324,3 @@ class Results(Markable):
dupes = property(__get_dupe_list)
groups = property(__get_groups, __set_groups)
stat_line = property(__get_stat_line)
class _ResultsHandler(handler.ContentHandler):
def __init__(self, get_file):
self.group = None
self.dupes = None
self.marked = set()
self.groups = []
self.get_file = get_file
def startElement(self, name, attrs):
if name == 'group':
self.group = engine.Group()
self.dupes = []
return
if (name == 'file') and (self.group is not None):
if not (('path' in attrs) and ('words' in attrs)):
return
path = attrs['path']
file = self.get_file(path)
if file is None:
return
file.words = attrs['words'].split(',')
file.is_ref = attrs.get('is_ref') == 'y'
self.dupes.append(file)
if attrs.get('marked') == 'y':
self.marked.add(file)
if (name == 'match') and (self.group is not None):
try:
first_file = self.dupes[int(attrs['first'])]
second_file = self.dupes[int(attrs['second'])]
percentage = int(attrs['percentage'])
self.group.add_match(engine.Match(first_file, second_file, percentage))
except (IndexError, KeyError, ValueError): # Covers missing attr, non-int values and indexes out of bounds
pass
def endElement(self, name):
def do_match(ref_file, other_files, group):
if not other_files:
return
for other_file in other_files:
group.add_match(engine.get_match(ref_file, other_file))
do_match(other_files[0], other_files[1:], group)
if name == 'group':
group = self.group
self.group = None
dupes = self.dupes
self.dupes = []
if group is None:
return
if len(dupes) < 2:
return
if not group.matches: # <match> elements not present, do it manually, without %
do_match(dupes[0], dupes[1:], group)
group.prioritize(lambda x: dupes.index(x))
self.groups.append(group)

View File

@@ -7,25 +7,36 @@
# http://www.hardcoded.net/licenses/hs_license
import logging
import re
from hsutil import job, io
from hscommon import job
from hsutil import 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)
class ScanType:
Filename = 0
Fields = 1
FieldsNoOrder = 2
Tag = 3
# number 4 is obsolete
Contents = 5
ContentsAudio = 6
SCANNABLE_TAGS = ['track', 'artist', 'album', 'title', 'genre', 'year']
RE_DIGIT_ENDING = re.compile(r'\d+|\(\d+\)|\[\d+\]|{\d+}')
def is_same_with_digit(name, refname):
# Returns True if name is the same as refname, but with digits (with brackets or not) at the end
if not name.startswith(refname):
return False
end = name[len(refname):].strip()
return RE_DIGIT_ENDING.match(end) is not None
class Scanner(object):
def __init__(self):
self.ignore_list = IgnoreList()
@@ -37,22 +48,22 @@ class Scanner(object):
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)
if self.scan_type in (ScanType.Contents, ScanType.ContentsAudio):
sizeattr = 'size' if self.scan_type == ScanType.Contents else 'audiosize'
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==ScanType.ContentsAudio, 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
if self.scan_type == ScanType.FieldsNoOrder:
self.scan_type = ScanType.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],
ScanType.Filename: lambda f: engine.getwords(rem_file_ext(f.name)),
ScanType.Fields: lambda f: engine.getfields(rem_file_ext(f.name)),
ScanType.Tag: lambda f: [engine.getwords(str(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
}[self.scan_type]
for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'):
f.words = func(f)
@@ -66,9 +77,13 @@ class Scanner(object):
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:
if 'copy' in dupename:
return False
if 'copy' in refname:
return True
if refname.startswith(dupename) and (refname[len(dupename):].strip().isdigit()):
if is_same_with_digit(dupename, refname):
return False
if is_same_with_digit(refname, dupename):
return True
return len(dupe.path) > len(ref.path)
@@ -87,7 +102,7 @@ class Scanner(object):
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))]
if not self.ignore_list.AreIgnored(str(m.first.path), str(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])
@@ -102,7 +117,7 @@ class Scanner(object):
match_similar_words = False
min_match_percentage = 80
mix_file_kind = True
scan_type = SCAN_TYPE_FILENAME
scan_type = ScanType.Filename
scanned_tags = set(['artist', 'title'])
size_threshold = 0
word_weighting = False

View File

@@ -1,414 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2006/11/11
# Copyright 2010 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")
from ..gui.details_panel import DetailsPanel
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 CallLogger(object):
"""This is a dummy object that logs all calls made to it.
It is used to simulate the GUI layer.
"""
def __init__(self):
self.calls = []
def __getattr__(self, func_name):
def func(*args, **kw):
self.calls.append(func_name)
return func
def clear_calls(self):
del self.calls[:]
class TCDupeGuru(TestCase):
def setUp(self):
self.app = DupeGuru()
self.dpanel_gui = CallLogger()
self.dpanel = DetailsPanel(self.dpanel_gui, self.app)
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 check_gui_calls(self, gui, expected, verify_order=False):
"""Checks that the expected calls have been made to 'gui', then clears the log.
`expected` is an iterable of strings representing method names.
If `verify_order` is True, the order of the calls matters.
"""
if verify_order:
eq_(gui.calls, expected)
else:
eq_(set(gui.calls), set(expected))
gui.clear_calls()
def check_gui_calls_partial(self, gui, expected=None, not_expected=None):
"""Checks that the expected calls have been made to 'gui', then clears the log.
`expected` is an iterable of strings representing method names. Order doesn't matter.
Moreover, if calls have been made that are not in expected, no failure occur.
`not_expected` can be used for a more explicit check (rather than calling `check_gui_calls`
with an empty `expected`) to assert that calls have *not* been made.
"""
calls = set(gui.calls)
if expected is not None:
expected = set(expected)
not_called = expected - calls
assert not not_called, u"These calls haven't been made: {0}".format(not_called)
if not_expected is not None:
not_expected = set(not_expected)
called = not_expected & calls
assert not called, u"These calls shouldn't have been made: {0}".format(called)
gui.clear_calls()
def clear_gui_calls(self):
for attr in dir(self):
if attr.endswith('_gui'):
gui = getattr(self, attr)
if hasattr(gui, 'calls'): # We might have test methods ending with '_gui'
gui.clear_calls()
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):
self.app.SelectPowerMarkerNodePaths(r2np([0,2]))
eq_(self.dpanel.row(0), ('Filename', 'bar bleh', 'foo bar'))
self.check_gui_calls(self.dpanel_gui, ['refresh'])
self.app.SelectPowerMarkerNodePaths([])
eq_(self.dpanel.row(0), ('Filename', '---', '---'))
self.check_gui_calls(self.dpanel_gui, ['refresh'])
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
# Directories
eq_(app.GetOutlineViewValues(1, [0, 2]), []) # no crash
# Normal results
app.GetOutlineViewValues(0, [42, 0]) # 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

@@ -7,24 +7,48 @@
# http://www.hardcoded.net/licenses/hs_license
import os
import logging
from hsutil.testutil import eq_
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 hscommon.job import nulljob
from . import data
from .. import app, fs
from .results_test import GetTestGroups
from .. import app, fs, engine
from ..app import DupeGuru as DupeGuruBase
from ..gui.details_panel import DetailsPanel
from ..gui.directory_tree import DirectoryTree
from ..gui.result_table import ResultTable
from ..scanner import ScanType
class DupeGuru(DupeGuruBase):
def __init__(self):
DupeGuruBase.__init__(self, data, '/tmp', appid=4)
def _start_job(self, jobid, func):
func(nulljob)
def _start_job(self, jobid, func, *args):
func(nulljob, *args)
class CallLogger(object):
"""This is a dummy object that logs all calls made to it.
It is used to simulate the GUI layer.
"""
def __init__(self):
self.calls = []
def __getattr__(self, func_name):
def func(*args, **kw):
self.calls.append(func_name)
return func
def clear_calls(self):
del self.calls[:]
class TCDupeGuru(TestCase):
@@ -86,7 +110,7 @@ class TCDupeGuru(TestCase):
def test_Scan_with_objects_evaluating_to_false(self):
class FakeFile(fs.File):
def __nonzero__(self):
def __bool__(self):
return False
@@ -95,10 +119,23 @@ class TCDupeGuru(TestCase):
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.get_files = lambda: iter([f1, f2])
app.directories._dirs.append('this is just so Scan() doesnt return 3')
app.start_scanning() # no exception
def test_ignore_hardlink_matches(self):
# If the ignore_hardlink_matches option is set, don't match files hardlinking to the same
# inode.
tmppath = Path(self.tmpdir())
io.open(tmppath + 'myfile', 'w').write('foo')
os.link(str(tmppath + 'myfile'), str(tmppath + 'hardlink'))
app = DupeGuru()
app.directories.add_path(tmppath)
app.scanner.scan_type = ScanType.Contents
app.options['ignore_hardlink_matches'] = True
app.start_scanning()
eq_(len(app.results.groups), 0)
class TCDupeGuru_clean_empty_dirs(TestCase):
cls_tested_module = app
@@ -133,3 +170,307 @@ class TCDupeGuru_clean_empty_dirs(TestCase):
self.assertEqual(Path('not-empty/empty'), calls[1]['path'])
self.assertEqual(Path('not-empty'), calls[2]['path'])
class TCDupeGuruWithResults(TestCase):
def setUp(self):
self.app = DupeGuru()
self.objects,self.matches,self.groups = GetTestGroups()
self.app.results.groups = self.groups
self.dpanel_gui = CallLogger()
self.dpanel = DetailsPanel(self.dpanel_gui, self.app)
self.dtree_gui = CallLogger()
self.dtree = DirectoryTree(self.dtree_gui, self.app)
self.rtable_gui = CallLogger()
self.rtable = ResultTable(self.rtable_gui, self.app)
self.dpanel.connect()
self.dtree.connect()
self.rtable.connect()
tmppath = self.tmppath()
io.mkdir(tmppath + 'foo')
io.mkdir(tmppath + 'bar')
self.app.directories.add_path(tmppath)
def check_gui_calls(self, gui, expected, verify_order=False):
"""Checks that the expected calls have been made to 'gui', then clears the log.
`expected` is an iterable of strings representing method names.
If `verify_order` is True, the order of the calls matters.
"""
if verify_order:
eq_(gui.calls, expected)
else:
eq_(set(gui.calls), set(expected))
gui.clear_calls()
def check_gui_calls_partial(self, gui, expected=None, not_expected=None):
"""Checks that the expected calls have been made to 'gui', then clears the log.
`expected` is an iterable of strings representing method names. Order doesn't matter.
Moreover, if calls have been made that are not in expected, no failure occur.
`not_expected` can be used for a more explicit check (rather than calling `check_gui_calls`
with an empty `expected`) to assert that calls have *not* been made.
"""
calls = set(gui.calls)
if expected is not None:
expected = set(expected)
not_called = expected - calls
assert not not_called, "These calls haven't been made: {0}".format(not_called)
if not_expected is not None:
not_expected = set(not_expected)
called = not_expected & calls
assert not called, "These calls shouldn't have been made: {0}".format(called)
gui.clear_calls()
def clear_gui_calls(self):
for attr in dir(self):
if attr.endswith('_gui'):
gui = getattr(self, attr)
if hasattr(gui, 'calls'): # We might have test methods ending with '_gui'
gui.clear_calls()
def test_GetObjects(self):
objects = self.objects
groups = self.groups
r = self.rtable[0]
assert r._group is groups[0]
assert r._dupe is objects[0]
r = self.rtable[1]
assert r._group is groups[0]
assert r._dupe is objects[1]
r = self.rtable[4]
assert r._group is groups[1]
assert r._dupe is objects[4]
def test_GetObjects_after_sort(self):
objects = self.objects
groups = self.groups[:] # we need an un-sorted reference
self.rtable.sort(0, False) #0 = Filename
r = self.rtable[1]
assert r._group is groups[1]
assert r._dupe is objects[4]
def test_selected_result_node_paths_after_deletion(self):
# cases where the selected dupes aren't there are correctly handled
self.rtable.select([1, 2, 3])
self.app.remove_selected()
# The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos.
eq_(self.rtable.selected_indexes, [1]) # no exception
def test_selectResultNodePaths(self):
app = self.app
objects = self.objects
self.rtable.select([1, 2])
eq_(len(app.selected_dupes), 2)
assert app.selected_dupes[0] is objects[1]
assert app.selected_dupes[1] is objects[2]
def test_selectResultNodePaths_with_ref(self):
app = self.app
objects = self.objects
self.rtable.select([1, 2, 3])
eq_(len(app.selected_dupes), 3)
assert app.selected_dupes[0] is objects[1]
assert app.selected_dupes[1] is objects[2]
assert app.selected_dupes[2] is self.groups[1].ref
def test_selectResultNodePaths_after_sort(self):
app = self.app
objects = self.objects
groups = self.groups[:] #To keep the old order in memory
self.rtable.sort(0, False) #0 = Filename
#Now, the group order is supposed to be reversed
self.rtable.select([1, 2, 3])
eq_(len(app.selected_dupes), 3)
assert app.selected_dupes[0] is objects[4]
assert app.selected_dupes[1] is groups[0].ref
assert app.selected_dupes[2] is objects[1]
def test_selected_powermarker_node_paths(self):
# app.selected_dupes is correctly converted into paths
app = self.app
objects = self.objects
self.rtable.power_marker = True
self.rtable.select([0, 1, 2])
self.rtable.power_marker = False
eq_(self.rtable.selected_indexes, [1, 2, 4])
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
self.rtable.power_marker = True
self.rtable.select([0, 1, 2])
app.remove_selected()
eq_(self.rtable.selected_indexes, []) # no exception
def test_selectPowerMarkerRows_after_sort(self):
app = self.app
objects = self.objects
self.rtable.power_marker = True
self.rtable.sort(0, False) #0 = Filename
self.rtable.select([0, 1, 2])
eq_(len(app.selected_dupes), 3)
assert app.selected_dupes[0] is objects[4]
assert app.selected_dupes[1] is objects[2]
assert app.selected_dupes[2] is objects[1]
def test_toggleSelectedMark(self):
app = self.app
objects = self.objects
app.toggle_selected_mark_state()
eq_(app.results.mark_count, 0)
self.rtable.select([1, 4])
app.toggle_selected_mark_state()
eq_(app.results.mark_count, 2)
assert not app.results.is_marked(objects[0])
assert app.results.is_marked(objects[1])
assert not app.results.is_marked(objects[2])
assert not app.results.is_marked(objects[3])
assert app.results.is_marked(objects[4])
def test_refreshDetailsWithSelected(self):
self.rtable.select([1, 4])
eq_(self.dpanel.row(0), ('Filename', 'bar bleh', 'foo bar'))
self.check_gui_calls(self.dpanel_gui, ['refresh'])
self.rtable.select([])
eq_(self.dpanel.row(0), ('Filename', '---', '---'))
self.check_gui_calls(self.dpanel_gui, ['refresh'])
def test_makeSelectedReference(self):
app = self.app
objects = self.objects
groups = self.groups
self.rtable.select([1, 4])
app.make_selected_reference()
assert groups[0].ref is objects[1]
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
self.rtable.select([1, 2, 4])
#Only [0, 0] and [1, 0] must go ref, not [0, 1] because it is a part of the same group
app.make_selected_reference()
assert groups[0].ref is objects[1]
assert groups[1].ref is objects[4]
def test_removeSelected(self):
app = self.app
self.rtable.select([1, 4])
app.remove_selected()
eq_(len(app.results.dupes), 1) # the first path is now selected
app.remove_selected()
eq_(len(app.results.dupes), 0)
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
self.rtable.select([4]) #The dupe of the second, 2 sized group
app.add_selected_to_ignore_list()
eq_(len(app.scanner.ignore_list), 1)
self.rtable.select([1]) #first dupe of the 3 dupes group
app.add_selected_to_ignore_list()
#BOTH the ref and the other dupe should have been added
eq_(len(app.scanner.ignore_list), 3)
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.purge_ignore_list()
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,str):
self.fail()
if not isinstance(second,str):
self.fail()
app = self.app
app.scanner.ignore_list.Ignore = FakeIgnore
self.rtable.select([4])
app.add_selected_to_ignore_list()
class TCDupeGuru_renameSelected(TestCase):
def setUp(self):
p = self.tmppath()
fp = open(str(p + 'foo bar 1'),mode='w')
fp.close()
fp = open(str(p + 'foo bar 2'),mode='w')
fp.close()
fp = open(str(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
self.rtable_gui = CallLogger()
self.rtable = ResultTable(self.rtable_gui, self.app)
self.rtable.connect()
def test_simple(self):
app = self.app
g = self.groups[0]
self.rtable.select([1])
assert app.rename_selected('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]
self.rtable.select([])
self.mock(logging, 'warning', log_calls(lambda msg: None))
assert not app.rename_selected('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]
self.rtable.select([1])
self.mock(logging, 'warning', log_calls(lambda msg: None))
assert not app.rename_selected('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')

28
core/tests/conftest.py Normal file
View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-07-11
# Copyright 2010 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
# This unit is required to make tests work with py.test. When running
import py
def get_testunit(item):
if hasattr(item, 'obj'):
testunit = py.builtin._getimself(item.obj)
if hasattr(testunit, 'global_setup'):
return testunit
def pytest_runtest_setup(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_setup()
def pytest_runtest_teardown(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_teardown()

View File

@@ -10,10 +10,9 @@ 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.testutil import eq_
from hsutil.testcase import TestCase
from ..directories import *
@@ -83,8 +82,8 @@ class TCDirectories(TestCase):
def test_AddPath_non_latin(self):
p = Path(self.tmpdir())
to_add = p + u'unicode\u201a'
os.mkdir(unicode(to_add))
to_add = p + 'unicode\u201a'
os.mkdir(str(to_add))
d = Directories()
try:
d.add_path(to_add)
@@ -112,7 +111,7 @@ class TCDirectories(TestCase):
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(p,list(d.states.keys())[0])
self.assertEqual(STATE_REFERENCE,d.states[p])
def test_get_state_with_path_not_there(self):
@@ -214,11 +213,11 @@ class TCDirectories(TestCase):
def test_unicode_save(self):
d = Directories()
p1 = self.tmppath() + u'hello\xe9'
p1 = self.tmppath() + 'hello\xe9'
io.mkdir(p1)
io.mkdir(p1 + u'foo\xe9')
io.mkdir(p1 + 'foo\xe9')
d.add_path(p1)
d.set_state(p1 + u'foo\xe9', STATE_EXCLUDED)
d.set_state(p1 + 'foo\xe9', STATE_EXCLUDED)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
try:
d.save_to_file(tmpxml)

View File

@@ -8,13 +8,13 @@
import sys
from nose.tools import eq_
from hsutil import job
from hscommon import job
from hsutil.decorators import log_calls
from hsutil.misc import first
from hsutil.testutil import eq_
from hsutil.testcase import TestCase
from .. import engine, fs
from .. import engine
from ..engine import *
class NamedObject(object):
@@ -46,6 +46,15 @@ def get_test_group():
result.add_match(m3)
return result
def assert_match(m, name1, name2):
# When testing matches, whether objects are in first or second position very often doesn't
# matter. This function makes this test more convenient.
if m.first.name == name1:
eq_(m.second.name, name2)
else:
eq_(m.first.name, name2)
eq_(m.second.name, name1)
class TCgetwords(TestCase):
def test_spaces(self):
self.assertEqual(['a', 'b', 'c', 'd'], getwords("a b c d"))
@@ -53,12 +62,12 @@ class TCgetwords(TestCase):
def test_splitter_chars(self):
self.assertEqual(
[chr(i) for i in xrange(ord('a'),ord('z')+1)],
[chr(i) for i in range(ord('a'),ord('z')+1)],
getwords("a-b_c&d+e(f)g;h\\i[j]k{l}m:n.o,p<q>r/s?t~u!v@w#x$y*z")
)
def test_joiner_chars(self):
self.assertEqual(["aec"], getwords(u"a'e\u0301c"))
self.assertEqual(["aec"], getwords("a'e\u0301c"))
def test_empty(self):
self.assertEqual([], getwords(''))
@@ -67,7 +76,7 @@ class TCgetwords(TestCase):
self.assertEqual(['foo', 'bar'], getwords('FOO BAR'))
def test_decompose_unicode(self):
self.assertEqual(getwords(u'foo\xe9bar'), ['fooebar'])
self.assertEqual(getwords('foo\xe9bar'), ['fooebar'])
class TCgetfields(TestCase):
@@ -229,10 +238,9 @@ class TCbuild_word_dict(TestCase):
self.log = []
s = "foo bar"
build_word_dict([NamedObject(s, True), NamedObject(s, True), NamedObject(s, True)], j)
# We don't have intermediate log because iter_with_progress is called with every > 1
self.assertEqual(0,self.log[0])
self.assertEqual(33,self.log[1])
self.assertEqual(66,self.log[2])
self.assertEqual(100,self.log[3])
self.assertEqual(100,self.log[1])
class TCmerge_similar_words(TestCase):
@@ -352,23 +360,18 @@ class GetMatches(TestCase):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
r = getmatches(l)
self.assertEqual(2,len(r))
seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh"
m = seek[0]
self.assertEqual(['foo','bar'],m.first.words)
self.assertEqual(['bar','bleh'],m.second.words)
seek = [m for m in r if m.percentage == 33] #"foo bar" and "a b c foo"
m = seek[0]
self.assertEqual(['foo','bar'],m.first.words)
self.assertEqual(['a','b','c','foo'],m.second.words)
m = first(m for m in r if m.percentage == 50) #"foo bar" and "bar bleh"
assert_match(m, 'foo bar', 'bar bleh')
m = first(m for m in r if m.percentage == 33) #"foo bar" and "a b c foo"
assert_match(m, 'foo bar', 'a b c foo')
def test_null_and_unrelated_objects(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")]
r = getmatches(l)
self.assertEqual(1,len(r))
eq_(len(r), 1)
m = r[0]
self.assertEqual(50,m.percentage)
self.assertEqual(['foo','bar'],m.first.words)
self.assertEqual(['bar','bleh'],m.second.words)
eq_(m.percentage, 50)
assert_match(m, 'foo bar', 'bar bleh')
def test_twice_the_same_word(self):
l = [NamedObject("foo foo bar"),NamedObject("bar bleh")]
@@ -765,7 +768,7 @@ class TCget_groups(TestCase):
self.assert_(o3 in g)
def test_four_sized_group(self):
l = [NamedObject("foobar") for i in xrange(4)]
l = [NamedObject("foobar") for i in range(4)]
m = getmatches(l)
r = get_groups(m)
self.assertEqual(1,len(r))

View File

@@ -6,10 +6,10 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import cStringIO
import xml.dom.minidom
import io
from xml.etree import ElementTree as ET
from nose.tools import eq_
from hsutil.testutil import eq_
from ..ignore import *
@@ -59,37 +59,35 @@ def test_save_to_xml():
il.Ignore('foo','bar')
il.Ignore('foo','bleh')
il.Ignore('bleh','bar')
f = cStringIO.StringIO()
f = io.BytesIO()
il.save_to_xml(f)
f.seek(0)
doc = xml.dom.minidom.parse(f)
root = doc.documentElement
eq_('ignore_list',root.nodeName)
children = [c for c in root.childNodes if c.localName]
eq_(2,len(children))
eq_(2,len([c for c in children if c.nodeName == 'file']))
f1,f2 = children
subchildren = [c for c in f1.childNodes if c.localName == 'file'] +\
[c for c in f2.childNodes if c.localName == 'file']
eq_(3,len(subchildren))
doc = ET.parse(f)
root = doc.getroot()
eq_(root.tag, 'ignore_list')
eq_(len(root), 2)
eq_(len([c for c in root if c.tag == 'file']), 2)
f1, f2 = root[:]
subchildren = [c for c in f1 if c.tag == 'file'] + [c for c in f2 if c.tag == 'file']
eq_(len(subchildren), 3)
def test_SaveThenLoad():
il = IgnoreList()
il.Ignore('foo', 'bar')
il.Ignore('foo', 'bleh')
il.Ignore('bleh', 'bar')
il.Ignore(u'\u00e9','bar')
f = cStringIO.StringIO()
il.Ignore('\u00e9', 'bar')
f = io.BytesIO()
il.save_to_xml(f)
f.seek(0)
il = IgnoreList()
il.load_from_xml(f)
eq_(4,len(il))
assert il.AreIgnored(u'\u00e9','bar')
assert il.AreIgnored('\u00e9','bar')
def test_LoadXML_with_empty_file_tags():
f = cStringIO.StringIO()
f.write('<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
f = io.BytesIO()
f.write(b'<?xml version="1.0" encoding="utf-8"?><ignore_list><file><file/></file></ignore_list>')
f.seek(0)
il = IgnoreList()
il.load_from_xml(f)
@@ -129,14 +127,14 @@ def test_filter():
assert not il.AreIgnored('foo','bar')
assert il.AreIgnored('bar','baz')
def test_save_with_non_ascii_non_unicode_items():
def test_save_with_non_ascii_items():
il = IgnoreList()
il.Ignore('\xac', '\xbf')
f = cStringIO.StringIO()
f = io.BytesIO()
try:
il.save_to_xml(f)
except Exception as e:
raise AssertionError(unicode(e))
raise AssertionError(str(e))
def test_len():
il = IgnoreList()

View File

@@ -7,24 +7,25 @@
# 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 io
import os.path as op
from xml.etree import ElementTree as ET
from hsutil.path import Path
from hsutil.testutil import eq_
from hsutil.testcase import TestCase
from hsutil.misc import first
from . import engine_test, data
from .. import engine
from ..results import *
from ..results import Results
class NamedObject(engine_test.NamedObject):
path = property(lambda x:Path('basepath') + x.name)
is_ref = False
def __nonzero__(self):
def __bool__(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:
@@ -53,21 +54,24 @@ class TCResultsEmpty(TestCase):
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)
eq_("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))
eq_(0,len(self.results.groups))
def test_get_group_of_duplicate(self):
self.assert_(self.results.get_group_of_duplicate('foo') is None)
assert self.results.get_group_of_duplicate('foo') is None
def test_save_to_xml(self):
f = StringIO.StringIO()
f = io.BytesIO()
self.results.save_to_xml(f)
f.seek(0)
doc = xml.dom.minidom.parse(f)
root = doc.documentElement
self.assertEqual('results',root.nodeName)
doc = ET.parse(f)
root = doc.getroot()
eq_('results', root.tag)
def test_is_modified(self):
assert not self.results.is_modified
class TCResultsWithSomeGroups(TestCase):
@@ -77,57 +81,57 @@ class TCResultsWithSomeGroups(TestCase):
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)
eq_("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))
eq_(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)
assert isinstance(g, engine.Group)
assert o in g
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)
eq_(2,len(g1))
assert g1 in self.results.groups
self.results.remove_duplicates([g1.ref])
self.assertEqual(2,len(g1))
self.assert_(g1 in self.results.groups)
eq_(2,len(g1))
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)
eq_(0,len(g1))
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))
eq_(0,len(g2))
assert g2 not in self.results.groups
eq_(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)
eq_(0,len(g1))
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)
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)
assert self.results.groups[0] is g2
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)
assert self.results.groups[0] is g1
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.
@@ -136,24 +140,24 @@ class TCResultsWithSomeGroups(TestCase):
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)
assert self.results.groups[0] is g2
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)
eq_([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)
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)
eq_([o2,o3,o5],self.results.dupes)
self.results.make_ref(o2)
self.assertEqual([o1,o3,o5],self.results.dupes)
eq_([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)
eq_([o2,o3,o5],self.results.dupes)
def test_dupe_list_sort(self):
o1,o2,o3,o4,o5 = self.objects
@@ -163,9 +167,9 @@ class TCResultsWithSomeGroups(TestCase):
o4.size = 2
o5.size = 1
self.results.sort_dupes(2)
self.assertEqual([o5,o3,o2],self.results.dupes)
eq_([o5,o3,o2],self.results.dupes)
self.results.sort_dupes(2,False)
self.assertEqual([o2,o3,o5],self.results.dupes)
eq_([o2,o3,o5],self.results.dupes)
def test_dupe_list_remember_sort(self):
o1,o2,o3,o4,o5 = self.objects
@@ -176,7 +180,7 @@ class TCResultsWithSomeGroups(TestCase):
o5.size = 1
self.results.sort_dupes(2)
self.results.make_ref(o2)
self.assertEqual([o5,o3,o1],self.results.dupes)
eq_([o5,o3,o1],self.results.dupes)
def test_dupe_list_sort_delta_values(self):
o1,o2,o3,o4,o5 = self.objects
@@ -186,19 +190,69 @@ class TCResultsWithSomeGroups(TestCase):
o4.size = 20
o5.size = 1 #-19
self.results.sort_dupes(2,delta=True)
self.assertEqual([o5,o2,o3],self.results.dupes)
eq_([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)
eq_([],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))
eq_(3,len(self.results.dupes))
self.results.remove_duplicates([o2])
self.assertEqual(2,len(self.results.dupes))
eq_(2,len(self.results.dupes))
def test_is_modified(self):
# Changing the groups sets the modified flag
assert self.results.is_modified
def test_is_modified_after_save_and_load(self):
# Saving/Loading a file sets the modified flag back to False
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
f = io.BytesIO()
self.results.save_to_xml(f)
assert not self.results.is_modified
self.results.groups = self.groups # sets the flag back
f.seek(0)
self.results.load_from_xml(f, get_file)
assert not self.results.is_modified
class ResultsWithSavedResults(TestCase):
def setUp(self):
self.results = Results(data)
self.objects,self.matches,self.groups = GetTestGroups()
self.results.groups = self.groups
self.f = io.BytesIO()
self.results.save_to_xml(self.f)
self.f.seek(0)
def test_is_modified(self):
# Saving a file sets the modified flag back to False
assert not self.results.is_modified
def test_is_modified_after_load(self):
# Loading a file sets the modified flag back to False
def get_file(path):
return [f for f in self.objects if str(f.path) == path][0]
self.results.groups = self.groups # sets the flag back
self.results.load_from_xml(self.f, get_file)
assert not self.results.is_modified
def test_is_modified_after_remove(self):
# Removing dupes sets the modified flag
self.results.remove_duplicates([self.results.groups[0].dupes[0]])
assert self.results.is_modified
def test_is_modified_after_make_ref(self):
# Making a dupe ref sets the modified flag
self.results.make_ref(self.results.groups[0].dupes[0])
assert self.results.is_modified
class TCResultsMarkings(TestCase):
@@ -208,27 +262,27 @@ class TCResultsMarkings(TestCase):
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)
eq_("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)
eq_("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)
eq_("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)
eq_("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)
eq_("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)
eq_("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]))
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)
eq_("1 / 2 (1.00 B / 2.00 B) duplicates marked.",self.results.stat_line)
def test_perform_on_marked(self):
def log_object(o):
@@ -238,33 +292,38 @@ class TCResultsMarkings(TestCase):
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))
assert self.objects[1] in log
assert self.objects[2] in log
assert self.objects[4] in log
eq_(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))
eq_(1,len(log))
assert self.objects[4] in log
eq_(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]
if o is self.objects[1]:
raise EnvironmentError('foobar')
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]))
assert self.results.is_marked(self.objects[1])
self.results.perform_on_marked(log_object, True)
eq_(len(log), 3)
eq_(len(self.results.groups), 1)
eq_(len(self.results.groups[0]), 2)
assert self.objects[1] in self.results.groups[0]
assert not self.results.is_marked(self.objects[2])
assert self.results.is_marked(self.objects[1])
eq_(len(self.results.problems), 1)
dupe, msg = self.results.problems[0]
assert dupe is self.objects[1]
eq_(msg, 'foobar')
def test_perform_on_marked_with_ref(self):
def log_object(o):
@@ -276,61 +335,61 @@ class TCResultsMarkings(TestCase):
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))
assert self.objects[1] not in log
assert self.objects[2] in log
assert self.objects[4] in log
eq_(2,len(log))
eq_(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))
eq_(3,len(g1))
eq_(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))
eq_(0,len(g1))
eq_(0,len(g2))
eq_(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)
eq_("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)
eq_("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)
eq_("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)
eq_("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)
eq_("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)
eq_("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()
f = io.BytesIO()
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)
doc = ET.parse(f)
root = doc.getroot()
g1, g2 = root.getiterator('group')
d1, d2, d3 = g1.getiterator('file')
eq_('n', d1.get('marked'))
eq_('n', d2.get('marked'))
eq_('y', d3.get('marked'))
d1, d2 = g2.getiterator('file')
eq_('n', d1.get('marked'))
eq_('y', d2.get('marked'))
def test_LoadXML(self):
def get_file(path):
@@ -339,16 +398,16 @@ class TCResultsMarkings(TestCase):
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()
f = io.BytesIO()
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]))
assert not r.is_marked(self.objects[0])
assert not r.is_marked(self.objects[1])
assert r.is_marked(self.objects[2])
assert not r.is_marked(self.objects[3])
assert r.is_marked(self.objects[4])
class TCResultsXML(TestCase):
@@ -363,41 +422,38 @@ class TCResultsXML(TestCase):
def test_save_to_xml(self):
self.objects[0].is_ref = True
self.objects[0].words = [['foo','bar']]
f = StringIO.StringIO()
f = io.BytesIO()
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)
doc = ET.parse(f)
root = doc.getroot()
eq_('results', root.tag)
eq_(2, len(root))
eq_(2, len([c for c in root if c.tag == 'group']))
g1, g2 = root
eq_(6,len(g1))
eq_(3,len([c for c in g1 if c.tag == 'file']))
eq_(3,len([c for c in g1 if c.tag == 'match']))
d1, d2, d3 = [c for c in g1 if c.tag == 'file']
eq_(op.join('basepath','foo bar'),d1.get('path'))
eq_(op.join('basepath','bar bleh'),d2.get('path'))
eq_(op.join('basepath','foo bleh'),d3.get('path'))
eq_('y',d1.get('is_ref'))
eq_('n',d2.get('is_ref'))
eq_('n',d3.get('is_ref'))
eq_('foo,bar',d1.get('words'))
eq_('bar,bleh',d2.get('words'))
eq_('foo,bleh',d3.get('words'))
eq_(3,len(g2))
eq_(2,len([c for c in g2 if c.tag == 'file']))
eq_(1,len([c for c in g2 if c.tag == 'match']))
d1, d2 = [c for c in g2 if c.tag == 'file']
eq_(op.join('basepath','ibabtu'),d1.get('path'))
eq_(op.join('basepath','ibabtu'),d2.get('path'))
eq_('n',d1.get('is_ref'))
eq_('n',d2.get('is_ref'))
eq_('ibabtu',d1.get('words'))
eq_('ibabtu',d2.get('words'))
def test_LoadXML(self):
def get_file(path):
@@ -405,30 +461,30 @@ class TCResultsXML(TestCase):
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()
f = io.BytesIO()
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))
eq_(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)
eq_(3,len(g1))
assert g1[0].is_ref
assert not g1[1].is_ref
assert not g1[2].is_ref
assert g1[0] is self.objects[0]
assert g1[1] is self.objects[1]
assert g1[2] is self.objects[2]
eq_(['foo','bar'],g1[0].words)
eq_(['bar','bleh'],g1[1].words)
eq_(['foo','bleh'],g1[2].words)
eq_(2,len(g2))
assert not g2[0].is_ref
assert not g2[1].is_ref
assert g2[0] is self.objects[3]
assert g2[1] is self.objects[4]
eq_(['ibabtu'],g2[0].words)
eq_(['ibabtu'],g2[1].words)
def test_LoadXML_with_filename(self):
def get_file(path):
@@ -439,7 +495,7 @@ class TCResultsXML(TestCase):
self.results.save_to_xml(filename)
r = Results(data)
r.load_from_xml(filename,get_file)
self.assertEqual(2,len(r.groups))
eq_(2,len(r.groups))
def test_LoadXML_with_some_files_that_dont_exist_anymore(self):
def get_file(path):
@@ -448,84 +504,84 @@ class TCResultsXML(TestCase):
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()
f = io.BytesIO()
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]))
eq_(1,len(r.groups))
eq_(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')
root = ET.Element('foobar') #The root element shouldn't matter, really.
group_node = ET.SubElement(root, 'group')
dupe_node = ET.SubElement(group_node, 'file') #Perfectly correct file
dupe_node.set('path', op.join('basepath','foo bar'))
dupe_node.set('is_ref', 'y')
dupe_node.set('words', 'foo,bar')
dupe_node = ET.SubElement(group_node, 'file') #is_ref missing, default to 'n'
dupe_node.set('path',op.join('basepath','foo bleh'))
dupe_node.set('words','foo,bleh')
dupe_node = ET.SubElement(group_node, 'file') #words are missing, valid.
dupe_node.set('path',op.join('basepath','bar bleh'))
dupe_node = ET.SubElement(group_node, 'file') #path is missing, invalid.
dupe_node.set('words','foo,bleh')
dupe_node = ET.SubElement(group_node, 'foobar') #Invalid element name
dupe_node.set('path',op.join('basepath','bar bleh'))
dupe_node.set('is_ref','y')
dupe_node.set('words','bar,bleh')
match_node = ET.SubElement(group_node, 'match') # match pointing to a bad index
match_node.set('first', '42')
match_node.set('second', '45')
match_node = ET.SubElement(group_node, 'match') # match with missing attrs
match_node = ET.SubElement(group_node, 'match') # match with non-int values
match_node.set('first', 'foo')
match_node.set('second', 'bar')
match_node.set('percentage', 'baz')
group_node = ET.SubElement(root, 'foobar') #invalid group
group_node = ET.SubElement(root, 'group') #empty group
f = io.BytesIO()
tree = ET.ElementTree(root)
tree.write(f, 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]))
eq_(1,len(r.groups))
eq_(3,len(r.groups[0]))
def test_xml_non_ascii(self):
def get_file(path):
if path == op.join('basepath',u'\xe9foo bar'):
if path == op.join('basepath','\xe9foo bar'):
return objects[0]
if path == op.join('basepath',u'bar bleh'):
if path == op.join('basepath','bar bleh'):
return objects[1]
objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)]
objects = [NamedObject("\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()
f = io.BytesIO()
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)
eq_("\xe9foo bar",g[0].name)
eq_(['efoo','bar'],g[0].words)
def test_load_invalid_xml(self):
f = StringIO.StringIO()
f.write('<this is invalid')
f = io.BytesIO()
f.write(b'<this is invalid')
f.seek(0)
r = Results(data)
r.load_from_xml(f,None)
self.assertEqual(0,len(r.groups))
eq_(0,len(r.groups))
def test_load_non_existant_xml(self):
r = Results(data)
@@ -533,7 +589,7 @@ class TCResultsXML(TestCase):
r.load_from_xml('does_not_exist.xml', None)
except IOError:
self.fail()
self.assertEqual(0,len(r.groups))
eq_(0,len(r.groups))
def test_remember_match_percentage(self):
group = self.groups[0]
@@ -543,7 +599,7 @@ class TCResultsXML(TestCase):
fake_matches.add(engine.Match(d1, d3, 43))
fake_matches.add(engine.Match(d2, d3, 46))
group.matches = fake_matches
f = StringIO.StringIO()
f = io.BytesIO()
results = self.results
results.save_to_xml(f)
f.seek(0)
@@ -552,21 +608,31 @@ class TCResultsXML(TestCase):
group = results.groups[0]
d1, d2, d3 = group
match = group.get_match_of(d2) #d1 - d2
self.assertEqual(42, match[2])
eq_(42, match[2])
match = group.get_match_of(d3) #d1 - d3
self.assertEqual(43, match[2])
eq_(43, match[2])
group.switch_ref(d2)
match = group.get_match_of(d3) #d2 - d3
self.assertEqual(46, match[2])
eq_(46, match[2])
def test_save_and_load(self):
# previously, when reloading matches, they wouldn't be reloaded as namedtuples
f = StringIO.StringIO()
f = io.BytesIO()
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
def test_apply_filter_works_on_paths(self):
# apply_filter() searches on the whole path, not just on the filename.
self.results.apply_filter('basepath')
eq_(len(self.results.groups), 2)
def test_save_xml_with_invalid_characters(self):
# Don't crash when saving files that have invalid xml characters in their path
self.objects[0].name = 'foo\x19'
self.results.save_to_xml(io.BytesIO()) # don't crash
class TCResultsFilter(TestCase):
def setUp(self):
@@ -576,47 +642,47 @@ class TCResultsFilter(TestCase):
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])
eq_(1, len(self.results.groups))
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])
eq_(1, len(self.results.dupes))
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))
eq_(3, len(self.results.dupes))
eq_(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])
eq_(1, len(self.results.dupes))
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))
eq_(1, len(self.results.groups))
eq_(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))
eq_(1, len(self.results.groups))
eq_(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)
eq_(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)
eq_(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)
eq_(expected, self.results.stat_line)
def test_mark_count_is_filtered_as_well(self):
self.results.apply_filter(None)
@@ -625,7 +691,7 @@ class TCResultsFilter(TestCase):
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)
eq_(expected, self.results.stat_line)
def test_sort_groups(self):
self.results.apply_filter(None)
@@ -633,22 +699,22 @@ class TCResultsFilter(TestCase):
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)
assert self.results.groups[0] is g2
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)
assert self.results.groups[0] is g2
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)
assert self.results.groups[1] is g2
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])
eq_(1, len(self.results.groups))
assert self.results.groups[0] is self.groups[0]
def test_load_cancels_filter(self):
def get_file(path):
@@ -660,23 +726,23 @@ class TCResultsFilter(TestCase):
r = Results(data)
r.apply_filter('foo')
r.load_from_xml(filename,get_file)
self.assertEqual(2,len(r.groups))
eq_(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))
eq_(2,len(self.results.groups))
eq_(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))
eq_(1,len(self.results.groups))
eq_(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))
eq_(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.
@@ -687,10 +753,10 @@ class TCResultsFilter(TestCase):
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)
eq_(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)
eq_(expected, self.results.stat_line)
class TCResultsRefFile(TestCase):
@@ -703,15 +769,15 @@ class TCResultsRefFile(TestCase):
def test_stat_line(self):
expected = '0 / 2 (0.00 B / 2.00 B) duplicates marked.'
self.assertEqual(expected, self.results.stat_line)
eq_(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)
eq_(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)
eq_(expected, self.results.stat_line)

View File

@@ -6,10 +6,11 @@
# 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 hscommon import job
from hsutil import io
from hsutil.path import Path
from hsutil.testutil import eq_
from hsutil.testcase import TestCase
from .. import fs
@@ -24,6 +25,9 @@ class NamedObject(object):
self.path = Path('')
self.words = getwords(name)
def __repr__(self):
return '<NamedObject %r>' % self.name
no = NamedObject
@@ -42,7 +46,7 @@ class ScannerTestFakeFiles(TestCase):
def test_default_settings(self):
s = Scanner()
eq_(s.min_match_percentage, 80)
eq_(s.scan_type, SCAN_TYPE_FILENAME)
eq_(s.scan_type, ScanType.Filename)
eq_(s.mix_file_kind, True)
eq_(s.word_weighting, False)
eq_(s.match_similar_words, False)
@@ -94,7 +98,7 @@ class ScannerTestFakeFiles(TestCase):
def test_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = f[0].md5partial = 'foobar'
f[1].md5 = f[1].md5partial = 'foobar'
@@ -111,13 +115,13 @@ class ScannerTestFakeFiles(TestCase):
raise AssertionError()
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
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
s.scan_type = ScanType.Contents
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = f[0].md5partial = 'foobar'
f[1].md5 = f[1].md5partial = 'foobar'
@@ -133,7 +137,7 @@ class ScannerTestFakeFiles(TestCase):
def test_content_scan_doesnt_put_md5_in_words_at_the_end(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT
s.scan_type = ScanType.Contents
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'
@@ -187,21 +191,21 @@ class ScannerTestFakeFiles(TestCase):
def test_fields(self):
s = Scanner()
s.scan_type = SCAN_TYPE_FIELDS
s.scan_type = ScanType.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
s.scan_type = ScanType.FieldsNoOrder
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
s.scan_type = ScanType.Tag
o1 = no('foo')
o2 = no('bar')
o1.artist = 'The White Stripes'
@@ -213,7 +217,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_with_album_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['artist', 'album', 'title'])
o1 = no('foo')
o2 = no('bar')
@@ -232,7 +236,7 @@ class ScannerTestFakeFiles(TestCase):
def test_that_dash_in_tags_dont_create_new_fields(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['artist', 'album', 'title'])
s.min_match_percentage = 50
o1 = no('foo')
@@ -248,7 +252,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_with_different_scanned(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['track', 'year'])
o1 = no('foo')
o2 = no('bar')
@@ -265,7 +269,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_only_scans_existing_tags(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['artist', 'foo'])
o1 = no('foo')
o2 = no('bar')
@@ -278,7 +282,7 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_converts_to_str(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['track'])
o1 = no('foo')
o2 = no('bar')
@@ -292,12 +296,12 @@ class ScannerTestFakeFiles(TestCase):
def test_tag_scan_non_ascii(self):
s = Scanner()
s.scan_type = SCAN_TYPE_TAG
s.scan_type = ScanType.Tag
s.scanned_tags = set(['title'])
o1 = no('foo')
o2 = no('bar')
o1.title = u'foobar\u00e9'
o2.title = u'foobar\u00e9'
o1.title = 'foobar\u00e9'
o2.title = 'foobar\u00e9'
try:
r = s.GetDupeGroups([o1, o2])
except UnicodeEncodeError:
@@ -306,7 +310,7 @@ class ScannerTestFakeFiles(TestCase):
def test_audio_content_scan(self):
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
s.scan_type = ScanType.ContentsAudio
f = [no('foo'), no('bar'), no('bleh')]
f[0].md5 = 'foo'
f[1].md5 = 'bar'
@@ -328,7 +332,7 @@ class ScannerTestFakeFiles(TestCase):
raise AssertionError()
s = Scanner()
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
s.scan_type = ScanType.ContentsAudio
f = [MyFile('foo'), MyFile('bar')]
f[0].audiosize = 1
f[1].audiosize = 2
@@ -361,11 +365,11 @@ class ScannerTestFakeFiles(TestCase):
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))
f1.path = Path('foo1\u00e9')
f2.path = Path('foo2\u00e9')
f3.path = Path('foo3\u00e9')
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]
@@ -378,7 +382,7 @@ class ScannerTestFakeFiles(TestCase):
# 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):
def __bool__(self):
return False
@@ -425,11 +429,20 @@ class ScannerTestFakeFiles(TestCase):
# 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 = no('foo bar 42')
o2 = no('foo bar [42]')
o3 = no('foo bar (42)')
o4 = no('foo bar {42}')
o5 = no('foo bar')
# all numbered names have deeper paths, so they'll end up ref if the digits aren't correctly
# used as tie breakers
o1.path = Path('deeper/path')
o2.path = Path('foo')
[group] = s.GetDupeGroups([o1, o2])
assert group.ref is o2
o2.path = Path('deeper/path')
o3.path = Path('deeper/path')
o4.path = Path('deeper/path')
o5.path = Path('foo')
[group] = s.GetDupeGroups([o1, o2, o3, o4, o5])
assert group.ref is o5
def test_partial_group_match(self):
# Count the number od discarded matches (when a file doesn't match all other dupes of the
@@ -452,7 +465,7 @@ class ScannerTest(TestCase):
# 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
s.scan_type = ScanType.Contents
p = self.tmppath()
io.open(p + 'file1', 'w').write('foo')
io.open(p + 'file2', 'w').write('foo')

View File

@@ -10,7 +10,7 @@ import logging
from appscript import app, k, CommandError
import time
from hsutil.cocoa import as_fetch
from hscommon.cocoa import as_fetch
from core.app_cocoa import JOBID2TITLE, DupeGuru as DupeGuruBase
@@ -41,7 +41,7 @@ class DupeGuruME(DupeGuruBase):
try:
track.delete(timeout=0)
except CommandError as e:
logging.warning('Error while trying to remove a track from iTunes: %s' % unicode(e))
logging.warning('Error while trying to remove a track from iTunes: %s' % str(e))
self._start_job(JOB_REMOVE_DEAD_TRACKS, do)

View File

@@ -18,7 +18,6 @@ COLUMNS = [
{'attr':'bitrate','display':'Bitrate'},
{'attr':'samplerate','display':'Sample Rate'},
{'attr':'extension','display':'Kind'},
{'attr':'ctime','display':'Creation'},
{'attr':'mtime','display':'Modification'},
{'attr':'title','display':'Title'},
{'attr':'artist','display':'Artist'},
@@ -32,7 +31,7 @@ COLUMNS = [
{'attr':'dupe_count','display':'Dupe Count'},
]
METADATA_TO_READ = ['size', 'ctime', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
METADATA_TO_READ = ['size', 'mtime', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment']
def GetDisplayInfo(dupe, group, delta):
@@ -40,7 +39,6 @@ def GetDisplayInfo(dupe, group, delta):
duration = dupe.duration
bitrate = dupe.bitrate
samplerate = dupe.samplerate
ctime = dupe.ctime
mtime = dupe.mtime
m = group.get_match_of(dupe)
if m:
@@ -52,7 +50,6 @@ def GetDisplayInfo(dupe, group, delta):
duration -= r.duration
bitrate -= r.bitrate
samplerate -= r.samplerate
ctime -= r.ctime
mtime -= r.mtime
else:
percentage = group.percentage
@@ -65,7 +62,6 @@ def GetDisplayInfo(dupe, group, delta):
str(bitrate),
str(samplerate),
dupe.extension,
format_timestamp(ctime,delta and m),
format_timestamp(mtime,delta and m),
dupe.title,
dupe.artist,

View File

@@ -7,7 +7,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsmedia import mpeg, wma, mp4, ogg, flac, aiff
from hsaudiotag import mpeg, wma, mp4, ogg, flac, aiff
from hsutil.str import get_file_ext
from core import fs
@@ -42,12 +42,12 @@ class Mp3File(MusicFile):
HANDLED_EXTS = set(['mp3'])
def _read_info(self, field):
if field == 'md5partial':
fileinfo = mpeg.Mpeg(unicode(self.path))
fileinfo = mpeg.Mpeg(str(self.path))
self._md5partial_offset = fileinfo.audio_offset
self._md5partial_size = fileinfo.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
fileinfo = mpeg.Mpeg(unicode(self.path))
fileinfo = mpeg.Mpeg(str(self.path))
self.audiosize = fileinfo.audio_size
self.bitrate = fileinfo.bitrate
self.duration = fileinfo.duration
@@ -70,12 +70,12 @@ class WmaFile(MusicFile):
HANDLED_EXTS = set(['wma'])
def _read_info(self, field):
if field == 'md5partial':
dec = wma.WMADecoder(unicode(self.path))
dec = wma.WMADecoder(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = wma.WMADecoder(unicode(self.path))
dec = wma.WMADecoder(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -92,13 +92,13 @@ class Mp4File(MusicFile):
HANDLED_EXTS = set(['m4a', 'm4p'])
def _read_info(self, field):
if field == 'md5partial':
dec = mp4.File(unicode(self.path))
dec = mp4.File(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
dec.close()
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = mp4.File(unicode(self.path))
dec = mp4.File(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -116,12 +116,12 @@ class OggFile(MusicFile):
HANDLED_EXTS = set(['ogg'])
def _read_info(self, field):
if field == 'md5partial':
dec = ogg.Vorbis(unicode(self.path))
dec = ogg.Vorbis(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = ogg.Vorbis(unicode(self.path))
dec = ogg.Vorbis(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -138,12 +138,12 @@ class FlacFile(MusicFile):
HANDLED_EXTS = set(['flac'])
def _read_info(self, field):
if field == 'md5partial':
dec = flac.FLAC(unicode(self.path))
dec = flac.FLAC(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = flac.FLAC(unicode(self.path))
dec = flac.FLAC(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
@@ -160,12 +160,12 @@ class AiffFile(MusicFile):
HANDLED_EXTS = set(['aif', 'aiff', 'aifc'])
def _read_info(self, field):
if field == 'md5partial':
dec = aiff.File(unicode(self.path))
dec = aiff.File(str(self.path))
self._md5partial_offset = dec.audio_offset
self._md5partial_size = dec.audio_size
MusicFile._read_info(self, field)
if field in TAG_FIELDS:
dec = aiff.File(unicode(self.path))
dec = aiff.File(str(self.path))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration

28
core_me/tests/conftest.py Normal file
View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-07-11
# Copyright 2010 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
# This unit is required to make tests work with py.test. When running
import py
def get_testunit(item):
if hasattr(item, 'obj'):
testunit = py.builtin._getimself(item.obj)
if hasattr(testunit, 'global_setup'):
return testunit
def pytest_runtest_setup(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_setup()
def pytest_runtest_teardown(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_teardown()

View File

@@ -7,17 +7,17 @@
# http://www.hardcoded.net/licenses/hs_license
import os.path as op
import logging
import plistlib
import logging
import re
from appscript import app, k, CommandError
from hsutil import io
from hsutil.str import get_file_ext
from hsutil.str import get_file_ext, remove_invalid_xml
from hsutil.path import Path
from hsutil.cocoa import as_fetch
from hsutil.cocoa.objcmin import NSUserDefaults, NSURL
from hscommon.cocoa import as_fetch
from hscommon.cocoa.objcmin import NSUserDefaults, NSURL
from core import fs
from core import app_cocoa, directories
@@ -38,15 +38,15 @@ class Photo(fs.File):
def _read_info(self, field):
fs.File._read_info(self, field)
if field == 'dimensions':
self.dimensions = _block_osx.get_image_size(unicode(self.path))
self.dimensions = _block_osx.get_image_size(str(self.path))
def get_blocks(self, block_count_per_side):
try:
blocks = _block_osx.getblocks(unicode(self.path), block_count_per_side)
blocks = _block_osx.getblocks(str(self.path), block_count_per_side)
except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(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' % unicode(self.path))
raise IOError('The picture %s could not be read' % str(self.path))
return blocks
@@ -68,16 +68,16 @@ def get_iphoto_database_path():
def get_iphoto_pictures(plistpath):
if not io.exists(plistpath):
return []
s = io.open(plistpath).read()
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 = s.replace('\x10', '')
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.readPlistFromString(s)
plist = plistlib.readPlistFromBytes(s.encode('utf-8'))
result = []
for photo_data in plist['Master Image List'].values():
if photo_data['MediaType'] != 'Image':
@@ -122,6 +122,15 @@ class Directories(directories.Directories):
else:
directories.Directories.add_path(self, path)
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 any(path == Path('iPhoto Library') for path in self._dirs):
return True
else:
return directories.Directories.has_any_file(self)
class DupeGuruPE(app_cocoa.DupeGuru):
def __init__(self):
@@ -130,10 +139,10 @@ class DupeGuruPE(app_cocoa.DupeGuru):
self.directories = Directories()
self.scanner.cache_path = op.join(self.appdata, 'cached_pictures.db')
def _do_delete(self, j):
def _do_delete(self, j, replace_with_hardlinks):
def op(dupe):
j.add_progress()
return self._do_delete_dupe(dupe)
return self._do_delete_dupe(dupe, replace_with_hardlinks)
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
self.path2iphoto = {}
@@ -146,34 +155,29 @@ class DupeGuruPE(app_cocoa.DupeGuru):
photos = as_fetch(a.photo_library_album().photos, k.item)
for photo in j.iter_with_progress(photos):
try:
self.path2iphoto[unicode(photo.image_path(timeout=0))] = photo
self.path2iphoto[str(photo.image_path(timeout=0))] = photo
except CommandError:
pass
except (CommandError, RuntimeError):
pass
j.start_job(self.results.mark_count, "Sending dupes to the Trash")
self.last_op_error_count = self.results.perform_on_marked(op, True)
self.results.perform_on_marked(op, True)
del self.path2iphoto
def _do_delete_dupe(self, dupe):
def _do_delete_dupe(self, dupe, replace_with_hardlinks):
if isinstance(dupe, IPhoto):
if unicode(dupe.path) in self.path2iphoto:
photo = self.path2iphoto[unicode(dupe.path)]
if str(dupe.path) in self.path2iphoto:
photo = self.path2iphoto[str(dupe.path)]
try:
a = app('iPhoto')
a.remove(photo, timeout=0)
return True
except (CommandError, RuntimeError):
return False
except (CommandError, RuntimeError) as e:
raise EnvironmentError(str(e))
else:
logging.warning(u"Could not find photo %s in iPhoto Library", unicode(dupe.path))
return False
msg = "Could not find photo %s in iPhoto Library" % str(dupe.path)
raise EnvironmentError(msg)
else:
return app_cocoa.DupeGuru._do_delete_dupe(self, dupe)
def _do_load(self, j):
self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml'))
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
app_cocoa.DupeGuru._do_delete_dupe(self, dupe, replace_with_hardlinks)
def _get_file(self, str_path):
p = Path(str_path)

View File

@@ -6,7 +6,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from _block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
from ._block import NoBlocksError, DifferentBlockCountError, avgdiff, getblocks2
# Converted to C
# def getblock(image):

View File

@@ -10,7 +10,7 @@ import os
import logging
import sqlite3 as sqlite
from _cache import string_to_colors
from ._cache import string_to_colors
def colors_to_string(colors):
"""Transform the 3 sized tuples 'colors' into a hex string.
@@ -82,7 +82,7 @@ class Cache(object):
self.con.execute(sql, [value, key])
except sqlite.OperationalError:
logging.warning('Picture cache could not set %r for key %r', value, key)
except sqlite.DatabaseError, e:
except sqlite.DatabaseError as e:
logging.warning('DatabaseError while setting %r for key %r: %s', value, key, str(e))
def _create_con(self, second_try=False):
@@ -97,7 +97,7 @@ class Cache(object):
self.con.execute("select * from pictures where 1=2")
except sqlite.OperationalError: # new db
create_tables()
except sqlite.DatabaseError, e: # corrupted db
except sqlite.DatabaseError as e: # corrupted db
if second_try:
raise # Something really strange is happening
logging.warning('Could not create picture cache because of an error: %s', str(e))

View File

@@ -18,19 +18,17 @@ COLUMNS = [
{'attr':'size','display':'Size (KB)'},
{'attr':'extension','display':'Kind'},
{'attr':'dimensions','display':'Dimensions'},
{'attr':'ctime','display':'Creation'},
{'attr':'mtime','display':'Modification'},
{'attr':'percentage','display':'Match %'},
{'attr':'dupe_count','display':'Dupe Count'},
]
METADATA_TO_READ = ['size', 'ctime', 'mtime', 'dimensions']
METADATA_TO_READ = ['size', 'mtime', 'dimensions']
def GetDisplayInfo(dupe,group,delta=False):
if (dupe is None) or (group is None):
return ['---'] * len(COLUMNS)
size = dupe.size
ctime = dupe.ctime
mtime = dupe.mtime
m = group.get_match_of(dupe)
if m:
@@ -39,7 +37,6 @@ def GetDisplayInfo(dupe,group,delta=False):
if delta:
r = group.ref
size -= r.size
ctime -= r.ctime
mtime -= r.mtime
else:
percentage = group.percentage
@@ -51,7 +48,6 @@ def GetDisplayInfo(dupe,group,delta=False):
format_size(size, 0, 1, False),
dupe.extension,
format_dimensions(dupe.dimensions),
format_timestamp(ctime, delta and m),
format_timestamp(mtime, delta and m),
format_perc(percentage),
format_dupe_count(dupe_count)

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env python
# Created By: Virgil Dupras
# Created On: 2009-05-26
# Copyright 2010 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
import os.path as op
def move(src, dst):
if not op.exists(src):
return
if op.exists(dst):
os.remove(dst)
print 'Moving %s --> %s' % (src, dst)
os.rename(src, dst)
os.chdir('modules')
os.system('python setup.py build_ext --inplace')
os.chdir('..')
move(op.join('modules', '_block.so'), '_block.so')
move(op.join('modules', '_block.pyd'), '_block.pyd')
move(op.join('modules', '_block_osx.so'), '_block_osx.so')
move(op.join('modules', '_cache.so'), '_cache.so')
move(op.join('modules', '_cache.pyd'), '_cache.pyd')

View File

@@ -8,11 +8,9 @@
import logging
import multiprocessing
from Queue import Empty
from collections import defaultdict
from collections import defaultdict, deque
from hsutil import job
from hsutil.misc import dedupe
from hscommon import job
from core.engine import Match
from .block import avgdiff, DifferentBlockCountError, NoBlocksError
@@ -36,16 +34,16 @@ def prepare_pictures(pictures, cache_path, j=job.nulljob):
try:
for picture in j.iter_with_progress(pictures, 'Analyzed %d/%d pictures'):
picture.dimensions
picture.unicode_path = unicode(picture.path)
picture.unicode_path = str(picture.path)
try:
if picture.unicode_path not in cache:
blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
cache[picture.unicode_path] = blocks
prepared.append(picture)
except IOError as e:
logging.warning(unicode(e))
except (IOError, ValueError) as e:
logging.warning(str(e))
except MemoryError:
logging.warning(u'Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size))
logging.warning('Ran out of memory while reading %s of size %d' % (picture.unicode_path, picture.size))
if picture.size < 10 * 1024 * 1024: # We're really running out of memory
raise
except MemoryError:
@@ -76,13 +74,6 @@ def async_compare(ref_id, other_ids, dbname, threshold):
return results
def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nulljob):
def empty_out_queue(queue, into):
try:
while True:
into.append(queue.get(block=False))
except Empty:
pass
j = j.start_subjob([3, 7])
pictures = prepare_pictures(pictures, cache_path, j)
j = j.start_subjob([9, 1], 'Preparing for matching')
@@ -100,7 +91,7 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
cache.close()
pictures = [p for p in pictures if hasattr(p, 'cache_id')]
pool = multiprocessing.Pool()
async_results = []
async_results = deque()
matches = []
pictures_copy = set(pictures)
for ref in j.iter_with_progress(pictures, 'Matched %d/%d pictures'):
@@ -111,10 +102,17 @@ def getmatches(pictures, cache_path, threshold=75, match_scaled=False, j=job.nul
others = [pic for pic in others if not pic.is_ref]
if others:
cache_ids = [f.cache_id for f in others]
args = (ref.cache_id, cache_ids, cache_path, threshold)
# We limit the number of cache_ids we send for multi-processing because otherwise, we
# might get an error saying "String or BLOB exceeded size limit"
ARG_LIMIT = 1000
while cache_ids:
args = (ref.cache_id, cache_ids[:ARG_LIMIT], cache_path, threshold)
async_results.append(pool.apply_async(async_compare, args))
if len(async_results) > RESULTS_QUEUE_LIMIT:
result = async_results.pop(0)
cache_ids = cache_ids[ARG_LIMIT:]
# We use a while here because it's possible that more than one result has been added if
# ARG_LIMIT has been reached.
while len(async_results) > RESULTS_QUEUE_LIMIT:
result = async_results.popleft()
matches.extend(result.get())
for result in async_results: # process the rest of the results
matches.extend(result.get())

View File

@@ -39,9 +39,9 @@ static PyObject* getblock(PyObject *image)
pg = PySequence_ITEM(ppixel, 1);
pb = PySequence_ITEM(ppixel, 2);
Py_DECREF(ppixel);
r = PyInt_AsSsize_t(pr);
g = PyInt_AsSsize_t(pg);
b = PyInt_AsSsize_t(pb);
r = PyLong_AsLong(pr);
g = PyLong_AsLong(pg);
b = PyLong_AsLong(pb);
Py_DECREF(pr);
Py_DECREF(pg);
Py_DECREF(pb);
@@ -67,14 +67,14 @@ static PyObject* getblock(PyObject *image)
*/
static int diff(PyObject *first, PyObject *second)
{
Py_ssize_t r1, g1, b1, r2, b2, g2;
int r1, g1, b1, r2, b2, g2;
PyObject *pr, *pg, *pb;
pr = PySequence_ITEM(first, 0);
pg = PySequence_ITEM(first, 1);
pb = PySequence_ITEM(first, 2);
r1 = PyInt_AsSsize_t(pr);
g1 = PyInt_AsSsize_t(pg);
b1 = PyInt_AsSsize_t(pb);
r1 = PyLong_AsLong(pr);
g1 = PyLong_AsLong(pg);
b1 = PyLong_AsLong(pb);
Py_DECREF(pr);
Py_DECREF(pg);
Py_DECREF(pb);
@@ -82,9 +82,9 @@ static int diff(PyObject *first, PyObject *second)
pr = PySequence_ITEM(second, 0);
pg = PySequence_ITEM(second, 1);
pb = PySequence_ITEM(second, 2);
r2 = PyInt_AsSsize_t(pr);
g2 = PyInt_AsSsize_t(pg);
b2 = PyInt_AsSsize_t(pb);
r2 = PyLong_AsLong(pr);
g2 = PyLong_AsLong(pg);
b2 = PyLong_AsLong(pb);
Py_DECREF(pr);
Py_DECREF(pg);
Py_DECREF(pb);
@@ -115,8 +115,8 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
pimage_size = PyObject_GetAttrString(image, "size");
pwidth = PySequence_ITEM(pimage_size, 0);
pheight = PySequence_ITEM(pimage_size, 1);
width = PyInt_AsSsize_t(pwidth);
height = PyInt_AsSsize_t(pheight);
width = PyLong_AsLong(pwidth);
height = PyLong_AsLong(pheight);
Py_DECREF(pimage_size);
Py_DECREF(pwidth);
Py_DECREF(pheight);
@@ -147,8 +147,8 @@ static PyObject* block_getblocks2(PyObject *self, PyObject *args)
left = min(iw*block_width, width-block_width);
right = left + block_width;
pbox = inttuple(4, left, top, right, bottom);
pmethodname = PyString_FromString("crop");
pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox);
pmethodname = PyUnicode_FromString("crop");
pcrop = PyObject_CallMethodObjArgs(image, pmethodname, pbox, NULL);
Py_DECREF(pmethodname);
Py_DECREF(pbox);
if (pcrop == NULL) {
@@ -207,7 +207,7 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
Py_DECREF(item1);
Py_DECREF(item2);
if ((sum > limit*iteration_count) && (iteration_count >= min_iterations)) {
return PyInt_FromSsize_t(limit + 1);
return PyLong_FromLong(limit + 1);
}
}
@@ -215,7 +215,7 @@ static PyObject* block_avgdiff(PyObject *self, PyObject *args)
if (!result && sum) {
result = 1;
}
return PyInt_FromSsize_t(result);
return PyLong_FromLong(result);
}
static PyMethodDef BlockMethods[] = {
@@ -224,16 +224,30 @@ static PyMethodDef BlockMethods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
init_block(void)
static struct PyModuleDef BlockDef = {
PyModuleDef_HEAD_INIT,
"_block",
NULL,
-1,
BlockMethods,
NULL,
NULL,
NULL,
NULL
};
PyObject *
PyInit__block(void)
{
PyObject *m = Py_InitModule("_block", BlockMethods);
PyObject *m = PyModule_Create(&BlockDef);
if (m == NULL) {
return;
return NULL;
}
NoBlocksError = PyErr_NewException("_block.NoBlocksError", NULL, NULL);
PyModule_AddObject(m, "NoBlocksError", NoBlocksError);
DifferentBlockCountError = PyErr_NewException("_block.DifferentBlockCountError", NULL, NULL);
PyModule_AddObject(m, "DifferentBlockCountError", DifferentBlockCountError);
return m;
}

View File

@@ -29,8 +29,8 @@ pystring2cfstring(PyObject *pystring)
Py_INCREF(encoded);
}
s = (UInt8*)PyString_AS_STRING(encoded);
size = PyString_GET_SIZE(encoded);
s = (UInt8*)PyBytes_AS_STRING(encoded);
size = PyUnicode_GET_SIZE(encoded);
result = CFStringCreateWithBytes(NULL, s, size, kCFStringEncodingUTF8, FALSE);
Py_DECREF(encoded);
return result;
@@ -43,7 +43,7 @@ static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)
CFURLRef image_url;
CGImageSourceRef source;
CGImageRef image;
size_t width, height;
long width, height;
PyObject *pwidth, *pheight;
PyObject *result;
@@ -72,11 +72,11 @@ static PyObject* block_osx_get_image_size(PyObject *self, PyObject *args)
CFRelease(source);
}
pwidth = PyInt_FromSsize_t(width);
pwidth = PyLong_FromLong(width);
if (pwidth == NULL) {
return NULL;
}
pheight = PyInt_FromSsize_t(height);
pheight = PyLong_FromLong(height);
if (pheight == NULL) {
return NULL;
}
@@ -150,7 +150,7 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
CGImageSourceRef source;
CGImageRef image;
size_t width, height;
int block_count, block_width, block_height, block_xcount, block_ycount, i;
int block_count, block_width, block_height, i;
width = 0;
height = 0;
@@ -158,6 +158,11 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
return NULL;
}
if (PySequence_Length(path) == 0) {
PyErr_SetString(PyExc_ValueError, "empty path");
return NULL;
}
image_path = pystring2cfstring(path);
if (image_path == NULL) {
return PyErr_NoMemory();
@@ -192,23 +197,24 @@ static PyObject* block_osx_getblocks(PyObject *self, PyObject *args)
block_width = max(width/block_count, 1);
block_height = max(height/block_count, 1);
/* block_count might have changed */
block_xcount = (width / block_width);
block_ycount = (height / block_height);
result = PyList_New(block_xcount * block_ycount);
result = PyList_New(block_count * block_count);
if (result == NULL) {
return NULL;
}
for(i=0; i<block_ycount; i++)
{
int j;
for(j=0; j<block_xcount; j++)
{
PyObject *block = getblock(bitmapData, width, height, j*block_width, i*block_height,
block_width, block_height);
PyList_SET_ITEM(result, i*block_ycount+j, block);
for(i=0; i<block_count; i++) {
int j, top;
top = min(i*block_height, height-block_height);
for(j=0; j<block_count; j++) {
int left;
left = min(j*block_width, width-block_width);
PyObject *block = getblock(bitmapData, width, height, left, top, block_width, block_height);
if (block == NULL) {
Py_DECREF(result);
return NULL;
}
PyList_SET_ITEM(result, i*block_count+j, block);
}
}
@@ -222,8 +228,24 @@ static PyMethodDef BlockOsxMethods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
init_block_osx(void)
static struct PyModuleDef BlockOsxDef = {
PyModuleDef_HEAD_INIT,
"_block_osx",
NULL,
-1,
BlockOsxMethods,
NULL,
NULL,
NULL,
NULL
};
PyObject *
PyInit__block_osx(void)
{
Py_InitModule("_block_osx", BlockOsxMethods);
PyObject *m = PyModule_Create(&BlockOsxDef);
if (m == NULL) {
return NULL;
}
return m;
}

View File

@@ -72,8 +72,24 @@ static PyMethodDef CacheMethods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
PyMODINIT_FUNC
init_cache(void)
static struct PyModuleDef CacheDef = {
PyModuleDef_HEAD_INIT,
"_cache",
NULL,
-1,
CacheMethods,
NULL,
NULL,
NULL,
NULL
};
PyObject *
PyInit__cache(void)
{
(void)Py_InitModule("_cache", CacheMethods);
PyObject *m = PyModule_Create(&CacheDef);
if (m == NULL) {
return NULL;
}
return m;
}

View File

@@ -32,7 +32,7 @@ PyObject* inttuple(int n, ...)
result = PyTuple_New(n);
for (i=0; i<n; i++) {
pnumber = PyInt_FromLong(va_arg(numbers, int));
pnumber = PyLong_FromLong(va_arg(numbers, long));
if (pnumber == NULL) {
Py_DECREF(result);
return NULL;

View File

@@ -1,30 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-04-23
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import sys
from distutils.core import setup
from distutils.extension import Extension
exts = []
exts.append(Extension("_block", ["block.c", "common.c"]))
exts.append(Extension("_cache", ["cache.c", "common.c"]))
if sys.platform == 'darwin':
exts.append(Extension(
"_block_osx", ["block_osx.m", "common.c"],
extra_link_args=[
"-framework", "CoreFoundation",
"-framework", "Foundation",
"-framework", "ApplicationServices",
]))
setup(
ext_modules = exts,
)

View File

@@ -258,8 +258,8 @@ class TCavgdiff(unittest.TestCase):
def test_return_at_least_1_at_the_slightest_difference(self):
ref = (0,0,0)
b1 = (1,0,0)
blocks1 = [ref for i in xrange(250)]
blocks2 = [ref for i in xrange(250)]
blocks1 = [ref for i in range(250)]
blocks2 = [ref for i in range(250)]
blocks2[0] = b1
self.assertEqual(1,my_avgdiff(blocks1,blocks2))

View File

@@ -6,10 +6,8 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from StringIO import StringIO
import os.path as op
import os
import threading
from hsutil.testcase import TestCase

28
core_pe/tests/conftest.py Normal file
View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-07-11
# Copyright 2010 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
# This unit is required to make tests work with py.test. When running
import py
def get_testunit(item):
if hasattr(item, 'obj'):
testunit = py.builtin._getimself(item.obj)
if hasattr(testunit, 'global_setup'):
return testunit
def pytest_runtest_setup(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_setup()
def pytest_runtest_teardown(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_teardown()

View File

@@ -6,13 +6,13 @@
# 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
from hsutil import io
from hsutil.path import Path
from hsutil.cocoa.objcmin import NSWorkspace
from hscommon.cocoa.objcmin import NSWorkspace
from core import fs
from core.app_cocoa import DupeGuru as DupeGuruBase
@@ -24,17 +24,17 @@ def is_bundle(str_path):
sw = NSWorkspace.sharedWorkspace()
uti, error = sw.typeOfFile_error_(str_path, None)
if error is not None:
logging.warning(u'There was an error trying to detect the UTI of %s', str_path)
logging.warning('There was an error trying to detect the UTI of %s', str_path)
return sw.type_conformsToType_(uti, 'com.apple.bundle') or sw.type_conformsToType_(uti, 'com.apple.package')
class Bundle(BundleBase):
@classmethod
def can_handle(cls, path):
return not io.islink(path) and io.isdir(path) and is_bundle(unicode(path))
return not io.islink(path) and io.isdir(path) and is_bundle(str(path))
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev'])
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, fs.File])

View File

@@ -15,18 +15,16 @@ COLUMNS = [
{'attr':'path','display':'Directory'},
{'attr':'size','display':'Size (KB)'},
{'attr':'extension','display':'Kind'},
{'attr':'ctime','display':'Creation'},
{'attr':'mtime','display':'Modification'},
{'attr':'percentage','display':'Match %'},
{'attr':'words','display':'Words Used'},
{'attr':'dupe_count','display':'Dupe Count'},
]
METADATA_TO_READ = ['size', 'ctime', 'mtime']
METADATA_TO_READ = ['size', 'mtime']
def GetDisplayInfo(dupe, group, delta):
size = dupe.size
ctime = dupe.ctime
mtime = dupe.mtime
m = group.get_match_of(dupe)
if m:
@@ -35,7 +33,6 @@ def GetDisplayInfo(dupe, group, delta):
if delta:
r = group.ref
size -= r.size
ctime -= r.ctime
mtime -= r.mtime
else:
percentage = group.percentage
@@ -45,7 +42,6 @@ def GetDisplayInfo(dupe, group, delta):
format_path(dupe.path),
format_size(size, 0, 1, False),
dupe.extension,
format_timestamp(ctime, delta and m),
format_timestamp(mtime, delta and m),
format_perc(percentage),
format_words(dupe.words) if hasattr(dupe, 'words') else '',

View File

@@ -20,12 +20,11 @@ class Bundle(fs.File):
to see them as files.
"""
def _read_info(self, field):
if field in ('size', 'ctime', 'mtime'):
if field in ('size', 'mtime'):
files = fs.get_all_files(self.path)
size = sum((file.size for file in files), 0)
self.size = size
stats = io.stat(self.path)
self.ctime = nonone(stats.st_ctime, 0)
self.mtime = nonone(stats.st_mtime, 0)
elif field in ('md5', 'md5partial'):
# What's sensitive here is that we must make sure that subfiles'
@@ -35,7 +34,7 @@ class Bundle(fs.File):
files = fs.get_all_files(self.path)
files.sort(key=lambda f:f.path)
md5s = [getattr(f, field) for f in files]
return ''.join(md5s)
return b''.join(md5s)
md5 = hashlib.md5(get_dir_md5_concat())
digest = md5.digest()

28
core_se/tests/conftest.py Normal file
View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2010-07-11
# Copyright 2010 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
# This unit is required to make tests work with py.test. When running
import py
def get_testunit(item):
if hasattr(item, 'obj'):
testunit = py.builtin._getimself(item.obj)
if hasattr(testunit, 'global_setup'):
return testunit
def pytest_runtest_setup(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_setup()
def pytest_runtest_teardown(item):
testunit = get_testunit(item)
if testunit is not None:
testunit.global_teardown()

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