New (native) commit window

This is based on the previous commit window code base. It replaces the NSTextView with a OakTextView for entering the commit messages. This allows us to take advantage of some of the git grammar features, e.g., fixup!.

If other SCM bundles are updated in the future to include any specific grammars, these can be used in the commit window by setting the bundle grammar to "text.SCM-commit", where SCM could be hg or svn for example.

Changes to note:

	*  The Modify row button for the "--action-cmd" commands are now implemented in the action menu and the table context menu.

	*  The shortcut for committing is ⌘↩ (but fn-return seems to (still) work).
This commit is contained in:
Ronald Wampler
2014-03-17 22:13:27 -04:00
committed by Allan Odgaard
parent 645c837262
commit 201f247b2f
10 changed files with 720 additions and 38 deletions

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="5053"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="OakCommitWindow"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application"/>
<tableCellView identifier="path" id="bP4-qe-84i" userLabel="CWTableCellView" customClass="CWTableCellView">
<rect key="frame" x="0.0" y="0.0" width="388" height="23"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="LDd-S9-MMU">
<rect key="frame" x="102" y="2" width="238" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingMiddle" sendsActionOnEndEditing="YES" title="File Path" id="bgw-pu-OYC">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="749" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="HMc-VC-30L">
<rect key="frame" x="26" y="2" width="74" height="17"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="SCM Status" id="xBR-Dj-ye7">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button horizontalHuggingPriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Vs9-e8-EXS">
<rect key="frame" x="3" y="0.0" width="22" height="18"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" bezelStyle="regularSquare" imagePosition="left" controlSize="small" state="on" inset="2" id="mV7-0q-gJ2">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="smallSystem"/>
</buttonCell>
</button>
<button horizontalHuggingPriority="1000" verticalHuggingPriority="750" horizontalCompressionResistancePriority="751" translatesAutoresizingMaskIntoConstraints="NO" id="Hsh-4o-6Ya">
<rect key="frame" x="343" y="1" width="40" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Diff" bezelStyle="rounded" alignment="center" controlSize="mini" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="UTd-Na-4I7">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="miniSystem"/>
</buttonCell>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="LDd-S9-MMU" secondAttribute="bottom" constant="2" id="2YQ-RR-3Fw"/>
<constraint firstItem="LDd-S9-MMU" firstAttribute="leading" secondItem="HMc-VC-30L" secondAttribute="trailing" constant="6" id="521-YW-4UB"/>
<constraint firstAttribute="bottom" secondItem="Hsh-4o-6Ya" secondAttribute="bottom" constant="2" id="PBM-bU-izb"/>
<constraint firstItem="Hsh-4o-6Ya" firstAttribute="leading" secondItem="LDd-S9-MMU" secondAttribute="trailing" constant="6" id="Q35-Qa-6hX"/>
<constraint firstItem="Vs9-e8-EXS" firstAttribute="leading" secondItem="bP4-qe-84i" secondAttribute="leading" constant="6" id="n05-9C-QpO"/>
<constraint firstAttribute="trailing" secondItem="Hsh-4o-6Ya" secondAttribute="trailing" constant="6" id="nMJ-z1-h0y"/>
<constraint firstAttribute="bottom" secondItem="Vs9-e8-EXS" secondAttribute="bottom" constant="3" id="pmk-IU-hgI"/>
<constraint firstAttribute="bottom" secondItem="HMc-VC-30L" secondAttribute="bottom" constant="2" id="qDm-5V-MXE"/>
<constraint firstItem="HMc-VC-30L" firstAttribute="leading" secondItem="Vs9-e8-EXS" secondAttribute="trailing" constant="5" id="sFO-ps-oUQ"/>
</constraints>
<connections>
<outlet property="commitCheckBox" destination="Vs9-e8-EXS" id="gTZ-f6-SQ5"/>
<outlet property="diffButton" destination="Hsh-4o-6Ya" id="Ce8-XX-JT0"/>
<outlet property="statusTextField" destination="HMc-VC-30L" id="pYg-Zj-p1L"/>
<outlet property="textField" destination="LDd-S9-MMU" id="9Q1-jt-T1l"/>
</connections>
</tableCellView>
</objects>
</document>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${TARGET_NAME}</string>
<key>CFBundleName</key>
<string>${TARGET_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.macromates.${APP_NAME}.${TARGET_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>NSPrincipalClass</key>
<string>${TARGET_NAME}</string>
</dict>
</plist>

View File

@@ -0,0 +1,7 @@
@interface CWItem : NSObject <NSCopying>
@property (nonatomic) NSString* path;
@property (nonatomic) BOOL commit;
@property (nonatomic) NSString* scmStatus;
+ (CWItem*)itemWithPath:(NSString*)aPath andSCMStatus:(NSString*)aStatus commit:(BOOL)state;
- (NSComparisonResult)compare:(CWItem*)item;
@end

View File

@@ -0,0 +1,30 @@
#import "CWItem.h"
@implementation CWItem
- (CWItem*)initWithPath:(NSString*)aPath andSCMStatus:(NSString*)aStatus commit:(BOOL)state
{
if((self = [super init]))
{
_path = [aPath stringByStandardizingPath];
_scmStatus = aStatus;
_commit = state;
}
return self;
}
+ (CWItem*)itemWithPath:(NSString*)aPath andSCMStatus:(NSString*)aStatus commit:(BOOL)state
{
return [[CWItem alloc] initWithPath:aPath andSCMStatus:aStatus commit:state];
}
- (id)copyWithZone:(NSZone*)zone
{
CWItem* newItem = [[CWItem allocWithZone:zone] initWithPath:_path andSCMStatus:_scmStatus commit:_commit];
return newItem;
}
- (NSComparisonResult)compare:(CWItem*)item
{
return [[self path] compare:[item path]];
}
@end

View File

@@ -0,0 +1,3 @@
@interface CWStatusStringTransformer : NSValueTransformer
+ (void)register;
@end

View File

@@ -0,0 +1,134 @@
// Created by Chris Thomas on 2/6/05.
// Copyright 2005-2007 Chris Thomas. All rights reserved.
// MIT license.
//
#import "CWStatusStringTransformer.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];\
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"] || [status isEqualToString:@"G"])
{
*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 if([status isEqualToString:@"I"])
{
*foreColor = ForeColorForFileIgnore();
*backColor = BackColorForFileIgnore();
}
else
{
*foreColor = [NSColor controlTextColor];
*backColor = [NSColor controlBackgroundColor];
}
}
static NSAttributedString* attributedStatusString (NSString* aString)
{
NSUInteger length = [aString length];
NSMutableAttributedString* attributedStatusString = [[NSMutableAttributedString alloc] init];
NSAttributedString* spaceString = [[NSAttributedString alloc] initWithString:@" " attributes:nil];
unichar emSpace = 0x2003;
unichar hairSpace = 0x200A;
for(NSUInteger i = 0; i < length; i++)
{
unichar character = [aString 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];
CGFloat width = [attributedCharString size].width;
CGFloat desiredWidth = 13.0f;
if(width < desiredWidth)
{
CGFloat hairSpaceWidth = [[[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%C", hairSpace] attributes:attributes] size].width;
CGFloat extraWidth = 0.5f * (desiredWidth - width) + hairSpaceWidth;
CGFloat 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;
}
@implementation CWStatusStringTransformer
+ (void)register { [NSValueTransformer setValueTransformer:[CWStatusStringTransformer new] forName:@"CWStatusStringTransformer"]; }
+ (Class)transformedValueClass { return [NSAttributedString class]; }
+ (BOOL)allowsReverseTransformation { return YES; }
- (id)transformedValue:(id)value { return attributedStatusString(value); }
- (id)reverseTransformedValue:(id)value { return [value string]; }
@end

View File

@@ -0,0 +1,5 @@
@interface CWTableCellView : NSTableCellView
@property (nonatomic) IBOutlet NSButton* commitCheckBox;
@property (nonatomic) IBOutlet NSButton* diffButton;
@property (nonatomic) IBOutlet NSTextField* statusTextField;
@end

View File

@@ -0,0 +1,5 @@
#import "CWTableCellView.h"
@implementation CWTableCellView
@end

View File

@@ -1,28 +1,92 @@
#import "CommitWindow.h"
#import "CWItem.h"
#import "CWStatusStringTransformer.h"
#import "CWTableCellView.h"
#import <OakAppKit/OakAppKit.h>
#import <OakAppKit/NSAlert Additions.h>
#import <OakAppKit/OakUIConstructionFunctions.h>
#import <OakFoundation/NSString Additions.h>
#import <OakTextView/OakDocumentView.h>
#import <bundles/bundles.h>
#import <document/document.h>
#import <io/io.h>
#import <text/trim.h>
#import <oak/oak.h>
@interface OakCommitWindow : NSWindowController <NSWindowDelegate, NSTableViewDataSource>
@property (nonatomic) NSArray* paths;
@property (nonatomic) NSString* clientPortName;
@property (nonatomic) NSScrollView* scrollView;
@property (nonatomic) NSTableView* tableView;
@property (nonatomic) OakCommitWindow* retainedSelf;
@interface actionCommandObj : NSObject
@property (nonatomic, readonly) NSString* name;
@property (nonatomic, readonly) NSSet* targetStatuses;
@property (nonatomic, readonly) NSArray* command;
+ (actionCommandObj*)actionCommandWithString:(NSString*)aString;
@end
@implementation OakCommitWindow
- (id)init
@implementation actionCommandObj
- (id)initWithName:(NSString*)aName command:(NSArray*)aCommand andTargetStatuses:(NSSet*)theTargetStatuses
{
if((self = [super init]))
{
_paths = @[
@{ @"path" : @"/path/to/foo" },
@{ @"path" : @"/path/to/bar" },
];
_name = aName;
_command = aCommand;
_targetStatuses = theTargetStatuses;
}
return self;
}
+ (actionCommandObj*)actionCommandWithString:(NSString*)aString
{
NSRange range = [aString rangeOfString:@":"];
NSArray* commandComponents = [[aString substringFromIndex:NSMaxRange(range)] componentsSeparatedByString:@","];
NSString* statuses = [aString substringToIndex:range.location];
NSArray* command = [commandComponents subarrayWithRange:NSMakeRange(1, [commandComponents count] - 1)];
return [[actionCommandObj alloc] initWithName:[commandComponents objectAtIndex:0] command:command andTargetStatuses:[NSSet setWithArray:[statuses componentsSeparatedByString:@","]]];
}
@end
static std::map<std::string, std::string> convert (NSDictionary* dict)
{
std::map<std::string, std::string> res;
for(NSString* key in dict)
res[[key UTF8String]] = [[dict objectForKey:key] UTF8String];
return res;
}
static NSString* const kOakCommitWindowCommitMessages = @"commitMessages";
static NSUInteger const kOakCommitWindowCommitMessagesTitleLength = 30;
static NSUInteger const kOakCommitWindowCommitMessagesMax = 5;
@interface OakCommitWindow : NSWindowController <NSWindowDelegate, NSTableViewDelegate, NSMenuDelegate, OakTextViewDelegate>
@property (nonatomic) NSMutableDictionary* options;
@property (nonatomic) NSMutableArray* parameters;
@property (nonatomic) std::map<std::string, std::string> environment;
@property (nonatomic) NSArrayController* arrayController;
@property (nonatomic) NSString* clientPortName;
@property (nonatomic) OakDocumentView* documentView;
@property (nonatomic) NSScrollView* scrollView;
@property (nonatomic) NSTableView* tableView;
@property (nonatomic) NSPopUpButton* actionPopUpButton;
@property (nonatomic) OakCommitWindow* retainedSelf;
@end
@implementation OakCommitWindow
- (id)initWithOptions:(NSDictionary*)someOptions
{
if((self = [super init]))
{
self.clientPortName = someOptions[kOakCommitWindowClientPortName];
[self parseArguments:someOptions[kOakCommitWindowArguments]];
self.environment = convert(someOptions[kOakCommitWindowEnvironment]);
self.documentView = [[OakDocumentView alloc] initWithFrame:NSZeroRect];
self.documentView.textView.delegate = self;
_arrayController = [[NSArrayController alloc] init];
_arrayController.objectClass = [CWItem class];
[CWStatusStringTransformer register];
NSTableColumn* tableColumn = [[NSTableColumn alloc] initWithIdentifier:@"path"];
tableColumn.editable = NO;
tableColumn.dataCell = [[NSTextFieldCell alloc] initTextCell:@""];
[tableColumn.dataCell setLineBreakMode:NSLineBreakByTruncatingMiddle];
NSTableView* tableView = [[NSTableView alloc] initWithFrame:NSZeroRect];
[tableView addTableColumn:tableColumn];
@@ -31,9 +95,15 @@
tableView.usesAlternatingRowBackgroundColors = YES;
tableView.doubleAction = @selector(didDoubleClickTableView:);
tableView.target = self;
tableView.dataSource = self;
tableView.delegate = self;
tableView.menu = [NSMenu new];
tableView.menu.delegate = self;
_tableView = tableView;
[_tableView registerNib:[[NSNib alloc] initWithNibNamed:@"CWTableCellView" bundle:[NSBundle bundleForClass:[self class]]] forIdentifier:@"path"];
[self populateTableView];
[_tableView bind:NSContentBinding toObject:_arrayController withKeyPath:@"arrangedObjects" options:0];
_scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect];
_scrollView.hasVerticalScroller = YES;
_scrollView.hasHorizontalScroller = NO;
@@ -43,19 +113,72 @@
self.window = [[NSWindow alloc] initWithContentRect:NSMakeRect(600, 700, 400, 500) styleMask:(NSTitledWindowMask|NSClosableWindowMask|NSResizableWindowMask|NSMiniaturizableWindowMask|NSTexturedBackgroundWindowMask) backing:NSBackingStoreBuffered defer:NO];
self.window.delegate = self;
self.window.level = NSFloatingWindowLevel;
self.window.releasedWhenClosed = NO;
self.window.title = @"Commit";
[self.window setAutorecalculatesContentBorderThickness:NO forEdge:NSMaxYEdge];
[self.window setContentBorderThickness:29 forEdge:NSMaxYEdge];
NSButton* commitButton = OakCreateButton(@"Commit", NSTexturedRoundedBezelStyle);
NSButton* cancelButton = OakCreateButton(@"Cancel", NSTexturedRoundedBezelStyle);
commitButton.action = @selector(performCommit:);
[commitButton setKeyEquivalent:@"\r"];
[commitButton setKeyEquivalentModifierMask:NSCommandKeyMask];
cancelButton.action = @selector(cancel:);
cancelButton.target = self;
_actionPopUpButton = OakCreateActionPopUpButton(YES);
_actionPopUpButton.bezelStyle = NSTexturedRoundedBezelStyle;
_actionPopUpButton.menu.delegate = self;
NSPopUpButton* previousCommitMessagesPopUpButton = [NSPopUpButton new];
previousCommitMessagesPopUpButton.bordered = YES;
previousCommitMessagesPopUpButton.pullsDown = YES;
previousCommitMessagesPopUpButton.bezelStyle = NSTexturedRoundedBezelStyle;
NSMenuItem* placeholder = [NSMenuItem new];
placeholder.title = @"Previous Commit Messages";
[[previousCommitMessagesPopUpButton cell] setUsesItemFromMenu:NO];
[[previousCommitMessagesPopUpButton cell] setMenuItem:placeholder];
// ========================================
// = Create previous commit messages menu =
// ========================================
NSMenu* aMenu = [previousCommitMessagesPopUpButton menu];
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSArray* commitMessages = [defaults arrayForKey:kOakCommitWindowCommitMessages];
if(commitMessages == nil)
{
[previousCommitMessagesPopUpButton setEnabled:NO];
}
else
{
[commitMessages enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSString* message, NSUInteger idx, BOOL* stop){
NSString* title = message;
if([title length] > kOakCommitWindowCommitMessagesTitleLength)
title = [[title substringToIndex:kOakCommitWindowCommitMessagesTitleLength] stringByAppendingString:@"…"];
NSMenuItem* item = [aMenu addItemWithTitle:title action:@selector(restorePreviousCommitMessage:) keyEquivalent:@""];
[item setToolTip:message];
[item setTarget:self];
[item setRepresentedObject:message];
}];
[previousCommitMessagesPopUpButton setMenu:aMenu];
}
// ===============
// = Constraints =
// ===============
NSDictionary* views = @{
@"previousMessages" : previousCommitMessagesPopUpButton,
@"topDivider" : OakCreateHorizontalLine([NSColor grayColor], [NSColor lightGrayColor]),
@"documentView" : self.documentView,
@"middleDivider" : OakCreateHorizontalLine([NSColor grayColor], [NSColor lightGrayColor]),
@"scrollView" : self.scrollView,
@"bottomDivider" : OakCreateHorizontalLine([NSColor grayColor], [NSColor lightGrayColor]),
@"action" : self.actionPopUpButton,
@"cancel" : cancelButton,
@"commit" : commitButton,
};
@@ -67,18 +190,91 @@
[contentView addSubview:view];
}
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[topDivider]|" options:NSLayoutFormatAlignAllLeft|NSLayoutFormatAlignAllRight metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[documentView(==middleDivider)]|" options:0 metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[scrollView(==bottomDivider)]|" options:0 metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[cancel]-[commit]-(8)-|" options:NSLayoutFormatAlignAllBaseline metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[scrollView(>=50)][bottomDivider]-(5)-[commit]-(6)-|" options:0 metrics:nil views:views]];
self.window.defaultButtonCell = commitButton.cell;
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[cancel]-[commit]-(8)-|" options:NSLayoutFormatAlignAllBaseline metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(8)-[action(==36)]" options:0 metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[previousMessages(>=200)]-(8)-|" options:0 metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(3)-[previousMessages]-(3)-[topDivider][documentView(>=100)][middleDivider][scrollView(>=200)][bottomDivider]-(5)-[commit]-(6)-|" options:0 metrics:nil views:views]];
[contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[bottomDivider]-(5)-[action]-(6)-|" options:0 metrics:nil views:views]];
}
return self;
}
- (void)parseArguments:(NSArray*)args
{
NSArray* optionKeys = @[@"--ask", @"--log", @"--diff-cmd", @"--action-cmd", @"--status"];
args = [args subarrayWithRange:NSMakeRange(1, [args count]-1)];
self.options = [NSMutableDictionary dictionary];
self.parameters = [NSMutableArray array];
NSEnumerator* enumerator = [args objectEnumerator];
NSString* arg;
NSMutableArray* actions = [NSMutableArray array];
if([args count] < 2)
[self cancel:nil];
while(arg = [enumerator nextObject])
{
if([optionKeys containsObject:arg])
{
if(NSString* value = [enumerator nextObject])
{
if([arg isEqualToString:@"--action-cmd"])
[actions addObject:[actionCommandObj actionCommandWithString:value]];
else [self.options addEntriesFromDictionary:@{arg : value}];
}
else
{
[self cancel:nil];
}
}
else
{
[self.parameters addObject:arg];
}
}
if(actions != nil | [actions count] != 0)
[self.options setObject:actions forKey:@"--action-cmd"];
}
- (void)populateTableView
{
auto selected_files = _environment.find("TM_SELECTED_FILES");
BOOL didSelectFiles = selected_files != _environment.end();
NSArray* statuses = [[self.options objectForKey:@"--status"] componentsSeparatedByString:@":"];
for(NSUInteger i = 0; i < [statuses count]; i++)
{
NSString* status = [statuses objectAtIndex:i];
CWItem* item = [CWItem itemWithPath:[self.parameters objectAtIndex:i] andSCMStatus:status commit:([status hasPrefix:@"X"] || ([status hasPrefix:@"?"] && !didSelectFiles)) ? NO : YES];
[self.arrayController addObject:item];
}
}
- (void)showWindow:(id)sender
{
std::string file_type = "text.plain";
auto scm_name = _environment.find("TM_SCM_NAME");
if(scm_name != _environment.end())
{
std::string file_grammar = "text." + scm_name->second + "-commit";
for(auto item : bundles::query(bundles::kFieldGrammarScope, file_grammar, scope::wildcard, bundles::kItemTypeGrammar))
file_type = item->value_for_field(bundles::kFieldGrammarScope);
}
document::document_ptr commitMessage = document::from_content("", file_type);
[self.documentView setDocument:commitMessage];
std::string title = text::format("Commit (%s %s: %s)", path::display_name(_environment["TM_PROJECT_DIRECTORY"]).c_str(), scm_name->second.c_str(), _environment["TM_SCM_BRANCH"].c_str());
[self.window setTitle:[NSString stringWithCxxString:title]];
[self.window recalculateKeyViewLoop];
[self.window makeFirstResponder:self.documentView];
[super showWindow:sender];
self.retainedSelf = self;
@@ -86,7 +282,8 @@
- (void)windowWillClose:(NSNotification*)aNotification
{
[self sendCommitMessageToClient:YES];
[self.documentView setDocument:document::document_ptr()];
[self sendCommitMessageToClient:NO];
}
- (void)sendCommitMessageToClient:(BOOL)success
@@ -100,9 +297,21 @@
if(success)
{
NSString* commitMessage = [NSString stringWithCxxString:self.documentView.document->content()];
if([[commitMessage stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] > 0)
[self saveCommitMessage:commitMessage];
NSMutableArray* outputArray = [NSMutableArray array];
[outputArray addObject:[NSString stringWithFormat:@" -m '%@' ", [commitMessage stringByReplacingOccurrencesOfString:@"'" withString:@"'\"'\"'"]]];
for(CWItem* item in [_arrayController arrangedObjects])
{
if(item.commit)
[outputArray addObject:[NSString stringWithCxxString:path::escape([item.path UTF8String])]];
}
[outputArray addObject:@"\n"];
[proxy connectFromServerWithOptions:@{
kOakCommitWindowStandardOutput : @"Hello world",
kOakCommitWindowStandardError : @"",
kOakCommitWindowStandardOutput : [outputArray componentsJoinedByString:@" "],
kOakCommitWindowReturnCode : @0,
}];
}
@@ -119,17 +328,101 @@
[self performSelector:@selector(setRetainedSelf:) withObject:nil afterDelay:0];
}
- (void)chooseAllItems:(BOOL)aState
{
for(CWItem* item in [_arrayController arrangedObjects])
item.commit = aState;
}
- (NSString*)absolutePathForPath:(NSString*)path
{
NSString* newPath;
std::string const oldPath = [path UTF8String];
std::string res = io::exec("/usr/bin/which", oldPath.c_str(), NULL);
if(res != NULL_STR)
newPath = [NSString stringWithCxxString:path::escape(text::trim(res))];
else newPath = [NSString stringWithCxxString:path::escape([path UTF8String])];
return newPath;
}
- (void)saveCommitMessage:(NSString*)aCommitMessage
{
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray* messages = [[defaults arrayForKey:kOakCommitWindowCommitMessages] mutableCopy];
if(messages)
{
NSUInteger currentIndex = [messages indexOfObject:aCommitMessage];
if(currentIndex != NSNotFound)
[messages removeObjectAtIndex:currentIndex];
[messages addObject:aCommitMessage];
if([messages count] > kOakCommitWindowCommitMessagesMax)
[messages removeObjectAtIndex:0];
}
else
{
messages = [NSMutableArray arrayWithObject:aCommitMessage];
}
[defaults setObject:messages forKey:kOakCommitWindowCommitMessages];
[defaults synchronize];
}
- (void)restorePreviousCommitMessage:(id)sender
{
NSString* message = [sender representedObject];
document::document_ptr commitMessage = document::from_content([message UTF8String], self.documentView.document->file_type());
[self.documentView setDocument:commitMessage];
}
- (void)performBundleItem:(bundles::item_ptr const&)anItem
{
if(anItem->kind() == bundles::kItemTypeTheme)
{
[self.documentView setThemeWithUUID:[NSString stringWithCxxString:anItem->uuid()]];
}
else
{
[self showWindow:self];
[self.window makeFirstResponder:self.documentView.textView];
[self.documentView.textView performBundleItem:anItem];
}
}
// ==================
// = Action Methods =
// ==================
- (void)didDoubleClickTableView:(id)sender
{
if(_tableView.clickedRow == -1)
if(_tableView.clickedRow == -1 && [_tableView rowForView:sender] == -1)
return;
NSDictionary* row = _paths[_tableView.clickedRow];
NSLog(@"%s show diff for %@", sel_getName(_cmd), row);
if(NSString* diffCommand = [self.options objectForKey:@"--diff-cmd"])
{
NSInteger row = [_tableView rowForView:sender] == -1 ? _tableView.clickedRow : [_tableView rowForView:sender];
std::string const pwd = _environment["PWD"];
std::string const tm_mate = _environment["TM_MATE"];
NSMutableArray* arguments = [[diffCommand componentsSeparatedByString:@","] mutableCopy];
NSString* filePath = [[[_arrayController arrangedObjects] objectAtIndex:row] path];
[arguments replaceObjectAtIndex:0 withObject:[self absolutePathForPath:[arguments objectAtIndex:0]]];
NSString* joinedArguments = [arguments componentsJoinedByString:@" "];
std::string const cmd_string = text::format("cd %s && %s %s|%s --async", path::escape(pwd).c_str(), [joinedArguments UTF8String], path::escape([filePath UTF8String]).c_str(), path::escape(tm_mate).c_str());
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
bool success = io::exec(_environment, "/bin/sh", "-c", cmd_string.c_str(), NULL) != NULL_STR;
if(!success)
{
dispatch_async(dispatch_get_main_queue(), ^{
NSAlert* alert = [NSAlert tmAlertWithMessageText:@"Failed running diff command." informativeText:[NSString stringWithCxxString:cmd_string] buttons:@"OK", nil];
OakShowAlertForWindow(alert, self.window, ^(NSInteger returnCode){});
});
}
});
}
}
- (void)performCommit:(id)sender
@@ -144,19 +437,128 @@
[self close];
}
// =========================
// = NSTableViewDataSource =
// =========================
- (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView
- (void)checkAll:(id)sender
{
return [_paths count];
[self chooseAllItems:YES];
}
- (id)tableView:(NSTableView*)aTableView objectValueForTableColumn:(NSTableColumn*)aTableColumn row:(NSInteger)rowIndex
- (void)uncheckAll:(id)sender
{
NSDictionary* row = _paths[rowIndex];
return row[aTableColumn.identifier];
[self chooseAllItems:NO];
}
- (void)performActionCommand:(id)sender
{
actionCommandObj* cmd = [sender representedObject];
NSMutableArray* arguments = [cmd.command mutableCopy];
NSInteger row = [_tableView clickedColumn] == -1 ? [_tableView selectedRow] : [_tableView clickedRow];
std::string const filePath = [[[[_arrayController arrangedObjects] objectAtIndex:row] path] UTF8String];
NSString* pathToCommand = [self absolutePathForPath:[arguments objectAtIndex:0]];
[arguments replaceObjectAtIndex:0 withObject:pathToCommand];
std::string const cmd_string = text::format("%s %s", [[arguments componentsJoinedByString:@" "] UTF8String], path::escape(filePath).c_str());
std::string res = io::exec(_environment, "/bin/sh", "-c", cmd_string.c_str(), NULL);
if(res != NULL_STR)
{
NSString* outputStatus = [NSString stringWithCxxString:res];
NSRange rangeOfStatus = [outputStatus rangeOfCharacterFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if(rangeOfStatus.location == NSNotFound)
{
NSAlert* alert = [NSAlert tmAlertWithMessageText:@"Cannot understand output from command" informativeText:[NSString stringWithCxxString:cmd_string] buttons:@"OK", nil];
OakShowAlertForWindow(alert, self.window, ^(NSInteger returnCode){});
}
NSString* newStatus = [outputStatus substringToIndex:rangeOfStatus.location];
CWItem* item = [[_arrayController arrangedObjects] objectAtIndex:row];
item.scmStatus = newStatus;
item.commit = NO;
}
else
{
NSAlert* alert = [NSAlert tmAlertWithMessageText:@"Failed running command" informativeText:[NSString stringWithCxxString:cmd_string] buttons:@"OK", nil];
OakShowAlertForWindow(alert, self.window, ^(NSInteger returnCode){});
}
}
// ===============
// = Action menu =
// ===============
- (void)menuNeedsUpdate:(NSMenu*)menu
{
[menu removeAllItems];
if([_tableView clickedColumn] == -1)
[menu addItemWithTitle:@"Dummy" action:@selector(nop:) keyEquivalent:@""];
NSInteger row = [_tableView clickedRow];
if(row == -1 && [_tableView selectedRow] == -1)
{
[menu addItemWithTitle:@"Check All" action:@selector(checkAll:) keyEquivalent:@""];
[menu addItemWithTitle:@"Uncheck All" action:@selector(uncheckAll:) keyEquivalent:@""];
}
else
{
if(NSArray* commands = [self.options objectForKey:@"--action-cmd"])
{
for(actionCommandObj* cmd in commands)
{
NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:cmd.name action:@selector(performActionCommand:) keyEquivalent:@""];
[item setRepresentedObject:cmd];
[menu addItem:item];
}
[menu addItem:[NSMenuItem separatorItem]];
}
[menu addItemWithTitle:@"Check All" action:@selector(checkAll:) keyEquivalent:@""];
[menu addItemWithTitle:@"Uncheck All" action:@selector(uncheckAll:) keyEquivalent:@""];
}
}
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem
{
BOOL active = YES;
NSInteger row = [_tableView clickedColumn] == -1 || [_tableView clickedRow] == -1 ? [_tableView selectedRow] : [_tableView clickedRow];
if([menuItem action] == @selector(performActionCommand:))
{
CWItem* cwItem = [[_arrayController arrangedObjects] objectAtIndex:row];
actionCommandObj* cmd = [menuItem representedObject];
if(![cmd.targetStatuses containsObject:cwItem.scmStatus])
active = NO;
}
return active;
}
// ========================
// = OakTextView Delegate =
// ========================
- (std::map<std::string, std::string>)variables
{
std::map<std::string, std::string> res;
auto project_directory = _environment.find("TM_PROJECT_DIRECTORY");
if(project_directory != _environment.end())
res["TM_PROJECT_DIRECTORY"] = project_directory->second;
return res;
}
// ========================
// = NSTableView Delegate =
// ========================
- (NSView*)tableView:(NSTableView*)aTableView viewForTableColumn:(NSTableColumn*)aTableColumn row:(NSInteger)row
{
CWTableCellView* cellView = [aTableView makeViewWithIdentifier:@"path" owner:self];
[cellView.textField bind:NSValueBinding toObject:cellView withKeyPath:@"objectValue.path" options:0];
[cellView.commitCheckBox bind:NSValueBinding toObject:cellView withKeyPath:@"objectValue.commit" options:0];
[cellView.statusTextField bind:NSValueBinding toObject:cellView withKeyPath:@"objectValue.scmStatus" options:@{ NSValueTransformerNameBindingOption : @"CWStatusStringTransformer" }];
[cellView.diffButton setAction:@selector(didDoubleClickTableView:)];
[cellView.diffButton setTarget:self];
if(![self.options objectForKey:@"--diff-cmd"])
[cellView.diffButton setHidden:YES];
return cellView;
}
@end
@@ -185,8 +587,7 @@
- (void)connectFromClientWithOptions:(NSDictionary*)someOptions
{
OakCommitWindow* commitWindow = [[OakCommitWindow alloc] init];
commitWindow.clientPortName = someOptions[kOakCommitWindowClientPortName];
OakCommitWindow* commitWindow = [[OakCommitWindow alloc] initWithOptions:someOptions];
[commitWindow showWindow:self];
}
@end

View File

@@ -1,4 +1,5 @@
SOURCES = src/*.mm
EXPORT = src/CommitWindow.h
LINK += OakAppKit OakFoundation
LINK += OakAppKit OakFoundation OakTextView bundles document io ns text
CP_Resources = resources/*
FRAMEWORKS = Cocoa