From 0d8ed92a684470d175e0031ebcb32ab80821aadb Mon Sep 17 00:00:00 2001 From: Virgil Dupras Date: Fri, 24 Sep 2010 15:48:59 +0200 Subject: [PATCH] Converted the result tree into a result table. --HG-- rename : cocoa/base/PyResultTree.h => cocoa/base/PyResultTable.h rename : cocoa/base/ResultOutline.h => cocoa/base/ResultTable.h rename : cocoa/base/ResultOutline.m => cocoa/base/ResultTable.m rename : core/gui/result_tree.py => core/gui/result_table.py --- .../base/{PyResultTree.h => PyResultTable.h} | 8 +- cocoa/base/ResultOutline.m | 207 -------- cocoa/base/{ResultOutline.h => ResultTable.h} | 9 +- cocoa/base/ResultTable.m | 162 ++++++ cocoa/base/ResultWindow.h | 8 +- cocoa/base/ResultWindow.m | 16 +- cocoa/base/xib/MainMenu.xib | 476 +++++++++--------- cocoa/se/ResultWindow.m | 2 +- cocoa/se/dupeguru.xcodeproj/project.pbxproj | 22 +- core/app_cocoa_inter.py | 17 +- core/gui/{result_tree.py => result_table.py} | 84 ++-- core/tests/app_test.py | 116 ++--- qt/base/main_window.py | 12 +- qt/base/main_window.ui | 26 +- qt/base/results_model.py | 99 +--- 15 files changed, 566 insertions(+), 698 deletions(-) rename cocoa/base/{PyResultTree.h => PyResultTable.h} (79%) delete mode 100644 cocoa/base/ResultOutline.m rename cocoa/base/{ResultOutline.h => ResultTable.h} (81%) create mode 100644 cocoa/base/ResultTable.m rename core/gui/{result_tree.py => result_table.py} (69%) diff --git a/cocoa/base/PyResultTree.h b/cocoa/base/PyResultTable.h similarity index 79% rename from cocoa/base/PyResultTree.h rename to cocoa/base/PyResultTable.h index bbfebba7..6de42cac 100644 --- a/cocoa/base/PyResultTree.h +++ b/cocoa/base/PyResultTable.h @@ -7,18 +7,18 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "PyOutline.h" +#import "PyTable.h" -@interface PyResultTree : PyOutline +@interface PyResultTable : PyTable - (BOOL)powerMarkerMode; - (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode; - (BOOL)deltaValuesMode; - (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode; -- (NSString *)valueForPath:(NSArray *)aPath column:(NSInteger)aColumn; +- (NSString *)valueForRow:(NSInteger)rowIndex column:(NSInteger)aColumn; - (BOOL)renameSelected:(NSString *)aNewName; - (void)sortBy:(NSInteger)aIdentifier ascending:(BOOL)aAscending; - (void)markSelected; - (void)removeSelected; -- (NSArray *)rootChildrenCounts; +- (NSInteger)selectedDupeCount; @end \ No newline at end of file diff --git a/cocoa/base/ResultOutline.m b/cocoa/base/ResultOutline.m deleted file mode 100644 index eaeac4d3..00000000 --- a/cocoa/base/ResultOutline.m +++ /dev/null @@ -1,207 +0,0 @@ -/* -Copyright 2010 Hardcoded Software (http://www.hardcoded.net) - -This software is licensed under the "HS" License as described in the "LICENSE" file, -which should be included with this package. The terms are also available at -http://www.hardcoded.net/licenses/hs_license -*/ - -#import "ResultOutline.h" -#import "Dialogs.h" -#import "Utils.h" -#import "Consts.h" - -@implementation ResultOutline -- (id)initWithPyParent:(id)aPyParent view:(HSOutlineView *)aOutlineView -{ - self = [super initWithPyClassName:@"PyResultOutline" pyParent:aPyParent view:aOutlineView]; - _rootChildrenCounts = nil; - [self connect]; - return self; -} - -- (void)dealloc -{ - [self disconnect]; - [_deltaColumns release]; - [super dealloc]; -} - -- (PyResultTree *)py -{ - return (PyResultTree *)py; -} - -/* Public */ -- (BOOL)powerMarkerMode -{ - return [[self py] powerMarkerMode]; -} - -- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode -{ - [[self py] setPowerMarkerMode:aPowerMarkerMode]; -} - -- (BOOL)deltaValuesMode -{ - return [[self py] deltaValuesMode]; -} - -- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode -{ - [[self py] setDeltaValuesMode:aDeltaValuesMode]; -} - -- (void)setDeltaColumns:(NSIndexSet *)aDeltaColumns -{ - [_deltaColumns release]; - _deltaColumns = [aDeltaColumns retain]; -} - -- (NSInteger)selectedDupeCount -{ - NSArray *selected = [self selectedIndexPaths]; - if ([self powerMarkerMode]) { - return [selected count]; - } - else { - NSInteger r = 0; - for (NSIndexPath *path in selected) { - if ([path length] == 2) { - r++; - } - } - return r; - } -} - -- (void)removeSelected -{ - NSInteger selectedDupeCount = [self selectedDupeCount]; - if (!selectedDupeCount) - return; - NSString *msg = [NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",selectedDupeCount]; - if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO - return; - [[self py] removeSelected]; -} - -/* Datasource */ -- (NSInteger)outlineView:(NSOutlineView *)aOutlineView numberOfChildrenOfItem:(id)item -{ - NSIndexPath *path = item; - if ((path != nil) && ([path length] == 1)) { - if (_rootChildrenCounts == nil) { - _rootChildrenCounts = [[[self py] rootChildrenCounts] retain]; - } - NSInteger index = [path indexAtPosition:0]; - return n2i([_rootChildrenCounts objectAtIndex:index]); - } - return [super outlineView:aOutlineView numberOfChildrenOfItem:item]; -} - -- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)column byItem:(id)item -{ - NSIndexPath *path = item; - NSString *identifier = [column identifier]; - if ([identifier isEqual:@"marked"]) { - return b2n([self boolProperty:@"marked" valueAtPath:path]); - } - NSInteger columnId = [identifier integerValue]; - return [[self py] valueForPath:p2a(path) column:columnId]; -} - -- (void)outlineView:(NSOutlineView *)aOutlineView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn byItem:(id)item -{ - if ([[tableColumn identifier] isEqual:@"0"]) { - NSIndexPath *path = item; - NSString *oldName = [[self py] valueForPath:p2a(path) column:0]; - NSString *newName = object; - if (![newName isEqual:oldName]) { - BOOL renamed = [[self py] renameSelected:newName]; - if (!renamed) { - [Dialogs showMessage:[NSString stringWithFormat:@"The name '%@' already exists.", newName]]; - } - else { - [self refreshItemAtPath:path]; - } - } - } - else { - [super outlineView:aOutlineView setObjectValue:object forTableColumn:tableColumn byItem:item]; - } -} - -/* Delegate */ -- (void)outlineView:(NSOutlineView *)aOutlineView didClickTableColumn:(NSTableColumn *)tableColumn -{ - if ([[outlineView sortDescriptors] count] < 1) - return; - NSSortDescriptor *sd = [[outlineView sortDescriptors] objectAtIndex:0]; - [[self py] sortBy:[[sd key] integerValue] ascending:[sd ascending]]; -} - -- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item -{ - NSIndexPath *path = item; - BOOL isMarkable = [self boolProperty:@"markable" valueAtPath:path]; - if ([[tableColumn identifier] isEqual:@"marked"]) { - [cell setEnabled:isMarkable]; - } - if ([cell isKindOfClass:[NSTextFieldCell class]]) { - // Determine if the text color will be blue due to directory being reference. - NSTextFieldCell *textCell = cell; - if (isMarkable) { - [textCell setTextColor:[NSColor blackColor]]; - } - else { - [textCell setTextColor:[NSColor blueColor]]; - } - if (([self deltaValuesMode]) && ([self powerMarkerMode] || ([path length] > 1))) { - NSInteger i = [[tableColumn identifier] integerValue]; - if ([_deltaColumns containsIndex:i]) { - [textCell setTextColor:[NSColor orangeColor]]; - } - } - } -} - -- (BOOL)tableViewHadDeletePressed:(NSTableView *)tableView -{ - [self removeSelected]; - return YES; -} - -- (BOOL)tableViewHadSpacePressed:(NSTableView *)tableView -{ - [[self py] markSelected]; - return YES; -} - -/* don't calls saveEdits and cancelEdits */ -- (void)outlineViewDidEndEditing:(HSOutlineView *)outlineView -{ -} - -- (void)outlineViewCancelsEdition:(HSOutlineView *)outlineView -{ -} - -/* Python --> Cocoa */ -- (void)refresh /* Override */ -{ - [_rootChildrenCounts release]; - _rootChildrenCounts = nil; - [super refresh]; - [outlineView expandItem:nil expandChildren:YES]; -} - -- (void)invalidateMarkings -{ - for (NSMutableDictionary *props in [itemData objectEnumerator]) { - [props removeObjectForKey:@"marked"]; - } - [outlineView setNeedsDisplay:YES]; -} -@end \ No newline at end of file diff --git a/cocoa/base/ResultOutline.h b/cocoa/base/ResultTable.h similarity index 81% rename from cocoa/base/ResultOutline.h rename to cocoa/base/ResultTable.h index 12aa4998..a3bb824e 100644 --- a/cocoa/base/ResultOutline.h +++ b/cocoa/base/ResultTable.h @@ -7,15 +7,14 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "HSOutline.h" -#import "PyResultTree.h" +#import "HSTable.h" +#import "PyResultTable.h" -@interface ResultOutline : HSOutline +@interface ResultTable : HSTable { NSIndexSet *_deltaColumns; - NSArray *_rootChildrenCounts; } -- (PyResultTree *)py; +- (PyResultTable *)py; - (BOOL)powerMarkerMode; - (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode; - (BOOL)deltaValuesMode; diff --git a/cocoa/base/ResultTable.m b/cocoa/base/ResultTable.m new file mode 100644 index 00000000..a88d5a8b --- /dev/null +++ b/cocoa/base/ResultTable.m @@ -0,0 +1,162 @@ +/* +Copyright 2010 Hardcoded Software (http://www.hardcoded.net) + +This software is licensed under the "HS" License as described in the "LICENSE" file, +which should be included with this package. The terms are also available at +http://www.hardcoded.net/licenses/hs_license +*/ + +#import "ResultTable.h" +#import "Dialogs.h" +#import "Utils.h" +#import "Consts.h" + +@implementation ResultTable +- (id)initWithPyParent:(id)aPyParent view:(NSTableView *)aTableView +{ + self = [super initWithPyClassName:@"PyResultTable" pyParent:aPyParent view:aTableView]; + [self connect]; + return self; +} + +- (void)dealloc +{ + [self disconnect]; + [_deltaColumns release]; + [super dealloc]; +} + +- (PyResultTable *)py +{ + return (PyResultTable *)py; +} + +/* Public */ +- (BOOL)powerMarkerMode +{ + return [[self py] powerMarkerMode]; +} + +- (void)setPowerMarkerMode:(BOOL)aPowerMarkerMode +{ + [[self py] setPowerMarkerMode:aPowerMarkerMode]; +} + +- (BOOL)deltaValuesMode +{ + return [[self py] deltaValuesMode]; +} + +- (void)setDeltaValuesMode:(BOOL)aDeltaValuesMode +{ + [[self py] setDeltaValuesMode:aDeltaValuesMode]; +} + +- (void)setDeltaColumns:(NSIndexSet *)aDeltaColumns +{ + [_deltaColumns release]; + _deltaColumns = [aDeltaColumns retain]; +} + +- (NSInteger)selectedDupeCount +{ + return [[self py] selectedDupeCount]; +} + +- (void)removeSelected +{ + NSInteger selectedDupeCount = [self selectedDupeCount]; + if (!selectedDupeCount) + return; + NSString *msg = [NSString stringWithFormat:@"You are about to remove %d files from results. Continue?",selectedDupeCount]; + if ([Dialogs askYesNo:msg] == NSAlertSecondButtonReturn) // NO + return; + [[self py] removeSelected]; +} + +/* Datasource */ +- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)column row:(NSInteger)row +{ + NSString *identifier = [column identifier]; + if ([identifier isEqual:@"marked"]) { + return [[self py] valueForColumn:@"marked" row:row]; + } + NSInteger columnId = [identifier integerValue]; + return [[self py] valueForRow:row column:columnId]; +} + +- (void)tableView:(NSTableView *)aTableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)column row:(NSInteger)row +{ + NSString *identifier = [column identifier]; + if ([identifier isEqual:@"marked"]) { + [[self py] setValue:object forColumn:identifier row:row]; + } + else if ([identifier isEqual:@"0"]) { + NSString *oldName = [[self py] valueForRow:row column:0]; + NSString *newName = object; + if (![newName isEqual:oldName]) { + BOOL renamed = [[self py] renameSelected:newName]; + if (!renamed) { + [Dialogs showMessage:[NSString stringWithFormat:@"The name '%@' already exists.", newName]]; + } + else { + [tableView setNeedsDisplay:YES]; + } + } + } +} + +/* Delegate */ +- (void)tableView:(NSTableView *)aTableView didClickTableColumn:(NSTableColumn *)tableColumn +{ + if ([[tableView sortDescriptors] count] < 1) + return; + NSSortDescriptor *sd = [[tableView sortDescriptors] objectAtIndex:0]; + [[self py] sortBy:[[sd key] integerValue] ascending:[sd ascending]]; +} + +- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)column row:(NSInteger)row +{ + BOOL isMarkable = n2b([[self py] valueForColumn:@"markable" row:row]); + if ([[column identifier] isEqual:@"marked"]) { + [cell setEnabled:isMarkable]; + // Low-tech solution, for indentation, but it works... + NSCellImagePosition pos = isMarkable ? NSImageRight : NSImageLeft; + [cell setImagePosition:pos]; + } + if ([cell isKindOfClass:[NSTextFieldCell class]]) { + // Determine if the text color will be blue due to directory being reference. + NSTextFieldCell *textCell = cell; + if (isMarkable) { + [textCell setTextColor:[NSColor blackColor]]; + } + else { + [textCell setTextColor:[NSColor blueColor]]; + if ([self deltaValuesMode]) { + NSInteger i = [[column identifier] integerValue]; + if ([_deltaColumns containsIndex:i]) { + [textCell setTextColor:[NSColor orangeColor]]; + } + } + } + } +} + +- (BOOL)tableViewHadDeletePressed:(NSTableView *)tableView +{ + [self removeSelected]; + return YES; +} + +- (BOOL)tableViewHadSpacePressed:(NSTableView *)tableView +{ + [[self py] markSelected]; + return YES; +} + +/* Python --> Cocoa */ +- (void)invalidateMarkings +{ + [tableView setNeedsDisplay:YES]; +} +@end \ No newline at end of file diff --git a/cocoa/base/ResultWindow.h b/cocoa/base/ResultWindow.h index 7eee7ce9..a3e89955 100644 --- a/cocoa/base/ResultWindow.h +++ b/cocoa/base/ResultWindow.h @@ -7,10 +7,10 @@ http://www.hardcoded.net/licenses/hs_license */ #import -#import "HSOutlineView.h" #import "StatsLabel.h" -#import "ResultOutline.h" +#import "ResultTable.h" #import "ProblemDialog.h" +#import "HSTableView.h" #import "PyDupeGuru.h" @interface ResultWindowBase : NSWindowController @@ -19,7 +19,7 @@ http://www.hardcoded.net/licenses/hs_license IBOutlet PyDupeGuruBase *py; IBOutlet id app; IBOutlet NSSegmentedControl *deltaSwitch; - IBOutlet HSOutlineView *matches; + IBOutlet HSTableView *matches; IBOutlet NSSegmentedControl *pmSwitch; IBOutlet NSTextField *stats; IBOutlet NSMenu *columnsMenu; @@ -27,7 +27,7 @@ http://www.hardcoded.net/licenses/hs_license NSMutableArray *_resultColumns; NSWindowController *preferencesPanel; - ResultOutline *outline; + ResultTable *table; StatsLabel *statsLabel; ProblemDialog *problemDialog; } diff --git a/cocoa/base/ResultWindow.m b/cocoa/base/ResultWindow.m index 0312d852..71897c13 100644 --- a/cocoa/base/ResultWindow.m +++ b/cocoa/base/ResultWindow.m @@ -19,7 +19,7 @@ http://www.hardcoded.net/licenses/hs_license { [self window]; preferencesPanel = [[NSWindowController alloc] initWithWindowNibName:@"Preferences"]; - outline = [[ResultOutline alloc] initWithPyParent:py view:matches]; + table = [[ResultTable alloc] initWithPyParent:py view:matches]; statsLabel = [[StatsLabel alloc] initWithPyParent:py labelView:stats]; problemDialog = [[ProblemDialog alloc] initWithPy:py]; [self initResultColumns]; @@ -37,7 +37,7 @@ http://www.hardcoded.net/licenses/hs_license - (void)dealloc { - [outline release]; + [table release]; [preferencesPanel release]; [statsLabel release]; [problemDialog release]; @@ -137,12 +137,12 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)changeDelta:(id)sender { - [outline setDeltaValuesMode:[deltaSwitch selectedSegment] == 1]; + [table setDeltaValuesMode:[deltaSwitch selectedSegment] == 1]; } - (IBAction)changePowerMarker:(id)sender { - [outline setPowerMarkerMode:[pmSwitch selectedSegment] == 1]; + [table setPowerMarkerMode:[pmSwitch selectedSegment] == 1]; } - (IBAction)copyMarked:(id)sender @@ -191,7 +191,7 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)ignoreSelected:(id)sender { - NSInteger selectedDupeCount = [outline selectedDupeCount]; + NSInteger selectedDupeCount = [table selectedDupeCount]; if (!selectedDupeCount) return; NSString *msg = [NSString stringWithFormat:@"All selected %d matches are going to be ignored in all subsequent scans. Continue?",selectedDupeCount]; @@ -293,7 +293,7 @@ http://www.hardcoded.net/licenses/hs_license - (IBAction)removeSelected:(id)sender { - [outline removeSelected]; + [table removeSelected]; } - (IBAction)renameSelected:(id)sender @@ -416,8 +416,8 @@ http://www.hardcoded.net/licenses/hs_license } } else if ([lastAction isEqualTo:jobScan]) { - NSInteger groupCount = [outline intProperty:@"children_count" valueAtPath:nil]; - if (groupCount == 0) + NSInteger rowCount = [[table py] numberOfRows]; + if (rowCount == 0) [Dialogs showMessage:@"No duplicates found."]; } diff --git a/cocoa/base/xib/MainMenu.xib b/cocoa/base/xib/MainMenu.xib index 26a70c66..53544577 100644 --- a/cocoa/base/xib/MainMenu.xib +++ b/cocoa/base/xib/MainMenu.xib @@ -12,7 +12,8 @@ YES - + + YES @@ -82,11 +83,9 @@ Power Marker - + 256 {{7, 14}, {67, 24}} - - YES 67239424 @@ -177,11 +176,9 @@ Filter - + 258 {{0, 14}, {81, 22}} - - YES 343014976 @@ -323,11 +320,9 @@ Action - + 256 {{0, 14}, {58, 26}} - - YES -2076049856 @@ -530,11 +525,9 @@ Delta Values - + 256 {{4, 14}, {67, 24}} - - YES 67239424 @@ -681,52 +674,76 @@ {1.79769e+308, 1.79769e+308} {340, 340} - + 256 YES - + + + 290 + {{17, 20}, {523, 17}} + + YES + + 67239424 + 138412032 + Marked: 0 files, 0 B. Total: 0 files, 0 B. + + + + 6 + System + controlColor + + 3 + MC42NjY2NjY2NjY3AA + + + + + + 274 YES - - + + 2304 YES - - - 274 + + + 256 {515, 317} - + YES - - + + 256 {515, 17} - - + + - - + + -2147483392 - {{-26, 0}, {16, 17}} - + {{224, 0}, {16, 17}} + YES - + marked - 47 - 16 - 1000 + 26 + 26 + 26 75628096 - 2048 + 134219776 - + 6 System headerColor @@ -739,64 +756,63 @@ - + 67239424 131072 - - LucidaGrande - 12 - 16 - - + + 1211912703 2 + + NSImage + NSSwitch + NSSwitch - 400 - 75 + 200 + 25 YES - + - + 0 195 16 - 1000 + 3.4028234663852886e+38 75628096 2048 Name - - 3 - MC4zMzMzMzI5OQA - + - + 337772096 2048 - - + Text Cell + + LucidaGrande + 11 + 16 + + 6 System controlBackgroundColor - - 3 - MC42NjY2NjY2NjY3AA - + 2 YES - + 0 YES @@ -821,7 +837,7 @@ 2 - 0 + 4 15 0 YES @@ -829,78 +845,62 @@ {{1, 17}, {515, 317}} - - + + + 4 - - + + -2147483392 - {{-30, 17}, {15, 302}} - - + {{224, 17}, {15, 102}} + + _doScroller: - 0.98739492893218994 + 37 + 0.1947367936372757 - - + + -2147483392 - {{1, 319}, {515, 15}} - + {{1, 119}, {223, 15}} + 1 - + _doScroller: - 0.85406301824212272 + 0.57142859697341919 - - + + 2304 YES - + {{1, 0}, {515, 17}} - - + + + 4 - + {{20, 45}, {517, 335}} + 562 - - - - - + + + + + QSAAAEEgAABBgAAAQYAAAA - - - 290 - {{17, 20}, {523, 17}} - - YES - - 67239424 - 138412032 - Marked: 0 files, 0 B. Total: 0 files, 0 B. - - - - 6 - System - controlColor - - - - - {557, 400} + {{0, 0}, {1440, 878}} {340, 418} @@ -1713,14 +1713,6 @@ 212 - - - initialFirstResponder - - - - 279 - stats @@ -1833,14 +1825,6 @@ 661 - - - menu - - - - 663 - openSelected: @@ -2241,14 +2225,6 @@ 1175 - - - matches - - - - 1176 - invokeCustomCommand: @@ -2273,6 +2249,22 @@ 1208 + + + matches + + + + 1225 + + + + menu + + + + 1226 + @@ -2317,56 +2309,11 @@ YES - + - - 219 - - - YES - - - - - - - - - 220 - - - YES - - - - - - - 222 - - - YES - - - - - - 406 - - - YES - - - - - - 407 - - - 291 @@ -2870,26 +2817,6 @@ - - 1140 - - - - - 1144 - - - - - 1145 - - - - - 1146 - - - 1147 @@ -3171,6 +3098,71 @@ + + 1209 + + + YES + + + + + + + + + 1210 + + + + + 1211 + + + + + 1212 + + + YES + + + + + + + 1213 + + + + + 1218 + + + YES + + + + + + 1222 + + + YES + + + + + + 1223 + + + + + 1224 + + + @@ -3205,14 +3197,6 @@ 1137.IBPluginDependency 1138.IBPluginDependency 1139.IBPluginDependency - 1140.IBPluginDependency - 1140.IBShouldRemoveOnLegacySave - 1144.IBPluginDependency - 1144.IBShouldRemoveOnLegacySave - 1145.IBPluginDependency - 1145.IBShouldRemoveOnLegacySave - 1146.IBPluginDependency - 1146.IBShouldRemoveOnLegacySave 1147.IBEditorWindowLastContentRect 1147.IBPluginDependency 1156.IBPluginDependency @@ -3229,6 +3213,16 @@ 1204.IBPluginDependency 1205.IBPluginDependency 1206.IBPluginDependency + 1209.IBPluginDependency + 1210.IBPluginDependency + 1211.IBPluginDependency + 1212.CustomClassName + 1212.IBPluginDependency + 1213.IBPluginDependency + 1218.IBPluginDependency + 1222.IBPluginDependency + 1223.IBPluginDependency + 1224.IBPluginDependency 134.IBPluginDependency 134.ImportedFromIB2 136.IBPluginDependency @@ -3256,13 +3250,6 @@ 21.NSWindowTemplate.visibleAtLaunch 21.windowTemplate.hasMinSize 21.windowTemplate.minSize - 219.IBPluginDependency - 219.ImportedFromIB2 - 220.CustomClassName - 220.IBPluginDependency - 220.ImportedFromIB2 - 222.IBPluginDependency - 222.ImportedFromIB2 23.IBPluginDependency 23.ImportedFromIB2 24.IBEditorWindowLastContentRect @@ -3277,10 +3264,6 @@ 398.ImportedFromIB2 399.IBPluginDependency 399.ImportedFromIB2 - 406.IBPluginDependency - 406.ImportedFromIB2 - 407.IBPluginDependency - 407.ImportedFromIB2 497.ImportedFromIB2 5.IBPluginDependency 5.ImportedFromIB2 @@ -3452,14 +3435,6 @@ com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - {{409, 745}, {617, 0}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3477,6 +3452,16 @@ com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + HSTableView + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -3496,22 +3481,15 @@ - {{439, 345}, {557, 400}} + {{324, 289}, {557, 400}} com.apple.InterfaceBuilder.CocoaPlugin - {{439, 345}, {557, 400}} + {{324, 289}, {557, 400}} {340, 340} com.apple.InterfaceBuilder.CocoaPlugin - HSOutlineView - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - {{531, 625}, {193, 143}} com.apple.InterfaceBuilder.CocoaPlugin @@ -3524,10 +3502,6 @@ com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin @@ -3686,7 +3660,7 @@ - 1208 + 1226 @@ -3863,16 +3837,23 @@ - HSOutlineView - NSOutlineView - + HSTableView + NSTableView + + IBProjectSource + ../views/HSTableView.h + + + + NSObject + IBProjectSource ../views/HSOutlineView.h NSObject - + NSObject @@ -4311,7 +4292,7 @@ NSMenu NSSegmentedControl NSSearchField - HSOutlineView + HSTableView NSSegmentedControl PyDupeGuruBase NSTextField @@ -4350,7 +4331,7 @@ matches - HSOutlineView + HSTableView pmSwitch @@ -4576,7 +4557,7 @@ NSObject - + IBFrameworkSource AppKit.framework/Headers/NSOutlineView.h @@ -4763,11 +4744,6 @@ Sparkle.framework/Headers/SUUpdater.h - - NSOutlineView - NSTableView - - NSPopUpButton NSButton @@ -5023,6 +4999,7 @@ NSApplicationIcon NSMenuCheckmark NSMenuMixedState + NSSwitch details32 folder32 preferences32 @@ -5033,6 +5010,7 @@ {128, 128} {9, 8} {7, 2} + {15, 15} {48, 48} {32, 32} {32, 32} diff --git a/cocoa/se/ResultWindow.m b/cocoa/se/ResultWindow.m index 16da92de..d35ca977 100644 --- a/cocoa/se/ResultWindow.m +++ b/cocoa/se/ResultWindow.m @@ -20,7 +20,7 @@ http://www.hardcoded.net/licenses/hs_license [super awakeFromNib]; NSMutableIndexSet *deltaColumns = [NSMutableIndexSet indexSetWithIndex:2]; [deltaColumns addIndex:4]; - [outline setDeltaColumns:deltaColumns]; + [table setDeltaColumns:deltaColumns]; } /* Actions */ diff --git a/cocoa/se/dupeguru.xcodeproj/project.pbxproj b/cocoa/se/dupeguru.xcodeproj/project.pbxproj index a1315a79..315f0646 100644 --- a/cocoa/se/dupeguru.xcodeproj/project.pbxproj +++ b/cocoa/se/dupeguru.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ CE4557B40AE3BC50005A9546 /* Sparkle.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE45579A0AE3BC2B005A9546 /* Sparkle.framework */; }; CE647E571173024A006D28BA /* ProblemDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = CE647E551173024A006D28BA /* ProblemDialog.m */; }; CE647E591173026F006D28BA /* ProblemDialog.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE647E581173026F006D28BA /* ProblemDialog.xib */; }; + CE6DD4E7124CA3070089A48D /* ResultTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6DD4E6124CA3070089A48D /* ResultTable.m */; }; + CE6DD547124CAF1F0089A48D /* HSTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6DD546124CAF1F0089A48D /* HSTableView.m */; }; CE6E0DFE1054E9EF008D9390 /* dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */; }; CE76FDC4111EE37C006618EA /* HSOutlineView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDBF111EE37C006618EA /* HSOutlineView.m */; }; CE76FDC5111EE37C006618EA /* NSIndexPathAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDC1111EE37C006618EA /* NSIndexPathAdditions.m */; }; @@ -29,7 +31,6 @@ CE76FDDF111EE42F006618EA /* HSOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDDE111EE42F006618EA /* HSOutline.m */; }; CE76FDF7111EE561006618EA /* NSEventAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CE76FDF6111EE561006618EA /* NSEventAdditions.m */; }; CE8C53BC117324CE0011B41F /* HSTable.m in Sources */ = {isa = PBXBuildFile; fileRef = CE8C53BB117324CE0011B41F /* HSTable.m */; }; - CE91F215113BC22D0010360B /* ResultOutline.m in Sources */ = {isa = PBXBuildFile; fileRef = CE91F212113BC22D0010360B /* ResultOutline.m */; }; CE91F216113BC22D0010360B /* StatsLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = CE91F214113BC22D0010360B /* StatsLabel.m */; }; CEAC6811109B0B7E00B43C85 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEAC6810109B0B7E00B43C85 /* Preferences.xib */; }; CEBC6C3912144A4B007B43AE /* registration.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEBC6C3712144A4B007B43AE /* registration.xib */; }; @@ -89,6 +90,11 @@ CE647E551173024A006D28BA /* ProblemDialog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ProblemDialog.m; path = ../base/ProblemDialog.m; sourceTree = SOURCE_ROOT; }; CE647E561173024A006D28BA /* PyProblemDialog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyProblemDialog.h; path = ../base/PyProblemDialog.h; sourceTree = SOURCE_ROOT; }; CE647E581173026F006D28BA /* ProblemDialog.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = ProblemDialog.xib; path = ../base/xib/ProblemDialog.xib; sourceTree = SOURCE_ROOT; }; + CE6DD4E4124CA3070089A48D /* PyResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyResultTable.h; path = ../base/PyResultTable.h; sourceTree = SOURCE_ROOT; }; + CE6DD4E5124CA3070089A48D /* ResultTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultTable.h; path = ../base/ResultTable.h; sourceTree = SOURCE_ROOT; }; + CE6DD4E6124CA3070089A48D /* ResultTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultTable.m; path = ../base/ResultTable.m; sourceTree = SOURCE_ROOT; }; + CE6DD545124CAF1F0089A48D /* HSTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HSTableView.h; path = ../../cocoalib/views/HSTableView.h; sourceTree = SOURCE_ROOT; }; + CE6DD546124CAF1F0089A48D /* HSTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HSTableView.m; path = ../../cocoalib/views/HSTableView.m; sourceTree = SOURCE_ROOT; }; CE6E0DFD1054E9EF008D9390 /* dsa_pub.pem */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = dsa_pub.pem; path = ../base/dsa_pub.pem; sourceTree = ""; }; CE6E7407111C997500C350E3 /* PyDetailsPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyDetailsPanel.h; path = ../base/PyDetailsPanel.h; sourceTree = SOURCE_ROOT; }; CE76FDBE111EE37C006618EA /* HSOutlineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HSOutlineView.h; sourceTree = ""; }; @@ -110,10 +116,7 @@ CE76FDF6111EE561006618EA /* NSEventAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NSEventAdditions.m; path = ../../cocoalib/NSEventAdditions.m; sourceTree = SOURCE_ROOT; }; CE8C53B61173248F0011B41F /* PyTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PyTable.h; sourceTree = ""; }; CE8C53BB117324CE0011B41F /* HSTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HSTable.m; sourceTree = ""; }; - CE91F20F113BC22D0010360B /* PyResultTree.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyResultTree.h; path = ../base/PyResultTree.h; sourceTree = SOURCE_ROOT; }; CE91F210113BC22D0010360B /* PyStatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PyStatsLabel.h; path = ../base/PyStatsLabel.h; sourceTree = SOURCE_ROOT; }; - CE91F211113BC22D0010360B /* ResultOutline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ResultOutline.h; path = ../base/ResultOutline.h; sourceTree = SOURCE_ROOT; }; - CE91F212113BC22D0010360B /* ResultOutline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ResultOutline.m; path = ../base/ResultOutline.m; sourceTree = SOURCE_ROOT; }; CE91F213113BC22D0010360B /* StatsLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = StatsLabel.h; path = ../base/StatsLabel.h; sourceTree = SOURCE_ROOT; }; CE91F214113BC22D0010360B /* StatsLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = StatsLabel.m; path = ../base/StatsLabel.m; sourceTree = SOURCE_ROOT; }; CEAC6810109B0B7E00B43C85 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Preferences.xib; path = xib/Preferences.xib; sourceTree = ""; }; @@ -260,6 +263,8 @@ CE76FDBD111EE37C006618EA /* views */ = { isa = PBXGroup; children = ( + CE6DD545124CAF1F0089A48D /* HSTableView.h */, + CE6DD546124CAF1F0089A48D /* HSTableView.m */, CE76FDBE111EE37C006618EA /* HSOutlineView.h */, CE76FDBF111EE37C006618EA /* HSOutlineView.m */, CE76FDC0111EE37C006618EA /* NSIndexPathAdditions.h */, @@ -362,10 +367,10 @@ CEFC7FB00FC9518F00CD5728 /* dgbase */ = { isa = PBXGroup; children = ( - CE91F20F113BC22D0010360B /* PyResultTree.h */, + CE6DD4E4124CA3070089A48D /* PyResultTable.h */, + CE6DD4E5124CA3070089A48D /* ResultTable.h */, + CE6DD4E6124CA3070089A48D /* ResultTable.m */, CE91F210113BC22D0010360B /* PyStatsLabel.h */, - CE91F211113BC22D0010360B /* ResultOutline.h */, - CE91F212113BC22D0010360B /* ResultOutline.m */, CE91F213113BC22D0010360B /* StatsLabel.h */, CE91F214113BC22D0010360B /* StatsLabel.m */, CE76FDD1111EE3A7006618EA /* DirectoryOutline.h */, @@ -491,10 +496,11 @@ CE76FDDF111EE42F006618EA /* HSOutline.m in Sources */, CE76FDF7111EE561006618EA /* NSEventAdditions.m in Sources */, CEBE4D74111F0EE1009AAC6D /* HSWindowController.m in Sources */, - CE91F215113BC22D0010360B /* ResultOutline.m in Sources */, CE91F216113BC22D0010360B /* StatsLabel.m in Sources */, CE647E571173024A006D28BA /* ProblemDialog.m in Sources */, CE8C53BC117324CE0011B41F /* HSTable.m in Sources */, + CE6DD4E7124CA3070089A48D /* ResultTable.m in Sources */, + CE6DD547124CAF1F0089A48D /* HSTableView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/core/app_cocoa_inter.py b/core/app_cocoa_inter.py index a8585981..1d11e13b 100644 --- a/core/app_cocoa_inter.py +++ b/core/app_cocoa_inter.py @@ -15,7 +15,7 @@ from .gui.details_panel import DetailsPanel from .gui.directory_tree import DirectoryTree from .gui.problem_dialog import ProblemDialog from .gui.problem_table import ProblemTable -from .gui.result_tree import ResultTree +from .gui.result_table import ResultTable from .gui.stats_label import StatsLabel # Fix py2app's problems on relative imports @@ -163,8 +163,8 @@ class PyDirectoryOutline(PyOutline): self.py.add_directory(path) -class PyResultOutline(PyOutline): - py_class = ResultTree +class PyResultTable(PyTable): + py_class = ResultTable @signature('c@:') def powerMarkerMode(self): @@ -182,9 +182,9 @@ class PyResultOutline(PyOutline): def setDeltaValuesMode_(self, value): self.py.delta_values = value - @signature('@@:@i') - def valueForPath_column_(self, path, column): - return self.py.get_node_value(path, column) + @signature('@@:ii') + def valueForRow_column_(self, row_index, column): + return self.py.get_row_value(row_index, column) @signature('c@:@') def renameSelected_(self, newname): @@ -200,8 +200,9 @@ class PyResultOutline(PyOutline): def removeSelected(self): self.py.app.remove_selected() - def rootChildrenCounts(self): - return self.py.root_children_counts() + @signature('i@:') + def selectedDupeCount(self): + return self.py.selected_dupe_count # python --> cocoa def invalidate_markings(self): diff --git a/core/gui/result_tree.py b/core/gui/result_table.py similarity index 69% rename from core/gui/result_tree.py rename to core/gui/result_table.py index f1d9a616..54744f9b 100644 --- a/core/gui/result_tree.py +++ b/core/gui/result_table.py @@ -9,14 +9,14 @@ from operator import attrgetter -from hsgui.tree import Tree, Node +from hsgui.table import GUITable, Row from .base import GUIObject -class DupeNode(Node): - def __init__(self, app, group, dupe): - Node.__init__(self, '') - self._app = app +class DupeRow(Row): + def __init__(self, table, group, dupe): + Row.__init__(self, table) + self._app = table.app self._group = group self._dupe = dupe self._data = None @@ -34,6 +34,10 @@ class DupeNode(Node): self._data_delta = self._app._get_display_info(self._dupe, self._group, True) return self._data_delta + @property + def isref(self): + return self._dupe is self._group.ref + @property def markable(self): return self._app.results.is_markable(self._dupe) @@ -47,10 +51,10 @@ class DupeNode(Node): self._app.mark_dupe(self._dupe, value) -class ResultTree(GUIObject, Tree): +class ResultTable(GUIObject, GUITable): def __init__(self, view, app): GUIObject.__init__(self, view, app) - Tree.__init__(self) + GUITable.__init__(self) self._power_marker = False self._delta_values = False self._sort_descriptors = (0, True) @@ -58,60 +62,54 @@ class ResultTree(GUIObject, Tree): #--- Override def connect(self): GUIObject.connect(self) - self._refresh() + self.refresh() self.view.refresh() - def _select_nodes(self, nodes): - Tree._select_nodes(self, nodes) - self.app._select_dupes(list(map(attrgetter('_dupe'), nodes))) + def _restore_selection(self, previous_selection): + if self.app.selected_dupes: + to_find = set(self.app.selected_dupes) + indexes = [i for i, r in enumerate(self) if r._dupe in to_find] + self.selected_indexes = indexes - #--- Private - def _refresh(self): - self.clear() + def _update_selection(self): + rows = self.selected_rows + self.app._select_dupes(list(map(attrgetter('_dupe'), rows))) + + def _fill(self): if not self.power_marker: for group in self.app.results.groups: - group_node = DupeNode(self.app, group, group.ref) - self.append(group_node) + self.append(DupeRow(self, group, group.ref)) for dupe in group.dupes: - group_node.append(DupeNode(self.app, group, dupe)) + self.append(DupeRow(self, group, dupe)) else: for dupe in self.app.results.dupes: group = self.app.results.get_group_of_duplicate(dupe) - self.append(DupeNode(self.app, group, dupe)) - if self.app.selected_dupes: - to_find = set(self.app.selected_dupes) - nodes = list(self.findall(lambda n: n is not self and n._dupe in to_find)) - self.selected_nodes = nodes + self.append(DupeRow(self, group, dupe)) #--- Public - def get_node_value(self, path, column): + def get_row_value(self, index, column): try: - node = self.get_node(path) + row = self[index] except IndexError: return '---' if self.delta_values: - return node.data_delta[column] + return row.data_delta[column] else: - return node.data[column] + return row.data[column] def rename_selected(self, newname): - node = self.selected_node - node._data = None - node._data_delta = None + row = self.selected_row + row._data = None + row._data_delta = None return self.app.rename_selected(newname) - def root_children_counts(self): - # This is a speed optimization for cases where there's a lot of results so that there is - # not thousands of children_count queries when expandAll is called. - return [len(node) for node in self] - def sort(self, key, asc): if self.power_marker: self.app.results.sort_dupes(key, asc, self.delta_values) else: self.app.results.sort_groups(key, asc) self._sort_descriptors = (key, asc) - self._refresh() + self.refresh() self.view.refresh() #--- Properties @@ -126,7 +124,7 @@ class ResultTree(GUIObject, Tree): self._power_marker = value key, asc = self._sort_descriptors self.sort(key, asc) - self._refresh() + self.refresh() self.view.refresh() @property @@ -138,22 +136,26 @@ class ResultTree(GUIObject, Tree): if value == self._delta_values: return self._delta_values = value - self._refresh() + self.refresh() self.view.refresh() + @property + def selected_dupe_count(self): + return sum(1 for row in self.selected_rows if not row.isref) + #--- Event Handlers def marking_changed(self): self.view.invalidate_markings() def results_changed(self): - self._refresh() + self.refresh() self.view.refresh() def results_changed_but_keep_selection(self): # What we want to to here is that instead of restoring selected *dupes* after refresh, we # restore selected *paths*. - paths = self.selected_paths - self._refresh() - self.selected_paths = paths + indexes = self.selected_indexes + self.refresh() + self.select(indexes) self.view.refresh() diff --git a/core/tests/app_test.py b/core/tests/app_test.py index 96216172..9a40749c 100644 --- a/core/tests/app_test.py +++ b/core/tests/app_test.py @@ -23,7 +23,7 @@ from .. import app, fs, engine from ..app import DupeGuru as DupeGuruBase from ..gui.details_panel import DetailsPanel from ..gui.directory_tree import DirectoryTree -from ..gui.result_tree import ResultTree +from ..gui.result_table import ResultTable class DupeGuru(DupeGuruBase): def __init__(self): @@ -166,11 +166,11 @@ class TCDupeGuruWithResults(TestCase): self.dpanel = DetailsPanel(self.dpanel_gui, self.app) self.dtree_gui = CallLogger() self.dtree = DirectoryTree(self.dtree_gui, self.app) - self.rtree_gui = CallLogger() - self.rtree = ResultTree(self.rtree_gui, self.app) + self.rtable_gui = CallLogger() + self.rtable = ResultTable(self.rtable_gui, self.app) self.dpanel.connect() self.dtree.connect() - self.rtree.connect() + self.rtable.connect() tmppath = self.tmppath() io.mkdir(tmppath + 'foo') io.mkdir(tmppath + 'bar') @@ -217,42 +217,35 @@ class TCDupeGuruWithResults(TestCase): def test_GetObjects(self): objects = self.objects groups = self.groups - n = self.rtree.get_node([0]) - assert n._group is groups[0] - assert n._dupe is objects[0] - n = self.rtree.get_node([0, 0]) - assert n._group is groups[0] - assert n._dupe is objects[1] - n = self.rtree.get_node([1, 0]) - assert n._group is groups[1] - assert n._dupe is objects[4] + r = self.rtable[0] + assert r._group is groups[0] + assert r._dupe is objects[0] + r = self.rtable[1] + assert r._group is groups[0] + assert r._dupe is objects[1] + r = self.rtable[4] + assert r._group is groups[1] + assert r._dupe is objects[4] def test_GetObjects_after_sort(self): objects = self.objects groups = self.groups[:] # we need an un-sorted reference - self.rtree.sort(0, False) #0 = Filename - n = self.rtree.get_node([0, 0]) - assert n._group is groups[1] - assert n._dupe is objects[4] - - def test_selected_result_node_paths(self): - # app.selected_dupes is correctly converted into node paths - paths = [[0, 0], [0, 1], [1]] - self.rtree.selected_paths = paths - eq_(self.rtree.selected_paths, paths) + self.rtable.sort(0, False) #0 = Filename + r = self.rtable[1] + assert r._group is groups[1] + assert r._dupe is objects[4] def test_selected_result_node_paths_after_deletion(self): # cases where the selected dupes aren't there are correctly handled - paths = [[0, 0], [0, 1], [1]] - self.rtree.selected_paths = paths + self.rtable.select([1, 2, 3]) self.app.remove_selected() # The first 2 dupes have been removed. The 3rd one is a ref. it stays there, in first pos. - eq_(self.rtree.selected_paths, [[0, 0]]) # no exception + eq_(self.rtable.selected_indexes, [1]) # no exception def test_selectResultNodePaths(self): app = self.app objects = self.objects - self.rtree.selected_paths = [[0, 0], [0, 1]] + self.rtable.select([1, 2]) eq_(len(app.selected_dupes), 2) assert app.selected_dupes[0] is objects[1] assert app.selected_dupes[1] is objects[2] @@ -260,7 +253,7 @@ class TCDupeGuruWithResults(TestCase): def test_selectResultNodePaths_with_ref(self): app = self.app objects = self.objects - self.rtree.selected_paths = [[0, 0], [0, 1], [1]] + self.rtable.select([1, 2, 3]) eq_(len(app.selected_dupes), 3) assert app.selected_dupes[0] is objects[1] assert app.selected_dupes[1] is objects[2] @@ -270,9 +263,9 @@ class TCDupeGuruWithResults(TestCase): app = self.app objects = self.objects groups = self.groups[:] #To keep the old order in memory - self.rtree.sort(0, False) #0 = Filename + self.rtable.sort(0, False) #0 = Filename #Now, the group order is supposed to be reversed - self.rtree.selected_paths = [[0, 0], [1], [1, 0]] + self.rtable.select([1, 2, 3]) eq_(len(app.selected_dupes), 3) assert app.selected_dupes[0] is objects[4] assert app.selected_dupes[1] is groups[0].ref @@ -282,39 +275,26 @@ class TCDupeGuruWithResults(TestCase): # app.selected_dupes is correctly converted into paths app = self.app objects = self.objects - self.rtree.power_marker = True - self.rtree.selected_paths = [[0], [1], [2]] - self.rtree.power_marker = False - eq_(self.rtree.selected_paths, [[0, 0], [0, 1], [1, 0]]) + self.rtable.power_marker = True + self.rtable.select([0, 1, 2]) + self.rtable.power_marker = False + eq_(self.rtable.selected_indexes, [1, 2, 4]) def test_selected_powermarker_node_paths_after_deletion(self): # cases where the selected dupes aren't there are correctly handled app = self.app objects = self.objects - self.rtree.power_marker = True - self.rtree.selected_paths = [[0], [1], [2]] + self.rtable.power_marker = True + self.rtable.select([0, 1, 2]) app.remove_selected() - eq_(self.rtree.selected_paths, []) # no exception - - def test_selectPowerMarkerRows(self): - app = self.app - objects = self.objects - self.rtree.selected_paths = [[0, 0], [0, 1], [1, 0]] - eq_(len(app.selected_dupes), 3) - assert app.selected_dupes[0] is objects[1] - assert app.selected_dupes[1] is objects[2] - assert app.selected_dupes[2] is objects[4] - - def test_selectPowerMarkerRows_empty(self): - self.rtree.selected_paths = [] - eq_(len(self.app.selected_dupes), 0) + eq_(self.rtable.selected_indexes, []) # no exception def test_selectPowerMarkerRows_after_sort(self): app = self.app objects = self.objects - self.rtree.power_marker = True - self.rtree.sort(0, False) #0 = Filename - self.rtree.selected_paths = [[0], [1], [2]] + self.rtable.power_marker = True + self.rtable.sort(0, False) #0 = Filename + self.rtable.select([0, 1, 2]) eq_(len(app.selected_dupes), 3) assert app.selected_dupes[0] is objects[4] assert app.selected_dupes[1] is objects[2] @@ -325,7 +305,7 @@ class TCDupeGuruWithResults(TestCase): objects = self.objects app.toggle_selected_mark_state() eq_(app.results.mark_count, 0) - self.rtree.selected_paths = [[0, 0], [1, 0]] + self.rtable.select([1, 4]) app.toggle_selected_mark_state() eq_(app.results.mark_count, 2) assert not app.results.is_marked(objects[0]) @@ -335,10 +315,10 @@ class TCDupeGuruWithResults(TestCase): assert app.results.is_marked(objects[4]) def test_refreshDetailsWithSelected(self): - self.rtree.selected_paths = [[0, 0], [1, 0]] + self.rtable.select([1, 4]) eq_(self.dpanel.row(0), ('Filename', 'bar bleh', 'foo bar')) self.check_gui_calls(self.dpanel_gui, ['refresh']) - self.rtree.selected_paths = [] + self.rtable.select([]) eq_(self.dpanel.row(0), ('Filename', '---', '---')) self.check_gui_calls(self.dpanel_gui, ['refresh']) @@ -346,7 +326,7 @@ class TCDupeGuruWithResults(TestCase): app = self.app objects = self.objects groups = self.groups - self.rtree.selected_paths = [[0, 0], [1, 0]] + self.rtable.select([1, 4]) app.make_selected_reference() assert groups[0].ref is objects[1] assert groups[1].ref is objects[4] @@ -355,7 +335,7 @@ class TCDupeGuruWithResults(TestCase): app = self.app objects = self.objects groups = self.groups - self.rtree.selected_paths = [[0, 0], [0, 1], [1, 0]] + self.rtable.select([1, 2, 4]) #Only [0, 0] and [1, 0] must go ref, not [0, 1] because it is a part of the same group app.make_selected_reference() assert groups[0].ref is objects[1] @@ -363,7 +343,7 @@ class TCDupeGuruWithResults(TestCase): def test_removeSelected(self): app = self.app - self.rtree.selected_paths = [[0, 0], [1, 0]] + self.rtable.select([1, 4]) app.remove_selected() eq_(len(app.results.dupes), 1) # the first path is now selected app.remove_selected() @@ -386,10 +366,10 @@ class TCDupeGuruWithResults(TestCase): def test_ignore(self): app = self.app - self.rtree.selected_path = [1, 0] #The dupe of the second, 2 sized group + self.rtable.select([4]) #The dupe of the second, 2 sized group app.add_selected_to_ignore_list() eq_(len(app.scanner.ignore_list), 1) - self.rtree.selected_path = [0, 0] #first dupe of the 3 dupes group + self.rtable.select([1]) #first dupe of the 3 dupes group app.add_selected_to_ignore_list() #BOTH the ref and the other dupe should have been added eq_(len(app.scanner.ignore_list), 3) @@ -416,7 +396,7 @@ class TCDupeGuruWithResults(TestCase): app = self.app app.scanner.ignore_list.Ignore = FakeIgnore - self.rtree.selected_path = [1, 0] + self.rtable.select([4]) app.add_selected_to_ignore_list() @@ -440,14 +420,14 @@ class TCDupeGuru_renameSelected(TestCase): self.groups = groups self.p = p self.files = files - self.rtree_gui = CallLogger() - self.rtree = ResultTree(self.rtree_gui, self.app) - self.rtree.connect() + self.rtable_gui = CallLogger() + self.rtable = ResultTable(self.rtable_gui, self.app) + self.rtable.connect() def test_simple(self): app = self.app g = self.groups[0] - self.rtree.selected_path = [0, 0] + self.rtable.select([1]) assert app.rename_selected('renamed') names = io.listdir(self.p) assert 'renamed' in names @@ -457,7 +437,7 @@ class TCDupeGuru_renameSelected(TestCase): def test_none_selected(self): app = self.app g = self.groups[0] - self.rtree.selected_paths = [] + self.rtable.select([]) self.mock(logging, 'warning', log_calls(lambda msg: None)) assert not app.rename_selected('renamed') msg = logging.warning.calls[0]['msg'] @@ -470,7 +450,7 @@ class TCDupeGuru_renameSelected(TestCase): def test_name_already_exists(self): app = self.app g = self.groups[0] - self.rtree.selected_path = [0, 0] + self.rtable.select([1]) self.mock(logging, 'warning', log_calls(lambda msg: None)) assert not app.rename_selected('foo bar 1') msg = logging.warning.calls[0]['msg'] diff --git a/qt/base/main_window.py b/qt/base/main_window.py index 922c48d8..f680f5c2 100644 --- a/qt/base/main_window.py +++ b/qt/base/main_window.py @@ -48,7 +48,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): def _setupUi(self): self.setupUi(self) # Stuff that can't be setup in the Designer - h = self.resultsView.header() + h = self.resultsView.horizontalHeader() h.setHighlightSections(False) h.setMovable(True) h.setStretchLastSection(False) @@ -110,7 +110,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): return answer == QMessageBox.Yes def _load_columns(self): - h = self.resultsView.header() + h = self.resultsView.horizontalHeader() h.setResizeMode(QHeaderView.Interactive) prefs = self.app.prefs attrs = list(zip(prefs.columns_width, prefs.columns_visible)) @@ -120,7 +120,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): h.setResizeMode(0, QHeaderView.Stretch) def _update_column_actions_status(self): - h = self.resultsView.header() + h = self.resultsView.horizontalHeader() for action in self._column_actions: colid = action.column_index action.setChecked(not h.isSectionHidden(colid)) @@ -185,7 +185,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.app.show_directories() def exportTriggered(self): - h = self.resultsView.header() + h = self.resultsView.horizontalHeader() column_ids = [] for i in range(len(self.app.data.COLUMNS)): if not h.isSectionHidden(i): @@ -278,7 +278,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): #--- Events def appWillSavePrefs(self): prefs = self.app.prefs - h = self.resultsView.header() + h = self.resultsView.horizontalHeader() widths = [] visible = [] for i in range(len(self.app.data.COLUMNS)): @@ -295,7 +295,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.app.prefs.reset_columns() self._load_columns() else: - h = self.resultsView.header() + h = self.resultsView.horizontalHeader() h.setSectionHidden(colid, not h.isSectionHidden(colid)) self._update_column_actions_status() diff --git a/qt/base/main_window.ui b/qt/base/main_window.ui index ccdc0f38..eb8f48e8 100644 --- a/qt/base/main_window.ui +++ b/qt/base/main_window.ui @@ -26,27 +26,21 @@ QAbstractItemView::SelectRows - - false - - - true - - - false - true - - false - - + false - + + 18 + + false + + 18 + @@ -57,7 +51,7 @@ 0 0 630 - 20 + 22 @@ -462,7 +456,7 @@ ResultsView - QTreeView + QTableView
.results_model
diff --git a/qt/base/results_model.py b/qt/base/results_model.py index 4569e9f9..8adf7430 100644 --- a/qt/base/results_model.py +++ b/qt/base/results_model.py @@ -7,62 +7,20 @@ # http://www.hardcoded.net/licenses/hs_license from PyQt4.QtCore import SIGNAL, Qt -from PyQt4.QtGui import (QBrush, QStyledItemDelegate, QFont, QTreeView, QColor, QItemSelectionModel, - QItemSelection) +from PyQt4.QtGui import QBrush, QFont, QTableView, QColor, QItemSelectionModel, QItemSelection -from qtlib.tree_model import TreeModel, RefNode +from qtlib.table import Table -from core.gui.result_tree import ResultTree as ResultTreeModel +from core.gui.result_table import ResultTable as ResultTableModel -class ResultsDelegate(QStyledItemDelegate): - def initStyleOption(self, option, index): - QStyledItemDelegate.initStyleOption(self, option, index) - node = index.internalPointer() - ref = node.ref - if ref._group.ref is ref._dupe: - newfont = QFont(option.font) - newfont.setBold(True) - option.font = newfont - - -class ResultsModel(TreeModel): +class ResultsModel(Table): def __init__(self, app, view): - TreeModel.__init__(self) - self.view = view + model = ResultTableModel(self, app) self._app = app self._data = app.data self._delta_columns = app.DELTA_COLUMNS - self.resultsDelegate = ResultsDelegate() - self.model = ResultTreeModel(self, app) - self.view.setItemDelegate(self.resultsDelegate) - self.view.setModel(self) + Table.__init__(self, model, view) self.model.connect() - - self.connect(self.view.selectionModel(), SIGNAL('selectionChanged(QItemSelection,QItemSelection)'), self.selectionChanged) - - def _createNode(self, ref, row): - return RefNode(self, None, ref, row) - - def _getChildren(self): - return list(self.model) - - def _updateSelection(self): - selectedIndexes = [] - for path in self.model.selected_paths: - modelIndex = self.findIndex(path) - if modelIndex.isValid(): - selectedIndexes.append(modelIndex) - if selectedIndexes: - selection = QItemSelection() - for modelIndex in selectedIndexes: - selection.select(modelIndex, modelIndex) - flags = QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows - self.view.selectionModel().select(selection, flags) - flags = QItemSelectionModel.Rows - self.view.selectionModel().setCurrentIndex(selectedIndexes[0], flags) - self.view.scrollTo(selectedIndexes[0]) - else: - self.view.selectionModel().clear() def columnCount(self, parent): return len(self._data.COLUMNS) @@ -70,22 +28,26 @@ class ResultsModel(TreeModel): def data(self, index, role): if not index.isValid(): return None - node = index.internalPointer() - ref = node.ref + row = self.model[index.row()] if role == Qt.DisplayRole: - data = ref.data_delta if self.model.delta_values else ref.data + data = row.data_delta if self.model.delta_values else row.data return data[index.column()] elif role == Qt.CheckStateRole: - if index.column() == 0 and ref.markable: - return Qt.Checked if ref.marked else Qt.Unchecked + if index.column() == 0 and row.markable: + return Qt.Checked if row.marked else Qt.Unchecked elif role == Qt.ForegroundRole: - if ref._dupe is ref._group.ref or ref._dupe.is_ref: + if row.isref: return QBrush(Qt.blue) elif self.model.delta_values and index.column() in self._delta_columns: return QBrush(QColor(255, 142, 40)) # orange + elif role == Qt.FontRole: + isBold = row.isref + font = QFont(self.view.font()) + font.setBold(isBold) + return font elif role == Qt.EditRole: if index.column() == 0: - return ref.data[index.column()] + return row.data[index.column()] return None def flags(self, index): @@ -93,7 +55,10 @@ class ResultsModel(TreeModel): return Qt.ItemIsEnabled flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable if index.column() == 0: - flags |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable + flags |= Qt.ItemIsEditable + row = self.model[index.row()] + if row.markable: + flags |= Qt.ItemIsUserCheckable return flags def headerData(self, section, orientation, role): @@ -104,13 +69,12 @@ class ResultsModel(TreeModel): def setData(self, index, value, role): if not index.isValid(): return False - node = index.internalPointer() - ref = node.ref + row = self.model[index.row()] if role == Qt.CheckStateRole: if index.column() == 0: - self._app.mark_dupe(ref._dupe, value.toBool()) + self._app.mark_dupe(row._dupe, value.toBool()) return True - if role == Qt.EditRole: + elif role == Qt.EditRole: if index.column() == 0: value = str(value.toString()) return self.model.rename_selected(value) @@ -136,18 +100,7 @@ class ResultsModel(TreeModel): def delta_values(self, value): self.model.delta_values = value - #--- Events - def selectionChanged(self, selected, deselected): - indexes = self.view.selectionModel().selectedRows() - nodes = [index.internalPointer() for index in indexes] - self.model.selected_nodes = [node.ref for node in nodes] - #--- model --> view - def refresh(self): - self.reset() - self.view.expandAll() - self._updateSelection() - def invalidate_markings(self): # redraw view # HACK. this is the only way I found to update the widget without reseting everything @@ -155,13 +108,13 @@ class ResultsModel(TreeModel): self.view.scroll(0, -1) -class ResultsView(QTreeView): +class ResultsView(QTableView): #--- Override def keyPressEvent(self, event): if event.text() == ' ': self.emit(SIGNAL('spacePressed()')) return - QTreeView.keyPressEvent(self, event) + QTableView.keyPressEvent(self, event) def mouseDoubleClickEvent(self, event): self.emit(SIGNAL('doubleClicked()'))