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

Compare commits

...

71 Commits

Author SHA1 Message Date
hsoft
b8bb40de62 dgme cocoa: v5.7.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40271
2009-12-18 13:04:30 +00:00
hsoft
67dff7fbf2 dgme qt: v5.7.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40270
2009-12-18 12:58:14 +00:00
hsoft
6e226f32fd dgme help: "packagified" help and updated to 5.7.0.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40269
2009-12-18 12:57:47 +00:00
hsoft
cf819dc0a8 dgme qt: fixed gen script and updated FAQ.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40268
2009-12-18 12:21:33 +00:00
hsoft
4f6af6e4dc dgpe cocoa: ugh... fixed typo
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40266
2009-12-16 16:16:22 +00:00
hsoft
a6d2a9b7b3 dgpe cocoa: Fixed a crash happening when iPhoto was never launched.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40265
2009-12-16 15:51:26 +00:00
hsoft
cf34164191 dg cocoa: Since there are problems with the latest pyobjc + py2app + Snow Leopard, I've got to stick with pyobjc1.4 for a while after all...
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40264
2009-12-16 14:48:37 +00:00
hsoft
9a7bb30df4 dgse cocoa: Since there are problems with the latest pyobjc + py2app + Snow Leopard, I've got to stick with pyobjc1.4 for a while after all...
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40263
2009-12-16 14:48:18 +00:00
hsoft
5dc78809b6 dgpe: oops, wrong release date for 1.8.0.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40262
2009-12-16 10:29:02 +00:00
hsoft
2b53a6e7d6 dgpe cocoa: removed the forgotten "-A" flag in bundle generation script.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40261
2009-12-16 10:10:18 +00:00
hsoft
eb82a35e5b dgpe cocoa v1.8.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40260
2009-12-16 09:54:51 +00:00
hsoft
51b14435e0 dgpe qt v1.8.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40259
2009-12-16 09:54:00 +00:00
hsoft
59de033523 [#79 state:fixed] Wrapped PIL's IOError into a warning logging.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40258
2009-12-15 16:54:47 +00:00
hsoft
c9b0a278ca [#78 state:fixed] Wrapped appscript errors, updated error message and the F.A.Q. to give users a clue of what to do.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40257
2009-12-15 16:23:02 +00:00
hsoft
b487189742 [#76 state:fixed] Added combobox painting for the selected item in Directories' State column.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40256
2009-12-15 14:09:13 +00:00
hsoft
d5a60b1580 dgpe cocoa: Made the help file generation process independent from the current work directory. Reverted XCode version of the project to 3.0 so that it can be compiled on Leopard.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40255
2009-12-15 12:52:21 +00:00
hsoft
e2665610e9 dgpe qt: Packagified help.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40254
2009-12-15 11:35:30 +00:00
hsoft
3262ee9938 dgbase: Changed ask_for_reg_code() to askForRegCode() to adapt to change in qtlib.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40253
2009-12-15 11:35:08 +00:00
hsoft
2f153003b3 dgpe help: packagified the help folder.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40252
2009-12-15 11:34:21 +00:00
hsoft
6724e710d8 cocoa: Removed fixed toolbar identifiers so that IB 3.0 can compile MainMenu.xib.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40248
2009-11-03 13:19:46 +00:00
hsoft
9729e05fe8 se: Fixed changelog for v2.9.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40247
2009-11-03 08:57:43 +00:00
hsoft
686c60b83b se qt v2.9.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40246
2009-11-02 16:59:48 +00:00
hsoft
5fe11f3b3a qt: fixed bug in registration mechanism.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40245
2009-11-02 16:55:36 +00:00
hsoft
30d29c6b34 dgbase qt: fixed rename.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40244
2009-11-02 16:43:05 +00:00
hsoft
fbfb16e77a se cocoa: v2.9.0
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40243
2009-11-02 16:31:16 +00:00
hsoft
7aea384f86 dgse qt: fixed the gen script (didn't gen rc file).
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40242
2009-11-02 16:10:40 +00:00
hsoft
78bef5c3c6 dgme cocoa: adjusted to the detail panel toggling change.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40241
2009-11-02 15:27:54 +00:00
hsoft
5c8d90a57c dgpe cocoa: adjusted to the detail panel toggling change.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40240
2009-11-02 15:22:30 +00:00
hsoft
13dc9ff76d Fixed a bug where the details panel would sometimes not be updated. Also, pushed the details toggling mechanism in dgbase.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40239
2009-11-02 15:16:34 +00:00
hsoft
eb3645d493 Fixed a bug where the mark state of a switched dupe would not be refreshed immediately on Make Selected Reference.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40238
2009-11-02 14:40:29 +00:00
hsoft
0a06e52d65 dg cocoa: adjusted to pyobjc upgrade (functions with multiple return values now require None placeholders to be placed at the places of the output arguments) and removed a couple of forgotten "dupe.parent".
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40237
2009-10-30 16:24:34 +00:00
hsoft
e004c0c2d4 [#10 state:port] Changed the Remove Selected from Results's shortcut to Cmd-Del.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40236
2009-10-30 15:31:18 +00:00
hsoft
6d5f6a0c3c dgpe cocoa: extracted the pref pane from MainMenu.xib into Preferences.xib, Removed code duplicated with dgbase.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40235
2009-10-30 15:26:09 +00:00
hsoft
024e3c380f dgse cocoa: Adjusted to the dgme change (applicationDidFinishLaunching: pushed down and column removal in xib).
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40234
2009-10-30 14:45:38 +00:00
hsoft
06859fe9cd dgme cocoa: extracted the pref pane from MainMenu.xib into Preferences.xib, Removed code duplicated with dgbase. Pushed down AppDelegate.applicationDidFinishLaunching: to dgbase.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40233
2009-10-30 14:40:17 +00:00
hsoft
6392d08584 dgse cocoa: Changed the MainMenu reference to the dgbase one.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40232
2009-10-30 13:27:55 +00:00
hsoft
80a5290bc8 Moved dgse's MainMenu.xib to dgbase
--HG--
rename : se/cocoa/xib/MainMenu.xib => base/cocoa/xib/MainMenu.xib
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40231
2009-10-30 13:26:39 +00:00
hsoft
6b30c88fba dgse cocoa: extracted the pref pane from MainMenu.xib into Preferences.xib, Removed column menu items, which are now generated in the code. other little things.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40230
2009-10-30 12:56:05 +00:00
hsoft
6adce9bf03 dgbase qt: Now uses qtlib's about_box and reg.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40229
2009-10-30 11:41:14 +00:00
hsoft
3c90ad81a7 [#13 state:fixed] Remember window size/pos.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40228
2009-10-30 11:31:40 +00:00
hsoft
484529add0 [#13] Remember window size/pos.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40227
2009-10-30 11:30:32 +00:00
hsoft
600c7906a4 [#13 state:port] On cocoa, the columns were already remembered, so only the window size/pos had to be remembered.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40226
2009-10-30 11:19:32 +00:00
hsoft
f070e90347 [#72 state:fixed] When files are deleted during the scan, don't include them in the grouping phase.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40225
2009-10-30 11:09:04 +00:00
hsoft
88127d8b8d [#9] Implemented directories drag&drop on the QT side.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40224
2009-10-29 12:02:49 +00:00
hsoft
607ab86188 [#9] mapped the delete key to the remove button.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40223
2009-10-28 16:13:50 +00:00
hsoft
c936f9ccc6 [#9 state:port] implemented drag&drop for directories on the cocoa side.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40222
2009-10-28 15:48:19 +00:00
hsoft
d4d8917956 dgpe cocoa: dropped tiger support. Added toolbar creation in the MainMenu nib.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40221
2009-10-25 11:46:26 +00:00
hsoft
89bce95c27 dgse cocoa: dropped tiger support. Added toolbar creation in the MainMenu nib.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40220
2009-10-25 11:17:13 +00:00
hsoft
f0a38a2b3f dgse cocoa: dropped tiger support. Moved Details.nib and Directories.nib to dgbase. Added toolbar creation in the MainMenu nib.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40219
2009-10-25 10:42:00 +00:00
hsoft
911521d8e0 dgpe qt: build related fixes.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40217
2009-10-24 16:30:37 +00:00
hsoft
b25c1c3a3b Added dgpe 1.7.8 to the changelog.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40215
2009-10-24 14:18:36 +00:00
hsoft
37a40040b3 [#73 state:port] Fixed a bug causing some matches to be ignored in the new pe match algo.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40212
2009-10-24 13:54:57 +00:00
hsoft
25dadc83eb sgpe cocoa: adjusted to hsfs removal.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40210
2009-10-24 12:21:39 +00:00
hsoft
b8c11b5aae dgpe cocoa: removed hsfs from externals.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40209
2009-10-24 12:21:09 +00:00
hsoft
a3ab314378 dgpe qt: adjusted to the hsfs move.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40208
2009-10-23 15:04:37 +00:00
hsoft
794192835d dgme cocoa: added dupeguru_me external and removed the hsfs one.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40207
2009-10-23 14:46:00 +00:00
hsoft
385768a69b dgme qt: adjusted code to the hsfs move.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40206
2009-10-23 14:35:51 +00:00
hsoft
a281931b16 dgme qt: added the dupeguru_me external and removed the hsfs one.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40205
2009-10-23 14:34:59 +00:00
hsoft
085311d559 Added the folder me/py
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40204
2009-10-23 14:05:06 +00:00
hsoft
4d7f032889 dgse cocoa: fixed quirks created by the hsfs move.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40203
2009-10-23 13:46:18 +00:00
hsoft
cf44c93013 dgse cocoa: added the dupeguru_se external and removed the hsfs one.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40202
2009-10-23 13:45:15 +00:00
hsoft
787cbcd01f dgse qt: removed hsfs external
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40201
2009-10-23 12:59:29 +00:00
hsoft
b2b316b642 dgse qt: removed all hsfs usages.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40200
2009-10-23 12:56:52 +00:00
hsoft
49165125e4 dg se: Moved se-specific code from dupeguru to dupeguru_se.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40199
2009-10-23 08:19:48 +00:00
hsoft
54ac0fd19e dg qt: oops, *now* I added the external ref.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40198
2009-10-23 08:19:02 +00:00
hsoft
0aff7f16e5 dg qt: Added the dupeguru_se external.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40197
2009-10-23 08:17:35 +00:00
hsoft
f9abc3b35d Added a dupeguru_se sub-package.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40196
2009-10-23 08:02:43 +00:00
hsoft
b167a51243 Added dupeguru.fs, which is a simpler fork of hsfs and aims to replace it in the dupeguru project.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40195
2009-10-22 15:23:32 +00:00
hsoft
371cdda911 dgpe cocoa: adjusted to MatchFactory removal.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40194
2009-10-18 09:29:33 +00:00
hsoft
11977c6533 dgpe: adjusted to the MatchFactory removal.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40193
2009-10-18 09:26:04 +00:00
hsoft
7228adf433 Changed the MatchFactory into a simple getmatch method, and added a separate getmatches_by_contents() method for contents scan, which results in faster and less memory hungry scans.
--HG--
extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%40192
2009-10-18 08:46:00 +00:00
134 changed files with 16761 additions and 3820 deletions

View File

@@ -9,17 +9,22 @@ http://www.hardcoded.net/licenses/hs_license
#import <Cocoa/Cocoa.h>
#import "RecentDirectories.h"
#import "PyDupeGuru.h"
#import "ResultWindow.h"
#import "DetailsPanel.h"
@interface AppDelegateBase : NSObject
{
IBOutlet PyDupeGuruBase *py;
IBOutlet RecentDirectories *recentDirectories;
IBOutlet NSMenuItem *unlockMenuItem;
IBOutlet ResultWindowBase *result;
NSString *_appName;
DetailsPanelBase *_detailsPanel;
}
- (IBAction)unlockApp:(id)sender;
- (PyDupeGuruBase *)py;
- (RecentDirectories *)recentDirectories;
- (DetailsPanelBase *)detailsPanel; // Virtual
@end

View File

@@ -27,7 +27,7 @@ http://www.hardcoded.net/licenses/hs_license
RegistrationInterface *ri = [[RegistrationInterface alloc] initWithApp:[self py] name:_appName limitDescription:LIMIT_DESC];
if ([ri enterCode] == NSOKButton)
{
NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@",_appName];
NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@!",_appName];
[unlockMenuItem setTitle:menuTitle];
}
[ri release];
@@ -35,4 +35,25 @@ http://www.hardcoded.net/licenses/hs_license
- (PyDupeGuruBase *)py { return py; }
- (RecentDirectories *)recentDirectories { return recentDirectories; }
- (DetailsPanelBase *)detailsPanel { return nil; } // Virtual
/* Delegate */
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[ProgressController mainProgressController] setWorker:py];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
//Restore Columns
NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"];
NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"];
if ([columnsOrder count])
[result restoreColumnsPosition:columnsOrder widths:columnsWidth];
else
[result resetColumnsToDefault:nil];
//Reg stuff
if ([RegistrationInterface showNagWithApp:[self py] name:_appName limitDescription:LIMIT_DESC])
[unlockMenuItem setTitle:[NSString stringWithFormat:@"Thanks for buying %@!",_appName]];
//Restore results
[py loadIgnoreList];
[py loadResults];
}
@end

View File

@@ -18,6 +18,7 @@ http://www.hardcoded.net/licenses/hs_license
- (id)initWithPy:(PyApp *)aPy;
- (void)refresh;
- (void)toggleVisibility;
/* Notifications */
- (void)duplicateSelectionChanged:(NSNotification *)aNotification;

View File

@@ -12,7 +12,7 @@ http://www.hardcoded.net/licenses/hs_license
@implementation DetailsPanelBase
- (id)initWithPy:(PyApp *)aPy
{
self = [super initWithWindowNibName:@"Details"];
self = [super initWithWindowNibName:@"DetailsPanel"];
[self window]; //So the detailsTable is initialized.
[detailsTable setPy:aPy];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil];
@@ -24,6 +24,17 @@ http://www.hardcoded.net/licenses/hs_license
[detailsTable reloadData];
}
- (void)toggleVisibility
{
if ([[self window] isVisible])
[[self window] close];
else
{
[self refresh]; // selection might have changed since last time
[[self window] orderFront:nil];
}
}
/* Notifications */
- (void)duplicateSelectionChanged:(NSNotification *)aNotification
{

View File

@@ -11,10 +11,19 @@ http://www.hardcoded.net/licenses/hs_license
#import "Outline.h"
#import "PyDupeGuru.h"
@interface DirectoryOutline : OutlineView
{
}
@end
@protocol DirectoryOutlineDelegate
- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory;
@end
@interface DirectoryPanelBase : NSWindowController
{
IBOutlet NSPopUpButton *addButtonPopUp;
IBOutlet OutlineView *directories;
IBOutlet DirectoryOutline *directories;
IBOutlet NSButton *removeButton;
PyDupeGuruBase *_py;

View File

@@ -11,10 +11,52 @@ 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 DirectoryPanelBase
- (id)initWithParentApp:(id)aParentApp
{
self = [super initWithWindowNibName:@"Directories"];
self = [super initWithWindowNibName:@"DirectoryPanel"];
[self window];
AppDelegateBase *app = aParentApp;
_py = [app py];
@@ -104,10 +146,7 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)toggleVisible:(id)sender
{
if ([[self window] isVisible])
[[self window] close];
else
[[self window] makeKeyAndOrderFront:nil];
[[self window] makeKeyAndOrderFront:nil];
}
/* Public */
@@ -154,6 +193,11 @@ http://www.hardcoded.net/licenses/hs_license
/* 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;

View File

@@ -8,7 +8,6 @@ http://www.hardcoded.net/licenses/hs_license
#import <Cocoa/Cocoa.h>
#import "Outline.h"
#import "DirectoryPanel.h"
#import "PyDupeGuru.h"
@interface MatchesView : OutlineView
@@ -20,29 +19,29 @@ http://www.hardcoded.net/licenses/hs_license
@protected
IBOutlet PyDupeGuruBase *py;
IBOutlet id app;
IBOutlet NSView *actionMenuView;
IBOutlet NSSegmentedControl *deltaSwitch;
IBOutlet NSView *deltaSwitchView;
IBOutlet NSView *filterFieldView;
IBOutlet MatchesView *matches;
IBOutlet NSSegmentedControl *pmSwitch;
IBOutlet NSView *pmSwitchView;
IBOutlet NSTextField *stats;
IBOutlet NSMenu *columnsMenu;
BOOL _powerMode;
BOOL _displayDelta;
NSMutableArray *_resultColumns;
NSWindowController *preferencesPanel;
}
/* Override */
- (NSString *)logoImageName;
/* Helpers */
- (void)fillColumnsMenu;
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn;
- (NSArray *)getColumnsOrder;
- (NSDictionary *)getColumnsWidth;
- (NSArray *)getSelected:(BOOL)aDupesOnly;
- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly;
- (void)initResultColumns;
- (void)updatePySelection;
- (void)performPySelection:(NSArray *)aIndexPaths;
- (void)refreshStats;
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth;
/* Actions */
- (IBAction)changeDelta:(id)sender;
@@ -52,7 +51,11 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)expandAll:(id)sender;
- (IBAction)exportToXHTML:(id)sender;
- (IBAction)moveMarked:(id)sender;
- (IBAction)resetColumnsToDefault:(id)sender;
- (IBAction)showPreferencesPanel:(id)sender;
- (IBAction)switchSelected:(id)sender;
- (IBAction)toggleColumn:(id)sender;
- (IBAction)toggleDetailsPanel:(id)sender;
- (IBAction)togglePowerMarker:(id)sender;
/* Notifications */

View File

@@ -14,15 +14,6 @@ http://www.hardcoded.net/licenses/hs_license
#import "AppDelegate.h"
#import "Consts.h"
#define tbbDirectories @"tbbDirectories"
#define tbbDetails @"tbbDetail"
#define tbbPreferences @"tbbPreferences"
#define tbbPowerMarker @"tbbPowerMarker"
#define tbbScan @"tbbScan"
#define tbbAction @"tbbAction"
#define tbbDelta @"tbbDelta"
#define tbbFilter @"tbbFilter"
@implementation MatchesView
- (void)keyDown:(NSEvent *)theEvent
{
@@ -60,6 +51,9 @@ http://www.hardcoded.net/licenses/hs_license
- (void)awakeFromNib
{
[self window];
preferencesPanel = [[NSWindowController alloc] initWithWindowNibName:@"Preferences"];
[self initResultColumns];
[self fillColumnsMenu];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registrationRequired:) name:RegistrationRequired object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil];
@@ -68,13 +62,42 @@ http://www.hardcoded.net/licenses/hs_license
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsUpdated:) name:ResultsUpdatedNotification object:nil];
}
/* Virtual */
- (NSString *)logoImageName
- (void)dealloc
{
return @"dg_logo32";
[preferencesPanel release];
[super dealloc];
}
/* Helpers */
- (void)fillColumnsMenu
{
// The columns menu is supposed to be empty and initResultColumns must have been called
for (NSTableColumn *col in _resultColumns)
{
NSMenuItem *mi = [columnsMenu addItemWithTitle:[[col headerCell] stringValue] action:@selector(toggleColumn:) keyEquivalent:@""];
[mi setTag:[[col identifier] integerValue]];
[mi setTarget:self];
if ([[matches tableColumns] containsObject:col])
[mi setState:NSOnState];
}
[columnsMenu addItem:[NSMenuItem separatorItem]];
NSMenuItem *mi = [columnsMenu addItemWithTitle:@"Reset to Default" action:@selector(resetColumnsToDefault:) keyEquivalent:@""];
[mi setTarget:self];
}
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn
{
NSNumber *n = [NSNumber numberWithInt:aIdentifier];
NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]];
[col setWidth:aWidth];
[col setEditable:NO];
[[col dataCell] setFont:[[aColumn dataCell] font]];
[[col headerCell] setStringValue:aTitle];
[col setResizingMask:NSTableColumnUserResizingMask];
[col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]];
return col;
}
//Returns an array of identifiers, in order.
- (NSArray *)getColumnsOrder
{
@@ -135,6 +158,40 @@ http://www.hardcoded.net/licenses/hs_license
return r;
}
- (void)initResultColumns
{
// Virtual
}
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth
{
NSTableColumn *col;
NSString *colId;
NSNumber *width;
NSMenuItem *mi;
//Remove all columns
NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator];
while (mi = [e nextObject])
{
if ([mi state] == NSOnState)
[self toggleColumn:mi];
}
//Add columns and set widths
e = [aColumnsOrder objectEnumerator];
while (colId = [e nextObject])
{
if (![colId isEqual:@"mark"])
{
col = [_resultColumns objectAtIndex:[colId intValue]];
width = [aColumnsWidth objectForKey:[col identifier]];
mi = [columnsMenu itemWithTag:[colId intValue]];
if (width)
[col setWidth:[width floatValue]];
[self toggleColumn:mi];
}
}
}
- (void)updatePySelection
{
NSArray *selection;
@@ -242,6 +299,16 @@ http://www.hardcoded.net/licenses/hs_license
}
}
- (IBAction)resetColumnsToDefault:(id)sender
{
// Virtual
}
- (IBAction)showPreferencesPanel:(id)sender
{
[preferencesPanel showWindow:sender];
}
- (IBAction)switchSelected:(id)sender
{
// It might look like a complicated way to get the length of the current dupe list on the py side
@@ -259,6 +326,31 @@ http://www.hardcoded.net/licenses/hs_license
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
}
- (IBAction)toggleColumn:(id)sender
{
NSMenuItem *mi = sender;
NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]];
NSTableColumn *col = [matches tableColumnWithIdentifier:colId];
if (col == nil)
{
//Add Column
col = [_resultColumns objectAtIndex:[mi tag]];
[matches addTableColumn:col];
[mi setState:NSOnState];
}
else
{
//Remove column
[matches removeTableColumn:col];
[mi setState:NSOffState];
}
}
- (IBAction)toggleDetailsPanel:(id)sender
{
[[(AppDelegateBase *)app detailsPanel] toggleVisibility];
}
- (IBAction)togglePowerMarker:(id)sender
{
if ([pmSwitch selectedSegment] == 1)
@@ -311,7 +403,11 @@ http://www.hardcoded.net/licenses/hs_license
if ([lastAction isEqualTo:jobDelete])
{
if (r > 0)
[Dialogs showMessage:[NSString stringWithFormat:@"%d file(s) couldn't be sent to Trash. They were kept in the results, and still are marked.",r]];
{
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
[Dialogs showMessage:@"All marked files were sucessfully sent to Trash."];
}
@@ -353,107 +449,7 @@ http://www.hardcoded.net/licenses/hs_license
- (void)resultsUpdated:(NSNotification *)aNotification
{
[matches invalidateBuffers];
}
/* Toolbar */
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
{
NSToolbarItem *tbi = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease];
if ([itemIdentifier isEqualTo:tbbDirectories])
{
[tbi setLabel: @"Directories"];
[tbi setToolTip: @"Show/Hide the directories panel."];
[tbi setImage: [NSImage imageNamed: @"folder32"]];
[tbi setTarget: app];
[tbi setAction: @selector(toggleDirectories:)];
}
else if ([itemIdentifier isEqualTo:tbbDetails])
{
[tbi setLabel: @"Details"];
[tbi setToolTip: @"Show/Hide the details panel."];
[tbi setImage: [NSImage imageNamed: @"details32"]];
[tbi setTarget: self];
[tbi setAction: @selector(toggleDetailsPanel:)];
}
else if ([itemIdentifier isEqualTo:tbbPreferences])
{
[tbi setLabel: @"Preferences"];
[tbi setToolTip: @"Show the preferences panel."];
[tbi setImage: [NSImage imageNamed: @"preferences32"]];
[tbi setTarget: self];
[tbi setAction: @selector(showPreferencesPanel:)];
}
else if ([itemIdentifier isEqualTo:tbbPowerMarker])
{
[tbi setLabel: @"Power Marker"];
[tbi setToolTip: @"When enabled, only the duplicates are shown, not the references."];
[tbi setView:pmSwitchView];
[tbi setMinSize:[pmSwitchView frame].size];
[tbi setMaxSize:[pmSwitchView frame].size];
}
else if ([itemIdentifier isEqualTo:tbbScan])
{
[tbi setLabel: @"Start Scanning"];
[tbi setToolTip: @"Start scanning for duplicates in the selected directories."];
[tbi setImage: [NSImage imageNamed:[self logoImageName]]];
[tbi setTarget: self];
[tbi setAction: @selector(startDuplicateScan:)];
}
else if ([itemIdentifier isEqualTo:tbbAction])
{
[tbi setLabel: @"Action"];
[tbi setView:actionMenuView];
[tbi setMinSize:[actionMenuView frame].size];
[tbi setMaxSize:[actionMenuView frame].size];
}
else if ([itemIdentifier isEqualTo:tbbDelta])
{
[tbi setLabel: @"Delta Values"];
[tbi setToolTip: @"When enabled, this option makes dupeGuru display, where applicable, delta values instead of absolute values."];
[tbi setView:deltaSwitchView];
[tbi setMinSize:[deltaSwitchView frame].size];
[tbi setMaxSize:[deltaSwitchView frame].size];
}
else if ([itemIdentifier isEqualTo:tbbFilter])
{
[tbi setLabel: @"Filter"];
[tbi setToolTip: @"Filters the results using regular expression."];
[tbi setView:filterFieldView];
[tbi setMinSize:[filterFieldView frame].size];
[tbi setMaxSize:NSMakeSize(1000, [filterFieldView frame].size.height)];
}
[tbi setPaletteLabel: [tbi label]];
return tbi;
}
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
{
return [NSArray arrayWithObjects:
tbbDirectories,
tbbDetails,
tbbPreferences,
tbbPowerMarker,
tbbScan,
tbbAction,
tbbDelta,
tbbFilter,
NSToolbarSeparatorItemIdentifier,
NSToolbarSpaceItemIdentifier,
NSToolbarFlexibleSpaceItemIdentifier,
nil];
}
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
{
return [NSArray arrayWithObjects:
tbbScan,
tbbAction,
tbbDirectories,
tbbDetails,
tbbPowerMarker,
tbbDelta,
tbbFilter,
nil];
[matches invalidateMarkings];
}
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

4618
base/cocoa/xib/MainMenu.xib Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,13 @@ import os
import os.path as op
import logging
from hsutil import job, io, files
from hsutil import io, files
from hsutil.path import Path
from hsutil.reg import RegistrableApplication, RegistrationRequired
from hsutil.misc import flatten, first
from hsutil.str import escape
from . import directories, results, scanner, export
from . import directories, results, scanner, export, fs
JOB_SCAN = 'job_scan'
JOB_LOAD = 'job_load'
@@ -70,12 +70,10 @@ class DupeGuru(RegistrableApplication):
def _do_delete_dupe(self, dupe):
if not io.exists(dupe.path):
dupe.parent = None
return True
self._recycle_dupe(dupe)
self.clean_empty_dirs(dupe.path[:-1])
if not io.exists(dupe.path):
dupe.parent = None
return True
logging.warning("Could not send {0} to trash.".format(unicode(dupe.path)))
return False
@@ -98,13 +96,8 @@ class DupeGuru(RegistrableApplication):
return ['---'] * len(self.data.COLUMNS)
def _get_file(self, str_path):
p = Path(str_path)
for d in self.directories:
if p not in d.path:
continue
result = d.find_path(p[d.path:])
if result is not None:
return result
path = Path(str_path)
return fs.get_file(path, self.directories.fileclasses)
@staticmethod
def _recycle_dupe(dupe):
@@ -150,7 +143,7 @@ class DupeGuru(RegistrableApplication):
2 = absolute re-creation.
"""
source_path = dupe.path
location_path = dupe.root.path
location_path = first(p for p in self.directories if dupe.path in p)
dest_path = Path(destination)
if dest_type == 2:
dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename

View File

@@ -7,18 +7,18 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import objc
from Foundation import *
from AppKit import *
import logging
import os.path as op
import hsfs as fs
from hsutil import io, cocoa, job
from hsutil.cocoa import install_exception_hook
from hsutil.misc import stripnone
from hsutil.reg import RegistrationRequired
import app, data
from . import app, fs
JOBID2TITLE = {
app.JOB_SCAN: "Scanning for duplicates",
@@ -43,8 +43,6 @@ class DupeGuru(app.DupeGuru):
logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s')
logging.debug('started in debug mode')
install_exception_hook()
if data_module is None:
data_module = data
appsupport = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0]
appdata = op.join(appsupport, appdata_subdir)
app.DupeGuru.__init__(self, data_module, appdata, appid)
@@ -56,18 +54,14 @@ class DupeGuru(app.DupeGuru):
#--- Override
@staticmethod
def _recycle_dupe(dupe):
if not io.exists(dupe.path):
dupe.parent = None
return True
directory = unicode(dupe.parent.path)
directory = unicode(dupe.path[:-1])
filename = dupe.name
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
NSWorkspaceRecycleOperation, directory, '', [filename])
if not io.exists(dupe.path):
dupe.parent = None
return True
logging.warning('Could not send %s to trash. tag: %d' % (unicode(dupe.path), tag))
return False
if objc.__version__ == '1.4': # For a while, we have to support this.
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
NSWorkspaceRecycleOperation, directory, '', [filename])
else:
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
NSWorkspaceRecycleOperation, directory, '', [filename], None)
def _start_job(self, jobid, func):
try:
@@ -91,15 +85,15 @@ class DupeGuru(app.DupeGuru):
except IndexError:
return (None,None)
def GetDirectory(self,node_path,curr_dir=None):
def get_folder_path(self, node_path, curr_path=None):
if not node_path:
return curr_dir
if curr_dir is not None:
l = curr_dir.dirs
return curr_path
current_index = node_path[0]
if curr_path is None:
curr_path = self.directories[current_index]
else:
l = self.directories
d = l[node_path[0]]
return self.GetDirectory(node_path[1:],d)
curr_path = self.directories.get_subfolders(curr_path)[current_index]
return self.get_folder_path(node_path[1:], curr_path)
def RefreshDetailsTable(self,dupe,group):
l1 = self._get_display_info(dupe, group, False)
@@ -146,13 +140,13 @@ class DupeGuru(app.DupeGuru):
def RemoveSelected(self):
self.results.remove_duplicates(self.selected_dupes)
def RenameSelected(self,newname):
def RenameSelected(self, newname):
try:
d = self.selected_dupes[0]
d = d.move(d.parent,newname)
d.rename(newname)
return True
except (IndexError,fs.FSError),e:
logging.warning("dupeGuru Warning: %s" % str(e))
except (IndexError, fs.FSError) as e:
logging.warning("dupeGuru Warning: %s" % unicode(e))
return False
def RevealSelected(self):
@@ -214,9 +208,9 @@ class DupeGuru(app.DupeGuru):
self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes))
]
def SetDirectoryState(self,node_path,state):
d = self.GetDirectory(node_path)
self.directories.set_state(d.path,state)
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)
@@ -245,8 +239,12 @@ class DupeGuru(app.DupeGuru):
return [len(g.dupes) for g in self.results.groups]
elif tag == 1: #Directories
try:
dirs = self.GetDirectory(node_path).dirs if node_path else self.directories
return [d.dircount for d in dirs]
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
@@ -270,8 +268,9 @@ class DupeGuru(app.DupeGuru):
return result
elif tag == 1: #Directories
try:
d = self.GetDirectory(node_path)
return [d.name, self.directories.get_state(d.path)]
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 []

View File

@@ -1,70 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-24
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import logging
from AppKit import *
from hsfs.phys import Directory as DirectoryBase
from hsfs.phys.bundle import Bundle
from hsutil.path import Path
from hsutil.misc import extract
from hsutil.str import get_file_ext
from . import app_cocoa, data
from .directories import Directories as DirectoriesBase, STATE_EXCLUDED
if NSWorkspace.sharedWorkspace().respondsToSelector_('typeOfFile:error:'): # Only from 10.5
def is_bundle(str_path):
sw = NSWorkspace.sharedWorkspace()
uti, error = sw.typeOfFile_error_(str_path)
if error is not None:
logging.warning(u'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')
else: # Tiger
def is_bundle(str_path): # just return a list of a few known bundle extensions.
return get_file_ext(str_path) in ('app', 'pages', 'numbers')
class DGDirectory(DirectoryBase):
def _create_sub_file(self, name, with_parent=True):
if is_bundle(unicode(self.path + name)):
parent = self if with_parent else None
return Bundle(parent, name)
else:
return super(DGDirectory, self)._create_sub_file(name, with_parent)
def _fetch_subitems(self):
subdirs, subfiles = super(DGDirectory, self)._fetch_subitems()
apps, normal_dirs = extract(lambda name: is_bundle(unicode(self.path + name)), subdirs)
subfiles += apps
return normal_dirs, subfiles
class Directories(DirectoriesBase):
ROOT_PATH_TO_EXCLUDE = map(Path, ['/Library', '/Volumes', '/System', '/bin', '/sbin', '/opt', '/private', '/dev'])
HOME_PATH_TO_EXCLUDE = [Path('Library')]
def __init__(self):
DirectoriesBase.__init__(self)
self.dirclass = DGDirectory
def _default_state_for_path(self, path):
result = DirectoriesBase._default_state_for_path(self, path)
if result is not None:
return result
if path in self.ROOT_PATH_TO_EXCLUDE:
return STATE_EXCLUDED
if path[:2] == Path('/Users') and path[3:] in self.HOME_PATH_TO_EXCLUDE:
return STATE_EXCLUDED
class DupeGuru(app_cocoa.DupeGuru):
def __init__(self):
app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru', appid=4)
self.directories = Directories()

View File

@@ -40,63 +40,3 @@ def format_dupe_count(c):
def cmp_value(value):
return value.lower() if isinstance(value, basestring) else value
COLUMNS = [
{'attr':'name','display':'Filename'},
{'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']
def GetDisplayInfo(dupe, group, delta):
size = dupe.size
ctime = dupe.ctime
mtime = dupe.mtime
m = group.get_match_of(dupe)
if m:
percentage = m.percentage
dupe_count = 0
if delta:
r = group.ref
size -= r.size
ctime -= r.ctime
mtime -= r.mtime
else:
percentage = group.percentage
dupe_count = len(group.dupes)
return [
dupe.name,
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),
format_dupe_count(dupe_count)
]
def GetDupeSortKey(dupe, get_group, key, delta):
if key == 6:
m = get_group().get_match_of(dupe)
return m.percentage
if key == 8:
return 0
r = cmp_value(getattr(dupe, COLUMNS[key]['attr']))
if delta and (key in (2, 4, 5)):
r -= cmp_value(getattr(get_group().ref, COLUMNS[key]['attr']))
return r
def GetGroupSortKey(group, key):
if key == 6:
return group.percentage
if key == 8:
return len(group)
return cmp_value(getattr(group.ref, COLUMNS[key]['attr']))

View File

@@ -9,11 +9,12 @@
import xml.dom.minidom
from hsfs import phys
import hsfs as fs
from hsutil import io
from hsutil.files import FileOrPath
from hsutil.path import Path
from . import fs
(STATE_NORMAL,
STATE_REFERENCE,
STATE_EXCLUDED) = range(3)
@@ -26,15 +27,14 @@ class InvalidPathError(Exception):
class Directories(object):
#---Override
def __init__(self):
def __init__(self, fileclasses=[fs.File]):
self._dirs = []
self.states = {}
self.dirclass = phys.Directory
self.special_dirclasses = {}
self.fileclasses = fileclasses
def __contains__(self,path):
for d in self._dirs:
if path in d.path:
def __contains__(self, path):
for p in self._dirs:
if path in p:
return True
return False
@@ -53,8 +53,7 @@ class Directories(object):
if path[-1].startswith('.'): # hidden
return STATE_EXCLUDED
def _get_files(self, from_dir):
from_path = from_dir.path
def _get_files(self, from_path):
state = self.get_state(from_path)
if state == STATE_EXCLUDED:
# Recursively get files from folders with lots of subfolder is expensive. However, there
@@ -62,14 +61,21 @@ class Directories(object):
# through self.states and see if we must continue, or we can stop right here to save time
if not any(p[:len(from_path)] == from_path for p in self.states):
return
result = []
for subdir in from_dir.dirs:
for file in self._get_files(subdir):
yield file
if state != STATE_EXCLUDED:
for file in from_dir.files:
file.is_ref = state == STATE_REFERENCE
yield file
try:
filepaths = set()
if state != STATE_EXCLUDED:
for file in fs.get_files(from_path, fileclasses=self.fileclasses):
file.is_ref = state == STATE_REFERENCE
filepaths.add(file.path)
yield file
subpaths = [from_path + name for name in io.listdir(from_path)]
# it's possible that a folder (bundle) gets into the file list. in that case, we don't want to recurse into it
subfolders = [p for p in subpaths if not io.islink(p) and io.isdir(p) and p not in filepaths]
for subfolder in subfolders:
for file in self._get_files(subfolder):
yield file
except (EnvironmentError, fs.InvalidPath):
pass
#---Public
def add_path(self, path):
@@ -80,29 +86,30 @@ class Directories(object):
under it will be removed. Can also raise InvalidPathError if 'path' does not exist.
"""
if path in self:
raise AlreadyThereError
self._dirs = [d for d in self._dirs if d.path not in path]
try:
dirclass = self.special_dirclasses.get(path, self.dirclass)
d = dirclass(None, unicode(path))
d[:] #If an InvalidPath exception has to be raised, it will be raised here
self._dirs.append(d)
return d
except fs.InvalidPath:
raise AlreadyThereError()
if not io.exists(path):
raise InvalidPathError()
self._dirs = [p for p in self._dirs if p not in path]
self._dirs.append(path)
@staticmethod
def get_subfolders(path):
"""returns a sorted list of paths corresponding to subfolders in `path`"""
try:
names = [name for name in io.listdir(path) if io.isdir(path + name)]
names.sort(key=lambda x:x.lower())
return [path + name for name in names]
except EnvironmentError:
return []
def get_files(self):
"""Returns a list of all files that are not excluded.
Returned files also have their 'is_ref' attr set.
"""
for d in self._dirs:
d.force_update()
try:
for file in self._get_files(d):
yield file
except fs.InvalidPath:
pass
for path in self._dirs:
for file in self._get_files(path):
yield file
def get_state(self, path):
"""Returns the state of 'path' (One of the STATE_* const.)
@@ -123,8 +130,8 @@ class Directories(object):
doc = xml.dom.minidom.parse(infile)
except:
return
root_dir_nodes = doc.getElementsByTagName('root_directory')
for rdn in root_dir_nodes:
root_path_nodes = doc.getElementsByTagName('root_directory')
for rdn in root_path_nodes:
if not rdn.getAttributeNode('path'):
continue
path = rdn.getAttributeNode('path').nodeValue
@@ -144,9 +151,9 @@ class Directories(object):
with FileOrPath(outfile, 'wb') as fp:
doc = xml.dom.minidom.Document()
root = doc.appendChild(doc.createElement('directories'))
for root_dir in self:
root_dir_node = root.appendChild(doc.createElement('root_directory'))
root_dir_node.setAttribute('path', unicode(root_dir.path).encode('utf-8'))
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'))

View File

@@ -9,6 +9,7 @@
from __future__ import division
import difflib
import itertools
import logging
import string
from collections import defaultdict, namedtuple
@@ -156,58 +157,71 @@ def get_match(first, second, flags=()):
percentage = compare(first.words, second.words, flags)
return Match(first, second, percentage)
class MatchFactory(object):
common_word_threshold = 50
match_similar_words = False
min_match_percentage = 0
weight_words = False
no_field_order = False
limit = 5000000
def getmatches(self, objects, j=job.nulljob):
j = j.start_subjob(2)
sj = j.start_subjob(2)
for o in objects:
if not hasattr(o, 'words'):
o.words = getwords(o.name)
word_dict = build_word_dict(objects, sj)
reduce_common_words(word_dict, self.common_word_threshold)
if self.match_similar_words:
merge_similar_words(word_dict)
match_flags = []
if self.weight_words:
match_flags.append(WEIGHT_WORDS)
if self.match_similar_words:
match_flags.append(MATCH_SIMILAR_WORDS)
if self.no_field_order:
match_flags.append(NO_FIELD_ORDER)
j.start_job(len(word_dict), '0 matches found')
compared = defaultdict(set)
result = []
try:
# This whole 'popping' thing is there to avoid taking too much memory at the same time.
while word_dict:
items = word_dict.popitem()[1]
while items:
ref = items.pop()
compared_already = compared[ref]
to_compare = items - compared_already
compared_already |= to_compare
for other in to_compare:
m = get_match(ref, other, match_flags)
if m.percentage >= self.min_match_percentage:
result.append(m)
if len(result) >= self.limit:
return result
j.add_progress(desc='%d matches found' % len(result))
except MemoryError:
# This is the place where the memory usage is at its peak during the scan.
# Just continue the process with an incomplete list of matches.
del compared # This should give us enough room to call logging.
logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict)))
return result
def getmatches(objects, min_match_percentage=0, match_similar_words=False, weight_words=False,
no_field_order=False, j=job.nulljob):
COMMON_WORD_THRESHOLD = 50
LIMIT = 5000000
j = j.start_subjob(2)
sj = j.start_subjob(2)
for o in objects:
if not hasattr(o, 'words'):
o.words = getwords(o.name)
word_dict = build_word_dict(objects, sj)
reduce_common_words(word_dict, COMMON_WORD_THRESHOLD)
if match_similar_words:
merge_similar_words(word_dict)
match_flags = []
if weight_words:
match_flags.append(WEIGHT_WORDS)
if match_similar_words:
match_flags.append(MATCH_SIMILAR_WORDS)
if no_field_order:
match_flags.append(NO_FIELD_ORDER)
j.start_job(len(word_dict), '0 matches found')
compared = defaultdict(set)
result = []
try:
# This whole 'popping' thing is there to avoid taking too much memory at the same time.
while word_dict:
items = word_dict.popitem()[1]
while items:
ref = items.pop()
compared_already = compared[ref]
to_compare = items - compared_already
compared_already |= to_compare
for other in to_compare:
m = get_match(ref, other, match_flags)
if m.percentage >= min_match_percentage:
result.append(m)
if len(result) >= LIMIT:
return result
j.add_progress(desc='%d matches found' % len(result))
except MemoryError:
# This is the place where the memory usage is at its peak during the scan.
# Just continue the process with an incomplete list of matches.
del compared # This should give us enough room to call logging.
logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict)))
return result
return result
def getmatches_by_contents(files, sizeattr='size', partial=False, j=job.nulljob):
j = j.start_subjob([2, 8])
size2files = defaultdict(set)
for file in j.iter_with_progress(files, 'Read size of %d/%d files'):
filesize = getattr(file, sizeattr)
if filesize:
size2files[filesize].add(file)
possible_matches = [files for files in size2files.values() if len(files) > 1]
del size2files
result = []
j.start_job(len(possible_matches), '0 matches found')
for group in possible_matches:
for first, second in itertools.combinations(group, 2):
if first.md5partial == second.md5partial:
if partial or first.md5 == second.md5:
result.append(Match(first, second, 100))
j.add_progress(desc='%d matches found' % len(result))
return result
class Group(object):
#---Override

178
base/py/fs.py Normal file
View File

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

View File

@@ -10,7 +10,7 @@
import logging
from hsutil import job
from hsutil import job, io
from hsutil.misc import dedupe
from hsutil.str import get_file_ext, rem_file_ext
@@ -32,40 +32,32 @@ class Scanner(object):
self.ignore_list = IgnoreList()
self.discarded_file_count = 0
@staticmethod
def _filter_matches_by_content(matches, partial, j):
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
md5attrname = 'md5partial' if partial else 'md5'
md5 = lambda f: getattr(f, md5attrname)
for matched_file in j.iter_with_progress(matched_files, 'Analyzed %d/%d matching files'):
md5(matched_file)
j.set_progress(100, 'Removing false matches')
return [m for m in matches if md5(m.first) == md5(m.second)]
def _getmatches(self, files, j):
j = j.start_subjob(2)
mf = engine.MatchFactory()
if self.scan_type != SCAN_TYPE_CONTENT:
mf.match_similar_words = self.match_similar_words
mf.weight_words = self.word_weighting
mf.min_match_percentage = self.min_match_percentage
if self.scan_type == SCAN_TYPE_FIELDS_NO_ORDER:
self.scan_type = SCAN_TYPE_FIELDS
mf.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],
SCAN_TYPE_CONTENT: lambda f: [str(f.size)],
SCAN_TYPE_CONTENT_AUDIO: lambda f: [str(f.audiosize)]
}[self.scan_type]
for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'):
if self.size_threshold:
f.size # pre-read, makes a smoother progress if read here (especially for bundles)
f.words = func(f)
if self.size_threshold:
j = j.start_subjob([2, 8])
for f in j.iter_with_progress(files, 'Read size of %d/%d files'):
f.size # pre-read, makes a smoother progress if read here (especially for bundles)
files = [f for f in files if f.size >= self.size_threshold]
return mf.getmatches(files, j)
if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO):
sizeattr = 'size' if self.scan_type == SCAN_TYPE_CONTENT else 'audiosize'
return engine.getmatches_by_contents(files, sizeattr, partial=self.scan_type==SCAN_TYPE_CONTENT_AUDIO, j=j)
else:
j = j.start_subjob([2, 8])
kw = {}
kw['match_similar_words'] = self.match_similar_words
kw['weight_words'] = self.word_weighting
kw['min_match_percentage'] = self.min_match_percentage
if self.scan_type == SCAN_TYPE_FIELDS_NO_ORDER:
self.scan_type = SCAN_TYPE_FIELDS
kw['no_field_order'] = True
func = {
SCAN_TYPE_FILENAME: lambda f: engine.getwords(rem_file_ext(f.name)),
SCAN_TYPE_FIELDS: lambda f: engine.getfields(rem_file_ext(f.name)),
SCAN_TYPE_TAG: lambda f: [engine.getwords(unicode(getattr(f, attrname))) for attrname in SCANNABLE_TAGS if attrname in self.scanned_tags],
}[self.scan_type]
for f in j.iter_with_progress(files, 'Read metadata of %d/%d files'):
f.words = func(f)
return engine.getmatches(files, j=j, **kw)
@staticmethod
def _key_func(dupe):
@@ -86,27 +78,17 @@ class Scanner(object):
for f in [f for f in files if not hasattr(f, 'is_ref')]:
f.is_ref = False
logging.info('Getting matches')
if self.match_factory is None:
matches = self._getmatches(files, j)
else:
matches = self.match_factory.getmatches(files, j)
matches = self._getmatches(files, j)
logging.info('Found %d matches' % len(matches))
j.set_progress(100, 'Removing false matches')
if not self.mix_file_kind:
j.set_progress(100, 'Removing false matches')
matches = [m for m in matches if get_file_ext(m.first.name) == get_file_ext(m.second.name)]
matches = [m for m in matches if io.exists(m.first.path) and io.exists(m.second.path)]
if self.ignore_list:
j = j.start_subjob(2)
iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list')
matches = [m for m in iter_matches
if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))]
if self.scan_type in (SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO):
j = j.start_subjob(3 if self.scan_type == SCAN_TYPE_CONTENT else 2)
matches = self._filter_matches_by_content(matches, partial=True, j=j)
if self.scan_type == SCAN_TYPE_CONTENT:
matches = self._filter_matches_by_content(matches, partial=False, j=j)
# We compared md5. No words were involved.
for m in matches:
m.first.words = m.second.words = ['--']
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])
@@ -118,7 +100,6 @@ class Scanner(object):
g.prioritize(self._key_func, self._tie_breaker)
return groups
match_factory = None
match_similar_words = False
min_match_percentage = 80
mix_file_kind = True
@@ -126,9 +107,3 @@ class Scanner(object):
scanned_tags = set(['artist', 'title'])
size_threshold = 0
word_weighting = False
class ScannerME(Scanner): # Scanner for Music Edition
@staticmethod
def _key_func(dupe):
return (not dupe.is_ref, -dupe.bitrate, -dupe.size)

View File

@@ -18,10 +18,10 @@ from hsutil.path import Path
from hsutil.testcase import TestCase
from hsutil.decorators import log_calls
from hsutil import io
import hsfs.phys
from . import data
from .results_test import GetTestGroups
from .. import engine, data
from .. import engine, fs
try:
from ..app_cocoa import DupeGuru as DupeGuruBase
except ImportError:
@@ -35,7 +35,6 @@ class DupeGuru(DupeGuruBase):
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]
@@ -310,15 +309,15 @@ class TCDupeGuru(TestCase):
class TCDupeGuru_renameSelected(TestCase):
def setUp(self):
p = Path(tempfile.mkdtemp())
fp = open(str(p + 'foo bar 1'),mode='w')
p = self.tmppath()
fp = open(unicode(p + 'foo bar 1'),mode='w')
fp.close()
fp = open(str(p + 'foo bar 2'),mode='w')
fp = open(unicode(p + 'foo bar 2'),mode='w')
fp.close()
fp = open(str(p + 'foo bar 3'),mode='w')
fp = open(unicode(p + 'foo bar 3'),mode='w')
fp.close()
refdir = hsfs.phys.Directory(None,str(p))
matches = engine.MatchFactory().getmatches(refdir.files)
files = fs.get_files(p)
matches = engine.getmatches(files)
groups = engine.get_groups(matches)
g = groups[0]
g.prioritize(lambda x:x.name)
@@ -327,45 +326,41 @@ class TCDupeGuru_renameSelected(TestCase):
self.app = app
self.groups = groups
self.p = p
self.refdir = refdir
def tearDown(self):
shutil.rmtree(str(self.p))
self.files = files
def test_simple(self):
app = self.app
refdir = self.refdir
g = self.groups[0]
app.SelectPowerMarkerNodePaths(r2np([0]))
self.assert_(app.RenameSelected('renamed'))
self.assert_('renamed' in refdir)
self.assert_('foo bar 2' not in refdir)
self.assert_(g.dupes[0] is refdir['renamed'])
self.assert_(g.dupes[0] in refdir)
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
refdir = self.refdir
g = self.groups[0]
app.SelectPowerMarkerNodePaths([])
self.mock(logging, 'warning', log_calls(lambda msg: None))
self.assert_(not app.RenameSelected('renamed'))
assert not app.RenameSelected('renamed')
msg = logging.warning.calls[0]['msg']
self.assertEqual('dupeGuru Warning: list index out of range', msg)
self.assert_('renamed' not in refdir)
self.assert_('foo bar 2' in refdir)
self.assert_(g.dupes[0] is refdir['foo bar 2'])
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
refdir = self.refdir
g = self.groups[0]
app.SelectPowerMarkerNodePaths(r2np([0]))
self.mock(logging, 'warning', log_calls(lambda msg: None))
self.assert_(not app.RenameSelected('foo bar 1'))
assert not app.RenameSelected('foo bar 1')
msg = logging.warning.calls[0]['msg']
self.assert_(msg.startswith('dupeGuru Warning: \'foo bar 2\' already exists in'))
self.assert_('foo bar 1' in refdir)
self.assert_('foo bar 2' in refdir)
self.assert_(g.dupes[0] is refdir['foo bar 2'])
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

@@ -13,12 +13,11 @@ from hsutil.testcase import TestCase
from hsutil import io
from hsutil.path import Path
from hsutil.decorators import log_calls
import hsfs as fs
import hsfs.phys
import hsutil.files
from hsutil.job import nulljob
from .. import data, app
from . import data
from .. import app, fs
from ..app import DupeGuru as DupeGuruBase
class DupeGuru(DupeGuruBase):
@@ -59,27 +58,27 @@ class TCDupeGuru(TestCase):
# The goal here is just to have a test for a previous blowup I had. I know my test coverage
# for this unit is pathetic. What's done is done. My approach now is to add tests for
# every change I want to make. The blowup was caused by a missing import.
dupe_parent = fs.Directory(None, 'foo')
dupe = fs.File(dupe_parent, 'bar')
dupe.copy = log_calls(lambda dest, newname: None)
p = self.tmppath()
io.open(p + 'foo', 'w').close()
self.mock(hsutil.files, 'copy', log_calls(lambda source_path, dest_path: None))
self.mock(os, 'makedirs', lambda path: None) # We don't want the test to create that fake directory
self.mock(fs.phys, 'Directory', fs.Directory) # We don't want an error because makedirs didn't work
app = DupeGuru()
app.copy_or_move(dupe, True, 'some_destination', 0)
app.directories.add_path(p)
[f] = app.directories.get_files()
app.copy_or_move(f, True, 'some_destination', 0)
self.assertEqual(1, len(hsutil.files.copy.calls))
call = hsutil.files.copy.calls[0]
self.assertEqual('some_destination', call['dest_path'])
self.assertEqual(dupe.path, call['source_path'])
self.assertEqual(f.path, call['source_path'])
def test_copy_or_move_clean_empty_dirs(self):
tmppath = Path(self.tmpdir())
sourcepath = tmppath + 'source'
io.mkdir(sourcepath)
io.open(sourcepath + 'myfile', 'w')
tmpdir = hsfs.phys.Directory(None, unicode(tmppath))
myfile = tmpdir['source']['myfile']
app = DupeGuru()
app.directories.add_path(tmppath)
[myfile] = app.directories.get_files()
self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None))
app.copy_or_move(myfile, False, tmppath + 'dest', 0)
calls = app.clean_empty_dirs.calls
@@ -87,9 +86,14 @@ class TCDupeGuru(TestCase):
self.assertEqual(sourcepath, calls[0]['path'])
def test_Scan_with_objects_evaluating_to_false(self):
class FakeFile(fs.File):
def __nonzero__(self):
return False
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
app = DupeGuru()
f1, f2 = [fs.File(None, 'foo') for i in range(2)]
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]

45
base/py/tests/data.py Normal file
View File

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

View File

@@ -10,20 +10,43 @@
import os.path as op
import os
import time
import shutil
from nose.tools import eq_
from hsutil import job, io
from hsutil import io
from hsutil.path import Path
from hsutil.testcase import TestCase
import hsfs.phys
from hsfs.tests import phys_test
from ..directories import *
testpath = Path(TestCase.datadirpath())
def create_fake_fs(rootpath):
rootpath = rootpath + 'fs'
io.mkdir(rootpath)
io.mkdir(rootpath + 'dir1')
io.mkdir(rootpath + 'dir2')
io.mkdir(rootpath + 'dir3')
fp = io.open(rootpath + 'file1.test', 'w')
fp.write('1')
fp.close()
fp = io.open(rootpath + 'file2.test', 'w')
fp.write('12')
fp.close()
fp = io.open(rootpath + 'file3.test', 'w')
fp.write('123')
fp.close()
fp = io.open(rootpath + ('dir1', 'file1.test'), 'w')
fp.write('1')
fp.close()
fp = io.open(rootpath + ('dir2', 'file2.test'), 'w')
fp.write('12')
fp.close()
fp = io.open(rootpath + ('dir3', 'file3.test'), 'w')
fp.write('123')
fp.close()
return rootpath
class TCDirectories(TestCase):
def test_empty(self):
d = Directories()
@@ -33,13 +56,11 @@ class TCDirectories(TestCase):
def test_add_path(self):
d = Directories()
p = testpath + 'utils'
added = d.add_path(p)
d.add_path(p)
self.assertEqual(1,len(d))
self.assert_(p in d)
self.assert_((p + 'foobar') in d)
self.assert_(p[:-1] not in d)
self.assertEqual(p,added.path)
self.assert_(d[0] is added)
p = self.tmppath()
d.add_path(p)
self.assertEqual(2,len(d))
@@ -53,13 +74,13 @@ class TCDirectories(TestCase):
self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar')
self.assertEqual(1, len(d))
def test_AddPath_containing_paths_already_there(self):
def test_add_path_containing_paths_already_there(self):
d = Directories()
d.add_path(testpath + 'utils')
self.assertEqual(1, len(d))
added = d.add_path(testpath)
self.assertEqual(1, len(d))
self.assert_(added is d[0])
d.add_path(testpath)
eq_(len(d), 1)
eq_(d[0], testpath)
def test_AddPath_non_latin(self):
p = Path(self.tmpdir())
@@ -114,7 +135,7 @@ class TCDirectories(TestCase):
def test_set_state_keep_state_dict_size_to_minimum(self):
d = Directories()
p = Path(phys_test.create_fake_fs(self.tmpdir()))
p = create_fake_fs(self.tmppath())
d.add_path(p)
d.set_state(p,STATE_REFERENCE)
d.set_state(p + 'dir1',STATE_REFERENCE)
@@ -129,17 +150,17 @@ class TCDirectories(TestCase):
def test_get_files(self):
d = Directories()
p = Path(phys_test.create_fake_fs(self.tmpdir()))
p = create_fake_fs(self.tmppath())
d.add_path(p)
d.set_state(p + 'dir1',STATE_REFERENCE)
d.set_state(p + 'dir2',STATE_EXCLUDED)
files = d.get_files()
self.assertEqual(5, len(list(files)))
files = list(d.get_files())
self.assertEqual(5, len(files))
for f in files:
if f.parent.path == p + 'dir1':
self.assert_(f.is_ref)
if f.path[:-1] == p + 'dir1':
assert f.is_ref
else:
self.assert_(not f.is_ref)
assert not f.is_ref
def test_get_files_with_inherited_exclusion(self):
d = Directories()
@@ -177,52 +198,28 @@ class TCDirectories(TestCase):
except LookupError:
self.fail()
def test_default_dirclass(self):
self.assert_(Directories().dirclass is hsfs.phys.Directory)
def test_dirclass(self):
class MySpecialDirclass(hsfs.phys.Directory): pass
d = Directories()
d.dirclass = MySpecialDirclass
d.add_path(testpath)
self.assert_(isinstance(d[0], MySpecialDirclass))
def test_load_from_file_with_invalid_path(self):
#This test simulates a load from file resulting in a
#InvalidPath raise. Other directories must be loaded.
d1 = Directories()
d1.add_path(testpath + 'utils')
#Will raise InvalidPath upon loading
d1.add_path(self.tmppath()).name = 'does_not_exist'
p = self.tmppath()
d1.add_path(p)
io.rmdir(p)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
d1.save_to_file(tmpxml)
d2 = Directories()
d2.load_from_file(tmpxml)
self.assertEqual(1, len(d2))
def test_load_from_file_with_same_paths(self):
#This test simulates a load from file resulting in a
#AlreadyExists raise. Other directories must be loaded.
d1 = Directories()
p1 = self.tmppath()
p2 = self.tmppath()
d1.add_path(p1)
d1.add_path(p2)
#Will raise AlreadyExists upon loading
d1.add_path(self.tmppath()).name = unicode(p1)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
d1.save_to_file(tmpxml)
d2 = Directories()
d2.load_from_file(tmpxml)
self.assertEqual(2, len(d2))
def test_unicode_save(self):
d = Directories()
p1 = self.tmppath() + u'hello\xe9'
io.mkdir(p1)
io.mkdir(p1 + u'foo\xe9')
d.add_path(p1)
d.set_state(d[0][0].path, STATE_EXCLUDED)
d.set_state(p1 + u'foo\xe9', STATE_EXCLUDED)
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
try:
d.save_to_file(tmpxml)
@@ -231,7 +228,7 @@ class TCDirectories(TestCase):
def test_get_files_refreshes_its_directories(self):
d = Directories()
p = Path(phys_test.create_fake_fs(self.tmpdir()))
p = create_fake_fs(self.tmppath())
d.add_path(p)
files = d.get_files()
self.assertEqual(6, len(list(files)))
@@ -258,16 +255,6 @@ class TCDirectories(TestCase):
d.set_state(hidden_dir_path, STATE_NORMAL)
self.assertEqual(d.get_state(hidden_dir_path), STATE_NORMAL)
def test_special_dirclasses(self):
# if a path is in special_dirclasses, use this class instead
class MySpecialDirclass(hsfs.phys.Directory): pass
d = Directories()
p1 = self.tmppath()
p2 = self.tmppath()
d.special_dirclasses[p1] = MySpecialDirclass
self.assert_(isinstance(d.add_path(p2), hsfs.phys.Directory))
self.assert_(isinstance(d.add_path(p1), MySpecialDirclass))
def test_default_path_state_override(self):
# It's possible for a subclass to override the default state of a path
class MyDirectories(Directories):

View File

@@ -15,16 +15,21 @@ from hsutil import job
from hsutil.decorators import log_calls
from hsutil.testcase import TestCase
from .. import engine
from .. import engine, fs
from ..engine import *
class NamedObject(object):
def __init__(self, name="foobar", with_words=False):
def __init__(self, name="foobar", with_words=False, size=1):
self.name = name
self.size = size
self.md5partial = name
self.md5 = name
if with_words:
self.words = getwords(name)
no = NamedObject
def get_match_triangle():
o1 = NamedObject(with_words=True)
o2 = NamedObject(with_words=True)
@@ -340,21 +345,13 @@ class TCget_match(TestCase):
self.assertEqual(int((6.0 / 13.0) * 100),get_match(NamedObject("foo bar",True),NamedObject("bar bleh",True),(WEIGHT_WORDS,)).percentage)
class TCMatchFactory(TestCase):
class GetMatches(TestCase):
def test_empty(self):
self.assertEqual([],MatchFactory().getmatches([]))
def test_defaults(self):
mf = MatchFactory()
self.assertEqual(50,mf.common_word_threshold)
self.assertEqual(False,mf.weight_words)
self.assertEqual(False,mf.match_similar_words)
self.assertEqual(False,mf.no_field_order)
self.assertEqual(0,mf.min_match_percentage)
eq_(getmatches([]), [])
def test_simple(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
r = MatchFactory().getmatches(l)
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]
@@ -367,7 +364,7 @@ class TCMatchFactory(TestCase):
def test_null_and_unrelated_objects(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")]
r = MatchFactory().getmatches(l)
r = getmatches(l)
self.assertEqual(1,len(r))
m = r[0]
self.assertEqual(50,m.percentage)
@@ -376,34 +373,33 @@ class TCMatchFactory(TestCase):
def test_twice_the_same_word(self):
l = [NamedObject("foo foo bar"),NamedObject("bar bleh")]
r = MatchFactory().getmatches(l)
r = getmatches(l)
self.assertEqual(1,len(r))
def test_twice_the_same_word_when_preworded(self):
l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)]
r = MatchFactory().getmatches(l)
r = getmatches(l)
self.assertEqual(1,len(r))
def test_two_words_match(self):
l = [NamedObject("foo bar"),NamedObject("foo bar bleh")]
r = MatchFactory().getmatches(l)
r = getmatches(l)
self.assertEqual(1,len(r))
def test_match_files_with_only_common_words(self):
#If a word occurs more than 50 times, it is excluded from the matching process
#The problem with the common_word_threshold is that the files containing only common
#words will never be matched together. We *should* match them.
mf = MatchFactory()
mf.common_word_threshold = 50
# This test assumes that the common word threashold const is 50
l = [NamedObject("foo") for i in range(50)]
r = mf.getmatches(l)
r = getmatches(l)
self.assertEqual(1225,len(r))
def test_use_words_already_there_if_there(self):
o1 = NamedObject('foo')
o2 = NamedObject('bar')
o2.words = ['foo']
self.assertEqual(1,len(MatchFactory().getmatches([o1,o2])))
eq_(1, len(getmatches([o1,o2])))
def test_job(self):
def do_progress(p,d=''):
@@ -413,75 +409,62 @@ class TCMatchFactory(TestCase):
j = job.Job(1,do_progress)
self.log = []
s = "foo bar"
MatchFactory().getmatches([NamedObject(s),NamedObject(s),NamedObject(s)],j)
getmatches([NamedObject(s), NamedObject(s), NamedObject(s)], j=j)
self.assert_(len(self.log) > 2)
self.assertEqual(0,self.log[0])
self.assertEqual(100,self.log[-1])
def test_weight_words(self):
mf = MatchFactory()
mf.weight_words = True
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
m = mf.getmatches(l)[0]
m = getmatches(l, weight_words=True)[0]
self.assertEqual(int((6.0 / 13.0) * 100),m.percentage)
def test_similar_word(self):
mf = MatchFactory()
mf.match_similar_words = True
l = [NamedObject("foobar"),NamedObject("foobars")]
self.assertEqual(1,len(mf.getmatches(l)))
self.assertEqual(100,mf.getmatches(l)[0].percentage)
eq_(len(getmatches(l, match_similar_words=True)), 1)
eq_(getmatches(l, match_similar_words=True)[0].percentage, 100)
l = [NamedObject("foobar"),NamedObject("foo")]
self.assertEqual(0,len(mf.getmatches(l))) #too far
eq_(len(getmatches(l, match_similar_words=True)), 0) #too far
l = [NamedObject("bizkit"),NamedObject("bizket")]
self.assertEqual(1,len(mf.getmatches(l)))
eq_(len(getmatches(l, match_similar_words=True)), 1)
l = [NamedObject("foobar"),NamedObject("foosbar")]
self.assertEqual(1,len(mf.getmatches(l)))
eq_(len(getmatches(l, match_similar_words=True)), 1)
def test_single_object_with_similar_words(self):
mf = MatchFactory()
mf.match_similar_words = True
l = [NamedObject("foo foos")]
self.assertEqual(0,len(mf.getmatches(l)))
eq_(len(getmatches(l, match_similar_words=True)), 0)
def test_double_words_get_counted_only_once(self):
mf = MatchFactory()
l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")]
m = mf.getmatches(l)[0]
m = getmatches(l)[0]
self.assertEqual(75,m.percentage)
def test_with_fields(self):
mf = MatchFactory()
o1 = NamedObject("foo bar - foo bleh")
o2 = NamedObject("foo bar - bleh bar")
o1.words = getfields(o1.name)
o2.words = getfields(o2.name)
m = mf.getmatches([o1, o2])[0]
m = getmatches([o1, o2])[0]
self.assertEqual(50, m.percentage)
def test_with_fields_no_order(self):
mf = MatchFactory()
mf.no_field_order = True
o1 = NamedObject("foo bar - foo bleh")
o2 = NamedObject("bleh bang - foo bar")
o1.words = getfields(o1.name)
o2.words = getfields(o2.name)
m = mf.getmatches([o1, o2])[0]
self.assertEqual(50 ,m.percentage)
m = getmatches([o1, o2], no_field_order=True)[0]
eq_(m.percentage, 50)
def test_only_match_similar_when_the_option_is_set(self):
mf = MatchFactory()
mf.match_similar_words = False
l = [NamedObject("foobar"),NamedObject("foobars")]
self.assertEqual(0,len(mf.getmatches(l)))
eq_(len(getmatches(l, match_similar_words=False)), 0)
def test_dont_recurse_do_match(self):
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
sys.setrecursionlimit(100)
mf = MatchFactory()
files = [NamedObject('foo bar') for i in range(101)]
try:
mf.getmatches(files)
getmatches(files)
except RuntimeError:
self.fail()
finally:
@@ -489,18 +472,9 @@ class TCMatchFactory(TestCase):
def test_min_match_percentage(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
mf = MatchFactory()
mf.min_match_percentage = 50
r = mf.getmatches(l)
r = getmatches(l, min_match_percentage=50)
self.assertEqual(1,len(r)) #Only "foo bar" / "bar bleh" should match
def test_limit(self):
l = [NamedObject(),NamedObject(),NamedObject()]
mf = MatchFactory()
mf.limit = 2
r = mf.getmatches(l)
self.assertEqual(2,len(r))
def test_MemoryError(self):
@log_calls
def mocked_match(first, second, flags):
@@ -510,14 +484,19 @@ class TCMatchFactory(TestCase):
objects = [NamedObject() for i in range(10)] # results in 45 matches
self.mock(engine, 'get_match', mocked_match)
mf = MatchFactory()
try:
r = mf.getmatches(objects)
r = getmatches(objects)
except MemoryError:
self.fail('MemorryError must be handled')
self.assertEqual(42, len(r))
class GetMatchesByContents(TestCase):
def test_dont_compare_empty_files(self):
o1, o2 = no(size=0), no(size=0)
assert not getmatches_by_contents([o1, o2])
class TCGroup(TestCase):
def test_empy(self):
g = Group()
@@ -738,7 +717,7 @@ class TCget_groups(TestCase):
def test_simple(self):
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
matches = MatchFactory().getmatches(l)
matches = getmatches(l)
m = matches[0]
r = get_groups(matches)
self.assertEqual(1,len(r))
@@ -749,7 +728,7 @@ class TCget_groups(TestCase):
def test_group_with_multiple_matches(self):
#This results in 3 matches
l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")]
matches = MatchFactory().getmatches(l)
matches = getmatches(l)
r = get_groups(matches)
self.assertEqual(1,len(r))
g = r[0]
@@ -759,7 +738,7 @@ class TCget_groups(TestCase):
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("b c"),NamedObject("c d"),NamedObject("c d")]
#There will be 2 groups here: group "a b" and group "c d"
#"b c" can go either of them, but not both.
matches = MatchFactory().getmatches(l)
matches = getmatches(l)
r = get_groups(matches)
self.assertEqual(2,len(r))
self.assertEqual(5,len(r[0])+len(r[1]))
@@ -768,7 +747,7 @@ class TCget_groups(TestCase):
l = [NamedObject("a b"),NamedObject("a b"),NamedObject("a b"),NamedObject("a b")]
#There will be 2 groups here: group "a b" and group "c d"
#"b c" can fit in both, but it must be in only one of them
matches = MatchFactory().getmatches(l)
matches = getmatches(l)
r = get_groups(matches)
self.assertEqual(1,len(r))
@@ -788,7 +767,7 @@ class TCget_groups(TestCase):
def test_four_sized_group(self):
l = [NamedObject("foobar") for i in xrange(4)]
m = MatchFactory().getmatches(l)
m = getmatches(l)
r = get_groups(m)
self.assertEqual(1,len(r))
self.assertEqual(4,len(r[0]))

View File

@@ -16,12 +16,11 @@ from hsutil.path import Path
from hsutil.testcase import TestCase
from hsutil.misc import first
from . import engine_test
from .. import data, engine
from . import engine_test, data
from .. import engine
from ..results import *
class NamedObject(engine_test.NamedObject):
size = 1
path = property(lambda x:Path('basepath') + x.name)
is_ref = False
@@ -37,7 +36,7 @@ class NamedObject(engine_test.NamedObject):
def GetTestGroups():
objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")]
objects[1].size = 1024
matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches
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
@@ -505,7 +504,7 @@ class TCResultsXML(TestCase):
return objects[1]
objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)]
matches = engine.MatchFactory().getmatches(objects) #we should have 5 matches
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

View File

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

View File

@@ -1,37 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-09
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import Qt, QCoreApplication, SIGNAL
from PyQt4.QtGui import QDialog, QDialogButtonBox, QPixmap
from about_box_ui import Ui_AboutBox
class AboutBox(QDialog, Ui_AboutBox):
def __init__(self, parent, app):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint | Qt.MSWindowsFixedSizeDialogHint
QDialog.__init__(self, parent, flags)
self.app = app
self._setupUi()
self.connect(self.buttonBox, SIGNAL('clicked(QAbstractButton*)'), self.buttonClicked)
def _setupUi(self):
self.setupUi(self)
# Stuff that can't be done in the Designer
self.setWindowTitle(u"About %s" % QCoreApplication.instance().applicationName())
self.nameLabel.setText(QCoreApplication.instance().applicationName())
self.versionLabel.setText('Version ' + QCoreApplication.instance().applicationVersion())
self.logoLabel.setPixmap(QPixmap(':/%s_big' % self.app.LOGO_NAME))
self.registerButton = self.buttonBox.addButton("Register", QDialogButtonBox.ActionRole)
#--- Events
def buttonClicked(self, button):
if button is self.registerButton:
self.app.ask_for_reg_code()

View File

@@ -1,133 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AboutBox</class>
<widget class="QDialog" name="AboutBox">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>190</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>About dupeGuru</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="logoLabel">
<property name="text">
<string/>
</property>
<property name="pixmap">
<pixmap resource="dg.qrc">:/logo_me_big</pixmap>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="nameLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>dupeGuru</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="versionLabel">
<property name="text">
<string>Version</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Copyright Hardcoded Software 2009</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Registered To:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="registeredEmailLabel">
<property name="text">
<string>UNREGISTERED</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="dg.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>AboutBox</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>AboutBox</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -16,21 +16,21 @@ import os.path as op
from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL
from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox
import hsfs as fs
from hsutil import job
from hsutil.reg import RegistrationRequired
from dupeguru import fs
from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY,
JOB_DELETE)
from qtlib.about_box import AboutBox
from qtlib.progress import Progress
from qtlib.reg import Registration
from . import platform
from .main_window import MainWindow
from .directories_dialog import DirectoriesDialog
from .about_box import AboutBox
from .reg import Registration
JOBID2TITLE = {
JOB_SCAN: "Scanning for duplicates",
@@ -54,6 +54,7 @@ class DupeGuru(DupeGuruBase, QObject):
LOGO_NAME = '<replace this>'
NAME = '<replace this>'
DELTA_COLUMNS = frozenset()
DEMO_LIMIT_DESC = "In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied."
def __init__(self, data_module, appid):
appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
@@ -143,9 +144,8 @@ class DupeGuru(DupeGuruBase, QObject):
DupeGuruBase.apply_filter(self, filter)
self.emit(SIGNAL('resultsChanged()'))
def ask_for_reg_code(self):
if self.reg.ask_for_code():
self._setup_ui_as_registered()
def askForRegCode(self):
self.reg.ask_for_code()
@demo_method
def copy_or_move_marked(self, copy):
@@ -197,7 +197,7 @@ class DupeGuru(DupeGuruBase, QObject):
def rename_dupe(self, dupe, newname):
try:
dupe.move(dupe.parent, newname)
dupe.rename(newname)
return True
except (IndexError, fs.FSError) as e:
logging.warning("dupeGuru Warning: %s" % unicode(e))

View File

@@ -8,7 +8,6 @@
<file alias="logo_se">images/dgse_logo_32.png</file>
<file alias="logo_se_big">images/dgse_logo_128.png</file>
<file alias="folder">images/folderwin32.png</file>
<file alias="gear">images/gear.png</file>
<file alias="preferences">images/preferences32.png</file>
<file alias="actions">images/actions32.png</file>
<file alias="delta">images/delta32.png</file>

View File

@@ -16,9 +16,18 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="treeView">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="editTriggers">
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
</property>
<property name="dragDropOverwriteMode">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DropOnly</enum>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
@@ -59,6 +68,9 @@
<property name="text">
<string>Remove</string>
</property>
<property name="shortcut">
<string>Del</string>
</property>
</widget>
</item>
<item>

View File

@@ -7,8 +7,11 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint
from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush
import urllib
from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint, QUrl
from PyQt4.QtGui import (QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush, QStyle,
QStyleOptionComboBox, QStyleOptionViewItemV4)
from qtlib.tree_model import TreeNode, TreeModel
@@ -21,6 +24,24 @@ class DirectoriesDelegate(QStyledItemDelegate):
editor.addItems(STATES)
return editor
def paint(self, painter, option, index):
self.initStyleOption(option, index)
# No idea why, but this cast is required if we want to have access to the V4 valuess
option = QStyleOptionViewItemV4(option)
if (index.column() == 1) and (option.state & QStyle.State_Selected):
cboption = QStyleOptionComboBox()
cboption.rect = option.rect
# On OS X (with Qt4.6.0), adding State_Enabled to the flags causes the whole drawing to
# fail (draw nothing), but it's an OS X only glitch. On Windows, it works alright.
cboption.state |= QStyle.State_Enabled
QApplication.style().drawComplexControl(QStyle.CC_ComboBox, cboption, painter)
painter.setBrush(option.palette.text())
rect = QRect(option.rect)
rect.setLeft(rect.left()+4)
painter.drawText(rect, Qt.AlignLeft, option.text)
else:
QStyledItemDelegate.paint(self, painter, option, index)
def setEditorData(self, editor, index):
value = index.model().data(index, Qt.EditRole)
editor.setCurrentIndex(value);
@@ -47,19 +68,27 @@ class DirectoryNode(TreeNode):
return DirectoryNode(self.model, self, ref, row)
def _getChildren(self):
return self.ref.dirs
return self.model.dirs.get_subfolders(self.ref)
@property
def name(self):
if self.parent is not None:
return self.ref[-1]
else:
return unicode(self.ref)
class DirectoriesModel(TreeModel):
def __init__(self, app):
self._dirs = app.directories
self.app = app
self.dirs = app.directories
TreeModel.__init__(self)
def _createNode(self, ref, row):
return DirectoryNode(self, None, ref, row)
def _getChildren(self):
return self._dirs
return self.dirs
def columnCount(self, parent):
return 2
@@ -70,23 +99,37 @@ class DirectoriesModel(TreeModel):
node = index.internalPointer()
if role == Qt.DisplayRole:
if index.column() == 0:
return node.ref.name
return node.name
else:
return STATES[self._dirs.get_state(node.ref.path)]
return STATES[self.dirs.get_state(node.ref)]
elif role == Qt.EditRole and index.column() == 1:
return self._dirs.get_state(node.ref.path)
return self.dirs.get_state(node.ref)
elif role == Qt.ForegroundRole:
state = self._dirs.get_state(node.ref.path)
state = self.dirs.get_state(node.ref)
if state == 1:
return QBrush(Qt.blue)
elif state == 2:
return QBrush(Qt.red)
return None
def dropMimeData(self, mimeData, action, row, column, parentIndex):
# the data in mimeData is urlencoded **in utf-8**!!! which means that urllib.unquote has
# to be called on the utf-8 encoded string, and *only then*, decoded to unicode.
if not mimeData.hasFormat('text/uri-list'):
return False
data = str(mimeData.data('text/uri-list'))
unquoted = urllib.unquote(data)
urls = unicode(unquoted, 'utf-8').split('\r\n')
paths = [unicode(QUrl(url).toLocalFile()) for url in urls if url]
for path in paths:
self.app.add_directory(path)
self.reset()
return True
def flags(self, index):
if not index.isValid():
return 0
result = Qt.ItemIsEnabled | Qt.ItemIsSelectable
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
result = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDropEnabled
if index.column() == 1:
result |= Qt.ItemIsEditable
return result
@@ -97,10 +140,18 @@ class DirectoriesModel(TreeModel):
return HEADERS[section]
return None
def mimeTypes(self):
return ['text/uri-list']
def setData(self, index, value, role):
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
return False
node = index.internalPointer()
self._dirs.set_state(node.ref.path, value)
self.dirs.set_state(node.ref, value)
return True
def supportedDropActions(self):
# Normally, the correct action should be ActionLink, but the drop doesn't work. It doesn't
# work with ActionMove either. So screw that, and accept anything.
return Qt.ActionMask

View File

@@ -1,36 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-09
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hashlib import md5
from PyQt4.QtGui import QDialog
from reg_submit_dialog import RegSubmitDialog
from reg_demo_dialog import RegDemoDialog
class Registration(object):
def __init__(self, app):
self.app = app
def ask_for_code(self):
dialog = RegSubmitDialog(self.app.main_window, self.app.is_code_valid)
result = dialog.exec_()
code = unicode(dialog.codeEdit.text())
email = unicode(dialog.emailEdit.text())
dialog.setParent(None) # free it
if result == QDialog.Accepted and self.app.is_code_valid(code, email):
self.app.set_registration(code, email)
return True
return False
def show_nag(self):
dialog = RegDemoDialog(self.app.main_window, self)
dialog.exec_()
dialog.setParent(None) # free it

View File

@@ -1,47 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-10
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication
from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices
from reg_demo_dialog_ui import Ui_RegDemoDialog
class RegDemoDialog(QDialog, Ui_RegDemoDialog):
def __init__(self, parent, reg):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
QDialog.__init__(self, parent, flags)
self.reg = reg
self._setupUi()
self.connect(self.enterCodeButton, SIGNAL('clicked()'), self.enterCodeClicked)
self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked)
def _setupUi(self):
self.setupUi(self)
# Stuff that can't be setup in the Designer
appname = QCoreApplication.instance().applicationName()
title = self.windowTitle()
title = title.replace('$appname', appname)
self.setWindowTitle(title)
title = self.titleLabel.text()
title = title.replace('$appname', appname)
self.titleLabel.setText(title)
desc = self.demoDescLabel.text()
desc = desc.replace('$appname', appname)
self.demoDescLabel.setText(desc)
#--- Events
def enterCodeClicked(self):
if self.reg.ask_for_code():
self.accept()
def purchaseClicked(self):
url = QUrl('http://www.hardcoded.net/purchase.htm')
QDesktopServices.openUrl(url)

View File

@@ -1,140 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RegDemoDialog</class>
<widget class="QDialog" name="RegDemoDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>387</width>
<height>161</height>
</rect>
</property>
<property name="windowTitle">
<string>$appname Demo Version</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="titleLabel">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>$appname Demo Version</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="demoDescLabel">
<property name="text">
<string>You are currently running a demo version of $appname. This version has limited functionalities, and you need to buy it to have access to these functionalities.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>In the demo version, only 10 duplicates per session can be sent to the recycle bin, moved or copied.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="tryButton">
<property name="minimumSize">
<size>
<width>110</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Try Demo</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="enterCodeButton">
<property name="minimumSize">
<size>
<width>110</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Enter Code</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="purchaseButton">
<property name="minimumSize">
<size>
<width>110</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Purchase</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>tryButton</sender>
<signal>clicked()</signal>
<receiver>RegDemoDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>112</x>
<y>161</y>
</hint>
<hint type="destinationlabel">
<x>201</x>
<y>94</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,49 +0,0 @@
# Created By: Virgil Dupras
# Created On: 2009-05-09
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from PyQt4.QtCore import SIGNAL, Qt, QUrl, QCoreApplication
from PyQt4.QtGui import QDialog, QMessageBox, QDesktopServices
from reg_submit_dialog_ui import Ui_RegSubmitDialog
class RegSubmitDialog(QDialog, Ui_RegSubmitDialog):
def __init__(self, parent, is_valid_func):
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
QDialog.__init__(self, parent, flags)
self._setupUi()
self.is_valid_func = is_valid_func
self.connect(self.submitButton, SIGNAL('clicked()'), self.submitClicked)
self.connect(self.purchaseButton, SIGNAL('clicked()'), self.purchaseClicked)
def _setupUi(self):
self.setupUi(self)
# Stuff that can't be setup in the Designer
appname = QCoreApplication.instance().applicationName()
prompt = self.promptLabel.text()
prompt = prompt.replace('$appname', appname)
self.promptLabel.setText(prompt)
#--- Events
def purchaseClicked(self):
url = QUrl('http://www.hardcoded.net/purchase.htm')
QDesktopServices.openUrl(url)
def submitClicked(self):
code = unicode(self.codeEdit.text())
email = unicode(self.emailEdit.text())
title = "Registration"
if self.is_valid_func(code, email):
msg = "This code is valid. Thanks!"
QMessageBox.information(self, title, msg)
self.accept()
else:
msg = "This code is invalid"
QMessageBox.warning(self, title, msg)

View File

@@ -1,149 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>RegSubmitDialog</class>
<widget class="QDialog" name="RegSubmitDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>365</width>
<height>134</height>
</rect>
</property>
<property name="windowTitle">
<string>Enter your registration code</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="promptLabel">
<property name="text">
<string>Please enter your $appname registration code and registered e-mail (the e-mail you used for the purchase), then press &quot;Submit&quot;.</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<property name="sizeConstraint">
<enum>QLayout::SetNoConstraint</enum>
</property>
<property name="fieldGrowthPolicy">
<enum>QFormLayout::ExpandingFieldsGrow</enum>
</property>
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="formAlignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Registration code:</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Registered e-mail:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="codeEdit"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="emailEdit"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="purchaseButton">
<property name="text">
<string>Purchase</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="submitButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Submit</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>cancelButton</sender>
<signal>clicked()</signal>
<receiver>RegSubmitDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>260</x>
<y>159</y>
</hint>
<hint type="destinationlabel">
<x>198</x>
<y>97</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -153,7 +153,7 @@ class ResultsModel(TreeModel):
if index.column() == 0:
value = unicode(value.toString())
if self._app.rename_dupe(node.dupe, value):
node.reset()
node.invalidate()
return True
return False

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 B

View File

@@ -13,17 +13,11 @@ http://www.hardcoded.net/licenses/hs_license
#import "PyDupeGuru.h"
@interface AppDelegate : AppDelegateBase
{
IBOutlet NSButton *presetsButton;
IBOutlet NSPopUpButton *presetsPopup;
IBOutlet ResultWindow *result;
{
DirectoryPanel *_directoryPanel;
}
- (IBAction)openWebsite:(id)sender;
- (IBAction)popupPresets:(id)sender;
- (IBAction)toggleDirectories:(id)sender;
- (IBAction)usePreset:(id)sender;
- (DirectoryPanel *)directoryPanel;
- (PyDupeGuru *)py;

View File

@@ -12,6 +12,7 @@ http://www.hardcoded.net/licenses/hs_license
#import "cocoalib/Utils.h"
#import "cocoalib/ValueTransformers.h"
#import "cocoalib/Dialogs.h"
#import "DetailsPanel.h"
#import "Consts.h"
@implementation AppDelegate
@@ -60,50 +61,17 @@ http://www.hardcoded.net/licenses/hs_license
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_me"]];
}
- (IBAction)popupPresets:(id)sender
{
[presetsPopup selectItem: nil];
[[presetsPopup cell] performClickWithFrame:[sender frame] inView:[sender superview]];
}
- (IBAction)toggleDirectories:(id)sender
{
[[self directoryPanel] toggleVisible:sender];
}
- (IBAction)usePreset:(id)sender
- (DetailsPanelBase *)detailsPanel
{
NSUserDefaultsController *ud = [NSUserDefaultsController sharedUserDefaultsController];
[ud revertToInitialValues:nil];
NSUserDefaults *d = [ud defaults];
switch ([sender tag])
{
case 0:
{
[d setInteger:5 forKey:@"scanType"];
break;
}
//case 1 is defaults
case 2:
{
[d setInteger:2 forKey:@"scanType"];
break;
}
case 3:
{
[d setInteger:0 forKey:@"scanType"];
[d setInteger:50 forKey:@"minMatchPercentage"];
break;
}
case 4:
{
[d setInteger:0 forKey:@"scanType"];
[d setInteger:50 forKey:@"minMatchPercentage"];
[d setBool:YES forKey:@"matchSimilarWords"];
[d setBool:YES forKey:@"wordWeighting"];
break;
}
}
if (!_detailsPanel)
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
return _detailsPanel;
}
- (DirectoryPanel *)directoryPanel
@@ -115,23 +83,6 @@ http://www.hardcoded.net/licenses/hs_license
- (PyDupeGuru *)py { return (PyDupeGuru *)py; }
//Delegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[ProgressController mainProgressController] setWorker:py];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
//Restore Columns
NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"];
NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"];
if ([columnsOrder count])
[result restoreColumnsPosition:columnsOrder widths:columnsWidth];
//Reg stuff
if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC])
[unlockMenuItem setTitle:@"Thanks for buying dupeGuru ME!"];
//Restore results
[py loadIgnoreList];
[py loadResults];
}
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
{
if (![[result window] isVisible])

View File

@@ -1,18 +0,0 @@
{
IBClasses = (
{
CLASS = DetailsPanel;
LANGUAGE = ObjC;
OUTLETS = {detailsTable = NSTableView; };
SUPERCLASS = NSWindowController;
},
{CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; },
{
CLASS = TableView;
LANGUAGE = ObjC;
OUTLETS = {py = PyApp; };
SUPERCLASS = NSTableView;
}
);
IBVersion = 1;
}

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBDocumentLocation</key>
<string>432 54 356 240 0 0 1024 746 </string>
<key>IBFramework Version</key>
<string>443.0</string>
<key>IBOpenObjects</key>
<array>
<integer>5</integer>
</array>
<key>IBSystem Version</key>
<string>8I127</string>
</dict>
</plist>

Binary file not shown.

View File

@@ -1,64 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBClasses</key>
<array>
<dict>
<key>CLASS</key>
<string>FirstResponder</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>addiTunes</key>
<string>id</string>
<key>askForDirectory</key>
<string>id</string>
<key>changeDirectoryState</key>
<string>id</string>
<key>popupAddDirectoryMenu</key>
<string>id</string>
<key>removeSelectedDirectory</key>
<string>id</string>
<key>toggleVisible</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>DirectoryPanel</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>addButtonPopUp</key>
<string>NSPopUpButton</string>
<key>directories</key>
<string>NSOutlineView</string>
<key>removeButton</key>
<string>NSButton</string>
</dict>
<key>SUPERCLASS</key>
<string>DirectoryPanelBase</string>
</dict>
<dict>
<key>CLASS</key>
<string>OutlineView</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>py</key>
<string>PyApp</string>
</dict>
<key>SUPERCLASS</key>
<string>NSOutlineView</string>
</dict>
</array>
<key>IBVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBFramework Version</key>
<string>629</string>
<key>IBLastKnownRelativeProjectPath</key>
<string>../../dupeguru.xcodeproj</string>
<key>IBOldestOS</key>
<integer>5</integer>
<key>IBOpenObjects</key>
<array>
<integer>5</integer>
</array>
<key>IBSystem Version</key>
<string>9B18</string>
<key>targetFramework</key>
<string>IBCocoaFramework</string>
</dict>
</plist>

View File

@@ -1,257 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBClasses</key>
<array>
<dict>
<key>CLASS</key>
<string>NSSegmentedControl</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSControl</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>openWebsite</key>
<string>id</string>
<key>popupPresets</key>
<string>id</string>
<key>toggleDirectories</key>
<string>id</string>
<key>unlockApp</key>
<string>id</string>
<key>usePreset</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>AppDelegate</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>defaultsController</key>
<string>NSUserDefaultsController</string>
<key>presetsButton</key>
<string>NSButton</string>
<key>presetsPopup</key>
<string>NSPopUpButton</string>
<key>py</key>
<string>PyDupeGuru</string>
<key>recentDirectories</key>
<string>RecentDirectories</string>
<key>result</key>
<string>ResultWindow</string>
<key>unlockMenuItem</key>
<string>NSMenuItem</string>
</dict>
<key>SUPERCLASS</key>
<string>AppDelegateBase</string>
</dict>
<dict>
<key>CLASS</key>
<string>PyApp</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>CLASS</key>
<string>MatchesView</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>OutlineView</string>
</dict>
<dict>
<key>CLASS</key>
<string>PyDupeGuruBase</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>PyApp</string>
</dict>
<dict>
<key>CLASS</key>
<string>PyDupeGuru</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>PyDupeGuruBase</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>changeDelta</key>
<string>id</string>
<key>changePowerMarker</key>
<string>id</string>
<key>clearIgnoreList</key>
<string>id</string>
<key>collapseAll</key>
<string>id</string>
<key>copyMarked</key>
<string>id</string>
<key>deleteMarked</key>
<string>id</string>
<key>expandAll</key>
<string>id</string>
<key>exportToXHTML</key>
<string>id</string>
<key>filter</key>
<string>id</string>
<key>ignoreSelected</key>
<string>id</string>
<key>markAll</key>
<string>id</string>
<key>markInvert</key>
<string>id</string>
<key>markNone</key>
<string>id</string>
<key>markSelected</key>
<string>id</string>
<key>markToggle</key>
<string>id</string>
<key>moveMarked</key>
<string>id</string>
<key>openSelected</key>
<string>id</string>
<key>refresh</key>
<string>id</string>
<key>removeDeadTracks</key>
<string>id</string>
<key>removeMarked</key>
<string>id</string>
<key>removeSelected</key>
<string>id</string>
<key>renameSelected</key>
<string>id</string>
<key>resetColumnsToDefault</key>
<string>id</string>
<key>revealSelected</key>
<string>id</string>
<key>showPreferencesPanel</key>
<string>id</string>
<key>startDuplicateScan</key>
<string>id</string>
<key>switchSelected</key>
<string>id</string>
<key>toggleColumn</key>
<string>id</string>
<key>toggleDelta</key>
<string>id</string>
<key>toggleDetailsPanel</key>
<string>id</string>
<key>togglePowerMarker</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>ResultWindow</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>actionMenu</key>
<string>NSPopUpButton</string>
<key>actionMenuView</key>
<string>NSView</string>
<key>app</key>
<string>id</string>
<key>columnsMenu</key>
<string>NSMenu</string>
<key>deltaSwitch</key>
<string>NSSegmentedControl</string>
<key>deltaSwitchView</key>
<string>NSView</string>
<key>filterField</key>
<string>NSSearchField</string>
<key>filterFieldView</key>
<string>NSView</string>
<key>matches</key>
<string>MatchesView</string>
<key>pmSwitch</key>
<string>NSSegmentedControl</string>
<key>pmSwitchView</key>
<string>NSView</string>
<key>preferencesPanel</key>
<string>NSWindow</string>
<key>py</key>
<string>PyDupeGuru</string>
<key>stats</key>
<string>NSTextField</string>
</dict>
<key>SUPERCLASS</key>
<string>ResultWindowBase</string>
</dict>
<dict>
<key>CLASS</key>
<string>FirstResponder</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>checkForUpdates</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>SUUpdater</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>clearMenu</key>
<string>id</string>
<key>menuClick</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>RecentDirectories</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>delegate</key>
<string>id</string>
<key>menu</key>
<string>NSMenu</string>
</dict>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>CLASS</key>
<string>ResultWindowBase</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSWindowController</string>
</dict>
<dict>
<key>CLASS</key>
<string>OutlineView</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>py</key>
<string>PyApp</string>
</dict>
<key>SUPERCLASS</key>
<string>NSOutlineView</string>
</dict>
</array>
<key>IBVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBFramework Version</key>
<string>629</string>
<key>IBLastKnownRelativeProjectPath</key>
<string>../../dupeguru.xcodeproj</string>
<key>IBOldestOS</key>
<integer>5</integer>
<key>IBOpenObjects</key>
<array>
<integer>598</integer>
</array>
<key>IBSystem Version</key>
<string>9E17</string>
<key>targetFramework</key>
<string>IBCocoaFramework</string>
</dict>
</plist>

View File

@@ -23,11 +23,13 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>5.6.6</string>
<string>5.7.0</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2009</string>
<key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
<key>SUPublicDSAKeyFile</key>

View File

@@ -9,19 +9,13 @@ http://www.hardcoded.net/licenses/hs_license
#import <Cocoa/Cocoa.h>
#import "cocoalib/Outline.h"
#import "dgbase/ResultWindow.h"
#import "DetailsPanel.h"
#import "DirectoryPanel.h"
@interface ResultWindow : ResultWindowBase
{
IBOutlet NSPopUpButton *actionMenu;
IBOutlet NSMenu *columnsMenu;
IBOutlet NSSearchField *filterField;
IBOutlet NSWindow *preferencesPanel;
NSString *_lastAction;
DetailsPanel *_detailsPanel;
NSMutableArray *_resultColumns;
NSMutableIndexSet *_deltaColumns;
}
- (IBAction)clearIgnoreList:(id)sender;
@@ -38,15 +32,7 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)removeMarked:(id)sender;
- (IBAction)removeSelected:(id)sender;
- (IBAction)renameSelected:(id)sender;
- (IBAction)resetColumnsToDefault:(id)sender;
- (IBAction)revealSelected:(id)sender;
- (IBAction)showPreferencesPanel:(id)sender;
- (IBAction)startDuplicateScan:(id)sender;
- (IBAction)toggleColumn:(id)sender;
- (IBAction)toggleDelta:(id)sender;
- (IBAction)toggleDetailsPanel:(id)sender;
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn;
- (void)initResultColumns;
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth;
@end

View File

@@ -19,7 +19,7 @@ http://www.hardcoded.net/licenses/hs_license
- (void)awakeFromNib
{
[super awakeFromNib];
_detailsPanel = nil;
[[self window] setTitle:@"dupeGuru Music Edition"];
_displayDelta = NO;
_powerMode = NO;
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,7)] retain];
@@ -29,23 +29,8 @@ http://www.hardcoded.net/licenses/hs_license
[py setDisplayDeltaValues:b2n(_displayDelta)];
[matches setTarget:self];
[matches setDoubleAction:@selector(openSelected:)];
[[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]];
[self initResultColumns];
[self refreshStats];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease];
[t setAllowsUserCustomization:YES];
[t setAutosavesConfiguration:YES];
[t setDisplayMode:NSToolbarDisplayModeIconAndLabel];
[t setDelegate:self];
[[self window] setToolbar:t];
}
/* Overrides */
- (NSString *)logoImageName
{
return @"dgme_logo_32";
}
/* Actions */
@@ -182,11 +167,6 @@ http://www.hardcoded.net/licenses/hs_license
[py revealSelected];
}
- (IBAction)showPreferencesPanel:(id)sender
{
[preferencesPanel makeKeyAndOrderFront:sender];
}
- (IBAction)startDuplicateScan:(id)sender
{
if ([matches numberOfRows] > 0)
@@ -219,26 +199,6 @@ http://www.hardcoded.net/licenses/hs_license
}
}
- (IBAction)toggleColumn:(id)sender
{
NSMenuItem *mi = sender;
NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]];
NSTableColumn *col = [matches tableColumnWithIdentifier:colId];
if (col == nil)
{
//Add Column
col = [_resultColumns objectAtIndex:[mi tag]];
[matches addTableColumn:col];
[mi setState:NSOnState];
}
else
{
//Remove column
[matches removeTableColumn:col];
[mi setState:NSOffState];
}
}
- (IBAction)toggleDelta:(id)sender
{
if ([deltaSwitch selectedSegment] == 1)
@@ -248,39 +208,22 @@ http://www.hardcoded.net/licenses/hs_license
[self changeDelta:sender];
}
- (IBAction)toggleDetailsPanel:(id)sender
{
if (!_detailsPanel)
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
if ([[_detailsPanel window] isVisible])
[[_detailsPanel window] close];
else
[[_detailsPanel window] orderFront:nil];
}
/* Public */
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn
{
NSNumber *n = [NSNumber numberWithInt:aIdentifier];
NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]];
[col setWidth:aWidth];
[col setEditable:NO];
[[col dataCell] setFont:[[aColumn dataCell] font]];
[[col headerCell] setStringValue:aTitle];
[col setResizingMask:NSTableColumnUserResizingMask];
[col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]];
return col;
}
- (void)initResultColumns
{
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];
_resultColumns = [[NSMutableArray alloc] init];
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name
[_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]];
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"3"]]; // Time
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"4"]]; // Bitrate
NSTableColumn *sizeCol = [self getColumnForIdentifier:2 title:@"Size (MB)" width:63 refCol:refCol];
[[sizeCol dataCell] setAlignment:NSRightTextAlignment];
[_resultColumns addObject:sizeCol];
NSTableColumn *timeCol = [self getColumnForIdentifier:3 title:@"Time" width:50 refCol:refCol];
[[timeCol dataCell] setAlignment:NSRightTextAlignment];
[_resultColumns addObject:timeCol];
NSTableColumn *brCol = [self getColumnForIdentifier:4 title:@"Bitrate" width:50 refCol:refCol];
[[brCol dataCell] setAlignment:NSRightTextAlignment];
[_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]];
@@ -292,40 +235,11 @@ http://www.hardcoded.net/licenses/hs_license
[_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:[matches tableColumnWithIdentifier:@"16"]]; // Match %
[_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]];
}
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth
{
NSTableColumn *col;
NSString *colId;
NSNumber *width;
NSMenuItem *mi;
//Remove all columns
NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator];
while (mi = [e nextObject])
{
if ([mi state] == NSOnState)
[self toggleColumn:mi];
}
//Add columns and set widths
e = [aColumnsOrder objectEnumerator];
while (colId = [e nextObject])
{
if (![colId isEqual:@"mark"])
{
col = [_resultColumns objectAtIndex:[colId intValue]];
width = [aColumnsWidth objectForKey:[col identifier]];
mi = [columnsMenu itemWithTag:[colId intValue]];
if (width)
[col setWidth:[width floatValue]];
[self toggleColumn:mi];
}
}
}
/* Delegate */
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{

View File

@@ -19,8 +19,6 @@
/* End PBXAppleScriptBuildPhase section */
/* Begin PBXBuildFile section */
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; };
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */; };
@@ -29,7 +27,8 @@
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 */; };
CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; };
CE3FBDD31094637800B72D77 /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD11094637800B72D77 /* DetailsPanel.xib */; };
CE3FBDD41094637800B72D77 /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */; };
CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; };
CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.m */; };
CE515DF30FC6C12E00EC695D /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE515DE10FC6C12E00EC695D /* Dialogs.m */; };
@@ -51,13 +50,11 @@
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 */; };
CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */; };
CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; };
CE900AD2109B238600754048 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD1109B238600754048 /* Preferences.xib */; };
CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD6109B2A9B00754048 /* MainMenu.xib */; };
CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; };
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; };
CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */ = {isa = PBXBuildFile; fileRef = CED2A6870A05102600AC4C3F /* power_marker32.png */; };
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; };
CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; };
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 */; };
@@ -79,11 +76,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = "<group>"; };
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = "<absolute>"; };
13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; };
29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = "<group>"; };
29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = "<absolute>"; };
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; };
@@ -95,7 +90,8 @@
CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; };
CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; };
CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; };
CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = "<group>"; };
CE3FBDD11094637800B72D77 /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DetailsPanel.xib; sourceTree = "<group>"; };
CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = "<group>"; };
CE49DEF20FDFEB810098617B /* BRSingleLineFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BRSingleLineFormatter.h; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.h; sourceTree = SOURCE_ROOT; };
CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; };
CE49DEF40FDFEB810098617B /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; };
@@ -136,13 +132,11 @@
CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; };
CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = "<group>"; };
CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = "<group>"; };
CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgme_logo_32.png; path = images/dgme_logo_32.png; sourceTree = SOURCE_ROOT; };
CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = "<group>"; };
CE900AD1109B238600754048 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = ../../xib/Preferences.xib; sourceTree = "<group>"; };
CE900AD6109B2A9B00754048 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = 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>"; };
CED2A6870A05102600AC4C3F /* power_marker32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = power_marker32.png; path = images/power_marker32.png; sourceTree = SOURCE_ROOT; };
CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = "<group>"; };
CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; 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; };
CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; };
@@ -231,16 +225,13 @@
29B97317FDCFA39411CA2CEA /* Resources */ = {
isa = PBXGroup;
children = (
CE3FBDD01094637800B72D77 /* xib */,
CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */,
CE381CF509915304003581CE /* dg_cocoa.plugin */,
CEFC294309C89E0000D9F998 /* images */,
CEEB135109C837A2004D2330 /* dupeguru.icns */,
8D1107310486CEB800E47090 /* Info.plist */,
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */,
CECA899709DB12CA00A3D774 /* Details.nib */,
CE3AA46509DB207900DB3A21 /* Directories.nib */,
29B97318FDCFA39411CA2CEA /* MainMenu.nib */,
);
name = Resources;
sourceTree = "<group>";
@@ -254,6 +245,18 @@
name = Frameworks;
sourceTree = "<group>";
};
CE3FBDD01094637800B72D77 /* xib */ = {
isa = PBXGroup;
children = (
CE900AD6109B2A9B00754048 /* MainMenu.xib */,
CE3FBDD11094637800B72D77 /* DetailsPanel.xib */,
CE3FBDD21094637800B72D77 /* DirectoryPanel.xib */,
CE900AD1109B238600754048 /* Preferences.xib */,
);
name = xib;
path = dgbase/xib;
sourceTree = "<group>";
};
CE49DEF10FDFEB810098617B /* brsinglelineformatter */ = {
isa = PBXGroup;
children = (
@@ -316,9 +319,6 @@
CEFC294309C89E0000D9F998 /* images */ = {
isa = PBXGroup;
children = (
CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */,
CED2A6870A05102600AC4C3F /* power_marker32.png */,
CEF7823709C8AA0200EF38FF /* gear.png */,
CEFC295309C89FF200D9F998 /* details32.png */,
CEFC295409C89FF200D9F998 /* preferences32.png */,
CEFC294509C89E3D00D9F998 /* folder32.png */,
@@ -371,23 +371,20 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */,
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */,
CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */,
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */,
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */,
CEFC295509C89FF200D9F998 /* details32.png in Resources */,
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */,
CEF7823809C8AA0200EF38FF /* gear.png in Resources */,
CECA899909DB12CA00A3D774 /* Details.nib in Resources */,
CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */,
CED2A6880A05102700AC4C3F /* power_marker32.png in Resources */,
CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */,
CE515E030FC6C13E00EC695D /* progress.nib in Resources */,
CE515E040FC6C13E00EC695D /* registration.nib in Resources */,
CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */,
CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */,
CE3FBDD31094637800B72D77 /* DetailsPanel.xib in Resources */,
CE3FBDD41094637800B72D77 /* DirectoryPanel.xib in Resources */,
CE900AD2109B238600754048 /* Preferences.xib in Resources */,
CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -424,30 +421,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
089C165DFE840E0CC02AAC07 /* English */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = {
isa = PBXVariantGroup;
children = (
29B97319FDCFA39411CA2CEA /* English */,
);
name = MainMenu.nib;
sourceTree = SOURCE_ROOT;
};
CE3AA46509DB207900DB3A21 /* Directories.nib */ = {
isa = PBXVariantGroup;
children = (
CE3AA46609DB207900DB3A21 /* English */,
);
name = Directories.nib;
sourceTree = "<group>";
};
CE515DFC0FC6C13E00EC695D /* ErrorReportWindow.xib */ = {
isa = PBXVariantGroup;
children = (
@@ -472,38 +445,9 @@
name = registration.nib;
sourceTree = SOURCE_ROOT;
};
CECA899709DB12CA00A3D774 /* Details.nib */ = {
isa = PBXVariantGroup;
children = (
CECA899809DB12CA00A3D774 /* English */,
);
name = Details.nib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4B08A954540054247B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
COPY_PHASE_STRIP = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(FRAMEWORK_SEARCH_PATHS)",
"$(SRCROOT)/../../../cocoalib/build/Release",
"\"$(SRCROOT)/../../base/cocoa/build/Release\"",
);
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_FIX_AND_CONTINUE = YES;
GCC_MODEL_TUNING = G5;
GCC_OPTIMIZATION_LEVEL = 0;
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(HOME)/Applications";
PRODUCT_NAME = dupeGuru;
WRAPPER_EXTENSION = app;
ZERO_LINK = YES;
};
name = Debug;
};
C01FCF4C08A954540054247B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -521,32 +465,16 @@
};
name = Release;
};
C01FCF4F08A954540054247B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
GCC_C_LANGUAGE_STANDARD = c99;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.4;
PREBINDING = NO;
SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk";
};
name = Debug;
};
C01FCF5008A954540054247B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)";
ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386";
COPY_PHASE_STRIP = NO;
GCC_C_LANGUAGE_STANDARD = c99;
GCC_VERSION = 4.0;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.4;
PREBINDING = NO;
SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk";
STRIP_INSTALLED_PRODUCT = NO;
MACOSX_DEPLOYMENT_TARGET = 10.5;
SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk";
};
name = Release;
};
@@ -556,7 +484,6 @@
C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C01FCF4B08A954540054247B /* Debug */,
C01FCF4C08A954540054247B /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -565,7 +492,6 @@
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C01FCF4F08A954540054247B /* Debug */,
C01FCF5008A954540054247B /* Release */,
);
defaultConfigurationIsVisible = 0;

View File

@@ -5,13 +5,15 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import sys
sys.path.insert(0, 'py') # for hsutil and hsdocgen
import os
from help import gen
print "Generating help"
os.chdir('help')
os.system('python -u gen.py')
os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_me_help')
os.chdir('..')
gen.generate()
os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer help/dupeguru_me_help')
print "Generating py plugin"
os.chdir('py')

View File

@@ -8,12 +8,13 @@
import objc
from AppKit import *
from dupeguru import app_me_cocoa, scanner
from dupeguru_me.app_cocoa import DupeGuruME
from dupeguru.scanner import (SCAN_TYPE_FILENAME, SCAN_TYPE_FIELDS, SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG, SCAN_TYPE_CONTENT, SCAN_TYPE_CONTENT_AUDIO)
# Fix py2app imports which chokes on relative imports
from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner
from hsfs import auto, stats, tree, music
from hsfs.phys import music
from dupeguru_me import app_cocoa, data, fs, scanner
from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner, fs
from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma
from hsutil import conflict
@@ -23,7 +24,7 @@ class PyApp(NSObject):
class PyDupeGuru(PyApp):
def init(self):
self = super(PyDupeGuru,self).init()
self.app = app_me_cocoa.DupeGuruME()
self.app = DupeGuruME()
return self
#---Directories
@@ -180,12 +181,12 @@ class PyDupeGuru(PyApp):
def setScanType_(self, scan_type):
try:
self.app.scanner.scan_type = [
scanner.SCAN_TYPE_FILENAME,
scanner.SCAN_TYPE_FIELDS,
scanner.SCAN_TYPE_FIELDS_NO_ORDER,
scanner.SCAN_TYPE_TAG,
scanner.SCAN_TYPE_CONTENT,
scanner.SCAN_TYPE_CONTENT_AUDIO
SCAN_TYPE_FILENAME,
SCAN_TYPE_FIELDS,
SCAN_TYPE_FIELDS_NO_ORDER,
SCAN_TYPE_TAG,
SCAN_TYPE_CONTENT,
SCAN_TYPE_CONTENT_AUDIO
][scan_type]
except IndexError:
pass

View File

@@ -16,4 +16,5 @@ if op.exists('build'):
if op.exists('dist'):
shutil.rmtree('dist')
os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5'
print_and_do('python -u setup.py py2app')

2611
me/cocoa/xib/Preferences.xib Normal file

File diff suppressed because it is too large Load Diff

0
me/help/__init__.py Normal file
View File

View File

@@ -1,3 +1,12 @@
- date: 2009-12-18
version: 5.7.0
description: |
* Added drag & drop support in the Directories panel. (#9)
* Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72)
* Clarified how directories' state are set by painting a combo box in the state cells. [Windows]
(#76)
* Fixed some crashes. (#78 and #79)
* Dropped Mac OS X Tiger support.
- date: 2009-10-14
version: 5.6.6
description: |

View File

@@ -5,10 +5,10 @@
# 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
from hsdocgen import generate_help, filters
tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}")
generate_help.main('.', 'dupeguru_me_help', force_render=True, tix=tix)
def generate(windows=False):
tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}")
basepath = op.dirname(__file__)
generate_help.main(basepath, op.join(basepath, 'dupeguru_me_help'), force_render=True, tix=tix, windows=windows)

View File

@@ -64,4 +64,12 @@ If your comparison threshold is low enough, you will probably end up with live a
* **Mac OS X**: Type "[*]" in the "Filter" field in the toolbar.
* Click on **Mark --> Mark All**.
* Click on **Actions --> Remove Selected from Results**.
### I tried to send my duplicates to Trash, but dupeGuru is telling me it can't do it. Why? What can I do?
Most of the time, the reason why dupeGuru can't send files to Trash is because of file permissions. You need *write* permissions on files you want to send to Trash. If you're not familiar with the command line, you can use utilities such as [BatChmod](http://macchampion.com/arbysoft/BatchMod) to fix your permissions.
If dupeGuru still gives you troubles after fixing your permissions, there have been some cases where using "Move Marked to..." as a workaround did the trick. So instead of sending your files to Trash, you send them to a temporary folder with the "Move Marked to..." action, and then you delete that temporary folder manually.
If all of this fail, [contact HS support](http://www.hardcoded.net/support), we'll figure it out.
</%text>

0
me/py/__init__.py Normal file
View File

View File

@@ -7,29 +7,29 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import os.path as op
import logging
from appscript import app, k, CommandError
import time
from hsutil.cocoa import as_fetch
import hsfs.phys.music
import app_cocoa, data_me, scanner
from dupeguru.app_cocoa import JOBID2TITLE, DupeGuru as DupeGuruBase
from . import data, scanner, fs
JOB_REMOVE_DEAD_TRACKS = 'jobRemoveDeadTracks'
JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks'
app_cocoa.JOBID2TITLE.update({
JOBID2TITLE.update({
JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library",
JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library",
})
class DupeGuruME(app_cocoa.DupeGuru):
class DupeGuruME(DupeGuruBase):
def __init__(self):
app_cocoa.DupeGuru.__init__(self, data_me, 'dupeGuru Music Edition', appid=1)
DupeGuruBase.__init__(self, data, 'dupeGuru Music Edition', appid=1)
self.scanner = scanner.ScannerME()
self.directories.dirclass = hsfs.phys.music.Directory
self.directories.fileclasses = [fs.Mp3File, fs.Mp4File, fs.WmaFile, fs.OggFile, fs.FlacFile, fs.AiffFile]
self.dead_tracks = []
def remove_dead_tracks(self):

View File

@@ -8,7 +8,7 @@
# http://www.hardcoded.net/licenses/hs_license
from hsutil.str import format_time, FT_MINUTES, format_size
from .data import (format_path, format_timestamp, format_words, format_perc,
from dupeguru.data import (format_path, format_timestamp, format_words, format_perc,
format_dupe_count, cmp_value)
COLUMNS = [
@@ -76,7 +76,7 @@ def GetDisplayInfo(dupe, group, delta):
str(dupe.track),
dupe.comment,
format_perc(percentage),
format_words(dupe.words),
format_words(dupe.words) if hasattr(dupe, 'words') else '',
format_dupe_count(dupe_count)
]

183
me/py/fs.py Normal file
View File

@@ -0,0 +1,183 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-10-23
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsmedia import mpeg, wma, mp4, ogg, flac, aiff
from hsutil.str import get_file_ext
from dupeguru import fs
TAG_FIELDS = ['audiosize', 'duration', 'bitrate', 'samplerate', 'title', 'artist',
'album', 'genre', 'year', 'track', 'comment']
class MusicFile(fs.File):
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
INITIAL_INFO.update({
'audiosize': 0,
'bitrate' : 0,
'duration' : 0,
'samplerate':0,
'artist' : '',
'album' : '',
'title' : '',
'genre' : '',
'comment' : '',
'year' : '',
'track' : 0,
})
HANDLED_EXTS = set()
@classmethod
def can_handle(cls, path):
if not fs.File.can_handle(path):
return False
return get_file_ext(path[-1]) in cls.HANDLED_EXTS
class Mp3File(MusicFile):
HANDLED_EXTS = set(['mp3'])
def _read_info(self, field):
if field == 'md5partial':
fileinfo = mpeg.Mpeg(unicode(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))
self.audiosize = fileinfo.audio_size
self.bitrate = fileinfo.bitrate
self.duration = fileinfo.duration
self.samplerate = fileinfo.sample_rate
i1 = fileinfo.id3v1
# id3v1, even when non-existant, gives empty values. not id3v2. if id3v2 don't exist,
# just replace it with id3v1
i2 = fileinfo.id3v2
if not i2.exists:
i2 = i1
self.artist = i2.artist or i1.artist
self.album = i2.album or i1.album
self.title = i2.title or i1.title
self.genre = i2.genre or i1.genre
self.comment = i2.comment or i1.comment
self.year = i2.year or i1.year
self.track = i2.track or i1.track
class WmaFile(MusicFile):
HANDLED_EXTS = set(['wma'])
def _read_info(self, field):
if field == 'md5partial':
dec = wma.WMADecoder(unicode(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))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
self.samplerate = dec.sample_rate
self.artist = dec.artist
self.album = dec.album
self.title = dec.title
self.genre = dec.genre
self.comment = dec.comment
self.year = dec.year
self.track = dec.track
class Mp4File(MusicFile):
HANDLED_EXTS = set(['m4a', 'm4p'])
def _read_info(self, field):
if field == 'md5partial':
dec = mp4.File(unicode(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))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
self.samplerate = dec.sample_rate
self.artist = dec.artist
self.album = dec.album
self.title = dec.title
self.genre = dec.genre
self.comment = dec.comment
self.year = dec.year
self.track = dec.track
dec.close()
class OggFile(MusicFile):
HANDLED_EXTS = set(['ogg'])
def _read_info(self, field):
if field == 'md5partial':
dec = ogg.Vorbis(unicode(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))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
self.samplerate = dec.sample_rate
self.artist = dec.artist
self.album = dec.album
self.title = dec.title
self.genre = dec.genre
self.comment = dec.comment
self.year = dec.year
self.track = dec.track
class FlacFile(MusicFile):
HANDLED_EXTS = set(['flac'])
def _read_info(self, field):
if field == 'md5partial':
dec = flac.FLAC(unicode(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))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
self.samplerate = dec.sample_rate
self.artist = dec.artist
self.album = dec.album
self.title = dec.title
self.genre = dec.genre
self.comment = dec.comment
self.year = dec.year
self.track = dec.track
class AiffFile(MusicFile):
HANDLED_EXTS = set(['aif', 'aiff', 'aifc'])
def _read_info(self, field):
if field == 'md5partial':
dec = aiff.File(unicode(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))
self.audiosize = dec.audio_size
self.bitrate = dec.bitrate
self.duration = dec.duration
self.samplerate = dec.sample_rate
tag = dec.tag
if tag is not None:
self.artist = tag.artist
self.album = tag.album
self.title = tag.title
self.genre = tag.genre
self.comment = tag.comment
self.year = tag.year
self.track = tag.track

16
me/py/scanner.py Normal file
View File

@@ -0,0 +1,16 @@
# Created By: Virgil Dupras
# Created On: 2006/03/03
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from dupeguru.scanner import Scanner as ScannerBase
class ScannerME(ScannerBase):
@staticmethod
def _key_func(dupe):
return (not dupe.is_ref, -dupe.bitrate, -dupe.size)

0
me/py/tests/__init__.py Normal file
View File

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-10-23
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from hsutil.path import Path
from dupeguru.engine import getwords
from ..scanner import *
class NamedObject(object):
def __init__(self, name="foobar", size=1):
self.name = name
self.size = size
self.path = Path('')
self.words = getwords(name)
no = NamedObject
def test_priorize_me():
# in ScannerME, bitrate goes first (right after is_ref) in priorization
s = ScannerME()
o1, o2 = no('foo'), no('foo')
o1.bitrate = 1
o2.bitrate = 2
[group] = s.GetDupeGroups([o1, o2])
assert group.ref is o2

View File

@@ -7,9 +7,7 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import hsfs.phys.music
from dupeguru import data_me, scanner
from dupeguru_me import data, scanner, fs
from base.app import DupeGuru as DupeGuruBase
from details_dialog import DetailsDialog
@@ -19,15 +17,15 @@ from preferences_dialog import PreferencesDialog
class DupeGuru(DupeGuruBase):
LOGO_NAME = 'logo_me'
NAME = 'dupeGuru Music Edition'
VERSION = '5.6.6'
VERSION = '5.7.0'
DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8])
def __init__(self):
DupeGuruBase.__init__(self, data_me, appid=1)
DupeGuruBase.__init__(self, data, appid=1)
def _setup(self):
self.scanner = scanner.ScannerME()
self.directories.dirclass = hsfs.phys.music.Directory
self.directories.fileclasses = [fs.Mp3File, fs.Mp4File, fs.WmaFile, fs.OggFile, fs.FlacFile, fs.AiffFile]
DupeGuruBase._setup(self)
def _update_options(self):

View File

@@ -13,10 +13,13 @@ import os.path as op
from hsutil.build import print_and_do, build_all_qt_ui
from help import gen
build_all_qt_ui(op.join('qtlib', 'ui'))
build_all_qt_ui('base')
build_all_qt_ui('.')
os.chdir('help')
print_and_do('python gen.py')
os.chdir('base')
print_and_do("pyrcc4 dg.qrc > dg_rc.py")
os.chdir('..')
gen.generate(windows=True)

View File

@@ -8,20 +8,14 @@ http://www.hardcoded.net/licenses/hs_license
#import <Cocoa/Cocoa.h>
#import "dgbase/AppDelegate.h"
#import "ResultWindow.h"
#import "DirectoryPanel.h"
#import "DetailsPanel.h"
#import "PyDupeGuru.h"
@interface AppDelegate : AppDelegateBase
{
IBOutlet ResultWindow *result;
DetailsPanel *_detailsPanel;
DirectoryPanel *_directoryPanel;
}
- (IBAction)openWebsite:(id)sender;
- (IBAction)toggleDetailsPanel:(id)sender;
- (IBAction)toggleDirectories:(id)sender;
- (DirectoryPanel *)directoryPanel;

View File

@@ -12,6 +12,7 @@ http://www.hardcoded.net/licenses/hs_license
#import "Utils.h"
#import "ValueTransformers.h"
#import "Consts.h"
#import "DetailsPanel.h"
@implementation AppDelegate
+ (void)initialize
@@ -36,29 +37,22 @@ http://www.hardcoded.net/licenses/hs_license
{
self = [super init];
_directoryPanel = nil;
_detailsPanel = nil;
_appName = APPNAME;
return self;
}
- (DetailsPanelBase *)detailsPanel
{
if (!_detailsPanel)
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
return _detailsPanel;
}
- (IBAction)openWebsite:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_pe"]];
}
- (IBAction)toggleDetailsPanel:(id)sender
{
if (!_detailsPanel)
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
if ([[_detailsPanel window] isVisible])
[[_detailsPanel window] close];
else
{
[[_detailsPanel window] orderFront:nil];
[_detailsPanel refresh];
}
}
- (IBAction)toggleDirectories:(id)sender
{
[[self directoryPanel] toggleVisible:sender];
@@ -75,19 +69,12 @@ http://www.hardcoded.net/licenses/hs_license
//Delegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[ProgressController mainProgressController] setWorker:py];
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
//Restore Columns
NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"];
NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"];
if ([columnsOrder count])
[result restoreColumnsPosition:columnsOrder widths:columnsWidth];
//Reg stuff
if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC])
[unlockMenuItem setTitle:@"Thanks for buying dupeGuru Picture Edition!"];
//Restore results
[py loadIgnoreList];
[py loadResults];
NSMenu *actionsMenu = [[[NSApp mainMenu] itemWithTitle:@"Actions"] submenu];
// index 2 is just after "Clear Ingore List"
NSMenuItem *mi = [actionsMenu insertItemWithTitle:@"Clear Picture Cache" action:@selector(clearPictureCache:) keyEquivalent:@"P" atIndex:2];
[mi setTarget:result];
[mi setKeyEquivalentModifierMask:NSCommandKeyMask|NSShiftKeyMask];
[super applicationDidFinishLaunching:aNotification];
}
- (void)applicationWillBecomeActive:(NSNotification *)aNotification

View File

@@ -1,24 +0,0 @@
{
IBClasses = (
{
CLASS = DetailsPanel;
LANGUAGE = ObjC;
OUTLETS = {
detailsTable = NSTableView;
dupeImage = NSImageView;
dupeProgressIndicator = NSProgressIndicator;
refImage = NSImageView;
refProgressIndicator = NSProgressIndicator;
};
SUPERCLASS = NSWindowController;
},
{CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; },
{
CLASS = TableView;
LANGUAGE = ObjC;
OUTLETS = {py = PyApp; };
SUPERCLASS = NSTableView;
}
);
IBVersion = 1;
}

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBDocumentLocation</key>
<string>701 68 356 240 0 0 1440 878 </string>
<key>IBFramework Version</key>
<string>446.1</string>
<key>IBOpenObjects</key>
<array>
<integer>5</integer>
</array>
<key>IBSystem Version</key>
<string>8R2232</string>
</dict>
</plist>

Binary file not shown.

View File

@@ -1,62 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBClasses</key>
<array>
<dict>
<key>CLASS</key>
<string>FirstResponder</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>askForDirectory</key>
<string>id</string>
<key>changeDirectoryState</key>
<string>id</string>
<key>popupAddDirectoryMenu</key>
<string>id</string>
<key>removeSelectedDirectory</key>
<string>id</string>
<key>toggleVisible</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>DirectoryPanel</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>addButtonPopUp</key>
<string>NSPopUpButton</string>
<key>directories</key>
<string>NSOutlineView</string>
<key>removeButton</key>
<string>NSButton</string>
</dict>
<key>SUPERCLASS</key>
<string>DirectoryPanelBase</string>
</dict>
<dict>
<key>CLASS</key>
<string>OutlineView</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>py</key>
<string>PyApp</string>
</dict>
<key>SUPERCLASS</key>
<string>NSOutlineView</string>
</dict>
</array>
<key>IBVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBFramework Version</key>
<string>629</string>
<key>IBLastKnownRelativeProjectPath</key>
<string>../../dupeguru.xcodeproj</string>
<key>IBOldestOS</key>
<integer>5</integer>
<key>IBOpenObjects</key>
<array>
<integer>5</integer>
</array>
<key>IBSystem Version</key>
<string>9B18</string>
<key>targetFramework</key>
<string>IBCocoaFramework</string>
</dict>
</plist>

View File

@@ -1,235 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBClasses</key>
<array>
<dict>
<key>CLASS</key>
<string>NSSegmentedControl</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSControl</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>openWebsite</key>
<string>id</string>
<key>toggleDetailsPanel</key>
<string>id</string>
<key>toggleDirectories</key>
<string>id</string>
<key>unlockApp</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>AppDelegate</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>py</key>
<string>PyDupeGuru</string>
<key>recentDirectories</key>
<string>RecentDirectories</string>
<key>result</key>
<string>ResultWindow</string>
<key>unlockMenuItem</key>
<string>NSMenuItem</string>
</dict>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>CLASS</key>
<string>PyApp</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>CLASS</key>
<string>MatchesView</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>OutlineView</string>
</dict>
<dict>
<key>CLASS</key>
<string>PyDupeGuru</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>PyApp</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>changeDelta</key>
<string>id</string>
<key>changePowerMarker</key>
<string>id</string>
<key>clearIgnoreList</key>
<string>id</string>
<key>clearPictureCache</key>
<string>id</string>
<key>collapseAll</key>
<string>id</string>
<key>copyMarked</key>
<string>id</string>
<key>deleteMarked</key>
<string>id</string>
<key>expandAll</key>
<string>id</string>
<key>exportToXHTML</key>
<string>id</string>
<key>filter</key>
<string>id</string>
<key>ignoreSelected</key>
<string>id</string>
<key>markAll</key>
<string>id</string>
<key>markInvert</key>
<string>id</string>
<key>markNone</key>
<string>id</string>
<key>markSelected</key>
<string>id</string>
<key>markToggle</key>
<string>id</string>
<key>moveMarked</key>
<string>id</string>
<key>openSelected</key>
<string>id</string>
<key>refresh</key>
<string>id</string>
<key>removeMarked</key>
<string>id</string>
<key>removeSelected</key>
<string>id</string>
<key>renameSelected</key>
<string>id</string>
<key>resetColumnsToDefault</key>
<string>id</string>
<key>revealSelected</key>
<string>id</string>
<key>showPreferencesPanel</key>
<string>id</string>
<key>startDuplicateScan</key>
<string>id</string>
<key>switchSelected</key>
<string>id</string>
<key>toggleColumn</key>
<string>id</string>
<key>toggleDelta</key>
<string>id</string>
<key>toggleDetailsPanel</key>
<string>id</string>
<key>toggleDirectories</key>
<string>id</string>
<key>togglePowerMarker</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>ResultWindow</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>actionMenu</key>
<string>NSPopUpButton</string>
<key>actionMenuView</key>
<string>NSView</string>
<key>app</key>
<string>id</string>
<key>columnsMenu</key>
<string>NSMenu</string>
<key>deltaSwitch</key>
<string>NSSegmentedControl</string>
<key>deltaSwitchView</key>
<string>NSView</string>
<key>filterField</key>
<string>NSSearchField</string>
<key>filterFieldView</key>
<string>NSView</string>
<key>matches</key>
<string>MatchesView</string>
<key>pmSwitch</key>
<string>NSSegmentedControl</string>
<key>pmSwitchView</key>
<string>NSView</string>
<key>preferencesPanel</key>
<string>NSWindow</string>
<key>py</key>
<string>PyDupeGuru</string>
<key>stats</key>
<string>NSTextField</string>
</dict>
<key>SUPERCLASS</key>
<string>NSWindowController</string>
</dict>
<dict>
<key>CLASS</key>
<string>FirstResponder</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>checkForUpdates</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>SUUpdater</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>ACTIONS</key>
<dict>
<key>clearMenu</key>
<string>id</string>
<key>menuClick</key>
<string>id</string>
</dict>
<key>CLASS</key>
<string>RecentDirectories</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>delegate</key>
<string>id</string>
<key>menu</key>
<string>NSMenu</string>
</dict>
<key>SUPERCLASS</key>
<string>NSObject</string>
</dict>
<dict>
<key>CLASS</key>
<string>OutlineView</string>
<key>LANGUAGE</key>
<string>ObjC</string>
<key>OUTLETS</key>
<dict>
<key>py</key>
<string>PyApp</string>
</dict>
<key>SUPERCLASS</key>
<string>NSOutlineView</string>
</dict>
</array>
<key>IBVersion</key>
<string>1</string>
</dict>
</plist>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IBFramework Version</key>
<string>629</string>
<key>IBLastKnownRelativeProjectPath</key>
<string>../../dupeguru.xcodeproj</string>
<key>IBOldestOS</key>
<integer>4</integer>
<key>IBOpenObjects</key>
<array>
<integer>524</integer>
</array>
<key>IBSystem Version</key>
<string>9B18</string>
<key>targetFramework</key>
<string>IBCocoaFramework</string>
</dict>
</plist>

View File

@@ -23,11 +23,13 @@
<key>CFBundleSignature</key>
<string>hsft</string>
<key>CFBundleVersion</key>
<string>1.7.7</string>
<string>1.8.0</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSHumanReadableCopyright</key>
<string>© Hardcoded Software, 2009</string>
<key>SUFeedURL</key>
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
<key>SUPublicDSAKeyFile</key>

View File

@@ -9,16 +9,11 @@ http://www.hardcoded.net/licenses/hs_license
#import <Cocoa/Cocoa.h>
#import "Outline.h"
#import "dgbase/ResultWindow.h"
#import "DirectoryPanel.h"
@interface ResultWindow : ResultWindowBase
{
IBOutlet NSPopUpButton *actionMenu;
IBOutlet NSMenu *columnsMenu;
IBOutlet NSSearchField *filterField;
IBOutlet NSWindow *preferencesPanel;
NSMutableArray *_resultColumns;
NSMutableIndexSet *_deltaColumns;
}
- (IBAction)clearIgnoreList:(id)sender;
@@ -35,16 +30,8 @@ http://www.hardcoded.net/licenses/hs_license
- (IBAction)removeMarked:(id)sender;
- (IBAction)removeSelected:(id)sender;
- (IBAction)renameSelected:(id)sender;
- (IBAction)resetColumnsToDefault:(id)sender;
- (IBAction)revealSelected:(id)sender;
- (IBAction)showPreferencesPanel:(id)sender;
- (IBAction)startDuplicateScan:(id)sender;
- (IBAction)toggleColumn:(id)sender;
- (IBAction)toggleDelta:(id)sender;
- (IBAction)toggleDetailsPanel:(id)sender;
- (IBAction)toggleDirectories:(id)sender;
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn;
- (void)initResultColumns;
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth;
@end

View File

@@ -19,6 +19,7 @@ http://www.hardcoded.net/licenses/hs_license
- (void)awakeFromNib
{
[super awakeFromNib];
[[self window] setTitle:@"dupeGuru Picture Edition"];
_displayDelta = NO;
_powerMode = NO;
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)] retain];
@@ -29,23 +30,8 @@ http://www.hardcoded.net/licenses/hs_license
[py setDisplayDeltaValues:b2n(_displayDelta)];
[matches setTarget:self];
[matches setDoubleAction:@selector(openSelected:)];
[[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]];
[self initResultColumns];
[self refreshStats];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease];
[t setAllowsUserCustomization:YES];
[t setAutosavesConfiguration:YES];
[t setDisplayMode:NSToolbarDisplayModeIconAndLabel];
[t setDelegate:self];
[[self window] setToolbar:t];
}
/* Overrides */
- (NSString *)logoImageName
{
return @"dgpe_logo_32";
}
/* Actions */
@@ -170,7 +156,7 @@ http://www.hardcoded.net/licenses/hs_license
[columnsOrder addObject:@"4"];
[columnsOrder addObject:@"7"];
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
[columnsWidth setObject:i2n(125) forKey:@"0"];
[columnsWidth setObject:i2n(121) forKey:@"0"];
[columnsWidth setObject:i2n(120) forKey:@"1"];
[columnsWidth setObject:i2n(63) forKey:@"2"];
[columnsWidth setObject:i2n(73) forKey:@"4"];
@@ -184,11 +170,6 @@ http://www.hardcoded.net/licenses/hs_license
[py revealSelected];
}
- (IBAction)showPreferencesPanel:(id)sender
{
[preferencesPanel makeKeyAndOrderFront:sender];
}
- (IBAction)startDuplicateScan:(id)sender
{
if ([matches numberOfRows] > 0)
@@ -215,26 +196,6 @@ http://www.hardcoded.net/licenses/hs_license
}
}
- (IBAction)toggleColumn:(id)sender
{
NSMenuItem *mi = sender;
NSString *colId = [NSString stringWithFormat:@"%d",[mi tag]];
NSTableColumn *col = [matches tableColumnWithIdentifier:colId];
if (col == nil)
{
//Add Column
col = [_resultColumns objectAtIndex:[mi tag]];
[matches addTableColumn:col];
[mi setState:NSOnState];
}
else
{
//Remove column
[matches removeTableColumn:col];
[mi setState:NSOffState];
}
}
- (IBAction)toggleDelta:(id)sender
{
if ([deltaSwitch selectedSegment] == 1)
@@ -244,75 +205,29 @@ http://www.hardcoded.net/licenses/hs_license
[self changeDelta:sender];
}
- (IBAction)toggleDetailsPanel:(id)sender
{
[(AppDelegate *)app toggleDetailsPanel:sender];
}
- (IBAction)toggleDirectories:(id)sender
{
[(AppDelegate *)app toggleDirectories:sender];
}
/* Public */
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn
{
NSNumber *n = [NSNumber numberWithInt:aIdentifier];
NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]];
[col setWidth:aWidth];
[col setEditable:NO];
[[col dataCell] setFont:[[aColumn dataCell] font]];
[[col headerCell] setStringValue:aTitle];
[col setResizingMask:NSTableColumnUserResizingMask];
[col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]];
return col;
}
- (void)initResultColumns
{
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];
_resultColumns = [[NSMutableArray alloc] init];
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"1"]]; // Directory
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size
[_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]];
NSTableColumn *sizeCol = [self getColumnForIdentifier:2 title:@"Size (KB)" width:63 refCol:refCol];
[[sizeCol dataCell] setAlignment:NSRightTextAlignment];
[_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:[matches tableColumnWithIdentifier:@"7"]]; // Match %
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Match %" width:58 refCol:refCol]];
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]];
}
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth
{
NSTableColumn *col;
NSString *colId;
NSNumber *width;
NSMenuItem *mi;
//Remove all columns
NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator];
while (mi = [e nextObject])
{
if ([mi state] == NSOnState)
[self toggleColumn:mi];
}
//Add columns and set widths
e = [aColumnsOrder objectEnumerator];
while (colId = [e nextObject])
{
if (![colId isEqual:@"mark"])
{
col = [_resultColumns objectAtIndex:[colId intValue]];
width = [aColumnsWidth objectForKey:[col identifier]];
mi = [columnsMenu itemWithTag:[colId intValue]];
if (width)
[col setWidth:[width floatValue]];
[self toggleColumn:mi];
}
}
}
/* Delegate */
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
{

View File

@@ -3,14 +3,14 @@
archiveVersion = 1;
classes = {
};
objectVersion = 42;
objectVersion = 44;
objects = {
/* Begin PBXBuildFile section */
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; };
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
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 */; };
CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C46A90FA0647E000BE99B /* PictureBlocks.m */; };
CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; };
@@ -18,10 +18,11 @@
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 */; };
CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; };
CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6044EB0FE6796200B71262 /* DetailsPanel.m */; };
CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; };
CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */; };
CE77C89E10946C6D0078B0DB /* DirectoryPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */; };
CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE77C8A710946CE20078B0DB /* DetailsPanel.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 */; };
@@ -42,11 +43,9 @@
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */; };
CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */; };
CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; };
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 */; };
CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; };
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 */; };
@@ -69,15 +68,15 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = "<group>"; };
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = "<absolute>"; };
13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; };
29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = "<group>"; };
29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = "<absolute>"; };
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 PE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru PE.app"; sourceTree = BUILT_PRODUCTS_DIR; };
CE031750109B340A00517EE6 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = ../../xib/Preferences.xib; sourceTree = "<group>"; };
CE031753109B345200517EE6 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_pe_help; path = help/dupeguru_pe_help; sourceTree = SOURCE_ROOT; };
CE0C46A80FA0647E000BE99B /* PictureBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PictureBlocks.h; sourceTree = "<group>"; };
CE0C46A90FA0647E000BE99B /* PictureBlocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PictureBlocks.m; sourceTree = "<group>"; };
@@ -87,12 +86,13 @@
CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; };
CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; };
CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; };
CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = "<group>"; };
CE6044EA0FE6796200B71262 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DetailsPanel.h; path = dgbase/DetailsPanel.h; sourceTree = SOURCE_ROOT; };
CE6044EB0FE6796200B71262 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DetailsPanel.m; path = dgbase/DetailsPanel.m; sourceTree = SOURCE_ROOT; };
CE68EE6509ABC48000971085 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DirectoryPanel.h; sourceTree = SOURCE_ROOT; };
CE68EE6609ABC48000971085 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DirectoryPanel.m; sourceTree = SOURCE_ROOT; };
CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = dgbase/dsa_pub.pem; sourceTree = "<group>"; };
CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DirectoryPanel.xib; sourceTree = "<group>"; };
CE77C8A710946CE20078B0DB /* DetailsPanel.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = DetailsPanel.xib; path = ../../xib/DetailsPanel.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; };
@@ -132,11 +132,9 @@
CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BRSingleLineFormatter.m; path = cocoalib/brsinglelineformatter/BRSingleLineFormatter.m; sourceTree = SOURCE_ROOT; };
CEBAE4250FDA97E000B7887D /* NSCharacterSet_Extensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSCharacterSet_Extensions.h; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.h; sourceTree = SOURCE_ROOT; };
CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSCharacterSet_Extensions.m; path = cocoalib/brsinglelineformatter/NSCharacterSet_Extensions.m; sourceTree = SOURCE_ROOT; };
CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; 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>"; };
CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = "<group>"; };
CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; 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; };
CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; };
@@ -228,16 +226,13 @@
29B97317FDCFA39411CA2CEA /* Resources */ = {
isa = PBXGroup;
children = (
CE77C89A10946C6D0078B0DB /* xib */,
CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */,
CE381CF509915304003581CE /* dg_cocoa.plugin */,
CEFC294309C89E0000D9F998 /* images */,
CEEB135109C837A2004D2330 /* dupeguru.icns */,
8D1107310486CEB800E47090 /* Info.plist */,
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */,
CECA899709DB12CA00A3D774 /* Details.nib */,
CE3AA46509DB207900DB3A21 /* Directories.nib */,
29B97318FDCFA39411CA2CEA /* MainMenu.nib */,
);
name = Resources;
sourceTree = "<group>";
@@ -251,6 +246,18 @@
name = Frameworks;
sourceTree = "<group>";
};
CE77C89A10946C6D0078B0DB /* xib */ = {
isa = PBXGroup;
children = (
CE031753109B345200517EE6 /* MainMenu.xib */,
CE77C8A710946CE20078B0DB /* DetailsPanel.xib */,
CE77C89C10946C6D0078B0DB /* DirectoryPanel.xib */,
CE031750109B340A00517EE6 /* Preferences.xib */,
);
name = xib;
path = dgbase/xib;
sourceTree = "<group>";
};
CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = {
isa = PBXGroup;
children = (
@@ -318,7 +325,6 @@
isa = PBXGroup;
children = (
CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */,
CEF7823709C8AA0200EF38FF /* gear.png */,
CEFC295309C89FF200D9F998 /* details32.png */,
CEFC295409C89FF200D9F998 /* preferences32.png */,
CEFC294509C89E3D00D9F998 /* folder32.png */,
@@ -354,7 +360,7 @@
29B97313FDCFA39411CA2CEA /* Project object */ = {
isa = PBXProject;
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
compatibilityVersion = "Xcode 2.4";
compatibilityVersion = "Xcode 3.0";
hasScannedForEncodings = 1;
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
projectDirPath = "";
@@ -370,22 +376,21 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */,
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */,
CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */,
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */,
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */,
CEFC295509C89FF200D9F998 /* details32.png in Resources */,
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */,
CEF7823809C8AA0200EF38FF /* gear.png in Resources */,
CECA899909DB12CA00A3D774 /* Details.nib in Resources */,
CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */,
CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */,
CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */,
CE80DB770FC194760086DCA6 /* progress.nib in Resources */,
CE80DB780FC194760086DCA6 /* registration.nib in Resources */,
CE6E0F3D1054EC62008D9390 /* dsa_pub.pem in Resources */,
CE77C89E10946C6D0078B0DB /* DirectoryPanel.xib in Resources */,
CE77C8A810946CE20078B0DB /* DetailsPanel.xib in Resources */,
CE031751109B340A00517EE6 /* Preferences.xib in Resources */,
CE031754109B345200517EE6 /* MainMenu.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -425,30 +430,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
089C165DFE840E0CC02AAC07 /* English */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = {
isa = PBXVariantGroup;
children = (
29B97319FDCFA39411CA2CEA /* English */,
);
name = MainMenu.nib;
sourceTree = SOURCE_ROOT;
};
CE3AA46509DB207900DB3A21 /* Directories.nib */ = {
isa = PBXVariantGroup;
children = (
CE3AA46609DB207900DB3A21 /* English */,
);
name = Directories.nib;
sourceTree = "<group>";
};
CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */ = {
isa = PBXVariantGroup;
children = (
@@ -473,39 +454,9 @@
name = registration.nib;
sourceTree = SOURCE_ROOT;
};
CECA899709DB12CA00A3D774 /* Details.nib */ = {
isa = PBXVariantGroup;
children = (
CECA899809DB12CA00A3D774 /* English */,
);
name = Details.nib;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
C01FCF4B08A954540054247B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
COPY_PHASE_STRIP = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(FRAMEWORK_SEARCH_PATHS)",
"$(SRCROOT)/cocoalib/build/Release",
"$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)",
);
FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/dgbase/build/Release\"";
GCC_DYNAMIC_NO_PIC = NO;
GCC_ENABLE_FIX_AND_CONTINUE = YES;
GCC_MODEL_TUNING = G5;
GCC_OPTIMIZATION_LEVEL = 0;
INFOPLIST_FILE = Info.plist;
INSTALL_PATH = "$(HOME)/Applications";
PRODUCT_NAME = dupeGuru;
WRAPPER_EXTENSION = app;
ZERO_LINK = YES;
};
name = Debug;
};
C01FCF4C08A954540054247B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -526,31 +477,16 @@
};
name = Release;
};
C01FCF4F08A954540054247B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
GCC_C_LANGUAGE_STANDARD = c99;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.4;
PREBINDING = NO;
SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
};
name = Debug;
};
C01FCF5008A954540054247B /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)";
ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386";
FRAMEWORK_SEARCH_PATHS = "";
GCC_C_LANGUAGE_STANDARD = c99;
GCC_VERSION = 4.0;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.4;
PREBINDING = NO;
SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
MACOSX_DEPLOYMENT_TARGET = 10.5;
SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk";
};
name = Release;
};
@@ -560,7 +496,6 @@
C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C01FCF4B08A954540054247B /* Debug */,
C01FCF4C08A954540054247B /* Release */,
);
defaultConfigurationIsVisible = 0;
@@ -569,7 +504,6 @@
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = {
isa = XCConfigurationList;
buildConfigurations = (
C01FCF4F08A954540054247B /* Debug */,
C01FCF5008A954540054247B /* Release */,
);
defaultConfigurationIsVisible = 0;

View File

@@ -5,13 +5,15 @@
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
import sys
sys.path.insert(0, 'py') # for hsutil and hsdocgen
import os
from help import gen
print "Generating help"
os.chdir('help')
os.system('python -u gen.py')
os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_pe_help')
os.chdir('..')
gen.generate()
os.system('/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer help/dupeguru_pe_help')
print "Generating py plugin"
os.chdir('py')

View File

@@ -12,7 +12,6 @@ from dupeguru_pe import app_cocoa as app_pe_cocoa
# Fix py2app imports which chokes on relative imports
from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner
from dupeguru_pe import block, cache, matchbase, data
from hsfs import auto, stats, tree
from hsutil import conflict
class PyApp(NSObject):
@@ -39,7 +38,7 @@ class PyDupeGuru(PyApp):
self.app.scanner.ignore_list.Clear()
def clearPictureCache(self):
self.app.scanner.match_factory.cached_blocks.clear()
self.app.scanner.cached_blocks.clear()
def doScan(self):
return self.app.start_scanning()
@@ -172,10 +171,10 @@ class PyDupeGuru(PyApp):
#---Properties
def setMatchScaled_(self,match_scaled):
self.app.scanner.match_factory.match_scaled = match_scaled
self.app.scanner.match_scaled = match_scaled
def setMinMatchPercentage_(self,percentage):
self.app.scanner.match_factory.threshold = int(percentage)
self.app.scanner.threshold = int(percentage)
def setMixFileKind_(self,mix_file_kind):
self.app.scanner.mix_file_kind = mix_file_kind

File diff suppressed because it is too large Load Diff

1650
pe/cocoa/xib/Preferences.xib Normal file

File diff suppressed because it is too large Load Diff

0
pe/help/__init__.py Normal file
View File

View File

@@ -1,3 +1,16 @@
- date: 2009-12-16
version: 1.8.0
description: |
* Added drag & drop support in the Directories panel. (#9)
* Fixed a bug causing dupeGuru to be confused if a scanned file was moved during the scan. (#72)
* Clarified how directories' state are set by painting a combo box in the state cells. [Windows]
(#76)
* Fixed some crashes. (#78 and #79)
* Dropped Mac OS X Tiger support.
- date: 2009-10-24
version: 1.7.8
description: |
* Fixed a bug sometimes causing some duplicates to be ignored during the scans. (#73)
- date: 2009-10-14
version: 1.7.7
description: |

View File

@@ -5,10 +5,10 @@
# 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
from hsdocgen import generate_help, filters
tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}")
generate_help.main('.', 'dupeguru_pe_help', force_render=True, tix=tix)
def generate(windows=False):
tix = filters.tixgen("https://hardcoded.lighthouseapp.com/projects/31699-dupeguru/tickets/{0}")
basepath = op.dirname(__file__)
generate_help.main(basepath, op.join(basepath, 'dupeguru_pe_help'), force_render=True, tix=tix, windows=windows)

View File

@@ -61,4 +61,14 @@ Enable the [Power Marker](power_marker.htm) mode and click on the Directory colu
* **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK.
* **Mac OS X**: Type "copy" in the "Filter" field in the toolbar.
* Click on **Mark --> Mark All**.
### I tried to send my duplicates to Trash, but dupeGuru is telling me it can't do it. Why? What can I do?
Most of the time, the reason why dupeGuru can't send files to Trash is because of file permissions. You need *write* permissions on files you want to send to Trash. If you're not familiar with the command line, you can use utilities such as [BatChmod](http://macchampion.com/arbysoft/BatchMod) to fix your permissions.
If dupeGuru still gives you troubles after fixing your permissions, there have been some cases where using "Move Marked to..." as a workaround did the trick. So instead of sending your files to Trash, you send them to a temporary folder with the "Move Marked to..." action, and then you delete that temporary folder manually.
If you're trying to delete *iPhoto* pictures, then the reason for the failure is different. The deletion fails because dupeGuru can't communicate with iPhoto. Be aware that for the deletion to work correctly, you're not supposed to play around iPhoto while dupeGuru is working. Also, sometimes, the Applescript system doesn't seem to know where to find iPhoto to launch it. It might help in these cases to launch iPhoto *before* you send your duplicates to Trash.
If all of this fail, [contact HS support](http://www.hardcoded.net/support), we'll figure it out.
</%text>

View File

@@ -7,41 +7,43 @@
# 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
import logging
import plistlib
import re
import objc
from Foundation import *
from AppKit import *
from appscript import app, k
from appscript import app, k, CommandError
from hsutil import job, io
import hsfs as fs
from hsfs import phys, InvalidPath
from hsutil import files
from hsutil import io
from hsutil.str import get_file_ext
from hsutil.path import Path
from hsutil.cocoa import as_fetch
from dupeguru import fs
from dupeguru import app_cocoa, directories
from . import data, matchbase
from . import data
from .cache import string_to_colors, Cache
from .scanner import ScannerPE
mainBundle = NSBundle.mainBundle()
PictureBlocks = mainBundle.classNamed_('PictureBlocks')
assert PictureBlocks is not None
class Photo(phys.File):
INITIAL_INFO = phys.File.INITIAL_INFO.copy()
class Photo(fs.File):
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
INITIAL_INFO.update({
'dimensions': (0,0),
})
HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'tif', 'nef', 'cr2'])
@classmethod
def can_handle(cls, path):
return fs.File.can_handle(path) and get_file_ext(path[-1]) in cls.HANDLED_EXTS
def _read_info(self, field):
super(Photo, self)._read_info(field)
fs.File._read_info(self, field)
if field == 'dimensions':
size = PictureBlocks.getImageSize_(unicode(self.path))
self.dimensions = (size.width, size.height)
@@ -49,7 +51,7 @@ class Photo(phys.File):
def get_blocks(self, block_count_per_side):
try:
blocks = PictureBlocks.getBlocksFromImagePath_blockCount_(unicode(self.path), block_count_per_side)
except Exception, e:
except Exception as e:
raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e)))
if not blocks:
raise IOError('The picture %s could not be read' % unicode(self.path))
@@ -57,89 +59,86 @@ class Photo(phys.File):
class IPhoto(Photo):
def __init__(self, parent, whole_path):
super(IPhoto, self).__init__(parent, whole_path[-1])
self.whole_path = whole_path
def _build_path(self):
return self.whole_path
@property
def display_path(self):
return super(IPhoto, self)._build_path()
return Path(('iPhoto Library', self.name))
def get_iphoto_database_path():
ud = NSUserDefaults.standardUserDefaults()
prefs = ud.persistentDomainForName_('com.apple.iApps')
if prefs is None:
raise directories.InvalidPathError()
if 'iPhotoRecentDatabases' not in prefs:
raise directories.InvalidPathError()
plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0])
return Path(plisturl.path())
class Directory(phys.Directory):
cls_file_class = Photo
cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2')
def _fetch_subitems(self):
subdirs, subfiles = super(Directory,self)._fetch_subitems()
return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts]
class IPhotoLibrary(fs.Directory):
def __init__(self, plistpath):
self.plistpath = plistpath
self.refpath = plistpath[:-1]
# the AlbumData.xml file lives right in the library path
super(IPhotoLibrary, self).__init__(None, 'iPhoto Library')
if not io.exists(plistpath):
raise InvalidPath(self)
def _update_photo(self, photo_data):
def get_iphoto_pictures(plistpath):
if not io.exists(plistpath):
raise InvalidPath(self)
s = io.open(plistpath).read()
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
s = s.replace('\x10', '')
# 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)
result = []
for photo_data in plist['Master Image List'].values():
if photo_data['MediaType'] != 'Image':
return
continue
photo_path = Path(photo_data['ImagePath'])
subpath = photo_path[len(self.refpath):-1]
subdir = self
for element in subpath:
try:
subdir = subdir[element]
except KeyError:
subdir = fs.Directory(subdir, element)
photo = IPhoto(photo_path)
result.append(photo)
return result
class Directories(directories.Directories):
def __init__(self):
directories.Directories.__init__(self, fileclasses=[Photo])
try:
IPhoto(subdir, photo_path)
except fs.AlreadyExistsError:
# it's possible for 2 entries in the plist to point to the same path. Ignore one of them.
pass
self.iphoto_libpath = get_iphoto_database_path()
self.set_state(self.iphoto_libpath[:-1], directories.STATE_EXCLUDED)
except directories.InvalidPathError:
self.iphoto_libpath = None
def update(self):
self.clear()
s = open(unicode(self.plistpath)).read()
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
s = s.replace('\x10', '')
# 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)
for photo_data in plist['Master Image List'].values():
self._update_photo(photo_data)
def _get_files(self, from_path):
if from_path == Path('iPhoto Library'):
if self.iphoto_libpath is None:
return []
is_ref = self.get_state(from_path) == directories.STATE_REFERENCE
photos = get_iphoto_pictures(self.iphoto_libpath)
for photo in photos:
photo.is_ref = is_ref
return photos
else:
return directories.Directories._get_files(self, from_path)
def force_update(self): # Don't update
pass
@staticmethod
def get_subfolders(path):
if path == Path('iPhoto Library'):
return []
else:
return directories.Directories.get_subfolders(path)
def add_path(self, path):
if path == Path('iPhoto Library'):
if path in self:
raise AlreadyThereError()
self._dirs.append(path)
else:
directories.Directories.add_path(self, path)
class DupeGuruPE(app_cocoa.DupeGuru):
def __init__(self):
app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru Picture Edition', appid=5)
self.scanner.match_factory = matchbase.AsyncMatchFactory()
self.directories.dirclass = Directory
self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library()
self.scanner = ScannerPE()
self.directories = Directories()
p = op.join(self.appdata, 'cached_pictures.db')
self.scanner.match_factory.cached_blocks = Cache(p)
def _create_iphoto_library(self):
ud = NSUserDefaults.standardUserDefaults()
prefs = ud.persistentDomainForName_('com.apple.iApps')
if 'iPhotoRecentDatabases' not in prefs:
raise directories.InvalidPathError
plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0])
plistpath = Path(plisturl.path())
return IPhotoLibrary(plistpath)
self.scanner.cached_blocks = Cache(p)
def _do_delete(self, j):
def op(dupe):
@@ -150,12 +149,18 @@ class DupeGuruPE(app_cocoa.DupeGuru):
self.path2iphoto = {}
if any(isinstance(dupe, IPhoto) for dupe in marked):
j = j.start_subjob([6, 4], "Probing iPhoto. Don\'t touch it during the operation!")
a = app('iPhoto')
a.activate(timeout=0)
a.select(a.photo_library_album(timeout=0), timeout=0)
photos = as_fetch(a.photo_library_album().photos, k.item)
for photo in j.iter_with_progress(photos):
self.path2iphoto[unicode(photo.image_path(timeout=0))] = photo
try:
a = app('iPhoto')
a.activate(timeout=0)
a.select(a.photo_library_album(timeout=0), timeout=0)
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
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)
del self.path2iphoto
@@ -164,50 +169,33 @@ class DupeGuruPE(app_cocoa.DupeGuru):
if isinstance(dupe, IPhoto):
if unicode(dupe.path) in self.path2iphoto:
photo = self.path2iphoto[unicode(dupe.path)]
a = app('iPhoto')
a.remove(photo, timeout=0)
return True
try:
a = app('iPhoto')
a.remove(photo, timeout=0)
return True
except (CommandError, RuntimeError):
return False
else:
logging.warning("Could not find photo {0} in iPhoto Library", unicode(dupe.path))
logging.warning(u"Could not find photo %s in iPhoto Library", unicode(dupe.path))
return False
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'))
for d in self.directories:
if isinstance(d, IPhotoLibrary):
d.update()
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
def _get_file(self, str_path):
p = Path(str_path)
for d in self.directories:
result = None
if p in d.path:
result = d.find_path(p[d.path:])
if isinstance(d, IPhotoLibrary) and p in d.refpath:
result = d.find_path(p[d.refpath:])
if result is not None:
return result
def add_directory(self, d):
result = app_cocoa.DupeGuru.add_directory(self, d)
if (result == 0) and (d == 'iPhoto Library'):
[iphotolib] = [dir for dir in self.directories if dir.path == d]
iphotolib.update()
return result
if (self.directories.iphoto_libpath is not None) and (p in self.directories.iphoto_libpath[:-1]):
return IPhoto(p)
return app_cocoa.DupeGuru._get_file(self, str_path)
def copy_or_move(self, dupe, copy, destination, dest_type):
if isinstance(dupe, IPhoto):
copy = True
return app_cocoa.DupeGuru.copy_or_move(self, dupe, copy, destination, dest_type)
def start_scanning(self):
for directory in self.directories:
if isinstance(directory, IPhotoLibrary):
self.directories.set_state(directory.refpath, directories.STATE_EXCLUDED)
return app_cocoa.DupeGuru.start_scanning(self)
def selected_dupe_path(self):
if not self.selected_dupes:
return None
@@ -217,5 +205,7 @@ class DupeGuruPE(app_cocoa.DupeGuru):
if not self.selected_dupes:
return None
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
if ref is self.selected_dupes[0]: # we don't want the same pic to be displayed on both sides
return None
return ref.path

View File

@@ -20,58 +20,42 @@ from .block import avgdiff, DifferentBlockCountError, NoBlocksError
from .cache import Cache
MIN_ITERATIONS = 3
BLOCK_COUNT_PER_SIDE = 15
# Enough so that we're sure that the main thread will not wait after a result.get() call
# cpucount*2 should be enough to be sure that the spawned process will not wait after the results
# collection made by the main process.
RESULTS_QUEUE_LIMIT = multiprocessing.cpu_count() * 2
def get_match(first,second,percentage):
def prepare_pictures(pictures, cached_blocks, j=job.nulljob):
# The MemoryError handlers in there use logging without first caring about whether or not
# there is enough memory left to carry on the operation because it is assumed that the
# MemoryError happens when trying to read an image file, which is freed from memory by the
# time that MemoryError is raised.
prepared = [] # only pictures for which there was no error getting blocks
try:
for picture in j.iter_with_progress(pictures, 'Analyzed %d/%d pictures'):
picture.dimensions
picture.unicode_path = unicode(picture.path)
try:
if picture.unicode_path not in cached_blocks:
blocks = picture.get_blocks(BLOCK_COUNT_PER_SIDE)
cached_blocks[picture.unicode_path] = blocks
prepared.append(picture)
except IOError as e:
logging.warning(unicode(e))
except MemoryError:
logging.warning(u'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:
logging.warning('Ran out of memory while preparing pictures')
return prepared
def get_match(first, second, percentage):
if percentage < 0:
percentage = 0
return Match(first,second,percentage)
class MatchFactory(object):
cached_blocks = None
block_count_per_side = 15
threshold = 75
match_scaled = False
def _do_getmatches(self, files, j):
raise NotImplementedError()
def getmatches(self, files, j=job.nulljob):
# The MemoryError handlers in there use logging without first caring about whether or not
# there is enough memory left to carry on the operation because it is assumed that the
# MemoryError happens when trying to read an image file, which is freed from memory by the
# time that MemoryError is raised.
j = j.start_subjob([3, 7])
logging.info('Preparing %d files' % len(files))
prepared = self.prepare_files(files, j)
logging.info('Finished preparing %d files' % len(prepared))
return self._do_getmatches(prepared, j)
def prepare_files(self, files, j=job.nulljob):
prepared = [] # only files for which there was no error getting blocks
try:
for picture in j.iter_with_progress(files, 'Analyzed %d/%d pictures'):
picture.dimensions
picture.unicode_path = unicode(picture.path)
try:
if picture.unicode_path not in self.cached_blocks:
blocks = picture.get_blocks(self.block_count_per_side)
self.cached_blocks[picture.unicode_path] = blocks
prepared.append(picture)
except IOError as e:
logging.warning(unicode(e))
except MemoryError:
logging.warning(u'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:
logging.warning('Ran out of memory while preparing files')
return prepared
return Match(first, second, percentage)
def async_compare(ref_id, other_ids, dbname, threshold):
cache = Cache(dbname, threaded=False)
@@ -89,53 +73,55 @@ def async_compare(ref_id, other_ids, dbname, threshold):
results.append((ref_id, other_id, percentage))
cache.con.close()
return results
class AsyncMatchFactory(MatchFactory):
def _do_getmatches(self, pictures, j):
def empty_out_queue(queue, into):
try:
while True:
into.append(queue.get(block=False))
except Empty:
pass
j = j.start_subjob([9, 1], 'Preparing for matching')
cache = self.cached_blocks
id2picture = {}
dimensions2pictures = defaultdict(set)
for picture in pictures:
try:
picture.cache_id = cache.get_id(picture.unicode_path)
id2picture[picture.cache_id] = picture
if not self.match_scaled:
dimensions2pictures[picture.dimensions].add(picture)
except ValueError:
pass
pictures = [p for p in pictures if hasattr(p, 'cache_id')]
pool = multiprocessing.Pool()
async_results = []
matches = []
pictures_copy = set(pictures)
for ref in j.iter_with_progress(pictures, 'Matched %d/%d pictures'):
others = pictures_copy if self.match_scaled else dimensions2pictures[ref.dimensions]
others.remove(ref)
if others:
cache_ids = [f.cache_id for f in others]
args = (ref.cache_id, cache_ids, self.cached_blocks.dbname, self.threshold)
async_results.append(pool.apply_async(async_compare, args))
if len(async_results) > RESULTS_QUEUE_LIMIT:
result = async_results.pop(0)
matches.extend(result.get())
result = []
for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10):
ref = id2picture[ref_id]
other = id2picture[other_id]
if percentage == 100 and ref.md5 != other.md5:
percentage = 99
if percentage >= self.threshold:
result.append(get_match(ref, other, percentage))
return result
def getmatches(pictures, cached_blocks, 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, cached_blocks, j)
j = j.start_subjob([9, 1], 'Preparing for matching')
cache = cached_blocks
id2picture = {}
dimensions2pictures = defaultdict(set)
for picture in pictures:
try:
picture.cache_id = cache.get_id(picture.unicode_path)
id2picture[picture.cache_id] = picture
if not match_scaled:
dimensions2pictures[picture.dimensions].add(picture)
except ValueError:
pass
pictures = [p for p in pictures if hasattr(p, 'cache_id')]
pool = multiprocessing.Pool()
async_results = []
matches = []
pictures_copy = set(pictures)
for ref in j.iter_with_progress(pictures, 'Matched %d/%d pictures'):
others = pictures_copy if match_scaled else dimensions2pictures[ref.dimensions]
others.remove(ref)
if others:
cache_ids = [f.cache_id for f in others]
args = (ref.cache_id, cache_ids, cached_blocks.dbname, threshold)
async_results.append(pool.apply_async(async_compare, args))
if len(async_results) > RESULTS_QUEUE_LIMIT:
result = async_results.pop(0)
matches.extend(result.get())
for result in async_results: # process the rest of the results
matches.extend(result.get())
result = []
for ref_id, other_id, percentage in j.iter_with_progress(matches, 'Verified %d/%d matches', every=10):
ref = id2picture[ref_id]
other = id2picture[other_id]
if percentage == 100 and ref.md5 != other.md5:
percentage = 99
if percentage >= threshold:
result.append(get_match(ref, other, percentage))
return result
multiprocessing.freeze_support()

22
pe/py/scanner.py Normal file
View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Created By: Virgil Dupras
# Created On: 2009-10-18
# $Id$
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
#
# This software is licensed under the "HS" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/hs_license
from dupeguru.scanner import Scanner
from . import matchbase
class ScannerPE(Scanner):
cached_blocks = None
match_scaled = False
threshold = 75
def _getmatches(self, files, j):
return matchbase.getmatches(files, self.cached_blocks, self.threshold, self.match_scaled, j)

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