mirror of
https://github.com/arsenetar/dupeguru.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6724e710d8 | ||
|
|
9729e05fe8 | ||
|
|
686c60b83b | ||
|
|
5fe11f3b3a | ||
|
|
30d29c6b34 | ||
|
|
fbfb16e77a | ||
|
|
7aea384f86 | ||
|
|
78bef5c3c6 | ||
|
|
5c8d90a57c | ||
|
|
13dc9ff76d | ||
|
|
eb3645d493 | ||
|
|
0a06e52d65 | ||
|
|
e004c0c2d4 | ||
|
|
6d5f6a0c3c | ||
|
|
024e3c380f | ||
|
|
06859fe9cd | ||
|
|
6392d08584 | ||
|
|
80a5290bc8 | ||
|
|
6b30c88fba | ||
|
|
6adce9bf03 | ||
|
|
3c90ad81a7 | ||
|
|
484529add0 | ||
|
|
600c7906a4 | ||
|
|
f070e90347 | ||
|
|
88127d8b8d | ||
|
|
607ab86188 | ||
|
|
c936f9ccc6 | ||
|
|
d4d8917956 | ||
|
|
89bce95c27 | ||
|
|
f0a38a2b3f | ||
|
|
911521d8e0 | ||
|
|
b25c1c3a3b | ||
|
|
37a40040b3 | ||
|
|
25dadc83eb | ||
|
|
b8c11b5aae | ||
|
|
a3ab314378 | ||
|
|
794192835d | ||
|
|
385768a69b | ||
|
|
a281931b16 | ||
|
|
085311d559 | ||
|
|
4d7f032889 | ||
|
|
cf44c93013 | ||
|
|
787cbcd01f | ||
|
|
b2b316b642 | ||
|
|
49165125e4 | ||
|
|
54ac0fd19e | ||
|
|
0aff7f16e5 | ||
|
|
f9abc3b35d | ||
|
|
b167a51243 | ||
|
|
371cdda911 | ||
|
|
11977c6533 | ||
|
|
7228adf433 |
@@ -9,17 +9,22 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "RecentDirectories.h"
|
#import "RecentDirectories.h"
|
||||||
#import "PyDupeGuru.h"
|
#import "PyDupeGuru.h"
|
||||||
|
#import "ResultWindow.h"
|
||||||
|
#import "DetailsPanel.h"
|
||||||
|
|
||||||
@interface AppDelegateBase : NSObject
|
@interface AppDelegateBase : NSObject
|
||||||
{
|
{
|
||||||
IBOutlet PyDupeGuruBase *py;
|
IBOutlet PyDupeGuruBase *py;
|
||||||
IBOutlet RecentDirectories *recentDirectories;
|
IBOutlet RecentDirectories *recentDirectories;
|
||||||
IBOutlet NSMenuItem *unlockMenuItem;
|
IBOutlet NSMenuItem *unlockMenuItem;
|
||||||
|
IBOutlet ResultWindowBase *result;
|
||||||
|
|
||||||
NSString *_appName;
|
NSString *_appName;
|
||||||
|
DetailsPanelBase *_detailsPanel;
|
||||||
}
|
}
|
||||||
- (IBAction)unlockApp:(id)sender;
|
- (IBAction)unlockApp:(id)sender;
|
||||||
|
|
||||||
- (PyDupeGuruBase *)py;
|
- (PyDupeGuruBase *)py;
|
||||||
- (RecentDirectories *)recentDirectories;
|
- (RecentDirectories *)recentDirectories;
|
||||||
|
- (DetailsPanelBase *)detailsPanel; // Virtual
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
RegistrationInterface *ri = [[RegistrationInterface alloc] initWithApp:[self py] name:_appName limitDescription:LIMIT_DESC];
|
RegistrationInterface *ri = [[RegistrationInterface alloc] initWithApp:[self py] name:_appName limitDescription:LIMIT_DESC];
|
||||||
if ([ri enterCode] == NSOKButton)
|
if ([ri enterCode] == NSOKButton)
|
||||||
{
|
{
|
||||||
NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@",_appName];
|
NSString *menuTitle = [NSString stringWithFormat:@"Thanks for buying %@!",_appName];
|
||||||
[unlockMenuItem setTitle:menuTitle];
|
[unlockMenuItem setTitle:menuTitle];
|
||||||
}
|
}
|
||||||
[ri release];
|
[ri release];
|
||||||
@@ -35,4 +35,25 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
|
|
||||||
- (PyDupeGuruBase *)py { return py; }
|
- (PyDupeGuruBase *)py { return py; }
|
||||||
- (RecentDirectories *)recentDirectories { return recentDirectories; }
|
- (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
|
@end
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (id)initWithPy:(PyApp *)aPy;
|
- (id)initWithPy:(PyApp *)aPy;
|
||||||
|
|
||||||
- (void)refresh;
|
- (void)refresh;
|
||||||
|
- (void)toggleVisibility;
|
||||||
|
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
- (void)duplicateSelectionChanged:(NSNotification *)aNotification;
|
- (void)duplicateSelectionChanged:(NSNotification *)aNotification;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
@implementation DetailsPanelBase
|
@implementation DetailsPanelBase
|
||||||
- (id)initWithPy:(PyApp *)aPy
|
- (id)initWithPy:(PyApp *)aPy
|
||||||
{
|
{
|
||||||
self = [super initWithWindowNibName:@"Details"];
|
self = [super initWithWindowNibName:@"DetailsPanel"];
|
||||||
[self window]; //So the detailsTable is initialized.
|
[self window]; //So the detailsTable is initialized.
|
||||||
[detailsTable setPy:aPy];
|
[detailsTable setPy:aPy];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil];
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil];
|
||||||
@@ -24,6 +24,17 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[detailsTable reloadData];
|
[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 */
|
/* Notifications */
|
||||||
- (void)duplicateSelectionChanged:(NSNotification *)aNotification
|
- (void)duplicateSelectionChanged:(NSNotification *)aNotification
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,10 +11,19 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import "Outline.h"
|
#import "Outline.h"
|
||||||
#import "PyDupeGuru.h"
|
#import "PyDupeGuru.h"
|
||||||
|
|
||||||
|
@interface DirectoryOutline : OutlineView
|
||||||
|
{
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@protocol DirectoryOutlineDelegate
|
||||||
|
- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory;
|
||||||
|
@end
|
||||||
|
|
||||||
@interface DirectoryPanelBase : NSWindowController
|
@interface DirectoryPanelBase : NSWindowController
|
||||||
{
|
{
|
||||||
IBOutlet NSPopUpButton *addButtonPopUp;
|
IBOutlet NSPopUpButton *addButtonPopUp;
|
||||||
IBOutlet OutlineView *directories;
|
IBOutlet DirectoryOutline *directories;
|
||||||
IBOutlet NSButton *removeButton;
|
IBOutlet NSButton *removeButton;
|
||||||
|
|
||||||
PyDupeGuruBase *_py;
|
PyDupeGuruBase *_py;
|
||||||
|
|||||||
@@ -11,10 +11,52 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import "Utils.h"
|
#import "Utils.h"
|
||||||
#import "AppDelegate.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
|
@implementation DirectoryPanelBase
|
||||||
- (id)initWithParentApp:(id)aParentApp
|
- (id)initWithParentApp:(id)aParentApp
|
||||||
{
|
{
|
||||||
self = [super initWithWindowNibName:@"Directories"];
|
self = [super initWithWindowNibName:@"DirectoryPanel"];
|
||||||
[self window];
|
[self window];
|
||||||
AppDelegateBase *app = aParentApp;
|
AppDelegateBase *app = aParentApp;
|
||||||
_py = [app py];
|
_py = [app py];
|
||||||
@@ -104,10 +146,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
|
|
||||||
- (IBAction)toggleVisible:(id)sender
|
- (IBAction)toggleVisible:(id)sender
|
||||||
{
|
{
|
||||||
if ([[self window] isVisible])
|
[[self window] makeKeyAndOrderFront:nil];
|
||||||
[[self window] close];
|
|
||||||
else
|
|
||||||
[[self window] makeKeyAndOrderFront:nil];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public */
|
/* Public */
|
||||||
@@ -154,6 +193,11 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
|
|
||||||
/* Delegate */
|
/* Delegate */
|
||||||
|
|
||||||
|
- (void)outlineView:(NSOutlineView *)outlineView addDirectory:(NSString *)directory
|
||||||
|
{
|
||||||
|
[self addDirectory:directory];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
||||||
{
|
{
|
||||||
OVNode *node = item;
|
OVNode *node = item;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "Outline.h"
|
#import "Outline.h"
|
||||||
#import "DirectoryPanel.h"
|
|
||||||
#import "PyDupeGuru.h"
|
#import "PyDupeGuru.h"
|
||||||
|
|
||||||
@interface MatchesView : OutlineView
|
@interface MatchesView : OutlineView
|
||||||
@@ -20,29 +19,29 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
@protected
|
@protected
|
||||||
IBOutlet PyDupeGuruBase *py;
|
IBOutlet PyDupeGuruBase *py;
|
||||||
IBOutlet id app;
|
IBOutlet id app;
|
||||||
IBOutlet NSView *actionMenuView;
|
|
||||||
IBOutlet NSSegmentedControl *deltaSwitch;
|
IBOutlet NSSegmentedControl *deltaSwitch;
|
||||||
IBOutlet NSView *deltaSwitchView;
|
|
||||||
IBOutlet NSView *filterFieldView;
|
|
||||||
IBOutlet MatchesView *matches;
|
IBOutlet MatchesView *matches;
|
||||||
IBOutlet NSSegmentedControl *pmSwitch;
|
IBOutlet NSSegmentedControl *pmSwitch;
|
||||||
IBOutlet NSView *pmSwitchView;
|
|
||||||
IBOutlet NSTextField *stats;
|
IBOutlet NSTextField *stats;
|
||||||
|
IBOutlet NSMenu *columnsMenu;
|
||||||
|
|
||||||
BOOL _powerMode;
|
BOOL _powerMode;
|
||||||
BOOL _displayDelta;
|
BOOL _displayDelta;
|
||||||
|
NSMutableArray *_resultColumns;
|
||||||
|
NSWindowController *preferencesPanel;
|
||||||
}
|
}
|
||||||
/* Override */
|
|
||||||
- (NSString *)logoImageName;
|
|
||||||
|
|
||||||
/* Helpers */
|
/* Helpers */
|
||||||
|
- (void)fillColumnsMenu;
|
||||||
|
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn;
|
||||||
- (NSArray *)getColumnsOrder;
|
- (NSArray *)getColumnsOrder;
|
||||||
- (NSDictionary *)getColumnsWidth;
|
- (NSDictionary *)getColumnsWidth;
|
||||||
- (NSArray *)getSelected:(BOOL)aDupesOnly;
|
- (NSArray *)getSelected:(BOOL)aDupesOnly;
|
||||||
- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly;
|
- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly;
|
||||||
|
- (void)initResultColumns;
|
||||||
- (void)updatePySelection;
|
- (void)updatePySelection;
|
||||||
- (void)performPySelection:(NSArray *)aIndexPaths;
|
- (void)performPySelection:(NSArray *)aIndexPaths;
|
||||||
- (void)refreshStats;
|
- (void)refreshStats;
|
||||||
|
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth;
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
- (IBAction)changeDelta:(id)sender;
|
- (IBAction)changeDelta:(id)sender;
|
||||||
@@ -52,7 +51,11 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (IBAction)expandAll:(id)sender;
|
- (IBAction)expandAll:(id)sender;
|
||||||
- (IBAction)exportToXHTML:(id)sender;
|
- (IBAction)exportToXHTML:(id)sender;
|
||||||
- (IBAction)moveMarked:(id)sender;
|
- (IBAction)moveMarked:(id)sender;
|
||||||
|
- (IBAction)resetColumnsToDefault:(id)sender;
|
||||||
|
- (IBAction)showPreferencesPanel:(id)sender;
|
||||||
- (IBAction)switchSelected:(id)sender;
|
- (IBAction)switchSelected:(id)sender;
|
||||||
|
- (IBAction)toggleColumn:(id)sender;
|
||||||
|
- (IBAction)toggleDetailsPanel:(id)sender;
|
||||||
- (IBAction)togglePowerMarker:(id)sender;
|
- (IBAction)togglePowerMarker:(id)sender;
|
||||||
|
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
|
|||||||
@@ -14,15 +14,6 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import "Consts.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
|
@implementation MatchesView
|
||||||
- (void)keyDown:(NSEvent *)theEvent
|
- (void)keyDown:(NSEvent *)theEvent
|
||||||
{
|
{
|
||||||
@@ -60,6 +51,9 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (void)awakeFromNib
|
- (void)awakeFromNib
|
||||||
{
|
{
|
||||||
[self window];
|
[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(registrationRequired:) name:RegistrationRequired object:nil];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil];
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobStarted:) name:JobStarted object:nil];
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(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];
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsUpdated:) name:ResultsUpdatedNotification object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Virtual */
|
- (void)dealloc
|
||||||
- (NSString *)logoImageName
|
|
||||||
{
|
{
|
||||||
return @"dg_logo32";
|
[preferencesPanel release];
|
||||||
|
[super dealloc];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Helpers */
|
/* 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.
|
//Returns an array of identifiers, in order.
|
||||||
- (NSArray *)getColumnsOrder
|
- (NSArray *)getColumnsOrder
|
||||||
{
|
{
|
||||||
@@ -135,6 +158,40 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
return r;
|
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
|
- (void)updatePySelection
|
||||||
{
|
{
|
||||||
NSArray *selection;
|
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
|
- (IBAction)switchSelected:(id)sender
|
||||||
{
|
{
|
||||||
// It might look like a complicated way to get the length of the current dupe list on the py side
|
// 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];
|
[[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
|
- (IBAction)togglePowerMarker:(id)sender
|
||||||
{
|
{
|
||||||
if ([pmSwitch selectedSegment] == 1)
|
if ([pmSwitch selectedSegment] == 1)
|
||||||
@@ -353,107 +445,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (void)resultsUpdated:(NSNotification *)aNotification
|
- (void)resultsUpdated:(NSNotification *)aNotification
|
||||||
{
|
{
|
||||||
[matches invalidateBuffers];
|
[matches invalidateBuffers];
|
||||||
}
|
[matches invalidateMarkings];
|
||||||
|
|
||||||
/* 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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
|
- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
|
||||||
|
|||||||
1083
base/cocoa/xib/DetailsPanel.xib
Normal file
1083
base/cocoa/xib/DetailsPanel.xib
Normal file
File diff suppressed because it is too large
Load Diff
1503
base/cocoa/xib/DirectoryPanel.xib
Normal file
1503
base/cocoa/xib/DirectoryPanel.xib
Normal file
File diff suppressed because it is too large
Load Diff
4618
base/cocoa/xib/MainMenu.xib
Normal file
4618
base/cocoa/xib/MainMenu.xib
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,13 @@ import os
|
|||||||
import os.path as op
|
import os.path as op
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from hsutil import job, io, files
|
from hsutil import io, files
|
||||||
from hsutil.path import Path
|
from hsutil.path import Path
|
||||||
from hsutil.reg import RegistrableApplication, RegistrationRequired
|
from hsutil.reg import RegistrableApplication, RegistrationRequired
|
||||||
from hsutil.misc import flatten, first
|
from hsutil.misc import flatten, first
|
||||||
from hsutil.str import escape
|
from hsutil.str import escape
|
||||||
|
|
||||||
from . import directories, results, scanner, export
|
from . import directories, results, scanner, export, fs
|
||||||
|
|
||||||
JOB_SCAN = 'job_scan'
|
JOB_SCAN = 'job_scan'
|
||||||
JOB_LOAD = 'job_load'
|
JOB_LOAD = 'job_load'
|
||||||
@@ -70,12 +70,10 @@ class DupeGuru(RegistrableApplication):
|
|||||||
|
|
||||||
def _do_delete_dupe(self, dupe):
|
def _do_delete_dupe(self, dupe):
|
||||||
if not io.exists(dupe.path):
|
if not io.exists(dupe.path):
|
||||||
dupe.parent = None
|
|
||||||
return True
|
return True
|
||||||
self._recycle_dupe(dupe)
|
self._recycle_dupe(dupe)
|
||||||
self.clean_empty_dirs(dupe.path[:-1])
|
self.clean_empty_dirs(dupe.path[:-1])
|
||||||
if not io.exists(dupe.path):
|
if not io.exists(dupe.path):
|
||||||
dupe.parent = None
|
|
||||||
return True
|
return True
|
||||||
logging.warning("Could not send {0} to trash.".format(unicode(dupe.path)))
|
logging.warning("Could not send {0} to trash.".format(unicode(dupe.path)))
|
||||||
return False
|
return False
|
||||||
@@ -98,13 +96,8 @@ class DupeGuru(RegistrableApplication):
|
|||||||
return ['---'] * len(self.data.COLUMNS)
|
return ['---'] * len(self.data.COLUMNS)
|
||||||
|
|
||||||
def _get_file(self, str_path):
|
def _get_file(self, str_path):
|
||||||
p = Path(str_path)
|
path = Path(str_path)
|
||||||
for d in self.directories:
|
return fs.get_file(path, self.directories.fileclasses)
|
||||||
if p not in d.path:
|
|
||||||
continue
|
|
||||||
result = d.find_path(p[d.path:])
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _recycle_dupe(dupe):
|
def _recycle_dupe(dupe):
|
||||||
@@ -150,7 +143,7 @@ class DupeGuru(RegistrableApplication):
|
|||||||
2 = absolute re-creation.
|
2 = absolute re-creation.
|
||||||
"""
|
"""
|
||||||
source_path = dupe.path
|
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)
|
dest_path = Path(destination)
|
||||||
if dest_type == 2:
|
if dest_type == 2:
|
||||||
dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename
|
dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename
|
||||||
|
|||||||
@@ -12,13 +12,12 @@ from AppKit import *
|
|||||||
import logging
|
import logging
|
||||||
import os.path as op
|
import os.path as op
|
||||||
|
|
||||||
import hsfs as fs
|
|
||||||
from hsutil import io, cocoa, job
|
from hsutil import io, cocoa, job
|
||||||
from hsutil.cocoa import install_exception_hook
|
from hsutil.cocoa import install_exception_hook
|
||||||
from hsutil.misc import stripnone
|
from hsutil.misc import stripnone
|
||||||
from hsutil.reg import RegistrationRequired
|
from hsutil.reg import RegistrationRequired
|
||||||
|
|
||||||
import app, data
|
from . import app, fs
|
||||||
|
|
||||||
JOBID2TITLE = {
|
JOBID2TITLE = {
|
||||||
app.JOB_SCAN: "Scanning for duplicates",
|
app.JOB_SCAN: "Scanning for duplicates",
|
||||||
@@ -43,8 +42,6 @@ class DupeGuru(app.DupeGuru):
|
|||||||
logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s')
|
logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s')
|
||||||
logging.debug('started in debug mode')
|
logging.debug('started in debug mode')
|
||||||
install_exception_hook()
|
install_exception_hook()
|
||||||
if data_module is None:
|
|
||||||
data_module = data
|
|
||||||
appsupport = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0]
|
appsupport = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, True)[0]
|
||||||
appdata = op.join(appsupport, appdata_subdir)
|
appdata = op.join(appsupport, appdata_subdir)
|
||||||
app.DupeGuru.__init__(self, data_module, appdata, appid)
|
app.DupeGuru.__init__(self, data_module, appdata, appid)
|
||||||
@@ -56,18 +53,10 @@ class DupeGuru(app.DupeGuru):
|
|||||||
#--- Override
|
#--- Override
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _recycle_dupe(dupe):
|
def _recycle_dupe(dupe):
|
||||||
if not io.exists(dupe.path):
|
directory = unicode(dupe.path[:-1])
|
||||||
dupe.parent = None
|
|
||||||
return True
|
|
||||||
directory = unicode(dupe.parent.path)
|
|
||||||
filename = dupe.name
|
filename = dupe.name
|
||||||
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
|
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
|
||||||
NSWorkspaceRecycleOperation, directory, '', [filename])
|
NSWorkspaceRecycleOperation, directory, '', [filename], None)
|
||||||
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
|
|
||||||
|
|
||||||
def _start_job(self, jobid, func):
|
def _start_job(self, jobid, func):
|
||||||
try:
|
try:
|
||||||
@@ -91,15 +80,15 @@ class DupeGuru(app.DupeGuru):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
return (None,None)
|
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:
|
if not node_path:
|
||||||
return curr_dir
|
return curr_path
|
||||||
if curr_dir is not None:
|
current_index = node_path[0]
|
||||||
l = curr_dir.dirs
|
if curr_path is None:
|
||||||
|
curr_path = self.directories[current_index]
|
||||||
else:
|
else:
|
||||||
l = self.directories
|
curr_path = self.directories.get_subfolders(curr_path)[current_index]
|
||||||
d = l[node_path[0]]
|
return self.get_folder_path(node_path[1:], curr_path)
|
||||||
return self.GetDirectory(node_path[1:],d)
|
|
||||||
|
|
||||||
def RefreshDetailsTable(self,dupe,group):
|
def RefreshDetailsTable(self,dupe,group):
|
||||||
l1 = self._get_display_info(dupe, group, False)
|
l1 = self._get_display_info(dupe, group, False)
|
||||||
@@ -146,13 +135,13 @@ class DupeGuru(app.DupeGuru):
|
|||||||
def RemoveSelected(self):
|
def RemoveSelected(self):
|
||||||
self.results.remove_duplicates(self.selected_dupes)
|
self.results.remove_duplicates(self.selected_dupes)
|
||||||
|
|
||||||
def RenameSelected(self,newname):
|
def RenameSelected(self, newname):
|
||||||
try:
|
try:
|
||||||
d = self.selected_dupes[0]
|
d = self.selected_dupes[0]
|
||||||
d = d.move(d.parent,newname)
|
d.rename(newname)
|
||||||
return True
|
return True
|
||||||
except (IndexError,fs.FSError),e:
|
except (IndexError, fs.FSError) as e:
|
||||||
logging.warning("dupeGuru Warning: %s" % str(e))
|
logging.warning("dupeGuru Warning: %s" % unicode(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def RevealSelected(self):
|
def RevealSelected(self):
|
||||||
@@ -214,9 +203,9 @@ class DupeGuru(app.DupeGuru):
|
|||||||
self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes))
|
self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes))
|
||||||
]
|
]
|
||||||
|
|
||||||
def SetDirectoryState(self,node_path,state):
|
def SetDirectoryState(self, node_path, state):
|
||||||
d = self.GetDirectory(node_path)
|
p = self.get_folder_path(node_path)
|
||||||
self.directories.set_state(d.path,state)
|
self.directories.set_state(p, state)
|
||||||
|
|
||||||
def sort_dupes(self,key,asc):
|
def sort_dupes(self,key,asc):
|
||||||
self.results.sort_dupes(key,asc,self.display_delta_values)
|
self.results.sort_dupes(key,asc,self.display_delta_values)
|
||||||
@@ -245,8 +234,12 @@ class DupeGuru(app.DupeGuru):
|
|||||||
return [len(g.dupes) for g in self.results.groups]
|
return [len(g.dupes) for g in self.results.groups]
|
||||||
elif tag == 1: #Directories
|
elif tag == 1: #Directories
|
||||||
try:
|
try:
|
||||||
dirs = self.GetDirectory(node_path).dirs if node_path else self.directories
|
if node_path:
|
||||||
return [d.dircount for d in dirs]
|
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
|
except IndexError: # node_path out of range
|
||||||
return []
|
return []
|
||||||
else: #Power Marker
|
else: #Power Marker
|
||||||
@@ -270,8 +263,9 @@ class DupeGuru(app.DupeGuru):
|
|||||||
return result
|
return result
|
||||||
elif tag == 1: #Directories
|
elif tag == 1: #Directories
|
||||||
try:
|
try:
|
||||||
d = self.GetDirectory(node_path)
|
path = self.get_folder_path(node_path)
|
||||||
return [d.name, self.directories.get_state(d.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
|
except IndexError: # node_path out of range
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
|
|
||||||
@@ -40,63 +40,3 @@ def format_dupe_count(c):
|
|||||||
|
|
||||||
def cmp_value(value):
|
def cmp_value(value):
|
||||||
return value.lower() if isinstance(value, basestring) else 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']))
|
|
||||||
|
|||||||
@@ -9,11 +9,12 @@
|
|||||||
|
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
from hsfs import phys
|
from hsutil import io
|
||||||
import hsfs as fs
|
|
||||||
from hsutil.files import FileOrPath
|
from hsutil.files import FileOrPath
|
||||||
from hsutil.path import Path
|
from hsutil.path import Path
|
||||||
|
|
||||||
|
from . import fs
|
||||||
|
|
||||||
(STATE_NORMAL,
|
(STATE_NORMAL,
|
||||||
STATE_REFERENCE,
|
STATE_REFERENCE,
|
||||||
STATE_EXCLUDED) = range(3)
|
STATE_EXCLUDED) = range(3)
|
||||||
@@ -26,15 +27,14 @@ class InvalidPathError(Exception):
|
|||||||
|
|
||||||
class Directories(object):
|
class Directories(object):
|
||||||
#---Override
|
#---Override
|
||||||
def __init__(self):
|
def __init__(self, fileclasses=[fs.File]):
|
||||||
self._dirs = []
|
self._dirs = []
|
||||||
self.states = {}
|
self.states = {}
|
||||||
self.dirclass = phys.Directory
|
self.fileclasses = fileclasses
|
||||||
self.special_dirclasses = {}
|
|
||||||
|
|
||||||
def __contains__(self,path):
|
def __contains__(self, path):
|
||||||
for d in self._dirs:
|
for p in self._dirs:
|
||||||
if path in d.path:
|
if path in p:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -53,8 +53,7 @@ class Directories(object):
|
|||||||
if path[-1].startswith('.'): # hidden
|
if path[-1].startswith('.'): # hidden
|
||||||
return STATE_EXCLUDED
|
return STATE_EXCLUDED
|
||||||
|
|
||||||
def _get_files(self, from_dir):
|
def _get_files(self, from_path):
|
||||||
from_path = from_dir.path
|
|
||||||
state = self.get_state(from_path)
|
state = self.get_state(from_path)
|
||||||
if state == STATE_EXCLUDED:
|
if state == STATE_EXCLUDED:
|
||||||
# Recursively get files from folders with lots of subfolder is expensive. However, there
|
# 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
|
# 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):
|
if not any(p[:len(from_path)] == from_path for p in self.states):
|
||||||
return
|
return
|
||||||
result = []
|
try:
|
||||||
for subdir in from_dir.dirs:
|
filepaths = set()
|
||||||
for file in self._get_files(subdir):
|
if state != STATE_EXCLUDED:
|
||||||
yield file
|
for file in fs.get_files(from_path, fileclasses=self.fileclasses):
|
||||||
if state != STATE_EXCLUDED:
|
file.is_ref = state == STATE_REFERENCE
|
||||||
for file in from_dir.files:
|
filepaths.add(file.path)
|
||||||
file.is_ref = state == STATE_REFERENCE
|
yield file
|
||||||
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
|
#---Public
|
||||||
def add_path(self, path):
|
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.
|
under it will be removed. Can also raise InvalidPathError if 'path' does not exist.
|
||||||
"""
|
"""
|
||||||
if path in self:
|
if path in self:
|
||||||
raise AlreadyThereError
|
raise AlreadyThereError()
|
||||||
self._dirs = [d for d in self._dirs if d.path not in path]
|
if not io.exists(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 InvalidPathError()
|
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):
|
def get_files(self):
|
||||||
"""Returns a list of all files that are not excluded.
|
"""Returns a list of all files that are not excluded.
|
||||||
|
|
||||||
Returned files also have their 'is_ref' attr set.
|
Returned files also have their 'is_ref' attr set.
|
||||||
"""
|
"""
|
||||||
for d in self._dirs:
|
for path in self._dirs:
|
||||||
d.force_update()
|
for file in self._get_files(path):
|
||||||
try:
|
yield file
|
||||||
for file in self._get_files(d):
|
|
||||||
yield file
|
|
||||||
except fs.InvalidPath:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_state(self, path):
|
def get_state(self, path):
|
||||||
"""Returns the state of 'path' (One of the STATE_* const.)
|
"""Returns the state of 'path' (One of the STATE_* const.)
|
||||||
@@ -123,8 +130,8 @@ class Directories(object):
|
|||||||
doc = xml.dom.minidom.parse(infile)
|
doc = xml.dom.minidom.parse(infile)
|
||||||
except:
|
except:
|
||||||
return
|
return
|
||||||
root_dir_nodes = doc.getElementsByTagName('root_directory')
|
root_path_nodes = doc.getElementsByTagName('root_directory')
|
||||||
for rdn in root_dir_nodes:
|
for rdn in root_path_nodes:
|
||||||
if not rdn.getAttributeNode('path'):
|
if not rdn.getAttributeNode('path'):
|
||||||
continue
|
continue
|
||||||
path = rdn.getAttributeNode('path').nodeValue
|
path = rdn.getAttributeNode('path').nodeValue
|
||||||
@@ -144,9 +151,9 @@ class Directories(object):
|
|||||||
with FileOrPath(outfile, 'wb') as fp:
|
with FileOrPath(outfile, 'wb') as fp:
|
||||||
doc = xml.dom.minidom.Document()
|
doc = xml.dom.minidom.Document()
|
||||||
root = doc.appendChild(doc.createElement('directories'))
|
root = doc.appendChild(doc.createElement('directories'))
|
||||||
for root_dir in self:
|
for root_path in self:
|
||||||
root_dir_node = root.appendChild(doc.createElement('root_directory'))
|
root_path_node = root.appendChild(doc.createElement('root_directory'))
|
||||||
root_dir_node.setAttribute('path', unicode(root_dir.path).encode('utf-8'))
|
root_path_node.setAttribute('path', unicode(root_path).encode('utf-8'))
|
||||||
for path, state in self.states.iteritems():
|
for path, state in self.states.iteritems():
|
||||||
state_node = root.appendChild(doc.createElement('state'))
|
state_node = root.appendChild(doc.createElement('state'))
|
||||||
state_node.setAttribute('path', unicode(path).encode('utf-8'))
|
state_node.setAttribute('path', unicode(path).encode('utf-8'))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
import difflib
|
import difflib
|
||||||
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import string
|
import string
|
||||||
from collections import defaultdict, namedtuple
|
from collections import defaultdict, namedtuple
|
||||||
@@ -156,58 +157,71 @@ def get_match(first, second, flags=()):
|
|||||||
percentage = compare(first.words, second.words, flags)
|
percentage = compare(first.words, second.words, flags)
|
||||||
return Match(first, second, percentage)
|
return Match(first, second, percentage)
|
||||||
|
|
||||||
class MatchFactory(object):
|
def getmatches(objects, min_match_percentage=0, match_similar_words=False, weight_words=False,
|
||||||
common_word_threshold = 50
|
no_field_order=False, j=job.nulljob):
|
||||||
match_similar_words = False
|
COMMON_WORD_THRESHOLD = 50
|
||||||
min_match_percentage = 0
|
LIMIT = 5000000
|
||||||
weight_words = False
|
j = j.start_subjob(2)
|
||||||
no_field_order = False
|
sj = j.start_subjob(2)
|
||||||
limit = 5000000
|
for o in objects:
|
||||||
|
if not hasattr(o, 'words'):
|
||||||
def getmatches(self, objects, j=job.nulljob):
|
o.words = getwords(o.name)
|
||||||
j = j.start_subjob(2)
|
word_dict = build_word_dict(objects, sj)
|
||||||
sj = j.start_subjob(2)
|
reduce_common_words(word_dict, COMMON_WORD_THRESHOLD)
|
||||||
for o in objects:
|
if match_similar_words:
|
||||||
if not hasattr(o, 'words'):
|
merge_similar_words(word_dict)
|
||||||
o.words = getwords(o.name)
|
match_flags = []
|
||||||
word_dict = build_word_dict(objects, sj)
|
if weight_words:
|
||||||
reduce_common_words(word_dict, self.common_word_threshold)
|
match_flags.append(WEIGHT_WORDS)
|
||||||
if self.match_similar_words:
|
if match_similar_words:
|
||||||
merge_similar_words(word_dict)
|
match_flags.append(MATCH_SIMILAR_WORDS)
|
||||||
match_flags = []
|
if no_field_order:
|
||||||
if self.weight_words:
|
match_flags.append(NO_FIELD_ORDER)
|
||||||
match_flags.append(WEIGHT_WORDS)
|
j.start_job(len(word_dict), '0 matches found')
|
||||||
if self.match_similar_words:
|
compared = defaultdict(set)
|
||||||
match_flags.append(MATCH_SIMILAR_WORDS)
|
result = []
|
||||||
if self.no_field_order:
|
try:
|
||||||
match_flags.append(NO_FIELD_ORDER)
|
# This whole 'popping' thing is there to avoid taking too much memory at the same time.
|
||||||
j.start_job(len(word_dict), '0 matches found')
|
while word_dict:
|
||||||
compared = defaultdict(set)
|
items = word_dict.popitem()[1]
|
||||||
result = []
|
while items:
|
||||||
try:
|
ref = items.pop()
|
||||||
# This whole 'popping' thing is there to avoid taking too much memory at the same time.
|
compared_already = compared[ref]
|
||||||
while word_dict:
|
to_compare = items - compared_already
|
||||||
items = word_dict.popitem()[1]
|
compared_already |= to_compare
|
||||||
while items:
|
for other in to_compare:
|
||||||
ref = items.pop()
|
m = get_match(ref, other, match_flags)
|
||||||
compared_already = compared[ref]
|
if m.percentage >= min_match_percentage:
|
||||||
to_compare = items - compared_already
|
result.append(m)
|
||||||
compared_already |= to_compare
|
if len(result) >= LIMIT:
|
||||||
for other in to_compare:
|
return result
|
||||||
m = get_match(ref, other, match_flags)
|
j.add_progress(desc='%d matches found' % len(result))
|
||||||
if m.percentage >= self.min_match_percentage:
|
except MemoryError:
|
||||||
result.append(m)
|
# This is the place where the memory usage is at its peak during the scan.
|
||||||
if len(result) >= self.limit:
|
# Just continue the process with an incomplete list of matches.
|
||||||
return result
|
del compared # This should give us enough room to call logging.
|
||||||
j.add_progress(desc='%d matches found' % len(result))
|
logging.warning('Memory Overflow. Matches: %d. Word dict: %d' % (len(result), len(word_dict)))
|
||||||
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
|
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):
|
class Group(object):
|
||||||
#---Override
|
#---Override
|
||||||
|
|||||||
178
base/py/fs.py
Normal file
178
base/py/fs.py
Normal 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
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
from hsutil import job
|
from hsutil import job, io
|
||||||
from hsutil.misc import dedupe
|
from hsutil.misc import dedupe
|
||||||
from hsutil.str import get_file_ext, rem_file_ext
|
from hsutil.str import get_file_ext, rem_file_ext
|
||||||
|
|
||||||
@@ -32,40 +32,32 @@ class Scanner(object):
|
|||||||
self.ignore_list = IgnoreList()
|
self.ignore_list = IgnoreList()
|
||||||
self.discarded_file_count = 0
|
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):
|
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:
|
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]
|
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
|
@staticmethod
|
||||||
def _key_func(dupe):
|
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')]:
|
for f in [f for f in files if not hasattr(f, 'is_ref')]:
|
||||||
f.is_ref = False
|
f.is_ref = False
|
||||||
logging.info('Getting matches')
|
logging.info('Getting matches')
|
||||||
if self.match_factory is None:
|
matches = self._getmatches(files, j)
|
||||||
matches = self._getmatches(files, j)
|
|
||||||
else:
|
|
||||||
matches = self.match_factory.getmatches(files, j)
|
|
||||||
logging.info('Found %d matches' % len(matches))
|
logging.info('Found %d matches' % len(matches))
|
||||||
|
j.set_progress(100, 'Removing false matches')
|
||||||
if not self.mix_file_kind:
|
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 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:
|
if self.ignore_list:
|
||||||
j = j.start_subjob(2)
|
j = j.start_subjob(2)
|
||||||
iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list')
|
iter_matches = j.iter_with_progress(matches, 'Processed %d/%d matches against the ignore list')
|
||||||
matches = [m for m in iter_matches
|
matches = [m for m in iter_matches
|
||||||
if not self.ignore_list.AreIgnored(unicode(m.first.path), unicode(m.second.path))]
|
if not self.ignore_list.AreIgnored(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')
|
logging.info('Grouping matches')
|
||||||
groups = engine.get_groups(matches, j)
|
groups = engine.get_groups(matches, j)
|
||||||
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
matched_files = dedupe([m.first for m in matches] + [m.second for m in matches])
|
||||||
@@ -118,7 +100,6 @@ class Scanner(object):
|
|||||||
g.prioritize(self._key_func, self._tie_breaker)
|
g.prioritize(self._key_func, self._tie_breaker)
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
match_factory = None
|
|
||||||
match_similar_words = False
|
match_similar_words = False
|
||||||
min_match_percentage = 80
|
min_match_percentage = 80
|
||||||
mix_file_kind = True
|
mix_file_kind = True
|
||||||
@@ -126,9 +107,3 @@ class Scanner(object):
|
|||||||
scanned_tags = set(['artist', 'title'])
|
scanned_tags = set(['artist', 'title'])
|
||||||
size_threshold = 0
|
size_threshold = 0
|
||||||
word_weighting = False
|
word_weighting = False
|
||||||
|
|
||||||
class ScannerME(Scanner): # Scanner for Music Edition
|
|
||||||
@staticmethod
|
|
||||||
def _key_func(dupe):
|
|
||||||
return (not dupe.is_ref, -dupe.bitrate, -dupe.size)
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ from hsutil.path import Path
|
|||||||
from hsutil.testcase import TestCase
|
from hsutil.testcase import TestCase
|
||||||
from hsutil.decorators import log_calls
|
from hsutil.decorators import log_calls
|
||||||
from hsutil import io
|
from hsutil import io
|
||||||
import hsfs.phys
|
|
||||||
|
|
||||||
|
from . import data
|
||||||
from .results_test import GetTestGroups
|
from .results_test import GetTestGroups
|
||||||
from .. import engine, data
|
from .. import engine, fs
|
||||||
try:
|
try:
|
||||||
from ..app_cocoa import DupeGuru as DupeGuruBase
|
from ..app_cocoa import DupeGuru as DupeGuruBase
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -35,7 +35,6 @@ class DupeGuru(DupeGuruBase):
|
|||||||
def _start_job(self, jobid, func):
|
def _start_job(self, jobid, func):
|
||||||
func(nulljob)
|
func(nulljob)
|
||||||
|
|
||||||
|
|
||||||
def r2np(rows):
|
def r2np(rows):
|
||||||
#Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]]
|
#Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]]
|
||||||
return [[i] for i in rows]
|
return [[i] for i in rows]
|
||||||
@@ -310,15 +309,15 @@ class TCDupeGuru(TestCase):
|
|||||||
|
|
||||||
class TCDupeGuru_renameSelected(TestCase):
|
class TCDupeGuru_renameSelected(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
p = Path(tempfile.mkdtemp())
|
p = self.tmppath()
|
||||||
fp = open(str(p + 'foo bar 1'),mode='w')
|
fp = open(unicode(p + 'foo bar 1'),mode='w')
|
||||||
fp.close()
|
fp.close()
|
||||||
fp = open(str(p + 'foo bar 2'),mode='w')
|
fp = open(unicode(p + 'foo bar 2'),mode='w')
|
||||||
fp.close()
|
fp.close()
|
||||||
fp = open(str(p + 'foo bar 3'),mode='w')
|
fp = open(unicode(p + 'foo bar 3'),mode='w')
|
||||||
fp.close()
|
fp.close()
|
||||||
refdir = hsfs.phys.Directory(None,str(p))
|
files = fs.get_files(p)
|
||||||
matches = engine.MatchFactory().getmatches(refdir.files)
|
matches = engine.getmatches(files)
|
||||||
groups = engine.get_groups(matches)
|
groups = engine.get_groups(matches)
|
||||||
g = groups[0]
|
g = groups[0]
|
||||||
g.prioritize(lambda x:x.name)
|
g.prioritize(lambda x:x.name)
|
||||||
@@ -327,45 +326,41 @@ class TCDupeGuru_renameSelected(TestCase):
|
|||||||
self.app = app
|
self.app = app
|
||||||
self.groups = groups
|
self.groups = groups
|
||||||
self.p = p
|
self.p = p
|
||||||
self.refdir = refdir
|
self.files = files
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
shutil.rmtree(str(self.p))
|
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
app = self.app
|
app = self.app
|
||||||
refdir = self.refdir
|
|
||||||
g = self.groups[0]
|
g = self.groups[0]
|
||||||
app.SelectPowerMarkerNodePaths(r2np([0]))
|
app.SelectPowerMarkerNodePaths(r2np([0]))
|
||||||
self.assert_(app.RenameSelected('renamed'))
|
assert app.RenameSelected('renamed')
|
||||||
self.assert_('renamed' in refdir)
|
names = io.listdir(self.p)
|
||||||
self.assert_('foo bar 2' not in refdir)
|
assert 'renamed' in names
|
||||||
self.assert_(g.dupes[0] is refdir['renamed'])
|
assert 'foo bar 2' not in names
|
||||||
self.assert_(g.dupes[0] in refdir)
|
eq_(g.dupes[0].name, 'renamed')
|
||||||
|
|
||||||
def test_none_selected(self):
|
def test_none_selected(self):
|
||||||
app = self.app
|
app = self.app
|
||||||
refdir = self.refdir
|
|
||||||
g = self.groups[0]
|
g = self.groups[0]
|
||||||
app.SelectPowerMarkerNodePaths([])
|
app.SelectPowerMarkerNodePaths([])
|
||||||
self.mock(logging, 'warning', log_calls(lambda msg: None))
|
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']
|
msg = logging.warning.calls[0]['msg']
|
||||||
self.assertEqual('dupeGuru Warning: list index out of range', msg)
|
eq_('dupeGuru Warning: list index out of range', msg)
|
||||||
self.assert_('renamed' not in refdir)
|
names = io.listdir(self.p)
|
||||||
self.assert_('foo bar 2' in refdir)
|
assert 'renamed' not in names
|
||||||
self.assert_(g.dupes[0] is refdir['foo bar 2'])
|
assert 'foo bar 2' in names
|
||||||
|
eq_(g.dupes[0].name, 'foo bar 2')
|
||||||
|
|
||||||
def test_name_already_exists(self):
|
def test_name_already_exists(self):
|
||||||
app = self.app
|
app = self.app
|
||||||
refdir = self.refdir
|
|
||||||
g = self.groups[0]
|
g = self.groups[0]
|
||||||
app.SelectPowerMarkerNodePaths(r2np([0]))
|
app.SelectPowerMarkerNodePaths(r2np([0]))
|
||||||
self.mock(logging, 'warning', log_calls(lambda msg: None))
|
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']
|
msg = logging.warning.calls[0]['msg']
|
||||||
self.assert_(msg.startswith('dupeGuru Warning: \'foo bar 2\' already exists in'))
|
assert msg.startswith('dupeGuru Warning: \'foo bar 1\' already exists in')
|
||||||
self.assert_('foo bar 1' in refdir)
|
names = io.listdir(self.p)
|
||||||
self.assert_('foo bar 2' in refdir)
|
assert 'foo bar 1' in names
|
||||||
self.assert_(g.dupes[0] is refdir['foo bar 2'])
|
assert 'foo bar 2' in names
|
||||||
|
eq_(g.dupes[0].name, 'foo bar 2')
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,11 @@ from hsutil.testcase import TestCase
|
|||||||
from hsutil import io
|
from hsutil import io
|
||||||
from hsutil.path import Path
|
from hsutil.path import Path
|
||||||
from hsutil.decorators import log_calls
|
from hsutil.decorators import log_calls
|
||||||
import hsfs as fs
|
|
||||||
import hsfs.phys
|
|
||||||
import hsutil.files
|
import hsutil.files
|
||||||
from hsutil.job import nulljob
|
from hsutil.job import nulljob
|
||||||
|
|
||||||
from .. import data, app
|
from . import data
|
||||||
|
from .. import app, fs
|
||||||
from ..app import DupeGuru as DupeGuruBase
|
from ..app import DupeGuru as DupeGuruBase
|
||||||
|
|
||||||
class DupeGuru(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
|
# 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
|
# 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.
|
# every change I want to make. The blowup was caused by a missing import.
|
||||||
dupe_parent = fs.Directory(None, 'foo')
|
p = self.tmppath()
|
||||||
dupe = fs.File(dupe_parent, 'bar')
|
io.open(p + 'foo', 'w').close()
|
||||||
dupe.copy = log_calls(lambda dest, newname: None)
|
|
||||||
self.mock(hsutil.files, 'copy', log_calls(lambda source_path, dest_path: None))
|
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(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 = 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))
|
self.assertEqual(1, len(hsutil.files.copy.calls))
|
||||||
call = hsutil.files.copy.calls[0]
|
call = hsutil.files.copy.calls[0]
|
||||||
self.assertEqual('some_destination', call['dest_path'])
|
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):
|
def test_copy_or_move_clean_empty_dirs(self):
|
||||||
tmppath = Path(self.tmpdir())
|
tmppath = Path(self.tmpdir())
|
||||||
sourcepath = tmppath + 'source'
|
sourcepath = tmppath + 'source'
|
||||||
io.mkdir(sourcepath)
|
io.mkdir(sourcepath)
|
||||||
io.open(sourcepath + 'myfile', 'w')
|
io.open(sourcepath + 'myfile', 'w')
|
||||||
tmpdir = hsfs.phys.Directory(None, unicode(tmppath))
|
|
||||||
myfile = tmpdir['source']['myfile']
|
|
||||||
app = DupeGuru()
|
app = DupeGuru()
|
||||||
|
app.directories.add_path(tmppath)
|
||||||
|
[myfile] = app.directories.get_files()
|
||||||
self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None))
|
self.mock(app, 'clean_empty_dirs', log_calls(lambda path: None))
|
||||||
app.copy_or_move(myfile, False, tmppath + 'dest', 0)
|
app.copy_or_move(myfile, False, tmppath + 'dest', 0)
|
||||||
calls = app.clean_empty_dirs.calls
|
calls = app.clean_empty_dirs.calls
|
||||||
@@ -87,9 +86,14 @@ class TCDupeGuru(TestCase):
|
|||||||
self.assertEqual(sourcepath, calls[0]['path'])
|
self.assertEqual(sourcepath, calls[0]['path'])
|
||||||
|
|
||||||
def test_Scan_with_objects_evaluating_to_false(self):
|
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
|
# At some point, any() was used in a wrong way that made Scan() wrongly return 1
|
||||||
app = DupeGuru()
|
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)
|
f1.is_ref, f2.is_ref = (False, False)
|
||||||
assert not (bool(f1) and bool(f2))
|
assert not (bool(f1) and bool(f2))
|
||||||
app.directories.get_files = lambda: [f1, f2]
|
app.directories.get_files = lambda: [f1, f2]
|
||||||
|
|||||||
45
base/py/tests/data.py
Normal file
45
base/py/tests/data.py
Normal 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']))
|
||||||
@@ -10,20 +10,43 @@
|
|||||||
import os.path as op
|
import os.path as op
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import shutil
|
|
||||||
|
|
||||||
from nose.tools import eq_
|
from nose.tools import eq_
|
||||||
|
|
||||||
from hsutil import job, io
|
from hsutil import io
|
||||||
from hsutil.path import Path
|
from hsutil.path import Path
|
||||||
from hsutil.testcase import TestCase
|
from hsutil.testcase import TestCase
|
||||||
import hsfs.phys
|
|
||||||
from hsfs.tests import phys_test
|
|
||||||
|
|
||||||
from ..directories import *
|
from ..directories import *
|
||||||
|
|
||||||
testpath = Path(TestCase.datadirpath())
|
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):
|
class TCDirectories(TestCase):
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
d = Directories()
|
d = Directories()
|
||||||
@@ -33,13 +56,11 @@ class TCDirectories(TestCase):
|
|||||||
def test_add_path(self):
|
def test_add_path(self):
|
||||||
d = Directories()
|
d = Directories()
|
||||||
p = testpath + 'utils'
|
p = testpath + 'utils'
|
||||||
added = d.add_path(p)
|
d.add_path(p)
|
||||||
self.assertEqual(1,len(d))
|
self.assertEqual(1,len(d))
|
||||||
self.assert_(p in d)
|
self.assert_(p in d)
|
||||||
self.assert_((p + 'foobar') in d)
|
self.assert_((p + 'foobar') in d)
|
||||||
self.assert_(p[:-1] not in d)
|
self.assert_(p[:-1] not in d)
|
||||||
self.assertEqual(p,added.path)
|
|
||||||
self.assert_(d[0] is added)
|
|
||||||
p = self.tmppath()
|
p = self.tmppath()
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
self.assertEqual(2,len(d))
|
self.assertEqual(2,len(d))
|
||||||
@@ -53,13 +74,13 @@ class TCDirectories(TestCase):
|
|||||||
self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar')
|
self.assertRaises(AlreadyThereError, d.add_path, p + 'foobar')
|
||||||
self.assertEqual(1, len(d))
|
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 = Directories()
|
||||||
d.add_path(testpath + 'utils')
|
d.add_path(testpath + 'utils')
|
||||||
self.assertEqual(1, len(d))
|
self.assertEqual(1, len(d))
|
||||||
added = d.add_path(testpath)
|
d.add_path(testpath)
|
||||||
self.assertEqual(1, len(d))
|
eq_(len(d), 1)
|
||||||
self.assert_(added is d[0])
|
eq_(d[0], testpath)
|
||||||
|
|
||||||
def test_AddPath_non_latin(self):
|
def test_AddPath_non_latin(self):
|
||||||
p = Path(self.tmpdir())
|
p = Path(self.tmpdir())
|
||||||
@@ -114,7 +135,7 @@ class TCDirectories(TestCase):
|
|||||||
|
|
||||||
def test_set_state_keep_state_dict_size_to_minimum(self):
|
def test_set_state_keep_state_dict_size_to_minimum(self):
|
||||||
d = Directories()
|
d = Directories()
|
||||||
p = Path(phys_test.create_fake_fs(self.tmpdir()))
|
p = create_fake_fs(self.tmppath())
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
d.set_state(p,STATE_REFERENCE)
|
d.set_state(p,STATE_REFERENCE)
|
||||||
d.set_state(p + 'dir1',STATE_REFERENCE)
|
d.set_state(p + 'dir1',STATE_REFERENCE)
|
||||||
@@ -129,17 +150,17 @@ class TCDirectories(TestCase):
|
|||||||
|
|
||||||
def test_get_files(self):
|
def test_get_files(self):
|
||||||
d = Directories()
|
d = Directories()
|
||||||
p = Path(phys_test.create_fake_fs(self.tmpdir()))
|
p = create_fake_fs(self.tmppath())
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
d.set_state(p + 'dir1',STATE_REFERENCE)
|
d.set_state(p + 'dir1',STATE_REFERENCE)
|
||||||
d.set_state(p + 'dir2',STATE_EXCLUDED)
|
d.set_state(p + 'dir2',STATE_EXCLUDED)
|
||||||
files = d.get_files()
|
files = list(d.get_files())
|
||||||
self.assertEqual(5, len(list(files)))
|
self.assertEqual(5, len(files))
|
||||||
for f in files:
|
for f in files:
|
||||||
if f.parent.path == p + 'dir1':
|
if f.path[:-1] == p + 'dir1':
|
||||||
self.assert_(f.is_ref)
|
assert f.is_ref
|
||||||
else:
|
else:
|
||||||
self.assert_(not f.is_ref)
|
assert not f.is_ref
|
||||||
|
|
||||||
def test_get_files_with_inherited_exclusion(self):
|
def test_get_files_with_inherited_exclusion(self):
|
||||||
d = Directories()
|
d = Directories()
|
||||||
@@ -177,52 +198,28 @@ class TCDirectories(TestCase):
|
|||||||
except LookupError:
|
except LookupError:
|
||||||
self.fail()
|
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):
|
def test_load_from_file_with_invalid_path(self):
|
||||||
#This test simulates a load from file resulting in a
|
#This test simulates a load from file resulting in a
|
||||||
#InvalidPath raise. Other directories must be loaded.
|
#InvalidPath raise. Other directories must be loaded.
|
||||||
d1 = Directories()
|
d1 = Directories()
|
||||||
d1.add_path(testpath + 'utils')
|
d1.add_path(testpath + 'utils')
|
||||||
#Will raise InvalidPath upon loading
|
#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')
|
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
|
||||||
d1.save_to_file(tmpxml)
|
d1.save_to_file(tmpxml)
|
||||||
d2 = Directories()
|
d2 = Directories()
|
||||||
d2.load_from_file(tmpxml)
|
d2.load_from_file(tmpxml)
|
||||||
self.assertEqual(1, len(d2))
|
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):
|
def test_unicode_save(self):
|
||||||
d = Directories()
|
d = Directories()
|
||||||
p1 = self.tmppath() + u'hello\xe9'
|
p1 = self.tmppath() + u'hello\xe9'
|
||||||
io.mkdir(p1)
|
io.mkdir(p1)
|
||||||
io.mkdir(p1 + u'foo\xe9')
|
io.mkdir(p1 + u'foo\xe9')
|
||||||
d.add_path(p1)
|
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')
|
tmpxml = op.join(self.tmpdir(), 'directories_testunit.xml')
|
||||||
try:
|
try:
|
||||||
d.save_to_file(tmpxml)
|
d.save_to_file(tmpxml)
|
||||||
@@ -231,7 +228,7 @@ class TCDirectories(TestCase):
|
|||||||
|
|
||||||
def test_get_files_refreshes_its_directories(self):
|
def test_get_files_refreshes_its_directories(self):
|
||||||
d = Directories()
|
d = Directories()
|
||||||
p = Path(phys_test.create_fake_fs(self.tmpdir()))
|
p = create_fake_fs(self.tmppath())
|
||||||
d.add_path(p)
|
d.add_path(p)
|
||||||
files = d.get_files()
|
files = d.get_files()
|
||||||
self.assertEqual(6, len(list(files)))
|
self.assertEqual(6, len(list(files)))
|
||||||
@@ -258,16 +255,6 @@ class TCDirectories(TestCase):
|
|||||||
d.set_state(hidden_dir_path, STATE_NORMAL)
|
d.set_state(hidden_dir_path, STATE_NORMAL)
|
||||||
self.assertEqual(d.get_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):
|
def test_default_path_state_override(self):
|
||||||
# It's possible for a subclass to override the default state of a path
|
# It's possible for a subclass to override the default state of a path
|
||||||
class MyDirectories(Directories):
|
class MyDirectories(Directories):
|
||||||
|
|||||||
@@ -15,16 +15,21 @@ from hsutil import job
|
|||||||
from hsutil.decorators import log_calls
|
from hsutil.decorators import log_calls
|
||||||
from hsutil.testcase import TestCase
|
from hsutil.testcase import TestCase
|
||||||
|
|
||||||
from .. import engine
|
from .. import engine, fs
|
||||||
from ..engine import *
|
from ..engine import *
|
||||||
|
|
||||||
class NamedObject(object):
|
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.name = name
|
||||||
|
self.size = size
|
||||||
|
self.md5partial = name
|
||||||
|
self.md5 = name
|
||||||
if with_words:
|
if with_words:
|
||||||
self.words = getwords(name)
|
self.words = getwords(name)
|
||||||
|
|
||||||
|
|
||||||
|
no = NamedObject
|
||||||
|
|
||||||
def get_match_triangle():
|
def get_match_triangle():
|
||||||
o1 = NamedObject(with_words=True)
|
o1 = NamedObject(with_words=True)
|
||||||
o2 = 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)
|
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):
|
def test_empty(self):
|
||||||
self.assertEqual([],MatchFactory().getmatches([]))
|
eq_(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)
|
|
||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
||||||
r = MatchFactory().getmatches(l)
|
r = getmatches(l)
|
||||||
self.assertEqual(2,len(r))
|
self.assertEqual(2,len(r))
|
||||||
seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh"
|
seek = [m for m in r if m.percentage == 50] #"foo bar" and "bar bleh"
|
||||||
m = seek[0]
|
m = seek[0]
|
||||||
@@ -367,7 +364,7 @@ class TCMatchFactory(TestCase):
|
|||||||
|
|
||||||
def test_null_and_unrelated_objects(self):
|
def test_null_and_unrelated_objects(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject(""),NamedObject("unrelated object")]
|
||||||
r = MatchFactory().getmatches(l)
|
r = getmatches(l)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
m = r[0]
|
m = r[0]
|
||||||
self.assertEqual(50,m.percentage)
|
self.assertEqual(50,m.percentage)
|
||||||
@@ -376,34 +373,33 @@ class TCMatchFactory(TestCase):
|
|||||||
|
|
||||||
def test_twice_the_same_word(self):
|
def test_twice_the_same_word(self):
|
||||||
l = [NamedObject("foo foo bar"),NamedObject("bar bleh")]
|
l = [NamedObject("foo foo bar"),NamedObject("bar bleh")]
|
||||||
r = MatchFactory().getmatches(l)
|
r = getmatches(l)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
|
|
||||||
def test_twice_the_same_word_when_preworded(self):
|
def test_twice_the_same_word_when_preworded(self):
|
||||||
l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)]
|
l = [NamedObject("foo foo bar",True),NamedObject("bar bleh",True)]
|
||||||
r = MatchFactory().getmatches(l)
|
r = getmatches(l)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
|
|
||||||
def test_two_words_match(self):
|
def test_two_words_match(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("foo bar bleh")]
|
l = [NamedObject("foo bar"),NamedObject("foo bar bleh")]
|
||||||
r = MatchFactory().getmatches(l)
|
r = getmatches(l)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
|
|
||||||
def test_match_files_with_only_common_words(self):
|
def test_match_files_with_only_common_words(self):
|
||||||
#If a word occurs more than 50 times, it is excluded from the matching process
|
#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
|
#The problem with the common_word_threshold is that the files containing only common
|
||||||
#words will never be matched together. We *should* match them.
|
#words will never be matched together. We *should* match them.
|
||||||
mf = MatchFactory()
|
# This test assumes that the common word threashold const is 50
|
||||||
mf.common_word_threshold = 50
|
|
||||||
l = [NamedObject("foo") for i in range(50)]
|
l = [NamedObject("foo") for i in range(50)]
|
||||||
r = mf.getmatches(l)
|
r = getmatches(l)
|
||||||
self.assertEqual(1225,len(r))
|
self.assertEqual(1225,len(r))
|
||||||
|
|
||||||
def test_use_words_already_there_if_there(self):
|
def test_use_words_already_there_if_there(self):
|
||||||
o1 = NamedObject('foo')
|
o1 = NamedObject('foo')
|
||||||
o2 = NamedObject('bar')
|
o2 = NamedObject('bar')
|
||||||
o2.words = ['foo']
|
o2.words = ['foo']
|
||||||
self.assertEqual(1,len(MatchFactory().getmatches([o1,o2])))
|
eq_(1, len(getmatches([o1,o2])))
|
||||||
|
|
||||||
def test_job(self):
|
def test_job(self):
|
||||||
def do_progress(p,d=''):
|
def do_progress(p,d=''):
|
||||||
@@ -413,75 +409,62 @@ class TCMatchFactory(TestCase):
|
|||||||
j = job.Job(1,do_progress)
|
j = job.Job(1,do_progress)
|
||||||
self.log = []
|
self.log = []
|
||||||
s = "foo bar"
|
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.assert_(len(self.log) > 2)
|
||||||
self.assertEqual(0,self.log[0])
|
self.assertEqual(0,self.log[0])
|
||||||
self.assertEqual(100,self.log[-1])
|
self.assertEqual(100,self.log[-1])
|
||||||
|
|
||||||
def test_weight_words(self):
|
def test_weight_words(self):
|
||||||
mf = MatchFactory()
|
|
||||||
mf.weight_words = True
|
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
|
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)
|
self.assertEqual(int((6.0 / 13.0) * 100),m.percentage)
|
||||||
|
|
||||||
def test_similar_word(self):
|
def test_similar_word(self):
|
||||||
mf = MatchFactory()
|
|
||||||
mf.match_similar_words = True
|
|
||||||
l = [NamedObject("foobar"),NamedObject("foobars")]
|
l = [NamedObject("foobar"),NamedObject("foobars")]
|
||||||
self.assertEqual(1,len(mf.getmatches(l)))
|
eq_(len(getmatches(l, match_similar_words=True)), 1)
|
||||||
self.assertEqual(100,mf.getmatches(l)[0].percentage)
|
eq_(getmatches(l, match_similar_words=True)[0].percentage, 100)
|
||||||
l = [NamedObject("foobar"),NamedObject("foo")]
|
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")]
|
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")]
|
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):
|
def test_single_object_with_similar_words(self):
|
||||||
mf = MatchFactory()
|
|
||||||
mf.match_similar_words = True
|
|
||||||
l = [NamedObject("foo foos")]
|
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):
|
def test_double_words_get_counted_only_once(self):
|
||||||
mf = MatchFactory()
|
|
||||||
l = [NamedObject("foo bar foo bleh"),NamedObject("foo bar bleh bar")]
|
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)
|
self.assertEqual(75,m.percentage)
|
||||||
|
|
||||||
def test_with_fields(self):
|
def test_with_fields(self):
|
||||||
mf = MatchFactory()
|
|
||||||
o1 = NamedObject("foo bar - foo bleh")
|
o1 = NamedObject("foo bar - foo bleh")
|
||||||
o2 = NamedObject("foo bar - bleh bar")
|
o2 = NamedObject("foo bar - bleh bar")
|
||||||
o1.words = getfields(o1.name)
|
o1.words = getfields(o1.name)
|
||||||
o2.words = getfields(o2.name)
|
o2.words = getfields(o2.name)
|
||||||
m = mf.getmatches([o1, o2])[0]
|
m = getmatches([o1, o2])[0]
|
||||||
self.assertEqual(50, m.percentage)
|
self.assertEqual(50, m.percentage)
|
||||||
|
|
||||||
def test_with_fields_no_order(self):
|
def test_with_fields_no_order(self):
|
||||||
mf = MatchFactory()
|
|
||||||
mf.no_field_order = True
|
|
||||||
o1 = NamedObject("foo bar - foo bleh")
|
o1 = NamedObject("foo bar - foo bleh")
|
||||||
o2 = NamedObject("bleh bang - foo bar")
|
o2 = NamedObject("bleh bang - foo bar")
|
||||||
o1.words = getfields(o1.name)
|
o1.words = getfields(o1.name)
|
||||||
o2.words = getfields(o2.name)
|
o2.words = getfields(o2.name)
|
||||||
m = mf.getmatches([o1, o2])[0]
|
m = getmatches([o1, o2], no_field_order=True)[0]
|
||||||
self.assertEqual(50 ,m.percentage)
|
eq_(m.percentage, 50)
|
||||||
|
|
||||||
def test_only_match_similar_when_the_option_is_set(self):
|
def test_only_match_similar_when_the_option_is_set(self):
|
||||||
mf = MatchFactory()
|
|
||||||
mf.match_similar_words = False
|
|
||||||
l = [NamedObject("foobar"),NamedObject("foobars")]
|
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):
|
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
|
# with nosetests, the stack is increased. The number has to be high enough not to be failing falsely
|
||||||
sys.setrecursionlimit(100)
|
sys.setrecursionlimit(100)
|
||||||
mf = MatchFactory()
|
|
||||||
files = [NamedObject('foo bar') for i in range(101)]
|
files = [NamedObject('foo bar') for i in range(101)]
|
||||||
try:
|
try:
|
||||||
mf.getmatches(files)
|
getmatches(files)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
self.fail()
|
self.fail()
|
||||||
finally:
|
finally:
|
||||||
@@ -489,18 +472,9 @@ class TCMatchFactory(TestCase):
|
|||||||
|
|
||||||
def test_min_match_percentage(self):
|
def test_min_match_percentage(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("a b c foo")]
|
||||||
mf = MatchFactory()
|
r = getmatches(l, min_match_percentage=50)
|
||||||
mf.min_match_percentage = 50
|
|
||||||
r = mf.getmatches(l)
|
|
||||||
self.assertEqual(1,len(r)) #Only "foo bar" / "bar bleh" should match
|
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):
|
def test_MemoryError(self):
|
||||||
@log_calls
|
@log_calls
|
||||||
def mocked_match(first, second, flags):
|
def mocked_match(first, second, flags):
|
||||||
@@ -510,14 +484,19 @@ class TCMatchFactory(TestCase):
|
|||||||
|
|
||||||
objects = [NamedObject() for i in range(10)] # results in 45 matches
|
objects = [NamedObject() for i in range(10)] # results in 45 matches
|
||||||
self.mock(engine, 'get_match', mocked_match)
|
self.mock(engine, 'get_match', mocked_match)
|
||||||
mf = MatchFactory()
|
|
||||||
try:
|
try:
|
||||||
r = mf.getmatches(objects)
|
r = getmatches(objects)
|
||||||
except MemoryError:
|
except MemoryError:
|
||||||
self.fail('MemorryError must be handled')
|
self.fail('MemorryError must be handled')
|
||||||
self.assertEqual(42, len(r))
|
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):
|
class TCGroup(TestCase):
|
||||||
def test_empy(self):
|
def test_empy(self):
|
||||||
g = Group()
|
g = Group()
|
||||||
@@ -738,7 +717,7 @@ class TCget_groups(TestCase):
|
|||||||
|
|
||||||
def test_simple(self):
|
def test_simple(self):
|
||||||
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
|
l = [NamedObject("foo bar"),NamedObject("bar bleh")]
|
||||||
matches = MatchFactory().getmatches(l)
|
matches = getmatches(l)
|
||||||
m = matches[0]
|
m = matches[0]
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
@@ -749,7 +728,7 @@ class TCget_groups(TestCase):
|
|||||||
def test_group_with_multiple_matches(self):
|
def test_group_with_multiple_matches(self):
|
||||||
#This results in 3 matches
|
#This results in 3 matches
|
||||||
l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")]
|
l = [NamedObject("foo"),NamedObject("foo"),NamedObject("foo")]
|
||||||
matches = MatchFactory().getmatches(l)
|
matches = getmatches(l)
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
g = r[0]
|
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")]
|
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"
|
#There will be 2 groups here: group "a b" and group "c d"
|
||||||
#"b c" can go either of them, but not both.
|
#"b c" can go either of them, but not both.
|
||||||
matches = MatchFactory().getmatches(l)
|
matches = getmatches(l)
|
||||||
r = get_groups(matches)
|
r = get_groups(matches)
|
||||||
self.assertEqual(2,len(r))
|
self.assertEqual(2,len(r))
|
||||||
self.assertEqual(5,len(r[0])+len(r[1]))
|
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")]
|
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"
|
#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
|
#"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)
|
r = get_groups(matches)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
|
|
||||||
@@ -788,7 +767,7 @@ class TCget_groups(TestCase):
|
|||||||
|
|
||||||
def test_four_sized_group(self):
|
def test_four_sized_group(self):
|
||||||
l = [NamedObject("foobar") for i in xrange(4)]
|
l = [NamedObject("foobar") for i in xrange(4)]
|
||||||
m = MatchFactory().getmatches(l)
|
m = getmatches(l)
|
||||||
r = get_groups(m)
|
r = get_groups(m)
|
||||||
self.assertEqual(1,len(r))
|
self.assertEqual(1,len(r))
|
||||||
self.assertEqual(4,len(r[0]))
|
self.assertEqual(4,len(r[0]))
|
||||||
|
|||||||
@@ -16,12 +16,11 @@ from hsutil.path import Path
|
|||||||
from hsutil.testcase import TestCase
|
from hsutil.testcase import TestCase
|
||||||
from hsutil.misc import first
|
from hsutil.misc import first
|
||||||
|
|
||||||
from . import engine_test
|
from . import engine_test, data
|
||||||
from .. import data, engine
|
from .. import engine
|
||||||
from ..results import *
|
from ..results import *
|
||||||
|
|
||||||
class NamedObject(engine_test.NamedObject):
|
class NamedObject(engine_test.NamedObject):
|
||||||
size = 1
|
|
||||||
path = property(lambda x:Path('basepath') + x.name)
|
path = property(lambda x:Path('basepath') + x.name)
|
||||||
is_ref = False
|
is_ref = False
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ class NamedObject(engine_test.NamedObject):
|
|||||||
def GetTestGroups():
|
def GetTestGroups():
|
||||||
objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")]
|
objects = [NamedObject("foo bar"),NamedObject("bar bleh"),NamedObject("foo bleh"),NamedObject("ibabtu"),NamedObject("ibabtu")]
|
||||||
objects[1].size = 1024
|
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
|
groups = engine.get_groups(matches) #We should have 2 groups
|
||||||
for g in 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
|
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]
|
return objects[1]
|
||||||
|
|
||||||
objects = [NamedObject(u"\xe9foo bar",True),NamedObject("bar bleh",True)]
|
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
|
groups = engine.get_groups(matches) #We should have 2 groups
|
||||||
for g in 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
|
g.prioritize(lambda x:objects.index(x)) #We want the dupes to be in the same order as the list is
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
|
|
||||||
from nose.tools import eq_
|
from nose.tools import eq_
|
||||||
|
|
||||||
from hsutil import job
|
from hsutil import job, io
|
||||||
from hsutil.path import Path
|
from hsutil.path import Path
|
||||||
|
from hsutil.testcase import TestCase
|
||||||
|
|
||||||
|
from .. import fs
|
||||||
from ..engine import getwords, Match
|
from ..engine import getwords, Match
|
||||||
from ..ignore import IgnoreList
|
from ..ignore import IgnoreList
|
||||||
from ..scanner import *
|
from ..scanner import *
|
||||||
@@ -27,443 +29,439 @@ class NamedObject(object):
|
|||||||
no = NamedObject
|
no = NamedObject
|
||||||
|
|
||||||
#--- Scanner
|
#--- Scanner
|
||||||
def test_empty():
|
class ScannerTestFakeFiles(TestCase):
|
||||||
s = Scanner()
|
def setUp(self):
|
||||||
r = s.GetDupeGroups([])
|
# This is a hack to avoid invalidating all previous tests since the scanner started to test
|
||||||
eq_(r, [])
|
# for file existence before doing the match grouping.
|
||||||
|
self.mock(io, 'exists', lambda _: True)
|
||||||
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()
|
|
||||||
|
|
||||||
s = Scanner()
|
def test_empty(self):
|
||||||
s.scan_type = SCAN_TYPE_CONTENT
|
s = Scanner()
|
||||||
f = [MyFile('foo', 1), MyFile('bar', 2)]
|
r = s.GetDupeGroups([])
|
||||||
eq_(len(s.GetDupeGroups(f)), 0)
|
eq_(r, [])
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
s = Scanner()
|
def test_default_settings(self):
|
||||||
log = []
|
s = Scanner()
|
||||||
f = [no('foo bar'), no('foo bar'), no('foo bleh')]
|
eq_(s.min_match_percentage, 80)
|
||||||
r = s.GetDupeGroups(f, job.Job(1, do_progress))
|
eq_(s.scan_type, SCAN_TYPE_FILENAME)
|
||||||
eq_(log[0], 0)
|
eq_(s.mix_file_kind, True)
|
||||||
eq_(log[-1], 100)
|
eq_(s.word_weighting, False)
|
||||||
|
eq_(s.match_similar_words, False)
|
||||||
def test_mix_file_kind():
|
assert isinstance(s.ignore_list, IgnoreList)
|
||||||
s = Scanner()
|
|
||||||
s.mix_file_kind = False
|
def test_simple_with_default_settings(self):
|
||||||
f = [no('foo.1'), no('foo.2')]
|
s = Scanner()
|
||||||
r = s.GetDupeGroups(f)
|
f = [no('foo bar'), no('foo bar'), no('foo bleh')]
|
||||||
eq_(len(r), 0)
|
r = s.GetDupeGroups(f)
|
||||||
|
eq_(len(r), 1)
|
||||||
def test_word_weighting():
|
g = r[0]
|
||||||
s = Scanner()
|
#'foo bleh' cannot be in the group because the default min match % is 80
|
||||||
s.min_match_percentage = 75
|
eq_(len(g), 2)
|
||||||
s.word_weighting = True
|
assert g.ref in f[:2]
|
||||||
f = [no('foo bar'), no('foo bar bleh')]
|
assert g.dupes[0] in f[:2]
|
||||||
r = s.GetDupeGroups(f)
|
|
||||||
eq_(len(r), 1)
|
def test_simple_with_lower_min_match(self):
|
||||||
g = r[0]
|
s = Scanner()
|
||||||
m = g.get_match_of(g.dupes[0])
|
s.min_match_percentage = 50
|
||||||
eq_(m.percentage, 75) # 16 letters, 12 matching
|
f = [no('foo bar'), no('foo bar'), no('foo bleh')]
|
||||||
|
r = s.GetDupeGroups(f)
|
||||||
def test_similar_words():
|
eq_(len(r), 1)
|
||||||
s = Scanner()
|
g = r[0]
|
||||||
s.match_similar_words = True
|
eq_(len(g), 3)
|
||||||
f = [no('The White Stripes'), no('The Whites Stripe'), no('Limp Bizkit'), no('Limp Bizkitt')]
|
|
||||||
r = s.GetDupeGroups(f)
|
def test_trim_all_ref_groups(self):
|
||||||
eq_(len(r), 2)
|
# 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.
|
||||||
def test_fields():
|
s = Scanner()
|
||||||
s = Scanner()
|
f = [no('foo'), no('foo'), no('bar'), no('bar')]
|
||||||
s.scan_type = SCAN_TYPE_FIELDS
|
f[2].is_ref = True
|
||||||
f = [no('The White Stripes - Little Ghost'), no('The White Stripes - Little Acorn')]
|
f[3].is_ref = True
|
||||||
r = s.GetDupeGroups(f)
|
r = s.GetDupeGroups(f)
|
||||||
eq_(len(r), 0)
|
eq_(len(r), 1)
|
||||||
|
eq_(s.discarded_file_count, 0)
|
||||||
def test_fields_no_order():
|
|
||||||
s = Scanner()
|
def test_priorize(self):
|
||||||
s.scan_type = SCAN_TYPE_FIELDS_NO_ORDER
|
s = Scanner()
|
||||||
f = [no('The White Stripes - Little Ghost'), no('Little Ghost - The White Stripes')]
|
f = [no('foo'), no('foo'), no('bar'), no('bar')]
|
||||||
r = s.GetDupeGroups(f)
|
f[1].size = 2
|
||||||
eq_(len(r), 1)
|
f[2].size = 3
|
||||||
|
f[3].is_ref = True
|
||||||
def test_tag_scan():
|
r = s.GetDupeGroups(f)
|
||||||
s = Scanner()
|
g1, g2 = r
|
||||||
s.scan_type = SCAN_TYPE_TAG
|
assert f[1] in (g1.ref,g2.ref)
|
||||||
o1 = no('foo')
|
assert f[0] in (g1.dupes[0],g2.dupes[0])
|
||||||
o2 = no('bar')
|
assert f[3] in (g1.ref,g2.ref)
|
||||||
o1.artist = 'The White Stripes'
|
assert f[2] in (g1.dupes[0],g2.dupes[0])
|
||||||
o1.title = 'The Air Near My Fingers'
|
|
||||||
o2.artist = 'The White Stripes'
|
def test_content_scan(self):
|
||||||
o2.title = 'The Air Near My Fingers'
|
s = Scanner()
|
||||||
r = s.GetDupeGroups([o1,o2])
|
s.scan_type = SCAN_TYPE_CONTENT
|
||||||
eq_(len(r), 1)
|
f = [no('foo'), no('bar'), no('bleh')]
|
||||||
|
f[0].md5 = f[0].md5partial = 'foobar'
|
||||||
def test_tag_with_album_scan():
|
f[1].md5 = f[1].md5partial = 'foobar'
|
||||||
s = Scanner()
|
f[2].md5 = f[2].md5partial = 'bleh'
|
||||||
s.scan_type = SCAN_TYPE_TAG
|
r = s.GetDupeGroups(f)
|
||||||
s.scanned_tags = set(['artist', 'album', 'title'])
|
eq_(len(r), 1)
|
||||||
o1 = no('foo')
|
eq_(len(r[0]), 2)
|
||||||
o2 = no('bar')
|
eq_(s.discarded_file_count, 0) # don't count the different md5 as discarded!
|
||||||
o3 = no('bleh')
|
|
||||||
o1.artist = 'The White Stripes'
|
def test_content_scan_compare_sizes_first(self):
|
||||||
o1.title = 'The Air Near My Fingers'
|
class MyFile(no):
|
||||||
o1.album = 'Elephant'
|
@property
|
||||||
o2.artist = 'The White Stripes'
|
def md5(file):
|
||||||
o2.title = 'The Air Near My Fingers'
|
raise AssertionError()
|
||||||
o2.album = 'Elephant'
|
|
||||||
o3.artist = 'The White Stripes'
|
s = Scanner()
|
||||||
o3.title = 'The Air Near My Fingers'
|
s.scan_type = SCAN_TYPE_CONTENT
|
||||||
o3.album = 'foobar'
|
f = [MyFile('foo', 1), MyFile('bar', 2)]
|
||||||
r = s.GetDupeGroups([o1,o2,o3])
|
eq_(len(s.GetDupeGroups(f)), 0)
|
||||||
eq_(len(r), 1)
|
|
||||||
|
def test_min_match_perc_doesnt_matter_for_content_scan(self):
|
||||||
def test_that_dash_in_tags_dont_create_new_fields():
|
s = Scanner()
|
||||||
s = Scanner()
|
s.scan_type = SCAN_TYPE_CONTENT
|
||||||
s.scan_type = SCAN_TYPE_TAG
|
f = [no('foo'), no('bar'), no('bleh')]
|
||||||
s.scanned_tags = set(['artist', 'album', 'title'])
|
f[0].md5 = f[0].md5partial = 'foobar'
|
||||||
s.min_match_percentage = 50
|
f[1].md5 = f[1].md5partial = 'foobar'
|
||||||
o1 = no('foo')
|
f[2].md5 = f[2].md5partial = 'bleh'
|
||||||
o2 = no('bar')
|
s.min_match_percentage = 101
|
||||||
o1.artist = 'The White Stripes - a'
|
r = s.GetDupeGroups(f)
|
||||||
o1.title = 'The Air Near My Fingers - a'
|
eq_(len(r), 1)
|
||||||
o1.album = 'Elephant - a'
|
eq_(len(r[0]), 2)
|
||||||
o2.artist = 'The White Stripes - b'
|
s.min_match_percentage = 0
|
||||||
o2.title = 'The Air Near My Fingers - b'
|
r = s.GetDupeGroups(f)
|
||||||
o2.album = 'Elephant - b'
|
eq_(len(r), 1)
|
||||||
r = s.GetDupeGroups([o1,o2])
|
eq_(len(r[0]), 2)
|
||||||
eq_(len(r), 1)
|
|
||||||
|
def test_content_scan_doesnt_put_md5_in_words_at_the_end(self):
|
||||||
def test_tag_scan_with_different_scanned():
|
s = Scanner()
|
||||||
s = Scanner()
|
s.scan_type = SCAN_TYPE_CONTENT
|
||||||
s.scan_type = SCAN_TYPE_TAG
|
f = [no('foo'),no('bar')]
|
||||||
s.scanned_tags = set(['track', 'year'])
|
f[0].md5 = f[0].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'
|
||||||
o1 = no('foo')
|
f[1].md5 = f[1].md5partial = '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f'
|
||||||
o2 = no('bar')
|
r = s.GetDupeGroups(f)
|
||||||
o1.artist = 'The White Stripes'
|
g = r[0]
|
||||||
o1.title = 'some title'
|
|
||||||
o1.track = 'foo'
|
def test_extension_is_not_counted_in_filename_scan(self):
|
||||||
o1.year = 'bar'
|
s = Scanner()
|
||||||
o2.artist = 'The White Stripes'
|
s.min_match_percentage = 100
|
||||||
o2.title = 'another title'
|
f = [no('foo.bar'), no('foo.bleh')]
|
||||||
o2.track = 'foo'
|
r = s.GetDupeGroups(f)
|
||||||
o2.year = 'bar'
|
eq_(len(r), 1)
|
||||||
r = s.GetDupeGroups([o1, o2])
|
eq_(len(r[0]), 2)
|
||||||
eq_(len(r), 1)
|
|
||||||
|
def test_job(self):
|
||||||
def test_tag_scan_only_scans_existing_tags():
|
def do_progress(progress, desc=''):
|
||||||
s = Scanner()
|
log.append(progress)
|
||||||
s.scan_type = SCAN_TYPE_TAG
|
return True
|
||||||
s.scanned_tags = set(['artist', 'foo'])
|
|
||||||
o1 = no('foo')
|
s = Scanner()
|
||||||
o2 = no('bar')
|
log = []
|
||||||
o1.artist = 'The White Stripes'
|
f = [no('foo bar'), no('foo bar'), no('foo bleh')]
|
||||||
o1.foo = 'foo'
|
r = s.GetDupeGroups(f, job.Job(1, do_progress))
|
||||||
o2.artist = 'The White Stripes'
|
eq_(log[0], 0)
|
||||||
o2.foo = 'bar'
|
eq_(log[-1], 100)
|
||||||
r = s.GetDupeGroups([o1, o2])
|
|
||||||
eq_(len(r), 1) # Because 'foo' is not scanned, they match
|
def test_mix_file_kind(self):
|
||||||
|
s = Scanner()
|
||||||
def test_tag_scan_converts_to_str():
|
s.mix_file_kind = False
|
||||||
s = Scanner()
|
f = [no('foo.1'), no('foo.2')]
|
||||||
s.scan_type = SCAN_TYPE_TAG
|
r = s.GetDupeGroups(f)
|
||||||
s.scanned_tags = set(['track'])
|
eq_(len(r), 0)
|
||||||
o1 = no('foo')
|
|
||||||
o2 = no('bar')
|
def test_word_weighting(self):
|
||||||
o1.track = 42
|
s = Scanner()
|
||||||
o2.track = 42
|
s.min_match_percentage = 75
|
||||||
try:
|
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])
|
r = s.GetDupeGroups([o1, o2])
|
||||||
except TypeError:
|
eq_(len(r), 1)
|
||||||
raise AssertionError()
|
|
||||||
eq_(len(r), 1)
|
def test_tag_scan_only_scans_existing_tags(self):
|
||||||
|
s = Scanner()
|
||||||
def test_tag_scan_non_ascii():
|
s.scan_type = SCAN_TYPE_TAG
|
||||||
s = Scanner()
|
s.scanned_tags = set(['artist', 'foo'])
|
||||||
s.scan_type = SCAN_TYPE_TAG
|
o1 = no('foo')
|
||||||
s.scanned_tags = set(['title'])
|
o2 = no('bar')
|
||||||
o1 = no('foo')
|
o1.artist = 'The White Stripes'
|
||||||
o2 = no('bar')
|
o1.foo = 'foo'
|
||||||
o1.title = u'foobar\u00e9'
|
o2.artist = 'The White Stripes'
|
||||||
o2.title = u'foobar\u00e9'
|
o2.foo = 'bar'
|
||||||
try:
|
|
||||||
r = s.GetDupeGroups([o1, o2])
|
r = s.GetDupeGroups([o1, o2])
|
||||||
except UnicodeEncodeError:
|
eq_(len(r), 1) # Because 'foo' is not scanned, they match
|
||||||
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)
|
|
||||||
|
|
||||||
def test_audio_content_scan_compare_sizes_first():
|
def test_tag_scan_converts_to_str(self):
|
||||||
class MyFile(no):
|
s = Scanner()
|
||||||
@property
|
s.scan_type = SCAN_TYPE_TAG
|
||||||
def md5partial(file):
|
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()
|
raise AssertionError()
|
||||||
|
eq_(len(r), 1)
|
||||||
|
|
||||||
s = Scanner()
|
def test_tag_scan_non_ascii(self):
|
||||||
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
|
s = Scanner()
|
||||||
f = [MyFile('foo'), MyFile('bar')]
|
s.scan_type = SCAN_TYPE_TAG
|
||||||
f[0].audiosize = 1
|
s.scanned_tags = set(['title'])
|
||||||
f[1].audiosize = 2
|
o1 = no('foo')
|
||||||
eq_(len(s.GetDupeGroups(f)), 0)
|
o2 = no('bar')
|
||||||
|
o1.title = u'foobar\u00e9'
|
||||||
def test_ignore_list():
|
o2.title = u'foobar\u00e9'
|
||||||
s = Scanner()
|
try:
|
||||||
f1 = no('foobar')
|
r = s.GetDupeGroups([o1, o2])
|
||||||
f2 = no('foobar')
|
except UnicodeEncodeError:
|
||||||
f3 = no('foobar')
|
raise AssertionError()
|
||||||
f1.path = Path('dir1/foobar')
|
eq_(len(r), 1)
|
||||||
f2.path = Path('dir2/foobar')
|
|
||||||
f3.path = Path('dir3/foobar')
|
def test_audio_content_scan(self):
|
||||||
s.ignore_list.Ignore(str(f1.path),str(f2.path))
|
s = Scanner()
|
||||||
s.ignore_list.Ignore(str(f1.path),str(f3.path))
|
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
|
||||||
r = s.GetDupeGroups([f1,f2,f3])
|
f = [no('foo'), no('bar'), no('bleh')]
|
||||||
eq_(len(r), 1)
|
f[0].md5 = 'foo'
|
||||||
g = r[0]
|
f[1].md5 = 'bar'
|
||||||
eq_(len(g.dupes), 1)
|
f[2].md5 = 'bleh'
|
||||||
assert f1 not in g
|
f[0].md5partial = 'foo'
|
||||||
assert f2 in g
|
f[1].md5partial = 'foo'
|
||||||
assert f3 in g
|
f[2].md5partial = 'bleh'
|
||||||
# Ignored matches are not counted as discarded
|
f[0].audiosize = 1
|
||||||
eq_(s.discarded_file_count, 0)
|
f[1].audiosize = 1
|
||||||
|
f[2].audiosize = 1
|
||||||
def test_ignore_list_checks_for_unicode():
|
r = s.GetDupeGroups(f)
|
||||||
#scanner was calling path_str for ignore list checks. Since the Path changes, it must
|
eq_(len(r), 1)
|
||||||
#be unicode(path)
|
eq_(len(r[0]), 2)
|
||||||
s = Scanner()
|
|
||||||
f1 = no('foobar')
|
def test_audio_content_scan_compare_sizes_first(self):
|
||||||
f2 = no('foobar')
|
class MyFile(no):
|
||||||
f3 = no('foobar')
|
@property
|
||||||
f1.path = Path(u'foo1\u00e9')
|
def md5partial(file):
|
||||||
f2.path = Path(u'foo2\u00e9')
|
raise AssertionError()
|
||||||
f3.path = Path(u'foo3\u00e9')
|
|
||||||
s.ignore_list.Ignore(unicode(f1.path),unicode(f2.path))
|
s = Scanner()
|
||||||
s.ignore_list.Ignore(unicode(f1.path),unicode(f3.path))
|
s.scan_type = SCAN_TYPE_CONTENT_AUDIO
|
||||||
r = s.GetDupeGroups([f1,f2,f3])
|
f = [MyFile('foo'), MyFile('bar')]
|
||||||
eq_(len(r), 1)
|
f[0].audiosize = 1
|
||||||
g = r[0]
|
f[1].audiosize = 2
|
||||||
eq_(len(g.dupes), 1)
|
eq_(len(s.GetDupeGroups(f)), 0)
|
||||||
assert f1 not in g
|
|
||||||
assert f2 in g
|
def test_ignore_list(self):
|
||||||
assert f3 in g
|
s = Scanner()
|
||||||
|
f1 = no('foobar')
|
||||||
def test_custom_match_factory():
|
f2 = no('foobar')
|
||||||
class MatchFactory(object):
|
f3 = no('foobar')
|
||||||
def getmatches(self, objects, j=None):
|
f1.path = Path('dir1/foobar')
|
||||||
return [Match(objects[0], objects[1], 420)]
|
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 = Scanner()
|
||||||
s.match_factory = MatchFactory()
|
f1 = FalseNamedObject('foobar')
|
||||||
o1, o2 = no('foo'), no('bar')
|
f2 = FalseNamedObject('foobar')
|
||||||
groups = s.GetDupeGroups([o1, o2])
|
r = s.GetDupeGroups([f1, f2])
|
||||||
eq_(len(groups), 1)
|
eq_(len(r), 1)
|
||||||
g = groups[0]
|
|
||||||
eq_(len(g), 2)
|
def test_size_threshold(self):
|
||||||
g.switch_ref(o1)
|
# Only file equal or higher than the size_threshold in size are scanned
|
||||||
m = g.get_match_of(o2)
|
s = Scanner()
|
||||||
eq_(m, (o1, o2, 420))
|
f1 = no('foo', 1)
|
||||||
|
f2 = no('foo', 2)
|
||||||
def test_file_evaluates_to_false():
|
f3 = no('foo', 3)
|
||||||
# A very wrong way to use any() was added at some point, causing resulting group list
|
s.size_threshold = 2
|
||||||
# to be empty.
|
groups = s.GetDupeGroups([f1,f2,f3])
|
||||||
class FalseNamedObject(NamedObject):
|
eq_(len(groups), 1)
|
||||||
def __nonzero__(self):
|
[group] = groups
|
||||||
return False
|
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])
|
||||||
|
|
||||||
@@ -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()
|
|
||||||
|
|
||||||
@@ -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>
|
|
||||||
@@ -16,21 +16,21 @@ import os.path as op
|
|||||||
from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL
|
from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL
|
||||||
from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox
|
from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox
|
||||||
|
|
||||||
import hsfs as fs
|
|
||||||
from hsutil import job
|
from hsutil import job
|
||||||
from hsutil.reg import RegistrationRequired
|
from hsutil.reg import RegistrationRequired
|
||||||
|
|
||||||
|
from dupeguru import fs
|
||||||
from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY,
|
from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY,
|
||||||
JOB_DELETE)
|
JOB_DELETE)
|
||||||
|
|
||||||
|
from qtlib.about_box import AboutBox
|
||||||
from qtlib.progress import Progress
|
from qtlib.progress import Progress
|
||||||
|
from qtlib.reg import Registration
|
||||||
|
|
||||||
from . import platform
|
from . import platform
|
||||||
|
|
||||||
from .main_window import MainWindow
|
from .main_window import MainWindow
|
||||||
from .directories_dialog import DirectoriesDialog
|
from .directories_dialog import DirectoriesDialog
|
||||||
from .about_box import AboutBox
|
|
||||||
from .reg import Registration
|
|
||||||
|
|
||||||
JOBID2TITLE = {
|
JOBID2TITLE = {
|
||||||
JOB_SCAN: "Scanning for duplicates",
|
JOB_SCAN: "Scanning for duplicates",
|
||||||
@@ -54,6 +54,7 @@ class DupeGuru(DupeGuruBase, QObject):
|
|||||||
LOGO_NAME = '<replace this>'
|
LOGO_NAME = '<replace this>'
|
||||||
NAME = '<replace this>'
|
NAME = '<replace this>'
|
||||||
DELTA_COLUMNS = frozenset()
|
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):
|
def __init__(self, data_module, appid):
|
||||||
appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
|
appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
|
||||||
@@ -144,8 +145,7 @@ class DupeGuru(DupeGuruBase, QObject):
|
|||||||
self.emit(SIGNAL('resultsChanged()'))
|
self.emit(SIGNAL('resultsChanged()'))
|
||||||
|
|
||||||
def ask_for_reg_code(self):
|
def ask_for_reg_code(self):
|
||||||
if self.reg.ask_for_code():
|
self.reg.ask_for_code()
|
||||||
self._setup_ui_as_registered()
|
|
||||||
|
|
||||||
@demo_method
|
@demo_method
|
||||||
def copy_or_move_marked(self, copy):
|
def copy_or_move_marked(self, copy):
|
||||||
@@ -197,7 +197,7 @@ class DupeGuru(DupeGuruBase, QObject):
|
|||||||
|
|
||||||
def rename_dupe(self, dupe, newname):
|
def rename_dupe(self, dupe, newname):
|
||||||
try:
|
try:
|
||||||
dupe.move(dupe.parent, newname)
|
dupe.rename(newname)
|
||||||
return True
|
return True
|
||||||
except (IndexError, fs.FSError) as e:
|
except (IndexError, fs.FSError) as e:
|
||||||
logging.warning("dupeGuru Warning: %s" % unicode(e))
|
logging.warning("dupeGuru Warning: %s" % unicode(e))
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<file alias="logo_se">images/dgse_logo_32.png</file>
|
<file alias="logo_se">images/dgse_logo_32.png</file>
|
||||||
<file alias="logo_se_big">images/dgse_logo_128.png</file>
|
<file alias="logo_se_big">images/dgse_logo_128.png</file>
|
||||||
<file alias="folder">images/folderwin32.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="preferences">images/preferences32.png</file>
|
||||||
<file alias="actions">images/actions32.png</file>
|
<file alias="actions">images/actions32.png</file>
|
||||||
<file alias="delta">images/delta32.png</file>
|
<file alias="delta">images/delta32.png</file>
|
||||||
|
|||||||
@@ -16,9 +16,18 @@
|
|||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTreeView" name="treeView">
|
<widget class="QTreeView" name="treeView">
|
||||||
|
<property name="acceptDrops">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
<property name="editTriggers">
|
<property name="editTriggers">
|
||||||
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
|
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="dragDropOverwriteMode">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="dragDropMode">
|
||||||
|
<enum>QAbstractItemView::DropOnly</enum>
|
||||||
|
</property>
|
||||||
<property name="uniformRowHeights">
|
<property name="uniformRowHeights">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@@ -59,6 +68,9 @@
|
|||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Remove</string>
|
<string>Remove</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>Del</string>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/hs_license
|
# http://www.hardcoded.net/licenses/hs_license
|
||||||
|
|
||||||
from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint
|
import urllib
|
||||||
|
|
||||||
|
from PyQt4.QtCore import QModelIndex, Qt, QRect, QEvent, QPoint, QUrl
|
||||||
from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush
|
from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush
|
||||||
|
|
||||||
from qtlib.tree_model import TreeNode, TreeModel
|
from qtlib.tree_model import TreeNode, TreeModel
|
||||||
@@ -47,19 +49,27 @@ class DirectoryNode(TreeNode):
|
|||||||
return DirectoryNode(self.model, self, ref, row)
|
return DirectoryNode(self.model, self, ref, row)
|
||||||
|
|
||||||
def _getChildren(self):
|
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):
|
class DirectoriesModel(TreeModel):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self._dirs = app.directories
|
self.app = app
|
||||||
|
self.dirs = app.directories
|
||||||
TreeModel.__init__(self)
|
TreeModel.__init__(self)
|
||||||
|
|
||||||
def _createNode(self, ref, row):
|
def _createNode(self, ref, row):
|
||||||
return DirectoryNode(self, None, ref, row)
|
return DirectoryNode(self, None, ref, row)
|
||||||
|
|
||||||
def _getChildren(self):
|
def _getChildren(self):
|
||||||
return self._dirs
|
return self.dirs
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
return 2
|
return 2
|
||||||
@@ -70,23 +80,37 @@ class DirectoriesModel(TreeModel):
|
|||||||
node = index.internalPointer()
|
node = index.internalPointer()
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
if index.column() == 0:
|
if index.column() == 0:
|
||||||
return node.ref.name
|
return node.name
|
||||||
else:
|
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:
|
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:
|
elif role == Qt.ForegroundRole:
|
||||||
state = self._dirs.get_state(node.ref.path)
|
state = self.dirs.get_state(node.ref)
|
||||||
if state == 1:
|
if state == 1:
|
||||||
return QBrush(Qt.blue)
|
return QBrush(Qt.blue)
|
||||||
elif state == 2:
|
elif state == 2:
|
||||||
return QBrush(Qt.red)
|
return QBrush(Qt.red)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def dropMimeData(self, mimeData, action, row, column, parentIndex):
|
||||||
|
# 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):
|
def flags(self, index):
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return 0
|
return Qt.ItemIsEnabled | Qt.ItemIsDropEnabled
|
||||||
result = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
result = Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDropEnabled
|
||||||
if index.column() == 1:
|
if index.column() == 1:
|
||||||
result |= Qt.ItemIsEditable
|
result |= Qt.ItemIsEditable
|
||||||
return result
|
return result
|
||||||
@@ -97,10 +121,18 @@ class DirectoriesModel(TreeModel):
|
|||||||
return HEADERS[section]
|
return HEADERS[section]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def mimeTypes(self):
|
||||||
|
return ['text/uri-list']
|
||||||
|
|
||||||
def setData(self, index, value, role):
|
def setData(self, index, value, role):
|
||||||
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
|
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
|
||||||
return False
|
return False
|
||||||
node = index.internalPointer()
|
node = index.internalPointer()
|
||||||
self._dirs.set_state(node.ref.path, value)
|
self.dirs.set_state(node.ref, value)
|
||||||
return True
|
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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
@@ -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>
|
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
@@ -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 "Submit".</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>
|
|
||||||
@@ -153,7 +153,7 @@ class ResultsModel(TreeModel):
|
|||||||
if index.column() == 0:
|
if index.column() == 0:
|
||||||
value = unicode(value.toString())
|
value = unicode(value.toString())
|
||||||
if self._app.rename_dupe(node.dupe, value):
|
if self._app.rename_dupe(node.dupe, value):
|
||||||
node.reset()
|
node.invalidate()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.1 KiB |
BIN
images/gear.png
BIN
images/gear.png
Binary file not shown.
|
Before Width: | Height: | Size: 394 B |
@@ -13,17 +13,11 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import "PyDupeGuru.h"
|
#import "PyDupeGuru.h"
|
||||||
|
|
||||||
@interface AppDelegate : AppDelegateBase
|
@interface AppDelegate : AppDelegateBase
|
||||||
{
|
{
|
||||||
IBOutlet NSButton *presetsButton;
|
|
||||||
IBOutlet NSPopUpButton *presetsPopup;
|
|
||||||
IBOutlet ResultWindow *result;
|
|
||||||
|
|
||||||
DirectoryPanel *_directoryPanel;
|
DirectoryPanel *_directoryPanel;
|
||||||
}
|
}
|
||||||
- (IBAction)openWebsite:(id)sender;
|
- (IBAction)openWebsite:(id)sender;
|
||||||
- (IBAction)popupPresets:(id)sender;
|
|
||||||
- (IBAction)toggleDirectories:(id)sender;
|
- (IBAction)toggleDirectories:(id)sender;
|
||||||
- (IBAction)usePreset:(id)sender;
|
|
||||||
|
|
||||||
- (DirectoryPanel *)directoryPanel;
|
- (DirectoryPanel *)directoryPanel;
|
||||||
- (PyDupeGuru *)py;
|
- (PyDupeGuru *)py;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import "cocoalib/Utils.h"
|
#import "cocoalib/Utils.h"
|
||||||
#import "cocoalib/ValueTransformers.h"
|
#import "cocoalib/ValueTransformers.h"
|
||||||
#import "cocoalib/Dialogs.h"
|
#import "cocoalib/Dialogs.h"
|
||||||
|
#import "DetailsPanel.h"
|
||||||
#import "Consts.h"
|
#import "Consts.h"
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
@@ -60,50 +61,17 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_me"]];
|
[[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
|
- (IBAction)toggleDirectories:(id)sender
|
||||||
{
|
{
|
||||||
[[self directoryPanel] toggleVisible:sender];
|
[[self directoryPanel] toggleVisible:sender];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (IBAction)usePreset:(id)sender
|
|
||||||
|
- (DetailsPanelBase *)detailsPanel
|
||||||
{
|
{
|
||||||
NSUserDefaultsController *ud = [NSUserDefaultsController sharedUserDefaultsController];
|
if (!_detailsPanel)
|
||||||
[ud revertToInitialValues:nil];
|
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
|
||||||
NSUserDefaults *d = [ud defaults];
|
return _detailsPanel;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (DirectoryPanel *)directoryPanel
|
- (DirectoryPanel *)directoryPanel
|
||||||
@@ -115,23 +83,6 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (PyDupeGuru *)py { return (PyDupeGuru *)py; }
|
- (PyDupeGuru *)py { return (PyDupeGuru *)py; }
|
||||||
|
|
||||||
//Delegate
|
//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
|
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
||||||
{
|
{
|
||||||
if (![[result window] isVisible])
|
if (![[result window] isVisible])
|
||||||
|
|||||||
18
me/cocoa/English.lproj/Details.nib/classes.nib
generated
18
me/cocoa/English.lproj/Details.nib/classes.nib
generated
@@ -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;
|
|
||||||
}
|
|
||||||
16
me/cocoa/English.lproj/Details.nib/info.nib
generated
16
me/cocoa/English.lproj/Details.nib/info.nib
generated
@@ -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>
|
|
||||||
BIN
me/cocoa/English.lproj/Details.nib/keyedobjects.nib
generated
BIN
me/cocoa/English.lproj/Details.nib/keyedobjects.nib
generated
Binary file not shown.
64
me/cocoa/English.lproj/Directories.nib/classes.nib
generated
64
me/cocoa/English.lproj/Directories.nib/classes.nib
generated
@@ -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>
|
|
||||||
20
me/cocoa/English.lproj/Directories.nib/info.nib
generated
20
me/cocoa/English.lproj/Directories.nib/info.nib
generated
@@ -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>
|
|
||||||
BIN
me/cocoa/English.lproj/Directories.nib/keyedobjects.nib
generated
BIN
me/cocoa/English.lproj/Directories.nib/keyedobjects.nib
generated
Binary file not shown.
Binary file not shown.
257
me/cocoa/English.lproj/MainMenu.nib/classes.nib
generated
257
me/cocoa/English.lproj/MainMenu.nib/classes.nib
generated
@@ -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>
|
|
||||||
20
me/cocoa/English.lproj/MainMenu.nib/info.nib
generated
20
me/cocoa/English.lproj/MainMenu.nib/info.nib
generated
@@ -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>
|
|
||||||
BIN
me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib
generated
BIN
me/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib
generated
Binary file not shown.
@@ -28,6 +28,8 @@
|
|||||||
<string>MainMenu</string>
|
<string>MainMenu</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© Hardcoded Software, 2009</string>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
|
<string>http://www.hardcoded.net/updates/dupeguru_me.appcast</string>
|
||||||
<key>SUPublicDSAKeyFile</key>
|
<key>SUPublicDSAKeyFile</key>
|
||||||
|
|||||||
@@ -9,19 +9,13 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "cocoalib/Outline.h"
|
#import "cocoalib/Outline.h"
|
||||||
#import "dgbase/ResultWindow.h"
|
#import "dgbase/ResultWindow.h"
|
||||||
#import "DetailsPanel.h"
|
|
||||||
#import "DirectoryPanel.h"
|
#import "DirectoryPanel.h"
|
||||||
|
|
||||||
@interface ResultWindow : ResultWindowBase
|
@interface ResultWindow : ResultWindowBase
|
||||||
{
|
{
|
||||||
IBOutlet NSPopUpButton *actionMenu;
|
|
||||||
IBOutlet NSMenu *columnsMenu;
|
|
||||||
IBOutlet NSSearchField *filterField;
|
IBOutlet NSSearchField *filterField;
|
||||||
IBOutlet NSWindow *preferencesPanel;
|
|
||||||
|
|
||||||
NSString *_lastAction;
|
NSString *_lastAction;
|
||||||
DetailsPanel *_detailsPanel;
|
|
||||||
NSMutableArray *_resultColumns;
|
|
||||||
NSMutableIndexSet *_deltaColumns;
|
NSMutableIndexSet *_deltaColumns;
|
||||||
}
|
}
|
||||||
- (IBAction)clearIgnoreList:(id)sender;
|
- (IBAction)clearIgnoreList:(id)sender;
|
||||||
@@ -38,15 +32,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (IBAction)removeMarked:(id)sender;
|
- (IBAction)removeMarked:(id)sender;
|
||||||
- (IBAction)removeSelected:(id)sender;
|
- (IBAction)removeSelected:(id)sender;
|
||||||
- (IBAction)renameSelected:(id)sender;
|
- (IBAction)renameSelected:(id)sender;
|
||||||
- (IBAction)resetColumnsToDefault:(id)sender;
|
|
||||||
- (IBAction)revealSelected:(id)sender;
|
- (IBAction)revealSelected:(id)sender;
|
||||||
- (IBAction)showPreferencesPanel:(id)sender;
|
|
||||||
- (IBAction)startDuplicateScan:(id)sender;
|
- (IBAction)startDuplicateScan:(id)sender;
|
||||||
- (IBAction)toggleColumn:(id)sender;
|
|
||||||
- (IBAction)toggleDelta:(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
|
@end
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (void)awakeFromNib
|
- (void)awakeFromNib
|
||||||
{
|
{
|
||||||
[super awakeFromNib];
|
[super awakeFromNib];
|
||||||
_detailsPanel = nil;
|
[[self window] setTitle:@"dupeGuru Music Edition"];
|
||||||
_displayDelta = NO;
|
_displayDelta = NO;
|
||||||
_powerMode = NO;
|
_powerMode = NO;
|
||||||
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,7)] retain];
|
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,7)] retain];
|
||||||
@@ -29,23 +29,8 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[py setDisplayDeltaValues:b2n(_displayDelta)];
|
[py setDisplayDeltaValues:b2n(_displayDelta)];
|
||||||
[matches setTarget:self];
|
[matches setTarget:self];
|
||||||
[matches setDoubleAction:@selector(openSelected:)];
|
[matches setDoubleAction:@selector(openSelected:)];
|
||||||
[[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]];
|
|
||||||
[self initResultColumns];
|
|
||||||
[self refreshStats];
|
[self refreshStats];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
|
[[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 */
|
/* Actions */
|
||||||
@@ -182,11 +167,6 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[py revealSelected];
|
[py revealSelected];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (IBAction)showPreferencesPanel:(id)sender
|
|
||||||
{
|
|
||||||
[preferencesPanel makeKeyAndOrderFront:sender];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction)startDuplicateScan:(id)sender
|
- (IBAction)startDuplicateScan:(id)sender
|
||||||
{
|
{
|
||||||
if ([matches numberOfRows] > 0)
|
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
|
- (IBAction)toggleDelta:(id)sender
|
||||||
{
|
{
|
||||||
if ([deltaSwitch selectedSegment] == 1)
|
if ([deltaSwitch selectedSegment] == 1)
|
||||||
@@ -248,39 +208,22 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[self changeDelta:sender];
|
[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 */
|
/* 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
|
- (void)initResultColumns
|
||||||
{
|
{
|
||||||
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];
|
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];
|
||||||
_resultColumns = [[NSMutableArray alloc] init];
|
_resultColumns = [[NSMutableArray alloc] init];
|
||||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name
|
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]];
|
[_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]];
|
||||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size
|
NSTableColumn *sizeCol = [self getColumnForIdentifier:2 title:@"Size (MB)" width:63 refCol:refCol];
|
||||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"3"]]; // Time
|
[[sizeCol dataCell] setAlignment:NSRightTextAlignment];
|
||||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"4"]]; // Bitrate
|
[_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:5 title:@"Sample Rate" width:60 refCol:refCol]];
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Kind" width:40 refCol:refCol]];
|
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Kind" width:40 refCol:refCol]];
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:7 title:@"Creation" width:120 refCol:refCol]];
|
[_resultColumns addObject:[self getColumnForIdentifier: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:13 title:@"Year" width:40 refCol:refCol]];
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Track Number" width:40 refCol:refCol]];
|
[_resultColumns addObject:[self getColumnForIdentifier:14 title:@"Track Number" width:40 refCol:refCol]];
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:15 title:@"Comment" width:120 refCol:refCol]];
|
[_resultColumns addObject:[self getColumnForIdentifier: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:17 title:@"Words Used" width:120 refCol:refCol]];
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:18 title:@"Dupe Count" width:80 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 */
|
/* Delegate */
|
||||||
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,8 +19,6 @@
|
|||||||
/* End PBXAppleScriptBuildPhase section */
|
/* End PBXAppleScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXBuildFile 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 = (); }; };
|
8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
|
||||||
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
|
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
|
||||||
CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */; };
|
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 */; };
|
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; };
|
||||||
CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.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 */; };
|
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 */; };
|
CE49DEF60FDFEB810098617B /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF30FDFEB810098617B /* BRSingleLineFormatter.m */; };
|
||||||
CE49DEF70FDFEB810098617B /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE49DEF50FDFEB810098617B /* NSCharacterSet_Extensions.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 */; };
|
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 */; };
|
CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; };
|
||||||
CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */; };
|
CE6E0E9F1054EB97008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */; };
|
||||||
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
|
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 */; };
|
CE900AD2109B238600754048 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD1109B238600754048 /* Preferences.xib */; };
|
||||||
CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; };
|
CE900AD7109B2A9B00754048 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE900AD6109B2A9B00754048 /* MainMenu.xib */; };
|
||||||
CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; };
|
CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; };
|
||||||
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; };
|
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 */; };
|
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 */; };
|
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; };
|
||||||
CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; };
|
CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; };
|
||||||
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; };
|
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; };
|
||||||
@@ -79,11 +76,9 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
CE900AD1109B238600754048 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = ../../xib/Preferences.xib; sourceTree = "<group>"; };
|
||||||
CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; 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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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; };
|
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; };
|
CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; };
|
||||||
@@ -231,16 +225,13 @@
|
|||||||
29B97317FDCFA39411CA2CEA /* Resources */ = {
|
29B97317FDCFA39411CA2CEA /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
CE3FBDD01094637800B72D77 /* xib */,
|
||||||
CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */,
|
CE073F5409CAE1A3005C1D2F /* dupeguru_me_help */,
|
||||||
CE381CF509915304003581CE /* dg_cocoa.plugin */,
|
CE381CF509915304003581CE /* dg_cocoa.plugin */,
|
||||||
CEFC294309C89E0000D9F998 /* images */,
|
CEFC294309C89E0000D9F998 /* images */,
|
||||||
CEEB135109C837A2004D2330 /* dupeguru.icns */,
|
CEEB135109C837A2004D2330 /* dupeguru.icns */,
|
||||||
8D1107310486CEB800E47090 /* Info.plist */,
|
8D1107310486CEB800E47090 /* Info.plist */,
|
||||||
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
|
|
||||||
CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */,
|
CE6E0E9E1054EB97008D9390 /* dsa_pub.pem */,
|
||||||
CECA899709DB12CA00A3D774 /* Details.nib */,
|
|
||||||
CE3AA46509DB207900DB3A21 /* Directories.nib */,
|
|
||||||
29B97318FDCFA39411CA2CEA /* MainMenu.nib */,
|
|
||||||
);
|
);
|
||||||
name = Resources;
|
name = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -254,6 +245,18 @@
|
|||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
CE49DEF10FDFEB810098617B /* brsinglelineformatter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -316,9 +319,6 @@
|
|||||||
CEFC294309C89E0000D9F998 /* images */ = {
|
CEFC294309C89E0000D9F998 /* images */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CEA7D2C40FDFED340037CD8C /* dgme_logo_32.png */,
|
|
||||||
CED2A6870A05102600AC4C3F /* power_marker32.png */,
|
|
||||||
CEF7823709C8AA0200EF38FF /* gear.png */,
|
|
||||||
CEFC295309C89FF200D9F998 /* details32.png */,
|
CEFC295309C89FF200D9F998 /* details32.png */,
|
||||||
CEFC295409C89FF200D9F998 /* preferences32.png */,
|
CEFC295409C89FF200D9F998 /* preferences32.png */,
|
||||||
CEFC294509C89E3D00D9F998 /* folder32.png */,
|
CEFC294509C89E3D00D9F998 /* folder32.png */,
|
||||||
@@ -371,23 +371,20 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */,
|
|
||||||
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
|
|
||||||
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */,
|
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */,
|
||||||
CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */,
|
CE073F6309CAE1A3005C1D2F /* dupeguru_me_help in Resources */,
|
||||||
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */,
|
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */,
|
||||||
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */,
|
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */,
|
||||||
CEFC295509C89FF200D9F998 /* details32.png in Resources */,
|
CEFC295509C89FF200D9F998 /* details32.png in Resources */,
|
||||||
CEFC295609C89FF200D9F998 /* preferences32.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 */,
|
CE515E020FC6C13E00EC695D /* ErrorReportWindow.xib in Resources */,
|
||||||
CE515E030FC6C13E00EC695D /* progress.nib in Resources */,
|
CE515E030FC6C13E00EC695D /* progress.nib in Resources */,
|
||||||
CE515E040FC6C13E00EC695D /* registration.nib in Resources */,
|
CE515E040FC6C13E00EC695D /* registration.nib in Resources */,
|
||||||
CEA7D2C50FDFED340037CD8C /* dgme_logo_32.png in Resources */,
|
|
||||||
CE6E0E9F1054EB97008D9390 /* dsa_pub.pem 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;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -424,30 +421,6 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup 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 */ = {
|
CE515DFC0FC6C13E00EC695D /* ErrorReportWindow.xib */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -472,38 +445,9 @@
|
|||||||
name = registration.nib;
|
name = registration.nib;
|
||||||
sourceTree = SOURCE_ROOT;
|
sourceTree = SOURCE_ROOT;
|
||||||
};
|
};
|
||||||
CECA899709DB12CA00A3D774 /* Details.nib */ = {
|
|
||||||
isa = PBXVariantGroup;
|
|
||||||
children = (
|
|
||||||
CECA899809DB12CA00A3D774 /* English */,
|
|
||||||
);
|
|
||||||
name = Details.nib;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 */ = {
|
C01FCF4C08A954540054247B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -521,32 +465,16 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
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 */ = {
|
C01FCF5008A954540054247B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)";
|
ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)";
|
||||||
ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386";
|
ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386";
|
||||||
COPY_PHASE_STRIP = NO;
|
|
||||||
GCC_C_LANGUAGE_STANDARD = c99;
|
GCC_C_LANGUAGE_STANDARD = c99;
|
||||||
GCC_VERSION = 4.0;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.4;
|
MACOSX_DEPLOYMENT_TARGET = 10.5;
|
||||||
PREBINDING = NO;
|
SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk";
|
||||||
SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk";
|
|
||||||
STRIP_INSTALLED_PRODUCT = NO;
|
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -556,7 +484,6 @@
|
|||||||
C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = {
|
C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
C01FCF4B08A954540054247B /* Debug */,
|
|
||||||
C01FCF4C08A954540054247B /* Release */,
|
C01FCF4C08A954540054247B /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@@ -565,7 +492,6 @@
|
|||||||
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = {
|
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
C01FCF4F08A954540054247B /* Debug */,
|
|
||||||
C01FCF5008A954540054247B /* Release */,
|
C01FCF5008A954540054247B /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
|
|||||||
@@ -8,12 +8,13 @@
|
|||||||
import objc
|
import objc
|
||||||
from AppKit import *
|
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
|
# Fix py2app imports which chokes on relative imports
|
||||||
from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner
|
from dupeguru_me import app_cocoa, data, fs, scanner
|
||||||
from hsfs import auto, stats, tree, music
|
from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner, fs
|
||||||
from hsfs.phys import music
|
|
||||||
from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma
|
from hsmedia import aiff, flac, genres, id3v1, id3v2, mp4, mpeg, ogg, wma
|
||||||
from hsutil import conflict
|
from hsutil import conflict
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ class PyApp(NSObject):
|
|||||||
class PyDupeGuru(PyApp):
|
class PyDupeGuru(PyApp):
|
||||||
def init(self):
|
def init(self):
|
||||||
self = super(PyDupeGuru,self).init()
|
self = super(PyDupeGuru,self).init()
|
||||||
self.app = app_me_cocoa.DupeGuruME()
|
self.app = DupeGuruME()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
#---Directories
|
#---Directories
|
||||||
@@ -180,12 +181,12 @@ class PyDupeGuru(PyApp):
|
|||||||
def setScanType_(self, scan_type):
|
def setScanType_(self, scan_type):
|
||||||
try:
|
try:
|
||||||
self.app.scanner.scan_type = [
|
self.app.scanner.scan_type = [
|
||||||
scanner.SCAN_TYPE_FILENAME,
|
SCAN_TYPE_FILENAME,
|
||||||
scanner.SCAN_TYPE_FIELDS,
|
SCAN_TYPE_FIELDS,
|
||||||
scanner.SCAN_TYPE_FIELDS_NO_ORDER,
|
SCAN_TYPE_FIELDS_NO_ORDER,
|
||||||
scanner.SCAN_TYPE_TAG,
|
SCAN_TYPE_TAG,
|
||||||
scanner.SCAN_TYPE_CONTENT,
|
SCAN_TYPE_CONTENT,
|
||||||
scanner.SCAN_TYPE_CONTENT_AUDIO
|
SCAN_TYPE_CONTENT_AUDIO
|
||||||
][scan_type]
|
][scan_type]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ if op.exists('build'):
|
|||||||
if op.exists('dist'):
|
if op.exists('dist'):
|
||||||
shutil.rmtree('dist')
|
shutil.rmtree('dist')
|
||||||
|
|
||||||
|
os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.5'
|
||||||
print_and_do('python -u setup.py py2app')
|
print_and_do('python -u setup.py py2app')
|
||||||
2611
me/cocoa/xib/Preferences.xib
Normal file
2611
me/cocoa/xib/Preferences.xib
Normal file
File diff suppressed because it is too large
Load Diff
0
me/py/__init__.py
Normal file
0
me/py/__init__.py
Normal file
@@ -7,29 +7,29 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/hs_license
|
# http://www.hardcoded.net/licenses/hs_license
|
||||||
|
|
||||||
import os.path as op
|
|
||||||
import logging
|
import logging
|
||||||
from appscript import app, k, CommandError
|
from appscript import app, k, CommandError
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from hsutil.cocoa import as_fetch
|
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_REMOVE_DEAD_TRACKS = 'jobRemoveDeadTracks'
|
||||||
JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks'
|
JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks'
|
||||||
|
|
||||||
app_cocoa.JOBID2TITLE.update({
|
JOBID2TITLE.update({
|
||||||
JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library",
|
JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library",
|
||||||
JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library",
|
JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library",
|
||||||
})
|
})
|
||||||
|
|
||||||
class DupeGuruME(app_cocoa.DupeGuru):
|
class DupeGuruME(DupeGuruBase):
|
||||||
def __init__(self):
|
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.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 = []
|
self.dead_tracks = []
|
||||||
|
|
||||||
def remove_dead_tracks(self):
|
def remove_dead_tracks(self):
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
# http://www.hardcoded.net/licenses/hs_license
|
# http://www.hardcoded.net/licenses/hs_license
|
||||||
|
|
||||||
from hsutil.str import format_time, FT_MINUTES, format_size
|
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)
|
format_dupe_count, cmp_value)
|
||||||
|
|
||||||
COLUMNS = [
|
COLUMNS = [
|
||||||
@@ -76,7 +76,7 @@ def GetDisplayInfo(dupe, group, delta):
|
|||||||
str(dupe.track),
|
str(dupe.track),
|
||||||
dupe.comment,
|
dupe.comment,
|
||||||
format_perc(percentage),
|
format_perc(percentage),
|
||||||
format_words(dupe.words),
|
format_words(dupe.words) if hasattr(dupe, 'words') else '',
|
||||||
format_dupe_count(dupe_count)
|
format_dupe_count(dupe_count)
|
||||||
]
|
]
|
||||||
|
|
||||||
183
me/py/fs.py
Normal file
183
me/py/fs.py
Normal 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
16
me/py/scanner.py
Normal 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
0
me/py/tests/__init__.py
Normal file
33
me/py/tests/scanner_test.py
Normal file
33
me/py/tests/scanner_test.py
Normal 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
|
||||||
@@ -7,9 +7,7 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/hs_license
|
# http://www.hardcoded.net/licenses/hs_license
|
||||||
|
|
||||||
import hsfs.phys.music
|
from dupeguru_me import data, scanner, fs
|
||||||
|
|
||||||
from dupeguru import data_me, scanner
|
|
||||||
|
|
||||||
from base.app import DupeGuru as DupeGuruBase
|
from base.app import DupeGuru as DupeGuruBase
|
||||||
from details_dialog import DetailsDialog
|
from details_dialog import DetailsDialog
|
||||||
@@ -23,11 +21,11 @@ class DupeGuru(DupeGuruBase):
|
|||||||
DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8])
|
DELTA_COLUMNS = frozenset([2, 3, 4, 5, 7, 8])
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
DupeGuruBase.__init__(self, data_me, appid=1)
|
DupeGuruBase.__init__(self, data, appid=1)
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
self.scanner = scanner.ScannerME()
|
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)
|
DupeGuruBase._setup(self)
|
||||||
|
|
||||||
def _update_options(self):
|
def _update_options(self):
|
||||||
|
|||||||
@@ -8,20 +8,14 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "dgbase/AppDelegate.h"
|
#import "dgbase/AppDelegate.h"
|
||||||
#import "ResultWindow.h"
|
|
||||||
#import "DirectoryPanel.h"
|
#import "DirectoryPanel.h"
|
||||||
#import "DetailsPanel.h"
|
|
||||||
#import "PyDupeGuru.h"
|
#import "PyDupeGuru.h"
|
||||||
|
|
||||||
@interface AppDelegate : AppDelegateBase
|
@interface AppDelegate : AppDelegateBase
|
||||||
{
|
{
|
||||||
IBOutlet ResultWindow *result;
|
|
||||||
|
|
||||||
DetailsPanel *_detailsPanel;
|
|
||||||
DirectoryPanel *_directoryPanel;
|
DirectoryPanel *_directoryPanel;
|
||||||
}
|
}
|
||||||
- (IBAction)openWebsite:(id)sender;
|
- (IBAction)openWebsite:(id)sender;
|
||||||
- (IBAction)toggleDetailsPanel:(id)sender;
|
|
||||||
- (IBAction)toggleDirectories:(id)sender;
|
- (IBAction)toggleDirectories:(id)sender;
|
||||||
|
|
||||||
- (DirectoryPanel *)directoryPanel;
|
- (DirectoryPanel *)directoryPanel;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import "Utils.h"
|
#import "Utils.h"
|
||||||
#import "ValueTransformers.h"
|
#import "ValueTransformers.h"
|
||||||
#import "Consts.h"
|
#import "Consts.h"
|
||||||
|
#import "DetailsPanel.h"
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
+ (void)initialize
|
+ (void)initialize
|
||||||
@@ -36,29 +37,22 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
_directoryPanel = nil;
|
_directoryPanel = nil;
|
||||||
_detailsPanel = nil;
|
|
||||||
_appName = APPNAME;
|
_appName = APPNAME;
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (DetailsPanelBase *)detailsPanel
|
||||||
|
{
|
||||||
|
if (!_detailsPanel)
|
||||||
|
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
|
||||||
|
return _detailsPanel;
|
||||||
|
}
|
||||||
|
|
||||||
- (IBAction)openWebsite:(id)sender
|
- (IBAction)openWebsite:(id)sender
|
||||||
{
|
{
|
||||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_pe"]];
|
[[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
|
- (IBAction)toggleDirectories:(id)sender
|
||||||
{
|
{
|
||||||
[[self directoryPanel] toggleVisible:sender];
|
[[self directoryPanel] toggleVisible:sender];
|
||||||
@@ -75,19 +69,12 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
//Delegate
|
//Delegate
|
||||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
||||||
{
|
{
|
||||||
[[ProgressController mainProgressController] setWorker:py];
|
NSMenu *actionsMenu = [[[NSApp mainMenu] itemWithTitle:@"Actions"] submenu];
|
||||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
// index 2 is just after "Clear Ingore List"
|
||||||
//Restore Columns
|
NSMenuItem *mi = [actionsMenu insertItemWithTitle:@"Clear Picture Cache" action:@selector(clearPictureCache:) keyEquivalent:@"P" atIndex:2];
|
||||||
NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"];
|
[mi setTarget:result];
|
||||||
NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"];
|
[mi setKeyEquivalentModifierMask:NSCommandKeyMask|NSShiftKeyMask];
|
||||||
if ([columnsOrder count])
|
[super applicationDidFinishLaunching:aNotification];
|
||||||
[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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
||||||
|
|||||||
24
pe/cocoa/English.lproj/Details.nib/classes.nib
generated
24
pe/cocoa/English.lproj/Details.nib/classes.nib
generated
@@ -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;
|
|
||||||
}
|
|
||||||
16
pe/cocoa/English.lproj/Details.nib/info.nib
generated
16
pe/cocoa/English.lproj/Details.nib/info.nib
generated
@@ -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>
|
|
||||||
BIN
pe/cocoa/English.lproj/Details.nib/keyedobjects.nib
generated
BIN
pe/cocoa/English.lproj/Details.nib/keyedobjects.nib
generated
Binary file not shown.
62
pe/cocoa/English.lproj/Directories.nib/classes.nib
generated
62
pe/cocoa/English.lproj/Directories.nib/classes.nib
generated
@@ -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>
|
|
||||||
20
pe/cocoa/English.lproj/Directories.nib/info.nib
generated
20
pe/cocoa/English.lproj/Directories.nib/info.nib
generated
@@ -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>
|
|
||||||
BIN
pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib
generated
BIN
pe/cocoa/English.lproj/Directories.nib/keyedobjects.nib
generated
Binary file not shown.
Binary file not shown.
235
pe/cocoa/English.lproj/MainMenu.nib/classes.nib
generated
235
pe/cocoa/English.lproj/MainMenu.nib/classes.nib
generated
@@ -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>
|
|
||||||
20
pe/cocoa/English.lproj/MainMenu.nib/info.nib
generated
20
pe/cocoa/English.lproj/MainMenu.nib/info.nib
generated
@@ -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>
|
|
||||||
BIN
pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib
generated
BIN
pe/cocoa/English.lproj/MainMenu.nib/keyedobjects.nib
generated
Binary file not shown.
@@ -23,11 +23,13 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>hsft</string>
|
<string>hsft</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.7.7</string>
|
<string>1.8.0b</string>
|
||||||
<key>NSMainNibFile</key>
|
<key>NSMainNibFile</key>
|
||||||
<string>MainMenu</string>
|
<string>MainMenu</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>© Hardcoded Software, 2009</string>
|
||||||
<key>SUFeedURL</key>
|
<key>SUFeedURL</key>
|
||||||
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
|
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
|
||||||
<key>SUPublicDSAKeyFile</key>
|
<key>SUPublicDSAKeyFile</key>
|
||||||
|
|||||||
@@ -9,16 +9,11 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "Outline.h"
|
#import "Outline.h"
|
||||||
#import "dgbase/ResultWindow.h"
|
#import "dgbase/ResultWindow.h"
|
||||||
#import "DirectoryPanel.h"
|
|
||||||
|
|
||||||
@interface ResultWindow : ResultWindowBase
|
@interface ResultWindow : ResultWindowBase
|
||||||
{
|
{
|
||||||
IBOutlet NSPopUpButton *actionMenu;
|
|
||||||
IBOutlet NSMenu *columnsMenu;
|
|
||||||
IBOutlet NSSearchField *filterField;
|
IBOutlet NSSearchField *filterField;
|
||||||
IBOutlet NSWindow *preferencesPanel;
|
|
||||||
|
|
||||||
NSMutableArray *_resultColumns;
|
|
||||||
NSMutableIndexSet *_deltaColumns;
|
NSMutableIndexSet *_deltaColumns;
|
||||||
}
|
}
|
||||||
- (IBAction)clearIgnoreList:(id)sender;
|
- (IBAction)clearIgnoreList:(id)sender;
|
||||||
@@ -35,16 +30,8 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (IBAction)removeMarked:(id)sender;
|
- (IBAction)removeMarked:(id)sender;
|
||||||
- (IBAction)removeSelected:(id)sender;
|
- (IBAction)removeSelected:(id)sender;
|
||||||
- (IBAction)renameSelected:(id)sender;
|
- (IBAction)renameSelected:(id)sender;
|
||||||
- (IBAction)resetColumnsToDefault:(id)sender;
|
|
||||||
- (IBAction)revealSelected:(id)sender;
|
- (IBAction)revealSelected:(id)sender;
|
||||||
- (IBAction)showPreferencesPanel:(id)sender;
|
|
||||||
- (IBAction)startDuplicateScan:(id)sender;
|
- (IBAction)startDuplicateScan:(id)sender;
|
||||||
- (IBAction)toggleColumn:(id)sender;
|
|
||||||
- (IBAction)toggleDelta:(id)sender;
|
- (IBAction)toggleDelta:(id)sender;
|
||||||
- (IBAction)toggleDetailsPanel:(id)sender;
|
|
||||||
- (IBAction)toggleDirectories:(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
|
@end
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
- (void)awakeFromNib
|
- (void)awakeFromNib
|
||||||
{
|
{
|
||||||
[super awakeFromNib];
|
[super awakeFromNib];
|
||||||
|
[[self window] setTitle:@"dupeGuru Picture Edition"];
|
||||||
_displayDelta = NO;
|
_displayDelta = NO;
|
||||||
_powerMode = NO;
|
_powerMode = NO;
|
||||||
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)] retain];
|
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)] retain];
|
||||||
@@ -29,23 +30,8 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[py setDisplayDeltaValues:b2n(_displayDelta)];
|
[py setDisplayDeltaValues:b2n(_displayDelta)];
|
||||||
[matches setTarget:self];
|
[matches setTarget:self];
|
||||||
[matches setDoubleAction:@selector(openSelected:)];
|
[matches setDoubleAction:@selector(openSelected:)];
|
||||||
[[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]];
|
|
||||||
[self initResultColumns];
|
|
||||||
[self refreshStats];
|
[self refreshStats];
|
||||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
|
[[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 */
|
/* Actions */
|
||||||
@@ -170,7 +156,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[columnsOrder addObject:@"4"];
|
[columnsOrder addObject:@"4"];
|
||||||
[columnsOrder addObject:@"7"];
|
[columnsOrder addObject:@"7"];
|
||||||
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
|
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(120) forKey:@"1"];
|
||||||
[columnsWidth setObject:i2n(63) forKey:@"2"];
|
[columnsWidth setObject:i2n(63) forKey:@"2"];
|
||||||
[columnsWidth setObject:i2n(73) forKey:@"4"];
|
[columnsWidth setObject:i2n(73) forKey:@"4"];
|
||||||
@@ -184,11 +170,6 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[py revealSelected];
|
[py revealSelected];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (IBAction)showPreferencesPanel:(id)sender
|
|
||||||
{
|
|
||||||
[preferencesPanel makeKeyAndOrderFront:sender];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction)startDuplicateScan:(id)sender
|
- (IBAction)startDuplicateScan:(id)sender
|
||||||
{
|
{
|
||||||
if ([matches numberOfRows] > 0)
|
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
|
- (IBAction)toggleDelta:(id)sender
|
||||||
{
|
{
|
||||||
if ([deltaSwitch selectedSegment] == 1)
|
if ([deltaSwitch selectedSegment] == 1)
|
||||||
@@ -244,75 +205,29 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
[self changeDelta:sender];
|
[self changeDelta:sender];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (IBAction)toggleDetailsPanel:(id)sender
|
|
||||||
{
|
|
||||||
[(AppDelegate *)app toggleDetailsPanel:sender];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (IBAction)toggleDirectories:(id)sender
|
- (IBAction)toggleDirectories:(id)sender
|
||||||
{
|
{
|
||||||
[(AppDelegate *)app toggleDirectories:sender];
|
[(AppDelegate *)app toggleDirectories:sender];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public */
|
/* 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
|
- (void)initResultColumns
|
||||||
{
|
{
|
||||||
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];
|
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];
|
||||||
_resultColumns = [[NSMutableArray alloc] init];
|
_resultColumns = [[NSMutableArray alloc] init];
|
||||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name
|
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name
|
||||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"1"]]; // Directory
|
[_resultColumns addObject:[self getColumnForIdentifier:1 title:@"Directory" width:120 refCol:refCol]];
|
||||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size
|
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:3 title:@"Kind" width:40 refCol:refCol]];
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Dimensions" width:80 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:5 title:@"Creation" width:120 refCol:refCol]];
|
||||||
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Modification" 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]];
|
[_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 */
|
/* Delegate */
|
||||||
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 42;
|
objectVersion = 46;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile 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 = (); }; };
|
8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
|
||||||
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
|
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 */; };
|
CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */; };
|
||||||
CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C46A90FA0647E000BE99B /* PictureBlocks.m */; };
|
CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C46A90FA0647E000BE99B /* PictureBlocks.m */; };
|
||||||
CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; };
|
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 */; };
|
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; };
|
||||||
CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.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 */; };
|
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 */; };
|
CE6044EC0FE6796200B71262 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6044EB0FE6796200B71262 /* DetailsPanel.m */; };
|
||||||
CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.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 */; };
|
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 */; };
|
CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; };
|
||||||
CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; };
|
CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; };
|
||||||
CE80DB300FC192D60086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB200FC192D60086DCA6 /* Outline.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 */; };
|
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
|
||||||
CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */; };
|
CEBAE4270FDA97E000B7887D /* BRSingleLineFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4240FDA97E000B7887D /* BRSingleLineFormatter.m */; };
|
||||||
CEBAE4280FDA97E000B7887D /* NSCharacterSet_Extensions.m in Sources */ = {isa = PBXBuildFile; fileRef = CEBAE4260FDA97E000B7887D /* NSCharacterSet_Extensions.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 */; };
|
CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; };
|
||||||
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; };
|
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; };
|
||||||
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; };
|
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 */; };
|
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; };
|
||||||
CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; };
|
CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; };
|
||||||
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; };
|
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; };
|
||||||
@@ -69,15 +68,15 @@
|
|||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
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; };
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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>"; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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; };
|
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; };
|
CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; };
|
||||||
@@ -228,16 +226,13 @@
|
|||||||
29B97317FDCFA39411CA2CEA /* Resources */ = {
|
29B97317FDCFA39411CA2CEA /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
CE77C89A10946C6D0078B0DB /* xib */,
|
||||||
CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */,
|
CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */,
|
||||||
CE381CF509915304003581CE /* dg_cocoa.plugin */,
|
CE381CF509915304003581CE /* dg_cocoa.plugin */,
|
||||||
CEFC294309C89E0000D9F998 /* images */,
|
CEFC294309C89E0000D9F998 /* images */,
|
||||||
CEEB135109C837A2004D2330 /* dupeguru.icns */,
|
CEEB135109C837A2004D2330 /* dupeguru.icns */,
|
||||||
8D1107310486CEB800E47090 /* Info.plist */,
|
8D1107310486CEB800E47090 /* Info.plist */,
|
||||||
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
|
|
||||||
CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */,
|
CE6E0F3C1054EC62008D9390 /* dsa_pub.pem */,
|
||||||
CECA899709DB12CA00A3D774 /* Details.nib */,
|
|
||||||
CE3AA46509DB207900DB3A21 /* Directories.nib */,
|
|
||||||
29B97318FDCFA39411CA2CEA /* MainMenu.nib */,
|
|
||||||
);
|
);
|
||||||
name = Resources;
|
name = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -251,6 +246,18 @@
|
|||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -318,7 +325,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */,
|
CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */,
|
||||||
CEF7823709C8AA0200EF38FF /* gear.png */,
|
|
||||||
CEFC295309C89FF200D9F998 /* details32.png */,
|
CEFC295309C89FF200D9F998 /* details32.png */,
|
||||||
CEFC295409C89FF200D9F998 /* preferences32.png */,
|
CEFC295409C89FF200D9F998 /* preferences32.png */,
|
||||||
CEFC294509C89E3D00D9F998 /* folder32.png */,
|
CEFC294509C89E3D00D9F998 /* folder32.png */,
|
||||||
@@ -354,7 +360,7 @@
|
|||||||
29B97313FDCFA39411CA2CEA /* Project object */ = {
|
29B97313FDCFA39411CA2CEA /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
|
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
|
||||||
compatibilityVersion = "Xcode 2.4";
|
compatibilityVersion = "Xcode 3.2";
|
||||||
hasScannedForEncodings = 1;
|
hasScannedForEncodings = 1;
|
||||||
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
|
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -370,22 +376,21 @@
|
|||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */,
|
|
||||||
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
|
|
||||||
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */,
|
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */,
|
||||||
CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */,
|
CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */,
|
||||||
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */,
|
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */,
|
||||||
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */,
|
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */,
|
||||||
CEFC295509C89FF200D9F998 /* details32.png in Resources */,
|
CEFC295509C89FF200D9F998 /* details32.png in Resources */,
|
||||||
CEFC295609C89FF200D9F998 /* preferences32.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 */,
|
CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */,
|
||||||
CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */,
|
CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */,
|
||||||
CE80DB770FC194760086DCA6 /* progress.nib in Resources */,
|
CE80DB770FC194760086DCA6 /* progress.nib in Resources */,
|
||||||
CE80DB780FC194760086DCA6 /* registration.nib in Resources */,
|
CE80DB780FC194760086DCA6 /* registration.nib in Resources */,
|
||||||
CE6E0F3D1054EC62008D9390 /* dsa_pub.pem 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;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -425,30 +430,6 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup 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 */ = {
|
CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -473,39 +454,9 @@
|
|||||||
name = registration.nib;
|
name = registration.nib;
|
||||||
sourceTree = SOURCE_ROOT;
|
sourceTree = SOURCE_ROOT;
|
||||||
};
|
};
|
||||||
CECA899709DB12CA00A3D774 /* Details.nib */ = {
|
|
||||||
isa = PBXVariantGroup;
|
|
||||||
children = (
|
|
||||||
CECA899809DB12CA00A3D774 /* English */,
|
|
||||||
);
|
|
||||||
name = Details.nib;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 */ = {
|
C01FCF4C08A954540054247B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -526,31 +477,15 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
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 */ = {
|
C01FCF5008A954540054247B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ARCHS = "$(ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1)";
|
ARCHS = "$(ARCHS_STANDARD_32_BIT)";
|
||||||
ARCHS_STANDARD_32_BIT_PRE_XCODE_3_1 = "ppc i386";
|
|
||||||
FRAMEWORK_SEARCH_PATHS = "";
|
|
||||||
GCC_C_LANGUAGE_STANDARD = c99;
|
GCC_C_LANGUAGE_STANDARD = c99;
|
||||||
GCC_VERSION = 4.0;
|
|
||||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.4;
|
MACOSX_DEPLOYMENT_TARGET = 10.5;
|
||||||
PREBINDING = NO;
|
SDKROOT = macosx10.5;
|
||||||
SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
|
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -560,7 +495,6 @@
|
|||||||
C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = {
|
C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
C01FCF4B08A954540054247B /* Debug */,
|
|
||||||
C01FCF4C08A954540054247B /* Release */,
|
C01FCF4C08A954540054247B /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
@@ -569,7 +503,6 @@
|
|||||||
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = {
|
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
C01FCF4F08A954540054247B /* Debug */,
|
|
||||||
C01FCF5008A954540054247B /* Release */,
|
C01FCF5008A954540054247B /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from dupeguru_pe import app_cocoa as app_pe_cocoa
|
|||||||
# Fix py2app imports which chokes on relative imports
|
# Fix py2app imports which chokes on relative imports
|
||||||
from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner
|
from dupeguru import app, app_cocoa, data, directories, engine, export, ignore, results, scanner
|
||||||
from dupeguru_pe import block, cache, matchbase, data
|
from dupeguru_pe import block, cache, matchbase, data
|
||||||
from hsfs import auto, stats, tree
|
|
||||||
from hsutil import conflict
|
from hsutil import conflict
|
||||||
|
|
||||||
class PyApp(NSObject):
|
class PyApp(NSObject):
|
||||||
@@ -39,7 +38,7 @@ class PyDupeGuru(PyApp):
|
|||||||
self.app.scanner.ignore_list.Clear()
|
self.app.scanner.ignore_list.Clear()
|
||||||
|
|
||||||
def clearPictureCache(self):
|
def clearPictureCache(self):
|
||||||
self.app.scanner.match_factory.cached_blocks.clear()
|
self.app.scanner.cached_blocks.clear()
|
||||||
|
|
||||||
def doScan(self):
|
def doScan(self):
|
||||||
return self.app.start_scanning()
|
return self.app.start_scanning()
|
||||||
@@ -172,10 +171,10 @@ class PyDupeGuru(PyApp):
|
|||||||
|
|
||||||
#---Properties
|
#---Properties
|
||||||
def setMatchScaled_(self,match_scaled):
|
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):
|
def setMinMatchPercentage_(self,percentage):
|
||||||
self.app.scanner.match_factory.threshold = int(percentage)
|
self.app.scanner.threshold = int(percentage)
|
||||||
|
|
||||||
def setMixFileKind_(self,mix_file_kind):
|
def setMixFileKind_(self,mix_file_kind):
|
||||||
self.app.scanner.mix_file_kind = mix_file_kind
|
self.app.scanner.mix_file_kind = mix_file_kind
|
||||||
|
|||||||
1472
pe/cocoa/xib/DetailsPanel.xib
Normal file
1472
pe/cocoa/xib/DetailsPanel.xib
Normal file
File diff suppressed because it is too large
Load Diff
1650
pe/cocoa/xib/Preferences.xib
Normal file
1650
pe/cocoa/xib/Preferences.xib
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,7 @@
|
|||||||
|
- 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
|
- date: 2009-10-14
|
||||||
version: 1.7.7
|
version: 1.7.7
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -7,41 +7,43 @@
|
|||||||
# which should be included with this package. The terms are also available at
|
# which should be included with this package. The terms are also available at
|
||||||
# http://www.hardcoded.net/licenses/hs_license
|
# http://www.hardcoded.net/licenses/hs_license
|
||||||
|
|
||||||
import os
|
|
||||||
import os.path as op
|
import os.path as op
|
||||||
import logging
|
import logging
|
||||||
import plistlib
|
import plistlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import objc
|
|
||||||
from Foundation import *
|
from Foundation import *
|
||||||
from AppKit import *
|
from AppKit import *
|
||||||
from appscript import app, k
|
from appscript import app, k
|
||||||
|
|
||||||
from hsutil import job, io
|
from hsutil import io
|
||||||
import hsfs as fs
|
|
||||||
from hsfs import phys, InvalidPath
|
|
||||||
from hsutil import files
|
|
||||||
from hsutil.str import get_file_ext
|
from hsutil.str import get_file_ext
|
||||||
from hsutil.path import Path
|
from hsutil.path import Path
|
||||||
from hsutil.cocoa import as_fetch
|
from hsutil.cocoa import as_fetch
|
||||||
|
|
||||||
|
from dupeguru import fs
|
||||||
from dupeguru import app_cocoa, directories
|
from dupeguru import app_cocoa, directories
|
||||||
from . import data, matchbase
|
from . import data
|
||||||
from .cache import string_to_colors, Cache
|
from .cache import string_to_colors, Cache
|
||||||
|
from .scanner import ScannerPE
|
||||||
|
|
||||||
mainBundle = NSBundle.mainBundle()
|
mainBundle = NSBundle.mainBundle()
|
||||||
PictureBlocks = mainBundle.classNamed_('PictureBlocks')
|
PictureBlocks = mainBundle.classNamed_('PictureBlocks')
|
||||||
assert PictureBlocks is not None
|
assert PictureBlocks is not None
|
||||||
|
|
||||||
class Photo(phys.File):
|
class Photo(fs.File):
|
||||||
INITIAL_INFO = phys.File.INITIAL_INFO.copy()
|
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||||
INITIAL_INFO.update({
|
INITIAL_INFO.update({
|
||||||
'dimensions': (0,0),
|
'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):
|
def _read_info(self, field):
|
||||||
super(Photo, self)._read_info(field)
|
fs.File._read_info(self, field)
|
||||||
if field == 'dimensions':
|
if field == 'dimensions':
|
||||||
size = PictureBlocks.getImageSize_(unicode(self.path))
|
size = PictureBlocks.getImageSize_(unicode(self.path))
|
||||||
self.dimensions = (size.width, size.height)
|
self.dimensions = (size.width, size.height)
|
||||||
@@ -49,7 +51,7 @@ class Photo(phys.File):
|
|||||||
def get_blocks(self, block_count_per_side):
|
def get_blocks(self, block_count_per_side):
|
||||||
try:
|
try:
|
||||||
blocks = PictureBlocks.getBlocksFromImagePath_blockCount_(unicode(self.path), block_count_per_side)
|
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)))
|
raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e)))
|
||||||
if not blocks:
|
if not blocks:
|
||||||
raise IOError('The picture %s could not be read' % unicode(self.path))
|
raise IOError('The picture %s could not be read' % unicode(self.path))
|
||||||
@@ -57,89 +59,79 @@ class Photo(phys.File):
|
|||||||
|
|
||||||
|
|
||||||
class IPhoto(Photo):
|
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
|
@property
|
||||||
def display_path(self):
|
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 'iPhotoRecentDatabases' not in prefs:
|
||||||
|
raise directories.InvalidPathError()
|
||||||
|
plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0])
|
||||||
|
return Path(plisturl.path())
|
||||||
|
|
||||||
class Directory(phys.Directory):
|
def get_iphoto_pictures(plistpath):
|
||||||
cls_file_class = Photo
|
if not io.exists(plistpath):
|
||||||
cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2')
|
raise InvalidPath(self)
|
||||||
|
s = io.open(plistpath).read()
|
||||||
def _fetch_subitems(self):
|
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
|
||||||
subdirs, subfiles = super(Directory,self)._fetch_subitems()
|
s = s.replace('\x10', '')
|
||||||
return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts]
|
# It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find
|
||||||
|
# any & char that is not a &-based entity (&, ", etc.). based on TextMate's XML
|
||||||
|
# bundle's regexp
|
||||||
class IPhotoLibrary(fs.Directory):
|
s, count = re.subn(r'&(?![a-zA-Z0-9_-]+|#[0-9]+|#x[0-9a-fA-F]+;)', '', s)
|
||||||
def __init__(self, plistpath):
|
if count:
|
||||||
self.plistpath = plistpath
|
logging.warning("%d invalid XML entities replacement made", count)
|
||||||
self.refpath = plistpath[:-1]
|
plist = plistlib.readPlistFromString(s)
|
||||||
# the AlbumData.xml file lives right in the library path
|
result = []
|
||||||
super(IPhotoLibrary, self).__init__(None, 'iPhoto Library')
|
for photo_data in plist['Master Image List'].values():
|
||||||
if not io.exists(plistpath):
|
|
||||||
raise InvalidPath(self)
|
|
||||||
|
|
||||||
def _update_photo(self, photo_data):
|
|
||||||
if photo_data['MediaType'] != 'Image':
|
if photo_data['MediaType'] != 'Image':
|
||||||
return
|
continue
|
||||||
photo_path = Path(photo_data['ImagePath'])
|
photo_path = Path(photo_data['ImagePath'])
|
||||||
subpath = photo_path[len(self.refpath):-1]
|
photo = IPhoto(photo_path)
|
||||||
subdir = self
|
result.append(photo)
|
||||||
for element in subpath:
|
return result
|
||||||
try:
|
|
||||||
subdir = subdir[element]
|
class Directories(directories.Directories):
|
||||||
except KeyError:
|
def __init__(self):
|
||||||
subdir = fs.Directory(subdir, element)
|
directories.Directories.__init__(self, fileclasses=[Photo])
|
||||||
try:
|
self.iphoto_libpath = get_iphoto_database_path()
|
||||||
IPhoto(subdir, photo_path)
|
self.set_state(self.iphoto_libpath[:-1], directories.STATE_EXCLUDED)
|
||||||
except fs.AlreadyExistsError:
|
|
||||||
# it's possible for 2 entries in the plist to point to the same path. Ignore one of them.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update(self):
|
def _get_files(self, from_path):
|
||||||
self.clear()
|
if from_path == Path('iPhoto Library'):
|
||||||
s = open(unicode(self.plistpath)).read()
|
is_ref = self.get_state(from_path) == directories.STATE_REFERENCE
|
||||||
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
|
photos = get_iphoto_pictures(self.iphoto_libpath)
|
||||||
s = s.replace('\x10', '')
|
for photo in photos:
|
||||||
# It seems that iPhoto sometimes doesn't properly escape & chars. The regexp below is to find
|
photo.is_ref = is_ref
|
||||||
# any & char that is not a &-based entity (&, ", etc.). based on TextMate's XML
|
return photos
|
||||||
# bundle's regexp
|
else:
|
||||||
s, count = re.subn(r'&(?![a-zA-Z0-9_-]+|#[0-9]+|#x[0-9a-fA-F]+;)', '', s)
|
return directories.Directories._get_files(self, from_path)
|
||||||
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 force_update(self): # Don't update
|
@staticmethod
|
||||||
pass
|
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):
|
class DupeGuruPE(app_cocoa.DupeGuru):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru Picture Edition', appid=5)
|
app_cocoa.DupeGuru.__init__(self, data, 'dupeGuru Picture Edition', appid=5)
|
||||||
self.scanner.match_factory = matchbase.AsyncMatchFactory()
|
self.scanner = ScannerPE()
|
||||||
self.directories.dirclass = Directory
|
self.directories = Directories()
|
||||||
self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library()
|
|
||||||
p = op.join(self.appdata, 'cached_pictures.db')
|
p = op.join(self.appdata, 'cached_pictures.db')
|
||||||
self.scanner.match_factory.cached_blocks = Cache(p)
|
self.scanner.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)
|
|
||||||
|
|
||||||
def _do_delete(self, j):
|
def _do_delete(self, j):
|
||||||
def op(dupe):
|
def op(dupe):
|
||||||
@@ -174,40 +166,19 @@ class DupeGuruPE(app_cocoa.DupeGuru):
|
|||||||
|
|
||||||
def _do_load(self, j):
|
def _do_load(self, j):
|
||||||
self.directories.load_from_file(op.join(self.appdata, 'last_directories.xml'))
|
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)
|
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
|
||||||
|
|
||||||
def _get_file(self, str_path):
|
def _get_file(self, str_path):
|
||||||
p = Path(str_path)
|
p = Path(str_path)
|
||||||
for d in self.directories:
|
if p in self.directories.iphoto_libpath[:-1]:
|
||||||
result = None
|
return IPhoto(p)
|
||||||
if p in d.path:
|
return app_cocoa.DupeGuru._get_file(self, str_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
|
|
||||||
|
|
||||||
def copy_or_move(self, dupe, copy, destination, dest_type):
|
def copy_or_move(self, dupe, copy, destination, dest_type):
|
||||||
if isinstance(dupe, IPhoto):
|
if isinstance(dupe, IPhoto):
|
||||||
copy = True
|
copy = True
|
||||||
return app_cocoa.DupeGuru.copy_or_move(self, dupe, copy, destination, dest_type)
|
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):
|
def selected_dupe_path(self):
|
||||||
if not self.selected_dupes:
|
if not self.selected_dupes:
|
||||||
return None
|
return None
|
||||||
@@ -217,5 +188,7 @@ class DupeGuruPE(app_cocoa.DupeGuru):
|
|||||||
if not self.selected_dupes:
|
if not self.selected_dupes:
|
||||||
return None
|
return None
|
||||||
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
|
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
|
return ref.path
|
||||||
|
|
||||||
|
|||||||
@@ -20,58 +20,42 @@ from .block import avgdiff, DifferentBlockCountError, NoBlocksError
|
|||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
|
|
||||||
MIN_ITERATIONS = 3
|
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
|
# 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
|
# cpucount*2 should be enough to be sure that the spawned process will not wait after the results
|
||||||
# collection made by the main process.
|
# collection made by the main process.
|
||||||
RESULTS_QUEUE_LIMIT = multiprocessing.cpu_count() * 2
|
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:
|
if percentage < 0:
|
||||||
percentage = 0
|
percentage = 0
|
||||||
return Match(first,second,percentage)
|
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
|
|
||||||
|
|
||||||
|
|
||||||
def async_compare(ref_id, other_ids, dbname, threshold):
|
def async_compare(ref_id, other_ids, dbname, threshold):
|
||||||
cache = Cache(dbname, threaded=False)
|
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))
|
results.append((ref_id, other_id, percentage))
|
||||||
cache.con.close()
|
cache.con.close()
|
||||||
return results
|
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()
|
multiprocessing.freeze_support()
|
||||||
22
pe/py/scanner.py
Normal file
22
pe/py/scanner.py
Normal 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)
|
||||||
|
|
||||||
34
pe/qt/app.py
34
pe/qt/app.py
@@ -12,12 +12,12 @@ import os.path as op
|
|||||||
from PyQt4.QtGui import QImage
|
from PyQt4.QtGui import QImage
|
||||||
import PIL.Image
|
import PIL.Image
|
||||||
|
|
||||||
from hsfs import phys
|
|
||||||
from hsutil.str import get_file_ext
|
from hsutil.str import get_file_ext
|
||||||
|
|
||||||
|
from dupeguru import fs
|
||||||
from dupeguru_pe import data as data_pe
|
from dupeguru_pe import data as data_pe
|
||||||
from dupeguru_pe.cache import Cache
|
from dupeguru_pe.cache import Cache
|
||||||
from dupeguru_pe.matchbase import AsyncMatchFactory
|
from dupeguru_pe.scanner import ScannerPE
|
||||||
|
|
||||||
from block import getblocks
|
from block import getblocks
|
||||||
from base.app import DupeGuru as DupeGuruBase
|
from base.app import DupeGuru as DupeGuruBase
|
||||||
@@ -26,14 +26,19 @@ from main_window import MainWindow
|
|||||||
from preferences import Preferences
|
from preferences import Preferences
|
||||||
from preferences_dialog import PreferencesDialog
|
from preferences_dialog import PreferencesDialog
|
||||||
|
|
||||||
class File(phys.File):
|
class File(fs.File):
|
||||||
INITIAL_INFO = phys.File.INITIAL_INFO.copy()
|
INITIAL_INFO = fs.File.INITIAL_INFO.copy()
|
||||||
INITIAL_INFO.update({
|
INITIAL_INFO.update({
|
||||||
'dimensions': (0,0),
|
'dimensions': (0,0),
|
||||||
})
|
})
|
||||||
|
HANDLED_EXTS = set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif'])
|
||||||
|
|
||||||
|
@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):
|
def _read_info(self, field):
|
||||||
super(File, self)._read_info(field)
|
fs.File._read_info(self, field)
|
||||||
if field == 'dimensions':
|
if field == 'dimensions':
|
||||||
im = PIL.Image.open(unicode(self.path))
|
im = PIL.Image.open(unicode(self.path))
|
||||||
self.dimensions = im.size
|
self.dimensions = im.size
|
||||||
@@ -44,15 +49,6 @@ class File(phys.File):
|
|||||||
return getblocks(image, block_count_per_side)
|
return getblocks(image, block_count_per_side)
|
||||||
|
|
||||||
|
|
||||||
class Directory(phys.Directory):
|
|
||||||
cls_file_class = File
|
|
||||||
cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff')
|
|
||||||
|
|
||||||
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 DupeGuru(DupeGuruBase):
|
class DupeGuru(DupeGuruBase):
|
||||||
LOGO_NAME = 'logo_pe'
|
LOGO_NAME = 'logo_pe'
|
||||||
NAME = 'dupeGuru Picture Edition'
|
NAME = 'dupeGuru Picture Edition'
|
||||||
@@ -63,15 +59,15 @@ class DupeGuru(DupeGuruBase):
|
|||||||
DupeGuruBase.__init__(self, data_pe, appid=5)
|
DupeGuruBase.__init__(self, data_pe, appid=5)
|
||||||
|
|
||||||
def _setup(self):
|
def _setup(self):
|
||||||
self.scanner.match_factory = AsyncMatchFactory()
|
self.scanner = ScannerPE()
|
||||||
self.directories.dirclass = Directory
|
self.directories.fileclasses = [File]
|
||||||
self.scanner.match_factory.cached_blocks = Cache(op.join(self.appdata, 'cached_pictures.db'))
|
self.scanner.cached_blocks = Cache(op.join(self.appdata, 'cached_pictures.db'))
|
||||||
DupeGuruBase._setup(self)
|
DupeGuruBase._setup(self)
|
||||||
|
|
||||||
def _update_options(self):
|
def _update_options(self):
|
||||||
DupeGuruBase._update_options(self)
|
DupeGuruBase._update_options(self)
|
||||||
self.scanner.match_factory.match_scaled = self.prefs.match_scaled
|
self.scanner.match_scaled = self.prefs.match_scaled
|
||||||
self.scanner.match_factory.threshold = self.prefs.filter_hardness
|
self.scanner.threshold = self.prefs.filter_hardness
|
||||||
|
|
||||||
def _create_details_dialog(self, parent):
|
def _create_details_dialog(self, parent):
|
||||||
return DetailsDialog(parent, self)
|
return DetailsDialog(parent, self)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'],
|
a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'],
|
||||||
pathex=['C:\\src\\dupeguru\\pe\\qt'])
|
pathex=[])
|
||||||
pyz = PYZ(a.pure)
|
pyz = PYZ(a.pure)
|
||||||
exe = EXE(pyz,
|
exe = EXE(pyz,
|
||||||
a.scripts,
|
a.scripts,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from hsutil.build import print_and_do, build_all_qt_ui
|
|||||||
build_all_qt_ui(op.join('qtlib', 'ui'))
|
build_all_qt_ui(op.join('qtlib', 'ui'))
|
||||||
build_all_qt_ui('base')
|
build_all_qt_ui('base')
|
||||||
build_all_qt_ui('.')
|
build_all_qt_ui('.')
|
||||||
|
print_and_do("pyrcc4 base\\dg.qrc > base\\dg_rc.py")
|
||||||
|
|
||||||
def move(src, dst):
|
def move(src, dst):
|
||||||
if not op.exists(src):
|
if not op.exists(src):
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ class MainWindow(MainWindowBase):
|
|||||||
title = "Clear Picture Cache"
|
title = "Clear Picture Cache"
|
||||||
msg = "Do you really want to remove all your cached picture analysis?"
|
msg = "Do you really want to remove all your cached picture analysis?"
|
||||||
if self._confirm(title, msg, QMessageBox.No):
|
if self._confirm(title, msg, QMessageBox.No):
|
||||||
self.app.scanner.match_factory.cached_blocks.clear()
|
self.app.scanner.cached_blocks.clear()
|
||||||
QMessageBox.information(self, title, "Picture cache cleared.")
|
QMessageBox.information(self, title, "Picture cache cleared.")
|
||||||
|
|
||||||
@@ -14,6 +14,9 @@ import base.dg_rc
|
|||||||
|
|
||||||
from app import DupeGuru
|
from app import DupeGuru
|
||||||
|
|
||||||
|
# This is a workaround for a pyinstaller problem where compiled dupeguru can't read tiff files
|
||||||
|
from PIL import TiffImagePlugin, TiffTags
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
app.setWindowIcon(QIcon(QPixmap(":/logo_pe")))
|
app.setWindowIcon(QIcon(QPixmap(":/logo_pe")))
|
||||||
|
|||||||
@@ -8,14 +8,11 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "dgbase/AppDelegate.h"
|
#import "dgbase/AppDelegate.h"
|
||||||
#import "ResultWindow.h"
|
|
||||||
#import "DirectoryPanel.h"
|
#import "DirectoryPanel.h"
|
||||||
#import "PyDupeGuru.h"
|
#import "PyDupeGuru.h"
|
||||||
|
|
||||||
@interface AppDelegate : AppDelegateBase
|
@interface AppDelegate : AppDelegateBase
|
||||||
{
|
{
|
||||||
IBOutlet ResultWindow *result;
|
|
||||||
|
|
||||||
DirectoryPanel *_directoryPanel;
|
DirectoryPanel *_directoryPanel;
|
||||||
}
|
}
|
||||||
- (IBAction)openWebsite:(id)sender;
|
- (IBAction)openWebsite:(id)sender;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
#import "cocoalib/RegistrationInterface.h"
|
#import "cocoalib/RegistrationInterface.h"
|
||||||
#import "cocoalib/Utils.h"
|
#import "cocoalib/Utils.h"
|
||||||
#import "cocoalib/ValueTransformers.h"
|
#import "cocoalib/ValueTransformers.h"
|
||||||
|
#import "DetailsPanel.h"
|
||||||
#import "Consts.h"
|
#import "Consts.h"
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
@@ -62,26 +63,17 @@ http://www.hardcoded.net/licenses/hs_license
|
|||||||
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
|
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
|
||||||
return _directoryPanel;
|
return _directoryPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (DetailsPanelBase *)detailsPanel
|
||||||
|
{
|
||||||
|
if (!_detailsPanel)
|
||||||
|
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
|
||||||
|
return _detailsPanel;
|
||||||
|
}
|
||||||
|
|
||||||
- (PyDupeGuru *)py { return (PyDupeGuru *)py; }
|
- (PyDupeGuru *)py { return (PyDupeGuru *)py; }
|
||||||
|
|
||||||
//Delegate
|
//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!"];
|
|
||||||
//Restore results
|
|
||||||
[py loadIgnoreList];
|
|
||||||
[py loadResults];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
||||||
{
|
{
|
||||||
if (![[result window] isVisible])
|
if (![[result window] isVisible])
|
||||||
|
|||||||
18
se/cocoa/English.lproj/Details.nib/classes.nib
generated
18
se/cocoa/English.lproj/Details.nib/classes.nib
generated
@@ -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;
|
|
||||||
}
|
|
||||||
16
se/cocoa/English.lproj/Details.nib/info.nib
generated
16
se/cocoa/English.lproj/Details.nib/info.nib
generated
@@ -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>
|
|
||||||
BIN
se/cocoa/English.lproj/Details.nib/keyedobjects.nib
generated
BIN
se/cocoa/English.lproj/Details.nib/keyedobjects.nib
generated
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user