├── Mockups ├── completions_bright.png └── completions_dark.png ├── English.lproj └── InfoPlist.strings ├── Dialog2.h ├── Commands ├── Utilities │ ├── TextMate.h │ ├── ValueTransformers.h │ ├── TextMate.mm │ └── ValueTransformers.mm ├── prototype │ ├── TMDChameleon.h │ ├── TMDChameleon.mm │ └── prototype.mm ├── tooltip │ ├── TMDHTMLTips.h │ ├── tooltip.mm │ └── TMDHTMLTips.mm ├── nib │ ├── TMDNibController.h │ ├── nib.mm │ └── TMDNibController.mm ├── insert.mm ├── popup │ ├── TMDIncrementalPopUpMenu.h │ ├── popup.mm │ └── TMDIncrementalPopUpMenu.mm ├── defaults.mm ├── images.mm ├── alert.mm ├── help.mm ├── menu.mm └── filepanel.mm ├── TMDCommand.h ├── default.rave ├── README.mdown ├── CLIProxy.h ├── Info.plist ├── TMDCommand.mm ├── Dialog2.mm ├── CLIProxy.mm └── tm_dialog2.mm /Mockups/completions_bright.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textmate/dialog/master/Mockups/completions_bright.png -------------------------------------------------------------------------------- /Mockups/completions_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textmate/dialog/master/Mockups/completions_dark.png -------------------------------------------------------------------------------- /English.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/textmate/dialog/master/English.lproj/InfoPlist.strings -------------------------------------------------------------------------------- /Dialog2.h: -------------------------------------------------------------------------------- 1 | static NSString* const kDialogServerConnectionName = @"com.macromates.dialog"; 2 | 3 | @protocol DialogServerProtocol 4 | - (void)connectFromClientWithOptions:(id)anArgument; 5 | @end 6 | -------------------------------------------------------------------------------- /Commands/Utilities/TextMate.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSObject (OakTextView) 4 | - (NSPoint)positionForWindowUnderCaret; 5 | @end 6 | 7 | void insert_text (NSString* someText); 8 | void insert_snippet (NSString* aSnippet); 9 | -------------------------------------------------------------------------------- /Commands/prototype/TMDChameleon.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMDChameleon.h 3 | // Created by Allan Odgaard on 2007-06-26. 4 | // 5 | 6 | @interface TMD2Chameleon : NSObject 7 | + (BOOL)createSubclassNamed:(NSString*)aName withValues:(NSDictionary*)values; 8 | @end 9 | -------------------------------------------------------------------------------- /Commands/tooltip/TMDHTMLTips.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMDIncrementalPopUpMenu.h 3 | // 4 | // Created by Ciarán Walsh on 2007-08-19. 5 | // 6 | 7 | @interface TMDHTMLTip : NSWindow 8 | + (void)showWithContent:(NSString*)content atLocation:(NSPoint)point transparent:(BOOL)transparent; 9 | @end 10 | -------------------------------------------------------------------------------- /Commands/Utilities/ValueTransformers.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface OakIntArrayToIndexPathTransformer : NSValueTransformer 4 | @end 5 | 6 | @interface OakIntArrayToIndexSetTransformer : NSValueTransformer 7 | @end 8 | 9 | @interface OakStringToColorTransformer : NSValueTransformer 10 | @end 11 | -------------------------------------------------------------------------------- /TMDCommand.h: -------------------------------------------------------------------------------- 1 | #import "CLIProxy.h" 2 | 3 | @interface TMDCommand : NSObject 4 | + (void)registerObject:(id)anObject forCommand:(NSString*)aCommand; 5 | + (NSDictionary*)registeredCommands; 6 | 7 | + (id)objectForCommand:(NSString*)aCommand; 8 | 9 | + (id)readPropertyList:(NSFileHandle*)aFileHandle error:(NSError**)error; 10 | + (void)writePropertyList:(id)aPlist toFileHandle:(NSFileHandle*)aFileHandle; 11 | 12 | - (NSString*)commandDescription; 13 | - (NSString*)usageForInvocation:(NSString*)invocation; 14 | @end 15 | -------------------------------------------------------------------------------- /default.rave: -------------------------------------------------------------------------------- 1 | target "tm_dialog2" { 2 | sources tm_dialog2.mm 3 | executable "${target}" 4 | frameworks Foundation 5 | } 6 | 7 | target "Dialog2" { 8 | add LN_FLAGS "-bundle" 9 | 10 | prefix "${target}.tmplugin/Contents" 11 | 12 | files English.lproj "Resources" 13 | files @tm_dialog2 "Resources" 14 | files Info.plist "." 15 | 16 | sources CLIProxy.mm Dialog2.mm OptionParser.mm TMDCommand.mm 17 | sources Commands/**/*.mm 18 | executable "MacOS/${target}" 19 | frameworks Cocoa WebKit Quartz 20 | } 21 | -------------------------------------------------------------------------------- /Commands/nib/TMDNibController.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMDNibController.h 3 | // Dialog2 4 | // 5 | 6 | @interface TMDNibController : NSObject 7 | + (TMDNibController*)controllerForToken:(NSString*)aToken; 8 | + (NSArray*)controllers; 9 | 10 | @property (nonatomic, readonly) NSString* token; 11 | @property (nonatomic) NSWindow* window; 12 | - (id)initWithNibPath:(NSString*)aPath; 13 | - (void)showWindowAndCenter:(BOOL)shouldCenter; 14 | 15 | - (void)addClientFileHandle:(NSFileHandle*)aFileHandle modal:(BOOL)flag; 16 | - (void)updateParametersWith:(id)plist; 17 | - (void)tearDown; 18 | @end 19 | -------------------------------------------------------------------------------- /Commands/insert.mm: -------------------------------------------------------------------------------- 1 | #import "../Dialog2.h" 2 | #import "../TMDCommand.h" 3 | #import "Utilities/TextMate.h" 4 | 5 | @interface TMDInsertCommands : TMDCommand 6 | @end 7 | 8 | @implementation TMDInsertCommands 9 | + (void)load 10 | { 11 | [TMDCommand registerObject:[self new] forCommand:@"x-insert"]; 12 | } 13 | 14 | - (void)handleCommand:(CLIProxy*)proxy; 15 | { 16 | NSDictionary* args = [proxy parameters]; 17 | 18 | if(NSString* text = [args objectForKey:@"text"]) 19 | insert_text(text); 20 | else if(NSString* snippet = [args objectForKey:@"snippet"]) 21 | insert_snippet(snippet); 22 | } 23 | @end 24 | -------------------------------------------------------------------------------- /Commands/popup/TMDIncrementalPopUpMenu.h: -------------------------------------------------------------------------------- 1 | // 2 | // TMDIncrementalPopUpMenu.h 3 | // 4 | // Created by Joachim Mårtensson on 2007-08-10. 5 | // 6 | 7 | #import "../../CLIProxy.h" 8 | 9 | static NSUInteger const MAX_ROWS = 15; 10 | 11 | @interface TMDIncrementalPopUpMenu : NSWindow 12 | @property (nonatomic) NSPoint caretPos; 13 | - (id)initWithItems:(NSArray*)someSuggestions alreadyTyped:(NSString*)aUserString staticPrefix:(NSString*)aStaticPrefix additionalWordCharacters:(NSString*)someAdditionalWordCharacters caseSensitive:(BOOL)isCaseSensitive writeChoiceToFileDescriptor:(NSFileHandle*)aFileDescriptor; 14 | @end 15 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | If not otherwise specified (see below), files in this repository fall under the following license: 4 | 5 | Permission to copy, use, modify, sell and distribute this 6 | software is granted. This software is provided "as is" without 7 | express or implied warranty, and with no claim as to its 8 | suitability for any purpose. 9 | 10 | An exception is made for files in readable text which contain their own license information, or files where an accompanying file exists (in the same directory) with a “-license” suffix added to the base-name name of the original file, and an extension of txt, html, or similar. For example “tidy” is accompanied by “tidy-license.txt”. -------------------------------------------------------------------------------- /CLIProxy.h: -------------------------------------------------------------------------------- 1 | // 2 | // CLIProxy.h 3 | // Dialog2 4 | // 5 | // Created by Ciaran Walsh on 16/02/2008. 6 | // 7 | 8 | @interface CLIProxy : NSObject 9 | @property (nonatomic, readonly) NSFileHandle* inputHandle; 10 | @property (nonatomic, readonly) NSFileHandle* outputHandle; 11 | @property (nonatomic, readonly) NSFileHandle* errorHandle; 12 | @property (nonatomic, readonly) NSDictionary* parameters; 13 | @property (nonatomic, readonly) NSDictionary* environment; 14 | @property (nonatomic, readonly) NSString* workingDirectory; 15 | 16 | + (instancetype)proxyWithOptions:(NSDictionary*)options; 17 | - (instancetype)initWithOptions:(NSDictionary*)options; 18 | 19 | - (void)writeStringToOutput:(NSString*)aString; 20 | - (void)writeStringToError:(NSString*)aString; 21 | - (id)readPropertyListFromInput; 22 | 23 | - (NSString*)argumentAtIndex:(NSUInteger)index; 24 | - (NSUInteger)numberOfArguments; 25 | @end 26 | -------------------------------------------------------------------------------- /Commands/prototype/TMDChameleon.mm: -------------------------------------------------------------------------------- 1 | // 2 | // TMDChameleon.mm 3 | // 4 | // Created by Allan Odgaard on 2007-06-26. 5 | // Copyright (c) 2007 MacroMates. All rights reserved. 6 | // 7 | 8 | #import "TMDChameleon.h" 9 | 10 | static NSMutableDictionary* DefaultValues = [NSMutableDictionary new]; 11 | 12 | @implementation TMD2Chameleon 13 | - (id)init 14 | { 15 | id res = [DefaultValues objectForKey:NSStringFromClass([self class])]; 16 | return [res mutableCopy]; 17 | } 18 | 19 | + (BOOL)createSubclassNamed:(NSString*)aName withValues:(NSDictionary*)values 20 | { 21 | [DefaultValues setObject:values forKey:aName]; 22 | 23 | const char* name = [aName UTF8String]; 24 | 25 | if(objc_lookUpClass(name)) 26 | return YES; 27 | 28 | Class sub_cl = objc_allocateClassPair([TMD2Chameleon class], name, 0); 29 | 30 | if(sub_cl == Nil) 31 | return NO; 32 | 33 | objc_registerClassPair(sub_cl); 34 | 35 | return YES; 36 | } 37 | @end 38 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${TARGET_NAME} 9 | CFBundleName 10 | ${TARGET_NAME} 11 | TMPlugInAPIVersion 12 | 2 13 | CFBundleIconFile 14 | 15 | CFBundleIdentifier 16 | com.macromates.plugin.${TARGET_NAME} 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundlePackageType 20 | BNDL 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.0 25 | NSPrincipalClass 26 | Dialog2 27 | 28 | 29 | -------------------------------------------------------------------------------- /Commands/defaults.mm: -------------------------------------------------------------------------------- 1 | #import "../TMDCommand.h" 2 | 3 | @interface TMDDefaults : TMDCommand 4 | @end 5 | 6 | @implementation TMDDefaults 7 | + (void)load 8 | { 9 | [TMDDefaults registerObject:[self new] forCommand:@"defaults"]; 10 | } 11 | 12 | - (void)handleCommand:(CLIProxy*)proxy 13 | { 14 | NSDictionary* args = [proxy parameters]; 15 | 16 | if(NSDictionary* defaults = [args objectForKey:@"register"]) 17 | { 18 | // FIXME this is needed only because we presently can’t express argument constraints (CLIProxy would otherwise correctly validate/convert CLI arguments) 19 | if([defaults isKindOfClass:[NSString class]]) 20 | defaults = [NSPropertyListSerialization propertyListWithData:[(NSString*)defaults dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:NULL error:NULL]; 21 | 22 | [NSUserDefaults.standardUserDefaults registerDefaults:defaults]; 23 | } 24 | 25 | if(NSString* key = [args objectForKey:@"read"]) 26 | { 27 | if(id obj = [NSUserDefaults.standardUserDefaults objectForKey:key]) 28 | [TMDCommand writePropertyList:obj toFileHandle:[proxy outputHandle]]; 29 | } 30 | } 31 | 32 | - (NSString*)commandDescription 33 | { 34 | return @"Register default values for user settings."; 35 | } 36 | 37 | - (NSString*)usageForInvocation:(NSString*)invocation; 38 | { 39 | return [NSString stringWithFormat:@"\t%1$@ --register '{ webOutputTheme = night; }'\n", invocation]; 40 | } 41 | @end 42 | -------------------------------------------------------------------------------- /Commands/images.mm: -------------------------------------------------------------------------------- 1 | #import "../TMDCommand.h" 2 | #import "../Dialog2.h" 3 | 4 | @interface TMDImages : TMDCommand 5 | @end 6 | 7 | @implementation TMDImages 8 | + (void)load 9 | { 10 | [TMDImages registerObject:[self new] forCommand:@"images"]; 11 | } 12 | 13 | - (void)handleCommand:(CLIProxy*)proxy 14 | { 15 | NSDictionary* args = [proxy parameters]; 16 | 17 | // Convert image paths to NSImages 18 | NSDictionary* imagePaths = [args objectForKey:@"register"]; 19 | if([imagePaths isKindOfClass:[NSString class]]) 20 | imagePaths = [NSPropertyListSerialization propertyListWithData:[(NSString*)imagePaths dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:NULL error:NULL]; 21 | 22 | for(NSString* imageName in [imagePaths allKeys]) 23 | { 24 | if([NSImage imageNamed:imageName]) // presumably this is not the first time the menu is invoked with this image, so skip loading it to avoid potential leaks 25 | continue; 26 | 27 | NSImage* image = [[NSImage alloc] initByReferencingFile:[imagePaths objectForKey:imageName]]; 28 | if(image && [image isValid]) 29 | [image setName:imageName]; 30 | } 31 | } 32 | 33 | - (NSString*)commandDescription 34 | { 35 | return @"Add image files as named images for use by other commands/nibs."; 36 | } 37 | 38 | - (NSString*)usageForInvocation:(NSString*)invocation; 39 | { 40 | return [NSString stringWithFormat:@"\t%1$@ --register \"{ macro = '$(find_app com.macromates.textmate)/Contents/Resources/Bundle Item Icons/Macros.png'; }\"\n", invocation]; 41 | } 42 | @end 43 | -------------------------------------------------------------------------------- /TMDCommand.mm: -------------------------------------------------------------------------------- 1 | #import "TMDCommand.h" 2 | 3 | static NSMutableDictionary* Commands = nil; 4 | 5 | @implementation TMDCommand 6 | + (void)registerObject:(id)anObject forCommand:(NSString*)aCommand 7 | { 8 | if(!Commands) 9 | Commands = [NSMutableDictionary new]; 10 | [Commands setObject:anObject forKey:aCommand]; 11 | } 12 | 13 | + (NSDictionary*)registeredCommands 14 | { 15 | return [Commands copy]; 16 | } 17 | 18 | + (id)objectForCommand:(NSString*)aCommand 19 | { 20 | return [Commands objectForKey:aCommand]; 21 | } 22 | 23 | + (id)readPropertyList:(NSFileHandle*)aFileHandle error:(NSError**)error; 24 | { 25 | NSData* data = [aFileHandle readDataToEndOfFile]; 26 | if([data length] == 0) 27 | return nil; 28 | 29 | id plist = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListMutableContainersAndLeaves format:nil error:error]; 30 | 31 | return plist; 32 | } 33 | 34 | + (void)writePropertyList:(id)aPlist toFileHandle:(NSFileHandle*)aFileHandle 35 | { 36 | NSError* error = nil; 37 | if(NSData* data = [NSPropertyListSerialization dataWithPropertyList:aPlist format:NSPropertyListXMLFormat_v1_0 options:0 error:&error]) 38 | { 39 | [aFileHandle writeData:data]; 40 | } 41 | else 42 | { 43 | fprintf(stderr, "%s\n", [[error localizedDescription] UTF8String] ?: "unknown error serializing returned property list"); 44 | fprintf(stderr, "%s\n", [[aPlist description] UTF8String]); 45 | } 46 | } 47 | 48 | - (NSString*)commandDescription 49 | { 50 | return @"No information available for this command"; 51 | } 52 | 53 | - (NSString*)usageForInvocation:(NSString*)invocation; 54 | { 55 | return @"No usage information available for this command"; 56 | } 57 | @end 58 | -------------------------------------------------------------------------------- /Commands/prototype/prototype.mm: -------------------------------------------------------------------------------- 1 | #import "TMDChameleon.h" 2 | #import "../../Dialog2.h" 3 | #import "../../TMDCommand.h" 4 | 5 | @interface TMDPrototype : TMDCommand 6 | @end 7 | 8 | @implementation TMDPrototype 9 | + (void)load 10 | { 11 | [TMDPrototype registerObject:[self new] forCommand:@"prototype"]; 12 | } 13 | 14 | - (void)handleCommand:(CLIProxy*)proxy 15 | { 16 | NSDictionary* args = [proxy parameters]; 17 | 18 | if(NSDictionary* values = [args objectForKey:@"register"]) 19 | { 20 | // FIXME this is needed only because we presently can’t express argument constraints (CLIProxy would otherwise correctly validate/convert CLI arguments) 21 | if([values isKindOfClass:[NSString class]]) 22 | values = [NSPropertyListSerialization propertyListWithData:[(NSString*)values dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:NULL error:NULL]; 23 | 24 | for(id key in [values allKeys]) 25 | [TMD2Chameleon createSubclassNamed:key withValues:[values objectForKey:key]]; 26 | } 27 | 28 | if(NSString* show = [args objectForKey:@"show"]) 29 | { 30 | id obj = [NSClassFromString(show) new]; 31 | [proxy writeStringToOutput:[obj description] ?: [NSString stringWithFormat:@"error: no class named ‘%@’", show]]; 32 | [proxy writeStringToOutput:@"\n"]; 33 | } 34 | } 35 | 36 | - (NSString*)commandDescription 37 | { 38 | return @"Register classes for use with NSArrayController."; 39 | } 40 | 41 | - (NSString*)usageForInvocation:(NSString*)invocation; 42 | { 43 | return [NSString stringWithFormat:@"\t%1$@ --register \"{ SQL_New_Connection = { title = untitled; serverType = MySQL; hostName = localhost; userName = '$LOGNAME'; }; }\"\n\t%1$@ --show SQL_New_Connection\n", invocation]; 44 | } 45 | @end 46 | -------------------------------------------------------------------------------- /Commands/tooltip/tooltip.mm: -------------------------------------------------------------------------------- 1 | #import "../../TMDCommand.h" 2 | #import "../../Dialog2.h" 3 | #import "TMDHTMLTips.h" 4 | #import "../Utilities/TextMate.h" // -positionForWindowUnderCaret 5 | 6 | @interface TMDHTMLTipsCommand : TMDCommand 7 | @end 8 | 9 | @implementation TMDHTMLTipsCommand 10 | + (void)load 11 | { 12 | [TMDCommand registerObject:[self new] forCommand:@"tooltip"]; 13 | } 14 | 15 | - (NSString*)commandDescription 16 | { 17 | return @"Shows a tooltip at the caret with the provided content, optionally rendered as HTML."; 18 | } 19 | 20 | - (NSString*)usageForInvocation:(NSString*)invocation; 21 | { 22 | return [NSString stringWithFormat:@"\t%1$@ --text 'regular text'\n\t%1$@ --html 'html'\nUse --transparent to give the tooltip window a transparent background", invocation]; 23 | } 24 | 25 | - (void)handleCommand:(CLIProxy*)proxy 26 | { 27 | NSDictionary* args = [proxy parameters]; 28 | 29 | NSString* html = nil; 30 | if(NSMutableString* text = [[args objectForKey:@"text"] mutableCopy]) 31 | { 32 | [text replaceOccurrencesOfString:@"&" withString:@"&" options:0 range:NSMakeRange(0, [text length])]; 33 | [text replaceOccurrencesOfString:@"<" withString:@"<" options:0 range:NSMakeRange(0, [text length])]; 34 | [text insertString:@"
" atIndex:0];
35 | 		[text appendString:@"
"]; 36 | html = text; 37 | } 38 | else 39 | { 40 | html = [args objectForKey:@"html"]; 41 | } 42 | 43 | NSPoint pos = [NSEvent mouseLocation]; 44 | if(id textView = [NSApp targetForAction:@selector(positionForWindowUnderCaret)]) 45 | pos = [textView positionForWindowUnderCaret]; 46 | 47 | [TMDHTMLTip showWithContent:html atLocation:pos transparent:([args objectForKey:@"transparent"] ? YES : NO)]; 48 | } 49 | @end 50 | -------------------------------------------------------------------------------- /Commands/alert.mm: -------------------------------------------------------------------------------- 1 | #import "../Dialog2.h" 2 | #import "../TMDCommand.h" 3 | 4 | /* 5 | echo '{alertStyle = warning; button1 = 'OK'; title = 'test'; body = 'Testing';}' | "$DIALOG" alert 6 | 7 | "$DIALOG" help alert 8 | "$DIALOG" alert --alertStyle critical --title "FOOL!" --body "test" --button1 foo --button2 bar --button3 baz 9 | */ 10 | 11 | // ========= 12 | // = Alert = 13 | // ========= 14 | 15 | @interface TMDAlertCommand : TMDCommand 16 | @end 17 | 18 | NSAlertStyle alert_style_from_string (NSString* str) 19 | { 20 | if([str isEqualToString:@"warning"]) 21 | return NSAlertStyleWarning; 22 | else if([str isEqualToString:@"critical"]) 23 | return NSAlertStyleCritical; 24 | else 25 | return NSAlertStyleInformational; 26 | } 27 | 28 | @implementation TMDAlertCommand 29 | + (void)load 30 | { 31 | [super registerObject:[self new] forCommand:@"alert"]; 32 | } 33 | 34 | - (void)handleCommand:(CLIProxy*)proxy 35 | { 36 | NSDictionary* args = [proxy parameters]; 37 | 38 | NSAlert* alert = [NSAlert new]; 39 | [alert setAlertStyle:alert_style_from_string([args objectForKey:@"alertStyle"])]; 40 | if(NSString* msg = [args objectForKey:@"title"]) 41 | [alert setMessageText:msg]; 42 | if(NSString* txt = [args objectForKey:@"body"]) 43 | [alert setInformativeText:txt]; 44 | 45 | NSInteger i = 0; 46 | while(NSString* button = [args objectForKey:[NSString stringWithFormat:@"button%ld", ++i]]) 47 | [alert addButtonWithTitle:button]; 48 | 49 | NSInteger alertResult = ([alert runModal] - NSAlertFirstButtonReturn); 50 | NSDictionary* resultDict = @{ @"buttonClicked": @(alertResult) }; 51 | 52 | [TMDCommand writePropertyList:resultDict toFileHandle:[proxy outputHandle]]; 53 | } 54 | 55 | - (NSString*)commandDescription 56 | { 57 | return @"Show an alert box."; 58 | } 59 | 60 | - (NSString*)usageForInvocation:(NSString*)invocation; 61 | { 62 | return [NSString stringWithFormat:@"\t%1$@ --alertStyle warning --title 'Delete File?' --body 'You cannot undo this action.' --button1 Delete --button2 Cancel\n", invocation]; 63 | } 64 | @end 65 | -------------------------------------------------------------------------------- /Dialog2.mm: -------------------------------------------------------------------------------- 1 | // 2 | // Dialog2.mm 3 | // Dialog2 4 | // 5 | // Created by Ciaran Walsh on 19/11/2007. 6 | // 7 | 8 | #import "Dialog2.h" 9 | #import "TMDCommand.h" 10 | #import "CLIProxy.h" 11 | 12 | @protocol TMPlugInController 13 | - (CGFloat)version; 14 | @end 15 | 16 | @interface Dialog2 : NSObject 17 | @property (nonatomic) NSConnection* connection; 18 | - (id)initWithPlugInController:(id )aController; 19 | @end 20 | 21 | 22 | @implementation Dialog2 23 | 24 | - (id)initWithPlugInController:(id )aController 25 | { 26 | NSApp = NSApplication.sharedApplication; 27 | if(self = [self init]) 28 | { 29 | _connection = [NSConnection new]; 30 | [_connection setRootObject:self]; 31 | 32 | NSString* portName = [NSString stringWithFormat:@"%@.%d", kDialogServerConnectionName, getpid()]; 33 | if([_connection registerName:portName] == NO) 34 | NSLog(@"couldn't setup dialog server."), NSBeep(); 35 | else if(NSString* path = [[NSBundle bundleForClass:[self class]] pathForResource:@"tm_dialog2" ofType:nil]) 36 | { 37 | char* oldDialog = getenv("DIALOG"); 38 | if(oldDialog == NULL || ![@(oldDialog) isEqualToString:path]) 39 | { 40 | if(oldDialog) 41 | setenv("DIALOG_1", oldDialog, 1); 42 | setenv("DIALOG", [path UTF8String], 1); 43 | } 44 | 45 | setenv("DIALOG_PORT_NAME", [portName UTF8String], 1); 46 | } 47 | } 48 | 49 | return self; 50 | } 51 | 52 | - (void)dispatch:(id)options 53 | { 54 | CLIProxy* interface = [CLIProxy proxyWithOptions:options]; 55 | 56 | NSString* command = [interface numberOfArguments] <= 1 ? @"help" : [interface argumentAtIndex:1]; 57 | 58 | if(id target = [TMDCommand objectForCommand:command]) 59 | [target performSelector:@selector(handleCommand:) withObject:interface]; 60 | else [interface writeStringToError:@"unknown command, try help.\n"]; 61 | } 62 | 63 | - (void)connectFromClientWithOptions:(id)options 64 | { 65 | [self performSelector:@selector(dispatch:) withObject:options afterDelay:0.0]; 66 | } 67 | 68 | @end 69 | /* 70 | echo '{ menuItems = ({title = 'foo';});}' | "$DIALOG" menu 71 | */ 72 | -------------------------------------------------------------------------------- /Commands/help.mm: -------------------------------------------------------------------------------- 1 | #import "../TMDCommand.h" 2 | 3 | // ======== 4 | // = Help = 5 | // ======== 6 | 7 | @interface TMDHelpCommand : TMDCommand 8 | @end 9 | 10 | @implementation TMDHelpCommand 11 | + (void)load 12 | { 13 | [TMDCommand registerObject:[self new] forCommand:@"help"]; 14 | } 15 | 16 | - (NSString*)commandDescription 17 | { 18 | return @"Gives a brief list of available commands, or usage details for a specific command."; 19 | } 20 | 21 | - (NSString*)usageForInvocation:(NSString*)invocation; 22 | { 23 | return [NSString stringWithFormat:@"%@ ", invocation]; 24 | } 25 | 26 | - (NSString*)commandSummaryText 27 | { 28 | NSDictionary* commands = [TMDCommand registeredCommands]; 29 | 30 | NSMutableArray* help = [NSMutableArray arrayWithCapacity:100]; 31 | 32 | NSMutableArray* registeredCommands = [NSMutableArray arrayWithCapacity:100]; 33 | for(NSString* commandName in commands) 34 | { 35 | if(![commandName hasPrefix:@"x-"]) 36 | { 37 | TMDCommand* command = [commands objectForKey:commandName]; 38 | NSString* description = [command commandDescription]; 39 | [registeredCommands addObject:[NSString stringWithFormat:@"\t%@: %@", commandName, description]]; 40 | } 41 | } 42 | 43 | [help addObject:@"usage: \"$DIALOG\" [--version] []"]; 44 | [help addObject:[NSString stringWithFormat:@"%ld commands registered:", [registeredCommands count]]]; 45 | [help addObjectsFromArray:[registeredCommands sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]]; 46 | [help addObject:@"Use `\"$DIALOG\" help ` for detailed help.\n"]; 47 | 48 | return [help componentsJoinedByString:@"\n"]; 49 | } 50 | 51 | - (NSString*)helpForCommand:(NSString*)commandName 52 | { 53 | NSMutableString* help = [NSMutableString stringWithCapacity:100]; 54 | 55 | TMDCommand* command = nil; 56 | if(![commandName hasPrefix:@"x-"] && (command = [TMDCommand objectForCommand:commandName])) 57 | { 58 | [help appendFormat:@"%@\n\n",[command commandDescription]]; 59 | [help appendFormat:@"%@ usage:\n",commandName]; 60 | [help appendFormat:@"%@\n",[command usageForInvocation:[NSString stringWithFormat:@"\"$DIALOG\" %@", commandName]]]; 61 | } 62 | else 63 | { 64 | [help appendFormat:@"Unknown command '%@'\n", commandName]; 65 | } 66 | 67 | return help; 68 | } 69 | 70 | - (void)handleCommand:(CLIProxy*)proxy 71 | { 72 | if([proxy numberOfArguments] < 3) 73 | [proxy writeStringToError:[self commandSummaryText]]; 74 | else [proxy writeStringToOutput:[self helpForCommand:[proxy argumentAtIndex:2]]]; 75 | } 76 | @end 77 | /* 78 | "$DIALOG" help 79 | "$DIALOG" help help 80 | */ 81 | -------------------------------------------------------------------------------- /Commands/popup/popup.mm: -------------------------------------------------------------------------------- 1 | #import "../../TMDCommand.h" 2 | #import "../../Dialog2.h" 3 | #import "TMDIncrementalPopUpMenu.h" 4 | #import "../Utilities/TextMate.h" // -positionForWindowUnderCaret 5 | 6 | /* 7 | "$DIALOG" popup --suggestions '( { display = "**law**"; image = NSRefreshTemplate; match = "law"; }, { display = "**laws**"; match = "laws"; insert = "(${1:hello}, ${2:again})"; children=({display="lawsless";}); } )' 8 | */ 9 | 10 | // ================== 11 | // = Extended Popup = 12 | // ================== 13 | @interface TMDXPopUp : TMDCommand 14 | @end 15 | 16 | @implementation TMDXPopUp 17 | + (void)load 18 | { 19 | [TMDCommand registerObject:[self new] forCommand:@"popup"]; 20 | } 21 | 22 | - (void)handleCommand:(CLIProxy*)proxy 23 | { 24 | NSDictionary* args = [proxy parameters]; 25 | 26 | NSString* filter = [args objectForKey:@"alreadyTyped"]; 27 | NSString* prefix = [args objectForKey:@"staticPrefix"]; 28 | NSString* allow = [args objectForKey:@"additionalWordCharacters"]; 29 | BOOL wait = [args objectForKey:@"returnChoice"] ? YES : NO; 30 | BOOL caseInsensitive = [args objectForKey:@"caseInsensitive"] ? YES : NO; 31 | 32 | NSArray* suggestions = [args objectForKey:@"suggestions"]; 33 | if([suggestions isKindOfClass:[NSString class]]) 34 | suggestions = [NSPropertyListSerialization propertyListWithData:[(NSString*)suggestions dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:NULL error:NULL]; 35 | 36 | TMDIncrementalPopUpMenu* xPopUp = [[TMDIncrementalPopUpMenu alloc] initWithItems:suggestions alreadyTyped:filter staticPrefix:prefix additionalWordCharacters:allow caseSensitive:!caseInsensitive writeChoiceToFileDescriptor:(wait ? [proxy outputHandle] : nil)]; 37 | 38 | NSPoint pos = [NSEvent mouseLocation]; 39 | if(id textView = [NSApp targetForAction:@selector(positionForWindowUnderCaret)]) 40 | pos = [textView positionForWindowUnderCaret]; 41 | 42 | if(filter) 43 | { 44 | NSFont* font = [NSFont fontWithName:[NSUserDefaults.standardUserDefaults stringForKey:@"OakTextViewNormalFontName"] ?: [[NSFont userFixedPitchFontOfSize:12.0] fontName] size:[NSUserDefaults.standardUserDefaults integerForKey:@"OakTextViewNormalFontSize"] ?: 12]; 45 | pos.x -= [filter sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]].width; 46 | } 47 | 48 | [xPopUp setCaretPos:pos]; 49 | [xPopUp orderFront:self]; 50 | } 51 | 52 | - (NSString*)commandDescription 53 | { 54 | return @"Presents the user with a list of items which can be filtered down by typing to select the item they want."; 55 | } 56 | 57 | - (NSString*)usageForInvocation:(NSString*)invocation; 58 | { 59 | return [NSString stringWithFormat:@"\t%1$@ --suggestions '( { display = law; }, { display = laws; insert = \"(${1:hello}, ${2:again})\"; } )'\n", invocation]; 60 | } 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /CLIProxy.mm: -------------------------------------------------------------------------------- 1 | // 2 | // CLIProxy.mm 3 | // Dialog2 4 | // 5 | // Created by Ciaran Walsh on 16/02/2008. 6 | // 7 | 8 | #import "CLIProxy.h" 9 | #import "TMDCommand.h" 10 | 11 | @interface CLIProxy () 12 | { 13 | NSArray* _arguments; 14 | NSDictionary* _parameters; 15 | } 16 | @end 17 | 18 | @implementation CLIProxy 19 | + (instancetype)proxyWithOptions:(NSDictionary*)options; 20 | { 21 | return [[CLIProxy alloc] initWithOptions:options]; 22 | } 23 | 24 | - (instancetype)initWithOptions:(NSDictionary*)options 25 | { 26 | if(self = [super init]) 27 | { 28 | _inputHandle = [NSFileHandle fileHandleForReadingAtPath:[options objectForKey:@"stdin"]]; 29 | _outputHandle = [NSFileHandle fileHandleForWritingAtPath:[options objectForKey:@"stdout"]]; 30 | _errorHandle = [NSFileHandle fileHandleForWritingAtPath:[options objectForKey:@"stderr"]]; 31 | _arguments = [options objectForKey:@"arguments"]; 32 | _environment = [options objectForKey:@"environment"]; 33 | _workingDirectory = [options objectForKey:@"cwd"]; 34 | } 35 | return self; 36 | } 37 | 38 | - (NSDictionary*)parameters 39 | { 40 | if(!_parameters) 41 | { 42 | NSMutableDictionary* res = [NSMutableDictionary dictionary]; 43 | if(id plist = [TMDCommand readPropertyList:self.inputHandle error:NULL]) 44 | { 45 | if([plist isKindOfClass:[NSDictionary class]]) 46 | res = plist; 47 | } 48 | 49 | for(NSUInteger i = 2; i < [_arguments count]; ) 50 | { 51 | NSString* key = _arguments[i++]; 52 | if(![key hasPrefix:@"--"]) 53 | continue; // TODO Report unexpected argument to user 54 | 55 | key = [key substringFromIndex:2]; 56 | NSString* value = i < _arguments.count ? _arguments[i] : @""; 57 | 58 | if([value hasPrefix:@"--"] && ![value isEqualToString:@"--"]) 59 | { 60 | value = @""; // We use NSString instead of NSNull because we may send mutableCopy to parameters 61 | } 62 | else 63 | { 64 | ++i; 65 | if([value isEqualToString:@"--"]) 66 | value = i < _arguments.count ? _arguments[i++] : @""; 67 | } 68 | res[key] = value; 69 | } 70 | 71 | _parameters = res; 72 | } 73 | return _parameters; 74 | } 75 | 76 | - (NSUInteger)numberOfArguments; 77 | { 78 | return [_arguments count]; 79 | } 80 | 81 | - (NSString*)argumentAtIndex:(NSUInteger)index; 82 | { 83 | return index < [_arguments count] ? _arguments[index] : nil; 84 | } 85 | 86 | // =================== 87 | // = Reading/Writing = 88 | // =================== 89 | - (void)writeStringToOutput:(NSString*)aString; 90 | { 91 | [self.outputHandle writeData:[aString dataUsingEncoding:NSUTF8StringEncoding]]; 92 | } 93 | 94 | - (void)writeStringToError:(NSString*)aString; 95 | { 96 | [self.errorHandle writeData:[aString dataUsingEncoding:NSUTF8StringEncoding]]; 97 | } 98 | 99 | - (id)readPropertyListFromInput; 100 | { 101 | NSError* error = nil; 102 | id plist = [TMDCommand readPropertyList:self.inputHandle error:&error]; 103 | 104 | if(!plist) 105 | [self writeStringToError:[error localizedDescription] ?: @"unknown error parsing property list\n"]; 106 | 107 | return plist; 108 | } 109 | @end 110 | -------------------------------------------------------------------------------- /Commands/menu.mm: -------------------------------------------------------------------------------- 1 | #import "../Dialog2.h" 2 | #import "../TMDCommand.h" 3 | #import "Utilities/TextMate.h" // -positionForWindowUnderCaret 4 | 5 | // ======== 6 | // = Menu = 7 | // ======== 8 | 9 | /* 10 | echo '{ items = ({title = "foo"; header = 1;},{title = "bar";}); }' | "$DIALOG" menu 11 | "$DIALOG" menu --items '({title = "foo"; header = 1;},{title = "bar";})' 12 | */ 13 | 14 | @interface DialogPopupMenuTarget : NSObject 15 | @property (nonatomic, retain) NSDictionary* selectedItem; 16 | @end 17 | 18 | @implementation DialogPopupMenuTarget 19 | - (BOOL)validateMenuItem:(NSMenuItem*)menuItem 20 | { 21 | return [menuItem action] == @selector(takeRepresentedObjectFrom:); 22 | } 23 | 24 | - (void)takeRepresentedObjectFrom:(NSMenuItem*)sender 25 | { 26 | NSAssert([sender isKindOfClass:[NSMenuItem class]], @"Unexpected sender for menu target"); 27 | self.selectedItem = [sender representedObject]; 28 | } 29 | @end 30 | 31 | @interface TMDMenuCommand : TMDCommand 32 | @end 33 | 34 | @implementation TMDMenuCommand 35 | + (void)load 36 | { 37 | [TMDCommand registerObject:[self new] forCommand:@"menu"]; 38 | } 39 | 40 | - (NSString*)commandDescription 41 | { 42 | return @"Presents a menu using the given structure and returns the option chosen by the user"; 43 | } 44 | 45 | - (NSString*)usageForInvocation:(NSString*)invocation; 46 | { 47 | return [NSString stringWithFormat:@"\t%1$@ --items '({title = foo;}, {separator = 1;}, {header=1; title = bar;}, {title = baz;})'\n", invocation]; 48 | } 49 | 50 | - (void)handleCommand:(CLIProxy*)proxy 51 | { 52 | NSDictionary* args = [proxy parameters]; 53 | NSArray* menuItems = [args objectForKey:@"items"]; 54 | 55 | // FIXME this is needed only because we presently can’t express argument constraints (CLIProxy would otherwise correctly validate/convert CLI arguments) 56 | if([menuItems isKindOfClass:[NSString class]]) 57 | menuItems = [NSPropertyListSerialization propertyListWithData:[(NSString*)menuItems dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:NULL error:NULL]; 58 | 59 | NSMenu* menu = [NSMenu new]; 60 | [menu setFont:[NSFont menuFontOfSize:([NSUserDefaults.standardUserDefaults integerForKey:@"OakBundleManagerDisambiguateMenuFontSize"] ?: 11)]]; 61 | DialogPopupMenuTarget* menuTarget = [[DialogPopupMenuTarget alloc] init]; 62 | 63 | NSInteger itemId = 0; 64 | BOOL inSection = NO; 65 | for(NSDictionary* menuItem : menuItems) 66 | { 67 | if([[menuItem objectForKey:@"separator"] intValue]) 68 | { 69 | [menu addItem:[NSMenuItem separatorItem]]; 70 | } 71 | else if([[menuItem objectForKey:@"header"] intValue]) 72 | { 73 | [menu addItemWithTitle:[menuItem objectForKey:@"title"] action:NULL keyEquivalent:@""]; 74 | inSection = YES; 75 | } 76 | else 77 | { 78 | NSMenuItem* theItem = [menu addItemWithTitle:[menuItem objectForKey:@"title"] action:@selector(takeRepresentedObjectFrom:) keyEquivalent:@""]; 79 | [theItem setTarget:menuTarget]; 80 | [theItem setRepresentedObject:menuItem]; 81 | if(++itemId <= 10) 82 | { 83 | [theItem setKeyEquivalent:[NSString stringWithFormat:@"%ld", itemId % 10]]; 84 | [theItem setKeyEquivalentModifierMask:0]; 85 | } 86 | if(inSection) 87 | [theItem setIndentationLevel:1]; 88 | } 89 | } 90 | 91 | NSPoint pos = [NSEvent mouseLocation]; 92 | if(id textView = [NSApp targetForAction:@selector(positionForWindowUnderCaret)]) 93 | pos = [textView positionForWindowUnderCaret]; 94 | 95 | 96 | if([menu popUpMenuPositioningItem:nil atLocation:pos inView:nil] && menuTarget.selectedItem) 97 | [TMDCommand writePropertyList:menuTarget.selectedItem toFileHandle:[proxy outputHandle]]; 98 | } 99 | @end 100 | -------------------------------------------------------------------------------- /Commands/Utilities/TextMate.mm: -------------------------------------------------------------------------------- 1 | #import "TextMate.h" 2 | 3 | static CGFloat insertionDelayForNewDoc = 0.1; 4 | 5 | // Declarations to avoid compiler warnings 6 | @interface NSObject (OakTextViewPrivate) 7 | - (void)insertSnippetWithOptions:(NSDictionary*)options; 8 | - (void)makeTextViewFirstResponder:(id)sender; 9 | - (void)newDocument:(id)sender; 10 | @end 11 | 12 | /** 13 | Returns the front most text view and by reference: 14 | - “isNew” – “YES” if no text view was found it was created a new document 15 | - “winForTextView” – the NSWindow which contains the the front most text view 16 | It returns “nil” if no text view could be found or created. 17 | */ 18 | id frontMostTextViewForSelector (SEL selector, BOOL* isNew, NSWindow** winForTextView) 19 | { 20 | 21 | // Return value if a new doc was created 22 | if(isNew) 23 | *isNew = NO; 24 | 25 | // unique method for identifying a OakTextView 26 | SEL checkSelector = @selector(insertSnippetWithOptions:); 27 | 28 | // Find the front most OakTextView 29 | for(NSWindow* win in [NSApp orderedWindows]) 30 | { 31 | NSMutableArray* views = [NSMutableArray array]; 32 | if(id firstResponder = [win firstResponder]) 33 | [views addObject:firstResponder]; 34 | [views addObject:[win contentView]]; 35 | 36 | for(NSUInteger i = 0; i < [views count]; ++i) 37 | { 38 | id view = [views objectAtIndex:i]; 39 | if([view respondsToSelector:checkSelector] && [view respondsToSelector:selector]) 40 | { 41 | if(winForTextView) 42 | *winForTextView = win; 43 | return view; 44 | } 45 | 46 | if([view respondsToSelector:@selector(subviews)]) 47 | [views addObjectsFromArray:[view performSelector:@selector(subviews)]]; 48 | } 49 | } 50 | 51 | // If no textView was found create a new document 52 | if(id tmApp = [NSApp targetForAction:@selector(newDocument:)]) 53 | { 54 | 55 | [tmApp newDocument:nil]; 56 | 57 | if([[NSApp orderedWindows] count] 58 | && [[[[NSApp orderedWindows] objectAtIndex:0] windowController] tryToPerform: 59 | @selector(makeTextViewFirstResponder:) with:nil]) 60 | { 61 | id textView = [NSApp targetForAction:checkSelector]; 62 | if(textView && [textView respondsToSelector:selector]) 63 | { 64 | if(isNew) 65 | *isNew = YES; 66 | if(winForTextView) 67 | *winForTextView = [[NSApp orderedWindows] objectAtIndex:0]; 68 | return textView; 69 | } 70 | } 71 | } 72 | 73 | return nil; 74 | 75 | } 76 | 77 | /** 78 | Tries to insert “someText” as text into the front most text view. 79 | */ 80 | void insert_text (NSString* someText) 81 | { 82 | BOOL isNewDocument = NO; 83 | if(id textView = frontMostTextViewForSelector(@selector(insertText:), &isNewDocument, NULL)) 84 | { 85 | if(isNewDocument) // delay the insertion to let TM finish the initialization of the new doc 86 | [textView performSelector:@selector(insertText:) withObject:someText afterDelay:insertionDelayForNewDoc]; 87 | else [textView insertText:someText]; 88 | } 89 | } 90 | 91 | /** 92 | Tries to insert “aSnippet” as snippet into the front most text view 93 | and set the key focus to the current document. 94 | */ 95 | void insert_snippet (NSString* aSnippet) 96 | { 97 | BOOL isNewDocument = NO; 98 | NSWindow* win = nil; 99 | if(id textView = frontMostTextViewForSelector(@selector(insertSnippetWithOptions:), &isNewDocument, &win)) 100 | { 101 | if(isNewDocument) // delay the insertion to let TM finish the initialization of the new doc 102 | [textView performSelector:@selector(insertSnippetWithOptions:) withObject:[NSDictionary dictionaryWithObject:aSnippet forKey:@"content"] afterDelay:insertionDelayForNewDoc]; 103 | else [textView insertSnippetWithOptions: [NSDictionary dictionaryWithObject:aSnippet forKey:@"content"]]; 104 | 105 | // Since after inserting a snippet the user should interact with the snippet 106 | // set key focus to current textView 107 | [win makeKeyWindow]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tm_dialog2.mm: -------------------------------------------------------------------------------- 1 | // 2 | // client.mm 3 | // Created by Allan Odgaard on 2007-09-22. 4 | // 5 | 6 | #import "Dialog2.h" 7 | 8 | static double const AppVersion = 2.0; 9 | 10 | id connect () 11 | { 12 | NSString* portName = kDialogServerConnectionName; 13 | if(char const* var = getenv("DIALOG_PORT_NAME")) 14 | portName = @(var); 15 | 16 | id proxy = [NSConnection rootProxyForConnectionWithRegisteredName:portName host:nil]; 17 | [proxy setProtocolForProxy:@protocol(DialogServerProtocol)]; 18 | return proxy; 19 | } 20 | 21 | char const* create_pipe (char const* name) 22 | { 23 | char* filename; 24 | asprintf(&filename, "%s/dialog_fifo_%d_%s", getenv("TMPDIR") ?: "/tmp", getpid(), name); 25 | int res = mkfifo(filename, 0666); 26 | if((res == -1) && (errno != EEXIST)) 27 | { 28 | perror("Error creating the named pipe"); 29 | exit(EX_OSERR); 30 | } 31 | return filename; 32 | } 33 | 34 | int open_pipe (char const* name, int oflag) 35 | { 36 | int fd = open(name, oflag); 37 | if(fd == -1) 38 | { 39 | perror("Error opening the named pipe"); 40 | exit(EX_IOERR); 41 | } 42 | return fd; 43 | } 44 | 45 | int main (int argc, char const* argv[]) 46 | { 47 | if(argc == 2 && strcmp(argv[1], "--version") == 0) 48 | { 49 | fprintf(stderr, "%1$s %2$.1f (" __DATE__ ")\n", getprogname(), AppVersion); 50 | return EX_OK; 51 | } 52 | 53 | // If the argument list starts with a switch then assume it’s meant for trunk dialog 54 | // and pass it off 55 | if(argc > 1 && *argv[1] == '-') 56 | execv(getenv("DIALOG_1"), (char* const*)argv); 57 | 58 | @autoreleasepool{ 59 | id proxy = connect(); 60 | if(!proxy) 61 | { 62 | fprintf(stderr, "error reaching server\n"); 63 | exit(EX_UNAVAILABLE); 64 | } 65 | 66 | char const* stdinName = create_pipe("stdin"); 67 | char const* stdoutName = create_pipe("stdout"); 68 | char const* stderrName = create_pipe("stderr"); 69 | 70 | NSMutableArray* args = [NSMutableArray array]; 71 | for(size_t i = 0; i < argc; ++i) 72 | [args addObject:@(argv[i])]; 73 | 74 | NSDictionary* dict = @{ 75 | @"stdin": @(stdinName), 76 | @"stdout": @(stdoutName), 77 | @"stderr": @(stderrName), 78 | @"cwd": @(getcwd(NULL, 0)), 79 | @"environment": [[NSProcessInfo processInfo] environment], 80 | @"arguments": args, 81 | }; 82 | 83 | [proxy connectFromClientWithOptions:dict]; 84 | 85 | int inputFd = open_pipe(stdinName, O_WRONLY); 86 | int outputFd = open_pipe(stdoutName, O_RDONLY); 87 | int errorFd = open_pipe(stderrName, O_RDONLY); 88 | 89 | std::map fdMap; 90 | fdMap[STDIN_FILENO] = inputFd; 91 | fdMap[outputFd] = STDOUT_FILENO; 92 | fdMap[errorFd] = STDERR_FILENO; 93 | 94 | if(isatty(STDIN_FILENO) != 0) 95 | { 96 | fdMap.erase(fdMap.find(STDIN_FILENO)); 97 | close(inputFd); 98 | } 99 | 100 | while(fdMap.size() > 1 || (fdMap.size() == 1 && fdMap.find(STDIN_FILENO) == fdMap.end())) 101 | { 102 | fd_set readfds, writefds; 103 | FD_ZERO(&readfds); FD_ZERO(&writefds); 104 | 105 | int fdCount = 0; 106 | for(auto const& pair : fdMap) 107 | { 108 | FD_SET(pair.first, &readfds); 109 | fdCount = std::max(fdCount, pair.first + 1); 110 | } 111 | 112 | int i = select(fdCount, &readfds, &writefds, NULL, NULL); 113 | if(i == -1) 114 | { 115 | perror("Error from select"); 116 | continue; 117 | } 118 | 119 | std::vector toRemove; 120 | for(auto const& pair : fdMap) 121 | { 122 | if(FD_ISSET(pair.first, &readfds)) 123 | { 124 | char buf[1024]; 125 | ssize_t len = read(pair.first, buf, sizeof(buf)); 126 | 127 | if(len == 0) 128 | toRemove.push_back(pair.first); // we can’t remove as long as we need the iterator for the ++ 129 | else write(pair.second, buf, len); 130 | } 131 | } 132 | 133 | for(int key : toRemove) 134 | { 135 | if(fdMap[key] == inputFd) 136 | close(inputFd); 137 | fdMap.erase(key); 138 | } 139 | } 140 | 141 | close(outputFd); 142 | close(errorFd); 143 | unlink(stdinName); 144 | unlink(stdoutName); 145 | unlink(stderrName); 146 | } 147 | 148 | return EX_OK; 149 | } 150 | -------------------------------------------------------------------------------- /Commands/Utilities/ValueTransformers.mm: -------------------------------------------------------------------------------- 1 | #import "ValueTransformers.h" 2 | #import "../../Dialog2.h" 3 | 4 | // =================================================== 5 | // = Int Array To Index Path Array Value Transformer = 6 | // =================================================== 7 | 8 | @implementation OakIntArrayToIndexPathTransformer 9 | + (Class)transformedValueClass { return [NSArray class]; } 10 | + (BOOL)allowsReverseTransformation { return YES; } 11 | 12 | + (void)load 13 | { 14 | id transformer = [self new]; 15 | [NSValueTransformer setValueTransformer:transformer forName:@"OakIntArrayToIndexPathTransformer"]; 16 | 17 | } 18 | 19 | - (NSIndexPath*)arrayToIndexPath:(NSArray*)anArray 20 | { 21 | NSIndexPath* indexPath = [NSIndexPath new]; 22 | for(id index in anArray) 23 | indexPath = [indexPath indexPathByAddingIndex:[index intValue]]; 24 | return indexPath; 25 | } 26 | 27 | - (id)transformedValue:(id)value 28 | { 29 | NSMutableArray* res = [NSMutableArray array]; 30 | for(NSArray* intArray in value) 31 | [res addObject:[self arrayToIndexPath:intArray]]; 32 | return res; 33 | } 34 | 35 | - (NSArray*)indexPathToArray:(NSIndexPath*)anIndexPath 36 | { 37 | NSMutableArray* array = [NSMutableArray array]; 38 | for(NSUInteger i = 0; i < [anIndexPath length]; ++i) 39 | [array addObject:[NSNumber numberWithUnsignedInteger:[anIndexPath indexAtPosition:i]]]; 40 | return array; 41 | } 42 | 43 | - (id)reverseTransformedValue:(id)value 44 | { 45 | NSMutableArray* array = [NSMutableArray array]; 46 | for(NSIndexPath* indexPath in value) 47 | [array addObject:[self indexPathToArray:indexPath]]; 48 | return array; 49 | } 50 | @end 51 | 52 | // ============================================ 53 | // = Int Array To Index Set Value Transformer = 54 | // ============================================ 55 | 56 | @implementation OakIntArrayToIndexSetTransformer 57 | + (Class)transformedValueClass { return [NSIndexSet class]; } 58 | + (BOOL)allowsReverseTransformation { return YES; } 59 | 60 | + (void)load 61 | { 62 | id transformer = [self new]; 63 | [NSValueTransformer setValueTransformer:transformer forName:@"OakIntArrayToIndexSetTransformer"]; 64 | 65 | } 66 | 67 | - (id)transformedValue:(id)value 68 | { 69 | NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet]; 70 | for(NSNumber* integer in value) 71 | [indexSet addIndex:[integer intValue]]; 72 | return indexSet; 73 | } 74 | 75 | - (id)reverseTransformedValue:(id)value 76 | { 77 | NSMutableArray* array = [NSMutableArray array]; 78 | NSUInteger buf[([value count])]; 79 | [(NSIndexSet*)value getIndexes:buf maxCount:[value count] inIndexRange:nil]; 80 | for(NSUInteger i = 0; i != [value count]; i++) 81 | [array addObject:[NSNumber numberWithUnsignedInteger:buf[i]]]; 82 | return array; 83 | } 84 | @end 85 | 86 | // ============================================= 87 | // = #RRGGBB String To Color Value Transformer = 88 | // ============================================= 89 | 90 | static NSColor* NSColorFromString (NSString* aColor) 91 | { 92 | if(!aColor || [aColor isEqualToString:@""]) 93 | return nil; 94 | 95 | unsigned int red = 0, green = 0, blue = 0, alpha = 0xFF; 96 | if(sscanf([aColor UTF8String], "#%02x%02x%02x%02x", &red, &green, &blue, &alpha) < 3) 97 | return nil; 98 | 99 | return [NSColor colorWithCalibratedRed:red/255.0 green:green/255.0 blue:blue/255.0 alpha:alpha/255.0]; 100 | } 101 | 102 | static NSString* NSStringFromColor (NSColor* aColor) 103 | { 104 | if(aColor == nil) 105 | return nil; 106 | 107 | aColor = [aColor colorUsingColorSpaceName:NSCalibratedRGBColorSpace]; 108 | if([aColor alphaComponent] != 1.0) 109 | return [NSString stringWithFormat:@"#%02lX%02lX%02lX%02lX", lroundf(255.0*[aColor redComponent]), lroundf(255.0*[aColor greenComponent]), lroundf(255.0*[aColor blueComponent]), lroundf(255.0*[aColor alphaComponent])]; 110 | else return [NSString stringWithFormat:@"#%02lX%02lX%02lX", lroundf(255.0*[aColor redComponent]), lroundf(255.0*[aColor greenComponent]), lroundf(255.0*[aColor blueComponent])]; 111 | } 112 | 113 | @implementation OakStringToColorTransformer 114 | + (Class)transformedValueClass { return [NSColor class]; } 115 | + (BOOL)allowsReverseTransformation { return YES; } 116 | 117 | - (id)transformedValue:(id)value { return NSColorFromString(value); } 118 | - (id)reverseTransformedValue:(id)value { return NSStringFromColor(value); } 119 | 120 | + (void)load 121 | { 122 | id transformer = [self new]; 123 | [NSValueTransformer setValueTransformer:transformer forName:@"OakStringToColorTransformer"]; 124 | 125 | } 126 | @end 127 | -------------------------------------------------------------------------------- /Commands/filepanel.mm: -------------------------------------------------------------------------------- 1 | #import "../Dialog2.h" 2 | #import "../TMDCommand.h" 3 | 4 | /* 5 | "$DIALOG" help filepanel 6 | */ 7 | 8 | // ======================= 9 | // = TMDFilePanelCommand = 10 | // ======================= 11 | 12 | @interface TMDFilePanelCommand : TMDCommand 13 | @end 14 | 15 | @implementation TMDFilePanelCommand 16 | + (void)load 17 | { 18 | [super registerObject:[self new] forCommand:@"filepanel"]; 19 | } 20 | 21 | - (NSSavePanel*)setupSavePanel:(NSSavePanel*)savePanel usingParameters:(NSDictionary*)args 22 | { 23 | if(NSString* title = args[@"title"]) 24 | [savePanel setTitle:title]; 25 | if(NSString* prompt = args[@"prompt"]) 26 | [savePanel setPrompt:prompt]; 27 | if(NSString* message = args[@"message"]) 28 | [savePanel setMessage:message]; 29 | if(NSString* label = args[@"label"]) 30 | [savePanel setNameFieldLabel:label]; 31 | if(NSString* filename = args[@"filename"]) 32 | [savePanel setNameFieldStringValue:filename]; 33 | 34 | if(args[@"canCreateDirectories"]) 35 | [savePanel setCanCreateDirectories:[args[@"canCreateDirectories"] boolValue]]; 36 | if(args[@"treatsFilePackagesAsDirectories"]) 37 | [savePanel setTreatsFilePackagesAsDirectories:[args[@"treatsFilePackagesAsDirectories"] boolValue]]; 38 | if(args[@"showsHiddenFiles"]) 39 | [savePanel setShowsHiddenFiles:[args[@"showsHiddenFiles"] boolValue]]; 40 | 41 | if(NSString* path = args[@"defaultDirectory"]) 42 | { 43 | if(NSURL* url = [NSURL fileURLWithPath:[path stringByResolvingSymlinksInPath] isDirectory:YES]) 44 | [savePanel setDirectoryURL:url]; 45 | } 46 | 47 | if(NSString* typesStr = args[@"allowedFileTypes"]) 48 | { 49 | id rawTypes = [NSPropertyListSerialization propertyListWithData:[typesStr dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:NULL error:NULL]; 50 | NSArray* types = nil; 51 | if([rawTypes isKindOfClass:[NSString class]]) 52 | types = [NSArray arrayWithObject:rawTypes]; 53 | else if([rawTypes isKindOfClass:[NSArray class]]) 54 | types = rawTypes; 55 | else 56 | fprintf(stderr, "no single string or plist array passed as value for option '--allowedFileTypes'\n"); 57 | 58 | if(types) 59 | [savePanel setAllowedFileTypes:types]; 60 | } 61 | 62 | if(args[@"allowsOtherFileTypes"]) 63 | [savePanel setAllowsOtherFileTypes:[args[@"allowsOtherFileTypes"] boolValue]]; 64 | 65 | return savePanel; 66 | } 67 | 68 | - (NSOpenPanel*)setupOpenPanel:(NSOpenPanel*)openPanel usingParameters:(NSDictionary*)args 69 | { 70 | [self setupSavePanel:openPanel usingParameters:args]; 71 | 72 | if(args[@"canChooseFiles"]) 73 | [openPanel setCanChooseFiles:[args[@"canChooseFiles"] boolValue]]; 74 | if(args[@"canChooseDirectories"]) 75 | [openPanel setCanChooseDirectories:[args[@"canChooseDirectories"] boolValue]]; 76 | if(args[@"allowsMultipleSelection"]) 77 | [openPanel setAllowsMultipleSelection:[args[@"allowsMultipleSelection"] boolValue]]; 78 | 79 | return openPanel; 80 | } 81 | 82 | - (void)handleCommand:(CLIProxy*)proxy 83 | { 84 | NSDictionary* args = [proxy parameters]; 85 | NSMutableDictionary* resultDict = [NSMutableDictionary dictionary]; 86 | 87 | if(args[@"isSavePanel"]) 88 | { 89 | NSSavePanel* panel = [self setupSavePanel:[NSSavePanel savePanel] usingParameters:args]; 90 | if([panel runModal] == NSModalResponseOK) 91 | resultDict[@"path"] = [[panel URL] path]; 92 | } 93 | else 94 | { 95 | NSOpenPanel* panel = [self setupOpenPanel:[NSOpenPanel openPanel] usingParameters:args]; 96 | if([panel runModal] == NSModalResponseOK) 97 | { 98 | NSMutableArray* paths = [NSMutableArray arrayWithCapacity:[[panel URLs] count]]; 99 | for(NSURL* url in [panel URLs]) 100 | [paths addObject:[url path]]; 101 | resultDict[@"paths"] = paths; 102 | 103 | if([[panel URLs] count] == 1) 104 | resultDict[@"path"] = [[panel URL] path]; 105 | } 106 | } 107 | 108 | [TMDCommand writePropertyList:resultDict toFileHandle:[proxy outputHandle]]; 109 | } 110 | 111 | - (NSString*)commandDescription 112 | { 113 | return @"Shows an open file/folder or save file panel."; 114 | } 115 | 116 | - (NSString*)usageForInvocation:(NSString*)invocation; 117 | { 118 | return [NSString stringWithFormat: 119 | @"\t%1$@ --title Title --prompt Prompt --message Message --defaultDirectory '~/Desktop' showsHiddenFiles 1\n" 120 | @"\t%1$@ --isSavePanel --title 'Save Me' --label 'Label:' --filename 'test.txt' --allowedFileTypes '(txt,tab)'\n" 121 | @"\nOptions:\n" 122 | @"\t--allowedFileTypes «plist array of allowed file types or a single string»\n" 123 | @"\t\te.g. --allowedFileTypes pdf\n" 124 | @"\t\t --allowedFileTypes '(txt,tab)'\n" 125 | @"\t--allowsMultipleSelection {1,0} [not in 'isSavePanel' mode]\n" 126 | @"\t--allowsOtherFileTypes {1,0}\n" 127 | @"\t--canChooseDirectories {1,0}\n" 128 | @"\t--canChooseFiles {1,0}\n" 129 | @"\t--canCreateDirectories {1,0}\n" 130 | @"\t--defaultDirectory «valid directory path»\n" 131 | @"\t\tdefault directory for panel, if not passed the last visited one will be used\n" 132 | @"\t--filename «default file name» [only in 'isSavePanel' mode]\n" 133 | @"\t--isSavePanel\n" 134 | @"\t\tif passed shows a save file panel otherwise an open file panel\n" 135 | @"\t--label «a label» [only in 'isSavePanel' mode]\n" 136 | @"\t\tdefault 'Save As:'\n" 137 | @"\t--message «a message»\n" 138 | @"\t--prompt «a prompt»\n" 139 | @"\t\taction button title - default 'Open' or 'Save' for isSavePanel mode\n" 140 | @"\t--showsHiddenFiles {1,0}\n" 141 | @"\t--title «a title»\n" 142 | @"\t\twindow title - default 'Open' or 'Save' for isSavePanel mode\n" 143 | @"\t--treatsFilePackagesAsDirectories {1,0}\n" 144 | , invocation]; 145 | } 146 | @end 147 | -------------------------------------------------------------------------------- /Commands/nib/nib.mm: -------------------------------------------------------------------------------- 1 | #import "../../Dialog2.h" 2 | #import "../../TMDCommand.h" 3 | #import "TMDNibController.h" 4 | 5 | // ========== 6 | // = Window = 7 | // ========== 8 | 9 | @interface TMDWindowCommand : TMDCommand 10 | @end 11 | 12 | // =================== 13 | // = Command handler = 14 | // =================== 15 | 16 | std::string find_nib (std::string nibName, std::string currentDirectory, NSDictionary* env) 17 | { 18 | std::vector candidates; 19 | 20 | if(nibName.find(".nib") == std::string::npos) 21 | nibName += ".nib"; 22 | 23 | if(nibName.size() && nibName[0] != '/') // relative path 24 | { 25 | candidates.push_back(currentDirectory + "/" + nibName); 26 | 27 | if(char const* bundleSupport = [[env objectForKey:@"TM_BUNDLE_SUPPORT"] UTF8String]) 28 | candidates.push_back(bundleSupport + std::string("/nibs/") + nibName); 29 | 30 | if(char const* supportPath = [[env objectForKey:@"TM_SUPPORT_PATH"] UTF8String]) 31 | candidates.push_back(supportPath + std::string("/nibs/") + nibName); 32 | } 33 | else 34 | { 35 | candidates.push_back(nibName); 36 | } 37 | 38 | for(std::string const& path : candidates) 39 | { 40 | struct stat sb; 41 | if(stat(path.c_str(), &sb) == 0) 42 | return path; 43 | } 44 | 45 | fprintf(stderr, "nib could not be loaded: %s (does not exist)\n", nibName.c_str()); 46 | return ""; 47 | } 48 | 49 | @implementation TMDWindowCommand 50 | + (void)load 51 | { 52 | [super registerObject:[self new] forCommand:@"nib"]; 53 | } 54 | 55 | /* 56 | env|egrep 'DIALOG|TM_SUPPORT'|grep -v DIALOG_1|perl -pe 's/(.*?)=(.*)/export $1="$2"/'|pbcopy 57 | 58 | "$DIALOG" nib --load "$TM_SUPPORT_PATH/../Bundles/Latex.tmbundle/Support/nibs/tex_prefs.nib" --defaults '{ latexEngineOptions = "bar"; }' 59 | 60 | "$DIALOG" nib --load RequestString --center --model '{title = "Name?"; prompt = "Please enter your name:"; }' 61 | "$DIALOG" nib --update 1 --model '{title = "updated title"; prompt = "updated prompt"; }' 62 | "$DIALOG" nib --dispose 1 63 | "$DIALOG" nib --list 64 | 65 | "$DIALOG" nib --load "$TM_SUPPORT_PATH/../Bundles/SQL.tmbundle/Support/nibs/connections.nib" --defaults "{'SQL Connections' = ( { title = untitled; serverType = MySQL; hostName = localhost; userName = '$LOGNAME'; } ); }" 66 | 67 | "$DIALOG" help nib 68 | */ 69 | 70 | - (void)handleCommand:(CLIProxy*)proxy 71 | { 72 | NSDictionary* args = [proxy parameters]; 73 | 74 | NSDictionary* model = [args objectForKey:@"model"]; 75 | BOOL shouldCenter = [args objectForKey:@"center"] ? YES : NO; 76 | BOOL shouldRunModal = [args objectForKey:@"modal"] ? YES : NO; 77 | 78 | // FIXME this is needed only because we presently can’t express argument constraints (CLIProxy would otherwise correctly validate/convert CLI arguments) 79 | if([model isKindOfClass:[NSString class]]) 80 | model = [NSPropertyListSerialization propertyListWithData:[(NSString*)model dataUsingEncoding:NSUTF8StringEncoding] options:NSPropertyListImmutable format:NULL error:NULL]; 81 | 82 | if(NSString* updateToken = [args objectForKey:@"update"]) 83 | { 84 | if(TMDNibController* nibController = [TMDNibController controllerForToken:updateToken]) 85 | [nibController updateParametersWith:model]; 86 | else [proxy writeStringToError:[NSString stringWithFormat:@"No nib found for token: %@\n", updateToken]]; 87 | } 88 | 89 | if(NSString* waitToken = [args objectForKey:@"wait"]) 90 | { 91 | if(TMDNibController* nibController = [TMDNibController controllerForToken:waitToken]) 92 | [nibController addClientFileHandle:[proxy outputHandle] modal:shouldRunModal]; 93 | else [proxy writeStringToError:[NSString stringWithFormat:@"No nib found for token: %@\n", waitToken]]; 94 | } 95 | 96 | if(NSString* disposeToken = [args objectForKey:@"dispose"]) 97 | { 98 | if(TMDNibController* nibController = [TMDNibController controllerForToken:disposeToken]) 99 | [nibController tearDown]; 100 | else [proxy writeStringToError:[NSString stringWithFormat:@"No nib found for token: %@\n", disposeToken]]; 101 | } 102 | 103 | if([args objectForKey:@"list"]) 104 | { 105 | [proxy writeStringToOutput:@"Loaded nibs:\n"]; 106 | for(TMDNibController* nibController in [TMDNibController controllers]) 107 | [proxy writeStringToOutput:[NSString stringWithFormat:@"%@ (%@)\n", nibController.token, [[nibController window] title]]]; 108 | } 109 | 110 | if(NSString* nibName = [args objectForKey:@"load"]) 111 | { 112 | // TODO we should let an option type be ‘filename’ and have CLIProxy resolve these (and error when file does not exist) 113 | NSString* nib = @(find_nib([nibName UTF8String], [[proxy workingDirectory] UTF8String] ?: "", [proxy environment]).c_str()); 114 | if(nib == nil || [nib length] == 0) 115 | { 116 | [proxy writeStringToError:[NSString stringWithFormat:@"No nib found for name: ‘%@’\n", nibName]]; 117 | } 118 | else 119 | { 120 | if(TMDNibController* nibController = [[TMDNibController alloc] initWithNibPath:nib]) 121 | { 122 | [nibController updateParametersWith:model]; 123 | [nibController showWindowAndCenter:shouldCenter]; 124 | 125 | [proxy writeStringToOutput:nibController.token]; 126 | } 127 | } 128 | } 129 | } 130 | 131 | - (NSString*)commandDescription 132 | { 133 | return @"Displays custom dialogs from NIBs."; 134 | } 135 | 136 | - (NSString*)usageForInvocation:(NSString*)invocation; 137 | { 138 | return [NSString stringWithFormat: 139 | @"%1$@ --load «nib file» [«options»]\n" 140 | @"%1$@ --update «token» [«options»]\n" 141 | @"%1$@ --wait «token»\n" 142 | @"%1$@ --dispose «token»\n" 143 | @"%1$@ --list\n" 144 | @"\nThe nib will be disposed after user closes its window unless --wait is being used.\n" 145 | @"\nOptions:\n" 146 | @"\t--center\n" 147 | @"\t--modal\n" 148 | @"\t--model «plist»\n", 149 | invocation]; 150 | } 151 | @end 152 | -------------------------------------------------------------------------------- /Commands/nib/TMDNibController.mm: -------------------------------------------------------------------------------- 1 | // 2 | // TMDNibController.mm 3 | // Dialog2 4 | // 5 | 6 | #import 7 | #import 8 | #import "../../Dialog2.h" 9 | #import "../../TMDCommand.h" 10 | #import "TMDNibController.h" 11 | 12 | static NSMutableDictionary* NibControllers = [NSMutableDictionary new]; 13 | static NSInteger NibTokenCount = 0; 14 | 15 | @interface TMDNibController () 16 | { 17 | NSMutableArray* clientFileHandles; 18 | BOOL runningModal; 19 | } 20 | @property (nonatomic, readwrite) NSString* token; 21 | @property (nonatomic) NSArray* topLevelObjects; 22 | @property (nonatomic) NSMutableDictionary* parameters; 23 | @end 24 | 25 | @implementation TMDNibController 26 | + (TMDNibController*)controllerForToken:(NSString*)aToken 27 | { 28 | return [NibControllers objectForKey:aToken]; 29 | } 30 | 31 | + (NSArray*)controllers 32 | { 33 | return [NibControllers allValues]; 34 | } 35 | 36 | - (id)init 37 | { 38 | if(self = [super init]) 39 | { 40 | _parameters = [NSMutableDictionary new]; 41 | [_parameters setObject:self forKey:@"controller"]; 42 | 43 | clientFileHandles = [NSMutableArray new]; 44 | 45 | _token = [NSString stringWithFormat:@"%ld", ++NibTokenCount]; 46 | [NibControllers setObject:self forKey:_token]; 47 | } 48 | return self; 49 | } 50 | 51 | - (id)initWithNibPath:(NSString*)aPath 52 | { 53 | if(self = [self init]) 54 | { 55 | NSData* nibData; 56 | NSString* keyedObjectsNibPath = [aPath stringByAppendingPathComponent:@"keyedobjects.nib"]; 57 | if([NSFileManager.defaultManager fileExistsAtPath:keyedObjectsNibPath]) 58 | nibData = [NSData dataWithContentsOfFile:keyedObjectsNibPath]; 59 | else nibData = [NSData dataWithContentsOfFile:aPath]; 60 | 61 | if(NSNib* nib = [[NSNib alloc] initWithNibData:nibData bundle:nil]) 62 | { 63 | BOOL didInstantiate = NO; 64 | NSArray* objects; 65 | 66 | didInstantiate = [nib instantiateWithOwner:self topLevelObjects:&objects]; 67 | 68 | if(didInstantiate) 69 | { 70 | _topLevelObjects = objects; 71 | for(id object in _topLevelObjects) 72 | { 73 | if([object isKindOfClass:[NSWindow class]]) 74 | [self setWindow:object]; 75 | } 76 | 77 | if(_window) 78 | return self; 79 | 80 | NSLog(@"%s failed to find window in nib: %@", sel_getName(_cmd), aPath); 81 | } 82 | } 83 | else 84 | { 85 | NSLog(@"%s failed loading nib: %@", sel_getName(_cmd), aPath); 86 | } 87 | } 88 | return nil; 89 | } 90 | 91 | - (void)dealloc 92 | { 93 | [self setWindow:nil]; 94 | } 95 | 96 | - (void)setWindow:(NSWindow*)aWindow 97 | { 98 | if(_window != aWindow) 99 | { 100 | [_window setDelegate:nil]; 101 | [_window orderOut:self]; 102 | 103 | _window = aWindow; 104 | [_window setDelegate:self]; 105 | [_window setReleasedWhenClosed:NO]; // incase this was set wrong in IB 106 | } 107 | } 108 | 109 | - (void)updateParametersWith:(id)plist 110 | { 111 | for(id key in [plist allKeys]) 112 | [self.parameters setValue:[plist valueForKey:key] forKey:key]; 113 | } 114 | 115 | - (void)showWindowAndCenter:(BOOL)shouldCenter 116 | { 117 | if(shouldCenter) 118 | { 119 | if(NSWindow* keyWindow = [NSApp keyWindow]) 120 | { 121 | NSRect frame = [_window frame], parentFrame = [keyWindow frame]; 122 | [_window setFrame:NSMakeRect(NSMidX(parentFrame) - 0.5 * NSWidth(frame), NSMidY(parentFrame) - 0.5 * NSHeight(frame), NSWidth(frame), NSHeight(frame)) display:NO]; 123 | } 124 | else 125 | { 126 | [_window center]; 127 | } 128 | } 129 | [_window makeKeyAndOrderFront:self]; 130 | } 131 | 132 | - (void)makeControllersCommitEditing 133 | { 134 | for(id object in self.topLevelObjects) 135 | { 136 | if([object respondsToSelector:@selector(commitEditing)]) 137 | [object commitEditing]; 138 | } 139 | 140 | [NSUserDefaults.standardUserDefaults synchronize]; 141 | } 142 | 143 | - (void)tearDown 144 | { 145 | [self.parameters removeObjectForKey:@"controller"]; 146 | 147 | // if we do not manually unbind, the object in the nib will keep us retained, and thus we will never reach dealloc 148 | for(id object in self.topLevelObjects) 149 | { 150 | if([object isKindOfClass:[NSObjectController class]]) 151 | [object unbind:@"contentObject"]; 152 | } 153 | 154 | // It isn’t always safe to release our window in windowWillClose: (at least on 10.9) which 155 | // is why we schedule the (implicit) release to run after current event loop cycle. 156 | [NibControllers performSelector:@selector(removeObjectForKey:) withObject:_token afterDelay:0]; 157 | } 158 | 159 | // ================================== 160 | // = Getting stuff from this window = 161 | // ================================== 162 | - (void)addClientFileHandle:(NSFileHandle*)aFileHandle modal:(BOOL)flag 163 | { 164 | [clientFileHandles addObject:aFileHandle]; 165 | if(!runningModal && (runningModal = flag)) 166 | [NSApp runModalForWindow:_window]; 167 | } 168 | 169 | - (void)return:(NSDictionary*)eventInfo 170 | { 171 | [self makeControllersCommitEditing]; 172 | 173 | id model = [self.parameters mutableCopy]; 174 | [model removeObjectForKey:@"controller"]; 175 | 176 | NSDictionary* res = @{ @"model": model, @"eventInfo": eventInfo }; 177 | 178 | for(NSFileHandle* fileHandle in clientFileHandles) 179 | [TMDCommand writePropertyList:res toFileHandle:fileHandle]; 180 | 181 | [clientFileHandles removeAllObjects]; 182 | 183 | if(runningModal) 184 | { 185 | [NSApp stopModal]; 186 | runningModal = NO; 187 | } 188 | } 189 | 190 | // ================================================ 191 | // = Events which return data to clients waiting = 192 | // ================================================ 193 | - (void)windowWillClose:(NSNotification*)aNotification 194 | { 195 | if([clientFileHandles count]) 196 | [self return:@{ @"type": @"closeWindow" }]; 197 | else [self tearDown]; 198 | } 199 | 200 | // ================================================ 201 | // = Faking a returnArgument:[…:]* implementation = 202 | // ================================================ 203 | // returnArgument: implementation. See 204 | - (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector 205 | { 206 | NSString* str = NSStringFromSelector(aSelector); 207 | if([str hasPrefix:@"returnArgument:"]) 208 | { 209 | std::string types; 210 | types += @encode(void); 211 | types += @encode(id); 212 | types += @encode(SEL); 213 | 214 | NSUInteger numberOfArgs = [[str componentsSeparatedByString:@":"] count]; 215 | while(numberOfArgs-- > 1) 216 | types += @encode(id); 217 | 218 | return [NSMethodSignature signatureWithObjCTypes:types.c_str()]; 219 | } 220 | return [super methodSignatureForSelector:aSelector]; 221 | } 222 | 223 | - (void)forwardInvocation:(NSInvocation*)invocation 224 | { 225 | NSString* str = NSStringFromSelector([invocation selector]); 226 | if([str hasPrefix:@"returnArgument:"]) 227 | { 228 | NSArray* argNames = [str componentsSeparatedByString:@":"]; 229 | 230 | NSMutableDictionary* res = [NSMutableDictionary dictionary]; 231 | [res setObject:@"bindingAction" forKey:@"type"]; 232 | 233 | for(NSUInteger i = 2; i < [[invocation methodSignature] numberOfArguments]; ++i) 234 | { 235 | __unsafe_unretained id arg = nil; 236 | [invocation getArgument:&arg atIndex:i]; 237 | [res setObject:(arg ?: @"") forKey:[argNames objectAtIndex:i - 2]]; 238 | } 239 | 240 | [self return:res]; 241 | } 242 | else 243 | { 244 | [super forwardInvocation:invocation]; 245 | } 246 | } 247 | 248 | // =============================== 249 | // = The old performButtonClicl: = 250 | // =============================== 251 | - (IBAction)performButtonClick:(id)sender 252 | { 253 | NSMutableDictionary* res = [NSMutableDictionary dictionary]; 254 | res[@"type"] = @"buttonClick"; 255 | 256 | if([sender respondsToSelector:@selector(title)]) 257 | res[@"title"] = [sender title]; 258 | if([sender respondsToSelector:@selector(tag)]) 259 | res[@"tag"] = @([sender tag]); 260 | 261 | [self return:res]; 262 | } 263 | @end 264 | -------------------------------------------------------------------------------- /Commands/tooltip/TMDHTMLTips.mm: -------------------------------------------------------------------------------- 1 | // 2 | // TMDHTMLTips.mm 3 | // 4 | // Created by Ciarán Walsh on 2007-08-19. 5 | // 6 | 7 | #import "TMDHTMLTips.h" 8 | 9 | /* 10 | "$DIALOG" tooltip --text '‘foobar’' 11 | "$DIALOG" tooltip --html '

‘foobar’

' 12 | */ 13 | 14 | NSString* const TMDTooltipPreferencesIdentifier = @"TM Tooltip"; 15 | 16 | @interface TMDHTMLTip () 17 | { 18 | WebView* webView; 19 | WebPreferences* webPreferences; 20 | 21 | NSDate* didOpenAtDate; // ignore mouse moves for the next second 22 | NSPoint mousePositionWhenOpened; 23 | } 24 | - (void)setContent:(NSString*)content transparent:(BOOL)transparent; 25 | - (void)runUntilUserActivity:(id)sender; 26 | @end 27 | 28 | @implementation TMDHTMLTip 29 | // ================== 30 | // = Setup/teardown = 31 | // ================== 32 | + (void)showWithContent:(NSString*)content atLocation:(NSPoint)point transparent:(BOOL)transparent 33 | { 34 | TMDHTMLTip* tip = [TMDHTMLTip new]; 35 | [tip setFrameTopLeftPoint:point]; 36 | [tip setContent:content transparent:transparent]; // The tooltip will show itself automatically when the HTML is loaded 37 | } 38 | 39 | - (id)init; 40 | { 41 | if(self = [self initWithContentRect:NSMakeRect(0, 0, 1, 1) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]) 42 | { 43 | // Since we are relying on `setReleaseWhenClosed:`, we need to ensure that we are over-retained. 44 | CFBridgingRetain(self); 45 | [self setReleasedWhenClosed:YES]; 46 | [self setAlphaValue:0.97]; 47 | [self setOpaque:NO]; 48 | [self setBackgroundColor:[NSColor colorWithDeviceRed:1.0 green:0.96 blue:0.76 alpha:1.0]]; 49 | [self setBackgroundColor:[NSColor clearColor]]; 50 | [self setHasShadow:YES]; 51 | [self setLevel:NSStatusWindowLevel]; 52 | [self setHidesOnDeactivate:YES]; 53 | [self setIgnoresMouseEvents:YES]; 54 | 55 | webPreferences = [[WebPreferences alloc] initWithIdentifier:TMDTooltipPreferencesIdentifier]; 56 | [webPreferences setJavaScriptEnabled:YES]; 57 | [webPreferences setPlugInsEnabled:NO]; 58 | [webPreferences setUsesPageCache:NO]; 59 | [webPreferences setCacheModel:WebCacheModelDocumentViewer]; 60 | NSString* fontName = [NSUserDefaults.standardUserDefaults stringForKey:@"fontName"]; 61 | int fontSize = [NSUserDefaults.standardUserDefaults integerForKey:@"fontSize"] ?: 11; 62 | NSFont* font = fontName ? [NSFont fontWithName:fontName size:fontSize] : [NSFont userFixedPitchFontOfSize:fontSize]; 63 | [webPreferences setStandardFontFamily:[font familyName]]; 64 | [webPreferences setDefaultFontSize:fontSize]; 65 | [webPreferences setDefaultFixedFontSize:fontSize]; 66 | 67 | webView = [[WebView alloc] initWithFrame:NSZeroRect]; 68 | [webView setPreferencesIdentifier:TMDTooltipPreferencesIdentifier]; 69 | [webView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; 70 | [webView setFrameLoadDelegate:self]; 71 | [webView setDrawsBackground:NO]; 72 | 73 | [self setContentView:webView]; 74 | } 75 | return self; 76 | } 77 | 78 | // =========== 79 | // = Webview = 80 | // =========== 81 | - (void)setContent:(NSString*)content transparent:(BOOL)transparent 82 | { 83 | NSString* fullContent = @"" 84 | @"" 85 | @" " 96 | @"" 97 | @"%@" 98 | @""; 99 | 100 | fullContent = [NSString stringWithFormat:fullContent, transparent ? @"transparent" : @"#F6EDC3", content]; 101 | [[webView mainFrame] loadHTMLString:fullContent baseURL:nil]; 102 | } 103 | 104 | - (void)sizeToContent 105 | { 106 | // Current tooltip position 107 | NSPoint pos = NSMakePoint([self frame].origin.x, [self frame].origin.y + [self frame].size.height); 108 | 109 | // Find the screen which we are displaying on 110 | NSRect screenFrame = [[NSScreen mainScreen] visibleFrame]; 111 | for(NSScreen* candidate in [NSScreen screens]) 112 | { 113 | if(NSPointInRect(pos, [candidate frame])) 114 | { 115 | screenFrame = [candidate visibleFrame]; 116 | break; 117 | } 118 | } 119 | 120 | // The webview is set to a large initial size and then sized down to fit the content 121 | [self setContentSize:NSMakeSize(screenFrame.size.width - screenFrame.size.width / 3.0, screenFrame.size.height)]; 122 | 123 | double height = ceil([[[webView windowScriptObject] evaluateWebScript:@"document.body.getBoundingClientRect().bottom;"] doubleValue]); 124 | double width = ceil([[[webView windowScriptObject] evaluateWebScript:@"document.body.getBoundingClientRect().right;"] doubleValue]); 125 | 126 | [webView setFrameSize:NSMakeSize(width, height)]; 127 | 128 | NSRect frame = [self frameRectForContentRect:[webView frame]]; 129 | frame.size.width = std::min(NSWidth(frame), NSWidth(screenFrame)); 130 | frame.size.height = std::min(NSHeight(frame), NSHeight(screenFrame)); 131 | [self setFrame:frame display:NO]; 132 | 133 | pos.x = std::max(NSMinX(screenFrame), std::min(pos.x, NSMaxX(screenFrame)-NSWidth(frame))); 134 | pos.y = std::min(std::max(NSMinY(screenFrame)+NSHeight(frame), pos.y), NSMaxY(screenFrame)); 135 | 136 | [self setFrameTopLeftPoint:pos]; 137 | } 138 | 139 | - (void)delayedSizeAndShow:(id)sender 140 | { 141 | [self sizeToContent]; 142 | [self orderFront:self]; 143 | [self runUntilUserActivity:self]; 144 | } 145 | 146 | - (void)webView:(WebView*)sender didFinishLoadForFrame:(WebFrame*)frame; 147 | { 148 | [self performSelector:@selector(delayedSizeAndShow:) withObject:self afterDelay:0]; 149 | } 150 | 151 | // ================== 152 | // = Event handling = 153 | // ================== 154 | - (BOOL)shouldCloseForMousePosition:(NSPoint)aPoint 155 | { 156 | CGFloat ignorePeriod = [NSUserDefaults.standardUserDefaults floatForKey:@"OakToolTipMouseMoveIgnorePeriod"]; 157 | if(-[didOpenAtDate timeIntervalSinceNow] < ignorePeriod) 158 | return NO; 159 | 160 | if(NSEqualPoints(mousePositionWhenOpened, NSZeroPoint)) 161 | { 162 | mousePositionWhenOpened = aPoint; 163 | return NO; 164 | } 165 | 166 | NSPoint const& p = mousePositionWhenOpened; 167 | CGFloat deltaX = p.x - aPoint.x; 168 | CGFloat deltaY = p.y - aPoint.y; 169 | CGFloat dist = sqrt(deltaX * deltaX + deltaY * deltaY); 170 | 171 | CGFloat moveThreshold = [NSUserDefaults.standardUserDefaults floatForKey:@"OakToolTipMouseDistanceThreshold"]; 172 | return dist > moveThreshold; 173 | } 174 | 175 | - (void)runUntilUserActivity:(id)sender 176 | { 177 | [self setValue:[NSDate date] forKey:@"didOpenAtDate"]; 178 | mousePositionWhenOpened = NSZeroPoint; 179 | 180 | NSWindow* keyWindow = [NSApp keyWindow]; 181 | BOOL didAcceptMouseMovedEvents = [keyWindow acceptsMouseMovedEvents]; 182 | [keyWindow setAcceptsMouseMovedEvents:YES]; 183 | 184 | BOOL slowFadeOut = NO; 185 | while(NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:YES]) 186 | { 187 | [NSApp sendEvent:event]; 188 | 189 | if([event type] == NSEventTypeLeftMouseDown || [event type] == NSEventTypeRightMouseDown || [event type] == NSEventTypeOtherMouseDown || [event type] == NSEventTypeKeyDown || [event type] == NSEventTypeScrollWheel) 190 | break; 191 | 192 | if([event type] == NSEventTypeMouseMoved && [self shouldCloseForMousePosition:[NSEvent mouseLocation]]) 193 | { 194 | slowFadeOut = YES; 195 | break; 196 | } 197 | 198 | if(keyWindow != [NSApp keyWindow] || ![NSApp isActive]) 199 | break; 200 | } 201 | 202 | [keyWindow setAcceptsMouseMovedEvents:didAcceptMouseMovedEvents]; 203 | 204 | 205 | [self fadeOutSlowly:slowFadeOut]; 206 | } 207 | 208 | // ============= 209 | // = Animation = 210 | // ============= 211 | - (void)fadeOutSlowly:(BOOL)slowly 212 | { 213 | [NSAnimationContext beginGrouping]; 214 | 215 | [NSAnimationContext currentContext].duration = slowly ? 0.5 : 0.25; 216 | [NSAnimationContext currentContext].completionHandler = ^{ 217 | [self orderOut:self]; 218 | }; 219 | 220 | [self.animator setAlphaValue:0]; 221 | 222 | [NSAnimationContext endGrouping]; 223 | } 224 | @end 225 | -------------------------------------------------------------------------------- /Commands/popup/TMDIncrementalPopUpMenu.mm: -------------------------------------------------------------------------------- 1 | // 2 | // TMDIncrementalPopUpMenu.mm 3 | // 4 | // Created by Joachim Mårtensson on 2007-08-10. 5 | // 6 | 7 | #import "TMDIncrementalPopUpMenu.h" 8 | #import "../Utilities/TextMate.h" // -insertSnippetWithOptions 9 | #import "../../TMDCommand.h" // -writeString: 10 | #import "../../Dialog2.h" 11 | 12 | @interface NSTableView (MovingSelectedRow) 13 | - (BOOL)TMDcanHandleEvent:(NSEvent*)anEvent; 14 | @end 15 | 16 | @implementation NSTableView (MovingSelectedRow) 17 | - (BOOL)TMDcanHandleEvent:(NSEvent*)anEvent 18 | { 19 | if([anEvent type] != NSEventTypeKeyDown || [[anEvent characters] length] != 1) 20 | return NO; 21 | 22 | int visibleRows = (int)floorf(NSHeight([self visibleRect]) / ([self rowHeight]+[self intercellSpacing].height)) - 1; 23 | struct { unichar key; int rows; } const keyMovements[] = 24 | { 25 | { NSUpArrowFunctionKey, -1 }, 26 | { NSDownArrowFunctionKey, +1 }, 27 | { NSPageUpFunctionKey, -visibleRows }, 28 | { NSPageDownFunctionKey, +visibleRows }, 29 | { NSHomeFunctionKey, -(INT_MAX >> 1) }, 30 | { NSEndFunctionKey, +(INT_MAX >> 1) }, 31 | }; 32 | 33 | unichar keyCode = [[anEvent characters] characterAtIndex:0]; 34 | for(auto const& keyMovement : keyMovements) 35 | { 36 | if(keyCode == keyMovement.key) 37 | { 38 | int row = std::max(0, std::min([self selectedRow] + keyMovement.rows, [self numberOfRows]-1)); 39 | [self selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; 40 | [self scrollRowToVisible:row]; 41 | 42 | return YES; 43 | } 44 | } 45 | 46 | return NO; 47 | } 48 | @end 49 | 50 | @interface TMDIncrementalPopUpMenu () 51 | { 52 | NSFileHandle* outputHandle; 53 | NSArray* suggestions; 54 | NSMutableString* mutablePrefix; 55 | NSString* staticPrefix; 56 | NSArray* filtered; 57 | NSTableView* theTableView; 58 | BOOL isAbove; 59 | BOOL closeMe; 60 | BOOL caseSensitive; 61 | 62 | NSMutableCharacterSet* textualInputCharacters; 63 | } 64 | 65 | - (NSRect)rectOfMainScreen; 66 | - (NSString*)filterString; 67 | - (void)setupInterface; 68 | - (void)filter; 69 | - (void)insertCommonPrefix; 70 | - (void)completeAndInsertSnippet; 71 | @end 72 | 73 | @implementation TMDIncrementalPopUpMenu 74 | // ============================= 75 | // = Setup/tear-down functions = 76 | // ============================= 77 | - (id)init 78 | { 79 | if(self = [super initWithContentRect:NSMakeRect(0, 0, 1, 1) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]) 80 | { 81 | mutablePrefix = [NSMutableString new]; 82 | textualInputCharacters = [NSMutableCharacterSet alphanumericCharacterSet]; 83 | caseSensitive = YES; 84 | 85 | [self setupInterface]; 86 | } 87 | return self; 88 | } 89 | 90 | - (id)initWithItems:(NSArray*)someSuggestions alreadyTyped:(NSString*)aUserString staticPrefix:(NSString*)aStaticPrefix additionalWordCharacters:(NSString*)someAdditionalWordCharacters caseSensitive:(BOOL)isCaseSensitive writeChoiceToFileDescriptor:(NSFileHandle*)aFileDescriptor 91 | { 92 | if(self = [self init]) 93 | { 94 | suggestions = someSuggestions; 95 | 96 | if(aUserString) 97 | [mutablePrefix appendString:aUserString]; 98 | 99 | if(aStaticPrefix) 100 | staticPrefix = aStaticPrefix; 101 | 102 | if(someAdditionalWordCharacters) 103 | [textualInputCharacters addCharactersInString:someAdditionalWordCharacters]; 104 | 105 | caseSensitive = isCaseSensitive; 106 | outputHandle = aFileDescriptor; 107 | } 108 | return self; 109 | } 110 | 111 | - (void)setCaretPos:(NSPoint)aPos 112 | { 113 | _caretPos = aPos; 114 | isAbove = NO; 115 | 116 | NSRect mainScreen = [self rectOfMainScreen]; 117 | 118 | CGFloat offx = (_caretPos.x/mainScreen.size.width) + 1.0; 119 | if((_caretPos.x + [self frame].size.width) > (mainScreen.size.width*offx)) 120 | _caretPos.x = _caretPos.x - [self frame].size.width; 121 | 122 | if(_caretPos.y>=0 && _caretPos.y<[self frame].size.height) 123 | { 124 | _caretPos.y = _caretPos.y + ([self frame].size.height + [NSUserDefaults.standardUserDefaults integerForKey:@"OakTextViewNormalFontSize"]*1.5); 125 | isAbove = YES; 126 | } 127 | if(_caretPos.y<0 && (mainScreen.size.height-[self frame].size.height)<(_caretPos.y*-1.0)) 128 | { 129 | _caretPos.y = _caretPos.y + ([self frame].size.height + [NSUserDefaults.standardUserDefaults integerForKey:@"OakTextViewNormalFontSize"]*1.5); 130 | isAbove = YES; 131 | } 132 | [self setFrameTopLeftPoint:_caretPos]; 133 | } 134 | 135 | - (void)setupInterface 136 | { 137 | // Since we are relying on `setReleaseWhenClosed:`, we need to ensure that we are over-retained. 138 | CFBridgingRetain(self); 139 | [self setReleasedWhenClosed:YES]; 140 | [self setLevel:NSStatusWindowLevel]; 141 | [self setHidesOnDeactivate:YES]; 142 | [self setHasShadow:YES]; 143 | 144 | NSScrollView* scrollView = [[NSScrollView alloc] initWithFrame:NSZeroRect]; 145 | [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; 146 | [scrollView setAutohidesScrollers:YES]; 147 | [scrollView setHasVerticalScroller:YES]; 148 | [[scrollView verticalScroller] setControlSize:NSControlSizeSmall]; 149 | 150 | theTableView = [[NSTableView alloc] initWithFrame:NSZeroRect]; 151 | [theTableView setFocusRingType:NSFocusRingTypeNone]; 152 | [theTableView setAllowsEmptySelection:NO]; 153 | [theTableView setHeaderView:nil]; 154 | //[theTableView setBackgroundColor:[NSColor blackColor]]; 155 | [theTableView setDoubleAction:@selector(didDoubleClickRow:)]; 156 | [theTableView setTarget:self]; 157 | 158 | if(@available(macos 11.0, *)) 159 | theTableView.style = NSTableViewStylePlain; 160 | 161 | NSTableColumn* column = [[NSTableColumn alloc] initWithIdentifier:@"display"]; 162 | [theTableView addTableColumn:column]; 163 | 164 | [theTableView setDataSource:self]; 165 | [theTableView setDelegate:self]; 166 | [scrollView setDocumentView:theTableView]; 167 | 168 | if(@available(macos 10.14, *)) 169 | { 170 | theTableView.backgroundColor = NSColor.clearColor; 171 | scrollView.drawsBackground = NO; 172 | 173 | NSVisualEffectView* effectView = [[NSVisualEffectView alloc] initWithFrame:NSZeroRect]; 174 | effectView.material = NSVisualEffectMaterialMenu; 175 | effectView.blendingMode = NSVisualEffectBlendingModeBehindWindow; 176 | effectView.autoresizingMask = NSViewWidthSizable|NSViewHeightSizable; 177 | [effectView addSubview:scrollView positioned:NSWindowBelow relativeTo:nil]; 178 | 179 | [self setContentView:effectView]; 180 | } 181 | else 182 | { 183 | [self setContentView:scrollView]; 184 | } 185 | } 186 | 187 | //- (void)tableView:(NSTableView*)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn*)aTableColumn row:(NSInteger)rowIndex { 188 | // [aCell setTextColor:[NSColor blueColor]]; 189 | //} 190 | 191 | 192 | // ======================== 193 | // = TableView DataSource = 194 | // ======================== 195 | 196 | - (NSInteger)numberOfRowsInTableView:(NSTableView*)aTableView 197 | { 198 | return [filtered count]; 199 | } 200 | 201 | // ====================== 202 | // = TableView Delegate = 203 | // ====================== 204 | 205 | - (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row 206 | { 207 | id object = [filtered objectAtIndex:row]; 208 | NSString* identifier = [object objectForKey:@"display"]; 209 | NSTableCellView* cell = [tableView makeViewWithIdentifier:identifier owner:self]; 210 | if(!cell) 211 | { 212 | cell = [[NSTableCellView alloc] initWithFrame:NSZeroRect]; 213 | NSImageView* image = [[NSImageView alloc] initWithFrame:NSZeroRect]; 214 | cell.imageView = image; 215 | [cell addSubview:image]; 216 | NSTextField* text = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 1, 1)]; 217 | text.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; 218 | text.bordered = NO; 219 | text.drawsBackground = NO; 220 | text.editable = NO; 221 | text.lineBreakMode = NSLineBreakByTruncatingTail; 222 | text.textColor = NSColor.controlTextColor; 223 | cell.textField = text; 224 | [cell addSubview:text]; 225 | } 226 | cell.textField.stringValue = identifier; 227 | cell.imageView.image = [NSImage imageNamed:[object objectForKey:@"image"]]; 228 | if(cell.imageView.image) 229 | { 230 | CGFloat h = tableView.rowHeight; 231 | cell.imageView.frame = NSMakeRect(0, 0, h, h); 232 | cell.textField.frame = NSMakeRect(h + 3, 0, 1, 1); 233 | } 234 | cell.toolTip = [object objectForKey:@"tooltip"]; 235 | return cell; 236 | } 237 | 238 | // ==================== 239 | // = Filter the items = 240 | // ==================== 241 | 242 | - (void)filter 243 | { 244 | NSRect mainScreen = [self rectOfMainScreen]; 245 | 246 | NSArray* newFiltered; 247 | NSArray* itemsWithChildren; 248 | if([mutablePrefix length] > 0) 249 | { 250 | NSPredicate* matchesFilter; 251 | NSPredicate* hasChildren; 252 | 253 | if(caseSensitive) 254 | matchesFilter = [NSPredicate predicateWithFormat:@"match BEGINSWITH %@ OR (match == NULL AND display BEGINSWITH %@)", [self filterString], [self filterString]]; 255 | else matchesFilter = [NSPredicate predicateWithFormat:@"match BEGINSWITH[c] %@ OR (match == NULL AND display BEGINSWITH[c] %@)", [self filterString], [self filterString]]; 256 | 257 | newFiltered = [suggestions filteredArrayUsingPredicate:matchesFilter]; 258 | if([newFiltered count] == 1) 259 | { 260 | newFiltered = [newFiltered arrayByAddingObjectsFromArray:[[newFiltered lastObject] objectForKey:@"children"]]; 261 | } 262 | else if([newFiltered count] == 0) 263 | { 264 | hasChildren = [NSPredicate predicateWithFormat:@"children != NULL"]; 265 | itemsWithChildren = [suggestions filteredArrayUsingPredicate:hasChildren]; 266 | for(NSUInteger i = 0; i < [itemsWithChildren count]; i++) 267 | { 268 | newFiltered=[newFiltered arrayByAddingObjectsFromArray:[[[itemsWithChildren objectAtIndex:i] objectForKey:@"children"] filteredArrayUsingPredicate:matchesFilter]]; 269 | } 270 | } 271 | } 272 | else 273 | { 274 | newFiltered = suggestions; 275 | } 276 | 277 | 278 | filtered = newFiltered; 279 | [theTableView reloadData]; 280 | 281 | NSPoint old = NSMakePoint([self frame].origin.x, [self frame].origin.y + [self frame].size.height); 282 | 283 | NSUInteger displayedRows = [newFiltered count] < MAX_ROWS ? [newFiltered count] : MAX_ROWS; 284 | CGFloat newHeight = ([theTableView rowHeight] + [theTableView intercellSpacing].height) * displayedRows; 285 | 286 | CGFloat maxWidth = 60; 287 | if([newFiltered count]>0) 288 | { 289 | for(NSUInteger i = 0; i < theTableView.numberOfRows; ++i) 290 | maxWidth = MAX(maxWidth, [self widthAtColumn:0 row:i]); 291 | maxWidth = MIN(maxWidth, 600); 292 | } 293 | if(_caretPos.y>=0 && (isAbove || _caretPos.y