unison

Fork of Unison, a bi-directional file synchronization tool
git clone git://git.laack.co/unison.git
Log | Files | Refs | README | LICENSE

ReconItem.m (21654B)


      1 #import "ReconItem.h"
      2 #import "Bridge.h"
      3 
      4 #import <Carbon/Carbon.h>
      5 
      6 @implementation ReconItem
      7 
      8 - init {
      9         [super init];
     10         selected = NO; // NB only used/updated during sorts. Not a
     11                                    // reliable indicator of whether item is selected
     12         fileSize = -1.;
     13         bytesTransferred = -1.;
     14         return self;
     15 }
     16 
     17 - (void)dealloc
     18 {
     19     [path release];
     20     [fullPath release];
     21     // [direction release];  // assuming retained by cache, so not retained
     22     // [directionSortString release];  // no retain/release necessary because is constant
     23         [super dealloc];
     24 }
     25 
     26 - (ReconItem *)parent
     27 {
     28         return parent;
     29 }
     30 
     31 - (void)setParent:(ReconItem *)p
     32 {
     33         parent = p;
     34 }
     35 
     36 - (void)willChange
     37 {
     38         // propagate up parent chain
     39         [parent willChange];
     40 }
     41 
     42 - (NSArray *)children
     43 {
     44         return nil;
     45 }
     46 
     47 - (BOOL)selected
     48 {
     49     return selected;
     50 }
     51 
     52 - (void)setSelected:(BOOL)x
     53 {
     54     selected = x;
     55 }
     56 
     57 - (NSString *)path
     58 {
     59     return path;
     60 }
     61 
     62 - (void)setPath:(NSString *)aPath
     63 {
     64         [path autorelease];
     65         path = [aPath retain];
     66 
     67         // invalidate
     68         [fullPath autorelease];
     69         fullPath = nil;
     70 }
     71 
     72 - (NSString *)fullPath
     73 {
     74         if (!fullPath) {
     75                 NSString *parentPath = [parent fullPath];
     76                 [self setFullPath:(([parentPath length] > 0) ? [parentPath stringByAppendingFormat:@"/%@", path] : path)];
     77         }
     78 
     79     return fullPath;
     80 }
     81 
     82 - (void)setFullPath:(NSString *)p
     83 {
     84         [fullPath autorelease];
     85         fullPath = [p retain];
     86 }
     87 
     88 - (NSString *)left
     89 {
     90     return nil;
     91 }
     92 
     93 - (NSString *)right
     94 {
     95     return nil;
     96 }
     97 
     98 static NSMutableDictionary *_ChangeIconsByType = nil;
     99 
    100 - (NSImage *)changeIconFor:(NSString *)type other:(NSString *)other
    101 {
    102         if (![type length]) {
    103                 if ([other isEqual:@"Created"]) {
    104                         type = @"Absent";
    105                 } else if ([other length]) {
    106                         type = @"Unmodified";
    107                 } else
    108                         return nil;
    109         }
    110 
    111         NSImage *result = [_ChangeIconsByType objectForKey:type];
    112         if (!result) {
    113                 NSString *imageName = [NSString stringWithFormat:@"Change_%@.png", type];
    114                 result = [NSImage imageNamed:imageName];
    115                 if (!_ChangeIconsByType) _ChangeIconsByType = [[NSMutableDictionary alloc] init];
    116                 [_ChangeIconsByType setObject:result forKey:type];
    117         }
    118         return result;
    119 }
    120 
    121 - (NSImage *)leftIcon
    122 {
    123         return [self changeIconFor:[self left] other:[self right]];
    124 }
    125 
    126 - (NSImage *)rightIcon
    127 {
    128         return [self changeIconFor:[self right] other:[self left]];
    129 }
    130 
    131 
    132 - (double)computeFileSize
    133 {
    134         return 0.;
    135 }
    136 
    137 - (double)bytesTransferred
    138 {
    139         return 0.;
    140 }
    141 
    142 - (long)fileCount
    143 {
    144         return 1;
    145 }
    146 
    147 - (double)fileSize
    148 {
    149         if (fileSize == -1.) fileSize = [self computeFileSize];
    150         return fileSize;
    151 }
    152 
    153 - (NSString *)formatFileSize:(double)size
    154 {
    155         if (size == 0) return @"--";
    156         if (size < 1024) return @"< 1KB"; // return [NSString stringWithFormat:@"%i bytes", size];
    157         size /= 1024;
    158         if (size < 1024) return [NSString stringWithFormat:@"%i KB", (int)size];
    159         size /= 1024;
    160         if (size < 1024) return [NSString stringWithFormat:@"%1.1f MB", size];
    161         size = size / 1024;
    162         return [NSString stringWithFormat:@"%1.1f GB", size];
    163 }
    164 
    165 - (NSString *)fileSizeString
    166 {
    167         return [self formatFileSize:[self fileSize]];
    168 }
    169 
    170 - (NSString *)bytesTransferredString
    171 {
    172         return [self formatFileSize:[self bytesTransferred]];
    173 }
    174 
    175 - (NSNumber *)percentTransferred
    176 {
    177         double size = [self computeFileSize];
    178         return (size > 0) ? [NSNumber numberWithDouble:([self bytesTransferred] / (size) * 100.0)]
    179                                           : nil;
    180 }
    181 
    182 static NSMutableDictionary *_iconsByExtension = nil;
    183 
    184 - (NSImage *)iconForExtension:(NSString *)extension
    185 {
    186         NSImage *icon = [_iconsByExtension objectForKey:extension];
    187         if (!_iconsByExtension) _iconsByExtension = [[NSMutableDictionary alloc] init];
    188         if (!icon) {
    189                 icon = [[NSWorkspace sharedWorkspace] iconForFileType:extension];
    190                 [icon setSize:NSMakeSize(16.0, 16.0)];
    191                 [_iconsByExtension setObject:icon forKey:extension];
    192         }
    193         return icon;
    194 }
    195 
    196 - (NSImage *)fileIcon
    197 {
    198         return [self iconForExtension:NSFileTypeForHFSTypeCode(kOpenFolderIcon)];
    199 }
    200 
    201 - (NSString *)dirString
    202 {
    203         return @"<-?->";
    204 }
    205 
    206 - (NSImage *)direction
    207 {
    208     if (direction) return direction;
    209     NSString * dirString = [self dirString];
    210 
    211     BOOL changedFromDefault = [self changedFromDefault];
    212 
    213     if ([dirString isEqual:@"<-?->"]) {
    214         if (changedFromDefault | resolved) {
    215             direction = [NSImage imageNamed: @"table-skip.tif"];
    216                         directionSortString = @"3";
    217                 }
    218         else {
    219             direction = [NSImage imageNamed: @"table-conflict.tif"];
    220                         directionSortString = @"2";
    221         }
    222     }
    223 
    224     else if ([dirString isEqual:@"---->"]) {
    225         if (changedFromDefault) {
    226             direction = [NSImage imageNamed: @"table-right-blue.tif"];
    227             directionSortString = @"6";
    228         }
    229         else {
    230             direction = [NSImage imageNamed: @"table-right-green.tif"];
    231             directionSortString = @"8";
    232         }
    233     }
    234 
    235     else if ([dirString isEqual:@"<----"]) {
    236         if (changedFromDefault) {
    237             direction = [NSImage imageNamed: @"table-left-blue.tif"];
    238             directionSortString = @"5";
    239         }
    240         else {
    241             direction = [NSImage imageNamed: @"table-left-green.tif"];
    242             directionSortString = @"7";
    243         }
    244     }
    245 
    246     else if ([dirString isEqual:@"<-M->"]) {
    247         direction = [NSImage imageNamed: @"table-merge.tif"];
    248         directionSortString = @"4";
    249     }
    250 
    251         else if ([dirString isEqual:@"<--->"]) {
    252                 direction = [NSImage imageNamed: @"table-mixed.tif"];
    253                 directionSortString = @"9";
    254         }
    255 
    256     else {
    257         direction = [NSImage imageNamed: @"table-error.tif"];
    258         directionSortString = @"1";
    259     }
    260 
    261     [direction retain];
    262     return direction;
    263 }
    264 
    265 - (void)setDirection:(char *)d
    266 {
    267     [direction autorelease];
    268     direction = nil;
    269 }
    270 
    271 - (void)doAction:(unichar)action
    272 {
    273     switch (action) {
    274     case '>':
    275         [self setDirection:"unisonRiSetRight"];
    276         break;
    277     case '<':
    278         [self setDirection:"unisonRiSetLeft"];
    279         break;
    280     case '/':
    281         [self setDirection:"unisonRiSetConflict"];
    282         resolved = YES;
    283         break;
    284     case '-':
    285         [self setDirection:"unisonRiForceOlder"];
    286         break;
    287     case '+':
    288         [self setDirection:"unisonRiForceNewer"];
    289         break;
    290     case 'm':
    291         [self setDirection:"unisonRiSetMerge"];
    292         break;
    293     case 'd':
    294         [self showDiffs];
    295         break;
    296     case 'R':
    297         [self revertDirection];
    298         break;
    299     default:
    300         NSLog(@"ReconItem.doAction : unknown action");
    301         break;
    302     }
    303 }
    304 
    305 - (void)doIgnore:(unichar)action
    306 {
    307     switch (action) {
    308     case 'I':
    309                 ocamlCall("xS", "unisonIgnorePath", [self fullPath]);
    310         break;
    311     case 'E':
    312                 ocamlCall("xS", "unisonIgnoreExt", [self path]);
    313         break;
    314     case 'N':
    315                 ocamlCall("xS", "unisonIgnoreName", [self path]);
    316         break;
    317     default:
    318         NSLog(@"ReconItem.doIgnore : unknown ignore");
    319         break;
    320     }
    321 }
    322 
    323 /* Sorting functions. These have names equal to
    324    column identifiers + "SortKey", and return NSStrings that
    325    can be automatically sorted with their compare method */
    326 
    327 - (NSString *) leftSortKey
    328 {
    329     return [self replicaSortKey:[self left]];
    330 }
    331 
    332 - (NSString *) rightSortKey
    333 {
    334     return [self replicaSortKey:[self right]];
    335 }
    336 
    337 - (NSString *) replicaSortKey:(NSString *)sortString
    338 {
    339     /* sort order for left and right replicas */
    340 
    341     if ([sortString isEqualToString:@"Created"]) return @"1";
    342     else if ([sortString isEqualToString:@"Deleted"]) return @"2";
    343     else if ([sortString isEqualToString:@"Modified"]) return @"3";
    344     else if ([sortString isEqualToString:@""]) return @"4";
    345     else return @"5";
    346 }
    347 
    348 - (NSString *) directionSortKey
    349 {
    350     /* Since the direction indicators are unsortable images, use
    351        directionSortString instead */
    352 
    353     if ([directionSortString isEqual:@""])
    354         [self direction];
    355     return directionSortString;
    356 }
    357 
    358 - (NSString *) progressSortKey
    359 {
    360     /* Percentages, "done" and "" are sorted OK without help,
    361        but "start " should be sorted after "" and before "0%" */
    362 
    363     NSString * progressString = [self progress];
    364     if ([progressString isEqualToString:@"start "]) progressString = @" ";
    365     return progressString;
    366 }
    367 
    368 - (NSString *) pathSortKey
    369 {
    370     /* default alphanumeric sort is fine for paths */
    371     return [self path];
    372 }
    373 
    374 - (NSString *)progress
    375 {
    376         return nil;
    377 }
    378 
    379 - (BOOL)transferInProgress
    380 {
    381         double soFar = [self bytesTransferred];
    382         return (soFar > 0) && (soFar < [self fileSize]);
    383 }
    384 
    385 - (void)resetProgress
    386 {
    387 }
    388 
    389 - (NSString *)progressString
    390 {
    391         NSString *progress = [self progress];
    392         if ([progress length] == 0. || [progress hasSuffix:@"%"])
    393                 progress = [self transferInProgress] ? [self bytesTransferredString] : @"";
    394         else if ([progress isEqual:@"done"]) progress = @"";
    395         return progress;
    396 }
    397 
    398 - (NSString *)details
    399 {
    400         return nil;
    401 }
    402 
    403 - (NSString *)updateDetails
    404 {
    405     return [self details];
    406 }
    407 
    408 - (BOOL)isConflict
    409 {
    410         return NO;
    411 }
    412 
    413 - (BOOL)changedFromDefault
    414 {
    415         return NO;
    416 }
    417 
    418 - (void)revertDirection
    419 {
    420         [self willChange];
    421     [direction release];
    422     direction = nil;
    423     resolved = NO;
    424 }
    425 
    426 - (BOOL)canDiff
    427 {
    428         return NO;
    429 }
    430 
    431 - (void)showDiffs
    432 {
    433 }
    434 
    435 - (ReconItem *)collapseParentsWithSingleChildren:(BOOL)isRoot
    436 {
    437         return self;
    438 }
    439 
    440 - (NSString *)description
    441 {
    442     return [NSString stringWithFormat:@"ReconItem: %@ %@", fullPath, path];
    443 }
    444 
    445 - (NSString *)debugDescription
    446 {
    447     return [self description];
    448 }
    449 @end
    450 
    451 
    452 // --- Leaf items -- actually corresponding to ReconItems in OCaml
    453 @implementation LeafReconItem
    454 
    455 - initWithRiAndIndex:(OCamlValue *)v index:(long)i
    456 {
    457     [super init];
    458     ri = [v retain];
    459         index = i;
    460     resolved = NO;
    461     directionSortString = @"";
    462     return self;
    463 }
    464 
    465 -(void)dealloc
    466 {
    467     [ri release];
    468     [left release];
    469     [right release];
    470     [progress release];
    471     [details release];
    472 
    473     [super dealloc];
    474 }
    475 
    476 - (NSString *)path
    477 {
    478     if (!path) path = [(NSString *)ocamlCall("S@", "unisonRiToPath", ri) retain];
    479     return path;
    480 }
    481 
    482 - (NSString *)left
    483 {
    484     if (!left) left = [(NSString *)ocamlCall("S@", "unisonRiToLeft", ri) retain];
    485         return left;
    486 }
    487 
    488 - (NSString *)right
    489 {
    490     if (!right)	right = [(NSString *)ocamlCall("S@", "unisonRiToRight", ri) retain];
    491         return right;
    492 }
    493 
    494 - (double)computeFileSize
    495 {
    496   return [(NSNumber *)ocamlCall("N@", "unisonRiToFileSize", ri) doubleValue];
    497 }
    498 
    499 - (double)bytesTransferred
    500 {
    501         if (bytesTransferred == -1.) {
    502                 // need to force to fileSize if done, otherwise may not match up to 100%
    503                 bytesTransferred = ([[self progress] isEqual:@"done"]) ? [self fileSize]
    504                   : [(NSNumber*)ocamlCall("N@", "unisonRiToBytesTransferred", ri) doubleValue];
    505         }
    506         return bytesTransferred;
    507 }
    508 
    509 - (NSImage *)fileIcon
    510 {
    511         NSString *extension = [[self path] pathExtension];
    512 
    513         if ([@"" isEqual:extension]) {
    514                 NSString *type = (NSString *)ocamlCall("S@", "unisonRiToFileType", ri);
    515                 extension = [type isEqual:@"dir"]
    516                         ? NSFileTypeForHFSTypeCode(kGenericFolderIcon)
    517                         : NSFileTypeForHFSTypeCode(kGenericDocumentIcon);
    518         }
    519         return [self iconForExtension:extension];
    520 }
    521 
    522 - (NSString *)dirString
    523 {
    524         return (NSString *)ocamlCall("S@", "unisonRiToDirection", ri);
    525 }
    526 
    527 - (void)setDirection:(char *)d
    528 {
    529         [self willChange];
    530     [super setDirection:d];
    531         ocamlCall("x@", d, ri);
    532 }
    533 
    534 - (NSString *)progress
    535 {
    536     if (!progress) {
    537                 progress = 	[(NSString *)ocamlCall("S@", "unisonRiToProgress", ri) retain];
    538                 if ([progress isEqual:@"FAILED"]) [self updateDetails];
    539     }
    540         return progress;
    541 }
    542 
    543 - (void)resetProgress
    544 {
    545     // Get rid of the memoized progress because we expect it to change
    546         [self willChange];
    547         bytesTransferred = -1.;
    548     [progress release];
    549 
    550         // Force update now so we get the result while the OCaml thread is available
    551         // [self progress];
    552         // [self bytesTransferred];
    553     progress = nil;
    554 }
    555 
    556 - (NSString *)details
    557 {
    558     if (details) return details;
    559     return [self updateDetails];
    560 }
    561 
    562 - (NSString *)updateDetails
    563 {
    564         [details autorelease];
    565         details = [(NSString *)ocamlCall("S@", "unisonRiToDetails", ri) retain];
    566     return details;
    567 }
    568 
    569 - (BOOL)isConflict
    570 {
    571         return ((long)ocamlCall("i@", "unisonRiIsConflict", ri) ? YES : NO);
    572 }
    573 
    574 - (BOOL)changedFromDefault
    575 {
    576         return ((long)ocamlCall("i@", "changedFromDefault", ri) ? YES : NO);
    577 }
    578 
    579 - (void)revertDirection
    580 {
    581         ocamlCall("x@", "unisonRiRevert", ri);
    582         [super revertDirection];
    583 }
    584 
    585 - (BOOL)canDiff
    586 {
    587         return ((long)ocamlCall("i@", "canDiff", ri) ? YES : NO);
    588 }
    589 
    590 - (void)showDiffs
    591 {
    592         ocamlCall("x@i", "runShowDiffs", ri, index);
    593 }
    594 
    595 @end
    596 
    597 @interface NSImage (TintedImage)
    598 - (NSImage *)tintedImageWithColor:(NSColor *) tint operation:(NSCompositingOperation) op;
    599 @end
    600 
    601 @implementation NSImage (TintedImage)
    602 
    603 - (NSImage *)tintedImageWithColor:(NSColor *) tint operation:(NSCompositingOperation) op
    604 {
    605         NSSize size = [self size];
    606         NSRect imageBounds = NSMakeRect(0, 0, size.width, size.height);
    607         NSImage *newImage = [[NSImage alloc] initWithSize:size];
    608 
    609         [newImage lockFocus];
    610         [self compositeToPoint:NSZeroPoint operation:NSCompositeSourceOver];
    611         [tint set];
    612         NSRectFillUsingOperation(imageBounds, op);
    613         [newImage unlockFocus];
    614 
    615         return [newImage autorelease];
    616 }
    617 
    618 @end
    619 
    620 // ----  Parent nodes in grouped items
    621 @implementation ParentReconItem
    622 
    623 - init
    624 {
    625         [super init];
    626         _children = [[NSMutableArray alloc] init];
    627         return self;
    628 }
    629 
    630 - initWithPath:(NSString *)aPath
    631 {
    632         [self init];
    633         path = [aPath retain];
    634         return self;
    635 }
    636 
    637 - (void)dealloc
    638 {
    639         [_children release];
    640         [super dealloc];
    641 }
    642 
    643 - (NSArray *)children;
    644 {
    645         return _children;
    646 }
    647 
    648 - (ReconItem *)findChildWithPath:(NSArray *)pathArray level:(NSUInteger)level{
    649     ReconItem *item;
    650     NSString *element = [pathArray count] ? [pathArray objectAtIndex:level] : @"";
    651 
    652     for (item in _children) {
    653         if ([item isKindOfClass:[ParentReconItem class]] && [[item path] isEqual:element]) {
    654             return item;
    655         }
    656     }
    657     return nil;
    658 }
    659 
    660 - (void)addChild:(ReconItem *)item pathArray:(NSArray *)pathArray level:(NSUInteger)level
    661 {
    662   //    NSLog(@"Adding child: %@", pathArray);
    663         NSString *element = [pathArray count] ? [pathArray objectAtIndex:level] : @"";
    664 
    665         // if we're at the leaf of the path, then add the item
    666         if (((0 == [pathArray count]) && (0 == level)) || (level == [pathArray count]-1)) {
    667                 [item setParent:self];
    668                 [item setPath:element];
    669                 [_children addObject:item];
    670                 return;
    671         }
    672 
    673         // find / add matching parent node
    674         ReconItem *parentItem = [self findChildWithPath:pathArray level:level];
    675         if (parentItem == nil || ![parentItem isKindOfClass:[ParentReconItem class]]) {
    676                 parentItem = [[ParentReconItem alloc] initWithPath:element];
    677                 [parentItem setParent:self];
    678                 [_children addObject:parentItem];
    679                 [parentItem release];
    680         }
    681 
    682         [(ParentReconItem *)parentItem addChild:item pathArray:pathArray level:level+1];
    683 }
    684 
    685 - (void)addChild:(ReconItem *)item nested:(BOOL)nested
    686 {
    687         [item setPath:nil]; // invalidate/reset
    688 
    689         if (nested) {
    690                 [self addChild:item pathArray:[[item path] pathComponents] level:0];
    691         } else {
    692                 [item setParent:self];
    693                 [_children addObject:item];
    694         }
    695 }
    696 
    697 - (void)sortUsingDescriptors:(NSArray *)sortDescriptors
    698 {
    699         // sort our children
    700         [_children sortUsingDescriptors:sortDescriptors];
    701 
    702         // then have them sort theirs
    703         NSUInteger i = [_children count];
    704         while (i--) {
    705                 id child = [_children objectAtIndex:i];
    706                 if ([child isKindOfClass:[ParentReconItem class]]) [child sortUsingDescriptors:sortDescriptors];
    707         }
    708 }
    709 
    710 - (ReconItem *)collapseParentsWithSingleChildren:(BOOL)isRoot
    711 {
    712         // replace ourselves?
    713         if (!isRoot && [_children count] == 1) {
    714                 ReconItem *child = [_children lastObject];
    715                 [child setPath:[path stringByAppendingFormat:@"/%@", [child path]]];
    716                 return [child collapseParentsWithSingleChildren:NO];
    717         }
    718 
    719         // recurse
    720         NSUInteger i = [_children count];
    721         while (i--) {
    722                 ReconItem *child = [_children objectAtIndex:i];
    723                 ReconItem *replacement = [child collapseParentsWithSingleChildren:NO];
    724                 if (child != replacement) {
    725                         [_children replaceObjectAtIndex:i withObject:replacement];
    726                         [replacement setParent:self];
    727                 }
    728         }
    729         return self;
    730 }
    731 
    732 - (void)willChange
    733 {
    734         // invalidate child-based state
    735         // Assuming caches / constant, so not retained / released
    736         // [direction autorelease];
    737         // [directionSortString autorelease];
    738         direction = nil;
    739         directionSortString = nil;
    740         bytesTransferred = -1.;
    741         // fileSize = -1;
    742     // resolved = NO;
    743 
    744         // propagate up parent chain
    745         [parent willChange];
    746 }
    747 
    748 // Propagation methods
    749 - (void)doAction:(unichar)action
    750 {
    751         NSUInteger i = [_children count];
    752         while (i--) {
    753                 ReconItem *child = [_children objectAtIndex:i];
    754                 [child doAction:action];
    755         }
    756 }
    757 
    758 - (void)doIgnore:(unichar)action
    759 {
    760         // handle Path ignores at this level, name and extension at the child nodes
    761         if (action == 'I') {
    762                 [super doIgnore:'I'];
    763         } else {
    764                 NSUInteger i = [_children count];
    765                 while (i--) {
    766                         ReconItem *child = [_children objectAtIndex:i];
    767                         [child doIgnore:action];
    768                 }
    769         }
    770 }
    771 
    772 // Rollup methods
    773 - (long)fileCount
    774 {
    775         if (fileCount == 0) {
    776                 NSUInteger i = [_children count];
    777                 while (i--) {
    778                         ReconItem *child = [_children objectAtIndex:i];
    779                         fileCount += [child fileCount];
    780                 }
    781         }
    782         return fileCount;
    783 }
    784 
    785 - (double)computeFileSize
    786 {
    787         double size = 0;
    788         NSUInteger i = [_children count];
    789         while (i--) {
    790                 ReconItem *child = [_children objectAtIndex:i];
    791                 size += [child fileSize];
    792         }
    793         return size;
    794 }
    795 
    796 - (double)bytesTransferred
    797 {
    798         if (bytesTransferred == -1.) {
    799                 bytesTransferred = 0.;
    800                 NSUInteger i = [_children count];
    801                 while (i--) {
    802                         ReconItem *child = [_children objectAtIndex:i];
    803                         bytesTransferred += [child bytesTransferred];
    804                 }
    805         }
    806         return bytesTransferred;
    807 }
    808 
    809 
    810 
    811 - (NSString *)dirString
    812 {
    813         NSString *rollup = nil;
    814         NSUInteger i = [_children count];
    815         while (i--) {
    816                 ReconItem *child = [_children objectAtIndex:i];
    817                 NSString *dirString = [child dirString];
    818                 if (!rollup || [dirString isEqual:rollup]) {
    819                         rollup = dirString;
    820                 } else {
    821                         // conflict
    822                         if ([dirString isEqual:@"---->"] || [dirString isEqual:@"<----"] || [dirString isEqual:@"<--->"]) {
    823                                 if ([rollup isEqual:@"---->"] || [rollup isEqual:@"<----"] || [rollup isEqual:@"<--->"]) {
    824                                         rollup = @"<--->";
    825                                 }
    826                         } else {
    827                                 rollup = @"<-?->";
    828                         }
    829                 }
    830         }
    831         // NSLog(@"dirString for %@: %@", path, rollup);
    832         return rollup;
    833 }
    834 
    835 - (BOOL)hasConflictedChildren
    836 {
    837         NSString *dirString = [self dirString];
    838         BOOL result = [dirString isEqual:@"<--->"] || [dirString isEqual:@"<-?->"];
    839         // NSLog(@"hasConflictedChildren (%@): %@: %i", [self path], dirString, result);
    840         return result;
    841 }
    842 
    843 static NSMutableDictionary *_parentImages = nil;
    844 static NSColor *_veryLightGreyColor = nil;
    845 - (NSImage *)direction
    846 {
    847         if (!_parentImages) {
    848                 _parentImages = [[NSMutableDictionary alloc] init];
    849                 _veryLightGreyColor = [[NSColor colorWithCalibratedRed:0.7 green:0.7 blue:0.7 alpha:1.0] retain];
    850         }
    851         NSImage *baseImage = [super direction];
    852         NSImage *parentImage = [_parentImages objectForKey:baseImage];
    853         if (!parentImage) {
    854                 // make parent images a grey version of the leaf images
    855                 parentImage = [baseImage tintedImageWithColor:_veryLightGreyColor operation:NSCompositeSourceIn];
    856                 [_parentImages setObject:parentImage forKey:baseImage];
    857         }
    858         return parentImage;
    859 }
    860 
    861 @end