Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

import CommitWindow sources

  • Loading branch information...
commit 3dcd5a383b6f7a41606b9e52d0072221d7aadd90 1 parent ce6e15a
@martinh martinh authored
Showing with 11,042 additions and 0 deletions.
  1. +46 −0 CommitWindow/CWTextView.h
  2. +266 −0 CommitWindow/CWTextView.m
  3. +18 −0 CommitWindow/CXMenuButton.h
  4. +53 −0 CommitWindow/CXMenuButton.m
  5. +46 −0 CommitWindow/CXShading.h
  6. +148 −0 CommitWindow/CXShading.m
  7. +29 −0 CommitWindow/CXTextWithButtonStripCell.h
  8. +461 −0 CommitWindow/CXTextWithButtonStripCell.m
  9. +15 −0 CommitWindow/CommitWindowCommandLine.h
  10. +302 −0 CommitWindow/CommitWindowCommandLine.m
  11. +57 −0 CommitWindow/CommitWindowController.h
  12. +659 −0 CommitWindow/CommitWindowController.m
  13. +6 −0 CommitWindow/CommitWindow_Prefix.pch
  14. BIN  CommitWindow/English.lproj/InfoPlist.strings
  15. +46 −0 CommitWindow/English.lproj/MainMenu.nib/classes.nib
  16. +29 −0 CommitWindow/English.lproj/MainMenu.nib/info.nib
  17. +8,468 −0 CommitWindow/English.lproj/MainMenu.nib/keyedobjects.nib
  18. BIN  CommitWindow/Icons/Action.tiff
  19. BIN  CommitWindow/Icons/ActionPressed.tiff
  20. +28 −0 CommitWindow/Info.plist
  21. +14 −0 CommitWindow/NSString+StatusString.h
  22. +145 −0 CommitWindow/NSString+StatusString.m
  23. +34 −0 CommitWindow/NSTask+CXAdditions.h
  24. +166 −0 CommitWindow/NSTask+CXAdditions.m
  25. +6 −0 CommitWindow/main.m
