Initial commit.
--HG-- extra : convert_revision : svn%3Ac306627e-7827-47d3-bdf0-9a457c9553a1/trunk%402
21
pe/cocoa/AppDelegate.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "dgbase/AppDelegate.h"
|
||||
#import "ResultWindow.h"
|
||||
#import "DirectoryPanel.h"
|
||||
#import "DetailsPanel.h"
|
||||
#import "PyDupeGuru.h"
|
||||
|
||||
@interface AppDelegate : AppDelegateBase
|
||||
{
|
||||
IBOutlet ResultWindow *result;
|
||||
|
||||
DetailsPanel *_detailsPanel;
|
||||
DirectoryPanel *_directoryPanel;
|
||||
}
|
||||
- (IBAction)openWebsite:(id)sender;
|
||||
- (IBAction)toggleDetailsPanel:(id)sender;
|
||||
- (IBAction)toggleDirectories:(id)sender;
|
||||
|
||||
- (DirectoryPanel *)directoryPanel;
|
||||
- (PyDupeGuru *)py;
|
||||
@end
|
||||
116
pe/cocoa/AppDelegate.m
Normal file
@@ -0,0 +1,116 @@
|
||||
#import "AppDelegate.h"
|
||||
#import "ProgressController.h"
|
||||
#import "RegistrationInterface.h"
|
||||
#import "Utils.h"
|
||||
#import "ValueTransformers.h"
|
||||
#import "Consts.h"
|
||||
|
||||
@implementation AppDelegate
|
||||
+ (void)initialize
|
||||
{
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
NSMutableDictionary *d = [NSMutableDictionary dictionaryWithCapacity:10];
|
||||
[d setObject:[NSNumber numberWithInt:95] forKey:@"minMatchPercentage"];
|
||||
[d setObject:[NSNumber numberWithInt:1] forKey:@"recreatePathType"];
|
||||
[d setObject:[NSNumber numberWithBool:NO] forKey:@"matchScaled"];
|
||||
[d setObject:[NSNumber numberWithBool:YES] forKey:@"mixFileKind"];
|
||||
[d setObject:[NSNumber numberWithBool:NO] forKey:@"useRegexpFilter"];
|
||||
[d setObject:[NSNumber numberWithBool:NO] forKey:@"removeEmptyFolders"];
|
||||
[d setObject:[NSNumber numberWithBool:NO] forKey:@"debug"];
|
||||
[d setObject:[NSArray array] forKey:@"recentDirectories"];
|
||||
[d setObject:[NSArray array] forKey:@"columnsOrder"];
|
||||
[d setObject:[NSDictionary dictionary] forKey:@"columnsWidth"];
|
||||
[[NSUserDefaultsController sharedUserDefaultsController] setInitialValues:d];
|
||||
[ud registerDefaults:d];
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
_directoryPanel = nil;
|
||||
_detailsPanel = nil;
|
||||
_appName = APPNAME;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IBAction)openWebsite:(id)sender
|
||||
{
|
||||
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"http://www.hardcoded.net/dupeguru_pe"]];
|
||||
}
|
||||
|
||||
- (IBAction)toggleDetailsPanel:(id)sender
|
||||
{
|
||||
if (!_detailsPanel)
|
||||
_detailsPanel = [[DetailsPanel alloc] initWithPy:py];
|
||||
if ([[_detailsPanel window] isVisible])
|
||||
[[_detailsPanel window] close];
|
||||
else
|
||||
{
|
||||
[[_detailsPanel window] orderFront:nil];
|
||||
[_detailsPanel refresh];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)toggleDirectories:(id)sender
|
||||
{
|
||||
[[self directoryPanel] toggleVisible:sender];
|
||||
}
|
||||
|
||||
- (DirectoryPanel *)directoryPanel
|
||||
{
|
||||
if (!_directoryPanel)
|
||||
_directoryPanel = [[DirectoryPanel alloc] initWithParentApp:self];
|
||||
return _directoryPanel;
|
||||
}
|
||||
- (PyDupeGuru *)py { return (PyDupeGuru *)py; }
|
||||
|
||||
//Delegate
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
||||
{
|
||||
[[ProgressController mainProgressController] setWorker:py];
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
//Restore Columns
|
||||
NSArray *columnsOrder = [ud arrayForKey:@"columnsOrder"];
|
||||
NSDictionary *columnsWidth = [ud dictionaryForKey:@"columnsWidth"];
|
||||
if ([columnsOrder count])
|
||||
[result restoreColumnsPosition:columnsOrder widths:columnsWidth];
|
||||
//Reg stuff
|
||||
if ([RegistrationInterface showNagWithApp:[self py] name:APPNAME limitDescription:LIMIT_DESC])
|
||||
[unlockMenuItem setTitle:@"Thanks for buying dupeGuru Picture Edition!"];
|
||||
//Restore results
|
||||
[py loadIgnoreList];
|
||||
[py loadResults];
|
||||
}
|
||||
|
||||
- (void)applicationWillBecomeActive:(NSNotification *)aNotification
|
||||
{
|
||||
if (![[result window] isVisible])
|
||||
[result showWindow:NSApp];
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification
|
||||
{
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
[ud setObject: [result getColumnsOrder] forKey:@"columnsOrder"];
|
||||
[ud setObject: [result getColumnsWidth] forKey:@"columnsWidth"];
|
||||
[py saveIgnoreList];
|
||||
[py saveResults];
|
||||
int sc = [ud integerForKey:@"sessionCountSinceLastIgnorePurge"];
|
||||
if (sc >= 10)
|
||||
{
|
||||
sc = -1;
|
||||
[py purgeIgnoreList];
|
||||
}
|
||||
sc++;
|
||||
[ud setInteger:sc forKey:@"sessionCountSinceLastIgnorePurge"];
|
||||
// NSApplication does not release nib instances objects, we must do it manually
|
||||
// Well, it isn't needed because the memory is freed anyway (we are quitting the application
|
||||
// But I need to release RecentDirectories so it saves the user defaults
|
||||
[recentDirectories release];
|
||||
}
|
||||
|
||||
- (void)recentDirecoryClicked:(NSString *)directory
|
||||
{
|
||||
[[self directoryPanel] addDirectory:directory];
|
||||
}
|
||||
@end
|
||||
4
pe/cocoa/Consts.h
Normal file
@@ -0,0 +1,4 @@
|
||||
#import "dgbase/Consts.h"
|
||||
|
||||
extern NSString *ImageLoadedNotification;
|
||||
extern NSString *APPNAME;
|
||||
5
pe/cocoa/Consts.m
Normal file
@@ -0,0 +1,5 @@
|
||||
#import "Consts.h"
|
||||
|
||||
NSString *ImageLoadedNotification = @"ImageLoadedNotification";
|
||||
NSString *APPNAME = @"dupeGuru PE";
|
||||
|
||||
21
pe/cocoa/DetailsPanel.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "PyApp.h"
|
||||
#import "Table.h"
|
||||
|
||||
|
||||
@interface DetailsPanel : NSWindowController
|
||||
{
|
||||
IBOutlet TableView *detailsTable;
|
||||
IBOutlet NSImageView *dupeImage;
|
||||
IBOutlet NSProgressIndicator *dupeProgressIndicator;
|
||||
IBOutlet NSImageView *refImage;
|
||||
IBOutlet NSProgressIndicator *refProgressIndicator;
|
||||
|
||||
PyApp *py;
|
||||
BOOL _needsRefresh;
|
||||
NSString *_dupePath;
|
||||
NSString *_refPath;
|
||||
}
|
||||
- (id)initWithPy:(PyApp *)aPy;
|
||||
- (void)refresh;
|
||||
@end
|
||||
79
pe/cocoa/DetailsPanel.m
Normal file
@@ -0,0 +1,79 @@
|
||||
#import "Utils.h"
|
||||
#import "NSNotificationAdditions.h"
|
||||
#import "NSImageAdditions.h"
|
||||
#import "PyDupeGuru.h"
|
||||
#import "DetailsPanel.h"
|
||||
#import "Consts.h"
|
||||
|
||||
@implementation DetailsPanel
|
||||
- (id)initWithPy:(PyApp *)aPy
|
||||
{
|
||||
self = [super initWithWindowNibName:@"Details"];
|
||||
[self window]; //So the detailsTable is initialized.
|
||||
[detailsTable setPy:aPy];
|
||||
py = aPy;
|
||||
_needsRefresh = YES;
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(duplicateSelectionChanged:) name:DuplicateSelectionChangedNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(imageLoaded:) name:ImageLoadedNotification object:self];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadImageAsync:(NSString *)imagePath
|
||||
{
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
NSImage *image = [[NSImage alloc] initByReferencingFile:imagePath];
|
||||
NSImage *thumbnail = [image imageByScalingProportionallyToSize:NSMakeSize(512,512)];
|
||||
[image release];
|
||||
NSMutableDictionary *params = [NSMutableDictionary dictionary];
|
||||
[params setValue:imagePath forKey:@"imagePath"];
|
||||
[params setValue:thumbnail forKey:@"image"];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationOnMainThreadWithName:ImageLoadedNotification object:self userInfo:params waitUntilDone:YES];
|
||||
[pool release];
|
||||
}
|
||||
|
||||
- (void)refresh
|
||||
{
|
||||
if (!_needsRefresh)
|
||||
return;
|
||||
[detailsTable reloadData];
|
||||
|
||||
NSString *refPath = [(PyDupeGuru *)py getSelectedDupeRefPath];
|
||||
if (_refPath != nil)
|
||||
[_refPath autorelease];
|
||||
_refPath = [refPath retain];
|
||||
[NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:refPath];
|
||||
NSString *dupePath = [(PyDupeGuru *)py getSelectedDupePath];
|
||||
if (_dupePath != nil)
|
||||
[_dupePath autorelease];
|
||||
_dupePath = [dupePath retain];
|
||||
if (![dupePath isEqual: refPath])
|
||||
[NSThread detachNewThreadSelector:@selector(loadImageAsync:) toTarget:self withObject:dupePath];
|
||||
[refProgressIndicator startAnimation:nil];
|
||||
[dupeProgressIndicator startAnimation:nil];
|
||||
_needsRefresh = NO;
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
- (void)duplicateSelectionChanged:(NSNotification *)aNotification
|
||||
{
|
||||
_needsRefresh = YES;
|
||||
if ([[self window] isVisible])
|
||||
[self refresh];
|
||||
}
|
||||
|
||||
- (void)imageLoaded:(NSNotification *)aNotification
|
||||
{
|
||||
NSString *imagePath = [[aNotification userInfo] valueForKey:@"imagePath"];
|
||||
NSImage *image = [[aNotification userInfo] valueForKey:@"image"];
|
||||
if ([imagePath isEqual: _refPath])
|
||||
{
|
||||
[refImage setImage:image];
|
||||
[refProgressIndicator stopAnimation:nil];
|
||||
}
|
||||
if ([imagePath isEqual: _dupePath])
|
||||
{
|
||||
[dupeImage setImage:image];
|
||||
[dupeProgressIndicator stopAnimation:nil];
|
||||
}
|
||||
}
|
||||
@end
|
||||
8
pe/cocoa/DirectoryPanel.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "dgbase/DirectoryPanel.h"
|
||||
|
||||
@interface DirectoryPanel : DirectoryPanelBase
|
||||
{
|
||||
}
|
||||
- (IBAction)addiPhoto:(id)sender;
|
||||
@end
|
||||
44
pe/cocoa/DirectoryPanel.m
Normal file
@@ -0,0 +1,44 @@
|
||||
#import "DirectoryPanel.h"
|
||||
#import "ProgressController.h"
|
||||
|
||||
static NSString* jobAddIPhoto = @"jobAddIPhoto";
|
||||
|
||||
@implementation DirectoryPanel
|
||||
- (id)initWithParentApp:(id)aParentApp
|
||||
{
|
||||
self = [super initWithParentApp:aParentApp];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (IBAction)addiPhoto:(id)sender
|
||||
{
|
||||
[[ProgressController mainProgressController] setJobDesc:@"Adding iPhoto Library..."];
|
||||
[[ProgressController mainProgressController] setJobId:jobAddIPhoto];
|
||||
[[ProgressController mainProgressController] showSheetForParent:[self window]];
|
||||
[self addDirectory:@"iPhoto Library"];
|
||||
}
|
||||
|
||||
- (IBAction)popupAddDirectoryMenu:(id)sender
|
||||
{
|
||||
NSMenu *m = [addButtonPopUp menu];
|
||||
while ([m numberOfItems] > 0)
|
||||
[m removeItemAtIndex:0];
|
||||
NSMenuItem *mi = [m addItemWithTitle:@"Add New Directory..." action:@selector(askForDirectory:) keyEquivalent:@""];
|
||||
[mi setTarget:self];
|
||||
mi = [m addItemWithTitle:@"Add iPhoto Directory" action:@selector(addiPhoto:) keyEquivalent:@""];
|
||||
[mi setTarget:self];
|
||||
[m addItem:[NSMenuItem separatorItem]];
|
||||
[_recentDirectories fillMenu:m];
|
||||
[addButtonPopUp selectItem:nil];
|
||||
[[addButtonPopUp cell] performClickWithFrame:[sender frame] inView:[sender superview]];
|
||||
}
|
||||
|
||||
- (void)jobCompleted:(NSNotification *)aNotification
|
||||
{
|
||||
if ([[ProgressController mainProgressController] jobId] == jobAddIPhoto)
|
||||
{
|
||||
[directories reloadData];
|
||||
}
|
||||
}
|
||||
@end
|
||||
24
pe/cocoa/English.lproj/Details.nib/classes.nib
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
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
Normal file
@@ -0,0 +1,16 @@
|
||||
<?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
Normal file
62
pe/cocoa/English.lproj/Directories.nib/classes.nib
generated
Normal file
@@ -0,0 +1,62 @@
|
||||
<?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
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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
Normal file
BIN
pe/cocoa/English.lproj/InfoPlist.strings
Normal file
235
pe/cocoa/English.lproj/MainMenu.nib/classes.nib
generated
Normal file
@@ -0,0 +1,235 @@
|
||||
<?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
Normal file
@@ -0,0 +1,20 @@
|
||||
<?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
Normal file
34
pe/cocoa/Info.plist
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleHelpBookFolder</key>
|
||||
<string>dupeguru_pe_help</string>
|
||||
<key>CFBundleHelpBookName</key>
|
||||
<string>dupeGuru PE Help</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>dupeguru</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.hardcoded_software.dupeguru_pe</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>${PRODUCT_NAME}</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>hsft</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.7.2</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
<key>SUFeedURL</key>
|
||||
<string>http://www.hardcoded.net/updates/dupeguru_pe.appcast</string>
|
||||
</dict>
|
||||
</plist>
|
||||
11
pe/cocoa/PictureBlocks.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
|
||||
@interface PictureBlocks : NSObject {
|
||||
}
|
||||
+ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount scanArea:(NSNumber *)scanArea;
|
||||
+ (NSSize)getImageSize:(NSString *)imagePath;
|
||||
@end
|
||||
|
||||
|
||||
NSString* GetBlocks(NSString *filePath, int blockCount, int scanSize);
|
||||
139
pe/cocoa/PictureBlocks.m
Normal file
@@ -0,0 +1,139 @@
|
||||
#import "PictureBlocks.h"
|
||||
#import "Utils.h"
|
||||
|
||||
|
||||
@implementation PictureBlocks
|
||||
+ (NSString *)getBlocksFromImagePath:(NSString *)imagePath blockCount:(NSNumber *)blockCount scanArea:(NSNumber *)scanArea
|
||||
{
|
||||
return GetBlocks(imagePath, n2i(blockCount), n2i(scanArea));
|
||||
}
|
||||
|
||||
+ (NSSize)getImageSize:(NSString *)imagePath
|
||||
{
|
||||
CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)imagePath, kCFURLPOSIXPathStyle, FALSE);
|
||||
CGImageSourceRef source = CGImageSourceCreateWithURL(fileURL, NULL);
|
||||
if (source == NULL)
|
||||
return NSMakeSize(0, 0);
|
||||
CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, NULL);
|
||||
if (image == NULL)
|
||||
return NSMakeSize(0, 0);
|
||||
size_t width = CGImageGetWidth(image);
|
||||
size_t height = CGImageGetHeight(image);
|
||||
CGImageRelease(image);
|
||||
CFRelease(source);
|
||||
CFRelease(fileURL);
|
||||
return NSMakeSize(width, height);
|
||||
}
|
||||
@end
|
||||
|
||||
CGContextRef MyCreateBitmapContext (int width, int height)
|
||||
{
|
||||
CGContextRef context = NULL;
|
||||
CGColorSpaceRef colorSpace;
|
||||
void * bitmapData;
|
||||
int bitmapByteCount;
|
||||
int bitmapBytesPerRow;
|
||||
|
||||
bitmapBytesPerRow = (width * 4);
|
||||
bitmapByteCount = (bitmapBytesPerRow * height);
|
||||
|
||||
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
|
||||
|
||||
bitmapData = malloc( bitmapByteCount );
|
||||
if (bitmapData == NULL)
|
||||
{
|
||||
fprintf (stderr, "Memory not allocated!");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
context = CGBitmapContextCreate (bitmapData,width,height,8,bitmapBytesPerRow,colorSpace,kCGImageAlphaNoneSkipLast);
|
||||
if (context== NULL)
|
||||
{
|
||||
free (bitmapData);
|
||||
fprintf (stderr, "Context not created!");
|
||||
return NULL;
|
||||
}
|
||||
CGColorSpaceRelease( colorSpace );
|
||||
return context;
|
||||
}
|
||||
|
||||
// returns 0x00RRGGBB
|
||||
int GetBlock(unsigned char *imageData, int imageWidth, int imageHeight, int boxX, int boxY, int boxW, int boxH)
|
||||
{
|
||||
int i,j;
|
||||
int totalR = 0;
|
||||
int totalG = 0;
|
||||
int totalB = 0;
|
||||
for(i = boxY; i < boxY + boxH; i++)
|
||||
{
|
||||
for(j = boxX; j < boxX + boxW; j++)
|
||||
{
|
||||
int offset = (i * imageWidth * 4) + (j * 4);
|
||||
totalR += *(imageData + offset);
|
||||
totalG += *(imageData + offset + 1);
|
||||
totalB += *(imageData + offset + 2);
|
||||
}
|
||||
}
|
||||
int pixelCount = boxH * boxW;
|
||||
int result = 0;
|
||||
result += (totalR / pixelCount) << 16;
|
||||
result += (totalG / pixelCount) << 8;
|
||||
result += (totalB / pixelCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
NSString* GetBlocks (NSString* filePath, int blockCount, int scanSize)
|
||||
{
|
||||
CFURLRef fileURL = CFURLCreateWithFileSystemPath(NULL, (CFStringRef)filePath, kCFURLPOSIXPathStyle, FALSE);
|
||||
CGImageSourceRef source = CGImageSourceCreateWithURL(fileURL, NULL);
|
||||
if (source == NULL)
|
||||
return NULL;
|
||||
CGImageRef image = CGImageSourceCreateImageAtIndex(source, 0, NULL);
|
||||
if (image == NULL)
|
||||
return NULL;
|
||||
size_t width = CGImageGetWidth(image);
|
||||
size_t height = CGImageGetHeight(image);
|
||||
if ((scanSize > 0) && (width > scanSize))
|
||||
width = scanSize;
|
||||
if ((scanSize > 0) && (height > scanSize))
|
||||
height = scanSize;
|
||||
CGContextRef myContext = MyCreateBitmapContext(width, height);
|
||||
CGRect myBoundingBox = CGRectMake (0, 0, width, height);
|
||||
CGContextDrawImage(myContext, myBoundingBox, image);
|
||||
unsigned char *bitmapData = CGBitmapContextGetData(myContext);
|
||||
if (bitmapData == NULL)
|
||||
return NULL;
|
||||
|
||||
int blockHeight = height / blockCount;
|
||||
if (blockHeight < 1)
|
||||
blockHeight = 1;
|
||||
int blockWidth = width / blockCount;
|
||||
if (blockWidth < 1)
|
||||
blockWidth = 1;
|
||||
//blockCount might have changed
|
||||
int blockXCount = (width / blockWidth);
|
||||
int blockYCount = (height / blockHeight);
|
||||
|
||||
CFMutableArrayRef blocks = CFArrayCreateMutable(NULL, blockXCount * blockYCount, &kCFTypeArrayCallBacks);
|
||||
int i,j;
|
||||
for(i = 0; i < blockYCount; i++)
|
||||
{
|
||||
for(j = 0; j < blockXCount; j++)
|
||||
{
|
||||
int block = GetBlock(bitmapData, width, height, j * blockWidth, i * blockHeight, blockWidth, blockHeight);
|
||||
CFStringRef strBlock = CFStringCreateWithFormat(NULL, NULL, CFSTR("%06x"), block);
|
||||
CFArrayAppendValue(blocks, strBlock);
|
||||
CFRelease(strBlock);
|
||||
}
|
||||
}
|
||||
|
||||
CGContextRelease (myContext);
|
||||
if (bitmapData) free(bitmapData);
|
||||
CGImageRelease(image);
|
||||
CFRelease(source);
|
||||
CFRelease(fileURL);
|
||||
|
||||
CFStringRef result = CFStringCreateByCombiningStrings(NULL, blocks, CFSTR(""));
|
||||
CFRelease(blocks);
|
||||
return (NSString *)result;
|
||||
}
|
||||
9
pe/cocoa/PyDupeGuru.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "dgbase/PyDupeGuru.h"
|
||||
|
||||
@interface PyDupeGuru : PyDupeGuruBase
|
||||
- (void)clearPictureCache;
|
||||
- (NSString *)getSelectedDupePath;
|
||||
- (NSString *)getSelectedDupeRefPath;
|
||||
- (void)setMatchScaled:(NSNumber *)match_scaled;
|
||||
@end
|
||||
59
pe/cocoa/ResultWindow.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import "Outline.h"
|
||||
#import "dgbase/ResultWindow.h"
|
||||
#import "DirectoryPanel.h"
|
||||
|
||||
@interface ResultWindow : ResultWindowBase
|
||||
{
|
||||
IBOutlet NSPopUpButton *actionMenu;
|
||||
IBOutlet NSView *actionMenuView;
|
||||
IBOutlet id app;
|
||||
IBOutlet NSMenu *columnsMenu;
|
||||
IBOutlet NSView *deltaSwitchView;
|
||||
IBOutlet NSSearchField *filterField;
|
||||
IBOutlet NSView *filterFieldView;
|
||||
IBOutlet NSSegmentedControl *pmSwitch;
|
||||
IBOutlet NSView *pmSwitchView;
|
||||
IBOutlet NSWindow *preferencesPanel;
|
||||
IBOutlet NSTextField *stats;
|
||||
|
||||
NSMutableArray *_resultColumns;
|
||||
NSMutableIndexSet *_deltaColumns;
|
||||
}
|
||||
- (IBAction)changePowerMarker:(id)sender;
|
||||
- (IBAction)clearIgnoreList:(id)sender;
|
||||
- (IBAction)clearPictureCache:(id)sender;
|
||||
- (IBAction)exportToXHTML:(id)sender;
|
||||
- (IBAction)filter:(id)sender;
|
||||
- (IBAction)ignoreSelected:(id)sender;
|
||||
- (IBAction)markAll:(id)sender;
|
||||
- (IBAction)markInvert:(id)sender;
|
||||
- (IBAction)markNone:(id)sender;
|
||||
- (IBAction)markSelected:(id)sender;
|
||||
- (IBAction)markToggle:(id)sender;
|
||||
- (IBAction)openSelected:(id)sender;
|
||||
- (IBAction)refresh:(id)sender;
|
||||
- (IBAction)removeMarked:(id)sender;
|
||||
- (IBAction)removeSelected:(id)sender;
|
||||
- (IBAction)renameSelected:(id)sender;
|
||||
- (IBAction)resetColumnsToDefault:(id)sender;
|
||||
- (IBAction)revealSelected:(id)sender;
|
||||
- (IBAction)showPreferencesPanel:(id)sender;
|
||||
- (IBAction)startDuplicateScan:(id)sender;
|
||||
- (IBAction)switchSelected:(id)sender;
|
||||
- (IBAction)toggleColumn:(id)sender;
|
||||
- (IBAction)toggleDelta:(id)sender;
|
||||
- (IBAction)toggleDetailsPanel:(id)sender;
|
||||
- (IBAction)togglePowerMarker:(id)sender;
|
||||
- (IBAction)toggleDirectories:(id)sender;
|
||||
|
||||
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn;
|
||||
- (NSArray *)getColumnsOrder;
|
||||
- (NSDictionary *)getColumnsWidth;
|
||||
- (NSArray *)getSelected:(BOOL)aDupesOnly;
|
||||
- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly;
|
||||
- (void)performPySelection:(NSArray *)aIndexPaths;
|
||||
- (void)initResultColumns;
|
||||
- (void)refreshStats;
|
||||
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth;
|
||||
@end
|
||||
569
pe/cocoa/ResultWindow.m
Normal file
@@ -0,0 +1,569 @@
|
||||
#import "ResultWindow.h"
|
||||
#import "Dialogs.h"
|
||||
#import "ProgressController.h"
|
||||
#import "RegistrationInterface.h"
|
||||
#import "Utils.h"
|
||||
#import "AppDelegate.h"
|
||||
#import "Consts.h"
|
||||
|
||||
static NSString* tbbDirectories = @"tbbDirectories";
|
||||
static NSString* tbbDetails = @"tbbDetail";
|
||||
static NSString* tbbPreferences = @"tbbPreferences";
|
||||
static NSString* tbbPowerMarker = @"tbbPowerMarker";
|
||||
static NSString* tbbScan = @"tbbScan";
|
||||
static NSString* tbbAction = @"tbbAction";
|
||||
static NSString* tbbDelta = @"tbbDelta";
|
||||
static NSString* tbbFilter = @"tbbFilter";
|
||||
|
||||
@implementation ResultWindow
|
||||
/* Override */
|
||||
- (void)awakeFromNib
|
||||
{
|
||||
[super awakeFromNib];
|
||||
_displayDelta = NO;
|
||||
_powerMode = NO;
|
||||
_deltaColumns = [[NSMutableIndexSet indexSetWithIndexesInRange:NSMakeRange(2,5)] retain];
|
||||
[_deltaColumns removeIndex:3];
|
||||
[_deltaColumns removeIndex:4];
|
||||
[deltaSwitch setSelectedSegment:0];
|
||||
[pmSwitch setSelectedSegment:0];
|
||||
[py setDisplayDeltaValues:b2n(_displayDelta)];
|
||||
[matches setTarget:self];
|
||||
[matches setDoubleAction:@selector(openSelected:)];
|
||||
[[actionMenu itemAtIndex:0] setImage:[NSImage imageNamed: @"gear"]];
|
||||
[self initResultColumns];
|
||||
[self refreshStats];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsMarkingChanged:) name:ResultsMarkingChangedNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(resultsChanged:) name:ResultsChangedNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(jobCompleted:) name:JobCompletedNotification object:nil];
|
||||
|
||||
NSToolbar *t = [[[NSToolbar alloc] initWithIdentifier:@"ResultWindowToolbar"] autorelease];
|
||||
[t setAllowsUserCustomization:YES];
|
||||
[t setAutosavesConfiguration:NO];
|
||||
[t setDisplayMode:NSToolbarDisplayModeIconAndLabel];
|
||||
[t setDelegate:self];
|
||||
[[self window] setToolbar:t];
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
|
||||
- (IBAction)changePowerMarker:(id)sender
|
||||
{
|
||||
_powerMode = [pmSwitch selectedSegment] == 1;
|
||||
if (_powerMode)
|
||||
[matches setTag:2];
|
||||
else
|
||||
[matches setTag:0];
|
||||
[self expandAll:nil];
|
||||
[self outlineView:matches didClickTableColumn:nil];
|
||||
}
|
||||
|
||||
- (IBAction)clearIgnoreList:(id)sender
|
||||
{
|
||||
int i = n2i([py getIgnoreListCount]);
|
||||
if (!i)
|
||||
return;
|
||||
if ([Dialogs askYesNo:[NSString stringWithFormat:@"Do you really want to remove all %d items from the ignore list?",i]] == NSAlertSecondButtonReturn) // NO
|
||||
return;
|
||||
[py clearIgnoreList];
|
||||
}
|
||||
|
||||
- (IBAction)clearPictureCache:(id)sender
|
||||
{
|
||||
if ([Dialogs askYesNo:@"Do you really want to remove all your cached picture analysis?"] == NSAlertSecondButtonReturn) // NO
|
||||
return;
|
||||
[(PyDupeGuru *)py clearPictureCache];
|
||||
}
|
||||
|
||||
- (IBAction)exportToXHTML:(id)sender
|
||||
{
|
||||
NSString *xsltPath = [[NSBundle mainBundle] pathForResource:@"dg" ofType:@"xsl"];
|
||||
NSString *cssPath = [[NSBundle mainBundle] pathForResource:@"hardcoded" ofType:@"css"];
|
||||
NSString *exported = [py exportToXHTMLwithColumns:[self getColumnsOrder] xslt:xsltPath css:cssPath];
|
||||
[[NSWorkspace sharedWorkspace] openFile:exported];
|
||||
}
|
||||
|
||||
- (IBAction)filter:(id)sender
|
||||
{
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
[py setEscapeFilterRegexp:b2n(!n2b([ud objectForKey:@"useRegexpFilter"]))];
|
||||
[py applyFilter:[filterField stringValue]];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)ignoreSelected:(id)sender
|
||||
{
|
||||
NSArray *nodeList = [self getSelected:YES];
|
||||
if (![nodeList count])
|
||||
return;
|
||||
if ([Dialogs askYesNo:[NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
|
||||
return;
|
||||
[self performPySelection:[self getSelectedPaths:YES]];
|
||||
[py addSelectedToIgnoreList];
|
||||
[py removeSelected];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)markAll:(id)sender
|
||||
{
|
||||
[py markAll];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)markInvert:(id)sender
|
||||
{
|
||||
[py markInvert];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)markNone:(id)sender
|
||||
{
|
||||
[py markNone];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)markSelected:(id)sender
|
||||
{
|
||||
[self performPySelection:[self getSelectedPaths:YES]];
|
||||
[py toggleSelectedMark];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)markToggle:(id)sender
|
||||
{
|
||||
OVNode *node = [matches itemAtRow:[matches clickedRow]];
|
||||
[self performPySelection:[NSArray arrayWithObject:p2a([node indexPath])]];
|
||||
[py toggleSelectedMark];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsMarkingChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)openSelected:(id)sender
|
||||
{
|
||||
[self performPySelection:[self getSelectedPaths:NO]];
|
||||
[py openSelected];
|
||||
}
|
||||
|
||||
- (IBAction)refresh:(id)sender
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)removeMarked:(id)sender
|
||||
{
|
||||
int mark_count = [[py getMarkCount] intValue];
|
||||
if (!mark_count)
|
||||
return;
|
||||
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",mark_count]] == NSAlertSecondButtonReturn) // NO
|
||||
return;
|
||||
[py removeMarked];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)removeSelected:(id)sender
|
||||
{
|
||||
NSArray *nodeList = [self getSelected:YES];
|
||||
if (![nodeList count])
|
||||
return;
|
||||
if ([Dialogs askYesNo:[NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",[nodeList count]]] == NSAlertSecondButtonReturn) // NO
|
||||
return;
|
||||
[self performPySelection:[self getSelectedPaths:YES]];
|
||||
[py removeSelected];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:ResultsChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (IBAction)renameSelected:(id)sender
|
||||
{
|
||||
int col = [matches columnWithIdentifier:@"0"];
|
||||
int row = [matches selectedRow];
|
||||
[matches editColumn:col row:row withEvent:[NSApp currentEvent] select:YES];
|
||||
}
|
||||
|
||||
- (IBAction)resetColumnsToDefault:(id)sender
|
||||
{
|
||||
NSMutableArray *columnsOrder = [NSMutableArray array];
|
||||
[columnsOrder addObject:@"0"];
|
||||
[columnsOrder addObject:@"1"];
|
||||
[columnsOrder addObject:@"2"];
|
||||
[columnsOrder addObject:@"4"];
|
||||
[columnsOrder addObject:@"7"];
|
||||
NSMutableDictionary *columnsWidth = [NSMutableDictionary dictionary];
|
||||
[columnsWidth setObject:i2n(125) forKey:@"0"];
|
||||
[columnsWidth setObject:i2n(120) forKey:@"1"];
|
||||
[columnsWidth setObject:i2n(63) forKey:@"2"];
|
||||
[columnsWidth setObject:i2n(73) forKey:@"4"];
|
||||
[columnsWidth setObject:i2n(58) forKey:@"7"];
|
||||
[self restoreColumnsPosition:columnsOrder widths:columnsWidth];
|
||||
}
|
||||
|
||||
- (IBAction)revealSelected:(id)sender
|
||||
{
|
||||
[self performPySelection:[self getSelectedPaths:NO]];
|
||||
[py revealSelected];
|
||||
}
|
||||
|
||||
- (IBAction)showPreferencesPanel:(id)sender
|
||||
{
|
||||
[preferencesPanel makeKeyAndOrderFront:sender];
|
||||
}
|
||||
|
||||
- (IBAction)startDuplicateScan:(id)sender
|
||||
{
|
||||
if ([matches numberOfRows] > 0)
|
||||
{
|
||||
if ([Dialogs askYesNo:@"Are you sure you want to start a new duplicate scan?"] == NSAlertSecondButtonReturn) // NO
|
||||
return;
|
||||
}
|
||||
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
|
||||
PyDupeGuru *_py = (PyDupeGuru *)py;
|
||||
[_py setMinMatchPercentage:[ud objectForKey:@"minMatchPercentage"]];
|
||||
[_py setMixFileKind:[ud objectForKey:@"mixFileKind"]];
|
||||
[_py setMatchScaled:[ud objectForKey:@"matchScaled"]];
|
||||
int r = n2i([py doScan]);
|
||||
[matches reloadData];
|
||||
[self refreshStats];
|
||||
if (r != 0)
|
||||
[[ProgressController mainProgressController] hide];
|
||||
if (r == 1)
|
||||
[Dialogs showMessage:@"You cannot make a duplicate scan with only reference directories."];
|
||||
if (r == 3)
|
||||
{
|
||||
[Dialogs showMessage:@"The selected directories contain no scannable file."];
|
||||
[app toggleDirectories:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (IBAction)switchSelected:(id)sender
|
||||
{
|
||||
[self performPySelection:[self getSelectedPaths:YES]];
|
||||
[py makeSelectedReference];
|
||||
[[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)toggleDelta:(id)sender
|
||||
{
|
||||
if ([deltaSwitch selectedSegment] == 1)
|
||||
[deltaSwitch setSelectedSegment:0];
|
||||
else
|
||||
[deltaSwitch setSelectedSegment:1];
|
||||
[self changeDelta:sender];
|
||||
}
|
||||
|
||||
|
||||
- (IBAction)toggleDetailsPanel:(id)sender
|
||||
{
|
||||
[(AppDelegate *)app toggleDetailsPanel:sender];
|
||||
}
|
||||
|
||||
- (IBAction)togglePowerMarker:(id)sender
|
||||
{
|
||||
if ([pmSwitch selectedSegment] == 1)
|
||||
[pmSwitch setSelectedSegment:0];
|
||||
else
|
||||
[pmSwitch setSelectedSegment:1];
|
||||
[self changePowerMarker:sender];
|
||||
}
|
||||
|
||||
- (IBAction)toggleDirectories:(id)sender
|
||||
{
|
||||
[(AppDelegate *)app toggleDirectories:sender];
|
||||
}
|
||||
|
||||
/* Public */
|
||||
- (NSTableColumn *)getColumnForIdentifier:(int)aIdentifier title:(NSString *)aTitle width:(int)aWidth refCol:(NSTableColumn *)aColumn
|
||||
{
|
||||
NSNumber *n = [NSNumber numberWithInt:aIdentifier];
|
||||
NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:[n stringValue]];
|
||||
[col setWidth:aWidth];
|
||||
[col setEditable:NO];
|
||||
[[col dataCell] setFont:[[aColumn dataCell] font]];
|
||||
[[col headerCell] setStringValue:aTitle];
|
||||
[col setResizingMask:NSTableColumnUserResizingMask];
|
||||
[col setSortDescriptorPrototype:[[NSSortDescriptor alloc] initWithKey:[n stringValue] ascending:YES]];
|
||||
return col;
|
||||
}
|
||||
|
||||
//Returns an array of identifiers, in order.
|
||||
- (NSArray *)getColumnsOrder
|
||||
{
|
||||
NSTableColumn *col;
|
||||
NSString *colId;
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
NSEnumerator *e = [[matches tableColumns] objectEnumerator];
|
||||
while (col = [e nextObject])
|
||||
{
|
||||
colId = [col identifier];
|
||||
[result addObject:colId];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSDictionary *)getColumnsWidth
|
||||
{
|
||||
NSMutableDictionary *result = [NSMutableDictionary dictionary];
|
||||
NSTableColumn *col;
|
||||
NSString *colId;
|
||||
NSNumber *width;
|
||||
NSEnumerator *e = [[matches tableColumns] objectEnumerator];
|
||||
while (col = [e nextObject])
|
||||
{
|
||||
colId = [col identifier];
|
||||
width = [NSNumber numberWithFloat:[col width]];
|
||||
[result setObject:width forKey:colId];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSArray *)getSelected:(BOOL)aDupesOnly
|
||||
{
|
||||
if (_powerMode)
|
||||
aDupesOnly = NO;
|
||||
NSIndexSet *indexes = [matches selectedRowIndexes];
|
||||
NSMutableArray *nodeList = [NSMutableArray array];
|
||||
OVNode *node;
|
||||
int i = [indexes firstIndex];
|
||||
while (i != NSNotFound)
|
||||
{
|
||||
node = [matches itemAtRow:i];
|
||||
if (!aDupesOnly || ([node level] > 1))
|
||||
[nodeList addObject:node];
|
||||
i = [indexes indexGreaterThanIndex:i];
|
||||
}
|
||||
return nodeList;
|
||||
}
|
||||
|
||||
- (NSArray *)getSelectedPaths:(BOOL)aDupesOnly
|
||||
{
|
||||
NSMutableArray *r = [NSMutableArray array];
|
||||
NSArray *selected = [self getSelected:aDupesOnly];
|
||||
NSEnumerator *e = [selected objectEnumerator];
|
||||
OVNode *node;
|
||||
while (node = [e nextObject])
|
||||
[r addObject:p2a([node indexPath])];
|
||||
return r;
|
||||
}
|
||||
|
||||
- (void)performPySelection:(NSArray *)aIndexPaths
|
||||
{
|
||||
if (_powerMode)
|
||||
[py selectPowerMarkerNodePaths:aIndexPaths];
|
||||
else
|
||||
[py selectResultNodePaths:aIndexPaths];
|
||||
}
|
||||
|
||||
- (void)initResultColumns
|
||||
{
|
||||
NSTableColumn *refCol = [matches tableColumnWithIdentifier:@"0"];
|
||||
_resultColumns = [[NSMutableArray alloc] init];
|
||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"0"]]; // File Name
|
||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"1"]]; // Directory
|
||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"2"]]; // Size
|
||||
[_resultColumns addObject:[self getColumnForIdentifier:3 title:@"Kind" width:40 refCol:refCol]];
|
||||
[_resultColumns addObject:[self getColumnForIdentifier:4 title:@"Dimensions" width:80 refCol:refCol]];
|
||||
[_resultColumns addObject:[self getColumnForIdentifier:5 title:@"Creation" width:120 refCol:refCol]];
|
||||
[_resultColumns addObject:[self getColumnForIdentifier:6 title:@"Modification" width:120 refCol:refCol]];
|
||||
[_resultColumns addObject:[matches tableColumnWithIdentifier:@"7"]]; // Match %
|
||||
[_resultColumns addObject:[self getColumnForIdentifier:8 title:@"Dupe Count" width:80 refCol:refCol]];
|
||||
}
|
||||
|
||||
-(void)refreshStats
|
||||
{
|
||||
[stats setStringValue:[py getStatLine]];
|
||||
}
|
||||
|
||||
- (void)restoreColumnsPosition:(NSArray *)aColumnsOrder widths:(NSDictionary *)aColumnsWidth
|
||||
{
|
||||
NSTableColumn *col;
|
||||
NSString *colId;
|
||||
NSNumber *width;
|
||||
NSMenuItem *mi;
|
||||
//Remove all columns
|
||||
NSEnumerator *e = [[columnsMenu itemArray] objectEnumerator];
|
||||
while (mi = [e nextObject])
|
||||
{
|
||||
if ([mi state] == NSOnState)
|
||||
[self toggleColumn:mi];
|
||||
}
|
||||
//Add columns and set widths
|
||||
e = [aColumnsOrder objectEnumerator];
|
||||
while (colId = [e nextObject])
|
||||
{
|
||||
if (![colId isEqual:@"mark"])
|
||||
{
|
||||
col = [_resultColumns objectAtIndex:[colId intValue]];
|
||||
width = [aColumnsWidth objectForKey:[col identifier]];
|
||||
mi = [columnsMenu itemWithTag:[colId intValue]];
|
||||
if (width)
|
||||
[col setWidth:[width floatValue]];
|
||||
[self toggleColumn:mi];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Delegate */
|
||||
- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
|
||||
{
|
||||
OVNode *node = item;
|
||||
if ([[tableColumn identifier] isEqual:@"mark"])
|
||||
{
|
||||
[cell setEnabled: [node isMarkable]];
|
||||
}
|
||||
if ([cell isKindOfClass:[NSTextFieldCell class]])
|
||||
{
|
||||
// Determine if the text color will be blue due to directory being reference.
|
||||
NSTextFieldCell *textCell = cell;
|
||||
if ([node isMarkable])
|
||||
[textCell setTextColor:[NSColor blackColor]];
|
||||
else
|
||||
[textCell setTextColor:[NSColor blueColor]];
|
||||
if ((_displayDelta) && (_powerMode || ([node level] > 1)))
|
||||
{
|
||||
int i = [[tableColumn identifier] intValue];
|
||||
if ([_deltaColumns containsIndex:i])
|
||||
[textCell setTextColor:[NSColor orangeColor]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)outlineViewSelectionDidChange:(NSNotification *)notification
|
||||
{
|
||||
[self performPySelection:[self getSelectedPaths:NO]];
|
||||
[py refreshDetailsWithSelected];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:DuplicateSelectionChangedNotification object:self];
|
||||
}
|
||||
|
||||
- (void)resultsChanged:(NSNotification *)aNotification
|
||||
{
|
||||
[matches reloadData];
|
||||
[self expandAll:nil];
|
||||
[self outlineViewSelectionDidChange:nil];
|
||||
[self refreshStats];
|
||||
}
|
||||
|
||||
- (void)resultsMarkingChanged:(NSNotification *)aNotification
|
||||
{
|
||||
[matches invalidateMarkings];
|
||||
[self refreshStats];
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
|
||||
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
|
||||
{
|
||||
NSToolbarItem *tbi = [[[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier] autorelease];
|
||||
if (itemIdentifier == tbbDirectories)
|
||||
{
|
||||
[tbi setLabel: @"Directories"];
|
||||
[tbi setToolTip: @"Show/Hide the directories panel."];
|
||||
[tbi setImage: [NSImage imageNamed: @"folder32"]];
|
||||
[tbi setTarget: self];
|
||||
[tbi setAction: @selector(toggleDirectories:)];
|
||||
}
|
||||
else if (itemIdentifier == 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 == 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 == 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 == tbbScan)
|
||||
{
|
||||
[tbi setLabel: @"Start Scanning"];
|
||||
[tbi setToolTip: @"Start scanning for duplicates in the selected diectories."];
|
||||
[tbi setImage: [NSImage imageNamed: @"dgpe_logo_32"]];
|
||||
[tbi setTarget: self];
|
||||
[tbi setAction: @selector(startDuplicateScan:)];
|
||||
}
|
||||
else if (itemIdentifier == tbbAction)
|
||||
{
|
||||
[tbi setLabel: @"Action"];
|
||||
[tbi setView:actionMenuView];
|
||||
[tbi setMinSize:[actionMenuView frame].size];
|
||||
[tbi setMaxSize:[actionMenuView frame].size];
|
||||
}
|
||||
else if (itemIdentifier == 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 == 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];
|
||||
}
|
||||
@end
|
||||
BIN
pe/cocoa/dupeguru.icns
Executable file
1380
pe/cocoa/dupeguru.xcodeproj/hsoft.mode1
Normal file
588
pe/cocoa/dupeguru.xcodeproj/project.pbxproj
Normal file
@@ -0,0 +1,588 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 42;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; };
|
||||
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; };
|
||||
8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; };
|
||||
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
|
||||
CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */ = {isa = PBXBuildFile; fileRef = CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */; };
|
||||
CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C46A90FA0647E000BE99B /* PictureBlocks.m */; };
|
||||
CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; };
|
||||
CE15C8C00ADEB8D40061D4A5 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */; };
|
||||
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9409914ACE003581CE /* AppDelegate.m */; };
|
||||
CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE381C9A09914ADF003581CE /* ResultWindow.m */; };
|
||||
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */ = {isa = PBXBuildFile; fileRef = CE381CF509915304003581CE /* dg_cocoa.plugin */; };
|
||||
CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE3AA46509DB207900DB3A21 /* Directories.nib */; };
|
||||
CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE68EE6609ABC48000971085 /* DirectoryPanel.m */; };
|
||||
CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1C0FC192D60086DCA6 /* Dialogs.m */; };
|
||||
CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */; };
|
||||
CE80DB300FC192D60086DCA6 /* Outline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB200FC192D60086DCA6 /* Outline.m */; };
|
||||
CE80DB310FC192D60086DCA6 /* ProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB220FC192D60086DCA6 /* ProgressController.m */; };
|
||||
CE80DB320FC192D60086DCA6 /* RecentDirectories.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB250FC192D60086DCA6 /* RecentDirectories.m */; };
|
||||
CE80DB330FC192D60086DCA6 /* RegistrationInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */; };
|
||||
CE80DB340FC192D60086DCA6 /* Table.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB290FC192D60086DCA6 /* Table.m */; };
|
||||
CE80DB350FC192D60086DCA6 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB2B0FC192D60086DCA6 /* Utils.m */; };
|
||||
CE80DB360FC192D60086DCA6 /* ValueTransformers.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */; };
|
||||
CE80DB470FC193650086DCA6 /* NSNotificationAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */; };
|
||||
CE80DB4A0FC193770086DCA6 /* NSImageAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB490FC193770086DCA6 /* NSImageAdditions.m */; };
|
||||
CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */; };
|
||||
CE80DB770FC194760086DCA6 /* progress.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB720FC194760086DCA6 /* progress.nib */; };
|
||||
CE80DB780FC194760086DCA6 /* registration.nib in Resources */ = {isa = PBXBuildFile; fileRef = CE80DB740FC194760086DCA6 /* registration.nib */; };
|
||||
CE80DB8A0FC1951C0086DCA6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB830FC1951C0086DCA6 /* AppDelegate.m */; };
|
||||
CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */; };
|
||||
CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CE80DB890FC1951C0086DCA6 /* ResultWindow.m */; };
|
||||
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE848A1809DD85810004CB44 /* Consts.h */; };
|
||||
CECA899909DB12CA00A3D774 /* Details.nib in Resources */ = {isa = PBXBuildFile; fileRef = CECA899709DB12CA00A3D774 /* Details.nib */; };
|
||||
CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = CECA899A09DB132E00A3D774 /* DetailsPanel.h */; };
|
||||
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = CECA899B09DB132E00A3D774 /* DetailsPanel.m */; };
|
||||
CEDA432E0B07C6E600B3091A /* dg.xsl in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432C0B07C6E600B3091A /* dg.xsl */; };
|
||||
CEDA432F0B07C6E600B3091A /* hardcoded.css in Resources */ = {isa = PBXBuildFile; fileRef = CEDA432D0B07C6E600B3091A /* hardcoded.css */; };
|
||||
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */ = {isa = PBXBuildFile; fileRef = CEEB135109C837A2004D2330 /* dupeguru.icns */; };
|
||||
CEF4112B0A11069600E7F110 /* Consts.m in Sources */ = {isa = PBXBuildFile; fileRef = CEF4112A0A11069600E7F110 /* Consts.m */; };
|
||||
CEF7823809C8AA0200EF38FF /* gear.png in Resources */ = {isa = PBXBuildFile; fileRef = CEF7823709C8AA0200EF38FF /* gear.png */; };
|
||||
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC294509C89E3D00D9F998 /* folder32.png */; };
|
||||
CEFC295509C89FF200D9F998 /* details32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295309C89FF200D9F998 /* details32.png */; };
|
||||
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFC295409C89FF200D9F998 /* preferences32.png */; };
|
||||
CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */ = {isa = PBXBuildFile; fileRef = CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
CECC02B709A36E8200CC0A94 /* CopyFiles */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
CE15C8C00ADEB8D40061D4A5 /* Sparkle.framework in CopyFiles */,
|
||||
CECA899C09DB132E00A3D774 /* DetailsPanel.h in CopyFiles */,
|
||||
CE848A1909DD85810004CB44 /* Consts.h in CopyFiles */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = "<absolute>"; };
|
||||
13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = "<absolute>"; };
|
||||
29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = SOURCE_ROOT; };
|
||||
29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = "<group>"; };
|
||||
29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = "<absolute>"; };
|
||||
29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = "<absolute>"; };
|
||||
8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = SOURCE_ROOT; };
|
||||
8D1107320486CEB800E47090 /* dupeGuru PE.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "dupeGuru PE.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dupeguru_pe_help; path = help/dupeguru_pe_help; sourceTree = SOURCE_ROOT; };
|
||||
CE0C46A80FA0647E000BE99B /* PictureBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PictureBlocks.h; sourceTree = "<group>"; };
|
||||
CE0C46A90FA0647E000BE99B /* PictureBlocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PictureBlocks.m; sourceTree = "<group>"; };
|
||||
CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Sparkle.framework; path = /Library/Frameworks/Sparkle.framework; sourceTree = "<absolute>"; };
|
||||
CE381C9409914ACE003581CE /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = SOURCE_ROOT; };
|
||||
CE381C9509914ACE003581CE /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = SOURCE_ROOT; };
|
||||
CE381C9A09914ADF003581CE /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = ResultWindow.m; sourceTree = SOURCE_ROOT; };
|
||||
CE381C9B09914ADF003581CE /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = ResultWindow.h; sourceTree = SOURCE_ROOT; };
|
||||
CE381CF509915304003581CE /* dg_cocoa.plugin */ = {isa = PBXFileReference; lastKnownFileType = folder; name = dg_cocoa.plugin; path = py/dist/dg_cocoa.plugin; sourceTree = SOURCE_ROOT; };
|
||||
CE3AA46609DB207900DB3A21 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Directories.nib; sourceTree = "<group>"; };
|
||||
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; };
|
||||
CE80DB1B0FC192D60086DCA6 /* Dialogs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Dialogs.h; path = cocoalib/Dialogs.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB1C0FC192D60086DCA6 /* Dialogs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Dialogs.m; path = cocoalib/Dialogs.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSErrorReportWindow.h; path = cocoalib/HSErrorReportWindow.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSErrorReportWindow.m; path = cocoalib/HSErrorReportWindow.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB1F0FC192D60086DCA6 /* Outline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Outline.h; path = cocoalib/Outline.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB200FC192D60086DCA6 /* Outline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Outline.m; path = cocoalib/Outline.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB210FC192D60086DCA6 /* ProgressController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ProgressController.h; path = cocoalib/ProgressController.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB220FC192D60086DCA6 /* ProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProgressController.m; path = cocoalib/ProgressController.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB230FC192D60086DCA6 /* PyApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyApp.h; path = cocoalib/PyApp.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB240FC192D60086DCA6 /* RecentDirectories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RecentDirectories.h; path = cocoalib/RecentDirectories.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB250FC192D60086DCA6 /* RecentDirectories.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RecentDirectories.m; path = cocoalib/RecentDirectories.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB260FC192D60086DCA6 /* RegistrationInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegistrationInterface.h; path = cocoalib/RegistrationInterface.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RegistrationInterface.m; path = cocoalib/RegistrationInterface.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB280FC192D60086DCA6 /* Table.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Table.h; path = cocoalib/Table.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB290FC192D60086DCA6 /* Table.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Table.m; path = cocoalib/Table.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB2A0FC192D60086DCA6 /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = cocoalib/Utils.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB2B0FC192D60086DCA6 /* Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Utils.m; path = cocoalib/Utils.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB2C0FC192D60086DCA6 /* ValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ValueTransformers.h; path = cocoalib/ValueTransformers.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ValueTransformers.m; path = cocoalib/ValueTransformers.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB450FC193650086DCA6 /* NSNotificationAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSNotificationAdditions.h; path = cocoalib/NSNotificationAdditions.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSNotificationAdditions.m; path = cocoalib/NSNotificationAdditions.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB480FC193770086DCA6 /* NSImageAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSImageAdditions.h; path = cocoalib/NSImageAdditions.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB490FC193770086DCA6 /* NSImageAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSImageAdditions.m; path = cocoalib/NSImageAdditions.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB710FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = English; path = cocoalib/English.lproj/ErrorReportWindow.xib; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB730FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/progress.nib; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB750FC194760086DCA6 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = cocoalib/English.lproj/registration.nib; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB820FC1951C0086DCA6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = dgbase/AppDelegate.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB830FC1951C0086DCA6 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = dgbase/AppDelegate.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB840FC1951C0086DCA6 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Consts.h; path = dgbase/Consts.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DirectoryPanel.h; path = dgbase/DirectoryPanel.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DirectoryPanel.m; path = dgbase/DirectoryPanel.m; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDupeGuru.h; path = dgbase/PyDupeGuru.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB880FC1951C0086DCA6 /* ResultWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultWindow.h; path = dgbase/ResultWindow.h; sourceTree = SOURCE_ROOT; };
|
||||
CE80DB890FC1951C0086DCA6 /* ResultWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultWindow.m; path = dgbase/ResultWindow.m; sourceTree = SOURCE_ROOT; };
|
||||
CE848A1809DD85810004CB44 /* Consts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Consts.h; sourceTree = "<group>"; };
|
||||
CECA899809DB12CA00A3D774 /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/Details.nib; sourceTree = "<group>"; };
|
||||
CECA899A09DB132E00A3D774 /* DetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = DetailsPanel.h; sourceTree = "<group>"; };
|
||||
CECA899B09DB132E00A3D774 /* DetailsPanel.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = DetailsPanel.m; sourceTree = "<group>"; };
|
||||
CEDA432C0B07C6E600B3091A /* dg.xsl */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text.xml; name = dg.xsl; path = w3/dg.xsl; sourceTree = SOURCE_ROOT; };
|
||||
CEDA432D0B07C6E600B3091A /* hardcoded.css */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = text; name = hardcoded.css; path = w3/hardcoded.css; sourceTree = SOURCE_ROOT; };
|
||||
CEEB135109C837A2004D2330 /* dupeguru.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = dupeguru.icns; sourceTree = "<group>"; };
|
||||
CEF4112A0A11069600E7F110 /* Consts.m */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.objc; path = Consts.m; sourceTree = SOURCE_ROOT; };
|
||||
CEF7823709C8AA0200EF38FF /* gear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = gear.png; path = images/gear.png; sourceTree = "<group>"; };
|
||||
CEFC294509C89E3D00D9F998 /* folder32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = folder32.png; path = images/folder32.png; sourceTree = SOURCE_ROOT; };
|
||||
CEFC295309C89FF200D9F998 /* details32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = details32.png; path = images/details32.png; sourceTree = SOURCE_ROOT; };
|
||||
CEFC295409C89FF200D9F998 /* preferences32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = preferences32.png; path = images/preferences32.png; sourceTree = SOURCE_ROOT; };
|
||||
CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = dgpe_logo_32.png; path = images/dgpe_logo_32.png; sourceTree = SOURCE_ROOT; };
|
||||
CEFF18A009A4D387005E6321 /* PyDupeGuru.h */ = {isa = PBXFileReference; fileEncoding = 5; lastKnownFileType = sourcecode.c.h; path = PyDupeGuru.h; sourceTree = SOURCE_ROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
8D11072E0486CEB800E47090 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */,
|
||||
CE15C8A80ADEB8B50061D4A5 /* Sparkle.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
080E96DDFE201D6D7F000001 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE0C46A80FA0647E000BE99B /* PictureBlocks.h */,
|
||||
CE0C46A90FA0647E000BE99B /* PictureBlocks.m */,
|
||||
CE381C9509914ACE003581CE /* AppDelegate.h */,
|
||||
CE381C9409914ACE003581CE /* AppDelegate.m */,
|
||||
CE848A1809DD85810004CB44 /* Consts.h */,
|
||||
CEF4112A0A11069600E7F110 /* Consts.m */,
|
||||
CECA899A09DB132E00A3D774 /* DetailsPanel.h */,
|
||||
CECA899B09DB132E00A3D774 /* DetailsPanel.m */,
|
||||
CE68EE6509ABC48000971085 /* DirectoryPanel.h */,
|
||||
CE68EE6609ABC48000971085 /* DirectoryPanel.m */,
|
||||
CEFF18A009A4D387005E6321 /* PyDupeGuru.h */,
|
||||
CE381C9B09914ADF003581CE /* ResultWindow.h */,
|
||||
CE381C9A09914ADF003581CE /* ResultWindow.m */,
|
||||
);
|
||||
name = Classes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE15C8A70ADEB8B50061D4A5 /* Sparkle.framework */,
|
||||
1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */,
|
||||
);
|
||||
name = "Linked Frameworks";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
29B97324FDCFA39411CA2CEA /* AppKit.framework */,
|
||||
13E42FB307B3F0F600E4EEF1 /* CoreData.framework */,
|
||||
29B97325FDCFA39411CA2CEA /* Foundation.framework */,
|
||||
);
|
||||
name = "Other Frameworks";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
19C28FACFE9D520D11CA2CBB /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8D1107320486CEB800E47090 /* dupeGuru PE.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97314FDCFA39411CA2CEA /* dupeguru */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
080E96DDFE201D6D7F000001 /* Classes */,
|
||||
CE80DB1A0FC192AB0086DCA6 /* cocoalib */,
|
||||
CE80DB810FC194BD0086DCA6 /* dgbase */,
|
||||
29B97315FDCFA39411CA2CEA /* Other Sources */,
|
||||
29B97317FDCFA39411CA2CEA /* Resources */,
|
||||
29B97323FDCFA39411CA2CEA /* Frameworks */,
|
||||
19C28FACFE9D520D11CA2CBB /* Products */,
|
||||
);
|
||||
name = dupeguru;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97315FDCFA39411CA2CEA /* Other Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
29B97316FDCFA39411CA2CEA /* main.m */,
|
||||
);
|
||||
name = "Other Sources";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97317FDCFA39411CA2CEA /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE073F5409CAE1A3005C1D2F /* dupeguru_pe_help */,
|
||||
CE381CF509915304003581CE /* dg_cocoa.plugin */,
|
||||
CEFC294309C89E0000D9F998 /* images */,
|
||||
CEDA432B0B07C6E600B3091A /* w3 */,
|
||||
CEEB135109C837A2004D2330 /* dupeguru.icns */,
|
||||
8D1107310486CEB800E47090 /* Info.plist */,
|
||||
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */,
|
||||
CECA899709DB12CA00A3D774 /* Details.nib */,
|
||||
CE3AA46509DB207900DB3A21 /* Directories.nib */,
|
||||
29B97318FDCFA39411CA2CEA /* MainMenu.nib */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97323FDCFA39411CA2CEA /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */,
|
||||
1058C7A2FEA54F0111CA2CBB /* Other Frameworks */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE80DB1A0FC192AB0086DCA6 /* cocoalib */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */,
|
||||
CE80DB720FC194760086DCA6 /* progress.nib */,
|
||||
CE80DB740FC194760086DCA6 /* registration.nib */,
|
||||
CE80DB480FC193770086DCA6 /* NSImageAdditions.h */,
|
||||
CE80DB490FC193770086DCA6 /* NSImageAdditions.m */,
|
||||
CE80DB450FC193650086DCA6 /* NSNotificationAdditions.h */,
|
||||
CE80DB460FC193650086DCA6 /* NSNotificationAdditions.m */,
|
||||
CE80DB1B0FC192D60086DCA6 /* Dialogs.h */,
|
||||
CE80DB1C0FC192D60086DCA6 /* Dialogs.m */,
|
||||
CE80DB1D0FC192D60086DCA6 /* HSErrorReportWindow.h */,
|
||||
CE80DB1E0FC192D60086DCA6 /* HSErrorReportWindow.m */,
|
||||
CE80DB1F0FC192D60086DCA6 /* Outline.h */,
|
||||
CE80DB200FC192D60086DCA6 /* Outline.m */,
|
||||
CE80DB210FC192D60086DCA6 /* ProgressController.h */,
|
||||
CE80DB220FC192D60086DCA6 /* ProgressController.m */,
|
||||
CE80DB230FC192D60086DCA6 /* PyApp.h */,
|
||||
CE80DB240FC192D60086DCA6 /* RecentDirectories.h */,
|
||||
CE80DB250FC192D60086DCA6 /* RecentDirectories.m */,
|
||||
CE80DB260FC192D60086DCA6 /* RegistrationInterface.h */,
|
||||
CE80DB270FC192D60086DCA6 /* RegistrationInterface.m */,
|
||||
CE80DB280FC192D60086DCA6 /* Table.h */,
|
||||
CE80DB290FC192D60086DCA6 /* Table.m */,
|
||||
CE80DB2A0FC192D60086DCA6 /* Utils.h */,
|
||||
CE80DB2B0FC192D60086DCA6 /* Utils.m */,
|
||||
CE80DB2C0FC192D60086DCA6 /* ValueTransformers.h */,
|
||||
CE80DB2D0FC192D60086DCA6 /* ValueTransformers.m */,
|
||||
);
|
||||
name = cocoalib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE80DB810FC194BD0086DCA6 /* dgbase */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE80DB820FC1951C0086DCA6 /* AppDelegate.h */,
|
||||
CE80DB830FC1951C0086DCA6 /* AppDelegate.m */,
|
||||
CE80DB840FC1951C0086DCA6 /* Consts.h */,
|
||||
CE80DB850FC1951C0086DCA6 /* DirectoryPanel.h */,
|
||||
CE80DB860FC1951C0086DCA6 /* DirectoryPanel.m */,
|
||||
CE80DB870FC1951C0086DCA6 /* PyDupeGuru.h */,
|
||||
CE80DB880FC1951C0086DCA6 /* ResultWindow.h */,
|
||||
CE80DB890FC1951C0086DCA6 /* ResultWindow.m */,
|
||||
);
|
||||
name = dgbase;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEDA432B0B07C6E600B3091A /* w3 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CEDA432C0B07C6E600B3091A /* dg.xsl */,
|
||||
CEDA432D0B07C6E600B3091A /* hardcoded.css */,
|
||||
);
|
||||
path = w3;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
CEFC294309C89E0000D9F998 /* images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CEFCDE2C0AB0418600C33A93 /* dgpe_logo_32.png */,
|
||||
CEF7823709C8AA0200EF38FF /* gear.png */,
|
||||
CEFC295309C89FF200D9F998 /* details32.png */,
|
||||
CEFC295409C89FF200D9F998 /* preferences32.png */,
|
||||
CEFC294509C89E3D00D9F998 /* folder32.png */,
|
||||
);
|
||||
name = images;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
8D1107260486CEB800E47090 /* dupeguru */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */;
|
||||
buildPhases = (
|
||||
CEABE1A60FCC00E3005F8031 /* ShellScript */,
|
||||
8D1107290486CEB800E47090 /* Resources */,
|
||||
8D11072C0486CEB800E47090 /* Sources */,
|
||||
8D11072E0486CEB800E47090 /* Frameworks */,
|
||||
CECC02B709A36E8200CC0A94 /* CopyFiles */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = dupeguru;
|
||||
productInstallPath = "$(HOME)/Applications";
|
||||
productName = dupeguru;
|
||||
productReference = 8D1107320486CEB800E47090 /* dupeGuru PE.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
29B97313FDCFA39411CA2CEA /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */;
|
||||
compatibilityVersion = "Xcode 2.4";
|
||||
hasScannedForEncodings = 1;
|
||||
mainGroup = 29B97314FDCFA39411CA2CEA /* dupeguru */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
8D1107260486CEB800E47090 /* dupeguru */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
8D1107290486CEB800E47090 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */,
|
||||
8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */,
|
||||
CE381D0509915304003581CE /* dg_cocoa.plugin in Resources */,
|
||||
CE073F6309CAE1A3005C1D2F /* dupeguru_pe_help in Resources */,
|
||||
CEEB135209C837A2004D2330 /* dupeguru.icns in Resources */,
|
||||
CEFC294609C89E3D00D9F998 /* folder32.png in Resources */,
|
||||
CEFC295509C89FF200D9F998 /* details32.png in Resources */,
|
||||
CEFC295609C89FF200D9F998 /* preferences32.png in Resources */,
|
||||
CEF7823809C8AA0200EF38FF /* gear.png in Resources */,
|
||||
CECA899909DB12CA00A3D774 /* Details.nib in Resources */,
|
||||
CE3AA46709DB207900DB3A21 /* Directories.nib in Resources */,
|
||||
CEFCDE2D0AB0418600C33A93 /* dgpe_logo_32.png in Resources */,
|
||||
CEDA432E0B07C6E600B3091A /* dg.xsl in Resources */,
|
||||
CEDA432F0B07C6E600B3091A /* hardcoded.css in Resources */,
|
||||
CE80DB760FC194760086DCA6 /* ErrorReportWindow.xib in Resources */,
|
||||
CE80DB770FC194760086DCA6 /* progress.nib in Resources */,
|
||||
CE80DB780FC194760086DCA6 /* registration.nib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
CEABE1A60FCC00E3005F8031 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "cd help\npython gen.py\n/Developer/Applications/Utilities/Help\\ Indexer.app/Contents/MacOS/Help\\ Indexer dupeguru_pe_help";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
8D11072C0486CEB800E47090 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
8D11072D0486CEB800E47090 /* main.m in Sources */,
|
||||
CE381C9609914ACE003581CE /* AppDelegate.m in Sources */,
|
||||
CE381C9C09914ADF003581CE /* ResultWindow.m in Sources */,
|
||||
CE68EE6809ABC48000971085 /* DirectoryPanel.m in Sources */,
|
||||
CECA899D09DB132E00A3D774 /* DetailsPanel.m in Sources */,
|
||||
CEF4112B0A11069600E7F110 /* Consts.m in Sources */,
|
||||
CE0C46AA0FA0647E000BE99B /* PictureBlocks.m in Sources */,
|
||||
CE80DB2E0FC192D60086DCA6 /* Dialogs.m in Sources */,
|
||||
CE80DB2F0FC192D60086DCA6 /* HSErrorReportWindow.m in Sources */,
|
||||
CE80DB300FC192D60086DCA6 /* Outline.m in Sources */,
|
||||
CE80DB310FC192D60086DCA6 /* ProgressController.m in Sources */,
|
||||
CE80DB320FC192D60086DCA6 /* RecentDirectories.m in Sources */,
|
||||
CE80DB330FC192D60086DCA6 /* RegistrationInterface.m in Sources */,
|
||||
CE80DB340FC192D60086DCA6 /* Table.m in Sources */,
|
||||
CE80DB350FC192D60086DCA6 /* Utils.m in Sources */,
|
||||
CE80DB360FC192D60086DCA6 /* ValueTransformers.m in Sources */,
|
||||
CE80DB470FC193650086DCA6 /* NSNotificationAdditions.m in Sources */,
|
||||
CE80DB4A0FC193770086DCA6 /* NSImageAdditions.m in Sources */,
|
||||
CE80DB8A0FC1951C0086DCA6 /* AppDelegate.m in Sources */,
|
||||
CE80DB8B0FC1951C0086DCA6 /* DirectoryPanel.m in Sources */,
|
||||
CE80DB8C0FC1951C0086DCA6 /* ResultWindow.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
089C165DFE840E0CC02AAC07 /* English */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
29B97319FDCFA39411CA2CEA /* English */,
|
||||
);
|
||||
name = MainMenu.nib;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
CE3AA46509DB207900DB3A21 /* Directories.nib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE3AA46609DB207900DB3A21 /* English */,
|
||||
);
|
||||
name = Directories.nib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE80DB700FC194760086DCA6 /* ErrorReportWindow.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE80DB710FC194760086DCA6 /* English */,
|
||||
);
|
||||
name = ErrorReportWindow.xib;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
CE80DB720FC194760086DCA6 /* progress.nib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE80DB730FC194760086DCA6 /* English */,
|
||||
);
|
||||
name = progress.nib;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
CE80DB740FC194760086DCA6 /* registration.nib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CE80DB750FC194760086DCA6 /* English */,
|
||||
);
|
||||
name = registration.nib;
|
||||
sourceTree = SOURCE_ROOT;
|
||||
};
|
||||
CECA899709DB12CA00A3D774 /* Details.nib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
CECA899809DB12CA00A3D774 /* English */,
|
||||
);
|
||||
name = Details.nib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
C01FCF4B08A954540054247B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
COPY_PHASE_STRIP = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(FRAMEWORK_SEARCH_PATHS)",
|
||||
"$(SRCROOT)/cocoalib/build/Release",
|
||||
"$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)",
|
||||
);
|
||||
FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1 = "\"$(SRCROOT)/dgbase/build/Release\"";
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_ENABLE_FIX_AND_CONTINUE = YES;
|
||||
GCC_MODEL_TUNING = G5;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
INSTALL_PATH = "$(HOME)/Applications";
|
||||
PRODUCT_NAME = dupeGuru;
|
||||
WRAPPER_EXTENSION = app;
|
||||
ZERO_LINK = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C01FCF4C08A954540054247B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = (
|
||||
ppc,
|
||||
i386,
|
||||
);
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(FRAMEWORK_SEARCH_PATHS)",
|
||||
"$(FRAMEWORK_SEARCH_PATHS_QUOTED_FOR_TARGET_1)",
|
||||
);
|
||||
GCC_GENERATE_DEBUGGING_SYMBOLS = NO;
|
||||
GCC_MODEL_TUNING = G5;
|
||||
INFOPLIST_FILE = Info.plist;
|
||||
INSTALL_PATH = "$(HOME)/Applications";
|
||||
PRODUCT_NAME = "dupeGuru PE";
|
||||
WRAPPER_EXTENSION = app;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C01FCF4F08A954540054247B /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
GCC_C_LANGUAGE_STANDARD = c99;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.4;
|
||||
PREBINDING = NO;
|
||||
SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C01FCF5008A954540054247B /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ARCHS = (
|
||||
ppc,
|
||||
i386,
|
||||
);
|
||||
FRAMEWORK_SEARCH_PATHS = "";
|
||||
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 = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "dupeguru" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C01FCF4B08A954540054247B /* Debug */,
|
||||
C01FCF4C08A954540054247B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C01FCF4E08A954540054247B /* Build configuration list for PBXProject "dupeguru" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C01FCF4F08A954540054247B /* Debug */,
|
||||
C01FCF5008A954540054247B /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 29B97313FDCFA39411CA2CEA /* Project object */;
|
||||
}
|
||||
21
pe/cocoa/main.m
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// main.m
|
||||
// dupeguru
|
||||
//
|
||||
// Created by Virgil Dupras on 2006/02/01.
|
||||
// Copyright __MyCompanyName__ 2006. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
NSString *pluginPath = [[NSBundle mainBundle]
|
||||
pathForResource:@"dg_cocoa"
|
||||
ofType:@"plugin"];
|
||||
NSBundle *pluginBundle = [NSBundle bundleWithPath:pluginPath];
|
||||
[pluginBundle load];
|
||||
[pool release];
|
||||
return NSApplicationMain(argc, (const char **) argv);
|
||||
}
|
||||
2
pe/cocoa/py/build_py.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
python setup.py py2app
|
||||
199
pe/cocoa/py/dg_cocoa.py
Normal file
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python
|
||||
import objc
|
||||
from AppKit import *
|
||||
from PyObjCTools import NibClassBuilder
|
||||
|
||||
NibClassBuilder.extractClasses("MainMenu", bundle=NSBundle.mainBundle())
|
||||
|
||||
from dupeguru import app_pe_cocoa, scanner
|
||||
|
||||
class PyApp(NibClassBuilder.AutoBaseClass):
|
||||
pass #fake class
|
||||
|
||||
class PyDupeGuru(PyApp):
|
||||
def init(self):
|
||||
self = super(PyDupeGuru,self).init()
|
||||
self.app = app_pe_cocoa.DupeGuruPE()
|
||||
return self
|
||||
|
||||
#---Directories
|
||||
def addDirectory_(self,directory):
|
||||
return self.app.AddDirectory(directory)
|
||||
|
||||
def removeDirectory_(self,index):
|
||||
self.app.RemoveDirectory(index)
|
||||
|
||||
def setDirectory_state_(self,node_path,state):
|
||||
self.app.SetDirectoryState(node_path,state)
|
||||
|
||||
#---Results
|
||||
def clearIgnoreList(self):
|
||||
self.app.scanner.ignore_list.Clear()
|
||||
|
||||
def clearPictureCache(self):
|
||||
self.app.scanner.match_factory.cached_blocks.clear()
|
||||
|
||||
def doScan(self):
|
||||
return self.app.start_scanning()
|
||||
|
||||
def exportToXHTMLwithColumns_xslt_css_(self,column_ids,xslt_path,css_path):
|
||||
return self.app.ExportToXHTML(column_ids,xslt_path,css_path)
|
||||
|
||||
def loadIgnoreList(self):
|
||||
self.app.LoadIgnoreList()
|
||||
|
||||
def loadResults(self):
|
||||
self.app.load()
|
||||
|
||||
def markAll(self):
|
||||
self.app.results.mark_all()
|
||||
|
||||
def markNone(self):
|
||||
self.app.results.mark_none()
|
||||
|
||||
def markInvert(self):
|
||||
self.app.results.mark_invert()
|
||||
|
||||
def purgeIgnoreList(self):
|
||||
self.app.PurgeIgnoreList()
|
||||
|
||||
def toggleSelectedMark(self):
|
||||
self.app.ToggleSelectedMarkState()
|
||||
|
||||
def saveIgnoreList(self):
|
||||
self.app.SaveIgnoreList()
|
||||
|
||||
def saveResults(self):
|
||||
self.app.Save()
|
||||
|
||||
def refreshDetailsWithSelected(self):
|
||||
self.app.RefreshDetailsWithSelected()
|
||||
|
||||
def selectResultNodePaths_(self,node_paths):
|
||||
self.app.SelectResultNodePaths(node_paths)
|
||||
|
||||
def selectPowerMarkerNodePaths_(self,node_paths):
|
||||
self.app.SelectPowerMarkerNodePaths(node_paths)
|
||||
|
||||
#---Actions
|
||||
def addSelectedToIgnoreList(self):
|
||||
self.app.AddSelectedToIgnoreList()
|
||||
|
||||
def deleteMarked(self):
|
||||
self.app.delete_marked()
|
||||
|
||||
def applyFilter_(self, filter):
|
||||
self.app.ApplyFilter(filter)
|
||||
|
||||
def makeSelectedReference(self):
|
||||
self.app.MakeSelectedReference()
|
||||
|
||||
def copyOrMove_markedTo_recreatePath_(self,copy,destination,recreate_path):
|
||||
self.app.copy_or_move_marked(copy, destination, recreate_path)
|
||||
|
||||
def openSelected(self):
|
||||
self.app.OpenSelected()
|
||||
|
||||
def removeMarked(self):
|
||||
self.app.results.perform_on_marked(lambda x:True,True)
|
||||
|
||||
def removeSelected(self):
|
||||
self.app.RemoveSelected()
|
||||
|
||||
def renameSelected_(self,newname):
|
||||
return self.app.RenameSelected(newname)
|
||||
|
||||
def revealSelected(self):
|
||||
self.app.RevealSelected()
|
||||
|
||||
#---Misc
|
||||
def sortDupesBy_ascending_(self,key,asc):
|
||||
self.app.sort_dupes(key,asc)
|
||||
|
||||
def sortGroupsBy_ascending_(self,key,asc):
|
||||
self.app.sort_groups(key,asc)
|
||||
|
||||
#---Information
|
||||
def getIgnoreListCount(self):
|
||||
return len(self.app.scanner.ignore_list)
|
||||
|
||||
def getMarkCount(self):
|
||||
return self.app.results.mark_count
|
||||
|
||||
def getStatLine(self):
|
||||
return self.app.stat_line
|
||||
|
||||
def getOperationalErrorCount(self):
|
||||
return self.app.last_op_error_count
|
||||
|
||||
def getSelectedDupePath(self):
|
||||
return unicode(self.app.selected_dupe_path())
|
||||
|
||||
def getSelectedDupeRefPath(self):
|
||||
return unicode(self.app.selected_dupe_ref_path())
|
||||
|
||||
#---Data
|
||||
@objc.signature('i@:i')
|
||||
def getOutlineViewMaxLevel_(self, tag):
|
||||
return self.app.GetOutlineViewMaxLevel(tag)
|
||||
|
||||
@objc.signature('@@:i@')
|
||||
def getOutlineView_childCountsForPath_(self, tag, node_path):
|
||||
return self.app.GetOutlineViewChildCounts(tag, node_path)
|
||||
|
||||
def getOutlineView_valuesForIndexes_(self,tag,node_path):
|
||||
return self.app.GetOutlineViewValues(tag,node_path)
|
||||
|
||||
def getOutlineView_markedAtIndexes_(self,tag,node_path):
|
||||
return self.app.GetOutlineViewMarked(tag,node_path)
|
||||
|
||||
def getTableViewCount_(self,tag):
|
||||
return self.app.GetTableViewCount(tag)
|
||||
|
||||
def getTableViewMarkedIndexes_(self,tag):
|
||||
return self.app.GetTableViewMarkedIndexes(tag)
|
||||
|
||||
def getTableView_valuesForRow_(self,tag,row):
|
||||
return self.app.GetTableViewValues(tag,row)
|
||||
|
||||
#---Properties
|
||||
def setMatchScaled_(self,match_scaled):
|
||||
self.app.scanner.match_factory.match_scaled = match_scaled
|
||||
|
||||
def setMinMatchPercentage_(self,percentage):
|
||||
self.app.scanner.match_factory.threshold = int(percentage)
|
||||
|
||||
def setMixFileKind_(self,mix_file_kind):
|
||||
self.app.scanner.mix_file_kind = mix_file_kind
|
||||
|
||||
def setDisplayDeltaValues_(self,display_delta_values):
|
||||
self.app.display_delta_values= display_delta_values
|
||||
|
||||
def setEscapeFilterRegexp_(self, escape_filter_regexp):
|
||||
self.app.options['escape_filter_regexp'] = escape_filter_regexp
|
||||
|
||||
def setRemoveEmptyFolders_(self, remove_empty_folders):
|
||||
self.app.options['clean_empty_dirs'] = remove_empty_folders
|
||||
|
||||
#---Worker
|
||||
def getJobProgress(self):
|
||||
return self.app.progress.last_progress
|
||||
|
||||
def getJobDesc(self):
|
||||
return self.app.progress.last_desc
|
||||
|
||||
def cancelJob(self):
|
||||
self.app.progress.job_cancelled = True
|
||||
|
||||
#---Registration
|
||||
@objc.signature('i@:')
|
||||
def isRegistered(self):
|
||||
return self.app.registered
|
||||
|
||||
@objc.signature('i@:@@')
|
||||
def isCodeValid_withEmail_(self, code, email):
|
||||
return self.app.is_code_valid(code, email)
|
||||
|
||||
def setRegisteredCode_andEmail_(self, code, email):
|
||||
self.app.set_registration(code, email)
|
||||
|
||||
11
pe/cocoa/py/setup.py
Normal file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
from setuptools import setup
|
||||
|
||||
from hs.build import set_buildenv
|
||||
|
||||
set_buildenv()
|
||||
|
||||
setup(
|
||||
plugin=['dg_cocoa.py'],
|
||||
setup_requires=['py2app'],
|
||||
)
|
||||
75
pe/cocoa/w3/dg.xsl
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="1.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
|
||||
<xsl:output
|
||||
method="xml"
|
||||
encoding="utf-8"
|
||||
doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
|
||||
indent="yes"/>
|
||||
|
||||
<xsl:template match="column">
|
||||
<xsl:if test="@enabled = 'y'">
|
||||
<th>
|
||||
<xsl:value-of select="@display"/>
|
||||
</th>
|
||||
</xsl:if>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="file">
|
||||
<tr>
|
||||
<xsl:variable name="td_class">
|
||||
<xsl:if test="position() > 1">
|
||||
<xsl:text>indented</xsl:text>
|
||||
</xsl:if>
|
||||
</xsl:variable>
|
||||
<xsl:variable name="file_node" select="."/>
|
||||
<xsl:for-each select="data">
|
||||
<xsl:variable name="data_pos" select="position()"/>
|
||||
<xsl:if test="document('columns.xml')/columns/column[$data_pos]/@enabled = 'y'">
|
||||
<td>
|
||||
<xsl:if test="position() = 1">
|
||||
<xsl:attribute name="class">
|
||||
<xsl:value-of select="$td_class"/>
|
||||
</xsl:attribute>
|
||||
</xsl:if>
|
||||
<xsl:value-of select="@value"/>
|
||||
</td>
|
||||
</xsl:if>
|
||||
</xsl:for-each>
|
||||
<!-- <xsl:for-each select="//results/column">
|
||||
<td>
|
||||
<xsl:variable name="attr_name">
|
||||
<xsl:text>attr_</xsl:text>
|
||||
<xsl:value-of select="@name"/>
|
||||
</xsl:variable>
|
||||
<xsl:value-of select="$file_node/@*[local-name(.) = $attr_name]"/>
|
||||
</td>
|
||||
</xsl:for-each> -->
|
||||
</tr>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="group">
|
||||
<xsl:apply-templates select="file"/>
|
||||
</xsl:template>
|
||||
|
||||
<xsl:template match="results">
|
||||
<html>
|
||||
<head>
|
||||
<title>dupeGuru Results</title>
|
||||
<link rel="stylesheet" href="hardcoded.css" type="text/css"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>dupeGuru Results</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<xsl:apply-templates select="document('columns.xml')/columns/column"/>
|
||||
</tr>
|
||||
<xsl:apply-templates select="group"/>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
|
||||
</xsl:stylesheet>
|
||||
71
pe/cocoa/w3/hardcoded.css
Normal file
@@ -0,0 +1,71 @@
|
||||
BODY
|
||||
{
|
||||
background-color:white;
|
||||
}
|
||||
|
||||
BODY,A,P,UL,TABLE,TR,TD
|
||||
{
|
||||
font-family:Tahoma,Arial,sans-serif;
|
||||
font-size:10pt;
|
||||
color: #4477AA;
|
||||
}
|
||||
|
||||
TABLE
|
||||
{
|
||||
background-color: #225588;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
TR
|
||||
{
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
TH
|
||||
{
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
background-color: #C8D6E5;
|
||||
}
|
||||
|
||||
TH TD
|
||||
{
|
||||
color:black;
|
||||
}
|
||||
|
||||
TD
|
||||
{
|
||||
padding-left: 2pt;
|
||||
}
|
||||
|
||||
TD.rightelem
|
||||
{
|
||||
text-align:right;
|
||||
/*padding-left:0pt;*/
|
||||
padding-right: 2pt;
|
||||
width: 17%;
|
||||
}
|
||||
|
||||
TD.indented
|
||||
{
|
||||
padding-left: 12pt;
|
||||
}
|
||||
|
||||
H1
|
||||
{
|
||||
font-family:"Courier New",monospace;
|
||||
color:#6699CC;
|
||||
font-size:18pt;
|
||||
color:#6da500;
|
||||
border-color: #70A0CF;
|
||||
border-width: 1pt;
|
||||
border-style: solid;
|
||||
margin-top: 16pt;
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding-top: 2pt;
|
||||
padding-bottom:2pt;
|
||||
text-align: center;
|
||||
}
|
||||
174
pe/help/changelog.yaml
Normal file
@@ -0,0 +1,174 @@
|
||||
- date: 2009-05-27
|
||||
version: 1.7.2
|
||||
description: |
|
||||
* Fixed a bug causing '.jpeg' files not to be scanned.
|
||||
* Fixed a bug causing a GUI freeze at the beginning of a scan with a lot of files.
|
||||
* Fixed a bug that sometimes caused a crash when an action was cancelled, and then started again.
|
||||
* Improved scanning speed.
|
||||
- date: 2009-05-26
|
||||
version: 1.7.1
|
||||
description: |
|
||||
* Fixed a bug causing the "Match Scaled" preference to be inverted.
|
||||
- date: 2009-05-20
|
||||
version: 1.7.0
|
||||
description: |
|
||||
* Fixed the bug from 1.6.0 preventing PowerPC macs from running the application.
|
||||
* Converted the Windows GUI to Qt, thus enabling multiprocessing and making the scanning process
|
||||
faster.
|
||||
- date: 2009-03-24
|
||||
description: "* **Improved** scanning speed, mainly on OS X where all cores of the\
|
||||
\ CPU are now used.\r\n* **Fixed** an occasional crash caused by permission issues.\r\
|
||||
\n* **Fixed** a bug where the \"X discarded\" notice would show a too large number\
|
||||
\ of discarded duplicates."
|
||||
version: 1.6.0
|
||||
- date: 2008-09-10
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> a notice in the status bar when\
|
||||
\ matches were discarded during the scan.</li>\n\t\t\t\t\t\t<li><b>Improved</b>\
|
||||
\ duplicate prioritization (smartly chooses which file you will keep).</li>\n\t\
|
||||
\t\t\t\t\t<li><b>Improved</b> scan progress feedback.</li>\n\t\t\t\t\t\t<li><b>Improved</b>\
|
||||
\ responsiveness of the user interface for certain actions.</li>\n\t\t \
|
||||
\ </ul>"
|
||||
version: 1.5.0
|
||||
- date: 2008-07-28
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Improved</b> iPhoto compatibility on Mac\
|
||||
\ OS X.</li>\n\t\t\t\t\t\t<li><b>Improved</b> the speed of results loading and\
|
||||
\ saving.</li>\n\t\t\t\t\t\t<li><b>Fixed</b> a crash sometimes occurring during\
|
||||
\ duplicate deletion.</li>\n\t\t </ul>"
|
||||
version: 1.4.2
|
||||
- date: 2008-04-12
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Improved</b> iPhoto Library loading feedback\
|
||||
\ on Mac OS X.</li>\n\t\t\t\t\t\t<li><b>Fixed</b> the directory selection dialog.\
|
||||
\ Bundles can be selected again on Mac OS X.</li>\n\t\t\t\t\t\t<li><b>Fixed</b>\
|
||||
\ \"Clear Ignore List\" crash in Windows.</li>\n\t\t </ul>"
|
||||
version: 1.4.1
|
||||
- date: 2008-02-20
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> iPhoto Library support on Mac OS\
|
||||
\ X.</li>\n\t\t\t\t\t\t<li><b>Fixed</b> occasional crashes when scanning corrupted\
|
||||
\ pictures.</li>\n\t\t </ul>"
|
||||
version: 1.4.0
|
||||
- date: 2008-02-20
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> iPhoto Library support on Mac OS\
|
||||
\ X.</li>\n\t\t\t\t\t\t<li><b>Fixed</b> occasional crashes when scanning corrupted\
|
||||
\ pictures.</li>\n\t\t </ul>"
|
||||
version: 1.4.0
|
||||
- date: 2008-01-12
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Improved</b> scan, delete and move speed\
|
||||
\ in situations where there were a lot of duplicates.</li>\n\t\t\t\t\t\t<li><b>Fixed</b>\
|
||||
\ occasional crashes when moving a lot of files at once.</li>\n\t\t\t\t\t\t<li><b>Fixed</b>\
|
||||
\ an issue sometimes preventing the application from starting at all.</li>\n\t\
|
||||
\t </ul>"
|
||||
version: 1.3.4
|
||||
- date: 2007-12-03
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Improved</b> the handling of low memory situations.</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Improved</b> the directory panel. The \"Remove\" button changes\
|
||||
\ to \"Put Back\" when an excluded directory is selected.</li>\n\t\t\t\t\t\t<li><b>Fixed</b>\
|
||||
\ the directory selection dialog. iPhoto '08 library files can now be selected.</li>\n\
|
||||
\t\t </ul>"
|
||||
version: 1.3.3
|
||||
- date: 2007-11-24
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> the \"Remove empty folders\" option.</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Fixed</b> results load/save issues.</li>\n\t\t\t\t\t\t<li><b>Fixed</b>\
|
||||
\ occasional status bar inaccuracies when the results are filtered.</li>\n\t\t\
|
||||
\ </ul>"
|
||||
version: 1.3.2
|
||||
- date: 2007-10-21
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Improved</b> results loading speed.</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Improved</b> details panel's picture loading (made it asynchronous).</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Fixed</b> a bug where the stats line at the bottom would sometimes\
|
||||
\ go confused while having a filter active.</li>\n\t\t\t\t\t\t<li><b>Fixed</b>\
|
||||
\ a bug under Windows where some duplicate markings would be lost.</li>\n\t\t\
|
||||
\ </ul>"
|
||||
version: 1.3.1
|
||||
- date: 2007-09-22
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> post scan filtering.</li>\n\t\t\
|
||||
\t\t\t\t<li><b>Fixed</b> issues with the rename feature under Windows</li>\n\t\
|
||||
\t\t\t\t\t<li><b>Fixed</b> some user interface annoyances under Windows</li>\n\
|
||||
\t\t </ul>"
|
||||
version: 1.3.0
|
||||
- date: 2007-05-19
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Improved</b> UI responsiveness (using threads)\
|
||||
\ under Mac OS X.</li>\n\t\t\t\t\t\t<li><b>Improved</b> result load/save speed\
|
||||
\ and memory usage.</li>\n\t\t </ul>"
|
||||
version: 1.2.1
|
||||
- date: 2007-03-17
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Changed</b> the picture decoding libraries\
|
||||
\ for both Mac OS X and Windows. The Mac OS X version uses the Core Graphics library\
|
||||
\ and the Windows version uses the .NET framework imaging capabilities. This results\
|
||||
\ in much faster scans. As a bonus, the Mac OS X version of dupeGuru PE now supports\
|
||||
\ RAW images.</li>\n\t\t </ul>"
|
||||
version: 1.2.0
|
||||
- date: 2007-02-11
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> Re-orderable columns. In fact,\
|
||||
\ I re-added the feature which was lost in the C# conversion in 2.4.0 (Windows).</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Fixed</b> a bug with all the Delete/Move/Copy actions with\
|
||||
\ certain kinds of files.</li>\n\t\t </ul>"
|
||||
version: 1.1.6
|
||||
- date: 2007-01-11
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Fixed</b> a bug with the Move action.</li>\n\
|
||||
\t\t </ul>"
|
||||
version: 1.1.5
|
||||
- date: 2007-01-09
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Fixed</b> a \"ghosting\" bug. Dupes deleted\
|
||||
\ by dupeGuru would sometimes come back in subsequent scans (Windows).</li>\n\t\
|
||||
\t\t\t\t\t<li><b>Fixed</b> bugs sometimes making dupeGuru crash when marking a\
|
||||
\ dupe (Windows).</li>\n\t\t\t\t\t\t<li><b>Fixed</b> some minor visual glitches\
|
||||
\ (Windows).</li>\n\t\t </ul>"
|
||||
version: 1.1.4
|
||||
- date: 2006-12-23
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Improved</b> the caching system. This makes\
|
||||
\ duplicate scans significantly faster.</li>\n\t\t\t\t\t\t<li><b>Improved</b>\
|
||||
\ the rename file dialog to exclude the extension from the original selection\
|
||||
\ (so when you start typing your new filename, it doesn't overwrite it) (Windows).</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Changed</b> some menu key shortcuts that created conflicts\
|
||||
\ (Windows).</li>\n\t\t\t\t\t\t<li><b>Fixed</b> a bug preventing files from \"\
|
||||
reference\" directories to be displayed in blue in the results (Windows).</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Fixed</b> a bug preventing some files to be sent to the recycle\
|
||||
\ bin (Windows).</li>\n\t\t\t\t\t\t<li><b>Fixed</b> a bug with the \"Remove\"\
|
||||
\ button of the directories panel (Windows).</li>\n\t\t\t\t\t\t<li><b>Fixed</b>\
|
||||
\ a bug in the packaging preventing certain Windows configurations to start dupeGuru\
|
||||
\ at all.</li>\n\t\t </ul>"
|
||||
version: 1.1.3
|
||||
- date: 2006-11-18
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Fixed</b> a bug with directory states.</li>\n\
|
||||
\t\t </ul>"
|
||||
version: 1.1.2
|
||||
- date: 2006-11-17
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Fixed</b> a bug causing the ignore list not\
|
||||
\ to be saved.</li>\n\t\t\t\t\t\t<li><b>Fixed</b> a bug with selection under Power\
|
||||
\ Marker mode.</li>\n\t\t </ul>"
|
||||
version: 1.1.1
|
||||
- date: 2006-11-15
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Changed</b> the Windows interface. It is\
|
||||
\ now .NET based.</li>\n\t\t\t\t\t\t<li><b>Added</b> an auto-update feature to\
|
||||
\ the windows version.</li>\n\t\t\t\t\t\t<li><b>Changed</b> the way power marking\
|
||||
\ works. It is now a mode instead of a separate window.</li>\n\t\t\t\t\t\t<li><b>Changed</b>\
|
||||
\ the \"Size (MB)\" column for a \"Size (KB)\" column. The values are now \"ceiled\"\
|
||||
\ instead of rounded. Therefore, a size \"0\" is now really 0 bytes, not just\
|
||||
\ a value too small to be rounded up. It is also the case for delta values.</li>\n\
|
||||
\t\t\t\t\t\t<li><b>Fixed</b> a bug sometimes making delete and move operations\
|
||||
\ stall.</li>\n\t\t </ul>"
|
||||
version: 1.1.0
|
||||
- date: 2006-10-12
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> an auto-update feature in the Mac\
|
||||
\ OS X version (with Sparkle).</li>\n\t\t \t<li><b>Fixed</b> a bug\
|
||||
\ sometimes causing inaccuracies of the Match %.</li>\n\t\t </ul>"
|
||||
version: 1.0.5
|
||||
- date: 2006-09-21
|
||||
description: "<ul>\n\t\t \t<li><b>Fixed</b> a bug with the cache system.</li>\n\
|
||||
\t\t </ul>"
|
||||
version: 1.0.4
|
||||
- date: 2006-09-15
|
||||
description: "<ul>\n\t\t\t\t\t\t<li><b>Added</b> the ability to search for scaled\
|
||||
\ duplicates.</li>\n\t\t\t\t\t\t<li><b>Added</b> a cache system for faster scans.</li>\n\
|
||||
\t\t \t<li><b>Improved</b> speed of the scanning engine.</li>\n\t\t\
|
||||
\ </ul>"
|
||||
version: 1.0.3
|
||||
- date: 2006-09-11
|
||||
description: "<ul>\n\t\t \t<li><b>Improved</b> speed of the scanning\
|
||||
\ engine.</li>\n\t\t\t\t\t\t<li><b>Improved</b> the display of pictures in the\
|
||||
\ details panel (Windows).</li>\n\t\t </ul>"
|
||||
version: 1.0.2
|
||||
- date: 2006-09-08
|
||||
description: "<ul>\n\t\t \t<li>Initial release.</li>\n\t\t \
|
||||
\ </ul>"
|
||||
version: 1.0.0
|
||||
12
pe/help/gen.py
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name:
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-24
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
import os
|
||||
|
||||
from web import generate_help
|
||||
|
||||
generate_help.main('.', 'dupeguru_pe_help', force_render=True)
|
||||
409
pe/help/skeleton/hardcoded.css
Normal file
@@ -0,0 +1,409 @@
|
||||
/*****************************************************
|
||||
General settings
|
||||
*****************************************************/
|
||||
|
||||
BODY
|
||||
{
|
||||
background-color:white;
|
||||
}
|
||||
|
||||
BODY,A,P,UL,TABLE,TR,TD
|
||||
{
|
||||
font-family:Tahoma,Arial,sans-serif;
|
||||
font-size:10pt;
|
||||
color: #4477AA;/*darker than 5588bb for the sake of the eyes*/
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
"A" settings
|
||||
*****************************************************/
|
||||
|
||||
A
|
||||
{
|
||||
color: #ae322b;
|
||||
text-decoration:underline;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
A.glossaryword {color:#A0A0A0;}
|
||||
|
||||
A.noline
|
||||
{
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************
|
||||
Menu and mainframe settings
|
||||
*****************************************************/
|
||||
|
||||
.maincontainer
|
||||
{
|
||||
display:block;
|
||||
margin-left:7%;
|
||||
margin-right:7%;
|
||||
padding-left:5px;
|
||||
padding-right:0px;
|
||||
border-color:#CCCCCC;
|
||||
border-style:solid;
|
||||
border-width:2px;
|
||||
border-right-width:0px;
|
||||
border-bottom-width:0px;
|
||||
border-top-color:#ae322b;
|
||||
vertical-align:top;
|
||||
}
|
||||
|
||||
TD.menuframe
|
||||
{
|
||||
width:30%;
|
||||
}
|
||||
|
||||
.menu
|
||||
{
|
||||
margin:4px 4px 4px 4px;
|
||||
margin-top: 16pt;
|
||||
border-color:gray;
|
||||
border-width:1px;
|
||||
border-style:dotted;
|
||||
padding-top:10pt;
|
||||
padding-bottom:10pt;
|
||||
padding-right:6pt;
|
||||
}
|
||||
|
||||
.submenu
|
||||
{
|
||||
list-style-type: none;
|
||||
margin-left:26pt;
|
||||
margin-top:0pt;
|
||||
margin-bottom:0pt;
|
||||
padding-left:0pt;
|
||||
}
|
||||
|
||||
A.menuitem,A.menuitem_selected
|
||||
{
|
||||
font-size:14pt;
|
||||
font-family:Tahoma,Arial,sans-serif;
|
||||
font-weight:normal;
|
||||
padding-left:10pt;
|
||||
color:#5588bb;
|
||||
margin-right:2pt;
|
||||
margin-left:4pt;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
A.menuitem_selected
|
||||
{
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
A.submenuitem
|
||||
{
|
||||
font-family:Tahoma,Arial,sans-serif;
|
||||
font-weight:normal;
|
||||
color:#5588bb;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.titleline
|
||||
{
|
||||
border-width:3px;
|
||||
border-style:solid;
|
||||
border-left-width:0px;
|
||||
border-right-width:0px;
|
||||
border-top-width:0px;
|
||||
border-color:#CCCCCC;
|
||||
margin-left:28pt;
|
||||
margin-right:2pt;
|
||||
line-height:1px;
|
||||
padding-top:0px;
|
||||
margin-top:0px;
|
||||
display:block;
|
||||
}
|
||||
|
||||
.titledescrip
|
||||
{
|
||||
text-align:left;
|
||||
display:block;
|
||||
margin-left:26pt;
|
||||
color:#ae322b;
|
||||
}
|
||||
|
||||
.mainlogo
|
||||
{
|
||||
display:block;
|
||||
margin-left:8%;
|
||||
margin-top:4pt;
|
||||
margin-bottom:4pt;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
IMG settings
|
||||
*****************************************************/
|
||||
|
||||
IMG
|
||||
{
|
||||
border-style:none;
|
||||
}
|
||||
|
||||
IMG.smallbutton
|
||||
{
|
||||
margin-right: 20px;
|
||||
float:none;
|
||||
}
|
||||
|
||||
IMG.floating
|
||||
{
|
||||
float:left;
|
||||
margin-right: 4pt;
|
||||
margin-bottom: 4pt;
|
||||
}
|
||||
|
||||
IMG.lefticon
|
||||
{
|
||||
vertical-align: middle;
|
||||
padding-right: 2pt;
|
||||
}
|
||||
|
||||
IMG.righticon
|
||||
{
|
||||
vertical-align: middle;
|
||||
padding-left: 2pt;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
TABLE settings
|
||||
*****************************************************/
|
||||
|
||||
TABLE
|
||||
{
|
||||
border-style:none;
|
||||
}
|
||||
|
||||
TABLE.box
|
||||
{
|
||||
width: 90%;
|
||||
margin-left:5%;
|
||||
}
|
||||
|
||||
TABLE.centered
|
||||
{
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
TABLE.hardcoded
|
||||
{
|
||||
background-color: #225588;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
TR { background-color: transparent; }
|
||||
|
||||
TABLE.hardcoded TR { background-color: white }
|
||||
|
||||
TABLE.hardcoded TR.header
|
||||
{
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
background-color: #C8D6E5;
|
||||
}
|
||||
|
||||
TABLE.hardcoded TR.header TD {color:black;}
|
||||
|
||||
TABLE.hardcoded TD { padding-left: 2pt; }
|
||||
|
||||
TD.minimelem {
|
||||
padding-right:0px;
|
||||
padding-left:0px;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
TD.rightelem
|
||||
{
|
||||
text-align:right;
|
||||
/*padding-left:0pt;*/
|
||||
padding-right: 2pt;
|
||||
width: 17%;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
P settings
|
||||
*****************************************************/
|
||||
|
||||
p,.sub{text-align:justify;}
|
||||
.centered{text-align:center;}
|
||||
.sub
|
||||
{
|
||||
padding-left: 16pt;
|
||||
padding-right:16pt;
|
||||
}
|
||||
|
||||
.Note, .ContactInfo
|
||||
{
|
||||
border-color: #ae322b;
|
||||
border-width: 1pt;
|
||||
border-style: dashed;
|
||||
text-align:justify;
|
||||
padding: 2pt 2pt 2pt 2pt;
|
||||
margin-bottom:4pt;
|
||||
margin-top:8pt;
|
||||
list-style-position:inside;
|
||||
}
|
||||
|
||||
.ContactInfo
|
||||
{
|
||||
width:60%;
|
||||
margin-left:5%;
|
||||
}
|
||||
|
||||
.NewsItem
|
||||
{
|
||||
border-color:#ae322b;
|
||||
border-style: solid;
|
||||
border-right:none;
|
||||
border-top:none;
|
||||
border-left:none;
|
||||
border-bottom-width:1px;
|
||||
text-align:justify;
|
||||
padding-left:4pt;
|
||||
padding-right:4pt;
|
||||
padding-bottom:8pt;
|
||||
}
|
||||
|
||||
/*****************************************************
|
||||
Lists settings
|
||||
*****************************************************/
|
||||
UL.plain
|
||||
{
|
||||
list-style-type: none;
|
||||
padding-left:0px;
|
||||
margin-left:0px;
|
||||
}
|
||||
|
||||
LI.plain
|
||||
{
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
LI.section
|
||||
{
|
||||
padding-top: 6pt;
|
||||
}
|
||||
|
||||
UL.longtext LI
|
||||
{
|
||||
border-color: #ae322b;
|
||||
border-width:0px;
|
||||
border-top-width:1px;
|
||||
border-style:solid;
|
||||
margin-top:12px;
|
||||
}
|
||||
|
||||
/*
|
||||
with UL.longtext LI, there can be anything between
|
||||
the UL and the LI, and it will still make the
|
||||
lontext thing, I must break it with this hack
|
||||
*/
|
||||
UL.longtext UL LI
|
||||
{
|
||||
border-style:none;
|
||||
margin-top:2px;
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************
|
||||
Titles settings
|
||||
*****************************************************/
|
||||
|
||||
H1,H2,H3
|
||||
{
|
||||
font-family:"Courier New",monospace;
|
||||
color:#5588bb;
|
||||
}
|
||||
|
||||
H1
|
||||
{
|
||||
font-size:18pt;
|
||||
color: #ae322b;
|
||||
border-color: #70A0CF;
|
||||
border-width: 1pt;
|
||||
border-style: solid;
|
||||
margin-top: 16pt;
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
padding-top: 2pt;
|
||||
padding-bottom:2pt;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
H2
|
||||
{
|
||||
border-color: #ae322b;
|
||||
border-bottom-width: 2px;
|
||||
border-top-width: 0pt;
|
||||
border-left-width: 2px;
|
||||
border-right-width: 0pt;
|
||||
border-bottom-color: #cccccc;
|
||||
border-style: solid;
|
||||
margin-top: 16pt;
|
||||
margin-left: 0pt;
|
||||
margin-right: 0pt;
|
||||
padding-bottom:3pt;
|
||||
padding-left:5pt;
|
||||
text-align: left;
|
||||
font-size:16pt;
|
||||
}
|
||||
|
||||
H3
|
||||
{
|
||||
display:block;
|
||||
color:#ae322b;
|
||||
border-color: #70A0CF;
|
||||
border-bottom-width: 2px;
|
||||
border-top-width: 0pt;
|
||||
border-left-width: 0pt;
|
||||
border-right-width: 0pt;
|
||||
border-style: dashed;
|
||||
margin-top: 12pt;
|
||||
margin-left: 0pt;
|
||||
margin-bottom: 4pt;
|
||||
width:auto;
|
||||
padding-bottom:3pt;
|
||||
padding-right:2pt;
|
||||
padding-left:2pt;
|
||||
text-align: left;
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
|
||||
/*****************************************************
|
||||
Misc. classes
|
||||
*****************************************************/
|
||||
.longtext:first-letter {font-size: 150%}
|
||||
|
||||
.price, .loweredprice, .specialprice {font-weight:bold;}
|
||||
|
||||
.loweredprice {text-decoration:line-through}
|
||||
|
||||
.specialprice {color:red}
|
||||
|
||||
form
|
||||
{
|
||||
margin:0px;
|
||||
}
|
||||
|
||||
.program_summary
|
||||
{
|
||||
float:right;
|
||||
margin: 32pt;
|
||||
margin-top:0pt;
|
||||
margin-bottom:0pt;
|
||||
}
|
||||
|
||||
.screenshot
|
||||
{
|
||||
float:left;
|
||||
margin: 8pt;
|
||||
}
|
||||
BIN
pe/help/skeleton/images/hs_title.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
14
pe/help/templates/base_dg.mako
Normal file
@@ -0,0 +1,14 @@
|
||||
<%inherit file="/base_help.mako"/>
|
||||
${next.body()}
|
||||
|
||||
<%def name="menu()"><%
|
||||
self.menuitem('intro.htm', 'Introduction', 'Introduction to dupeGuru')
|
||||
self.menuitem('quick_start.htm', 'Quick Start', 'Quickly get into the action')
|
||||
self.menuitem('directories.htm', 'Directories', 'Managing dupeGuru directories')
|
||||
self.menuitem('preferences.htm', 'Preferences', 'Setting dupeGuru preferences')
|
||||
self.menuitem('results.htm', 'Results', 'Time to delete these duplicates!')
|
||||
self.menuitem('power_marker.htm', 'Power Marker', 'Take control of your duplicates')
|
||||
self.menuitem('faq.htm', 'F.A.Q.', 'Frequently Asked Questions')
|
||||
self.menuitem('versions.htm', 'Version History', 'Changes dupeGuru went through')
|
||||
self.menuitem('credits.htm', 'Credits', 'People who contributed to dupeGuru')
|
||||
%></%def>
|
||||
25
pe/help/templates/credits.mako
Normal file
@@ -0,0 +1,25 @@
|
||||
## -*- coding: utf-8 -*-
|
||||
<%!
|
||||
title = 'Credits'
|
||||
selected_menu_item = 'Credits'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
Below is the list of people who contributed, directly or indirectly to dupeGuru.
|
||||
|
||||
${self.credit('Virgil Dupras', 'Developer', "That's me, Hardcoded Software founder", 'www.hardcoded.net', 'hsoft@hardcoded.net')}
|
||||
|
||||
${self.credit(u'Jérôme Cantin', u'Icon designer', u"Icons in dupeGuru are from him")}
|
||||
|
||||
${self.credit('Python', 'Programming language', "The bestest of the bests", 'www.python.org')}
|
||||
|
||||
${self.credit('PyObjC', 'Python-to-Cocoa bridge', "Used for the Mac OS X version", 'pyobjc.sourceforge.net')}
|
||||
|
||||
${self.credit('PyQt', 'Python-to-Qt bridge', "Used for the Windows version", 'www.riverbankcomputing.co.uk')}
|
||||
|
||||
${self.credit('Qt', 'GUI Toolkit', "Used for the Windows version", 'www.qtsoftware.com')}
|
||||
|
||||
${self.credit('Sparkle', 'Auto-update library', "Used for the Mac OS X version", 'andymatuschak.org/pages/sparkle')}
|
||||
|
||||
${self.credit('Python Imaging Library', 'Picture analyzer', "Used for the Windows version", 'www.pythonware.com/products/pil/')}
|
||||
|
||||
${self.credit('You', 'dupeGuru user', "What would I do without you?")}
|
||||
24
pe/help/templates/directories.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
<%!
|
||||
title = 'Directories'
|
||||
selected_menu_item = 'Directories'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
|
||||
There is a panel in dupeGuru called **Directories**. You can open it by clicking on the **Directories** button. This directory contains the list of the directories that will be scanned when you click on **Start Scanning**.
|
||||
|
||||
This panel is quite straightforward to use. If you want to add a directory, click on **Add**. If you added directories before, a popup menu with a list of recent directories you added will pop. You can click on one of them to add it directly to your list. If you click on the first item of the popup menu, **Add New Directory...**, you will be prompted for a directory to add. If you never added a directory, no menu will pop and you will directly be prompted for a new directory to add.
|
||||
|
||||
To remove a directory, select the directory to remove and click on **Remove**. If a subdirectory is selected when you click remove, the selected directory will be set to **excluded** state (see below) instead of being removed.
|
||||
|
||||
Directory states
|
||||
-----
|
||||
|
||||
Every directory can be in one of these 3 states:
|
||||
|
||||
* **Normal:** Duplicates found in these directories can be deleted.
|
||||
* **Reference:** Duplicates found in this directory **cannot** be deleted. Files in reference directories will be in a blue color in the results.
|
||||
* **Excluded:** Files in this directory will not be included in the scan.
|
||||
|
||||
The default state of a directory is, of course, **Normal**. You can use **Reference** state for a directory if you want to be sure that you won't delete any file from it.
|
||||
|
||||
When you set the state of a directory, all subdirectories of this directory automatically inherit this state unless you explicitly set a subdirectory's state.
|
||||
64
pe/help/templates/faq.mako
Normal file
@@ -0,0 +1,64 @@
|
||||
<%!
|
||||
title = 'dupeGuru F.A.Q.'
|
||||
selected_menu_item = 'F.A.Q.'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
|
||||
<%text filter="md">
|
||||
### What is dupeGuru PE?
|
||||
|
||||
dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality.
|
||||
|
||||
### What makes it better than other duplicate scanners?
|
||||
|
||||
The scanning engine is extremely flexible. You can tweak it to really get the kind of results you want. You can read more about dupeGuru tweaking option at the [Preferences page](preferences.htm).
|
||||
|
||||
### How safe is it to use dupeGuru PE?
|
||||
|
||||
Very safe. dupeGuru has been designed to make sure you don't delete files you didn't mean to delete. First, there is the reference directory system that lets you define directories where you absolutely **don't** want dupeGuru to let you delete files there, and then there is the group reference system that makes sure that you will **always** keep at least one member of the duplicate group.
|
||||
|
||||
### What are the demo limitations of dupeGuru PE?
|
||||
|
||||
In demo mode, you can only perform actions (delete/copy/move) on 10 duplicates per session.
|
||||
|
||||
### The mark box of a file I want to delete is disabled. What must I do?
|
||||
|
||||
You cannot mark the reference (The first file) of a duplicate group. However, what you can do is to promote a duplicate file to reference. Thus, if a file you want to mark is reference, select a duplicate file in the group that you want to promote to reference, and click on **Actions-->Make Selected Reference**. If the reference file is from a reference directory (filename written in blue letters), you cannot remove it from the reference position.
|
||||
|
||||
### I have a directory from which I really don't want to delete files.
|
||||
|
||||
If you want to be sure that dupeGuru will never delete file from a particular directory, just open the **Directories panel**, select that directory, and set its state to **Reference**.
|
||||
|
||||
### What is this '(X discarded)' notice in the status bar?
|
||||
|
||||
In some cases, some matches are not included in the final results for security reasons. Let me use an example. We have 3 file: A, B and C. We scan them using a low filter hardness. The scanner determines that A matches with B, A matches with C, but B does **not** match with C. Here, dupeGuru has kind of a problem. It cannot create a duplicate group with A, B and C in it because not all files in the group would match together. It could create 2 groups: one A-B group and then one A-C group, but it will not, for security reasons. Lets think about it: If B doesn't match with C, it probably means that either B, C or both are not actually duplicates. If there would be 2 groups (A-B and A-C), you would end up delete both B and C. And if one of them is not a duplicate, that is really not what you want to do, right? So what dupeGuru does in a case like this is to discard the A-C match (and adds a notice in the status bar). Thus, if you delete B and re-run a scan, you will have a A-C match in your next results.
|
||||
|
||||
### I want to mark all files from a specific directory. What can I do?
|
||||
|
||||
Enable the [Power Marker](power_marker.htm) mode and click on the Directory column to sort your duplicates by Directory. It will then be easy for you to select all duplicates from the same directory, and then press Space to mark all selected duplicates.
|
||||
|
||||
### I want to remove all files that are more than 300 KB away from their reference file. What can I do?
|
||||
|
||||
* Enable the [Power Marker](power_marker.htm) mode.
|
||||
* Enable the **Delta Values** mode.
|
||||
* Click on the "Size" column to sort the results by size.
|
||||
* Select all duplicates below -300.
|
||||
* Click on **Remove Selected from Results**.
|
||||
* Select all duplicates over 300.
|
||||
* Click on **Remove Selected from Results**.
|
||||
|
||||
### I want to make my latest modified files reference files. What can I do?
|
||||
|
||||
* Enable the [Power Marker](power_marker.htm) mode.
|
||||
* Enable the **Delta Values** mode.
|
||||
* Click on the "Modification" column to sort the results by modification date.
|
||||
* Click on the "Modification" column again to reverse the sort order (see Power Marker page to know why).
|
||||
* Select all duplicates over 0.
|
||||
* Click on **Make Selected Reference**.
|
||||
|
||||
### I want to mark all duplicates containing the word "copy". How do I do that?
|
||||
|
||||
* **Windows**: Click on **Actions --> Apply Filter**, then type "copy", then click OK.
|
||||
* **Mac OS X**: Type "copy" in the "Filter" field in the toolbar.
|
||||
* Click on **Mark --> Mark All**.
|
||||
</%text>
|
||||
13
pe/help/templates/intro.mako
Normal file
@@ -0,0 +1,13 @@
|
||||
<%!
|
||||
title = 'Introduction to dupeGuru PE'
|
||||
selected_menu_item = 'introduction'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
|
||||
dupeGuru Picture Edition (PE for short) is a tool to find duplicate pictures on your computer. Not only can it find exact matches, but it can also find duplicates among pictures of different kind (PNG, JPG, GIF etc..) and quality.
|
||||
|
||||
Although dupeGuru can easily be used without documentation, reading this file will help you to master it. If you are looking for guidance for your first duplicate scan, you can take a look at the [Quick Start](quick_start.htm) section.
|
||||
|
||||
It is a good idea to keep dupeGuru PE updated. You can download the latest version on the [dupeGuru PE homepage](http://www.hardcoded.net/dupeguru_pe/).
|
||||
|
||||
<%def name="meta()"><meta name="AppleTitle" content="dupeGuru PE Help"></meta></%def>
|
||||
33
pe/help/templates/power_marker.mako
Normal file
@@ -0,0 +1,33 @@
|
||||
<%!
|
||||
title = 'Power Marker'
|
||||
selected_menu_item = 'Power Marker'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
|
||||
You will probably not use the Power Marker feature very often, but if you get into a situation where you need it, you will be pretty happy that this feature exists.
|
||||
|
||||
What is it?
|
||||
-----
|
||||
|
||||
When the Power Marker mode is enabled, the duplicates are shown without their respective reference file. You can select, mark and sort this list, just like in normal mode.
|
||||
|
||||
So, what is it for?
|
||||
-----
|
||||
|
||||
The dupeGuru results, when in normal mode, are sorted according to duplicate groups' **reference file**. This means that if you want, for example, to mark all duplicates with the "exe" extension, you cannot just sort the results by "Kind" to have all exe duplicates together because a group can be composed of more than one kind of files. That is where Power Marker comes into play. To mark all your "exe" duplicates, you just have to:
|
||||
|
||||
* Enable the Power marker mode.
|
||||
* Add the "Kind" column with the "Columns" menu.
|
||||
* Click on that "Kind" column to sort the list by kind.
|
||||
* Locate the first duplicate with a "exe" kind.
|
||||
* Select it.
|
||||
* Scroll down the list to locate the last duplicate with a "exe" kind.
|
||||
* Hold Shift and click on it.
|
||||
* Press Space to mark all selected duplicates.
|
||||
|
||||
Power Marker and delta values
|
||||
-----
|
||||
|
||||
The Power Marker unveil its true power when you use it with the **Delta Values** switch turned on. When you turn it on, relative values will be displayed instead of absolute ones. So if, for example, you want to remove from your results all duplicates that are more than 300 KB away from their reference, you could sort the Power Marker by Size, select all duplicates under -300 in the Size column, delete them, and then do the same for duplicates over 300 at the bottom of the list.
|
||||
|
||||
You could also use it to change the reference priority of your duplicate list. When you make a fresh scan, if there are no reference directories, the reference file of every group is the biggest file. If you want to change that, for example, to the latest modification time, you can sort the Power Marker by modification time in **descending** order, select all duplicates with a modification time delta value higher than 0 and click on **Make Selected Reference**. The reason why you must make the sort order descending is because if 2 files among the same duplicate group are selected when you click on **Make Selected Reference**, only the first of the list will be made reference, the other will be ignored. And since you want the last modified file to be reference, having the sort order descending assures you that the first item of the list will be the last modified.
|
||||
23
pe/help/templates/preferences.mako
Normal file
@@ -0,0 +1,23 @@
|
||||
<%!
|
||||
title = 'Preferences'
|
||||
selected_menu_item = 'Preferences'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
|
||||
**Filter Hardness:** The higher is this setting, the "harder" is the filter (In other words, the less results you get). Most pictures of the same quality match at 100% even if the format is different (PNG and JPG for example.). However, if you want to make a PNG match with a lower quality JPG, you will have to set the filer hardness to lower than 100. The default, 95, is a sweet spot.
|
||||
|
||||
**Match scaled pictures together:** If you check this box, pictures of different dimensions will be allowed in the same duplicate group.
|
||||
|
||||
**Can mix file kind:** If you check this box, duplicate groups are allowed to have files with different extensions. If you don't check it, well, they aren't!
|
||||
|
||||
**Use regular expressions when filtering:** If you check this box, the filtering feature will treat your filter query as a **regular expression**. Explaining them is beyond the scope of this document. A good place to start learning it is <http://www.regular-expressions.info>.
|
||||
|
||||
**Remove empty folders after delete or move:** When this option is enabled, folders are deleted after a file is deleted or moved and the folder is empty.
|
||||
|
||||
**Copy and Move:** Determines how the Copy and Move operations (in the Action menu) will behave.
|
||||
|
||||
* **Right in destination:** All files will be sent directly in the selected destination, without trying to recreate the source path at all.
|
||||
* **Recreate relative path:** The source file's path will be re-created in the destination directory up to the root selection in the Directories panel. For example, if you added "/Users/foobar/Picture" to your Directories panel and you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/2006/06" ("/Users/foobar/Picture" has been trimmed from source's path in the final destination.).
|
||||
* **Recreate absolute path:** The source file's path will be re-created in the destination directory in it's entirety. For example, if you move "/Users/foobar/Picture/2006/06/photo.jpg" to the destination "/Users/foobar/MyDestination", the final destination for the file will be "/Users/foobar/MyDestination/Users/foobar/Picture/2006/06".</li>
|
||||
|
||||
In all cases, dupeGuru PE nicely handles naming conflicts by prepending a number to the destination filename if the filename already exists in the destination.
|
||||
18
pe/help/templates/quick_start.mako
Normal file
@@ -0,0 +1,18 @@
|
||||
<%!
|
||||
title = 'Quick Start'
|
||||
selected_menu_item = 'Quick Start'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
|
||||
To get you quickly started with dupeGuru, let's just make a standard scan using default preferences.
|
||||
|
||||
* Click on **Directories**.
|
||||
* Click on **Add**.
|
||||
* Choose a directory you want to scan for duplicates.
|
||||
* Click on **Start Scanning**.
|
||||
* Wait until the scan process is over.
|
||||
* Look at every duplicate (The files that are indented) and verify that it is indeed a duplicate to the group's reference (The file above the duplicate that is not indented and have a disabled mark box).
|
||||
* If a file is a false duplicate, select it and click on **Actions-->Remove Selected from Results**.
|
||||
* Once you are sure that there is no false duplicate in your results, click on **Edit-->Mark All**, and then **Actions-->Send Marked to Recycle bin**.
|
||||
|
||||
That is only a basic scan. There are a lot of tweaking you can do to get different results and several methods of examining and modifying your results. To know about them, just read the rest of this help file.
|
||||
73
pe/help/templates/results.mako
Normal file
@@ -0,0 +1,73 @@
|
||||
<%!
|
||||
title = 'Results'
|
||||
selected_menu_item = 'Results'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
|
||||
When dupeGuru is finished scanning for duplicates, it will show its results in the form of duplicate group list.
|
||||
|
||||
About duplicate groups
|
||||
-----
|
||||
|
||||
A duplicate group is a group of files that all match together. Every group has a **reference file** and one or more **duplicate files**. The reference file is the first file of the group. Its mark box is disabled. Below it, and indented, are the duplicate files.
|
||||
|
||||
You can mark duplicate files, but you can never mark the reference file of a group. This is a security measure to prevent dupeGuru from deleting not only duplicate files, but their reference. You sure don't want that, do you?
|
||||
|
||||
What determines which files are reference and which files are duplicates is first their directory state. A files from a reference directory will always be reference in a duplicate group. If all files are from a normal directory, the size determine which file will be the reference of a duplicate group. dupeGuru assumes that you always want to keep the biggest file, so the biggest files will take the reference position.
|
||||
|
||||
You can change the reference file of a group manually. To do so, select the duplicate file you want to promote to reference, and click on **Actions-->Make Selected Reference**.
|
||||
|
||||
Reviewing results
|
||||
-----
|
||||
|
||||
Although you can just click on **Edit-->Mark All** and then **Actions-->Send Marked to Recycle bin** to quickly delete all duplicate files in your results, it is always recommended to review all duplicates before deleting them.
|
||||
|
||||
To help you reviewing the results, you can bring up the **Details panel**. This panel shows all the details of the currently selected file as well as its reference's details. This is very handy to quickly determine if a duplicate really is a duplicate. You can also double-click on a file to open it with its associated application.
|
||||
|
||||
If you have more false duplicates than true duplicates (If your filter hardness is very low), the best way to proceed would be to review duplicates, mark true duplicates and then click on **Actions-->Send Marked to Recycle bin**. If you have more true duplicates than false duplicates, you can instead mark all files that are false duplicates, and use **Actions-->Remove Marked from Results**.
|
||||
|
||||
Marking and Selecting
|
||||
-----
|
||||
|
||||
A **marked** duplicate is a duplicate with the little box next to it having a check-mark. A **selected** duplicate is a duplicate being highlighted. The multiple selection actions can be performed in dupeGuru in the standard way (Shift/Command/Control click). You can toggle all selected duplicates' mark state by pressing **space**.
|
||||
|
||||
Delta Values
|
||||
-----
|
||||
|
||||
If you turn this switch on, some columns will display the value relative to the duplicate's reference instead of the absolute values. These delta values will also be displayed in a different color so you can spot them easily. For example, if a duplicate is 1.2 MB and its reference is 1.4 MB, the Size column will display -0.2 MB. This option is a killer feature when combined with the [Power Marker](power_marker.htm).
|
||||
|
||||
Filtering
|
||||
-----
|
||||
|
||||
dupeGuru supports post-scan filtering. With it, you can narrow down your results so you can perform actions on a subset of it. For example, you could easily mark all duplicates with their filename containing "copy" from your results using the filter.
|
||||
|
||||
**Windows:** To use the filtering feature, click on Actions --> Apply Filter, write down the filter you want to apply and click OK. To go back to unfiltered results, click on Actions --> Cancel Filter.
|
||||
|
||||
**Mac OS X:** To use the filtering feature, type your filter in the "Filter" search field in the toolbar. To go back to unfiltered result, blank out the field, or click on the "X".
|
||||
|
||||
In simple mode (the default mode), whatever you type as the filter is the string used to perform the actual filtering, with the exception of one wildcard: **\***. Thus, if you type "[*]" as your filter, it will match anything with [] brackets in it, whatever is in between those brackets.
|
||||
|
||||
For more advanced filtering, you can turn "Use regular expressions when filtering" on. The filtering feature will then use **regular expressions**. A regular expression is a language for matching text. Explaining them is beyond the scope of this document. A good place to start learning it is <http://www.regular-expressions.info>.
|
||||
|
||||
Matches are case insensitive in both simple and regexp mode.
|
||||
|
||||
For the filter to match, your regular expression don't have to match the whole filename, it just have to contain a string matching the expression.
|
||||
|
||||
You might notice that not all duplicates in the filtered results will match your filter. That is because as soon as one single duplicate in a group matches the filter, the whole group stays in the results so you can have a better view of the duplicate's context. However, non-matching duplicates are in "reference mode". Therefore, you can perform actions like Mark All and be sure to only mark filtered duplicates.
|
||||
|
||||
Action Menu
|
||||
-----
|
||||
|
||||
* **Start Duplicate Scan:** Starts a new duplicate scan.
|
||||
* **Clear Ignore List:** Remove all ignored matches you added. You have to start a new scan for the newly cleared ignore list to be effective.
|
||||
* **Export Results to XHTML:** Take the current results, and create an XHTML file out of it. The columns that are visible when you click on this button will be the columns present in the XHTML file. The file will automatically be opened in your default browser.
|
||||
* **Send Marked to Trash:** Send all marked duplicates to trash, obviously.
|
||||
* **Move Marked to...:** Prompt you for a destination, and then move all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference.
|
||||
* **Copy Marked to...:** Prompt you for a destination, and then copy all marked files to that destination. Source file's path might be re-created in destination, depending on the "Copy and Move" preference.
|
||||
* **Remove Marked from Results:** Remove all marked duplicates from results. The actual files will not be touched and will stay where they are.
|
||||
* **Remove Selected from Results:** Remove all selected duplicates from results. Note that all selected reference files will be ignored, only duplicates can be removed with this action.
|
||||
* **Make Selected Reference:** Promote all selected duplicates to reference. If a duplicate is a part of a group having a reference file coming from a reference directory (in blue color), no action will be taken for this duplicate. If more than one duplicate among the same group are selected, only the first of each group will be promoted.
|
||||
* **Add Selected to Ignore List:** This first removes all selected duplicates from results, and then add the match of that duplicate and the current reference in the ignore list. This match will not come up again in further scan. The duplicate itself might come back, but it will be matched with another reference file. You can clear the ignore list with the Clear Ignore List command.
|
||||
* **Open Selected with Default Application:** Open the file with the application associated with selected file's type.
|
||||
* **Reveal Selected in Finder:** Open the folder containing selected file.
|
||||
* **Rename Selected:** Prompts you for a new name, and then rename the selected file.
|
||||
6
pe/help/templates/versions.mako
Normal file
@@ -0,0 +1,6 @@
|
||||
<%!
|
||||
title = 'dupeGuru PE version history'
|
||||
selected_menu_item = 'Version History'
|
||||
%>
|
||||
<%inherit file="/base_dg.mako"/>
|
||||
${self.output_changelogs(changelog)}
|
||||
97
pe/qt/app.py
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: app
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-04-25
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
import os.path as op
|
||||
|
||||
from PyQt4.QtGui import QImage
|
||||
import PIL.Image
|
||||
|
||||
from hs import fs
|
||||
from hs.fs import phys
|
||||
from hs.utils.str import get_file_ext
|
||||
|
||||
from dupeguru import data_pe
|
||||
from dupeguru.picture.cache import Cache
|
||||
from dupeguru.picture.matchbase import AsyncMatchFactory
|
||||
|
||||
from block import getblocks
|
||||
from base.app import DupeGuru as DupeGuruBase
|
||||
from details_dialog import DetailsDialog
|
||||
from main_window import MainWindow
|
||||
from preferences import Preferences
|
||||
from preferences_dialog import PreferencesDialog
|
||||
|
||||
class File(phys.File):
|
||||
cls_info_map = {
|
||||
'size': fs.IT_ATTRS,
|
||||
'ctime': fs.IT_ATTRS,
|
||||
'mtime': fs.IT_ATTRS,
|
||||
'md5': fs.IT_MD5,
|
||||
'md5partial': fs.IT_MD5,
|
||||
'dimensions': fs.IT_EXTRA,
|
||||
}
|
||||
|
||||
def _initialize_info(self, section):
|
||||
super(File, self)._initialize_info(section)
|
||||
if section == fs.IT_EXTRA:
|
||||
self._info.update({
|
||||
'dimensions': (0,0),
|
||||
})
|
||||
|
||||
def _read_info(self, section):
|
||||
super(File, self)._read_info(section)
|
||||
if section == fs.IT_EXTRA:
|
||||
im = PIL.Image.open(unicode(self.path))
|
||||
self._info['dimensions'] = im.size
|
||||
|
||||
def get_blocks(self, block_count_per_side):
|
||||
image = QImage(unicode(self.path))
|
||||
image = image.convertToFormat(QImage.Format_RGB888)
|
||||
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):
|
||||
LOGO_NAME = 'logo_pe'
|
||||
NAME = 'dupeGuru Picture Edition'
|
||||
VERSION = '1.7.2'
|
||||
DELTA_COLUMNS = frozenset([2, 5, 6])
|
||||
|
||||
def __init__(self):
|
||||
DupeGuruBase.__init__(self, data_pe, appid=5)
|
||||
|
||||
def _setup(self):
|
||||
self.scanner.match_factory = AsyncMatchFactory()
|
||||
self.directories.dirclass = Directory
|
||||
self.scanner.match_factory.cached_blocks = Cache(op.join(self.appdata, 'cached_pictures.db'))
|
||||
DupeGuruBase._setup(self)
|
||||
|
||||
def _update_options(self):
|
||||
DupeGuruBase._update_options(self)
|
||||
self.scanner.match_factory.match_scaled = self.prefs.match_scaled
|
||||
self.scanner.match_factory.threshold = self.prefs.filter_hardness
|
||||
|
||||
def _create_details_dialog(self, parent):
|
||||
return DetailsDialog(parent, self)
|
||||
|
||||
def _create_main_window(self):
|
||||
return MainWindow(app=self)
|
||||
|
||||
def _create_preferences(self):
|
||||
return Preferences()
|
||||
|
||||
def _create_preferences_dialog(self, parent):
|
||||
return PreferencesDialog(parent, self)
|
||||
|
||||
16
pe/qt/app_win.py
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: app_win
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-02
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
import winshell
|
||||
|
||||
import app
|
||||
|
||||
class DupeGuru(app.DupeGuru):
|
||||
@staticmethod
|
||||
def _recycle_dupe(dupe):
|
||||
winshell.delete_file(unicode(dupe.path), no_confirm=True)
|
||||
|
||||
0
pe/qt/base/__init__.py
Normal file
35
pe/qt/base/about_box.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: about_box
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-09
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
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()
|
||||
|
||||
133
pe/qt/base/about_box.ui
Normal file
@@ -0,0 +1,133 @@
|
||||
<?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>
|
||||
269
pe/qt/base/app.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: app
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-04-25
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
import logging
|
||||
import os.path as op
|
||||
import traceback
|
||||
|
||||
from PyQt4.QtCore import Qt, QTimer, QObject, QCoreApplication, QUrl, SIGNAL
|
||||
from PyQt4.QtGui import QProgressDialog, QDesktopServices, QFileDialog, QDialog, QMessageBox
|
||||
|
||||
from hsutil import job
|
||||
from hsutil.reg import RegistrationRequired
|
||||
|
||||
from dupeguru import data_pe
|
||||
from dupeguru.app import (DupeGuru as DupeGuruBase, JOB_SCAN, JOB_LOAD, JOB_MOVE, JOB_COPY,
|
||||
JOB_DELETE)
|
||||
|
||||
from main_window import MainWindow
|
||||
from directories_dialog import DirectoriesDialog
|
||||
from about_box import AboutBox
|
||||
from reg import Registration
|
||||
from error_report_dialog import ErrorReportDialog
|
||||
|
||||
JOBID2TITLE = {
|
||||
JOB_SCAN: "Scanning for duplicates",
|
||||
JOB_LOAD: "Loading",
|
||||
JOB_MOVE: "Moving",
|
||||
JOB_COPY: "Copying",
|
||||
JOB_DELETE: "Sending files to the recycle bin",
|
||||
}
|
||||
|
||||
class Progress(QProgressDialog, job.ThreadedJobPerformer):
|
||||
def __init__(self, parent):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
QProgressDialog.__init__(self, '', u"Cancel", 0, 100, parent, flags)
|
||||
self.setModal(True)
|
||||
self.setAutoReset(False)
|
||||
self.setAutoClose(False)
|
||||
self._timer = QTimer()
|
||||
self._jobid = ''
|
||||
self.connect(self._timer, SIGNAL('timeout()'), self.updateProgress)
|
||||
|
||||
def updateProgress(self):
|
||||
# the values might change before setValue happens
|
||||
last_progress = self.last_progress
|
||||
last_desc = self.last_desc
|
||||
if not self._job_running or last_progress is None:
|
||||
self._timer.stop()
|
||||
self.close()
|
||||
self.emit(SIGNAL('finished(QString)'), self._jobid)
|
||||
if self._last_error is not None:
|
||||
s = ''.join(traceback.format_exception(*self._last_error))
|
||||
dialog = ErrorReportDialog(self.parent(), s)
|
||||
dialog.exec_()
|
||||
return
|
||||
if self.wasCanceled():
|
||||
self.job_cancelled = True
|
||||
return
|
||||
if last_desc:
|
||||
self.setLabelText(last_desc)
|
||||
self.setValue(last_progress)
|
||||
|
||||
def run(self, jobid, title, target, args=()):
|
||||
self._jobid = jobid
|
||||
self.reset()
|
||||
self.setLabelText('')
|
||||
self.run_threaded(target, args)
|
||||
self.setWindowTitle(title)
|
||||
self.show()
|
||||
self._timer.start(500)
|
||||
|
||||
|
||||
def demo_method(method):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return method(self, *args, **kwargs)
|
||||
except RegistrationRequired:
|
||||
msg = "The demo version of dupeGuru only allows 10 actions (delete/move/copy) per session."
|
||||
QMessageBox.information(self.main_window, 'Demo', msg)
|
||||
|
||||
return wrapper
|
||||
|
||||
class DupeGuru(DupeGuruBase, QObject):
|
||||
LOGO_NAME = '<replace this>'
|
||||
NAME = '<replace this>'
|
||||
DELTA_COLUMNS = frozenset()
|
||||
|
||||
def __init__(self, data_module, appid):
|
||||
appdata = unicode(QDesktopServices.storageLocation(QDesktopServices.DataLocation))
|
||||
DupeGuruBase.__init__(self, data_module, appdata, appid)
|
||||
QObject.__init__(self)
|
||||
self._setup()
|
||||
|
||||
#--- Private
|
||||
def _setup(self):
|
||||
self.selected_dupe = None
|
||||
self.prefs = self._create_preferences()
|
||||
self.prefs.load()
|
||||
self._update_options()
|
||||
self.main_window = self._create_main_window()
|
||||
self._progress = Progress(self.main_window)
|
||||
self.directories_dialog = DirectoriesDialog(self.main_window, self)
|
||||
self.details_dialog = self._create_details_dialog(self.main_window)
|
||||
self.preferences_dialog = self._create_preferences_dialog(self.main_window)
|
||||
self.about_box = AboutBox(self.main_window, self)
|
||||
|
||||
self.reg = Registration(self)
|
||||
self.set_registration(self.prefs.registration_code, self.prefs.registration_email)
|
||||
if not self.registered:
|
||||
self.reg.show_nag()
|
||||
self.main_window.show()
|
||||
self.load()
|
||||
|
||||
self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate)
|
||||
self.connect(self._progress, SIGNAL('finished(QString)'), self.job_finished)
|
||||
|
||||
def _setup_as_registered(self):
|
||||
self.prefs.registration_code = self.registration_code
|
||||
self.prefs.registration_email = self.registration_email
|
||||
self.main_window.actionRegister.setVisible(False)
|
||||
self.about_box.registerButton.hide()
|
||||
self.about_box.registeredEmailLabel.setText(self.prefs.registration_email)
|
||||
|
||||
def _update_options(self):
|
||||
self.scanner.mix_file_kind = self.prefs.mix_file_kind
|
||||
self.options['escape_filter_regexp'] = self.prefs.use_regexp
|
||||
self.options['clean_empty_dirs'] = self.prefs.remove_empty_folders
|
||||
|
||||
#--- Virtual
|
||||
def _create_details_dialog(self, parent):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _create_main_window(self):
|
||||
return MainWindow(app=self)
|
||||
|
||||
def _create_preferences(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _create_preferences_dialog(self, parent):
|
||||
raise NotImplementedError()
|
||||
|
||||
#--- Override
|
||||
def _start_job(self, jobid, func):
|
||||
title = JOBID2TITLE[jobid]
|
||||
try:
|
||||
j = self._progress.create_job()
|
||||
self._progress.run(jobid, title, func, args=(j, ))
|
||||
except job.JobInProgressError:
|
||||
msg = "A previous action is still hanging in there. You can't start a new one yet. Wait a few seconds, then try again."
|
||||
QMessageBox.information(self.main_window, 'Action in progress', msg)
|
||||
|
||||
#--- Public
|
||||
def add_dupes_to_ignore_list(self, duplicates):
|
||||
for dupe in duplicates:
|
||||
self.AddToIgnoreList(dupe)
|
||||
self.remove_duplicates(duplicates)
|
||||
|
||||
def ApplyFilter(self, filter):
|
||||
DupeGuruBase.ApplyFilter(self, filter)
|
||||
self.emit(SIGNAL('resultsChanged()'))
|
||||
|
||||
def ask_for_reg_code(self):
|
||||
if self.reg.ask_for_code():
|
||||
self._setup_ui_as_registered()
|
||||
|
||||
@demo_method
|
||||
def copy_or_move_marked(self, copy):
|
||||
opname = 'copy' if copy else 'move'
|
||||
title = "Select a directory to {0} marked files to".format(opname)
|
||||
flags = QFileDialog.ShowDirsOnly
|
||||
destination = unicode(QFileDialog.getExistingDirectory(self.main_window, title, '', flags))
|
||||
if not destination:
|
||||
return
|
||||
recreate_path = self.prefs.destination_type
|
||||
DupeGuruBase.copy_or_move_marked(self, copy, destination, recreate_path)
|
||||
|
||||
delete_marked = demo_method(DupeGuruBase.delete_marked)
|
||||
|
||||
def make_reference(self, duplicates):
|
||||
DupeGuruBase.make_reference(self, duplicates)
|
||||
self.emit(SIGNAL('resultsChanged()'))
|
||||
|
||||
def mark_all(self):
|
||||
self.results.mark_all()
|
||||
self.emit(SIGNAL('dupeMarkingChanged()'))
|
||||
|
||||
def mark_invert(self):
|
||||
self.results.mark_invert()
|
||||
self.emit(SIGNAL('dupeMarkingChanged()'))
|
||||
|
||||
def mark_none(self):
|
||||
self.results.mark_none()
|
||||
self.emit(SIGNAL('dupeMarkingChanged()'))
|
||||
|
||||
def open_selected(self):
|
||||
if self.selected_dupe is None:
|
||||
return
|
||||
url = QUrl.fromLocalFile(unicode(self.selected_dupe.path))
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def remove_duplicates(self, duplicates):
|
||||
self.results.remove_duplicates(duplicates)
|
||||
self.emit(SIGNAL('resultsChanged()'))
|
||||
|
||||
def remove_marked_duplicates(self):
|
||||
marked = [d for d in self.results.dupes if self.results.is_marked(d)]
|
||||
self.remove_duplicates(marked)
|
||||
|
||||
def rename_dupe(self, dupe, newname):
|
||||
try:
|
||||
dupe.move(dupe.parent, newname)
|
||||
return True
|
||||
except (IndexError, fs.FSError) as e:
|
||||
logging.warning("dupeGuru Warning: %s" % unicode(e))
|
||||
return False
|
||||
|
||||
def reveal_selected(self):
|
||||
if self.selected_dupe is None:
|
||||
return
|
||||
url = QUrl.fromLocalFile(unicode(self.selected_dupe.path[:-1]))
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def select_duplicate(self, dupe):
|
||||
self.selected_dupe = dupe
|
||||
self.emit(SIGNAL('duplicateSelected()'))
|
||||
|
||||
def show_about_box(self):
|
||||
self.about_box.show()
|
||||
|
||||
def show_details(self):
|
||||
self.details_dialog.show()
|
||||
|
||||
def show_directories(self):
|
||||
self.directories_dialog.show()
|
||||
|
||||
def show_help(self):
|
||||
url = QUrl.fromLocalFile(op.abspath('help/intro.htm'))
|
||||
QDesktopServices.openUrl(url)
|
||||
|
||||
def show_preferences(self):
|
||||
self.preferences_dialog.load()
|
||||
result = self.preferences_dialog.exec_()
|
||||
if result == QDialog.Accepted:
|
||||
self.preferences_dialog.save()
|
||||
self.prefs.save()
|
||||
self._update_options()
|
||||
|
||||
def toggle_marking_for_dupes(self, dupes):
|
||||
for dupe in dupes:
|
||||
self.results.mark_toggle(dupe)
|
||||
self.emit(SIGNAL('dupeMarkingChanged()'))
|
||||
|
||||
#--- Events
|
||||
def application_will_terminate(self):
|
||||
self.Save()
|
||||
self.SaveIgnoreList()
|
||||
|
||||
def job_finished(self, jobid):
|
||||
self.emit(SIGNAL('resultsChanged()'))
|
||||
if jobid == JOB_LOAD:
|
||||
self.emit(SIGNAL('directoriesChanged()'))
|
||||
if jobid in (JOB_MOVE, JOB_COPY, JOB_DELETE) and self.last_op_error_count > 0:
|
||||
msg = "{0} files could not be processed.".format(self.results.mark_count)
|
||||
QMessageBox.warning(self.main_window, 'Warning', msg)
|
||||
|
||||
78
pe/qt/base/details_table.py
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: details_table
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-17
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant
|
||||
from PyQt4.QtGui import QHeaderView, QTableView
|
||||
|
||||
HEADER = ['Attribute', 'Selected', 'Reference']
|
||||
|
||||
class DetailsModel(QAbstractTableModel):
|
||||
def __init__(self, app):
|
||||
QAbstractTableModel.__init__(self)
|
||||
self._app = app
|
||||
self._data = app.data
|
||||
self._dupe_data = None
|
||||
self._ref_data = None
|
||||
self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected)
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(HEADER)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
if role != Qt.DisplayRole:
|
||||
return QVariant()
|
||||
column = index.column()
|
||||
row = index.row()
|
||||
if column == 0:
|
||||
return QVariant(self._data.COLUMNS[row]['display'])
|
||||
elif column == 1 and self._dupe_data:
|
||||
return QVariant(self._dupe_data[row])
|
||||
elif column == 2 and self._ref_data:
|
||||
return QVariant(self._ref_data[row])
|
||||
return QVariant()
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(HEADER):
|
||||
return QVariant(HEADER[section])
|
||||
return QVariant()
|
||||
|
||||
def rowCount(self, parent):
|
||||
return len(self._data.COLUMNS)
|
||||
|
||||
#--- Events
|
||||
def duplicateSelected(self):
|
||||
dupe = self._app.selected_dupe
|
||||
group = self._app.results.get_group_of_duplicate(dupe)
|
||||
ref = group.ref
|
||||
self._dupe_data = self._data.GetDisplayInfo(dupe, group)
|
||||
self._ref_data = self._data.GetDisplayInfo(ref, group)
|
||||
self.reset()
|
||||
|
||||
|
||||
class DetailsTable(QTableView):
|
||||
def __init__(self, *args):
|
||||
QTableView.__init__(self, *args)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSelectionBehavior(QTableView.SelectRows)
|
||||
self.setShowGrid(False)
|
||||
|
||||
def setModel(self, model):
|
||||
QTableView.setModel(self, model)
|
||||
# The model needs to be set to set header stuff
|
||||
hheader = self.horizontalHeader()
|
||||
hheader.setHighlightSections(False)
|
||||
hheader.setStretchLastSection(False)
|
||||
hheader.resizeSection(0, 100)
|
||||
hheader.setResizeMode(0, QHeaderView.Fixed)
|
||||
hheader.setResizeMode(1, QHeaderView.Stretch)
|
||||
hheader.setResizeMode(2, QHeaderView.Stretch)
|
||||
vheader = self.verticalHeader()
|
||||
vheader.setVisible(False)
|
||||
vheader.setDefaultSectionSize(18)
|
||||
|
||||
17
pe/qt/base/dg.qrc
Normal file
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE RCC><RCC version="1.0">
|
||||
<qresource>
|
||||
<file alias="details">images/details32.png</file>
|
||||
<file alias="logo_pe">images/dgpe_logo_32.png</file>
|
||||
<file alias="logo_pe_big">images/dgpe_logo_128.png</file>
|
||||
<file alias="logo_me">images/dgme_logo_32.png</file>
|
||||
<file alias="logo_me_big">images/dgme_logo_128.png</file>
|
||||
<file alias="logo_se">images/dgse_logo_32.png</file>
|
||||
<file alias="logo_se_big">images/dgse_logo_128.png</file>
|
||||
<file alias="folder">images/folderwin32.png</file>
|
||||
<file alias="gear">images/gear.png</file>
|
||||
<file alias="preferences">images/preferences32.png</file>
|
||||
<file alias="actions">images/actions32.png</file>
|
||||
<file alias="delta">images/delta32.png</file>
|
||||
<file alias="power_marker">images/power_marker32.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
80
pe/qt/base/directories_dialog.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: directories_dialog
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-04-25
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import SIGNAL, Qt
|
||||
from PyQt4.QtGui import QDialog, QFileDialog, QHeaderView
|
||||
|
||||
from directories_dialog_ui import Ui_DirectoriesDialog
|
||||
from directories_model import DirectoriesModel, DirectoriesDelegate
|
||||
|
||||
class DirectoriesDialog(QDialog, Ui_DirectoriesDialog):
|
||||
def __init__(self, parent, app):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
QDialog.__init__(self, parent, flags)
|
||||
self.app = app
|
||||
self._setupUi()
|
||||
self._updateRemoveButton()
|
||||
|
||||
self.connect(self.doneButton, SIGNAL('clicked()'), self.doneButtonClicked)
|
||||
self.connect(self.addButton, SIGNAL('clicked()'), self.addButtonClicked)
|
||||
self.connect(self.removeButton, SIGNAL('clicked()'), self.removeButtonClicked)
|
||||
self.connect(self.treeView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged)
|
||||
self.connect(self.app, SIGNAL('directoriesChanged()'), self.directoriesChanged)
|
||||
|
||||
def _setupUi(self):
|
||||
self.setupUi(self)
|
||||
# Stuff that can't be done in the Designer
|
||||
self.directoriesModel = DirectoriesModel(self.app)
|
||||
self.directoriesDelegate = DirectoriesDelegate()
|
||||
self.treeView.setItemDelegate(self.directoriesDelegate)
|
||||
self.treeView.setModel(self.directoriesModel)
|
||||
|
||||
header = self.treeView.header()
|
||||
header.setStretchLastSection(False)
|
||||
header.setResizeMode(0, QHeaderView.Stretch)
|
||||
header.setResizeMode(1, QHeaderView.Fixed)
|
||||
header.resizeSection(1, 100)
|
||||
|
||||
def _updateRemoveButton(self):
|
||||
indexes = self.treeView.selectedIndexes()
|
||||
if not indexes:
|
||||
self.removeButton.setEnabled(False)
|
||||
return
|
||||
self.removeButton.setEnabled(True)
|
||||
index = indexes[0]
|
||||
node = index.internalPointer()
|
||||
# label = 'Remove' if node.parent is None else 'Exclude'
|
||||
|
||||
def addButtonClicked(self):
|
||||
title = u"Select a directory to add to the scanning list"
|
||||
flags = QFileDialog.ShowDirsOnly
|
||||
dirpath = unicode(QFileDialog.getExistingDirectory(self, title, '', flags))
|
||||
if not dirpath:
|
||||
return
|
||||
self.app.AddDirectory(dirpath)
|
||||
self.directoriesModel.reset()
|
||||
|
||||
def directoriesChanged(self):
|
||||
self.directoriesModel.reset()
|
||||
|
||||
def doneButtonClicked(self):
|
||||
self.hide()
|
||||
|
||||
def removeButtonClicked(self):
|
||||
indexes = self.treeView.selectedIndexes()
|
||||
if not indexes:
|
||||
return
|
||||
index = indexes[0]
|
||||
node = index.internalPointer()
|
||||
if node.parent is None:
|
||||
row = index.row()
|
||||
del self.app.directories[row]
|
||||
self.directoriesModel.reset()
|
||||
|
||||
def selectionChanged(self, selected, deselected):
|
||||
self._updateRemoveButton()
|
||||
|
||||
133
pe/qt/base/directories_dialog.ui
Normal file
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DirectoriesDialog</class>
|
||||
<widget class="QDialog" name="DirectoriesDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>420</width>
|
||||
<height>338</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Directories</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTreeView" name="treeView">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
|
||||
</property>
|
||||
<property name="uniformRowHeights">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="headerStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>91</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Remove</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="addButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>91</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Add</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="doneButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>91</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Done</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
108
pe/qt/base/directories_model.py
Normal file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: directories_model
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-04-25
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import QVariant, QModelIndex, Qt, QRect, QEvent, QPoint
|
||||
from PyQt4.QtGui import QComboBox, QStyledItemDelegate, QMouseEvent, QApplication, QBrush
|
||||
|
||||
from tree_model import TreeNode, TreeModel
|
||||
|
||||
HEADERS = ['Name', 'State']
|
||||
STATES = ['Normal', 'Reference', 'Excluded']
|
||||
|
||||
class DirectoriesDelegate(QStyledItemDelegate):
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = QComboBox(parent);
|
||||
editor.addItems(STATES)
|
||||
return editor
|
||||
|
||||
def setEditorData(self, editor, index):
|
||||
value, ok = index.model().data(index, Qt.EditRole).toInt()
|
||||
assert ok
|
||||
editor.setCurrentIndex(value);
|
||||
press = QMouseEvent(QEvent.MouseButtonPress, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
|
||||
release = QMouseEvent(QEvent.MouseButtonRelease, QPoint(0, 0), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
|
||||
QApplication.sendEvent(editor, press)
|
||||
QApplication.sendEvent(editor, release)
|
||||
# editor.showPopup() # this causes a weird glitch. the ugly workaround is above.
|
||||
|
||||
def setModelData(self, editor, model, index):
|
||||
value = QVariant(editor.currentIndex())
|
||||
model.setData(index, value, Qt.EditRole)
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
editor.setGeometry(option.rect)
|
||||
|
||||
|
||||
class DirectoryNode(TreeNode):
|
||||
def __init__(self, parent, ref, row):
|
||||
TreeNode.__init__(self, parent, row)
|
||||
self.ref = ref
|
||||
|
||||
def _get_children(self):
|
||||
children = []
|
||||
for index, directory in enumerate(self.ref.dirs):
|
||||
node = DirectoryNode(self, directory, index)
|
||||
children.append(node)
|
||||
return children
|
||||
|
||||
|
||||
class DirectoriesModel(TreeModel):
|
||||
def __init__(self, app):
|
||||
self._dirs = app.directories
|
||||
TreeModel.__init__(self)
|
||||
|
||||
def _root_nodes(self):
|
||||
nodes = []
|
||||
for index, directory in enumerate(self._dirs):
|
||||
nodes.append(DirectoryNode(None, directory, index))
|
||||
return nodes
|
||||
|
||||
def columnCount(self, parent):
|
||||
return 2
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
node = index.internalPointer()
|
||||
if role == Qt.DisplayRole:
|
||||
if index.column() == 0:
|
||||
return QVariant(node.ref.name)
|
||||
else:
|
||||
return QVariant(STATES[self._dirs.GetState(node.ref.path)])
|
||||
elif role == Qt.EditRole and index.column() == 1:
|
||||
return QVariant(self._dirs.GetState(node.ref.path))
|
||||
elif role == Qt.ForegroundRole:
|
||||
state = self._dirs.GetState(node.ref.path)
|
||||
if state == 1:
|
||||
return QVariant(QBrush(Qt.blue))
|
||||
elif state == 2:
|
||||
return QVariant(QBrush(Qt.red))
|
||||
return QVariant()
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return 0
|
||||
result = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
if index.column() == 1:
|
||||
result |= Qt.ItemIsEditable
|
||||
return result
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal:
|
||||
if role == Qt.DisplayRole and section < len(HEADERS):
|
||||
return QVariant(HEADERS[section])
|
||||
return QVariant()
|
||||
|
||||
def setData(self, index, value, role):
|
||||
if not index.isValid() or role != Qt.EditRole or index.column() != 1:
|
||||
return False
|
||||
node = index.internalPointer()
|
||||
state, ok = value.toInt()
|
||||
assert ok
|
||||
self._dirs.SetState(node.ref.path, state)
|
||||
return True
|
||||
|
||||
25
pe/qt/base/error_report_dialog.py
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: error_report_dialog
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-23
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import Qt, QUrl
|
||||
from PyQt4.QtGui import QDialog, QDesktopServices
|
||||
|
||||
from error_report_dialog_ui import Ui_ErrorReportDialog
|
||||
|
||||
class ErrorReportDialog(QDialog, Ui_ErrorReportDialog):
|
||||
def __init__(self, parent, error):
|
||||
flags = Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowSystemMenuHint
|
||||
QDialog.__init__(self, parent, flags)
|
||||
self.setupUi(self)
|
||||
self.errorTextEdit.setPlainText(error)
|
||||
|
||||
def accept(self):
|
||||
text = self.errorTextEdit.toPlainText()
|
||||
url = QUrl("mailto:support@hardcoded.net?SUBJECT=Error Report&BODY=%s" % text)
|
||||
QDesktopServices.openUrl(url)
|
||||
QDialog.accept(self)
|
||||
|
||||
117
pe/qt/base/error_report_dialog.ui
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ErrorReportDialog</class>
|
||||
<widget class="QDialog" name="ErrorReportDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>553</width>
|
||||
<height>349</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Error Report</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Something went wrong. Would you like to send the error report to Hardcoded Software?</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="errorTextEdit">
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="dontSendButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Don't Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="sendButton">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>110</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Send</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>sendButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ErrorReportDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>485</x>
|
||||
<y>320</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>276</x>
|
||||
<y>174</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>dontSendButton</sender>
|
||||
<signal>clicked()</signal>
|
||||
<receiver>ErrorReportDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>373</x>
|
||||
<y>320</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>276</x>
|
||||
<y>174</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
20
pe/qt/base/gen.py
Normal file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: gen
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-22
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
import os
|
||||
|
||||
def print_and_do(cmd):
|
||||
print cmd
|
||||
os.system(cmd)
|
||||
|
||||
print_and_do("pyuic4 main_window.ui > main_window_ui.py")
|
||||
print_and_do("pyuic4 directories_dialog.ui > directories_dialog_ui.py")
|
||||
print_and_do("pyuic4 about_box.ui > about_box_ui.py")
|
||||
print_and_do("pyuic4 reg_submit_dialog.ui > reg_submit_dialog_ui.py")
|
||||
print_and_do("pyuic4 reg_demo_dialog.ui > reg_demo_dialog_ui.py")
|
||||
print_and_do("pyuic4 error_report_dialog.ui > error_report_dialog_ui.py")
|
||||
print_and_do("pyrcc4 dg.qrc > dg_rc.py")
|
||||
BIN
pe/qt/base/images/actions32.png
Executable file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
pe/qt/base/images/delta32.png
Executable file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
pe/qt/base/images/details32.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
pe/qt/base/images/dgme_logo.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
pe/qt/base/images/dgme_logo_128.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
pe/qt/base/images/dgme_logo_32.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
pe/qt/base/images/dgpe_logo.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
pe/qt/base/images/dgpe_logo_128.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
pe/qt/base/images/dgpe_logo_32.png
Executable file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
pe/qt/base/images/dgse_logo.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
pe/qt/base/images/dgse_logo_128.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
pe/qt/base/images/dgse_logo_32.png
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
pe/qt/base/images/folder32.png
Executable file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
pe/qt/base/images/folderwin32.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
pe/qt/base/images/gear.png
Executable file
|
After Width: | Height: | Size: 394 B |
BIN
pe/qt/base/images/power_marker32.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
pe/qt/base/images/preferences32.png
Executable file
|
After Width: | Height: | Size: 2.8 KiB |
304
pe/qt/base/main_window.py
Normal file
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: main_window
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-04-25
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import Qt, QCoreApplication, QProcess, SIGNAL
|
||||
from PyQt4.QtGui import (QMainWindow, QMenu, QPixmap, QIcon, QToolButton, QLabel, QHeaderView,
|
||||
QMessageBox, QInputDialog, QLineEdit)
|
||||
|
||||
from hsutil.misc import nonone
|
||||
|
||||
from dupeguru.app import NoScannableFileError, AllFilesAreRefError
|
||||
|
||||
import dg_rc
|
||||
from main_window_ui import Ui_MainWindow
|
||||
from results_model import ResultsDelegate, ResultsModel
|
||||
|
||||
class MainWindow(QMainWindow, Ui_MainWindow):
|
||||
def __init__(self, app):
|
||||
QMainWindow.__init__(self, None)
|
||||
self.app = app
|
||||
self._last_filter = None
|
||||
self._setupUi()
|
||||
self.resultsDelegate = ResultsDelegate()
|
||||
self.resultsModel = ResultsModel(self.app)
|
||||
self.resultsView.setModel(self.resultsModel)
|
||||
self.resultsView.setItemDelegate(self.resultsDelegate)
|
||||
self._load_columns()
|
||||
self._update_column_actions_status()
|
||||
self.resultsView.expandAll()
|
||||
self._update_status_line()
|
||||
|
||||
self.connect(self.app, SIGNAL('resultsChanged()'), self.resultsChanged)
|
||||
self.connect(self.app, SIGNAL('dupeMarkingChanged()'), self.dupeMarkingChanged)
|
||||
self.connect(self.actionQuit, SIGNAL('triggered()'), QCoreApplication.instance().quit)
|
||||
self.connect(self.resultsView.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged)
|
||||
self.connect(self.menuColumns, SIGNAL('triggered(QAction*)'), self.columnToggled)
|
||||
self.connect(QCoreApplication.instance(), SIGNAL('aboutToQuit()'), self.application_will_terminate)
|
||||
self.connect(self.resultsModel, SIGNAL('modelReset()'), self.resultsReset)
|
||||
|
||||
def _setupUi(self):
|
||||
self.setupUi(self)
|
||||
# Stuff that can't be setup in the Designer
|
||||
h = self.resultsView.header()
|
||||
h.setHighlightSections(False)
|
||||
h.setMovable(True)
|
||||
h.setStretchLastSection(False)
|
||||
h.setDefaultAlignment(Qt.AlignLeft)
|
||||
|
||||
self.setWindowTitle(QCoreApplication.instance().applicationName())
|
||||
self.actionScan.setIcon(QIcon(QPixmap(':/%s' % self.app.LOGO_NAME)))
|
||||
|
||||
# Columns menu
|
||||
menu = self.menuColumns
|
||||
self._column_actions = []
|
||||
for index, column in enumerate(self.app.data.COLUMNS):
|
||||
action = menu.addAction(column['display'])
|
||||
action.setCheckable(True)
|
||||
action.column_index = index
|
||||
self._column_actions.append(action)
|
||||
menu.addSeparator()
|
||||
action = menu.addAction("Reset to Defaults")
|
||||
action.column_index = -1
|
||||
|
||||
# Action menu
|
||||
actionMenu = QMenu('Actions', self.toolBar)
|
||||
actionMenu.setIcon(QIcon(QPixmap(":/actions")))
|
||||
actionMenu.addAction(self.actionDeleteMarked)
|
||||
actionMenu.addAction(self.actionMoveMarked)
|
||||
actionMenu.addAction(self.actionCopyMarked)
|
||||
actionMenu.addAction(self.actionRemoveMarked)
|
||||
actionMenu.addSeparator()
|
||||
actionMenu.addAction(self.actionRemoveSelected)
|
||||
actionMenu.addAction(self.actionIgnoreSelected)
|
||||
actionMenu.addAction(self.actionMakeSelectedReference)
|
||||
actionMenu.addSeparator()
|
||||
actionMenu.addAction(self.actionOpenSelected)
|
||||
actionMenu.addAction(self.actionRevealSelected)
|
||||
actionMenu.addAction(self.actionRenameSelected)
|
||||
self.actionActions.setMenu(actionMenu)
|
||||
button = QToolButton(self.toolBar)
|
||||
button.setDefaultAction(actionMenu.menuAction())
|
||||
button.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
||||
self.actionsButton = button
|
||||
self.toolBar.insertWidget(self.actionActions, button) # the action is a placeholder
|
||||
self.toolBar.removeAction(self.actionActions)
|
||||
|
||||
self.statusLabel = QLabel(self)
|
||||
self.statusbar.addPermanentWidget(self.statusLabel, 1)
|
||||
|
||||
#--- Private
|
||||
def _confirm(self, title, msg, default_button=QMessageBox.Yes):
|
||||
buttons = QMessageBox.Yes | QMessageBox.No
|
||||
answer = QMessageBox.question(self, title, msg, buttons, default_button)
|
||||
return answer == QMessageBox.Yes
|
||||
|
||||
def _load_columns(self):
|
||||
h = self.resultsView.header()
|
||||
h.setResizeMode(QHeaderView.Interactive)
|
||||
prefs = self.app.prefs
|
||||
attrs = zip(prefs.columns_width, prefs.columns_visible)
|
||||
for index, (width, visible) in enumerate(attrs):
|
||||
h.resizeSection(index, width)
|
||||
h.setSectionHidden(index, not visible)
|
||||
h.setResizeMode(0, QHeaderView.Stretch)
|
||||
|
||||
def _redraw_results(self):
|
||||
# HACK. this is the only way I found to update the widget without reseting everything
|
||||
self.resultsView.scroll(0, 1)
|
||||
self.resultsView.scroll(0, -1)
|
||||
|
||||
def _save_columns(self):
|
||||
h = self.resultsView.header()
|
||||
widths = []
|
||||
visible = []
|
||||
for i in range(len(self.app.data.COLUMNS)):
|
||||
widths.append(h.sectionSize(i))
|
||||
visible.append(not h.isSectionHidden(i))
|
||||
prefs = self.app.prefs
|
||||
prefs.columns_width = widths
|
||||
prefs.columns_visible = visible
|
||||
prefs.save()
|
||||
|
||||
def _update_column_actions_status(self):
|
||||
h = self.resultsView.header()
|
||||
for action in self._column_actions:
|
||||
colid = action.column_index
|
||||
action.setChecked(not h.isSectionHidden(colid))
|
||||
|
||||
def _update_status_line(self):
|
||||
self.statusLabel.setText(self.app.stat_line)
|
||||
|
||||
#--- Actions
|
||||
def aboutTriggered(self):
|
||||
self.app.show_about_box()
|
||||
|
||||
def actionsTriggered(self):
|
||||
self.actionsButton.showMenu()
|
||||
|
||||
def addToIgnoreListTriggered(self):
|
||||
dupes = self.resultsView.selectedDupes()
|
||||
if not dupes:
|
||||
return
|
||||
title = "Add to Ignore List"
|
||||
msg = "All selected {0} matches are going to be ignored in all subsequent scans. Continue?".format(len(dupes))
|
||||
if self._confirm(title, msg):
|
||||
self.app.add_dupes_to_ignore_list(dupes)
|
||||
|
||||
def applyFilterTriggered(self):
|
||||
title = "Apply Filter"
|
||||
msg = "Type the filter you want to apply on your results. See help for details."
|
||||
text = nonone(self._last_filter, '[*]')
|
||||
answer, ok = QInputDialog.getText(self, title, msg, QLineEdit.Normal, text)
|
||||
if not ok:
|
||||
return
|
||||
answer = unicode(answer)
|
||||
self.app.ApplyFilter(answer)
|
||||
self._last_filter = answer
|
||||
|
||||
def cancelFilterTriggered(self):
|
||||
self.app.ApplyFilter('')
|
||||
|
||||
def checkForUpdateTriggered(self):
|
||||
QProcess.execute('updater.exe', ['/checknow'])
|
||||
|
||||
def clearIgnoreListTriggered(self):
|
||||
title = "Clear Ignore List"
|
||||
count = len(self.app.scanner.ignore_list)
|
||||
if not count:
|
||||
QMessageBox.information(self, title, "Nothing to clear.")
|
||||
return
|
||||
msg = "Do you really want to remove all {0} items from the ignore list?".format(count)
|
||||
if self._confirm(title, msg, QMessageBox.No):
|
||||
self.app.scanner.ignore_list.Clear()
|
||||
QMessageBox.information(self, title, "Ignore list cleared.")
|
||||
|
||||
def copyTriggered(self):
|
||||
self.app.copy_or_move_marked(True)
|
||||
|
||||
def deleteTriggered(self):
|
||||
count = self.app.results.mark_count
|
||||
if not count:
|
||||
return
|
||||
title = "Delete duplicates"
|
||||
msg = "You are about to send {0} files to the recycle bin. Continue?".format(count)
|
||||
if self._confirm(title, msg):
|
||||
self.app.delete_marked()
|
||||
|
||||
def deltaTriggered(self):
|
||||
self.resultsModel.delta = self.actionDelta.isChecked()
|
||||
self._redraw_results()
|
||||
|
||||
def detailsTriggered(self):
|
||||
self.app.show_details()
|
||||
|
||||
def directoriesTriggered(self):
|
||||
self.app.show_directories()
|
||||
|
||||
def makeReferenceTriggered(self):
|
||||
self.app.make_reference(self.resultsView.selectedDupes())
|
||||
|
||||
def markAllTriggered(self):
|
||||
self.app.mark_all()
|
||||
|
||||
def markInvertTriggered(self):
|
||||
self.app.mark_invert()
|
||||
|
||||
def markNoneTriggered(self):
|
||||
self.app.mark_none()
|
||||
|
||||
def markSelectedTriggered(self):
|
||||
dupes = self.resultsView.selectedDupes()
|
||||
self.app.toggle_marking_for_dupes(dupes)
|
||||
|
||||
def moveTriggered(self):
|
||||
self.app.copy_or_move_marked(False)
|
||||
|
||||
def openTriggered(self):
|
||||
self.app.open_selected()
|
||||
|
||||
def powerMarkerTriggered(self):
|
||||
self.resultsModel.power_marker = self.actionPowerMarker.isChecked()
|
||||
|
||||
def preferencesTriggered(self):
|
||||
self.app.show_preferences()
|
||||
|
||||
def registerTrigerred(self):
|
||||
self.app.ask_for_reg_code()
|
||||
|
||||
def removeMarkedTriggered(self):
|
||||
count = self.app.results.mark_count
|
||||
if not count:
|
||||
return
|
||||
title = "Remove duplicates"
|
||||
msg = "You are about to remove {0} files from results. Continue?".format(count)
|
||||
if self._confirm(title, msg):
|
||||
self.app.remove_marked_duplicates()
|
||||
|
||||
def removeSelectedTriggered(self):
|
||||
dupes = self.resultsView.selectedDupes()
|
||||
if not dupes:
|
||||
return
|
||||
title = "Remove duplicates"
|
||||
msg = "You are about to remove {0} files from results. Continue?".format(len(dupes))
|
||||
if self._confirm(title, msg):
|
||||
self.app.remove_duplicates(dupes)
|
||||
|
||||
def renameTriggered(self):
|
||||
self.resultsView.edit(self.resultsView.selectionModel().currentIndex())
|
||||
|
||||
def revealTriggered(self):
|
||||
self.app.reveal_selected()
|
||||
|
||||
def scanTriggered(self):
|
||||
title = "Start a new scan"
|
||||
if len(self.app.results.groups) > 0:
|
||||
msg = "Are you sure you want to start a new duplicate scan?"
|
||||
if not self._confirm(title, msg):
|
||||
return
|
||||
try:
|
||||
self.app.start_scanning()
|
||||
except NoScannableFileError:
|
||||
msg = "The selected directories contain no scannable file."
|
||||
QMessageBox.warning(self, title, msg)
|
||||
self.app.show_directories()
|
||||
except AllFilesAreRefError:
|
||||
msg = "You cannot make a duplicate scan with only reference directories."
|
||||
QMessageBox.warning(self, title, msg)
|
||||
|
||||
def showHelpTriggered(self):
|
||||
self.app.show_help()
|
||||
|
||||
#--- Events
|
||||
def application_will_terminate(self):
|
||||
self._save_columns()
|
||||
|
||||
def columnToggled(self, action):
|
||||
colid = action.column_index
|
||||
if colid == -1:
|
||||
self.app.prefs.reset_columns()
|
||||
self._load_columns()
|
||||
else:
|
||||
h = self.resultsView.header()
|
||||
h.setSectionHidden(colid, not h.isSectionHidden(colid))
|
||||
self._update_column_actions_status()
|
||||
|
||||
def dupeMarkingChanged(self):
|
||||
self._redraw_results()
|
||||
self._update_status_line()
|
||||
|
||||
def resultsChanged(self):
|
||||
self.resultsView.model().reset()
|
||||
|
||||
def resultsReset(self):
|
||||
self.resultsView.expandAll()
|
||||
self._update_status_line()
|
||||
|
||||
def selectionChanged(self, selected, deselected):
|
||||
index = self.resultsView.selectionModel().currentIndex()
|
||||
dupe = index.internalPointer().dupe if index.isValid() else None
|
||||
self.app.select_duplicate(dupe)
|
||||
|
||||
911
pe/qt/base/main_window.ui
Normal file
@@ -0,0 +1,911 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>630</width>
|
||||
<height>514</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>dupeGuru</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="ResultsView" name="resultsView">
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="rootIsDecorated">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="uniformRowHeights">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="itemsExpandable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="expandsOnDoubleClick">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="headerStretchLastSection">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>630</width>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuColumns">
|
||||
<property name="title">
|
||||
<string>Columns</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuActions">
|
||||
<property name="title">
|
||||
<string>Actions</string>
|
||||
</property>
|
||||
<addaction name="actionDeleteMarked"/>
|
||||
<addaction name="actionMoveMarked"/>
|
||||
<addaction name="actionCopyMarked"/>
|
||||
<addaction name="actionRemoveMarked"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionRemoveSelected"/>
|
||||
<addaction name="actionIgnoreSelected"/>
|
||||
<addaction name="actionMakeSelectedReference"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionOpenSelected"/>
|
||||
<addaction name="actionRevealSelected"/>
|
||||
<addaction name="actionRenameSelected"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionApplyFilter"/>
|
||||
<addaction name="actionCancelFilter"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuMark">
|
||||
<property name="title">
|
||||
<string>Mark</string>
|
||||
</property>
|
||||
<addaction name="actionMarkAll"/>
|
||||
<addaction name="actionMarkNone"/>
|
||||
<addaction name="actionInvertMarking"/>
|
||||
<addaction name="actionMarkSelected"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuModes">
|
||||
<property name="title">
|
||||
<string>Modes</string>
|
||||
</property>
|
||||
<addaction name="actionPowerMarker"/>
|
||||
<addaction name="actionDelta"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuWindow">
|
||||
<property name="title">
|
||||
<string>Windows</string>
|
||||
</property>
|
||||
<addaction name="actionDetails"/>
|
||||
<addaction name="actionDirectories"/>
|
||||
<addaction name="actionPreferences"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuHelp">
|
||||
<property name="title">
|
||||
<string>Help</string>
|
||||
</property>
|
||||
<addaction name="actionShowHelp"/>
|
||||
<addaction name="actionRegister"/>
|
||||
<addaction name="actionCheckForUpdate"/>
|
||||
<addaction name="actionAbout"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="actionScan"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionClearIgnoreList"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionQuit"/>
|
||||
</widget>
|
||||
<addaction name="menuFile"/>
|
||||
<addaction name="menuMark"/>
|
||||
<addaction name="menuActions"/>
|
||||
<addaction name="menuColumns"/>
|
||||
<addaction name="menuModes"/>
|
||||
<addaction name="menuWindow"/>
|
||||
<addaction name="menuHelp"/>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="toolBar">
|
||||
<property name="windowTitle">
|
||||
<string>toolBar</string>
|
||||
</property>
|
||||
<property name="movable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextUnderIcon</enum>
|
||||
</property>
|
||||
<property name="floatable">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionScan"/>
|
||||
<addaction name="actionActions"/>
|
||||
<addaction name="actionDirectories"/>
|
||||
<addaction name="actionDetails"/>
|
||||
<addaction name="actionPreferences"/>
|
||||
<addaction name="actionDelta"/>
|
||||
<addaction name="actionPowerMarker"/>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar">
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<action name="actionScan">
|
||||
<property name="icon">
|
||||
<iconset resource="dg.qrc">
|
||||
<normaloff>:/logo_pe</normaloff>:/logo_pe</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Start Scan</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Start scanning for duplicates</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+S</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDirectories">
|
||||
<property name="icon">
|
||||
<iconset resource="dg.qrc">
|
||||
<normaloff>:/folder</normaloff>:/folder</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Directories</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+4</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDetails">
|
||||
<property name="icon">
|
||||
<iconset resource="dg.qrc">
|
||||
<normaloff>:/details</normaloff>:/details</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Details</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+3</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionActions">
|
||||
<property name="icon">
|
||||
<iconset resource="dg.qrc">
|
||||
<normaloff>:/actions</normaloff>:/actions</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Actions</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionPreferences">
|
||||
<property name="icon">
|
||||
<iconset resource="dg.qrc">
|
||||
<normaloff>:/preferences</normaloff>:/preferences</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Preferences</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+5</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDelta">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="dg.qrc">
|
||||
<normaloff>:/delta</normaloff>:/delta</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Delta Values</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+2</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionPowerMarker">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="dg.qrc">
|
||||
<normaloff>:/power_marker</normaloff>:/power_marker</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Power Marker</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+1</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDeleteMarked">
|
||||
<property name="text">
|
||||
<string>Send Marked to Recycle Bin</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+D</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionMoveMarked">
|
||||
<property name="text">
|
||||
<string>Move Marked to...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+M</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCopyMarked">
|
||||
<property name="text">
|
||||
<string>Copy Marked to...</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+M</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionRemoveMarked">
|
||||
<property name="text">
|
||||
<string>Remove Marked from Results</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+R</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionRemoveSelected">
|
||||
<property name="text">
|
||||
<string>Remove Selected from Results</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Del</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionIgnoreSelected">
|
||||
<property name="text">
|
||||
<string>Add Selected to Ignore List</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+Del</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionMakeSelectedReference">
|
||||
<property name="text">
|
||||
<string>Make Selected Reference</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Space</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOpenSelected">
|
||||
<property name="text">
|
||||
<string>Open Selected with Default Application</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+O</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionRevealSelected">
|
||||
<property name="text">
|
||||
<string>Open Containing Folder of Selected</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+O</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionRenameSelected">
|
||||
<property name="text">
|
||||
<string>Rename Selected</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F2</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionMarkAll">
|
||||
<property name="text">
|
||||
<string>Mark All</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+A</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionMarkNone">
|
||||
<property name="text">
|
||||
<string>Mark None</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+A</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionInvertMarking">
|
||||
<property name="text">
|
||||
<string>Invert Marking</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Alt+A</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionMarkSelected">
|
||||
<property name="text">
|
||||
<string>Mark Selected</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionClearIgnoreList">
|
||||
<property name="text">
|
||||
<string>Clear Ignore List</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionQuit">
|
||||
<property name="text">
|
||||
<string>Quit</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Q</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionApplyFilter">
|
||||
<property name="text">
|
||||
<string>Apply Filter</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+F</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCancelFilter">
|
||||
<property name="text">
|
||||
<string>Cancel Filter</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+Shift+F</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionShowHelp">
|
||||
<property name="text">
|
||||
<string>dupeGuru Help</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F1</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAbout">
|
||||
<property name="text">
|
||||
<string>About dupeGuru</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionRegister">
|
||||
<property name="text">
|
||||
<string>Register dupeGuru</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionCheckForUpdate">
|
||||
<property name="text">
|
||||
<string>Check for Update</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ResultsView</class>
|
||||
<extends>QTreeView</extends>
|
||||
<header>results_model</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources>
|
||||
<include location="dg.qrc"/>
|
||||
</resources>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>actionDirectories</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>directoriesTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionActions</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>actionsTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionCopyMarked</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>copyTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionDeleteMarked</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>deleteTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionDelta</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>deltaTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionDetails</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>detailsTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionIgnoreSelected</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>addToIgnoreListTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionMakeSelectedReference</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>makeReferenceTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionMoveMarked</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>moveTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionOpenSelected</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>openTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionPowerMarker</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>powerMarkerTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionPreferences</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>preferencesTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionRemoveMarked</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>removeMarkedTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionRemoveSelected</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>removeSelectedTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionRevealSelected</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>revealTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionRenameSelected</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>renameTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionScan</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>scanTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionClearIgnoreList</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>clearIgnoreListTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionMarkAll</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>markAllTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionMarkNone</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>markNoneTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionMarkSelected</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>markSelectedTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionInvertMarking</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>markInvertTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionApplyFilter</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>applyFilterTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionCancelFilter</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>cancelFilterTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionShowHelp</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>showHelpTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionAbout</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>aboutTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionRegister</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>registerTrigerred()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>actionCheckForUpdate</sender>
|
||||
<signal>triggered()</signal>
|
||||
<receiver>MainWindow</receiver>
|
||||
<slot>checkForUpdateTriggered()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>-1</x>
|
||||
<y>-1</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>314</x>
|
||||
<y>256</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<slots>
|
||||
<slot>directoriesTriggered()</slot>
|
||||
<slot>scanTriggered()</slot>
|
||||
<slot>actionsTriggered()</slot>
|
||||
<slot>detailsTriggered()</slot>
|
||||
<slot>preferencesTriggered()</slot>
|
||||
<slot>deltaTriggered()</slot>
|
||||
<slot>powerMarkerTriggered()</slot>
|
||||
<slot>deleteTriggered()</slot>
|
||||
<slot>moveTriggered()</slot>
|
||||
<slot>copyTriggered()</slot>
|
||||
<slot>removeMarkedTriggered()</slot>
|
||||
<slot>removeSelectedTriggered()</slot>
|
||||
<slot>addToIgnoreListTriggered()</slot>
|
||||
<slot>makeReferenceTriggered()</slot>
|
||||
<slot>openTriggered()</slot>
|
||||
<slot>revealTriggered()</slot>
|
||||
<slot>renameTriggered()</slot>
|
||||
<slot>clearIgnoreListTriggered()</slot>
|
||||
<slot>clearPictureCacheTriggered()</slot>
|
||||
<slot>markAllTriggered()</slot>
|
||||
<slot>markNoneTriggered()</slot>
|
||||
<slot>markInvertTriggered()</slot>
|
||||
<slot>markSelectedTriggered()</slot>
|
||||
<slot>applyFilterTriggered()</slot>
|
||||
<slot>cancelFilterTriggered()</slot>
|
||||
<slot>showHelpTriggered()</slot>
|
||||
<slot>aboutTriggered()</slot>
|
||||
<slot>registerTrigerred()</slot>
|
||||
<slot>checkForUpdateTriggered()</slot>
|
||||
</slots>
|
||||
</ui>
|
||||
109
pe/qt/base/preferences.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: preferences
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-03
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import QSettings, QVariant
|
||||
|
||||
from hsutil.misc import tryint
|
||||
|
||||
def variant_to_py(v):
|
||||
value = None
|
||||
ok = False
|
||||
t = v.type()
|
||||
if t == QVariant.String:
|
||||
value = unicode(v.toString())
|
||||
ok = True # anyway
|
||||
# might be bool or int, try them
|
||||
if v == 'true':
|
||||
value = True
|
||||
elif value == 'false':
|
||||
value = False
|
||||
else:
|
||||
value = tryint(value, value)
|
||||
elif t == QVariant.Int:
|
||||
value, ok = v.toInt()
|
||||
elif t == QVariant.Bool:
|
||||
value, ok = v.toBool(), True
|
||||
elif t in (QVariant.List, QVariant.StringList):
|
||||
value, ok = map(variant_to_py, v.toList()), True
|
||||
if not ok:
|
||||
raise TypeError()
|
||||
return value
|
||||
|
||||
class Preferences(object):
|
||||
# (width, is_visible)
|
||||
COLUMNS_DEFAULT_ATTRS = []
|
||||
|
||||
def __init__(self):
|
||||
self.reset()
|
||||
self.reset_columns()
|
||||
|
||||
def _load_specific(self, settings, get):
|
||||
# load prefs specific to the dg edition
|
||||
pass
|
||||
|
||||
def load(self):
|
||||
self.reset()
|
||||
settings = QSettings()
|
||||
def get(name, default):
|
||||
if settings.contains(name):
|
||||
return variant_to_py(settings.value(name))
|
||||
else:
|
||||
return default
|
||||
|
||||
self.filter_hardness = get('FilterHardness', self.filter_hardness)
|
||||
self.mix_file_kind = get('MixFileKind', self.mix_file_kind)
|
||||
self.use_regexp = get('UseRegexp', self.use_regexp)
|
||||
self.remove_empty_folders = get('RemoveEmptyFolders', self.remove_empty_folders)
|
||||
self.destination_type = get('DestinationType', self.destination_type)
|
||||
widths = get('ColumnsWidth', self.columns_width)
|
||||
# only set nonzero values
|
||||
for index, width in enumerate(widths[:len(self.columns_width)]):
|
||||
if width > 0:
|
||||
self.columns_width[index] = width
|
||||
self.columns_visible = get('ColumnsVisible', self.columns_visible)
|
||||
self.registration_code = get('RegistrationCode', self.registration_code)
|
||||
self.registration_email = get('RegistrationEmail', self.registration_email)
|
||||
self._load_specific(settings, get)
|
||||
|
||||
def _reset_specific(self):
|
||||
# reset prefs specific to the dg edition
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
self.filter_hardness = 95
|
||||
self.mix_file_kind = True
|
||||
self.use_regexp = False
|
||||
self.remove_empty_folders = False
|
||||
self.destination_type = 1
|
||||
self.registration_code = ''
|
||||
self.registration_email = ''
|
||||
self._reset_specific()
|
||||
|
||||
def reset_columns(self):
|
||||
self.columns_width = [width for width, _ in self.COLUMNS_DEFAULT_ATTRS]
|
||||
self.columns_visible = [visible for _, visible in self.COLUMNS_DEFAULT_ATTRS]
|
||||
|
||||
def _save_specific(self, settings, set_):
|
||||
# save prefs specific to the dg edition
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
settings = QSettings()
|
||||
def set_(name, value):
|
||||
settings.setValue(name, QVariant(value))
|
||||
|
||||
set_('FilterHardness', self.filter_hardness)
|
||||
set_('MixFileKind', self.mix_file_kind)
|
||||
set_('UseRegexp', self.use_regexp)
|
||||
set_('RemoveEmptyFolders', self.remove_empty_folders)
|
||||
set_('DestinationType', self.destination_type)
|
||||
set_('ColumnsWidth', self.columns_width)
|
||||
set_('ColumnsVisible', self.columns_visible)
|
||||
set_('RegistrationCode', self.registration_code)
|
||||
set_('RegistrationEmail', self.registration_email)
|
||||
self._save_specific(settings, set_)
|
||||
|
||||
34
pe/qt/base/reg.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: reg
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-09
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
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
|
||||
|
||||
45
pe/qt/base/reg_demo_dialog.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: reg_demo_dialog
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-10
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
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)
|
||||
|
||||
140
pe/qt/base/reg_demo_dialog.ui
Normal file
@@ -0,0 +1,140 @@
|
||||
<?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>
|
||||
47
pe/qt/base/reg_submit_dialog.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: reg_submit_dialog
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-09
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
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)
|
||||
|
||||
149
pe/qt/base/reg_submit_dialog.ui
Normal file
@@ -0,0 +1,149 @@
|
||||
<?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>
|
||||
175
pe/qt/base/results_model.py
Normal file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name:
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-04-23
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import SIGNAL, Qt, QAbstractItemModel, QVariant, QModelIndex, QRect
|
||||
from PyQt4.QtGui import QBrush, QStyledItemDelegate, QFont, QTreeView, QColor
|
||||
|
||||
from tree_model import TreeNode, TreeModel
|
||||
|
||||
class ResultNode(TreeNode):
|
||||
def __init__(self, model, parent, row, dupe, group):
|
||||
TreeNode.__init__(self, parent, row)
|
||||
self.model = model
|
||||
self.dupe = dupe
|
||||
self.group = group
|
||||
self._normalData = None
|
||||
self._deltaData = None
|
||||
|
||||
def _get_children(self):
|
||||
children = []
|
||||
if self.dupe is self.group.ref:
|
||||
for index, dupe in enumerate(self.group.dupes):
|
||||
children.append(ResultNode(self.model, self, index, dupe, self.group))
|
||||
return children
|
||||
|
||||
def reset(self):
|
||||
self._normalData = None
|
||||
self._deltaData = None
|
||||
|
||||
@property
|
||||
def normalData(self):
|
||||
if self._normalData is None:
|
||||
self._normalData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=False)
|
||||
return self._normalData
|
||||
|
||||
@property
|
||||
def deltaData(self):
|
||||
if self._deltaData is None:
|
||||
self._deltaData = self.model._data.GetDisplayInfo(self.dupe, self.group, delta=True)
|
||||
return self._deltaData
|
||||
|
||||
|
||||
class ResultsDelegate(QStyledItemDelegate):
|
||||
def initStyleOption(self, option, index):
|
||||
QStyledItemDelegate.initStyleOption(self, option, index)
|
||||
node = index.internalPointer()
|
||||
if node.group.ref is node.dupe:
|
||||
newfont = QFont(option.font)
|
||||
newfont.setBold(True)
|
||||
option.font = newfont
|
||||
|
||||
|
||||
class ResultsModel(TreeModel):
|
||||
def __init__(self, app):
|
||||
self._app = app
|
||||
self._results = app.results
|
||||
self._data = app.data
|
||||
self._delta_columns = app.DELTA_COLUMNS
|
||||
self.delta = False
|
||||
self._power_marker = False
|
||||
TreeModel.__init__(self)
|
||||
|
||||
def _root_nodes(self):
|
||||
nodes = []
|
||||
if self.power_marker:
|
||||
for index, dupe in enumerate(self._results.dupes):
|
||||
group = self._results.get_group_of_duplicate(dupe)
|
||||
nodes.append(ResultNode(self, None, index, dupe, group))
|
||||
else:
|
||||
for index, group in enumerate(self._results.groups):
|
||||
nodes.append(ResultNode(self, None, index, group.ref, group))
|
||||
return nodes
|
||||
|
||||
def columnCount(self, parent):
|
||||
return len(self._data.COLUMNS)
|
||||
|
||||
def data(self, index, role):
|
||||
if not index.isValid():
|
||||
return QVariant()
|
||||
node = index.internalPointer()
|
||||
if role == Qt.DisplayRole:
|
||||
data = node.deltaData if self.delta else node.normalData
|
||||
return QVariant(data[index.column()])
|
||||
elif role == Qt.CheckStateRole:
|
||||
if index.column() == 0 and node.dupe is not node.group.ref:
|
||||
state = Qt.Checked if self._results.is_marked(node.dupe) else Qt.Unchecked
|
||||
return QVariant(state)
|
||||
elif role == Qt.ForegroundRole:
|
||||
if node.dupe is node.group.ref or node.dupe.is_ref:
|
||||
return QVariant(QBrush(Qt.blue))
|
||||
elif self.delta and index.column() in self._delta_columns:
|
||||
return QVariant(QBrush(QColor(255, 142, 40))) # orange
|
||||
elif role == Qt.EditRole:
|
||||
if index.column() == 0:
|
||||
return QVariant(node.normalData[index.column()])
|
||||
return QVariant()
|
||||
|
||||
def dupesForIndexes(self, indexes):
|
||||
nodes = [index.internalPointer() for index in indexes]
|
||||
return [node.dupe for node in nodes]
|
||||
|
||||
def flags(self, index):
|
||||
if not index.isValid():
|
||||
return Qt.ItemIsEnabled
|
||||
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
if index.column() == 0:
|
||||
flags |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable
|
||||
return flags
|
||||
|
||||
def headerData(self, section, orientation, role):
|
||||
if orientation == Qt.Horizontal and role == Qt.DisplayRole and section < len(self._data.COLUMNS):
|
||||
return QVariant(self._data.COLUMNS[section]['display'])
|
||||
|
||||
return QVariant()
|
||||
|
||||
def setData(self, index, value, role):
|
||||
if not index.isValid():
|
||||
return False
|
||||
node = index.internalPointer()
|
||||
if role == Qt.CheckStateRole:
|
||||
if index.column() == 0:
|
||||
self._app.toggle_marking_for_dupes([node.dupe])
|
||||
return True
|
||||
if role == Qt.EditRole:
|
||||
if index.column() == 0:
|
||||
value = unicode(value.toString())
|
||||
if self._app.rename_dupe(node.dupe, value):
|
||||
node.reset()
|
||||
return True
|
||||
return False
|
||||
|
||||
def sort(self, column, order):
|
||||
if self.power_marker:
|
||||
self._results.sort_dupes(column, order == Qt.AscendingOrder, self.delta)
|
||||
else:
|
||||
self._results.sort_groups(column, order == Qt.AscendingOrder)
|
||||
self.reset()
|
||||
|
||||
def toggleMarked(self, indexes):
|
||||
assert indexes
|
||||
dupes = self.dupesForIndexes(indexes)
|
||||
self._app.toggle_marking_for_dupes(dupes)
|
||||
|
||||
#--- Properties
|
||||
@property
|
||||
def power_marker(self):
|
||||
return self._power_marker
|
||||
|
||||
@power_marker.setter
|
||||
def power_marker(self, value):
|
||||
if value == self._power_marker:
|
||||
return
|
||||
self._power_marker = value
|
||||
self.reset()
|
||||
|
||||
|
||||
class ResultsView(QTreeView):
|
||||
#--- Override
|
||||
def keyPressEvent(self, event):
|
||||
if event.text() == ' ':
|
||||
self.model().toggleMarked(self.selectionModel().selectedRows())
|
||||
return
|
||||
QTreeView.keyPressEvent(self, event)
|
||||
|
||||
def setModel(self, model):
|
||||
assert isinstance(model, ResultsModel)
|
||||
QTreeView.setModel(self, model)
|
||||
|
||||
#--- Public
|
||||
def selectedDupes(self):
|
||||
return self.model().dupesForIndexes(self.selectionModel().selectedRows())
|
||||
|
||||
66
pe/qt/base/tree_model.py
Normal file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: tree_model
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-04
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import Qt, QAbstractItemModel, QVariant, QModelIndex
|
||||
|
||||
class TreeNode(object):
|
||||
def __init__(self, parent, row):
|
||||
self.parent = parent
|
||||
self.row = row
|
||||
self._children = None
|
||||
|
||||
def _get_children(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
if self._children is None:
|
||||
self._children = self._get_children()
|
||||
return self._children
|
||||
|
||||
|
||||
class TreeModel(QAbstractItemModel):
|
||||
def __init__(self):
|
||||
QAbstractItemModel.__init__(self)
|
||||
self._nodes = None
|
||||
|
||||
def _root_nodes(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def index(self, row, column, parent):
|
||||
if not self.nodes:
|
||||
return QModelIndex()
|
||||
if not parent.isValid():
|
||||
return self.createIndex(row, column, self.nodes[row])
|
||||
node = parent.internalPointer()
|
||||
return self.createIndex(row, column, node.children[row])
|
||||
|
||||
def parent(self, index):
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
node = index.internalPointer()
|
||||
if node.parent is None:
|
||||
return QModelIndex()
|
||||
else:
|
||||
return self.createIndex(node.parent.row, 0, node.parent)
|
||||
|
||||
def reset(self):
|
||||
self._nodes = None
|
||||
QAbstractItemModel.reset(self)
|
||||
|
||||
def rowCount(self, parent):
|
||||
if not parent.isValid():
|
||||
return len(self.nodes)
|
||||
node = parent.internalPointer()
|
||||
return len(node.children)
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
if self._nodes is None:
|
||||
self._nodes = self._root_nodes()
|
||||
return self._nodes
|
||||
|
||||
41
pe/qt/block.py
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: block
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-10
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from _block import getblocks
|
||||
|
||||
# Converted to Cython
|
||||
# def getblock(image):
|
||||
# width = image.width()
|
||||
# height = image.height()
|
||||
# if width:
|
||||
# pixel_count = width * height
|
||||
# red = green = blue = 0
|
||||
# s = image.bits().asstring(image.numBytes())
|
||||
# for i in xrange(pixel_count):
|
||||
# offset = i * 3
|
||||
# red += ord(s[offset])
|
||||
# green += ord(s[offset + 1])
|
||||
# blue += ord(s[offset + 2])
|
||||
# return (red // pixel_count, green // pixel_count, blue // pixel_count)
|
||||
# else:
|
||||
# return (0, 0, 0)
|
||||
#
|
||||
# def getblocks(image, block_count_per_side):
|
||||
# width = image.width()
|
||||
# height = image.height()
|
||||
# if not width:
|
||||
# return []
|
||||
# block_width = max(width // block_count_per_side, 1)
|
||||
# block_height = max(height // block_count_per_side, 1)
|
||||
# result = []
|
||||
# for ih in xrange(block_count_per_side):
|
||||
# top = min(ih * block_height, height - block_height)
|
||||
# for iw in range(block_count_per_side):
|
||||
# left = min(iw * block_width, width - block_width)
|
||||
# crop = image.copy(left, top, block_width, block_height)
|
||||
# result.append(getblock(crop))
|
||||
# return result
|
||||
40
pe/qt/build.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: build
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-22
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
# On Windows, PyInstaller is used to build an exe (py2exe creates a very bad looking icon
|
||||
# The release version is outdated. Use at least r672 on http://svn.pyinstaller.org/trunk
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from app import DupeGuru
|
||||
|
||||
def print_and_do(cmd):
|
||||
print cmd
|
||||
os.system(cmd)
|
||||
|
||||
# Removing build and dist
|
||||
shutil.rmtree('build')
|
||||
shutil.rmtree('dist')
|
||||
|
||||
version = DupeGuru.VERSION
|
||||
versioncomma = version.replace('.', ', ') + ', 0'
|
||||
verinfo = open('verinfo').read()
|
||||
verinfo = verinfo.replace('$versioncomma', versioncomma).replace('$version', version)
|
||||
fp = open('verinfo_tmp', 'w')
|
||||
fp.write(verinfo)
|
||||
fp.close()
|
||||
print_and_do("python C:\\Python26\\pyinstaller\\Build.py dgpe.spec")
|
||||
os.remove('verinfo_tmp')
|
||||
|
||||
print_and_do("xcopy /Y C:\\src\\vs_comp\\msvcrt dist")
|
||||
print_and_do("xcopy /Y /S /I help\\dupeguru_pe_help dist\\help")
|
||||
|
||||
aicom = '"\\Program Files\\Caphyon\\Advanced Installer\\AdvancedInstaller.com"'
|
||||
shutil.copy('installer.aip', 'installer_tmp.aip') # this is so we don'a have to re-commit installer.aip at every version change
|
||||
print_and_do('%s /edit installer_tmp.aip /SetVersion %s' % (aicom, version))
|
||||
print_and_do('%s /build installer_tmp.aip -force' % aicom)
|
||||
os.remove('installer_tmp.aip')
|
||||
61
pe/qt/details_dialog.py
Normal file
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: details_dialog
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-04-27
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
from PyQt4.QtCore import Qt, SIGNAL, QAbstractTableModel, QVariant
|
||||
from PyQt4.QtGui import QDialog, QHeaderView, QPixmap
|
||||
|
||||
from base.details_table import DetailsModel
|
||||
from details_dialog_ui import Ui_DetailsDialog
|
||||
|
||||
class DetailsDialog(QDialog, Ui_DetailsDialog):
|
||||
def __init__(self, parent, app):
|
||||
QDialog.__init__(self, parent, Qt.Tool)
|
||||
self.app = app
|
||||
self.selectedPixmap = None
|
||||
self.referencePixmap = None
|
||||
self.setupUi(self)
|
||||
self.model = DetailsModel(app)
|
||||
self.tableView.setModel(self.model)
|
||||
self.connect(app, SIGNAL('duplicateSelected()'), self.duplicateSelected)
|
||||
|
||||
def _update(self):
|
||||
dupe = self.app.selected_dupe
|
||||
if dupe is None:
|
||||
return
|
||||
group = self.app.results.get_group_of_duplicate(dupe)
|
||||
ref = group.ref
|
||||
|
||||
self.selectedPixmap = QPixmap(unicode(dupe.path))
|
||||
if ref is dupe:
|
||||
self.referencePixmap = self.selectedPixmap
|
||||
else:
|
||||
self.referencePixmap = QPixmap(unicode(ref.path))
|
||||
self._updateImages()
|
||||
|
||||
def _updateImages(self):
|
||||
if self.selectedPixmap is not None:
|
||||
target_size = self.selectedImage.size()
|
||||
scaledPixmap = self.selectedPixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
self.selectedImage.setPixmap(scaledPixmap)
|
||||
if self.referencePixmap is not None:
|
||||
target_size = self.referenceImage.size()
|
||||
scaledPixmap = self.referencePixmap.scaled(target_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
self.referenceImage.setPixmap(scaledPixmap)
|
||||
|
||||
#--- Override
|
||||
def resizeEvent(self, event):
|
||||
self._updateImages()
|
||||
|
||||
def show(self):
|
||||
QDialog.show(self)
|
||||
self._update()
|
||||
|
||||
#--- Events
|
||||
def duplicateSelected(self):
|
||||
if self.isVisible():
|
||||
self._update()
|
||||
|
||||
113
pe/qt/details_dialog.ui
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DetailsDialog</class>
|
||||
<widget class="QDialog" name="DetailsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>502</width>
|
||||
<height>295</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<height>250</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Details</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="margin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="referenceImage">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="selectedImage">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Ignored" vsizetype="Ignored">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="DetailsTable" name="tableView">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>188</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>188</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="showGrid">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>DetailsTable</class>
|
||||
<extends>QTableView</extends>
|
||||
<header>base.details_table</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
19
pe/qt/dgpe.spec
Normal file
@@ -0,0 +1,19 @@
|
||||
# -*- mode: python -*-
|
||||
a = Analysis([os.path.join(HOMEPATH,'support\\_mountzlib.py'), os.path.join(HOMEPATH,'support\\useUnicode.py'), 'start.py'],
|
||||
pathex=['C:\\src\\dupeguru\\pe\\qt'])
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
exclude_binaries=1,
|
||||
name=os.path.join('build\\pyi.win32\\dupeGuru PE', 'dupeGuru PE.exe'),
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False , icon='base\\images\\dgpe_logo.ico', version='verinfo_tmp')
|
||||
coll = COLLECT( exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
name=os.path.join('dist'))
|
||||
1
pe/qt/dupeguru/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
229
pe/qt/dupeguru/app.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Unit Name: dupeguru.app
|
||||
Created By: Virgil Dupras
|
||||
Created On: 2006/11/11
|
||||
Last modified by:$Author: virgil $
|
||||
Last modified on:$Date: 2009-05-28 16:02:48 +0200 (Thu, 28 May 2009) $
|
||||
$Revision: 4388 $
|
||||
Copyright 2006 Hardcoded Software (http://www.hardcoded.net)
|
||||
"""
|
||||
import os
|
||||
import os.path as op
|
||||
import logging
|
||||
|
||||
from hsfs import IT_ATTRS, IT_EXTRA
|
||||
from hsutil import job, io, files
|
||||
from hsutil.path import Path
|
||||
from hsutil.reg import RegistrableApplication, RegistrationRequired
|
||||
from hsutil.misc import flatten, first
|
||||
from hsutil.str import escape
|
||||
|
||||
import directories
|
||||
import results
|
||||
import scanner
|
||||
|
||||
JOB_SCAN = 'job_scan'
|
||||
JOB_LOAD = 'job_load'
|
||||
JOB_MOVE = 'job_move'
|
||||
JOB_COPY = 'job_copy'
|
||||
JOB_DELETE = 'job_delete'
|
||||
|
||||
class NoScannableFileError(Exception):
|
||||
pass
|
||||
|
||||
class AllFilesAreRefError(Exception):
|
||||
pass
|
||||
|
||||
class DupeGuru(RegistrableApplication):
|
||||
def __init__(self, data_module, appdata, appid):
|
||||
RegistrableApplication.__init__(self, appid)
|
||||
self.appdata = appdata
|
||||
if not op.exists(self.appdata):
|
||||
os.makedirs(self.appdata)
|
||||
self.data = data_module
|
||||
self.directories = directories.Directories()
|
||||
self.results = results.Results(data_module)
|
||||
self.scanner = scanner.Scanner()
|
||||
self.action_count = 0
|
||||
self.last_op_error_count = 0
|
||||
self.options = {
|
||||
'escape_filter_regexp': True,
|
||||
'clean_empty_dirs': False,
|
||||
}
|
||||
|
||||
def _demo_check(self):
|
||||
if self.registered:
|
||||
return
|
||||
count = self.results.mark_count
|
||||
if count + self.action_count > 10:
|
||||
raise RegistrationRequired()
|
||||
else:
|
||||
self.action_count += count
|
||||
|
||||
def _do_delete(self, j):
|
||||
def op(dupe):
|
||||
j.add_progress()
|
||||
return self._do_delete_dupe(dupe)
|
||||
|
||||
j.start_job(self.results.mark_count)
|
||||
self.last_op_error_count = self.results.perform_on_marked(op, True)
|
||||
|
||||
def _do_delete_dupe(self, dupe):
|
||||
if not io.exists(dupe.path):
|
||||
dupe.parent = None
|
||||
return True
|
||||
self._recycle_dupe(dupe)
|
||||
self.clean_empty_dirs(dupe.path[:-1])
|
||||
if not io.exists(dupe.path):
|
||||
dupe.parent = None
|
||||
return True
|
||||
logging.warning(u"Could not send {0} to trash.".format(unicode(dupe.path)))
|
||||
return False
|
||||
|
||||
def _do_load(self, j):
|
||||
self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml'))
|
||||
j = j.start_subjob([1, 9])
|
||||
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
|
||||
files = flatten(g[:] for g in self.results.groups)
|
||||
for file in j.iter_with_progress(files, 'Reading metadata %d/%d'):
|
||||
file._read_all_info(sections=[IT_ATTRS, IT_EXTRA])
|
||||
|
||||
def _get_file(self, str_path):
|
||||
p = Path(str_path)
|
||||
for d in self.directories:
|
||||
if p not in d.path:
|
||||
continue
|
||||
result = d.find_path(p[d.path:])
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _recycle_dupe(dupe):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _start_job(self, jobid, func):
|
||||
# func(j)
|
||||
raise NotImplementedError()
|
||||
|
||||
def AddDirectory(self, d):
|
||||
try:
|
||||
self.directories.add_path(Path(d))
|
||||
return 0
|
||||
except directories.AlreadyThereError:
|
||||
return 1
|
||||
except directories.InvalidPathError:
|
||||
return 2
|
||||
|
||||
def AddToIgnoreList(self, dupe):
|
||||
g = self.results.get_group_of_duplicate(dupe)
|
||||
for other in g:
|
||||
if other is not dupe:
|
||||
self.scanner.ignore_list.Ignore(unicode(other.path), unicode(dupe.path))
|
||||
|
||||
def ApplyFilter(self, filter):
|
||||
self.results.apply_filter(None)
|
||||
if self.options['escape_filter_regexp']:
|
||||
filter = escape(filter, '()[]\\.|+?^')
|
||||
filter = escape(filter, '*', '.')
|
||||
self.results.apply_filter(filter)
|
||||
|
||||
def clean_empty_dirs(self, path):
|
||||
if self.options['clean_empty_dirs']:
|
||||
while files.delete_if_empty(path, ['.DS_Store']):
|
||||
path = path[:-1]
|
||||
|
||||
def CopyOrMove(self, dupe, copy, destination, dest_type):
|
||||
"""
|
||||
copy: True = Copy False = Move
|
||||
destination: string.
|
||||
dest_type: 0 = right in destination.
|
||||
1 = relative re-creation.
|
||||
2 = absolute re-creation.
|
||||
"""
|
||||
source_path = dupe.path
|
||||
location_path = dupe.root.path
|
||||
dest_path = Path(destination)
|
||||
if dest_type == 2:
|
||||
dest_path = dest_path + source_path[1:-1] #Remove drive letter and filename
|
||||
elif dest_type == 1:
|
||||
dest_path = dest_path + source_path[location_path:-1]
|
||||
if not io.exists(dest_path):
|
||||
io.makedirs(dest_path)
|
||||
try:
|
||||
if copy:
|
||||
files.copy(source_path, dest_path)
|
||||
else:
|
||||
files.move(source_path, dest_path)
|
||||
self.clean_empty_dirs(source_path[:-1])
|
||||
except (IOError, OSError) as e:
|
||||
operation = 'Copy' if copy else 'Move'
|
||||
logging.warning('%s operation failed on %s. Error: %s' % (operation, unicode(dupe.path), unicode(e)))
|
||||
return False
|
||||
return True
|
||||
|
||||
def copy_or_move_marked(self, copy, destination, recreate_path):
|
||||
def do(j):
|
||||
def op(dupe):
|
||||
j.add_progress()
|
||||
return self.CopyOrMove(dupe, copy, destination, recreate_path)
|
||||
|
||||
j.start_job(self.results.mark_count)
|
||||
self.last_op_error_count = self.results.perform_on_marked(op, not copy)
|
||||
|
||||
self._demo_check()
|
||||
jobid = JOB_COPY if copy else JOB_MOVE
|
||||
self._start_job(jobid, do)
|
||||
|
||||
def delete_marked(self):
|
||||
self._demo_check()
|
||||
self._start_job(JOB_DELETE, self._do_delete)
|
||||
|
||||
def load(self):
|
||||
self._start_job(JOB_LOAD, self._do_load)
|
||||
self.LoadIgnoreList()
|
||||
|
||||
def LoadIgnoreList(self):
|
||||
p = op.join(self.appdata, 'ignore_list.xml')
|
||||
self.scanner.ignore_list.load_from_xml(p)
|
||||
|
||||
def make_reference(self, duplicates):
|
||||
changed_groups = set()
|
||||
for dupe in duplicates:
|
||||
g = self.results.get_group_of_duplicate(dupe)
|
||||
if g not in changed_groups:
|
||||
self.results.make_ref(dupe)
|
||||
changed_groups.add(g)
|
||||
|
||||
def Save(self):
|
||||
self.directories.SaveToFile(op.join(self.appdata, 'last_directories.xml'))
|
||||
self.results.save_to_xml(op.join(self.appdata, 'last_results.xml'))
|
||||
|
||||
def SaveIgnoreList(self):
|
||||
p = op.join(self.appdata, 'ignore_list.xml')
|
||||
self.scanner.ignore_list.save_to_xml(p)
|
||||
|
||||
def start_scanning(self):
|
||||
def do(j):
|
||||
j.set_progress(0, 'Collecting files to scan')
|
||||
files = list(self.directories.get_files())
|
||||
logging.info('Scanning %d files' % len(files))
|
||||
self.results.groups = self.scanner.GetDupeGroups(files, j)
|
||||
|
||||
files = self.directories.get_files()
|
||||
first_file = first(files)
|
||||
if first_file is None:
|
||||
raise NoScannableFileError()
|
||||
if first_file.is_ref and all(f.is_ref for f in files):
|
||||
raise AllFilesAreRefError()
|
||||
self.results.groups = []
|
||||
self._start_job(JOB_SCAN, do)
|
||||
|
||||
#--- Properties
|
||||
@property
|
||||
def stat_line(self):
|
||||
result = self.results.stat_line
|
||||
if self.scanner.discarded_file_count:
|
||||
result = '%s (%d discarded)' % (result, self.scanner.discarded_file_count)
|
||||
return result
|
||||
|
||||
304
pe/qt/dupeguru/app_cocoa.py
Normal file
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Unit Name: dupeguru.app_cocoa
|
||||
Created By: Virgil Dupras
|
||||
Created On: 2006/11/11
|
||||
Last modified by:$Author: virgil $
|
||||
Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $
|
||||
$Revision: 4392 $
|
||||
Copyright 2006 Hardcoded Software (http://www.hardcoded.net)
|
||||
"""
|
||||
from AppKit import *
|
||||
import logging
|
||||
import os.path as op
|
||||
|
||||
import hsfs as fs
|
||||
from hsfs.phys.bundle import Bundle
|
||||
from hsutil.cocoa import install_exception_hook
|
||||
from hsutil.str import get_file_ext
|
||||
from hsutil import io, cocoa, job
|
||||
from hsutil.reg import RegistrationRequired
|
||||
|
||||
import export, app, data
|
||||
|
||||
JOBID2TITLE = {
|
||||
app.JOB_SCAN: "Scanning for duplicates",
|
||||
app.JOB_LOAD: "Loading",
|
||||
app.JOB_MOVE: "Moving",
|
||||
app.JOB_COPY: "Copying",
|
||||
app.JOB_DELETE: "Sending to Trash",
|
||||
}
|
||||
|
||||
class DGDirectory(fs.phys.Directory):
|
||||
def _create_sub_dir(self,name,with_parent = True):
|
||||
ext = get_file_ext(name)
|
||||
if ext == 'app':
|
||||
if with_parent:
|
||||
parent = self
|
||||
else:
|
||||
parent = None
|
||||
return Bundle(parent,name)
|
||||
else:
|
||||
return super(DGDirectory,self)._create_sub_dir(name,with_parent)
|
||||
|
||||
|
||||
def demo_method(method):
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
return method(self, *args, **kwargs)
|
||||
except RegistrationRequired:
|
||||
NSNotificationCenter.defaultCenter().postNotificationName_object_('RegistrationRequired', self)
|
||||
|
||||
return wrapper
|
||||
|
||||
class DupeGuru(app.DupeGuru):
|
||||
def __init__(self, data_module, appdata_subdir, appid):
|
||||
LOGGING_LEVEL = logging.DEBUG if NSUserDefaults.standardUserDefaults().boolForKey_('debug') else logging.WARNING
|
||||
logging.basicConfig(level=LOGGING_LEVEL, format='%(levelname)s %(message)s')
|
||||
logging.debug('started in debug mode')
|
||||
install_exception_hook()
|
||||
if data_module is None:
|
||||
data_module = data
|
||||
appdata = op.expanduser(op.join('~', '.hsoftdata', appdata_subdir))
|
||||
app.DupeGuru.__init__(self, data_module, appdata, appid)
|
||||
self.progress = cocoa.ThreadedJobPerformer()
|
||||
self.directories.dirclass = DGDirectory
|
||||
self.display_delta_values = False
|
||||
self.selected_dupes = []
|
||||
self.RefreshDetailsTable(None,None)
|
||||
|
||||
#--- Override
|
||||
@staticmethod
|
||||
def _recycle_dupe(dupe):
|
||||
if not io.exists(dupe.path):
|
||||
dupe.parent = None
|
||||
return True
|
||||
directory = unicode(dupe.parent.path)
|
||||
filename = dupe.name
|
||||
result, tag = NSWorkspace.sharedWorkspace().performFileOperation_source_destination_files_tag_(
|
||||
NSWorkspaceRecycleOperation, directory, '', [filename])
|
||||
if not io.exists(dupe.path):
|
||||
dupe.parent = None
|
||||
return True
|
||||
logging.warning('Could not send %s to trash. tag: %d' % (unicode(dupe.path), tag))
|
||||
return False
|
||||
|
||||
def _start_job(self, jobid, func):
|
||||
try:
|
||||
j = self.progress.create_job()
|
||||
self.progress.run_threaded(func, args=(j, ))
|
||||
except job.JobInProgressError:
|
||||
NSNotificationCenter.defaultCenter().postNotificationName_object_('JobInProgress', self)
|
||||
else:
|
||||
ud = {'desc': JOBID2TITLE[jobid], 'jobid':jobid}
|
||||
NSNotificationCenter.defaultCenter().postNotificationName_object_userInfo_('JobStarted', self, ud)
|
||||
|
||||
#---Helpers
|
||||
def GetObjects(self,node_path):
|
||||
#returns a tuple g,d
|
||||
try:
|
||||
g = self.results.groups[node_path[0]]
|
||||
if len(node_path) == 2:
|
||||
return (g,self.results.groups[node_path[0]].dupes[node_path[1]])
|
||||
else:
|
||||
return (g,None)
|
||||
except IndexError:
|
||||
return (None,None)
|
||||
|
||||
def GetDirectory(self,node_path,curr_dir=None):
|
||||
if not node_path:
|
||||
return curr_dir
|
||||
if curr_dir is not None:
|
||||
l = curr_dir.dirs
|
||||
else:
|
||||
l = self.directories
|
||||
d = l[node_path[0]]
|
||||
return self.GetDirectory(node_path[1:],d)
|
||||
|
||||
def RefreshDetailsTable(self,dupe,group):
|
||||
l1 = self.data.GetDisplayInfo(dupe,group,False)
|
||||
if group is not None:
|
||||
l2 = self.data.GetDisplayInfo(group.ref,group,False)
|
||||
else:
|
||||
l2 = l1 #To have a list of empty '---' values
|
||||
names = [c['display'] for c in self.data.COLUMNS]
|
||||
self.details_table = zip(names,l1,l2)
|
||||
|
||||
#---Public
|
||||
def AddSelectedToIgnoreList(self):
|
||||
for dupe in self.selected_dupes:
|
||||
self.AddToIgnoreList(dupe)
|
||||
|
||||
copy_or_move_marked = demo_method(app.DupeGuru.copy_or_move_marked)
|
||||
delete_marked = demo_method(app.DupeGuru.delete_marked)
|
||||
|
||||
def ExportToXHTML(self,column_ids,xslt_path,css_path):
|
||||
columns = []
|
||||
for index,column in enumerate(self.data.COLUMNS):
|
||||
display = column['display']
|
||||
enabled = str(index) in column_ids
|
||||
columns.append((display,enabled))
|
||||
xml_path = op.join(self.appdata,'results_export.xml')
|
||||
self.results.save_to_xml(xml_path,self.data.GetDisplayInfo)
|
||||
return export.export_to_xhtml(xml_path,xslt_path,css_path,columns)
|
||||
|
||||
def MakeSelectedReference(self):
|
||||
self.make_reference(self.selected_dupes)
|
||||
|
||||
def OpenSelected(self):
|
||||
if self.selected_dupes:
|
||||
path = unicode(self.selected_dupes[0].path)
|
||||
NSWorkspace.sharedWorkspace().openFile_(path)
|
||||
|
||||
def PurgeIgnoreList(self):
|
||||
self.scanner.ignore_list.Filter(lambda f,s:op.exists(f) and op.exists(s))
|
||||
|
||||
def RefreshDetailsWithSelected(self):
|
||||
if self.selected_dupes:
|
||||
self.RefreshDetailsTable(
|
||||
self.selected_dupes[0],
|
||||
self.results.get_group_of_duplicate(self.selected_dupes[0])
|
||||
)
|
||||
else:
|
||||
self.RefreshDetailsTable(None,None)
|
||||
|
||||
def RemoveDirectory(self,index):
|
||||
try:
|
||||
del self.directories[index]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def RemoveSelected(self):
|
||||
self.results.remove_duplicates(self.selected_dupes)
|
||||
|
||||
def RenameSelected(self,newname):
|
||||
try:
|
||||
d = self.selected_dupes[0]
|
||||
d = d.move(d.parent,newname)
|
||||
return True
|
||||
except (IndexError,fs.FSError),e:
|
||||
logging.warning("dupeGuru Warning: %s" % str(e))
|
||||
return False
|
||||
|
||||
def RevealSelected(self):
|
||||
if self.selected_dupes:
|
||||
path = unicode(self.selected_dupes[0].path)
|
||||
NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(path,'')
|
||||
|
||||
def start_scanning(self):
|
||||
self.RefreshDetailsTable(None, None)
|
||||
try:
|
||||
app.DupeGuru.start_scanning(self)
|
||||
return 0
|
||||
except app.NoScannableFileError:
|
||||
return 3
|
||||
except app.AllFilesAreRefError:
|
||||
return 1
|
||||
|
||||
def SelectResultNodePaths(self,node_paths):
|
||||
def extract_dupe(t):
|
||||
g,d = t
|
||||
if d is not None:
|
||||
return d
|
||||
else:
|
||||
if g is not None:
|
||||
return g.ref
|
||||
|
||||
selected = [extract_dupe(self.GetObjects(p)) for p in node_paths]
|
||||
self.selected_dupes = [dupe for dupe in selected if dupe is not None]
|
||||
|
||||
def SelectPowerMarkerNodePaths(self,node_paths):
|
||||
rows = [p[0] for p in node_paths]
|
||||
self.selected_dupes = [
|
||||
self.results.dupes[row] for row in rows if row in xrange(len(self.results.dupes))
|
||||
]
|
||||
|
||||
def SetDirectoryState(self,node_path,state):
|
||||
d = self.GetDirectory(node_path)
|
||||
self.directories.SetState(d.path,state)
|
||||
|
||||
def sort_dupes(self,key,asc):
|
||||
self.results.sort_dupes(key,asc,self.display_delta_values)
|
||||
|
||||
def sort_groups(self,key,asc):
|
||||
self.results.sort_groups(key,asc)
|
||||
|
||||
def ToggleSelectedMarkState(self):
|
||||
for dupe in self.selected_dupes:
|
||||
self.results.mark_toggle(dupe)
|
||||
|
||||
#---Data
|
||||
def GetOutlineViewMaxLevel(self, tag):
|
||||
if tag == 0:
|
||||
return 2
|
||||
elif tag == 1:
|
||||
return 0
|
||||
elif tag == 2:
|
||||
return 1
|
||||
|
||||
def GetOutlineViewChildCounts(self, tag, node_path):
|
||||
if self.progress._job_running:
|
||||
return []
|
||||
if tag == 0: #Normal results
|
||||
assert not node_path # no other value is possible
|
||||
return [len(g.dupes) for g in self.results.groups]
|
||||
elif tag == 1: #Directories
|
||||
dirs = self.GetDirectory(node_path).dirs if node_path else self.directories
|
||||
return [d.dircount for d in dirs]
|
||||
else: #Power Marker
|
||||
assert not node_path # no other value is possible
|
||||
return [0 for d in self.results.dupes]
|
||||
|
||||
def GetOutlineViewValues(self, tag, node_path):
|
||||
if self.progress._job_running:
|
||||
return
|
||||
if not node_path:
|
||||
return
|
||||
if tag in (0,2): #Normal results / Power Marker
|
||||
if tag == 0:
|
||||
g, d = self.GetObjects(node_path)
|
||||
if d is None:
|
||||
d = g.ref
|
||||
else:
|
||||
d = self.results.dupes[node_path[0]]
|
||||
g = self.results.get_group_of_duplicate(d)
|
||||
result = self.data.GetDisplayInfo(d, g, self.display_delta_values)
|
||||
return result
|
||||
elif tag == 1: #Directories
|
||||
d = self.GetDirectory(node_path)
|
||||
return [
|
||||
d.name,
|
||||
self.directories.GetState(d.path)
|
||||
]
|
||||
|
||||
def GetOutlineViewMarked(self, tag, node_path):
|
||||
# 0=unmarked 1=marked 2=unmarkable
|
||||
if self.progress._job_running:
|
||||
return
|
||||
if not node_path:
|
||||
return 2
|
||||
if tag == 1: #Directories
|
||||
return 2
|
||||
if tag == 0: #Normal results
|
||||
g, d = self.GetObjects(node_path)
|
||||
else: #Power Marker
|
||||
d = self.results.dupes[node_path[0]]
|
||||
if (d is None) or (not self.results.is_markable(d)):
|
||||
return 2
|
||||
elif self.results.is_marked(d):
|
||||
return 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def GetTableViewCount(self, tag):
|
||||
if self.progress._job_running:
|
||||
return 0
|
||||
return len(self.details_table)
|
||||
|
||||
def GetTableViewMarkedIndexes(self,tag):
|
||||
return []
|
||||
|
||||
def GetTableViewValues(self,tag,row):
|
||||
return self.details_table[row]
|
||||
|
||||
|
||||
320
pe/qt/dupeguru/app_cocoa_test.py
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Unit Name: dupeguru.tests.app_cocoa
|
||||
Created By: Virgil Dupras
|
||||
Created On: 2006/11/11
|
||||
Last modified by:$Author: virgil $
|
||||
Last modified on:$Date: 2009-05-29 17:51:41 +0200 (Fri, 29 May 2009) $
|
||||
$Revision: 4409 $
|
||||
Copyright 2006 Hardcoded Software (http://www.hardcoded.net)
|
||||
"""
|
||||
import tempfile
|
||||
import shutil
|
||||
import logging
|
||||
|
||||
from hsutil.path import Path
|
||||
from hsutil.testcase import TestCase
|
||||
from hsutil.decorators import log_calls
|
||||
import hsfs.phys
|
||||
import os.path as op
|
||||
|
||||
from . import engine, data
|
||||
try:
|
||||
from .app_cocoa import DupeGuru as DupeGuruBase, DGDirectory
|
||||
except ImportError:
|
||||
from nose.plugins.skip import SkipTest
|
||||
raise SkipTest("These tests can only be run on OS X")
|
||||
from .results_test import GetTestGroups
|
||||
|
||||
class DupeGuru(DupeGuruBase):
|
||||
def __init__(self):
|
||||
DupeGuruBase.__init__(self, data, '/tmp', appid=4)
|
||||
|
||||
def _start_job(self, jobid, func):
|
||||
func(nulljob)
|
||||
|
||||
|
||||
def r2np(rows):
|
||||
#Transforms a list of rows [1,2,3] into a list of node paths [[1],[2],[3]]
|
||||
return [[i] for i in rows]
|
||||
|
||||
class TCDupeGuru(TestCase):
|
||||
def setUp(self):
|
||||
self.app = DupeGuru()
|
||||
self.objects,self.matches,self.groups = GetTestGroups()
|
||||
self.app.results.groups = self.groups
|
||||
|
||||
def test_GetObjects(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups
|
||||
g,d = app.GetObjects([0])
|
||||
self.assert_(g is groups[0])
|
||||
self.assert_(d is None)
|
||||
g,d = app.GetObjects([0,0])
|
||||
self.assert_(g is groups[0])
|
||||
self.assert_(d is objects[1])
|
||||
g,d = app.GetObjects([1,0])
|
||||
self.assert_(g is groups[1])
|
||||
self.assert_(d is objects[4])
|
||||
|
||||
def test_GetObjects_after_sort(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups[:] #To keep the old order in memory
|
||||
app.sort_groups(0,False) #0 = Filename
|
||||
#Now, the group order is supposed to be reversed
|
||||
g,d = app.GetObjects([0,0])
|
||||
self.assert_(g is groups[1])
|
||||
self.assert_(d is objects[4])
|
||||
|
||||
def test_GetObjects_out_of_range(self):
|
||||
app = self.app
|
||||
self.assertEqual((None,None),app.GetObjects([2]))
|
||||
self.assertEqual((None,None),app.GetObjects([]))
|
||||
self.assertEqual((None,None),app.GetObjects([1,2]))
|
||||
|
||||
def test_selectResultNodePaths(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
app.SelectResultNodePaths([[0,0],[0,1]])
|
||||
self.assertEqual(2,len(app.selected_dupes))
|
||||
self.assert_(app.selected_dupes[0] is objects[1])
|
||||
self.assert_(app.selected_dupes[1] is objects[2])
|
||||
|
||||
def test_selectResultNodePaths_with_ref(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
app.SelectResultNodePaths([[0,0],[0,1],[1]])
|
||||
self.assertEqual(3,len(app.selected_dupes))
|
||||
self.assert_(app.selected_dupes[0] is objects[1])
|
||||
self.assert_(app.selected_dupes[1] is objects[2])
|
||||
self.assert_(app.selected_dupes[2] is self.groups[1].ref)
|
||||
|
||||
def test_selectResultNodePaths_empty(self):
|
||||
self.app.SelectResultNodePaths([])
|
||||
self.assertEqual(0,len(self.app.selected_dupes))
|
||||
|
||||
def test_selectResultNodePaths_after_sort(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups[:] #To keep the old order in memory
|
||||
app.sort_groups(0,False) #0 = Filename
|
||||
#Now, the group order is supposed to be reversed
|
||||
app.SelectResultNodePaths([[0,0],[1],[1,0]])
|
||||
self.assertEqual(3,len(app.selected_dupes))
|
||||
self.assert_(app.selected_dupes[0] is objects[4])
|
||||
self.assert_(app.selected_dupes[1] is groups[0].ref)
|
||||
self.assert_(app.selected_dupes[2] is objects[1])
|
||||
|
||||
def test_selectResultNodePaths_out_of_range(self):
|
||||
app = self.app
|
||||
app.SelectResultNodePaths([[0,0],[0,1],[1],[1,1],[2]])
|
||||
self.assertEqual(3,len(app.selected_dupes))
|
||||
|
||||
def test_selectPowerMarkerRows(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,1,2]))
|
||||
self.assertEqual(3,len(app.selected_dupes))
|
||||
self.assert_(app.selected_dupes[0] is objects[1])
|
||||
self.assert_(app.selected_dupes[1] is objects[2])
|
||||
self.assert_(app.selected_dupes[2] is objects[4])
|
||||
|
||||
def test_selectPowerMarkerRows_empty(self):
|
||||
self.app.SelectPowerMarkerNodePaths([])
|
||||
self.assertEqual(0,len(self.app.selected_dupes))
|
||||
|
||||
def test_selectPowerMarkerRows_after_sort(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
app.sort_dupes(0,False) #0 = Filename
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,1,2]))
|
||||
self.assertEqual(3,len(app.selected_dupes))
|
||||
self.assert_(app.selected_dupes[0] is objects[4])
|
||||
self.assert_(app.selected_dupes[1] is objects[2])
|
||||
self.assert_(app.selected_dupes[2] is objects[1])
|
||||
|
||||
def test_selectPowerMarkerRows_out_of_range(self):
|
||||
app = self.app
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,1,2,3]))
|
||||
self.assertEqual(3,len(app.selected_dupes))
|
||||
|
||||
def test_toggleSelectedMark(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
app.ToggleSelectedMarkState()
|
||||
self.assertEqual(0,app.results.mark_count)
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,2]))
|
||||
app.ToggleSelectedMarkState()
|
||||
self.assertEqual(2,app.results.mark_count)
|
||||
self.assert_(not app.results.is_marked(objects[0]))
|
||||
self.assert_(app.results.is_marked(objects[1]))
|
||||
self.assert_(not app.results.is_marked(objects[2]))
|
||||
self.assert_(not app.results.is_marked(objects[3]))
|
||||
self.assert_(app.results.is_marked(objects[4]))
|
||||
|
||||
def test_refreshDetailsWithSelected(self):
|
||||
def mock_refresh(dupe,group):
|
||||
self.called = True
|
||||
if self.app.selected_dupes:
|
||||
self.assert_(dupe is self.app.selected_dupes[0])
|
||||
self.assert_(group is self.app.results.get_group_of_duplicate(dupe))
|
||||
else:
|
||||
self.assert_(dupe is None)
|
||||
self.assert_(group is None)
|
||||
|
||||
self.app.RefreshDetailsTable = mock_refresh
|
||||
self.called = False
|
||||
self.app.SelectPowerMarkerNodePaths(r2np([0,2]))
|
||||
self.app.RefreshDetailsWithSelected()
|
||||
self.assert_(self.called)
|
||||
self.called = False
|
||||
self.app.SelectPowerMarkerNodePaths([])
|
||||
self.app.RefreshDetailsWithSelected()
|
||||
self.assert_(self.called)
|
||||
|
||||
def test_makeSelectedReference(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,2]))
|
||||
app.MakeSelectedReference()
|
||||
self.assert_(groups[0].ref is objects[1])
|
||||
self.assert_(groups[1].ref is objects[4])
|
||||
|
||||
def test_makeSelectedReference_by_selecting_two_dupes_in_the_same_group(self):
|
||||
app = self.app
|
||||
objects = self.objects
|
||||
groups = self.groups
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,1,2]))
|
||||
#Only 0 and 2 must go ref, not 1 because it is a part of the same group
|
||||
app.MakeSelectedReference()
|
||||
self.assert_(groups[0].ref is objects[1])
|
||||
self.assert_(groups[1].ref is objects[4])
|
||||
|
||||
def test_removeSelected(self):
|
||||
app = self.app
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,2]))
|
||||
app.RemoveSelected()
|
||||
self.assertEqual(1,len(app.results.dupes))
|
||||
app.RemoveSelected()
|
||||
self.assertEqual(1,len(app.results.dupes))
|
||||
app.SelectPowerMarkerNodePaths(r2np([0,2]))
|
||||
app.RemoveSelected()
|
||||
self.assertEqual(0,len(app.results.dupes))
|
||||
|
||||
def test_addDirectory_simple(self):
|
||||
app = self.app
|
||||
self.assertEqual(0,app.AddDirectory(self.datadirpath()))
|
||||
self.assertEqual(1,len(app.directories))
|
||||
|
||||
def test_addDirectory_already_there(self):
|
||||
app = self.app
|
||||
self.assertEqual(0,app.AddDirectory(self.datadirpath()))
|
||||
self.assertEqual(1,app.AddDirectory(self.datadirpath()))
|
||||
|
||||
def test_addDirectory_does_not_exist(self):
|
||||
app = self.app
|
||||
self.assertEqual(2,app.AddDirectory('/does_not_exist'))
|
||||
|
||||
def test_ignore(self):
|
||||
app = self.app
|
||||
app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group
|
||||
app.AddSelectedToIgnoreList()
|
||||
self.assertEqual(1,len(app.scanner.ignore_list))
|
||||
app.SelectPowerMarkerNodePaths(r2np([0])) #first dupe of the 3 dupes group
|
||||
app.AddSelectedToIgnoreList()
|
||||
#BOTH the ref and the other dupe should have been added
|
||||
self.assertEqual(3,len(app.scanner.ignore_list))
|
||||
|
||||
def test_purgeIgnoreList(self):
|
||||
app = self.app
|
||||
p1 = self.filepath('zerofile')
|
||||
p2 = self.filepath('zerofill')
|
||||
dne = '/does_not_exist'
|
||||
app.scanner.ignore_list.Ignore(dne,p1)
|
||||
app.scanner.ignore_list.Ignore(p2,dne)
|
||||
app.scanner.ignore_list.Ignore(p1,p2)
|
||||
app.PurgeIgnoreList()
|
||||
self.assertEqual(1,len(app.scanner.ignore_list))
|
||||
self.assert_(app.scanner.ignore_list.AreIgnored(p1,p2))
|
||||
self.assert_(not app.scanner.ignore_list.AreIgnored(dne,p1))
|
||||
|
||||
def test_only_unicode_is_added_to_ignore_list(self):
|
||||
def FakeIgnore(first,second):
|
||||
if not isinstance(first,unicode):
|
||||
self.fail()
|
||||
if not isinstance(second,unicode):
|
||||
self.fail()
|
||||
|
||||
app = self.app
|
||||
app.scanner.ignore_list.Ignore = FakeIgnore
|
||||
app.SelectPowerMarkerNodePaths(r2np([2])) #The dupe of the second, 2 sized group
|
||||
app.AddSelectedToIgnoreList()
|
||||
|
||||
def test_dirclass(self):
|
||||
self.assert_(self.app.directories.dirclass is DGDirectory)
|
||||
|
||||
|
||||
class TCDupeGuru_renameSelected(TestCase):
|
||||
def setUp(self):
|
||||
p = Path(tempfile.mkdtemp())
|
||||
fp = open(str(p + 'foo bar 1'),mode='w')
|
||||
fp.close()
|
||||
fp = open(str(p + 'foo bar 2'),mode='w')
|
||||
fp.close()
|
||||
fp = open(str(p + 'foo bar 3'),mode='w')
|
||||
fp.close()
|
||||
refdir = hsfs.phys.Directory(None,str(p))
|
||||
matches = engine.MatchFactory().getmatches(refdir.files)
|
||||
groups = engine.get_groups(matches)
|
||||
g = groups[0]
|
||||
g.prioritize(lambda x:x.name)
|
||||
app = DupeGuru()
|
||||
app.results.groups = groups
|
||||
self.app = app
|
||||
self.groups = groups
|
||||
self.p = p
|
||||
self.refdir = refdir
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(str(self.p))
|
||||
|
||||
def test_simple(self):
|
||||
app = self.app
|
||||
refdir = self.refdir
|
||||
g = self.groups[0]
|
||||
app.SelectPowerMarkerNodePaths(r2np([0]))
|
||||
self.assert_(app.RenameSelected('renamed'))
|
||||
self.assert_('renamed' in refdir)
|
||||
self.assert_('foo bar 2' not in refdir)
|
||||
self.assert_(g.dupes[0] is refdir['renamed'])
|
||||
self.assert_(g.dupes[0] in refdir)
|
||||
|
||||
def test_none_selected(self):
|
||||
app = self.app
|
||||
refdir = self.refdir
|
||||
g = self.groups[0]
|
||||
app.SelectPowerMarkerNodePaths([])
|
||||
self.mock(logging, 'warning', log_calls(lambda msg: None))
|
||||
self.assert_(not app.RenameSelected('renamed'))
|
||||
msg = logging.warning.calls[0]['msg']
|
||||
self.assertEqual('dupeGuru Warning: list index out of range', msg)
|
||||
self.assert_('renamed' not in refdir)
|
||||
self.assert_('foo bar 2' in refdir)
|
||||
self.assert_(g.dupes[0] is refdir['foo bar 2'])
|
||||
|
||||
def test_name_already_exists(self):
|
||||
app = self.app
|
||||
refdir = self.refdir
|
||||
g = self.groups[0]
|
||||
app.SelectPowerMarkerNodePaths(r2np([0]))
|
||||
self.mock(logging, 'warning', log_calls(lambda msg: None))
|
||||
self.assert_(not app.RenameSelected('foo bar 1'))
|
||||
msg = logging.warning.calls[0]['msg']
|
||||
self.assert_(msg.startswith('dupeGuru Warning: \'foo bar 2\' already exists in'))
|
||||
self.assert_('foo bar 1' in refdir)
|
||||
self.assert_('foo bar 2' in refdir)
|
||||
self.assert_(g.dupes[0] is refdir['foo bar 2'])
|
||||
|
||||
68
pe/qt/dupeguru/app_me_cocoa.py
Normal file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Unit Name: dupeguru.app_me_cocoa
|
||||
Created By: Virgil Dupras
|
||||
Created On: 2006/11/16
|
||||
Last modified by:$Author: virgil $
|
||||
Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $
|
||||
$Revision: 4392 $
|
||||
Copyright 2006 Hardcoded Software (http://www.hardcoded.net)
|
||||
"""
|
||||
import os.path as op
|
||||
import logging
|
||||
from appscript import app, k, CommandError
|
||||
import time
|
||||
|
||||
from hsutil.cocoa import as_fetch
|
||||
import hsfs.phys.music
|
||||
|
||||
import app_cocoa, data_me, scanner
|
||||
|
||||
JOB_REMOVE_DEAD_TRACKS = 'jobRemoveDeadTracks'
|
||||
JOB_SCAN_DEAD_TRACKS = 'jobScanDeadTracks'
|
||||
|
||||
app_cocoa.JOBID2TITLE.update({
|
||||
JOB_REMOVE_DEAD_TRACKS: "Removing dead tracks from your iTunes Library",
|
||||
JOB_SCAN_DEAD_TRACKS: "Scanning the iTunes Library",
|
||||
})
|
||||
|
||||
class DupeGuruME(app_cocoa.DupeGuru):
|
||||
def __init__(self):
|
||||
app_cocoa.DupeGuru.__init__(self, data_me, 'dupeguru_me', appid=1)
|
||||
self.scanner = scanner.ScannerME()
|
||||
self.directories.dirclass = hsfs.phys.music.Directory
|
||||
self.dead_tracks = []
|
||||
|
||||
def remove_dead_tracks(self):
|
||||
def do(j):
|
||||
a = app('iTunes')
|
||||
for index, track in enumerate(j.iter_with_progress(self.dead_tracks)):
|
||||
if index % 100 == 0:
|
||||
time.sleep(.1)
|
||||
try:
|
||||
track.delete()
|
||||
except CommandError as e:
|
||||
logging.warning('Error while trying to remove a track from iTunes: %s' % unicode(e))
|
||||
|
||||
self._start_job(JOB_REMOVE_DEAD_TRACKS, do)
|
||||
|
||||
def scan_dead_tracks(self):
|
||||
def do(j):
|
||||
a = app('iTunes')
|
||||
try:
|
||||
[source] = [s for s in a.sources() if s.kind() == k.library]
|
||||
[library] = source.library_playlists()
|
||||
except ValueError:
|
||||
logging.warning('Some unexpected iTunes configuration encountered')
|
||||
return
|
||||
self.dead_tracks = []
|
||||
tracks = as_fetch(library.file_tracks, k.file_track)
|
||||
for index, track in enumerate(j.iter_with_progress(tracks)):
|
||||
if index % 100 == 0:
|
||||
time.sleep(.1)
|
||||
if track.location() == k.missing_value:
|
||||
self.dead_tracks.append(track)
|
||||
logging.info('Found %d dead tracks' % len(self.dead_tracks))
|
||||
|
||||
self._start_job(JOB_SCAN_DEAD_TRACKS, do)
|
||||
|
||||
212
pe/qt/dupeguru/app_pe_cocoa.py
Normal file
@@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Unit Name: dupeguru.app_pe_cocoa
|
||||
Created By: Virgil Dupras
|
||||
Created On: 2006/11/13
|
||||
Last modified by:$Author: virgil $
|
||||
Last modified on:$Date: 2009-05-28 16:33:32 +0200 (Thu, 28 May 2009) $
|
||||
$Revision: 4392 $
|
||||
Copyright 2006 Hardcoded Software (http://www.hardcoded.net)
|
||||
"""
|
||||
import os
|
||||
import os.path as op
|
||||
import logging
|
||||
import plistlib
|
||||
|
||||
import objc
|
||||
from Foundation import *
|
||||
from AppKit import *
|
||||
from appscript import app, k
|
||||
|
||||
from hsutil import job, io
|
||||
import hsfs as fs
|
||||
from hsfs import phys
|
||||
from hsutil import files
|
||||
from hsutil.str import get_file_ext
|
||||
from hsutil.path import Path
|
||||
from hsutil.cocoa import as_fetch
|
||||
|
||||
import app_cocoa, data_pe, directories, picture.matchbase
|
||||
from picture.cache import string_to_colors, Cache
|
||||
|
||||
mainBundle = NSBundle.mainBundle()
|
||||
PictureBlocks = mainBundle.classNamed_('PictureBlocks')
|
||||
assert PictureBlocks is not None
|
||||
|
||||
class Photo(phys.File):
|
||||
cls_info_map = {
|
||||
'size': fs.IT_ATTRS,
|
||||
'ctime': fs.IT_ATTRS,
|
||||
'mtime': fs.IT_ATTRS,
|
||||
'md5': fs.IT_MD5,
|
||||
'md5partial': fs.IT_MD5,
|
||||
'dimensions': fs.IT_EXTRA,
|
||||
}
|
||||
|
||||
def _initialize_info(self,section):
|
||||
super(Photo, self)._initialize_info(section)
|
||||
if section == fs.IT_EXTRA:
|
||||
self._info.update({
|
||||
'dimensions': (0,0),
|
||||
})
|
||||
|
||||
def _read_info(self,section):
|
||||
super(Photo, self)._read_info(section)
|
||||
if section == fs.IT_EXTRA:
|
||||
size = PictureBlocks.getImageSize_(unicode(self.path))
|
||||
self._info['dimensions'] = (size.width, size.height)
|
||||
|
||||
def get_blocks(self, block_count_per_side):
|
||||
try:
|
||||
blocks = PictureBlocks.getBlocksFromImagePath_blockCount_scanArea_(unicode(self.path), block_count_per_side, 0)
|
||||
except Exception, e:
|
||||
raise IOError('The reading of "%s" failed with "%s"' % (unicode(self.path), unicode(e)))
|
||||
if not blocks:
|
||||
raise IOError('The picture %s could not be read' % unicode(self.path))
|
||||
return string_to_colors(blocks)
|
||||
|
||||
|
||||
class IPhoto(Photo):
|
||||
def __init__(self, parent, whole_path):
|
||||
super(IPhoto, self).__init__(parent, whole_path[-1])
|
||||
self.whole_path = whole_path
|
||||
|
||||
def _build_path(self):
|
||||
return self.whole_path
|
||||
|
||||
@property
|
||||
def display_path(self):
|
||||
return super(IPhoto, self)._build_path()
|
||||
|
||||
|
||||
class Directory(phys.Directory):
|
||||
cls_file_class = Photo
|
||||
cls_supported_exts = ('png', 'jpg', 'jpeg', 'gif', 'psd', 'bmp', 'tiff', 'nef', 'cr2')
|
||||
|
||||
def _fetch_subitems(self):
|
||||
subdirs, subfiles = super(Directory,self)._fetch_subitems()
|
||||
return subdirs, [name for name in subfiles if get_file_ext(name) in self.cls_supported_exts]
|
||||
|
||||
|
||||
class IPhotoLibrary(fs.Directory):
|
||||
def __init__(self, plistpath):
|
||||
self.plistpath = plistpath
|
||||
self.refpath = plistpath[:-1]
|
||||
# the AlbumData.xml file lives right in the library path
|
||||
super(IPhotoLibrary, self).__init__(None, 'iPhoto Library')
|
||||
|
||||
def _update_photo(self, photo_data):
|
||||
if photo_data['MediaType'] != 'Image':
|
||||
return
|
||||
photo_path = Path(photo_data['ImagePath'])
|
||||
subpath = photo_path[len(self.refpath):-1]
|
||||
subdir = self
|
||||
for element in subpath:
|
||||
try:
|
||||
subdir = subdir[element]
|
||||
except KeyError:
|
||||
subdir = fs.Directory(subdir, element)
|
||||
IPhoto(subdir, photo_path)
|
||||
|
||||
def update(self):
|
||||
self.clear()
|
||||
s = open(unicode(self.plistpath)).read()
|
||||
# There was a case where a guy had 0x10 chars in his plist, causing expat errors on loading
|
||||
s = s.replace('\x10', '')
|
||||
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
|
||||
pass
|
||||
|
||||
|
||||
class DupeGuruPE(app_cocoa.DupeGuru):
|
||||
def __init__(self):
|
||||
app_cocoa.DupeGuru.__init__(self, data_pe, 'dupeguru_pe', appid=5)
|
||||
self.scanner.match_factory = picture.matchbase.AsyncMatchFactory()
|
||||
self.directories.dirclass = Directory
|
||||
self.directories.special_dirclasses[Path('iPhoto Library')] = lambda _, __: self._create_iphoto_library()
|
||||
p = op.join(self.appdata, 'cached_pictures.db')
|
||||
self.scanner.match_factory.cached_blocks = Cache(p)
|
||||
|
||||
def _create_iphoto_library(self):
|
||||
ud = NSUserDefaults.standardUserDefaults()
|
||||
prefs = ud.persistentDomainForName_('com.apple.iApps')
|
||||
plisturl = NSURL.URLWithString_(prefs['iPhotoRecentDatabases'][0])
|
||||
plistpath = Path(plisturl.path())
|
||||
return IPhotoLibrary(plistpath)
|
||||
|
||||
def _do_delete(self, j):
|
||||
def op(dupe):
|
||||
j.add_progress()
|
||||
return self._do_delete_dupe(dupe)
|
||||
|
||||
marked = [dupe for dupe in self.results.dupes if self.results.is_marked(dupe)]
|
||||
self.path2iphoto = {}
|
||||
if any(isinstance(dupe, IPhoto) for dupe in marked):
|
||||
a = app('iPhoto')
|
||||
a.select(a.photo_library_album())
|
||||
photos = as_fetch(a.photo_library_album().photos, k.item)
|
||||
for photo in photos:
|
||||
self.path2iphoto[photo.image_path()] = photo
|
||||
self.last_op_error_count = self.results.perform_on_marked(op, True)
|
||||
del self.path2iphoto
|
||||
|
||||
def _do_delete_dupe(self, dupe):
|
||||
if isinstance(dupe, IPhoto):
|
||||
photo = self.path2iphoto[unicode(dupe.path)]
|
||||
app('iPhoto').remove(photo)
|
||||
return True
|
||||
else:
|
||||
return app_cocoa.DupeGuru._do_delete_dupe(self, dupe)
|
||||
|
||||
def _do_load(self, j):
|
||||
self.directories.LoadFromFile(op.join(self.appdata, 'last_directories.xml'))
|
||||
for d in self.directories:
|
||||
if isinstance(d, IPhotoLibrary):
|
||||
d.update()
|
||||
self.results.load_from_xml(op.join(self.appdata, 'last_results.xml'), self._get_file, j)
|
||||
|
||||
def _get_file(self, str_path):
|
||||
p = Path(str_path)
|
||||
for d in self.directories:
|
||||
result = None
|
||||
if p in d.path:
|
||||
result = d.find_path(p[d.path:])
|
||||
if isinstance(d, IPhotoLibrary) and p in d.refpath:
|
||||
result = d.find_path(p[d.refpath:])
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
def AddDirectory(self, d):
|
||||
try:
|
||||
added = self.directories.add_path(Path(d))
|
||||
if d == 'iPhoto Library':
|
||||
added.update()
|
||||
return 0
|
||||
except directories.AlreadyThereError:
|
||||
return 1
|
||||
|
||||
def CopyOrMove(self, dupe, copy, destination, dest_type):
|
||||
if isinstance(dupe, IPhoto):
|
||||
copy = True
|
||||
return app_cocoa.DupeGuru.CopyOrMove(self, dupe, copy, destination, dest_type)
|
||||
|
||||
def start_scanning(self):
|
||||
for directory in self.directories:
|
||||
if isinstance(directory, IPhotoLibrary):
|
||||
self.directories.SetState(directory.refpath, directories.STATE_EXCLUDED)
|
||||
return app_cocoa.DupeGuru.start_scanning(self)
|
||||
|
||||
def selected_dupe_path(self):
|
||||
if not self.selected_dupes:
|
||||
return None
|
||||
return self.selected_dupes[0].path
|
||||
|
||||
def selected_dupe_ref_path(self):
|
||||
if not self.selected_dupes:
|
||||
return None
|
||||
ref = self.results.get_group_of_duplicate(self.selected_dupes[0]).ref
|
||||
return ref.path
|
||||
|
||||
13
pe/qt/dupeguru/app_se_cocoa.py
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python
|
||||
# Unit Name: app_se_cocoa
|
||||
# Created By: Virgil Dupras
|
||||
# Created On: 2009-05-24
|
||||
# $Id$
|
||||
# Copyright 2009 Hardcoded Software (http://www.hardcoded.net)
|
||||
|
||||
import app_cocoa, data
|
||||
|
||||
class DupeGuru(app_cocoa.DupeGuru):
|
||||
def __init__(self):
|
||||
app_cocoa.DupeGuru.__init__(self, data, 'dupeguru', appid=4)
|
||||
|
||||