View
46 CommitWindow/CWTextView.h
@@ -0,0 +1,46 @@
+//
+// CWTextView.m
+// CommitWindow
+//
+// Created by Chris Thomas on 3/7/05.
+// Copyright 2005-2006 Chris Thomas. All rights reserved.
+// MIT license.
+//
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface CWTextView : NSTextView
+{
+ float fMinHeight;
+ float fMaxHeight;
+ float fMinWidth;
+ float fMaxWidth;
+
+ NSRect fInitialViewFrame;
+ NSPoint fInitialMousePoint;
+ BOOL fTrackingGrowBox;
+
+ BOOL fAllowGrowHorizontally;
+ BOOL fAllowGrowVertically;
+}
+
+- (BOOL)allowHorizontalResize;
+- (void)setAllowHorizontalResize:(BOOL)newAllowGrowHorizontally;
+
+- (BOOL)allowVerticalResize;
+- (void)setAllowVerticalResize:(BOOL)newAllowGrowVertically;
+
+- (float)maxWidth;
+- (void)setMaxWidth:(float)newMaxWidth;
+
+- (float)minWidth;
+- (void)setMinWidth:(float)newMinWidth;
+
+- (float)minHeight;
+- (void)setMinHeight:(float)newMinHeight;
+
+- (float)maxHeight;
+- (void)setMaxHeight:(float)newMaxHeight;
+
+@end
View
266 CommitWindow/CWTextView.m
@@ -0,0 +1,266 @@
+//
+// CWTextView.m
+// CommitWindow
+//
+// Created by Chris Thomas on 3/7/05.
+// Copyright 2005-2006 Chris Thomas. All rights reserved.
+// MIT license.
+//
+
+#import "CWTextView.h"
+
+@implementation CWTextView
+
+- (void) awakeFromNib
+{
+ // Arbitrary factory settings
+ fMinHeight = 40.0f;
+ fMaxHeight = 32767.0f;
+ fMinWidth = 100.0f;
+ fMaxWidth = 32767.0f;
+
+ fAllowGrowHorizontally = NO;
+ fAllowGrowVertically = YES;
+}
+
+#if 0
+#pragma mark -
+#pragma mark Do not eat the enter key
+#endif
+
+- (void) keyDown:(NSEvent *)event
+{
+ // don't let the textview eat the enter key
+ if( [[event characters] isEqualToString:@"\x03"] )
+ {
+ [[self nextResponder] keyDown:event];
+ }
+ else
+ {
+ [super keyDown:event];
+ }
+}
+
+#if 0
+#pragma mark -
+#pragma mark Resize box
+#endif
+
+- (NSRect) growBoxRect
+{
+ NSRect bounds = [self bounds];
+ NSRect growBoxRect;
+
+ growBoxRect.size.width = 16;
+ growBoxRect.size.height = 16;
+ growBoxRect.origin.y = NSMaxY(bounds) - growBoxRect.size.height;
+ growBoxRect.origin.x = NSMaxX(bounds) - growBoxRect.size.width;
+
+ return growBoxRect;
+}
+
+- (void) drawRect:(NSRect)rect
+{
+// NSRect bounds = [self bounds];
+ NSRect growBoxRect = [self growBoxRect];
+
+ [super drawRect:rect];
+
+ if( NSContainsRect(rect, growBoxRect) )
+ {
+ [NSGraphicsContext saveGraphicsState];
+ [[NSColor darkGrayColor] set];
+
+ [NSBezierPath clipRect:NSInsetRect(growBoxRect, 1, 1)];
+
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(growBoxRect.origin.x, growBoxRect.origin.y + 20 )
+ toPoint:NSMakePoint(growBoxRect.origin.x + 20, growBoxRect.origin.y)];
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(growBoxRect.origin.x, growBoxRect.origin.y + 24)
+ toPoint:NSMakePoint(growBoxRect.origin.x + 24, growBoxRect.origin.y)];
+ [NSBezierPath strokeLineFromPoint:NSMakePoint(growBoxRect.origin.x, growBoxRect.origin.y + 28)
+ toPoint:NSMakePoint(growBoxRect.origin.x + 28, growBoxRect.origin.y)];
+ [NSGraphicsContext restoreGraphicsState];
+ }
+
+}
+
+- (void) mouseDown:(NSEvent *)event
+{
+ NSPoint locationInWindow = [event locationInWindow];
+ NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
+ NSRect growBoxRect = [self growBoxRect];
+
+ if( NSMouseInRect(locationInView, growBoxRect, YES) )
+ {
+ fInitialViewFrame = [[self enclosingScrollView] frame];
+ fInitialMousePoint = locationInWindow;
+ fTrackingGrowBox = YES;
+ }
+ else
+ {
+ [super mouseDown:event];
+ }
+}
+
+- (void) mouseUp:(NSEvent *)event
+{
+ if(fTrackingGrowBox)
+ {
+ fTrackingGrowBox = NO;
+ }
+ else
+ {
+ [super mouseUp:event];
+ }
+}
+
+- (void) mouseDragged:(NSEvent *)event
+{
+ NSPoint currentPoint = [event locationInWindow];//[self convertPoint: fromView:nil];
+
+ if(fTrackingGrowBox)
+ {
+ NSScrollView * scrollView = [self enclosingScrollView];
+ NSRect scrollFrame = [scrollView frame];
+ NSRect newFrame = scrollFrame;
+ float deltaY;
+
+ // Horizontal
+ if( fAllowGrowHorizontally )
+ {
+ newFrame.size.width = fInitialViewFrame.size.width + (currentPoint.x - fInitialMousePoint.x);
+ if(newFrame.size.width < fMinWidth )
+ {
+ newFrame.size.width = fMinWidth;
+ }
+ else if(newFrame.size.width > fMaxWidth )
+ {
+ newFrame.size.width = fMaxWidth;
+ }
+ }
+
+ // Vertical (FIXME: assumes the scroll view's superview is _not_ flipped)
+ if( fAllowGrowVertically )
+ {
+ deltaY = currentPoint.y - fInitialMousePoint.y;
+ newFrame.size.height = fInitialViewFrame.size.height - deltaY;
+
+ // Check size
+ if(newFrame.size.height < fMinHeight )
+ {
+ newFrame.size.height = fMinHeight;
+ }
+ else if(newFrame.size.height > fMaxHeight )
+ {
+ newFrame.size.height = fMaxHeight;
+ }
+
+ // Adjust origin of frame
+ newFrame.origin.y += scrollFrame.size.height - newFrame.size.height;
+ }
+
+ [scrollView setNeedsDisplayInRect:[scrollView bounds]];
+ [scrollView setFrame:newFrame];
+ [[NSCursor arrowCursor] set];
+ }
+ else
+ {
+ [super mouseDragged:event];
+ }
+}
+
+// This alone is not enough -- see mouseMoved: below -- but it does cause the arrow to be correctly displayed during resize
+- (void) resetCursorRects
+{
+ [super resetCursorRects];
+ [self addCursorRect:[self growBoxRect] cursor:[NSCursor arrowCursor]];
+}
+
+// Required to override NSTextView's setting of the cursor during mouseMoved events
+- (void) mouseMoved:(NSEvent *)event
+{
+ NSPoint locationInWindow = [event locationInWindow];
+ NSPoint locationInView = [self convertPoint:locationInWindow fromView:nil];
+ NSRect growBoxRect = [self growBoxRect];
+
+ if( NSMouseInRect(locationInView, growBoxRect, YES) )
+ {
+ [[NSCursor arrowCursor] set];
+ }
+ else
+ {
+ [super mouseMoved:event];
+ }
+}
+
+#if 0
+#pragma mark -
+#pragma mark Simple accessors
+#endif
+
+// Grow planes
+
+- (BOOL)allowHorizontalResize
+{
+ return fAllowGrowHorizontally;
+}
+
+- (void)setAllowHorizontalResize:(BOOL)newAllowGrowHorizontally
+{
+ fAllowGrowHorizontally = newAllowGrowHorizontally;
+}
+
+- (BOOL)allowVerticalResize
+{
+ return fAllowGrowVertically;
+}
+
+- (void)setAllowVerticalResize:(BOOL)newAllowGrowVertically
+{
+ fAllowGrowVertically = newAllowGrowVertically;
+}
+
+// Geometry
+
+- (float)maxWidth
+{
+ return fMaxWidth;
+}
+
+- (void)setMaxWidth:(float)newMaxWidth
+{
+ fMaxWidth = newMaxWidth;
+}
+
+- (float)minWidth
+{
+ return fMinWidth;
+}
+
+- (void)setMinWidth:(float)newMinWidth
+{
+ fMinWidth = newMinWidth;
+}
+
+- (float)minHeight
+{
+ return fMinHeight;
+}
+
+- (void)setMinHeight:(float)newMinHeight
+{
+ fMinHeight = newMinHeight;
+}
+
+- (float)maxHeight
+{
+ return fMaxHeight;
+}
+
+- (void)setMaxHeight:(float)newMaxHeight
+{
+ fMaxHeight = newMaxHeight;
+}
+
+
+@end
View
18 CommitWindow/CXMenuButton.h
@@ -0,0 +1,18 @@
+//
+// CXMenuButton.h
+//
+// Created by Chris Thomas on 2006-10-09.
+// Copyright 2006 Chris Thomas. All rights reserved.
+// MIT license.
+//
+
+@interface CXMenuButton : NSButton
+{
+ IBOutlet NSMenu * menu;
+}
+
+- (NSMenu *)menu;
+- (void)setMenu:(NSMenu *)aValue;
+
+
+@end
View
53 CommitWindow/CXMenuButton.m
@@ -0,0 +1,53 @@
+//
+// CXMenuButton.m
+//
+// Created by Chris Thomas on 2006-10-09.
+// Copyright 2006 Chris Thomas. All rights reserved.
+// MIT license.
+//
+
+#import "CXMenuButton.h"
+
+@implementation CXMenuButton
+
+// Initialization
+
+- (void) commonInit
+{
+ // Use alternateImage for pressed state
+ [[self cell] setHighlightsBy:NSCellLightsByContents];
+}
+
+- (void) awakeFromNib
+{
+ [self commonInit];
+}
+
+// Events
+
+- (void) mouseDown:(NSEvent *)event
+{
+ [self highlight:YES];
+ [NSMenu popUpContextMenu:menu withEvent:event forView:self withFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ [self highlight:NO];
+}
+
+// Accessors
+
+- (NSMenu *)menu
+{
+ return menu;
+}
+
+- (void)setMenu:(NSMenu *)aValue
+{
+ NSMenu *oldMenu = menu;
+ menu = [aValue retain];
+ [oldMenu release];
+}
+
+@end
+
+
+
+
View
46 CommitWindow/CXShading.h
@@ -0,0 +1,46 @@
+//
+// Copyright 2003,2005-2006 Chris Thomas. All rights reserved.
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// Chris's eXperimental Objective-C interface to CGShading.
+
+#include <ApplicationServices/ApplicationServices.h>
+
+ // delegate for shading function - (void) shade:(float)alpha toColor:(float *)outColor
+typedef struct
+{
+ id target;
+ SEL selector;
+} _CXShadingDelegateMethod;
+
+@interface CXShading : NSObject
+{
+ CGShadingRef fShading;
+ CGColorSpaceRef fColorSpace;
+ CGFunctionRef fFunction;
+ _CXShadingDelegateMethod fMethod;
+
+ struct
+ {
+ CGFloat from[4];
+ CGFloat to[4];
+ } fColors;
+}
+
+- (id)initWithStartingColor:(NSColor *)startColor endingColor:(NSColor *)endColor;
+
+- (void)drawFromPoint:(NSPoint)fromPoint toPoint:(NSPoint)toPoint;
+
+@end
View
148 CommitWindow/CXShading.m
@@ -0,0 +1,148 @@
+//
+// Copyright 2003,2005-2006 Chris Thomas. All rights reserved.
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+#import "CXShading.h"
+#import <objc/objc-runtime.h>
+
+@implementation CXShading
+
+static void ShadingFunction (void * info,
+ const CGFloat * in,
+ CGFloat * out)
+{
+ _CXShadingDelegateMethod * method = (_CXShadingDelegateMethod *) info;
+
+ objc_msgSend( method->target, method->selector, in, out );
+// NSLog( @"%X:%g: %g,%g,%g,%g", method->target, *in, out[0], out[1], out[2], out[3] );
+}
+
+
+- (CGFunctionRef) shadingFunctionForColorspace:(CGColorSpaceRef) colorspace info:(void *)info
+{
+ size_t components;
+ static const CGFloat inputValueRange [2] = { 0, 1 };
+ static const CGFloat outputValueRanges [8] = { 0, 1, 0, 1, 0, 1, 0, 1 };
+ static const CGFunctionCallbacks callbacks = { 0, &ShadingFunction, NULL };
+
+ components = 1 + CGColorSpaceGetNumberOfComponents (colorspace);
+ return CGFunctionCreate ( info,
+ 1,
+ inputValueRange,
+ components,
+ outputValueRanges,
+ &callbacks);
+}
+
+
+// kludge
+static inline CGPoint CGPointFromNSPoint( NSPoint point )
+{
+ return *(CGPoint *)&point;
+}
+
+- (void) commonInit
+{
+ fColorSpace = CGColorSpaceCreateDeviceRGB();
+ fFunction = [self shadingFunctionForColorspace:fColorSpace info:&fMethod];
+
+ fMethod.target = self;
+ fMethod.selector = @selector(linearInterpolationFunction:toColor:);
+}
+
+- (id)initWithStartingColor:(NSColor *)startColor endingColor:(NSColor *)endColor
+{
+ // Get both colors as Device RGBA
+ startColor = [startColor colorUsingColorSpaceName:NSDeviceRGBColorSpace];
+ endColor = [endColor colorUsingColorSpaceName:NSDeviceRGBColorSpace];
+
+ [startColor getRed:&fColors.from[0]
+ green:&fColors.from[1]
+ blue:&fColors.from[2]
+ alpha:&fColors.from[3]];
+
+ [endColor getRed:&fColors.to[0]
+ green:&fColors.to[1]
+ blue:&fColors.to[2]
+ alpha:&fColors.to[3]];
+
+ [self commonInit];
+
+ return self;
+}
+
+- (void) dealloc
+{
+ if(fFunction != NULL)
+ {
+ CGFunctionRelease(fFunction);
+ }
+
+ if(fColorSpace != NULL)
+ {
+ CGColorSpaceRelease(fColorSpace);
+ }
+
+ if(fShading != NULL)
+ {
+ CGShadingRelease(fShading);
+ }
+
+ [super dealloc];
+}
+
+#if 0
+#pragma mark -
+#pragma mark Simple linear gradient
+#endif
+
+static inline float LinearInterpolate(float from, float to, float alpha)
+{
+ return (((1.0f - alpha) * from) + (alpha * to));
+}
+
+- (void) linearInterpolationFunction:(const float *)alpha toColor:(float *)color
+{
+ float a = *alpha;
+
+ color[0] = LinearInterpolate( fColors.from[0], fColors.to[0], a );
+ color[1] = LinearInterpolate( fColors.from[1], fColors.to[1], a );
+ color[2] = LinearInterpolate( fColors.from[2], fColors.to[2], a );
+ color[3] = LinearInterpolate( fColors.from[3], fColors.to[3], a );
+}
+
+#if 0
+#pragma mark -
+#pragma mark Drawing
+#endif
+
+- (void)drawFromPoint:(NSPoint)fromPoint toPoint:(NSPoint)toPoint
+{
+ if(fShading != NULL)
+ {
+ CGShadingRelease(fShading);
+ }
+
+ fShading = CGShadingCreateAxial(fColorSpace, CGPointFromNSPoint(fromPoint), CGPointFromNSPoint(toPoint), fFunction, true, true);
+ if( fShading == NULL )
+ {
+ [NSException raise:@"EvilCGShading" format:@"Cannot allocate CGShading!"];
+ }
+
+ CGContextDrawShading( [[NSGraphicsContext currentContext] graphicsPort], fShading );
+}
+
+
+@end
View
29 CommitWindow/CXTextWithButtonStripCell.h
@@ -0,0 +1,29 @@
+//
+// CXTextWithButtonStripCell.m
+// NSCell supporting row action buttons aligned to one side of a text table column.
+//
+// Created by Chris Thomas on 2006-10-11.
+// Copyright 2006 Chris Thomas. All rights reserved.
+//
+
+@interface CXTextWithButtonStripCell : NSTextFieldCell
+{
+ // buttons contains a set of button definitions to draw at the right (or left) side of the cell.
+ //
+ // Each item is a dictionary with the following entries:
+ // • either @"title" NSString -- localized name of button
+ // OR @"icon" -- NSImage -- icon
+ // • @"menu" -- NSMenu -- if present, indicates that this button is a menu button
+ // • @"invocation" -- NSInvocation -- required if this is a push button, useless for menu buttons
+ //
+ NSMutableArray * fButtons;
+ NSUInteger fButtonPressedIndex;
+ float fButtonStripWidth;
+ BOOL fRightToLeft;
+
+}
+
+- (NSArray *)buttonDefinitions;
+- (void)setButtonDefinitions:(NSArray *)newButtonDefinitions;
+
+@end
View
461 CommitWindow/CXTextWithButtonStripCell.m
@@ -0,0 +1,461 @@
+//
+// CXTextWithButtonStripCell.m
+// NSCell supporting row action buttons aligned to one side of a text table column.
+//
+// Created by Chris Thomas on 2006-10-11.
+// Copyright 2006 Chris Thomas. All rights reserved.
+//
+
+#import "CXTextWithButtonStripCell.h"
+#import "CXShading.h"
+
+#define kVerticalMargin 1.0
+#define kHorizontalMargin 2.0
+#define kMarginBetweenTextAndButtons 8.0
+#define kIconButtonWidth 16.0
+#define kButtonInteriorVerticalEdgeMargin 8.0
+
+
+
+@interface NSBezierPath (CXBezierPathAdditions)
++ (NSBezierPath*)bezierPathWithCapsuleRect:(NSRect)rect;
+@end
+
+@implementation NSBezierPath (CXBezierPathAdditions)
+
++ (NSBezierPath*)bezierPathWithCapsuleRect:(NSRect)rect
+{
+ NSBezierPath * path = [self bezierPath];
+ float radius = 0.5f * MIN(NSWidth(rect), NSHeight(rect));
+
+ rect = NSInsetRect(rect, radius, radius);
+
+ [path appendBezierPathWithArcWithCenter:NSMakePoint(NSMinX(rect), NSMidY(rect)) radius:radius startAngle:90.0 endAngle:270.0];
+ [path appendBezierPathWithArcWithCenter:NSMakePoint(NSMaxX(rect), NSMidY(rect)) radius:radius startAngle:270.0 endAngle:90.0];
+ [path closePath];
+ return path;
+}
+
+@end
+
+@interface CXTextWithButtonStripCell (Private)
+- (NSDictionary *) titleTextAttributes;
+@end
+
+@implementation CXTextWithButtonStripCell
+
+// NSTableView frequently wants to make copies of the cell. Retain anything that later will be released.
+- (id)copyWithZone:(NSZone *)zone
+{
+ CXTextWithButtonStripCell * copiedCell = [super copyWithZone:zone];
+
+ [copiedCell->fButtons retain];
+ return copiedCell;
+}
+
+- (void) dealloc
+{
+ [fButtons release];
+ [super dealloc];
+}
+
+#if 0
+#pragma mark -
+#pragma mark Geometry
+#endif
+
+// Maintaining a cache of the button boundrects simplifies the rest of the code
+- (void)calcButtonRects
+{
+ NSRect buttonRect = NSMakeRect(0,0,16,16);
+ unsigned int buttonsCount = [fButtons count];
+ float currentX = 0.0;
+ float currentButtonWidth;
+ NSString * downwardTriangle = [NSString stringWithFormat:@" %C", 0x25BE];
+
+ for(unsigned int index = 0; index < buttonsCount; index += 1)
+ {
+ NSMutableDictionary * buttonDefinition = [fButtons objectAtIndex:index];
+// NSImage * icon = [buttonDefinition objectForKey:@"icon"];
+ NSString * text = [buttonDefinition objectForKey:@"title"];
+ NSMenu * menu = [buttonDefinition objectForKey:@"menu"];
+
+ if( text != nil )
+ {
+ // Text size
+ // Add popup triangle for menu buttons
+ if( menu != nil && ![text hasSuffix:downwardTriangle] )
+ {
+ text = [text stringByAppendingString:downwardTriangle];
+ [buttonDefinition setObject:text forKey:@"title"];
+ }
+
+ currentButtonWidth = [text sizeWithAttributes:[self titleTextAttributes]].width;
+ }
+ else
+ {
+ // Default to icon width
+ currentButtonWidth = kIconButtonWidth;
+ if( menu != nil )
+ {
+ currentButtonWidth += [downwardTriangle sizeWithAttributes:[self titleTextAttributes]].width;
+ }
+ }
+
+ // Add margin to both sides
+ currentButtonWidth += (kButtonInteriorVerticalEdgeMargin * 2);
+
+ buttonRect.origin.x = currentX;
+ buttonRect.size.width = currentButtonWidth;
+
+ // Cache the calculated rect
+ NSValue * rectValue = [NSValue valueWithRect:buttonRect];
+ [buttonDefinition setObject:rectValue forKey:@"_cachedRect"];
+
+ currentX += (currentButtonWidth + kHorizontalMargin);
+
+ }
+ fButtonStripWidth = currentX;
+}
+
+- (NSRect)rectForButtonAtIndex:(UInt32)rectIndex inCellFrame:(NSRect)cellFrame
+{
+ NSRect buttonRect = [[[fButtons objectAtIndex:rectIndex] objectForKey:@"_cachedRect"] rectValue];
+ if( !fRightToLeft )
+ {
+ // Left to right: need to offset the buttonRects to the right edge of the cellFrame
+ buttonRect = NSOffsetRect(buttonRect, NSMaxX(cellFrame) - fButtonStripWidth, 0.0);
+ }
+
+ buttonRect.origin.y = cellFrame.origin.y;
+ buttonRect.size.height = cellFrame.size.height;
+
+ return buttonRect;
+}
+
+
+#if 0
+#pragma mark -
+#pragma mark Mouse tracking
+#endif
+
+- (UInt32) buttonIndexAtPoint:(NSPoint)point inRect:(NSRect)cellFrame ofView:(NSView *)controlView
+{
+ NSUInteger buttonIndexHit = NSNotFound;
+ unsigned int buttonsCount = [fButtons count];
+
+ for( unsigned int i = 0; i < buttonsCount; i += 1 )
+ {
+ NSRect buttonRect = [self rectForButtonAtIndex:i inCellFrame:cellFrame];
+
+ if( [controlView mouse:point inRect:buttonRect] )
+ {
+ buttonIndexHit = i;
+ break;
+ }
+ }
+
+ return buttonIndexHit;
+}
+
++ (BOOL)prefersTrackingUntilMouseUp
+{
+ return YES;
+}
+
+- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)flag
+{
+ NSPoint origPoint;
+ NSPoint curPoint;
+ BOOL firstIteration = YES;
+ BOOL handledMouseUp = YES;
+
+ origPoint = [controlView convertPoint:[theEvent locationInWindow] fromView:nil];
+ curPoint = origPoint;
+
+ for (;;)
+ {
+ NSMenu * menu;
+ NSUInteger hitButton = [self buttonIndexAtPoint:[controlView convertPoint:[theEvent locationInWindow] fromView:nil]
+ inRect:cellFrame
+ ofView:controlView];
+
+ // Mouse up --> invoke the appropriate invocation, if any
+ if ([theEvent type] == NSLeftMouseUp)
+ {
+ if( hitButton != NSNotFound )
+ {
+ NSInvocation * invocation = [[fButtons objectAtIndex:fButtonPressedIndex] objectForKey:@"invocation"];
+
+ [invocation invoke];
+ }
+
+ fButtonPressedIndex = NSNotFound;
+ break;
+ }
+
+
+ // Exit early if the first hit wasn't a button
+ if( firstIteration && hitButton == NSNotFound )
+ {
+ handledMouseUp = NO;
+ break;
+ }
+
+ // Got a hit?
+ if( hitButton != fButtonPressedIndex )
+ {
+ // Refresh old button
+ if(fButtonPressedIndex != NSNotFound)
+ {
+ [controlView setNeedsDisplayInRect:[self rectForButtonAtIndex:fButtonPressedIndex inCellFrame:cellFrame]];
+ }
+
+ // Refresh current button
+ if(hitButton != NSNotFound)
+ {
+ [controlView setNeedsDisplayInRect:[self rectForButtonAtIndex:hitButton inCellFrame:cellFrame]];
+ }
+
+ fButtonPressedIndex = hitButton;
+ }
+
+ // Pop up the menu, if we have a menu button. popUpContextMenu will track the mouse from this point forward.
+ if(fButtonPressedIndex != NSNotFound)
+ {
+ menu = [[fButtons objectAtIndex:fButtonPressedIndex] objectForKey:@"menu"];
+ if( menu != nil )
+ {
+ [NSMenu popUpContextMenu:menu withEvent:theEvent forView:controlView withFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
+ break;
+ }
+ }
+
+ // Next event
+ theEvent = [[controlView window] nextEventMatchingMask:(NSLeftMouseDraggedMask | NSLeftMouseUpMask)];
+ curPoint = [controlView convertPoint:[theEvent locationInWindow] fromView:nil];
+
+ firstIteration = NO;
+ }
+
+ return handledMouseUp;
+}
+
+#if 0
+#pragma mark -
+#pragma mark Drawing
+#endif
+
+- (BOOL)drawAsHighlighted
+{
+ NSView * selfView = [self controlView];
+ NSWindow * selfWindow = [selfView window];
+
+ return (([selfWindow firstResponder] == selfView)
+ && [selfWindow isKeyWindow]);
+}
+
+- (NSDictionary *) titleTextAttributes
+{
+ NSMutableDictionary * attributes = [NSMutableDictionary dictionary];
+ NSColor * foreColor;
+
+ if( [self isHighlighted] && [self drawAsHighlighted] )
+ {
+ foreColor = [NSColor alternateSelectedControlColor];
+ }
+ else
+ {
+ foreColor = [NSColor whiteColor];
+ }
+
+ [attributes setObject:[NSFont systemFontOfSize:9.0] forKey:NSFontAttributeName];
+// [attributes setObject:foreColor forKey:NSForegroundColorAttributeName]; TODO: should use foreColor for flat style only
+
+ return attributes;
+}
+
+- (void)drawButtonContent:(id)content inRect:(NSRect)rect selected:(BOOL)selected menu:(NSMenu *)menu
+{
+ //
+ // Draw content
+ //
+ if( [content isKindOfClass:[NSString class]] )
+ {
+ NSRect textRect = NSInsetRect(rect, kButtonInteriorVerticalEdgeMargin, 0.5f);
+
+ [content drawInRect:textRect withAttributes:[self titleTextAttributes]];
+ }
+ else if( [content isKindOfClass:[NSImage class]] )
+ {
+ NSRect iconRect = NSInsetRect(rect, kButtonInteriorVerticalEdgeMargin, 1.5f);
+
+ NSFrameRect(iconRect);
+ [content compositeToPoint:iconRect.origin operation:NSCompositeSourceOver];
+
+ // Add popup triangle for menu button
+ if( menu != nil )
+ {
+ NSRect textRect = iconRect;
+ NSString * downwardTriangle = [NSString stringWithFormat:@" %C", 0x25BE];
+
+ textRect.origin.x += kIconButtonWidth;
+ [downwardTriangle drawInRect:textRect withAttributes:[self titleTextAttributes]];
+ }
+ }
+}
+
+- (void)drawFlatButton:(id)content inRect:(NSRect)rect selected:(BOOL)selected menu:(NSMenu *)menu
+{
+ //
+ // Draw background
+ //
+ NSBezierPath * path = [NSBezierPath bezierPathWithCapsuleRect:NSInsetRect(rect, 0.5f, 0.5f)];
+ NSColor * backgroundColor = nil;
+// NSColor * foregroundColor = nil;
+
+ // Table row is selected?
+ if( [self isHighlighted] && [self drawAsHighlighted] )
+ {
+ if( selected )
+ {
+ backgroundColor = [NSColor whiteColor];
+ }
+ else
+ {
+ backgroundColor = [NSColor colorForControlTint:[NSColor currentControlTint]];
+ backgroundColor = [[backgroundColor shadowWithLevel:0.1] blendedColorWithFraction:0.85 ofColor:[NSColor whiteColor]];
+ }
+ }
+ else
+ {
+ if( selected )
+ {
+ backgroundColor = [NSColor colorWithDeviceWhite:0.2 alpha:1.0];
+ }
+ else
+ {
+ backgroundColor = [NSColor lightGrayColor];
+ }
+ }
+
+ [backgroundColor set];
+ [path fill];
+
+ [self drawButtonContent:content inRect:rect selected:selected menu:menu];
+
+}
+
+- (void)drawClickableButton:(id)content inRect:(NSRect)rect selected:(BOOL)selected menu:(NSMenu *)menu
+{
+ //
+ // Draw background
+ //
+ NSBezierPath * path = [NSBezierPath bezierPathWithCapsuleRect:NSInsetRect(rect, 0.5f, 0.5f)];
+ NSColor * borderColor = [NSColor lightGrayColor];
+ NSColor * lightColor = [NSColor colorWithDeviceWhite:1.0 alpha:1.0];
+ NSColor * darkColor = [NSColor colorWithDeviceWhite:0.8 alpha:1.0];
+ CXShading * shading;
+
+ [path setLineWidth:0.0f];
+
+ if( selected )
+ {
+ lightColor = [NSColor colorWithDeviceWhite:0.65 alpha:1.0];
+ darkColor = [NSColor colorWithDeviceWhite:0.85 alpha:1.0];
+ }
+
+ shading = [[CXShading alloc] initWithStartingColor:lightColor endingColor:darkColor];
+
+ [shading autorelease];
+
+ // Interior
+ [NSGraphicsContext saveGraphicsState];
+
+ [path addClip];
+ [shading drawFromPoint:rect.origin toPoint:NSMakePoint(rect.origin.x, NSMaxY(rect))];
+
+ [NSGraphicsContext restoreGraphicsState];
+
+ // Button outline goes on top of the fill, so that we don't lose the inner antialising.
+ [borderColor set];
+ [path stroke];
+
+ [self drawButtonContent:content inRect:rect selected:selected menu:menu];
+}
+
+- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
+{
+ unsigned int buttonsCount = [fButtons count];
+
+ for( unsigned int i = 0; i < buttonsCount; i += 1 )
+ {
+ NSMutableDictionary * buttonDefinition = [fButtons objectAtIndex:i];
+ NSRect buttonRect = [self rectForButtonAtIndex:i inCellFrame:cellFrame];
+ BOOL selected = (fButtonPressedIndex == i);
+ id content = [buttonDefinition objectForKey:@"icon"];
+ NSMenu * menu = [buttonDefinition objectForKey:@"menu"];
+
+ if( content == NULL )
+ {
+ content = [buttonDefinition objectForKey:@"title"];
+ }
+
+ if( [buttonDefinition objectForKey:@"invocation"] == nil
+ && menu == nil)
+ {
+ [self drawFlatButton:content inRect:buttonRect selected:selected menu:menu];
+ }
+ else
+ {
+ [self drawClickableButton:content inRect:buttonRect selected:selected menu:menu];
+ }
+ }
+
+ // Adjust the text bounds to avoid overlapping the buttons
+ if( fRightToLeft )
+ {
+ cellFrame.origin.x += (fButtonStripWidth + kMarginBetweenTextAndButtons);
+ }
+ else
+ {
+ cellFrame.size.width -= (fButtonStripWidth + kMarginBetweenTextAndButtons);
+ }
+
+ [super drawWithFrame:cellFrame inView:controlView];
+}
+
+#if 0
+#pragma mark -
+#pragma mark Accessors
+#endif
+
+- (void)setButtonDefinitions:(NSArray *)newDefs
+{
+ unsigned int buttonsCount = [newDefs count];
+ NSArray * oldButtonDefinitions = fButtons;
+
+ // Get mutable copies of the button definitions; we want to store additional data in them
+ fButtons = [[NSMutableArray alloc] init];
+
+ for(unsigned int index = 0; index < buttonsCount; index += 1)
+ {
+ NSDictionary * button = [newDefs objectAtIndex:index];
+
+ [fButtons addObject:[button mutableCopy]];
+ }
+
+ [oldButtonDefinitions release];
+
+ [self calcButtonRects];
+
+ // Reset pressed index
+ fButtonPressedIndex = NSNotFound;
+}
+
+- (NSArray *)buttonDefinitions
+{
+ return fButtons;
+}
+
+@end
View
15 CommitWindow/CommitWindowCommandLine.h
@@ -0,0 +1,15 @@
+//
+// CommitWindowCommandLine.h
+// CommitWindow
+//
+// Created by Chris Thomas on 6/24/06.
+// Copyright 2006 Chris Thomas. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+#import "CommitWindowController.h"
+
+@interface CommitWindowController(CommandLine)
+- (IBAction) commit:(id) sender;
+- (IBAction) cancel:(id) sender;
+@end
View
302 CommitWindow/CommitWindowCommandLine.m
@@ -0,0 +1,302 @@
+//
+// CommitWindowCommandLine.m
+// CommitWindow
+//
+// Created by Chris Thomas on 6/24/06.
+// Copyright 2006 Chris Thomas. All rights reserved.
+//
+
+#import "CommitWindowCommandLine.h"
+
+#import "NSTask+CXAdditions.h"
+
+@implementation CommitWindowController(CommandLine)
+
+- (void) awakeFromNib
+{
+ NSProcessInfo * processInfo = [NSProcessInfo processInfo];
+ NSArray * args;
+ int i;
+ int argc;
+
+ args = [processInfo arguments];
+ argc = [args count];
+
+ if( args == nil || argc < 2 )
+ {
+ fprintf(stderr, "commit window: Arguments required\n");
+ [self cancel:nil];
+ }
+
+ //
+ // Parse the command line.
+ //
+ // fDiffCommand and fActionCommands are set up according to the arguments given.
+ //
+
+ // Program name is the first argument -- get rid of it.
+ argc -= 1;
+ args = [args subarrayWithRange:NSMakeRange(1, argc)];
+
+ // Populate our NSArrayController with the command line arguments
+ for( i = 0; i < argc; i += 1 )
+ {
+ NSString * argument = [args objectAtIndex:i];
+
+ if( [argument isEqualToString:@"--ask"] )
+ {
+ // Next argument should be the query text.
+ if( i >= (argc - 1) )
+ {
+ fprintf(stderr, "commit window: missing text: --ask \"some text\"\n");
+ [self cancel:nil];
+ }
+
+ i += 1;
+ argument = [args objectAtIndex:i];
+ [fRequestText setStringValue:argument];
+ }
+ else if( [argument isEqualToString:@"--log"] )
+ {
+ // Next argument should be the initial log message text.
+ if( i >= (argc - 1) )
+ {
+ fprintf(stderr, "commit window: missing text: --log \"log message text\"\n");
+ [self cancel:nil];
+ }
+
+ i += 1;
+ argument = [args objectAtIndex:i];
+ [fCommitMessage setString:argument];
+ }
+ else if( [argument isEqualToString:@"--status"] )
+ {
+ // Next argument should be a colon-seperated list of status strings, one for each path
+ if( i >= (argc - 1) )
+ {
+ fprintf(stderr, "commit window: missing text: --status \"A:D:M:M:M\"\n");
+ [self cancel:nil];
+ }
+
+ i += 1;
+ argument = [args objectAtIndex:i];
+ fFileStatusStrings = [[argument componentsSeparatedByString:@":"] retain];
+ }
+ else if( [argument isEqualToString:@"--diff-cmd"] )
+ {
+ // Next argument should be a comma-seperated list of command arguments to use to execute the diff
+ if( i >= (argc - 1) )
+ {
+ fprintf(stderr, "commit window: missing text: --diff-cmd \"/usr/bin/svn,diff\"\n");
+ [self cancel:nil];
+ }
+
+ i += 1;
+ argument = [args objectAtIndex:i];
+ fDiffCommand = [argument retain];
+ }
+ else if( [argument isEqualToString:@"--action-cmd"] )
+ {
+ //
+ // --action-cmd provides an action that may be performed on a file.
+ // Provide multiple action commands by passing --action-cmd multiple times, each with a different command argument.
+ //
+ // The argument to --action-cmd is two comma-seperated lists separated by a colon, of the form "A,M,D:Revert,/usr/local/bin/svn,revert"
+ //
+ // On the left side of the colon is a list of status character or character sequences; a file must have one of these
+ // for this command to be enabled.
+ //
+ // On the right side is a list:
+ // Item 1 is the human-readable name of the command.
+ // Item 2 is the path (either absolute or accessible via $PATH) to the executable.
+ // Items 3 through n are the arguments to the executable.
+ // CommitWindow will append the file path as the last argument before executing the command.
+ // Multiple paths may be appended in the future.
+ //
+ // The executable should return a single line of the form "<new status character(s)><whitespace><file path>" for each path.
+ //
+ // For Subversion, commands might be:
+ // "?:Add,/usr/local/bin/svn,add"
+ // "A:Mark Executable,/usr/local/bin/svn,propset,svn:executable,true"
+ // "A,M,D,C:Revert,/usr/local/bin/svn,revert"
+ // "C:Resolved,/usr/local/bin/svn,resolved"
+ //
+ // Only the first colon is significant, so that, for example, 'svn:executable' in the example above works as expected.
+ // This does scheme assume that neither comma nor colon will be used in status sequences. The file paths themselves may contain
+ // commas, since those are handled out of bounds. We could introduce comma or colon quoting if needed. But I hope not.
+ //
+ if( i >= (argc - 1) )
+ {
+ fprintf(stderr, "commit window: missing text: --action-cmd \"M,Revert,/usr/bin/svn,revert\"\n");
+ [self cancel:nil];
+ }
+
+ i += 1;
+ argument = [args objectAtIndex:i];
+
+ // Get status strings
+ NSString * statusSubstringString;
+ NSString * commandArgumentString;
+ NSArray * statusSubstrings;
+ NSArray * commandArguments;
+ NSRange range;
+
+ range = [argument rangeOfString:@":"];
+ if(range.location == NSNotFound)
+ {
+ fprintf(stderr, "commit window: missing ':' in --action-cmd\n");
+ [self cancel:nil];
+ }
+
+ statusSubstringString = [argument substringToIndex:range.location];
+ commandArgumentString = [argument substringFromIndex:NSMaxRange(range)];
+
+ statusSubstrings = [statusSubstringString componentsSeparatedByString:@","];
+ commandArguments = [commandArgumentString componentsSeparatedByString:@","];
+
+ unsigned int statusSubstringCount = [statusSubstrings count];
+
+ // Add the command to each substring
+ for(unsigned int index = 0; index < statusSubstringCount; index += 1)
+ {
+ NSString * statusSubstring = [statusSubstrings objectAtIndex:index];
+
+ [self addAction:[commandArguments objectAtIndex:0]
+ command:[commandArguments subarrayWithRange:NSMakeRange(1, [commandArguments count] - 1)]
+ forStatus:statusSubstring];
+ }
+ }
+ else
+ {
+ NSMutableDictionary * dictionary = [fFilesController newObject];
+
+ [dictionary setObject:[argument stringByAbbreviatingWithTildeInPath] forKey:@"path"];
+ [fFilesController addObject:dictionary];
+ }
+ }
+
+ //
+ // Done processing arguments, now add status to each item
+ // and choose default commit state
+ //
+ [self setupUserInterface];
+
+}
+
+
+#if 0
+#pragma mark -
+#pragma mark Actions
+#endif
+
+
+
+- (IBAction) commit:(id) sender
+{
+ NSArray * objects = [fFilesController arrangedObjects];
+ int i;
+ int pathsToCommitCount = 0;
+ NSMutableString * commitString;
+
+ [self saveSummary];
+
+ //
+ // Quote any single-quotes in the commit message
+ // \' doesn't work with bash. We must use string concatenation.
+ // This sort of thing is why the Unix Hater's Handbook exists.
+ //
+ commitString = [[[fCommitMessage string] mutableCopy] autorelease];
+ [commitString replaceOccurrencesOfString:@"'" withString:@"'\"'\"'" options:0 range:NSMakeRange(0, [commitString length])];
+
+ fprintf(stdout, "-m '%s' ", [commitString UTF8String] );
+
+ //
+ // Return only the files we care about
+ //
+ for( i = 0; i < [objects count]; i += 1 )
+ {
+ NSMutableDictionary * dictionary;
+ NSNumber * commit;
+
+ dictionary = [objects objectAtIndex:i];
+ commit = [dictionary objectForKey:@"commit"];
+
+ if( commit == nil || [commit boolValue] ) // missing commit key defaults to true
+ {
+ NSMutableString * path;
+
+ //
+ // Quote any single-quotes in the path
+ //
+ path = [dictionary objectForKey:@"path"];
+ path = [[[path stringByStandardizingPath] mutableCopy] autorelease];
+ [path replaceOccurrencesOfString:@"'" withString:@"'\"'\"'" options:0 range:NSMakeRange(0, [path length])];
+
+ fprintf( stdout, "'%s' ", [path UTF8String] );
+ pathsToCommitCount += 1;
+ }
+ }
+
+ fprintf( stdout, "\n" );
+
+ //
+ // SVN will commit the current directory, recursively, if we don't specify files.
+ // So, to prevent surprises, if the user's unchecked all the boxes, let's be on the safe side and cancel.
+ //
+ if( pathsToCommitCount == 0 )
+ {
+ [self cancel:nil];
+ }
+
+ [NSApp terminate:self];
+}
+
+- (IBAction) cancel:(id) sender
+{
+ [self saveSummary];
+
+ fprintf(stdout, "commit window: cancel\n");
+ exit(-128);
+}
+
+
+- (IBAction) doubleClickRowInTable:(id)sender
+{
+ if( fDiffCommand != nil )
+ {
+ static NSString * sCommandAbsolutePath = nil;
+
+ NSMutableArray * arguments = [[fDiffCommand componentsSeparatedByString:@","] mutableCopy];
+ NSString * filePath = [[[[fFilesController arrangedObjects] objectAtIndex:[sender selectedRow]] objectForKey:@"path"] stringByStandardizingPath];
+ NSData * diffData;
+ NSString * errorText;
+ int exitStatus;
+
+ // Resolve the command to an absolute path (only do this once per launch)
+ if(sCommandAbsolutePath == nil)
+ {
+ sCommandAbsolutePath = [[self absolutePathForPath:[arguments objectAtIndex:0]] retain];
+ }
+ [arguments replaceObjectAtIndex:0 withObject:sCommandAbsolutePath];
+
+ // Run the diff
+ [arguments addObject:filePath];
+ exitStatus = [NSTask executeTaskWithArguments:arguments
+ input:nil
+ outputData:&diffData
+ errorString:&errorText];
+ [self checkExitStatus:exitStatus forCommand:arguments errorText:errorText];
+
+ // Success, send the diff to TextMate
+ arguments = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%s/bin/mate", getenv("TM_SUPPORT_PATH")], @"-a", nil];
+
+ exitStatus = [NSTask executeTaskWithArguments:arguments
+ input:diffData
+ outputData:nil
+ errorString:&errorText];
+ [self checkExitStatus:exitStatus forCommand:arguments errorText:errorText];
+ }
+}
+
+
+@end
View
57 CommitWindow/CommitWindowController.h
@@ -0,0 +1,57 @@
+//
+// CommitWindowController.h
+//
+// Created by Chris Thomas on 2/6/05.
+// Copyright 2005 Chris Thomas. All rights reserved.
+// MIT license.
+//
+
+#import <Cocoa/Cocoa.h>
+
+@class CWTextView;
+@class CXMenuButton;
+
+@interface CommitWindowController : NSWindowController <NSMenuDelegate>
+{
+// NSMutableArray * fFiles; // {@"commit", @"path"}
+ IBOutlet NSArrayController * fFilesController;
+
+ IBOutlet NSWindow * fWindow;
+
+ IBOutlet NSTextField * fRequestText;
+ IBOutlet CWTextView * fCommitMessage;
+ IBOutlet NSPopUpButton * fPreviousSummaryPopUp;
+ IBOutlet CXMenuButton * fFileListActionPopUp;
+
+ IBOutlet NSButton * fCancelButton;
+ IBOutlet NSButton * fOKButton;
+
+ IBOutlet NSTableView * fTableView;
+ IBOutlet NSTableColumn * fCheckBoxColumn;
+ IBOutlet NSTableColumn * fStatusColumn;
+ IBOutlet NSTableColumn * fPathColumn;
+
+ IBOutlet NSScrollView * fSummaryScrollView;
+ NSRect fPreviousSummaryFrame;
+ IBOutlet NSView * fLowerControlsView;
+
+ NSString * fDiffCommand;
+ NSMutableDictionary * fActionCommands;
+
+ NSArray * fFileStatusStrings;
+}
+
+- (void) addAction:(NSString *)name command:(NSArray *)arguments forStatus:(NSString *)statusString;
+- (void) setupUserInterface;
+- (void) resetStatusColumnSize;
+
+- (void) saveSummary;
+
+- (IBAction) chooseAllFiles:(id)sender;
+- (IBAction) chooseNoFiles:(id)sender;
+- (IBAction) revertToStandardChosenState:(id)sender;
+
+- (NSString *) absolutePathForPath:(NSString *)path;
+- (void) checkExitStatus:(int)exitStatus forCommand:(NSArray *)arguments errorText:(NSString *)errorText;
+
+@end
View
659 CommitWindow/CommitWindowController.m
@@ -0,0 +1,659 @@
+//
+// CommitWindowController.m
+//
+// Created by Chris Thomas on 2/6/05.
+// Copyright 2005-2007 Chris Thomas. All rights reserved.
+// MIT license.
+//
+
+#import "CommitWindowController.h"
+#import "CXMenuButton.h"
+#import "CWTextView.h"
+
+#import "CXTextWithButtonStripCell.h"
+#import "NSString+StatusString.h"
+#import "NSTask+CXAdditions.h"
+
+#define kStatusColumnWidthForSingleChar 13
+#define kStatusColumnWidthForPadding 13
+
+@interface CommitWindowController (Private)
+- (void) populatePreviousSummaryMenu;
+- (void) windowDidResize:(NSNotification *)notification;
+- (void) summaryScrollViewDidResize:(NSNotification *)notification;
+@end
+
+// Forward string comparisons to NSString
+@interface NSAttributedString (CommitWindowExtensions)
+- (NSComparisonResult)compare:(id)anArgument;
+@end
+
+@implementation NSAttributedString (CommitWindowExtensions)
+- (NSComparisonResult)compare:(id)aString
+{
+ return [[self string] compare:[aString string]];
+}
+@end
+
+
+@implementation CommitWindowController
+
+// Not necessary while CommitWindow is a separate process, but it might be more integrated in the future.
+- (void) dealloc
+{
+ // TODO: make sure the nib objects are being released properly
+
+ [fDiffCommand release];
+ [fActionCommands release];
+ [fFileStatusStrings release];
+
+ [super dealloc];
+}
+
+// Add a command to the array of commands available for the given status substring
+- (void) addAction:(NSString *)name command:(NSArray *)arguments forStatus:(NSString *)statusString
+{
+ NSArray * commandArguments = [NSArray arrayWithObjects:name, arguments, nil];
+ NSMutableArray * commandsForAction = nil;
+
+ if(fActionCommands == nil)
+ {
+ fActionCommands = [[NSMutableDictionary alloc] init];
+ }
+ else
+ {
+ commandsForAction = [fActionCommands objectForKey:statusString];
+ }
+
+ if(commandsForAction == nil)
+ {
+ commandsForAction = [NSMutableArray array];
+ [fActionCommands setObject:commandsForAction forKey:statusString];
+ }
+
+ [commandsForAction addObject:commandArguments];
+}
+
+- (BOOL)standardChosenStateForStatus:(NSString *)status
+{
+ BOOL chosen = YES;
+
+ // Deselect external commits and files not added by default
+ // We intentionally do not deselect file conflicts by default
+ // -- those are most likely to be a problem.
+
+ if( [status hasPrefix:@"X"]
+ || [status hasPrefix:@"?"])
+ {
+ chosen = NO;
+ }
+
+ return chosen;
+}
+
+// fFilesController and fFilesStatusStrings should be set up before calling setupUserInterface.
+- (void) setupUserInterface
+{
+ CXTextWithButtonStripCell * cell = (CXTextWithButtonStripCell *)[fPathColumn dataCell];
+
+ if([cell respondsToSelector:@selector(setLineBreakMode:)])
+ {
+ [cell setLineBreakMode:NSLineBreakByTruncatingHead];
+ }
+
+ //
+ // Set up button strip
+ //
+ NSMutableArray * buttonDefinitions = [NSMutableArray array];
+
+ // Diff command
+ if( fDiffCommand != nil )
+ {
+ NSMutableDictionary * diffButtonDefinition;
+ NSMethodSignature * diffMethodSignature = [self methodSignatureForSelector:@selector(doubleClickRowInTable:)];
+ NSInvocation * diffInvocation = [NSInvocation invocationWithMethodSignature:diffMethodSignature];
+
+ // Arguments 0 and 1
+ [diffInvocation setTarget:self];
+ [diffInvocation setSelector:@selector(doubleClickRowInTable:)];
+
+ // Pretend the table view is the sender
+ [diffInvocation setArgument:&fTableView atIndex:2];
+
+ diffButtonDefinition = [NSMutableDictionary dictionary];
+ [diffButtonDefinition setObject:@"Diff" forKey:@"title"];
+ [diffButtonDefinition setObject:diffInvocation forKey:@"invocation"];
+
+ [buttonDefinitions addObject:diffButtonDefinition];
+ }
+
+ // Action menu
+ if(fActionCommands != nil)
+ {
+ NSMenu * itemActionMenu;
+ NSMutableDictionary * actionMenuButtonDefinition;
+
+ itemActionMenu = [[NSMenu alloc] initWithTitle:@"Test"];
+ [itemActionMenu setDelegate:self];
+
+ actionMenuButtonDefinition = [NSMutableDictionary dictionaryWithObject:itemActionMenu forKey:@"menu"];
+ [actionMenuButtonDefinition setObject:@"Modify" forKey:@"title"];
+
+ [buttonDefinitions addObject:actionMenuButtonDefinition];
+
+ [itemActionMenu release];
+ }
+
+ if( [buttonDefinitions count] > 0 )
+ {
+ [cell setButtonDefinitions:buttonDefinitions];
+ }
+
+ //
+ // Set up summary text view resizing
+ //
+ [self windowDidResize:nil];
+
+ fPreviousSummaryFrame = [fSummaryScrollView frame];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(summaryScrollViewDidResize:)
+ name:NSViewFrameDidChangeNotification
+ object:fSummaryScrollView];
+
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(windowDidResize:)
+ name:NSWindowDidResizeNotification
+ object:fWindow];
+
+ //
+ // Add status to each item and choose default commit state
+ //
+ if( fFileStatusStrings != nil )
+ {
+ NSArray * files = [fFilesController arrangedObjects];
+ int count = MIN([files count], [fFileStatusStrings count]);
+ int i;
+
+ UInt32 maxCharsToDisplay = 0;
+
+ for( i = 0; i < count; i += 1 )
+ {
+ NSMutableDictionary * dictionary = [files objectAtIndex:i];
+ NSString * status = [fFileStatusStrings objectAtIndex:i];
+ BOOL itemSelectedForCommit;
+ UInt32 statusLength;
+
+ // Set high-water mark
+ statusLength = [status length];
+ if( statusLength > maxCharsToDisplay )
+ {
+ maxCharsToDisplay = statusLength;
+ }
+
+ [dictionary setObject:status forKey:@"status"];
+ [dictionary setObject:[status attributedStatusString] forKey:@"attributedStatus"];
+
+ itemSelectedForCommit = [self standardChosenStateForStatus:status];
+ [dictionary setObject:[NSNumber numberWithBool:itemSelectedForCommit] forKey:@"commit"];
+ }
+
+ // Set status column size
+ [fStatusColumn setWidth:12 + maxCharsToDisplay * kStatusColumnWidthForSingleChar + (maxCharsToDisplay-1) * kStatusColumnWidthForPadding];
+ }
+
+ //
+ // Populate previous summary menu
+ //
+ [self populatePreviousSummaryMenu];
+
+ [fTableView setTarget:self];
+ [fTableView setDoubleAction:@selector(doubleClickRowInTable:)];
+
+ //
+ // Map the enter key to the OK button
+ //
+ [fOKButton setKeyEquivalent:@"\x03"];
+ [fOKButton setKeyEquivalentModifierMask:0];
+
+ //
+ // Bring the window to absolute front.
+ // -[NSWindow orderFrontRegardless] doesn't work (maybe because we're an LSUIElement).
+ //
+
+ // Process Manager works, though!
+ {
+ ProcessSerialNumber process;
+
+ GetCurrentProcess(&process);
+ SetFrontProcess(&process);
+ }
+
+
+ [self setWindow:fWindow];
+ [fWindow setLevel:NSModalPanelWindowLevel];
+ [fWindow center];
+
+ //
+ // Grow the window to fit as much of the file list onscreen as possible
+ //
+ {
+ NSScreen * screen = [fWindow screen];
+ NSRect usableRect = [screen visibleFrame];
+ NSRect windowRect = [fWindow frame];
+ NSTableView * tableView = [fPathColumn tableView];
+ float rowHeight = [tableView rowHeight] + [tableView intercellSpacing].height;
+ int rowCount = [[fFilesController arrangedObjects] count];
+ float idealVisibleHeight;
+ float currentVisibleHeight;
+ float deltaVisibleHeight;
+
+ currentVisibleHeight = [[tableView superview] frame].size.height;
+ idealVisibleHeight = (rowHeight * rowCount) + [[tableView headerView] frame].size.height;
+
+// NSLog(@"current: %g ideal:%g", currentVisibleHeight, idealVisibleHeight );
+
+ // Don't bother shrinking the window
+ if(currentVisibleHeight < idealVisibleHeight)
+ {
+ deltaVisibleHeight = (idealVisibleHeight - currentVisibleHeight);
+
+// NSLog( @"old windowRect: %@", NSStringFromRect(windowRect) );
+
+ // reasonable margin
+ usableRect = NSInsetRect( usableRect, 20, 20 );
+ windowRect = NSIntersectionRect(usableRect, NSInsetRect(windowRect, 0, ceilf(0.5f * -deltaVisibleHeight)));
+
+// NSLog( @"new windowRect: %@", NSStringFromRect(windowRect) );
+
+ [fWindow setFrame:windowRect display:NO];
+ }
+ }
+
+ // center again after resize
+ [fWindow center];
+ [fWindow makeKeyAndOrderFront:self];
+
+}
+
+- (void) resetStatusColumnSize
+{
+ //
+ // Add status to each item and choose default commit state
+ //
+ NSArray * files = [fFilesController arrangedObjects];
+ int count = [files count];
+ int i;
+
+ UInt32 maxCharsToDisplay = 0;
+
+ for( i = 0; i < count; i += 1 )
+ {
+ NSMutableDictionary * dictionary = [files objectAtIndex:i];
+ NSString * status = [dictionary objectForKey:@"status"];
+ UInt32 statusLength;
+
+ // Set high-water mark
+ statusLength = [status length];
+ if( statusLength > maxCharsToDisplay )
+ {
+ maxCharsToDisplay = statusLength;
+ }
+ }
+
+ // Set status column size
+ [fStatusColumn setWidth:12 + maxCharsToDisplay * kStatusColumnWidthForSingleChar + (maxCharsToDisplay-1) * kStatusColumnWidthForPadding];
+}
+
+#if 0
+#pragma mark -
+#pragma mark Summary save/restore
+#endif
+
+#define kMaxSavedSummariesCount 5
+#define kDisplayCharsOfSummaryInMenuItemCount 30
+#define kPreviousSummariesKey "prev-summaries"
+#define kPreviousSummariesItemTitle "Previous Summaries"
+
+- (void) populatePreviousSummaryMenu
+{
+ NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
+ NSArray * summaries = [defaults arrayForKey:@kPreviousSummariesKey];
+
+ if( summaries == nil )
+ {
+ // No previous summaries, no menu
+ [fPreviousSummaryPopUp setEnabled:NO];
+ }
+ else
+ {
+ NSMenu * menu = [[NSMenu alloc] initWithTitle:@kPreviousSummariesItemTitle];
+ NSMenuItem * item;
+
+ int summaryCount = [summaries count];
+ int index;
+
+ // PopUp title
+ [menu addItemWithTitle:@kPreviousSummariesItemTitle action:@selector(restoreSummary:) keyEquivalent:@""];
+
+ // Add items in reverse-chronological order
+ for(index = (summaryCount - 1); index >= 0; index -= 1)
+ {
+ NSString * summary = [summaries objectAtIndex:index];
+ NSString * itemName;
+
+ itemName = summary;
+
+ // Limit length of menu item names
+ if( [itemName length] > kDisplayCharsOfSummaryInMenuItemCount )
+ {
+ itemName = [itemName substringToIndex:kDisplayCharsOfSummaryInMenuItemCount];
+
+ // append ellipsis
+ itemName = [itemName stringByAppendingFormat: @"%C", 0x2026];
+ }
+
+ item = [menu addItemWithTitle:itemName action:@selector(restoreSummary:) keyEquivalent:@""];
+ [item setTarget:self];
+
+ [item setRepresentedObject:summary];
+ }
+
+ [fPreviousSummaryPopUp setMenu:menu];
+ }
+}
+
+// To make redo work, we need to add a new undo each time
+- (void) restoreTextForUndo:(NSString *)newSummary
+{
+ NSUndoManager * undoManager = [[fCommitMessage window] undoManager];
+ NSString * oldSummary = [fCommitMessage string];
+
+ [undoManager registerUndoWithTarget:self
+ selector:@selector(restoreTextForUndo:)
+ object:[[oldSummary copy] autorelease]];
+
+ [fCommitMessage setString:newSummary];
+
+}
+
+- (void) restoreSummary:(id)sender
+{
+ NSString * newSummary = [sender representedObject];
+
+ [self restoreTextForUndo:newSummary];
+}
+
+// Save, in a MRU list, the most recent commit summary
+- (void) saveSummary
+{
+ NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults];
+ NSString * latestSummary = [fCommitMessage string];
+
+ // avoid empty string
+ if( ! [latestSummary isEqualToString:@""] )
+ {
+ NSArray * oldSummaries = [defaults arrayForKey:@kPreviousSummariesKey];
+ NSMutableArray * newSummaries;
+
+ if( oldSummaries != nil )
+ {
+ NSUInteger oldIndex;
+
+ newSummaries = [oldSummaries mutableCopy];
+
+ // Already in the array? Move it to latest position
+ oldIndex = [newSummaries indexOfObject:latestSummary];
+ if( oldIndex != NSNotFound )
+ {
+ [newSummaries exchangeObjectAtIndex:oldIndex withObjectAtIndex:[newSummaries count] - 1];
+ }
+ else
+ {
+ // Add object, remove oldest object
+ [newSummaries addObject:latestSummary];
+ if( [newSummaries count] > kMaxSavedSummariesCount )
+ {
+ [newSummaries removeObjectAtIndex:0];
+ }
+ }
+ }
+ else
+ {
+ // First time
+ newSummaries = [NSMutableArray arrayWithObject:latestSummary];
+ }
+
+ [defaults setObject:newSummaries forKey:@kPreviousSummariesKey];
+
+ // Write the defaults to disk
+ [defaults synchronize];
+ }
+}
+
+#if 0
+#pragma mark -
+#pragma mark File action menu
+#endif
+
+
+
+- (void) chooseAllItems:(BOOL)chosen
+{
+ NSArray * files = [fFilesController arrangedObjects];
+ int count = [files count];
+ int i;
+
+ for( i = 0; i < count; i += 1 )
+ {
+ NSMutableDictionary * dictionary = [files objectAtIndex:i];
+
+ [dictionary setObject:[NSNumber numberWithBool:chosen] forKey:@"commit"];
+ }
+}
+
+- (void) choose:(BOOL)chosen itemsWithStatus:(NSString *)status
+{
+ NSArray * files = [fFilesController arrangedObjects];
+ int count = [files count];
+ int i;
+
+ for( i = 0; i < count; i += 1 )
+ {
+ NSMutableDictionary * dictionary = [files objectAtIndex:i];
+
+ if( [[dictionary objectForKey:@"status"] hasPrefix:status] )
+ {
+ [dictionary setObject:[NSNumber numberWithBool:chosen] forKey:@"commit"];
+ }
+ }
+}
+
+- (IBAction) chooseAllFiles:(id)sender
+{
+ [self chooseAllItems:YES];
+}
+
+- (IBAction) chooseNoFiles:(id)sender
+{
+ [self chooseAllItems:NO];
+}
+
+- (IBAction) revertToStandardChosenState:(id)sender
+{
+ NSArray * files = [fFilesController arrangedObjects];
+ int count = [files count];
+ int i;
+
+ for( i = 0; i < count; i += 1 )
+ {
+ NSMutableDictionary * dictionary = [files objectAtIndex:i];
+ BOOL itemChosen = YES;
+ NSString * status = [dictionary objectForKey:@"status"];
+
+ itemChosen = [self standardChosenStateForStatus:status];
+ [dictionary setObject:[NSNumber numberWithBool:itemChosen] forKey:@"commit"];
+ }
+}
+
+#if 0
+#pragma mark -
+#pragma mark Summary view resize
+#endif
+
+- (void) summaryScrollViewDidResize:(NSNotification *)notification
+{
+ // Adjust the size of the lower controls
+ NSRect currentSummaryFrame = [fSummaryScrollView frame];
+ NSRect currentLowerControlsFrame = [fLowerControlsView frame];
+
+ float deltaV = currentSummaryFrame.size.height - fPreviousSummaryFrame.size.height;
+
+ [fLowerControlsView setNeedsDisplayInRect:[fLowerControlsView bounds]];
+
+ currentLowerControlsFrame.size.height -= deltaV;
+
+ [fLowerControlsView setFrame:currentLowerControlsFrame];
+
+ fPreviousSummaryFrame = currentSummaryFrame;
+}
+
+- (void) windowDidResize:(NSNotification *)notification
+{
+ // Adjust max allowed summary size to 60% of window size
+ [fCommitMessage setMaxHeight:[fWindow frame].size.height * 0.60];
+}
+
+#if 0
+#pragma mark -
+#pragma mark Command utilities
+#endif
+
+- (NSString *) absolutePathForPath:(NSString *)path
+{
+ if([path hasPrefix:@"/"])
+ return path;
+
+ NSString * absolutePath = nil;
+ NSString * errorText;
+ int exitStatus;
+ NSArray * arguments = [NSArray arrayWithObjects:@"/usr/bin/which", path, nil];
+
+ exitStatus = [NSTask executeTaskWithArguments:arguments
+ input:nil
+ outputString:&absolutePath
+ errorString:&errorText];
+
+ [self checkExitStatus:exitStatus forCommand:arguments errorText:errorText];
+
+ // Trim whitespace
+ absolutePath = [absolutePath stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+
+ return absolutePath;
+}
+
+- (void) checkExitStatus:(int)exitStatus forCommand:(NSArray *)arguments errorText:(NSString *)errorText
+{
+ if( exitStatus != 0 )
+ {
+ // This error dialog text sucks for an isolated end user, but allows us to diagnose the problem accurately.
+ NSRunAlertPanel(errorText, @"Exit status (%d) while executing %@", @"OK", nil, nil, exitStatus, arguments);
+ [NSException raise:@"ProcessFailed" format:@"Subprocess %@ unsuccessful.", arguments];
+ }
+}
+
+
+#if 0
+#pragma mark -
+#pragma mark ButtonStrip action menu delegate
+#endif
+
+- (void)chooseActionCommand:(id)sender
+{
+ NSMutableArray * arguments = [[sender representedObject] mutableCopy];
+ NSString * pathToCommand;
+ NSMutableDictionary * fileDictionary = [[fFilesController arrangedObjects] objectAtIndex:[fTableView selectedRow]];
+ NSString * filePath = [[fileDictionary objectForKey:@"path"] stringByStandardizingPath];
+ NSString * errorText;
+ NSString * outputStatus;
+ int exitStatus;
+
+ // make sure we have an absolute path
+ pathToCommand = [self absolutePathForPath:[arguments objectAtIndex:0]];
+ [arguments replaceObjectAtIndex:0 withObject:pathToCommand];
+
+ [arguments addObject:filePath];
+
+ exitStatus = [NSTask executeTaskWithArguments:arguments
+ input:nil
+ outputString:&outputStatus
+ errorString:&errorText];
+ [self checkExitStatus:exitStatus forCommand:arguments errorText:errorText];
+
+ //
+ // Set the file status to the new status
+ //
+ NSRange rangeOfStatus;
+ NSString * newStatus;
+
+ rangeOfStatus = [outputStatus rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ if( rangeOfStatus.location == NSNotFound)
+ {
+ NSRunAlertPanel(@"Cannot understand output from command", @"Command %@ returned '%@'", @"OK", nil, nil, arguments, outputStatus);
+ [NSException raise:@"CannotUnderstandReturnValue" format:@"Don't understand %@", outputStatus];
+ }
+
+ newStatus = [outputStatus substringToIndex:rangeOfStatus.location];
+
+ [fileDictionary setObject:newStatus forKey:@"status"];
+ [fileDictionary setObject:[newStatus attributedStatusString] forKey:@"attributedStatus"];
+ [fileDictionary setObject:[NSNumber numberWithBool:[self standardChosenStateForStatus:newStatus]] forKey:@"commit"];
+
+ [self resetStatusColumnSize];
+}
+
+- (void)menuNeedsUpdate:(NSMenu*)menu
+{
+ //
+ // Remove old items
+ //
+ UInt32 itemCount = [menu numberOfItems];
+ for( UInt32 i = 0; i < itemCount; i += 1 )
+ {
+ [menu removeItemAtIndex:0];
+ }
+
+ //
+ // Find action items usable for the selected row
+ //
+ NSArray * keys = [fActionCommands allKeys];
+ NSString * fileStatus = [[[fFilesController arrangedObjects] objectAtIndex:[fTableView selectedRow]] objectForKey:@"status"];
+
+ unsigned int possibleStatusCount = [keys count];
+
+ for(unsigned int index = 0; index < possibleStatusCount; index += 1)
+ {
+ NSString * possibleStatus = [keys objectAtIndex:index];
+
+ if( [fileStatus rangeOfString:possibleStatus].location != NSNotFound )
+ {
+ // Add all the commands we find for this status
+ NSArray * commands = [fActionCommands objectForKey:possibleStatus];
+ unsigned int commandCount = [commands count];
+
+ for(unsigned int arrayOfCommandsIndex = 0; arrayOfCommandsIndex < commandCount; arrayOfCommandsIndex += 1)
+ {
+ NSArray * commandArguments = [commands objectAtIndex:arrayOfCommandsIndex];
+
+ NSMenuItem * item = [menu addItemWithTitle:[commandArguments objectAtIndex:0]
+ action:@selector(chooseActionCommand:)
+ keyEquivalent:@""];
+
+ [item setRepresentedObject:[commandArguments objectAtIndex:1]];
+ [item setTarget:self];
+ }
+ }
+ }
+}
+
+@end
View
6 CommitWindow/CommitWindow_Prefix.pch
@@ -0,0 +1,6 @@
+
+#ifdef __OBJC__
+ #import <Cocoa/Cocoa.h>
+#endif
+
+#import <ApplicationServices/ApplicationServices.h>
View
BIN  CommitWindow/English.lproj/InfoPlist.strings
Binary file not shown
View
46 CommitWindow/English.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,46 @@
+{
+ IBClasses = (
+ {CLASS = CWTextView; LANGUAGE = ObjC; SUPERCLASS = NSTextView; },
+ {
+ CLASS = CXMenuButton;
+ LANGUAGE = ObjC;
+ OUTLETS = {menu = NSMenu; };
+ SUPERCLASS = NSButton;
+ },
+ {
+ CLASS = CXTextWithButtonStripCell;
+ LANGUAGE = ObjC;
+ SUPERCLASS = NSTextFieldCell;
+ },
+ {
+ ACTIONS = {
+ cancel = id;
+ chooseAllFiles = id;
+ chooseNoFiles = id;
+ commit = id;
+ revertToStandardChosenState = id;
+ };
+ CLASS = CommitWindowController;
+ LANGUAGE = ObjC;
+ OUTLETS = {
+ fCancelButton = NSButton;
+ fCheckBoxColumn = NSTableColumn;
+ fCommitMessage = NSTextView;
+ fFileListActionPopUp = CXMenuButton;
+ fFilesController = NSArrayController;
+ fLowerControlsView = NSView;
+ fOKButton = NSButton;
+ fPathColumn = NSTableColumn;
+ fPreviousSummaryPopUp = NSPopUpButton;
+ fRequestText = NSTextField;
+ fStatusColumn = NSTableColumn;
+ fSummaryScrollView = NSScrollView;
+ fTableView = NSTableView;
+ fWindow = NSWindow;
+ };
+ SUPERCLASS = NSWindowController;
+ },
+ {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }
+ );
+ IBVersion = 1;
+}
View
29 CommitWindow/English.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,29 @@
+<?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>468 587 356 240 0 0 1280 1002 </string>
+ <key>IBEditorPositions</key>
+ <dict>
+ <key>263</key>
+ <string>66 389 125 44 0 0 1280 1002 </string>
+ <key>399</key>
+ <string>623 889 208 99 0 0 1280 1002 </string>
+ </dict>
+ <key>IBFramework Version</key>
+ <string>451.0</string>
+ <key>IBOldestOS</key>
+ <integer>3</integer>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>21</integer>
+ <integer>399</integer>
+ <integer>263</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>9A241e</string>
+ <key>IBUsesTextArchiving</key>
+ <true/>
+</dict>
+</plist>
View
8,468 CommitWindow/English.lproj/MainMenu.nib/keyedobjects.nib
8,468 additions, 0 deletions not shown
View
BIN  CommitWindow/Icons/Action.tiff
Binary file not shown
View
BIN  CommitWindow/Icons/ActionPressed.tiff
Binary file not shown
View
28 CommitWindow/Info.plist
@@ -0,0 +1,28 @@
+<?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>CommitWindow</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>com.cjack.tmbundles.commit-window</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>LSUIElement</key>
+ <integer>1</integer>
+</dict>
+</plist>
View
14 CommitWindow/NSString+StatusString.h
@@ -0,0 +1,14 @@
+//
+// NSString+StatusString.h
+// CommitWindow
+//
+// Created by Chris Thomas on 6/24/06.
+// Copyright 2006 Chris Thomas. All rights reserved.
+//
+
+#import <Cocoa/Cocoa.h>
+
+
+@interface NSString(VersionControlStatusString)
+- (NSAttributedString *) attributedStatusString;
+@end
View
145 CommitWindow/NSString+StatusString.m
@@ -0,0 +1,145 @@
+//
+// NSString+StatusString.m
+// CommitWindow
+//
+// Created by Chris Thomas on 6/24/06.
+// Copyright 2006 __MyCompanyName__. All rights reserved.
+//
+
+#import "NSString+StatusString.h"
+
+#define RGB8ComponentTransform(component) ((component) == 0 ? 0.0 : 1.0/(255.0/(component)))
+
+#define OneShotNSColorFromTriplet(accessorName,r,g,b) \
+static inline NSColor * accessorName(void)\
+{\
+ static NSColor * color = nil;\
+ \
+ if(color == nil)\
+ {\
+ color = [[NSColor colorWithDeviceRed:RGB8ComponentTransform(r)\
+ green:RGB8ComponentTransform(g)\
+ blue:RGB8ComponentTransform(b)\
+ alpha:1.0] retain];\
+ }\
+\
+ return color;\
+}
+
+OneShotNSColorFromTriplet(ForeColorForFileAdded, 0x00, 0xAA, 0x00)
+OneShotNSColorFromTriplet(BackColorForFileAdded, 0xBB, 0xFF, 0xB3)
+
+OneShotNSColorFromTriplet(ForeColorForFileModified, 0xEB, 0x64, 0x00)
+OneShotNSColorFromTriplet(BackColorForFileModified, 0xF7, 0xE1, 0xAD)
+
+OneShotNSColorFromTriplet(ForeColorForFileDeleted, 0xFF, 0x00, 0x00)
+OneShotNSColorFromTriplet(BackColorForFileDeleted, 0xF5, 0xBD, 0xBD)
+
+OneShotNSColorFromTriplet(ForeColorForFileConflict, 0x00, 0x80, 0x80)
+OneShotNSColorFromTriplet(BackColorForFileConflict, 0xA3, 0xCE, 0xD0)
+
+OneShotNSColorFromTriplet(ForeColorForFileIgnore, 0x80, 0x00, 0x80)
+OneShotNSColorFromTriplet(BackColorForFileIgnore, 0xED, 0xAE, 0xF5)
+
+OneShotNSColorFromTriplet(ForeColorForExternal, 0xFF, 0xFF, 0xFF)
+OneShotNSColorFromTriplet(BackColorForExternal, 0x00, 0x00, 0x00)
+
+
+static inline void ColorsFromStatus( NSString * status, NSColor ** foreColor, NSColor ** backColor )
+{
+ if([status isEqualToString:@"M"])
+ {
+ *foreColor = ForeColorForFileModified();
+ *backColor = BackColorForFileModified();
+ }
+ else if([status isEqualToString:@"X"])
+ {
+ *foreColor = ForeColorForExternal();
+ *backColor = BackColorForExternal();
+ }
+ else if([status isEqualToString:@"A"])
+ {
+ *foreColor = ForeColorForFileAdded();
+ *backColor = BackColorForFileAdded();
+ }
+ else if([status isEqualToString:@"D"]
+ || [status isEqualToString:@"R"])
+ {
+ *foreColor = ForeColorForFileDeleted();
+ *backColor = BackColorForFileDeleted();
+ }
+ else if([status isEqualToString:@"C"]
+ || [status isEqualToString:@"?"])
+ {
+ *foreColor = ForeColorForFileConflict();
+ *backColor = BackColorForFileConflict();
+ }
+ else
+ {
+ *foreColor = [NSColor controlTextColor];
+ *backColor = [NSColor controlBackgroundColor];
+ }
+}
+
+
+@implementation NSString(VersionControlStatusString)
+
+
+//
+// Return a string with status chars highlighted appropriately appropriate for use in a table, with spaces filled with blanks
+//
+- (NSAttributedString *) attributedStatusString
+{
+ UInt32 length = [self length];
+ NSMutableAttributedString * attributedStatusString = [[[NSMutableAttributedString alloc] init] autorelease];
+ unsigned int i;
+ unichar emSpace = 0x2003;
+ unichar hairSpace = 0x200A;
+ NSAttributedString * spaceString = [[[NSAttributedString alloc] initWithString:@" " attributes:nil] autorelease];
+
+ for( i = 0; i < length; i += 1 )
+ {
+ unichar character = [self characterAtIndex:i];
+ NSString * charString;
+ NSMutableAttributedString * attributedCharString;
+ NSColor * foreColor;
+ NSColor * backColor;
+ NSDictionary * attributes;
+
+ // We pass in underscores for empty multicolumn attributes
+ if(character == '_')
+ {
+ character = emSpace;
+ }
+ charString = [NSString stringWithCharacters:&character length:1];
+
+ ColorsFromStatus( charString, &foreColor, &backColor );
+
+ attributes = [NSDictionary dictionaryWithObjectsAndKeys:foreColor, NSForegroundColorAttributeName,
+ backColor, NSBackgroundColorAttributeName,
+ nil];
+
+ attributedCharString = [[[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%C%@%C", hairSpace, charString, hairSpace] attributes:attributes] autorelease];
+
+ float width = [attributedCharString size].width;
+ float desiredWidth = 13.0f;
+ if(width < desiredWidth)
+ {
+ float hairSpaceWidth = [[[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%C", hairSpace] attributes:attributes] autorelease] size].width;
+ float extraWidth = 0.5f * (desiredWidth - width) + hairSpaceWidth;
+ float scale = logf(extraWidth - (hairSpaceWidth - 1.0f));
+
+ NSMutableDictionary* dict = [NSMutableDictionary dictionary];
+ [dict setObject:[NSNumber numberWithFloat:scale] forKey:NSExpansionAttributeName];
+ [attributedCharString addAttributes:dict range:NSMakeRange(0, 1)];
+ [attributedCharString addAttributes:dict range:NSMakeRange(2, 1)];
+ }
+
+ [attributedStatusString appendAttributedString:attributedCharString];
+ [attributedStatusString appendAttributedString:spaceString];
+ }
+
+ return attributedStatusString;
+}
+
+@end
View
34 CommitWindow/NSTask+CXAdditions.h
@@ -0,0 +1,34 @@
+//
+// NSTask+CXAdditions.h
+//
+// Created by Chris Thomas on 2006-10-20.
+// Copyright 2006 Chris Thomas. All rights reserved.
+//
+
+
+@interface NSTask (CXAdditions)
+
+// For the three methods below:
+// Argument index 0 is an absolute path to the executable.
+// Each file/data/string output is allocated and returned to the caller unless the caller passes NULL.
+// input may be NULL, an NSString object, or an NSData object.
+
+// Return a task (not yet launched) and optionally allocate stdout/stdin/stderr streams for communication with it
++ (NSTask *) taskWithArguments:(NSArray *)arguments
+ input:(NSFileHandle **)outWriteHandle
+ output:(NSFileHandle **)outReadHandle
+ error:(NSFileHandle **)outErrorHandle;
+
+// Atomically execute the task and return output as data
++ (int) executeTaskWithArguments:(NSArray *)args
+ input:(id)inputDataOrString
+ outputData:(NSData **)outputData
+ errorString:(NSString **)errorString;
+
+// Atomically execute the task and return output as string
++ (int) executeTaskWithArguments:(NSArray *)args
+ input:(id)inputDataOrString
+ outputString:(NSString **)outputString
+ errorString:(NSString **)errorString;
+
+@end
View
166 CommitWindow/NSTask+CXAdditions.m
@@ -0,0 +1,166 @@
+//
+// NSTask+CXAdditions.m
+//
+// Created by Chris Thomas on 2006-10-20.
+// Copyright 2006 Chris Thomas. All rights reserved.
+//
+
+#import "NSTask+CXAdditions.h"
+
+@interface NSFileHandle (CXAdditions)
+- (NSData *) reallyReadDataToEndOfFile;
+@end
+
+@implementation NSFileHandle (CXAdditions)
+
+// This method exists mainly for ease of debugging. readDataToEndOfFile should actually do the job.
+- (NSData *) reallyReadDataToEndOfFile
+{
+ NSMutableData * outData = [[[NSMutableData alloc] init] autorelease];
+ NSData * currentData;
+
+ currentData = [self availableData];
+ while( currentData != nil && [currentData length] > 0 )
+ {
+ [outData appendData:currentData];
+ currentData = [self availableData];
+ }
+
+ return outData;
+}
+@end
+
+@implementation NSTask (CXAdditions)
+
+// helper method called in its own thread and writes data to a file descriptor
++ (void)writeDataToFileHandleAndClose