Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

1661 lines (1399 sloc) 59.256 kb
/* vi:set ts=8 sts=4 sw=4 ft=objc:
*
* VIM - Vi IMproved by Bram Moolenaar
* MacVim GUI port by Bjorn Winckler
*
* Do ":help uganda" in Vim to read copying and usage conditions.
* Do ":help credits" in Vim to see a list of people who contributed.
* See README.txt for an overview of the Vim source code.
*/
/*
* MMVimController
*
* Coordinates input/output to/from backend. A MMVimController sends input
* directly to a MMBackend, but communication from MMBackend to MMVimController
* goes via MMAppController so that it can coordinate all incoming distributed
* object messages.
*
* MMVimController does not deal with visual presentation. Essentially it
* should be able to run with no window present.
*
* Output from the backend is received in processInputQueue: (this message is
* called from MMAppController so it is not a DO call). Input is sent to the
* backend via sendMessage:data: or addVimInput:. The latter allows execution
* of arbitrary strings in the Vim process, much like the Vim script function
* remote_send() does. The messages that may be passed between frontend and
* backend are defined in an enum in MacVim.h.
*/
#import "MMAppController.h"
#import "MMFindReplaceController.h"
#import "MMTextView.h"
#import "MMVimController.h"
#import "MMVimView.h"
#import "MMWindowController.h"
#import "Miscellaneous.h"
#import "MMCoreTextView.h"
#import "MMWindow.h"
static NSString *MMDefaultToolbarImageName = @"Attention";
static int MMAlertTextFieldHeight = 22;
// NOTE: By default a message sent to the backend will be dropped if it cannot
// be delivered instantly; otherwise there is a possibility that MacVim will
// 'beachball' while waiting to deliver DO messages to an unresponsive Vim
// process. This means that you cannot rely on any message sent with
// sendMessage: to actually reach Vim.
static NSTimeInterval MMBackendProxyRequestTimeout = 0;
// Timeout used for setDialogReturn:.
static NSTimeInterval MMSetDialogReturnTimeout = 1.0;
static unsigned identifierCounter = 1;
static BOOL isUnsafeMessage(int msgid);
// HACK! AppKit private methods from NSToolTipManager. As an alternative to
// using private methods, it would be possible to set the user default
// NSInitialToolTipDelay (in ms) on app startup, but then it is impossible to
// change the balloon delay without closing/reopening a window.
@interface NSObject (NSToolTipManagerPrivateAPI)
+ (id)sharedToolTipManager;
- (void)setInitialToolTipDelay:(double)arg1;
@end
@interface MMAlert : NSAlert {
NSTextField *textField;
}
- (void)setTextFieldString:(NSString *)textFieldString;
- (NSTextField *)textField;
@end
@interface MMVimController (Private)
- (void)doProcessInputQueue:(NSArray *)queue;
- (void)handleMessage:(int)msgid data:(NSData *)data;
- (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
context:(void *)context;
- (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context;
- (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc;
- (NSMenu *)parentMenuForDescriptor:(NSArray *)desc;
- (NSMenu *)topLevelMenuForTitle:(NSString *)title;
- (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)index;
- (void)addMenuItemWithDescriptor:(NSArray *)desc
atIndex:(int)index
tip:(NSString *)tip
icon:(NSString *)icon
keyEquivalent:(NSString *)keyEquivalent
modifierMask:(int)modifierMask
action:(NSString *)action
isAlternate:(BOOL)isAlternate;
- (void)removeMenuItemWithDescriptor:(NSArray *)desc;
- (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on;
- (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
toolTip:(NSString *)tip icon:(NSString *)icon;
- (void)addToolbarItemWithLabel:(NSString *)label
tip:(NSString *)tip icon:(NSString *)icon
atIndex:(int)idx;
- (void)popupMenuWithDescriptor:(NSArray *)desc
atRow:(NSNumber *)row
column:(NSNumber *)col;
- (void)popupMenuWithAttributes:(NSDictionary *)attrs;
- (void)connectionDidDie:(NSNotification *)notification;
- (void)scheduleClose;
- (void)handleBrowseForFile:(NSDictionary *)attr;
- (void)handleShowDialog:(NSDictionary *)attr;
- (void)handleDeleteSign:(NSDictionary *)attr;
- (void)setToolTipDelay:(NSTimeInterval)seconds;
@end
@implementation MMVimController
- (id)initWithBackend:(id)backend pid:(int)processIdentifier
{
if (!(self = [super init]))
return nil;
// TODO: Come up with a better way of creating an identifier.
identifier = identifierCounter++;
windowController =
[[MMWindowController alloc] initWithVimController:self];
backendProxy = [backend retain];
popupMenuItems = [[NSMutableArray alloc] init];
toolbarItemDict = [[NSMutableDictionary alloc] init];
pid = processIdentifier;
creationDate = [[NSDate alloc] init];
NSConnection *connection = [backendProxy connectionForProxy];
// TODO: Check that this will not set the timeout for the root proxy
// (in MMAppController).
[connection setRequestTimeout:MMBackendProxyRequestTimeout];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(connectionDidDie:)
name:NSConnectionDidDieNotification object:connection];
// Set up a main menu with only a "MacVim" menu (copied from a template
// which itself is set up in MainMenu.nib). The main menu is populated
// by Vim later on.
mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"];
NSMenuItem *appMenuItem = [[MMAppController sharedInstance]
appMenuItemTemplate];
appMenuItem = [[appMenuItem copy] autorelease];
// Note: If the title of the application menu is anything but what
// CFBundleName says then the application menu will not be typeset in
// boldface for some reason. (It should already be set when we copy
// from the default main menu, but this is not the case for some
// reason.)
NSString *appName = [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleName"];
[appMenuItem setTitle:appName];
[mainMenu addItem:appMenuItem];
isInitialized = YES;
return self;
}
- (void)dealloc
{
ASLogDebug(@"");
isInitialized = NO;
[serverName release]; serverName = nil;
[backendProxy release]; backendProxy = nil;
[toolbarItemDict release]; toolbarItemDict = nil;
[toolbar release]; toolbar = nil;
[popupMenuItems release]; popupMenuItems = nil;
[windowController release]; windowController = nil;
[vimState release]; vimState = nil;
[mainMenu release]; mainMenu = nil;
[creationDate release]; creationDate = nil;
[super dealloc];
}
- (unsigned)vimControllerId
{
return identifier;
}
- (MMWindowController *)windowController
{
return windowController;
}
- (NSDictionary *)vimState
{
return vimState;
}
- (id)objectForVimStateKey:(NSString *)key
{
return [vimState objectForKey:key];
}
- (NSMenu *)mainMenu
{
return mainMenu;
}
- (BOOL)isPreloading
{
return isPreloading;
}
- (void)setIsPreloading:(BOOL)yn
{
isPreloading = yn;
}
- (BOOL)hasModifiedBuffer
{
return hasModifiedBuffer;
}
- (NSDate *)creationDate
{
return creationDate;
}
- (void)setServerName:(NSString *)name
{
if (name != serverName) {
[serverName release];
serverName = [name copy];
}
}
- (NSString *)serverName
{
return serverName;
}
- (int)pid
{
return pid;
}
- (void)dropFiles:(NSArray *)filenames forceOpen:(BOOL)force
{
filenames = normalizeFilenames(filenames);
ASLogInfo(@"filenames=%@ force=%d", filenames, force);
NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
// Default to opening in tabs if layout is invalid or set to "windows".
int layout = [ud integerForKey:MMOpenLayoutKey];
if (layout < 0 || layout > MMLayoutTabs)
layout = MMLayoutTabs;
BOOL splitVert = [ud boolForKey:MMVerticalSplitKey];
if (splitVert && MMLayoutHorizontalSplit == layout)
layout = MMLayoutVerticalSplit;
NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:layout], @"layout",
filenames, @"filenames",
[NSNumber numberWithBool:force], @"forceOpen",
nil];
[self sendMessage:DropFilesMsgID data:[args dictionaryAsData]];
// Add dropped files to the "Recent Files" menu.
[[NSDocumentController sharedDocumentController]
noteNewRecentFilePaths:filenames];
}
- (void)file:(NSString *)filename draggedToTabAtIndex:(NSUInteger)tabIndex
{
filename = normalizeFilename(filename);
ASLogInfo(@"filename=%@ index=%ld", filename, tabIndex);
NSString *fnEsc = [filename stringByEscapingSpecialFilenameCharacters];
NSString *input = [NSString stringWithFormat:@"<C-\\><C-N>:silent "
"tabnext %ld |"
"edit! %@<CR>", tabIndex + 1, fnEsc];
[self addVimInput:input];
}
- (void)filesDraggedToTabBar:(NSArray *)filenames
{
filenames = normalizeFilenames(filenames);
ASLogInfo(@"%@", filenames);
NSUInteger i, count = [filenames count];
NSMutableString *input = [NSMutableString stringWithString:@"<C-\\><C-N>"
":silent! tabnext 9999"];
for (i = 0; i < count; i++) {
NSString *fn = [filenames objectAtIndex:i];
NSString *fnEsc = [fn stringByEscapingSpecialFilenameCharacters];
[input appendFormat:@"|tabedit %@", fnEsc];
}
[input appendString:@"<CR>"];
[self addVimInput:input];
}
- (void)dropString:(NSString *)string
{
ASLogInfo(@"%@", string);
int len = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 1;
if (len > 0) {
NSMutableData *data = [NSMutableData data];
[data appendBytes:&len length:sizeof(int)];
[data appendBytes:[string UTF8String] length:len];
[self sendMessage:DropStringMsgID data:data];
}
}
- (void)passArguments:(NSDictionary *)args
{
if (!args) return;
ASLogDebug(@"args=%@", args);
[self sendMessage:OpenWithArgumentsMsgID data:[args dictionaryAsData]];
}
- (void)sendMessage:(int)msgid data:(NSData *)data
{
ASLogDebug(@"msg=%s (isInitialized=%d)",
MessageStrings[msgid], isInitialized);
if (!isInitialized) return;
@try {
[backendProxy processInput:msgid data:data];
}
@catch (NSException *ex) {
ASLogDebug(@"processInput:data: failed: pid=%d id=%d msg=%s reason=%@",
pid, identifier, MessageStrings[msgid], ex);
}
}
- (BOOL)sendMessageNow:(int)msgid data:(NSData *)data
timeout:(NSTimeInterval)timeout
{
// Send a message with a timeout. USE WITH EXTREME CAUTION! Sending
// messages in rapid succession with a timeout may cause MacVim to beach
// ball forever. In almost all circumstances sendMessage:data: should be
// used instead.
ASLogDebug(@"msg=%s (isInitialized=%d)",
MessageStrings[msgid], isInitialized);
if (!isInitialized)
return NO;
if (timeout < 0) timeout = 0;
BOOL sendOk = YES;
NSConnection *conn = [backendProxy connectionForProxy];
NSTimeInterval oldTimeout = [conn requestTimeout];
[conn setRequestTimeout:timeout];
@try {
[backendProxy processInput:msgid data:data];
}
@catch (NSException *ex) {
sendOk = NO;
ASLogDebug(@"processInput:data: failed: pid=%d id=%d msg=%s reason=%@",
pid, identifier, MessageStrings[msgid], ex);
}
@finally {
[conn setRequestTimeout:oldTimeout];
}
return sendOk;
}
- (void)addVimInput:(NSString *)string
{
ASLogDebug(@"%@", string);
// This is a very general method of adding input to the Vim process. It is
// basically the same as calling remote_send() on the process (see
// ':h remote_send').
if (string) {
NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
[self sendMessage:AddInputMsgID data:data];
}
}
- (NSString *)evaluateVimExpression:(NSString *)expr
{
NSString *eval = nil;
@try {
eval = [backendProxy evaluateExpression:expr];
ASLogDebug(@"eval(%@)=%@", expr, eval);
}
@catch (NSException *ex) {
ASLogDebug(@"evaluateExpression: failed: pid=%d id=%d reason=%@",
pid, identifier, ex);
}
return eval;
}
- (id)evaluateVimExpressionCocoa:(NSString *)expr
errorString:(NSString **)errstr
{
id eval = nil;
@try {
eval = [backendProxy evaluateExpressionCocoa:expr
errorString:errstr];
ASLogDebug(@"eval(%@)=%@", expr, eval);
} @catch (NSException *ex) {
ASLogDebug(@"evaluateExpressionCocoa: failed: pid=%d id=%d reason=%@",
pid, identifier, ex);
*errstr = [ex reason];
}
return eval;
}
- (id)backendProxy
{
return backendProxy;
}
- (void)cleanup
{
if (!isInitialized) return;
// Remove any delayed calls made on this object.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
isInitialized = NO;
[toolbar setDelegate:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self];
//[[backendProxy connectionForProxy] invalidate];
//[windowController close];
[windowController cleanup];
}
- (void)processInputQueue:(NSArray *)queue
{
if (!isInitialized) return;
// NOTE: This method must not raise any exceptions (see comment in the
// calling method).
@try {
[self doProcessInputQueue:queue];
[windowController processInputQueueDidFinish];
}
@catch (NSException *ex) {
ASLogDebug(@"Exception: pid=%d id=%d reason=%@", pid, identifier, ex);
}
}
- (NSToolbarItem *)toolbar:(NSToolbar *)theToolbar
itemForItemIdentifier:(NSString *)itemId
willBeInsertedIntoToolbar:(BOOL)flag
{
NSToolbarItem *item = [toolbarItemDict objectForKey:itemId];
if (!item) {
ASLogWarn(@"No toolbar item with id '%@'", itemId);
}
return item;
}
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)theToolbar
{
return nil;
}
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)theToolbar
{
return nil;
}
@end // MMVimController
@implementation MMVimController (Private)
- (void)doProcessInputQueue:(NSArray *)queue
{
NSMutableArray *delayQueue = nil;
unsigned i, count = [queue count];
if (count % 2) {
ASLogWarn(@"Uneven number of components (%d) in command queue. "
"Skipping...", count);
return;
}
for (i = 0; i < count; i += 2) {
NSData *value = [queue objectAtIndex:i];
NSData *data = [queue objectAtIndex:i+1];
int msgid = *((int*)[value bytes]);
BOOL inDefaultMode = [[[NSRunLoop currentRunLoop] currentMode]
isEqual:NSDefaultRunLoopMode];
if (!inDefaultMode && isUnsafeMessage(msgid)) {
// NOTE: Because we may be listening to DO messages in "event
// tracking mode" we have to take extra care when doing things
// like releasing view items (and other Cocoa objects).
// Messages that may be potentially "unsafe" are delayed until
// the run loop is back to default mode at which time they are
// safe to call again.
// A problem with this approach is that it is hard to
// classify which messages are unsafe. As a rule of thumb, if
// a message may release an object used by the Cocoa framework
// (e.g. views) then the message should be considered unsafe.
// Delaying messages may have undesired side-effects since it
// means that messages may not be processed in the order Vim
// sent them, so beware.
if (!delayQueue)
delayQueue = [NSMutableArray array];
ASLogDebug(@"Adding unsafe message '%s' to delay queue (mode=%@)",
MessageStrings[msgid],
[[NSRunLoop currentRunLoop] currentMode]);
[delayQueue addObject:value];
[delayQueue addObject:data];
} else {
[self handleMessage:msgid data:data];
}
}
if (delayQueue) {
ASLogDebug(@" Flushing delay queue (%ld items)",
[delayQueue count]/2);
[self performSelector:@selector(processInputQueue:)
withObject:delayQueue
afterDelay:0];
}
}
- (void)handleMessage:(int)msgid data:(NSData *)data
{
if (OpenWindowMsgID == msgid) {
[windowController openWindow];
// HACK: Delay actually presenting the window onscreen until after
// processing the queue since it contains drawing commands that need to
// be issued before presentation; otherwise the window may flash white
// just as it opens.
if (!isPreloading)
[windowController performSelector:@selector(presentWindow:)
withObject:nil
afterDelay:0];
} else if (BatchDrawMsgID == msgid) {
[[[windowController vimView] textView] performBatchDrawWithData:data];
} else if (SelectTabMsgID == msgid) {
#if 0 // NOTE: Tab selection is done inside updateTabsWithData:.
const void *bytes = [data bytes];
int idx = *((int*)bytes);
[windowController selectTabWithIndex:idx];
#endif
} else if (UpdateTabBarMsgID == msgid) {
[windowController updateTabsWithData:data];
} else if (ShowTabBarMsgID == msgid) {
[windowController showTabBar:YES];
} else if (HideTabBarMsgID == msgid) {
[windowController showTabBar:NO];
} else if (SetTextDimensionsMsgID == msgid || LiveResizeMsgID == msgid ||
SetTextDimensionsReplyMsgID == msgid) {
const void *bytes = [data bytes];
int rows = *((int*)bytes); bytes += sizeof(int);
int cols = *((int*)bytes);
// NOTE: When a resize message originated in the frontend, Vim
// acknowledges it with a reply message. When this happens the window
// should not move (the frontend would already have moved the window).
BOOL onScreen = SetTextDimensionsReplyMsgID!=msgid;
[windowController setTextDimensionsWithRows:rows
columns:cols
isLive:(LiveResizeMsgID==msgid)
keepOnScreen:onScreen];
} else if (SetWindowTitleMsgID == msgid) {
const void *bytes = [data bytes];
int len = *((int*)bytes); bytes += sizeof(int);
NSString *string = [[NSString alloc] initWithBytes:(void*)bytes
length:len encoding:NSUTF8StringEncoding];
// While in live resize the window title displays the dimensions of the
// window so don't clobber this with a spurious "set title" message
// from Vim.
if (![[windowController vimView] inLiveResize])
[windowController setTitle:string];
[string release];
} else if (SetDocumentFilenameMsgID == msgid) {
const void *bytes = [data bytes];
int len = *((int*)bytes); bytes += sizeof(int);
if (len > 0) {
NSString *filename = [[NSString alloc] initWithBytes:(void*)bytes
length:len encoding:NSUTF8StringEncoding];
[windowController setDocumentFilename:filename];
[filename release];
} else {
[windowController setDocumentFilename:@""];
}
} else if (AddMenuMsgID == msgid) {
NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
[self addMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
atIndex:[[attrs objectForKey:@"index"] intValue]];
} else if (AddMenuItemMsgID == msgid) {
NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
[self addMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
atIndex:[[attrs objectForKey:@"index"] intValue]
tip:[attrs objectForKey:@"tip"]
icon:[attrs objectForKey:@"icon"]
keyEquivalent:[attrs objectForKey:@"keyEquivalent"]
modifierMask:[[attrs objectForKey:@"modifierMask"] intValue]
action:[attrs objectForKey:@"action"]
isAlternate:[[attrs objectForKey:@"isAlternate"] boolValue]];
} else if (RemoveMenuItemMsgID == msgid) {
NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
[self removeMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]];
} else if (EnableMenuItemMsgID == msgid) {
NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
[self enableMenuItemWithDescriptor:[attrs objectForKey:@"descriptor"]
state:[[attrs objectForKey:@"enable"] boolValue]];
} else if (ShowToolbarMsgID == msgid) {
const void *bytes = [data bytes];
int enable = *((int*)bytes); bytes += sizeof(int);
int flags = *((int*)bytes);
int mode = NSToolbarDisplayModeDefault;
if (flags & ToolbarLabelFlag) {
mode = flags & ToolbarIconFlag ? NSToolbarDisplayModeIconAndLabel
: NSToolbarDisplayModeLabelOnly;
} else if (flags & ToolbarIconFlag) {
mode = NSToolbarDisplayModeIconOnly;
}
int size = flags & ToolbarSizeRegularFlag ? NSToolbarSizeModeRegular
: NSToolbarSizeModeSmall;
[windowController showToolbar:enable size:size mode:mode];
} else if (CreateScrollbarMsgID == msgid) {
const void *bytes = [data bytes];
int32_t ident = *((int32_t*)bytes); bytes += sizeof(int32_t);
int type = *((int*)bytes);
[windowController createScrollbarWithIdentifier:ident type:type];
} else if (DestroyScrollbarMsgID == msgid) {
const void *bytes = [data bytes];
int32_t ident = *((int32_t*)bytes);
[windowController destroyScrollbarWithIdentifier:ident];
} else if (ShowScrollbarMsgID == msgid) {
const void *bytes = [data bytes];
int32_t ident = *((int32_t*)bytes); bytes += sizeof(int32_t);
int visible = *((int*)bytes);
[windowController showScrollbarWithIdentifier:ident state:visible];
} else if (SetScrollbarPositionMsgID == msgid) {
const void *bytes = [data bytes];
int32_t ident = *((int32_t*)bytes); bytes += sizeof(int32_t);
int pos = *((int*)bytes); bytes += sizeof(int);
int len = *((int*)bytes);
[windowController setScrollbarPosition:pos length:len
identifier:ident];
} else if (SetScrollbarThumbMsgID == msgid) {
const void *bytes = [data bytes];
int32_t ident = *((int32_t*)bytes); bytes += sizeof(int32_t);
float val = *((float*)bytes); bytes += sizeof(float);
float prop = *((float*)bytes);
[windowController setScrollbarThumbValue:val proportion:prop
identifier:ident];
} else if (SetFontMsgID == msgid) {
const void *bytes = [data bytes];
float size = *((float*)bytes); bytes += sizeof(float);
int len = *((int*)bytes); bytes += sizeof(int);
NSString *name = [[NSString alloc]
initWithBytes:(void*)bytes length:len
encoding:NSUTF8StringEncoding];
NSFont *font = [NSFont fontWithName:name size:size];
if (!font) {
// This should only happen if the system default font has changed
// name since MacVim was compiled in which case we fall back on
// using the user fixed width font.
font = [NSFont userFixedPitchFontOfSize:size];
}
[windowController setFont:font];
[name release];
} else if (SetWideFontMsgID == msgid) {
const void *bytes = [data bytes];
float size = *((float*)bytes); bytes += sizeof(float);
int len = *((int*)bytes); bytes += sizeof(int);
if (len > 0) {
NSString *name = [[NSString alloc]
initWithBytes:(void*)bytes length:len
encoding:NSUTF8StringEncoding];
NSFont *font = [NSFont fontWithName:name size:size];
[windowController setWideFont:font];
[name release];
} else {
[windowController setWideFont:nil];
}
} else if (SetDefaultColorsMsgID == msgid) {
const void *bytes = [data bytes];
unsigned bg = *((unsigned*)bytes); bytes += sizeof(unsigned);
unsigned fg = *((unsigned*)bytes);
NSColor *back = [NSColor colorWithArgbInt:bg];
NSColor *fore = [NSColor colorWithRgbInt:fg];
[windowController setDefaultColorsBackground:back foreground:fore];
} else if (ExecuteActionMsgID == msgid) {
const void *bytes = [data bytes];
int len = *((int*)bytes); bytes += sizeof(int);
NSString *actionName = [[NSString alloc]
initWithBytes:(void*)bytes length:len
encoding:NSUTF8StringEncoding];
SEL sel = NSSelectorFromString(actionName);
[NSApp sendAction:sel to:nil from:self];
[actionName release];
} else if (ShowPopupMenuMsgID == msgid) {
NSDictionary *attrs = [NSDictionary dictionaryWithData:data];
// The popup menu enters a modal loop so delay this call so that we
// don't block inside processInputQueue:.
[self performSelector:@selector(popupMenuWithAttributes:)
withObject:attrs
afterDelay:0];
} else if (SetMouseShapeMsgID == msgid) {
const void *bytes = [data bytes];
int shape = *((int*)bytes);
[windowController setMouseShape:shape];
} else if (AdjustLinespaceMsgID == msgid) {
const void *bytes = [data bytes];
int linespace = *((int*)bytes);
[windowController adjustLinespace:linespace];
} else if (ActivateMsgID == msgid) {
[NSApp activateIgnoringOtherApps:YES];
[[windowController window] makeKeyAndOrderFront:self];
} else if (SetServerNameMsgID == msgid) {
NSString *name = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
[self setServerName:name];
[name release];
} else if (EnterFullScreenMsgID == msgid) {
const void *bytes = [data bytes];
int fuoptions = *((int*)bytes); bytes += sizeof(int);
int bg = *((int*)bytes);
NSColor *back = [NSColor colorWithArgbInt:bg];
[windowController enterFullScreen:fuoptions backgroundColor:back];
} else if (LeaveFullScreenMsgID == msgid) {
[windowController leaveFullScreen];
} else if (SetBuffersModifiedMsgID == msgid) {
const void *bytes = [data bytes];
// state < 0 <-> some buffer modified
// state > 0 <-> current buffer modified
int state = *((int*)bytes);
// NOTE: The window controller tracks whether current buffer is
// modified or not (and greys out the proxy icon as well as putting a
// dot in the red "close button" if necessary). The Vim controller
// tracks whether any buffer has been modified (used to decide whether
// to show a warning or not when quitting).
//
// TODO: Make 'hasModifiedBuffer' part of the Vim state?
[windowController setBufferModified:(state > 0)];
hasModifiedBuffer = (state != 0);
} else if (SetPreEditPositionMsgID == msgid) {
const int *dim = (const int*)[data bytes];
[[[windowController vimView] textView] setPreEditRow:dim[0]
column:dim[1]];
} else if (EnableAntialiasMsgID == msgid) {
[[[windowController vimView] textView] setAntialias:YES];
} else if (DisableAntialiasMsgID == msgid) {
[[[windowController vimView] textView] setAntialias:NO];
} else if (SetVimStateMsgID == msgid) {
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
if (dict) {
[vimState release];
vimState = [dict retain];
}
} else if (CloseWindowMsgID == msgid) {
[self scheduleClose];
} else if (SetFullScreenColorMsgID == msgid) {
const int *bg = (const int*)[data bytes];
NSColor *color = [NSColor colorWithRgbInt:*bg];
[windowController setFullScreenBackgroundColor:color];
} else if (ShowFindReplaceDialogMsgID == msgid) {
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
if (dict) {
[[MMFindReplaceController sharedInstance]
showWithText:[dict objectForKey:@"text"]
flags:[[dict objectForKey:@"flags"] intValue]];
}
} else if (ActivateKeyScriptMsgID == msgid) {
[[[windowController vimView] textView] activateIm:YES];
} else if (DeactivateKeyScriptMsgID == msgid) {
[[[windowController vimView] textView] activateIm:NO];
} else if (EnableImControlMsgID == msgid) {
[[[windowController vimView] textView] setImControl:YES];
} else if (DisableImControlMsgID == msgid) {
[[[windowController vimView] textView] setImControl:NO];
} else if (BrowseForFileMsgID == msgid) {
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
if (dict)
[self handleBrowseForFile:dict];
} else if (ShowDialogMsgID == msgid) {
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
if (dict)
[self handleShowDialog:dict];
} else if (DeleteSignMsgID == msgid) {
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
if (dict)
[self handleDeleteSign:dict];
} else if (ZoomMsgID == msgid) {
const void *bytes = [data bytes];
int rows = *((int*)bytes); bytes += sizeof(int);
int cols = *((int*)bytes); bytes += sizeof(int);
int state = *((int*)bytes);
[windowController zoomWithRows:rows
columns:cols
state:state];
} else if (SetWindowPositionMsgID == msgid) {
const void *bytes = [data bytes];
int x = *((int*)bytes); bytes += sizeof(int);
int y = *((int*)bytes);
// NOTE: Vim measures Y-coordinates from top of screen.
NSRect frame = [[[windowController window] screen] frame];
y = NSMaxY(frame) - y;
[windowController setTopLeft:NSMakePoint(x,y)];
} else if (SetTooltipMsgID == msgid) {
id textView = [[windowController vimView] textView];
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
NSString *toolTip = dict ? [dict objectForKey:@"toolTip"] : nil;
if (toolTip && [toolTip length] > 0)
[textView setToolTipAtMousePoint:toolTip];
else
[textView setToolTipAtMousePoint:nil];
} else if (SetTooltipDelayMsgID == msgid) {
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
NSNumber *delay = dict ? [dict objectForKey:@"delay"] : nil;
if (delay)
[self setToolTipDelay:[delay floatValue]];
} else if (AddToMRUMsgID == msgid) {
NSDictionary *dict = [NSDictionary dictionaryWithData:data];
NSArray *filenames = dict ? [dict objectForKey:@"filenames"] : nil;
if (filenames)
[[NSDocumentController sharedDocumentController]
noteNewRecentFilePaths:filenames];
// IMPORTANT: When adding a new message, make sure to update
// isUnsafeMessage() if necessary!
} else {
ASLogWarn(@"Unknown message received (msgid=%d)", msgid);
}
}
- (void)savePanelDidEnd:(NSSavePanel *)panel code:(int)code
context:(void *)context
{
NSString *path = nil;
if (code == NSOKButton) {
NSURL *url = [panel URL];
if ([url isFileURL])
path = [url path];
}
ASLogDebug(@"Open/save panel path=%@", path);
// NOTE! This causes the sheet animation to run its course BEFORE the rest
// of this function is executed. If we do not wait for the sheet to
// disappear before continuing it can happen that the controller is
// released from under us (i.e. we'll crash and burn) because this
// animation is otherwise performed in the default run loop mode!
[panel orderOut:self];
// NOTE! setDialogReturn: is a synchronous call so set a proper timeout to
// avoid waiting forever for it to finish. We make this a synchronous call
// so that we can be fairly certain that Vim doesn't think the dialog box
// is still showing when MacVim has in fact already dismissed it.
NSConnection *conn = [backendProxy connectionForProxy];
NSTimeInterval oldTimeout = [conn requestTimeout];
[conn setRequestTimeout:MMSetDialogReturnTimeout];
@try {
[backendProxy setDialogReturn:path];
// Add file to the "Recent Files" menu (this ensures that files that
// are opened/saved from a :browse command are added to this menu).
if (path)
[[NSDocumentController sharedDocumentController]
noteNewRecentFilePath:path];
}
@catch (NSException *ex) {
ASLogDebug(@"Exception: pid=%d id=%d reason=%@", pid, identifier, ex);
}
@finally {
[conn setRequestTimeout:oldTimeout];
}
}
- (void)alertDidEnd:(MMAlert *)alert code:(int)code context:(void *)context
{
NSArray *ret = nil;
code = code - NSAlertFirstButtonReturn + 1;
if ([alert isKindOfClass:[MMAlert class]] && [alert textField]) {
ret = [NSArray arrayWithObjects:[NSNumber numberWithInt:code],
[[alert textField] stringValue], nil];
} else {
ret = [NSArray arrayWithObject:[NSNumber numberWithInt:code]];
}
ASLogDebug(@"Alert return=%@", ret);
// NOTE! This causes the sheet animation to run its course BEFORE the rest
// of this function is executed. If we do not wait for the sheet to
// disappear before continuing it can happen that the controller is
// released from under us (i.e. we'll crash and burn) because this
// animation is otherwise performed in the default run loop mode!
[[alert window] orderOut:self];
@try {
[backendProxy setDialogReturn:ret];
}
@catch (NSException *ex) {
ASLogDebug(@"setDialogReturn: failed: pid=%d id=%d reason=%@",
pid, identifier, ex);
}
}
- (NSMenuItem *)menuItemForDescriptor:(NSArray *)desc
{
if (!(desc && [desc count] > 0)) return nil;
NSString *rootName = [desc objectAtIndex:0];
NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
: [mainMenu itemArray];
NSMenuItem *item = nil;
int i, count = [rootItems count];
for (i = 0; i < count; ++i) {
item = [rootItems objectAtIndex:i];
if ([[item title] isEqual:rootName])
break;
}
if (i == count) return nil;
count = [desc count];
for (i = 1; i < count; ++i) {
item = [[item submenu] itemWithTitle:[desc objectAtIndex:i]];
if (!item) return nil;
}
return item;
}
- (NSMenu *)parentMenuForDescriptor:(NSArray *)desc
{
if (!(desc && [desc count] > 0)) return nil;
NSString *rootName = [desc objectAtIndex:0];
NSArray *rootItems = [rootName hasPrefix:@"PopUp"] ? popupMenuItems
: [mainMenu itemArray];
NSMenu *menu = nil;
int i, count = [rootItems count];
for (i = 0; i < count; ++i) {
NSMenuItem *item = [rootItems objectAtIndex:i];
if ([[item title] isEqual:rootName]) {
menu = [item submenu];
break;
}
}
if (!menu) return nil;
count = [desc count] - 1;
for (i = 1; i < count; ++i) {
NSMenuItem *item = [menu itemWithTitle:[desc objectAtIndex:i]];
menu = [item submenu];
if (!menu) return nil;
}
return menu;
}
- (NSMenu *)topLevelMenuForTitle:(NSString *)title
{
// Search only the top-level menus.
unsigned i, count = [popupMenuItems count];
for (i = 0; i < count; ++i) {
NSMenuItem *item = [popupMenuItems objectAtIndex:i];
if ([title isEqual:[item title]])
return [item submenu];
}
count = [mainMenu numberOfItems];
for (i = 0; i < count; ++i) {
NSMenuItem *item = [mainMenu itemAtIndex:i];
if ([title isEqual:[item title]])
return [item submenu];
}
return nil;
}
- (void)addMenuWithDescriptor:(NSArray *)desc atIndex:(int)idx
{
if (!(desc && [desc count] > 0 && idx >= 0)) return;
NSString *rootName = [desc objectAtIndex:0];
if ([rootName isEqual:@"ToolBar"]) {
// The toolbar only has one menu, we take this as a hint to create a
// toolbar, then we return.
if (!toolbar) {
// NOTE! Each toolbar must have a unique identifier, else each
// window will have the same toolbar.
NSString *ident = [NSString stringWithFormat:@"%d", identifier];
toolbar = [[NSToolbar alloc] initWithIdentifier:ident];
[toolbar setShowsBaselineSeparator:NO];
[toolbar setDelegate:self];
[toolbar setDisplayMode:NSToolbarDisplayModeIconOnly];
[toolbar setSizeMode:NSToolbarSizeModeSmall];
[windowController setToolbar:toolbar];
}
return;
}
// This is either a main menu item or a popup menu item.
NSString *title = [desc lastObject];
NSMenuItem *item = [[NSMenuItem alloc] init];
NSMenu *menu = [[NSMenu alloc] initWithTitle:title];
[item setTitle:title];
[item setSubmenu:menu];
NSMenu *parent = [self parentMenuForDescriptor:desc];
if (!parent && [rootName hasPrefix:@"PopUp"]) {
if ([popupMenuItems count] <= idx) {
[popupMenuItems addObject:item];
} else {
[popupMenuItems insertObject:item atIndex:idx];
}
} else {
// If descriptor has no parent and its not a popup (or toolbar) menu,
// then it must belong to main menu.
if (!parent) parent = mainMenu;
if ([parent numberOfItems] <= idx) {
[parent addItem:item];
} else {
[parent insertItem:item atIndex:idx];
}
}
[item release];
[menu release];
}
- (void)addMenuItemWithDescriptor:(NSArray *)desc
atIndex:(int)idx
tip:(NSString *)tip
icon:(NSString *)icon
keyEquivalent:(NSString *)keyEquivalent
modifierMask:(int)modifierMask
action:(NSString *)action
isAlternate:(BOOL)isAlternate
{
if (!(desc && [desc count] > 1 && idx >= 0)) return;
NSString *title = [desc lastObject];
NSString *rootName = [desc objectAtIndex:0];
if ([rootName isEqual:@"ToolBar"]) {
if (toolbar && [desc count] == 2)
[self addToolbarItemWithLabel:title tip:tip icon:icon atIndex:idx];
return;
}
NSMenu *parent = [self parentMenuForDescriptor:desc];
if (!parent) {
ASLogWarn(@"Menu item '%@' has no parent",
[desc componentsJoinedByString:@"->"]);
return;
}
NSMenuItem *item = nil;
if (0 == [title length]
|| ([title hasPrefix:@"-"] && [title hasSuffix:@"-"])) {
item = [NSMenuItem separatorItem];
[item setTitle:title];
} else {
item = [[[NSMenuItem alloc] init] autorelease];
[item setTitle:title];
// Note: It is possible to set the action to a message that "doesn't
// exist" without problems. We take advantage of this when adding
// "dummy items" e.g. when dealing with the "Recent Files" menu (in
// which case a recentFilesDummy: action is set, although it is never
// used).
if ([action length] > 0)
[item setAction:NSSelectorFromString(action)];
else
[item setAction:@selector(vimMenuItemAction:)];
if ([tip length] > 0) [item setToolTip:tip];
if ([keyEquivalent length] > 0) {
[item setKeyEquivalent:keyEquivalent];
[item setKeyEquivalentModifierMask:modifierMask];
}
[item setAlternate:isAlternate];
// The tag is used to indicate whether Vim thinks a menu item should be
// enabled or disabled. By default Vim thinks menu items are enabled.
[item setTag:1];
}
if ([parent numberOfItems] <= idx) {
[parent addItem:item];
} else {
[parent insertItem:item atIndex:idx];
}
}
- (void)removeMenuItemWithDescriptor:(NSArray *)desc
{
if (!(desc && [desc count] > 0)) return;
NSString *title = [desc lastObject];
NSString *rootName = [desc objectAtIndex:0];
if ([rootName isEqual:@"ToolBar"]) {
if (toolbar) {
// Only remove toolbar items, never actually remove the toolbar
// itself or strange things may happen.
if ([desc count] == 2) {
NSUInteger idx = [toolbar indexOfItemWithItemIdentifier:title];
if (idx != NSNotFound)
[toolbar removeItemAtIndex:idx];
}
}
return;
}
NSMenuItem *item = [self menuItemForDescriptor:desc];
if (!item) {
ASLogWarn(@"Failed to remove menu item, descriptor not found: %@",
[desc componentsJoinedByString:@"->"]);
return;
}
[item retain];
if ([item menu] == [NSApp mainMenu] || ![item menu]) {
// NOTE: To be on the safe side we try to remove the item from
// both arrays (it is ok to call removeObject: even if an array
// does not contain the object to remove).
[popupMenuItems removeObject:item];
}
if ([item menu])
[[item menu] removeItem:item];
[item release];
}
- (void)enableMenuItemWithDescriptor:(NSArray *)desc state:(BOOL)on
{
if (!(desc && [desc count] > 0)) return;
NSString *rootName = [desc objectAtIndex:0];
if ([rootName isEqual:@"ToolBar"]) {
if (toolbar && [desc count] == 2) {
NSString *title = [desc lastObject];
[[toolbar itemWithItemIdentifier:title] setEnabled:on];
}
} else {
// Use tag to set whether item is enabled or disabled instead of
// calling setEnabled:. This way the menus can autoenable themselves
// but at the same time Vim can set if a menu is enabled whenever it
// wants to.
[[self menuItemForDescriptor:desc] setTag:on];
}
}
- (void)addToolbarItemToDictionaryWithLabel:(NSString *)title
toolTip:(NSString *)tip
icon:(NSString *)icon
{
// If the item corresponds to a separator then do nothing, since it is
// already defined by Cocoa.
if (!title || [title isEqual:NSToolbarSeparatorItemIdentifier]
|| [title isEqual:NSToolbarSpaceItemIdentifier]
|| [title isEqual:NSToolbarFlexibleSpaceItemIdentifier])
return;
NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:title];
[item setLabel:title];
[item setToolTip:tip];
[item setAction:@selector(vimToolbarItemAction:)];
[item setAutovalidates:NO];
NSImage *img = [NSImage imageNamed:icon];
if (!img) {
img = [[[NSImage alloc] initByReferencingFile:icon] autorelease];
if (!(img && [img isValid]))
img = nil;
}
if (!img) {
ASLogNotice(@"Could not find image with name '%@' to use as toolbar"
" image for identifier '%@';"
" using default toolbar icon '%@' instead.",
icon, title, MMDefaultToolbarImageName);
img = [NSImage imageNamed:MMDefaultToolbarImageName];
}
[item setImage:img];
[toolbarItemDict setObject:item forKey:title];
[item release];
}
- (void)addToolbarItemWithLabel:(NSString *)label
tip:(NSString *)tip
icon:(NSString *)icon
atIndex:(int)idx
{
if (!toolbar) return;
// Check for separator items.
if (!label) {
label = NSToolbarSeparatorItemIdentifier;
} else if ([label length] >= 2 && [label hasPrefix:@"-"]
&& [label hasSuffix:@"-"]) {
// The label begins and ends with '-'; decided which kind of separator
// item it is by looking at the prefix.
if ([label hasPrefix:@"-space"]) {
label = NSToolbarSpaceItemIdentifier;
} else if ([label hasPrefix:@"-flexspace"]) {
label = NSToolbarFlexibleSpaceItemIdentifier;
} else {
label = NSToolbarSeparatorItemIdentifier;
}
}
[self addToolbarItemToDictionaryWithLabel:label toolTip:tip icon:icon];
int maxIdx = [[toolbar items] count];
if (maxIdx < idx) idx = maxIdx;
[toolbar insertItemWithItemIdentifier:label atIndex:idx];
}
- (void)popupMenuWithDescriptor:(NSArray *)desc
atRow:(NSNumber *)row
column:(NSNumber *)col
{
NSMenu *menu = [[self menuItemForDescriptor:desc] submenu];
if (!menu) return;
id textView = [[windowController vimView] textView];
NSPoint pt;
if (row && col) {
// TODO: Let textView convert (row,col) to NSPoint.
int r = [row intValue];
int c = [col intValue];
NSSize cellSize = [textView cellSize];
pt = NSMakePoint((c+1)*cellSize.width, (r+1)*cellSize.height);
pt = [textView convertPoint:pt toView:nil];
} else {
pt = [[windowController window] mouseLocationOutsideOfEventStream];
}
NSEvent *event = [NSEvent mouseEventWithType:NSRightMouseDown
location:pt
modifierFlags:0
timestamp:0
windowNumber:[[windowController window] windowNumber]
context:nil
eventNumber:0
clickCount:0
pressure:1.0];
[NSMenu popUpContextMenu:menu withEvent:event forView:textView];
}
- (void)popupMenuWithAttributes:(NSDictionary *)attrs
{
if (!attrs) return;
[self popupMenuWithDescriptor:[attrs objectForKey:@"descriptor"]
atRow:[attrs objectForKey:@"row"]
column:[attrs objectForKey:@"column"]];
}
- (void)connectionDidDie:(NSNotification *)notification
{
ASLogDebug(@"%@", notification);
[self scheduleClose];
}
- (void)scheduleClose
{
ASLogDebug(@"pid=%d id=%d", pid, identifier);
// NOTE! This message can arrive at pretty much anytime, e.g. while
// the run loop is the 'event tracking' mode. This means that Cocoa may
// well be in the middle of processing some message while this message is
// received. If we were to remove the vim controller straight away we may
// free objects that Cocoa is currently using (e.g. view objects). The
// following call ensures that the vim controller is not released until the
// run loop is back in the 'default' mode.
// Also, since the app may be multithreaded (e.g. as a result of showing
// the open panel) we have to ensure this call happens on the main thread,
// else there is a race condition that may lead to a crash.
[[MMAppController sharedInstance]
performSelectorOnMainThread:@selector(removeVimController:)
withObject:self
waitUntilDone:NO
modes:[NSArray arrayWithObject:
NSDefaultRunLoopMode]];
}
// NSSavePanel delegate
- (void)panel:(id)sender willExpand:(BOOL)expanding
{
// Show or hide the "show hidden files" button
if (expanding) {
[sender setAccessoryView:showHiddenFilesView()];
} else {
[sender setShowsHiddenFiles:NO];
[sender setAccessoryView:nil];
}
}
- (void)handleBrowseForFile:(NSDictionary *)attr
{
if (!isInitialized) return;
NSString *dir = [attr objectForKey:@"dir"];
BOOL saving = [[attr objectForKey:@"saving"] boolValue];
BOOL browsedir = [[attr objectForKey:@"browsedir"] boolValue];
if (!dir) {
// 'dir == nil' means: set dir to the pwd of the Vim process, or let
// open dialog decide (depending on the below user default).
BOOL trackPwd = [[NSUserDefaults standardUserDefaults]
boolForKey:MMDialogsTrackPwdKey];
if (trackPwd)
dir = [vimState objectForKey:@"pwd"];
}
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6)
// 10.6+ APIs uses URLs instead of paths
dir = [dir stringByExpandingTildeInPath];
NSURL *dirURL = dir ? [NSURL fileURLWithPath:dir isDirectory:YES] : nil;
#endif
if (saving) {
NSSavePanel *panel = [NSSavePanel savePanel];
// The delegate will be notified when the panel is expanded at which
// time we may hide/show the "show hidden files" button (this button is
// always visible for the open panel since it is always expanded).
[panel setDelegate:self];
if ([panel isExpanded])
[panel setAccessoryView:showHiddenFilesView()];
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6)
// NOTE: -[NSSavePanel beginSheetForDirectory::::::] is deprecated on
// 10.6 but -[NSSavePanel setDirectoryURL:] requires 10.6 so jump
// through the following hoops on 10.6+.
if (dirURL)
[panel setDirectoryURL:dirURL];
[panel beginSheetModalForWindow:[windowController window]
completionHandler:^(NSInteger result) {
[self savePanelDidEnd:panel code:result context:nil];
}];
#else
[panel beginSheetForDirectory:dir file:nil
modalForWindow:[windowController window]
modalDelegate:self
didEndSelector:@selector(savePanelDidEnd:code:context:)
contextInfo:NULL];
#endif
} else {
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowsMultipleSelection:NO];
[panel setAccessoryView:showHiddenFilesView()];
if (browsedir) {
[panel setCanChooseDirectories:YES];
[panel setCanChooseFiles:NO];
}
#if (MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_6)
// NOTE: -[NSOpenPanel beginSheetForDirectory:::::::] is deprecated on
// 10.6 but -[NSOpenPanel setDirectoryURL:] requires 10.6 so jump
// through the following hoops on 10.6+.
if (dirURL)
[panel setDirectoryURL:dirURL];
[panel beginSheetModalForWindow:[windowController window]
completionHandler:^(NSInteger result) {
[self savePanelDidEnd:panel code:result context:nil];
}];
#else
[panel beginSheetForDirectory:dir file:nil types:nil
modalForWindow:[windowController window]
modalDelegate:self
didEndSelector:@selector(savePanelDidEnd:code:context:)
contextInfo:NULL];
#endif
}
}
- (void)handleShowDialog:(NSDictionary *)attr
{
if (!isInitialized) return;
NSArray *buttonTitles = [attr objectForKey:@"buttonTitles"];
if (!(buttonTitles && [buttonTitles count])) return;
int style = [[attr objectForKey:@"alertStyle"] intValue];
NSString *message = [attr objectForKey:@"messageText"];
NSString *text = [attr objectForKey:@"informativeText"];
NSString *textFieldString = [attr objectForKey:@"textFieldString"];
MMAlert *alert = [[MMAlert alloc] init];
// NOTE! This has to be done before setting the informative text.
if (textFieldString)
[alert setTextFieldString:textFieldString];
[alert setAlertStyle:style];
if (message) {
[alert setMessageText:message];
} else {
// If no message text is specified 'Alert' is used, which we don't
// want, so set an empty string as message text.
[alert setMessageText:@""];
}
if (text) {
[alert setInformativeText:text];
} else if (textFieldString) {
// Make sure there is always room for the input text field.
[alert setInformativeText:@""];
}
unsigned i, count = [buttonTitles count];
for (i = 0; i < count; ++i) {
NSString *title = [buttonTitles objectAtIndex:i];
// NOTE: The title of the button may contain the character '&' to
// indicate that the following letter should be the key equivalent
// associated with the button. Extract this letter and lowercase it.
NSString *keyEquivalent = nil;
NSRange hotkeyRange = [title rangeOfString:@"&"];
if (NSNotFound != hotkeyRange.location) {
if ([title length] > NSMaxRange(hotkeyRange)) {
NSRange keyEquivRange = NSMakeRange(hotkeyRange.location+1, 1);
keyEquivalent = [[title substringWithRange:keyEquivRange]
lowercaseString];
}
NSMutableString *string = [NSMutableString stringWithString:title];
[string deleteCharactersInRange:hotkeyRange];
title = string;
}
[alert addButtonWithTitle:title];
// Set key equivalent for the button, but only if NSAlert hasn't
// already done so. (Check the documentation for
// - [NSAlert addButtonWithTitle:] to see what key equivalents are
// automatically assigned.)
NSButton *btn = [[alert buttons] lastObject];
if ([[btn keyEquivalent] length] == 0 && keyEquivalent) {
[btn setKeyEquivalent:keyEquivalent];
}
}
[alert beginSheetModalForWindow:[windowController window]
modalDelegate:self
didEndSelector:@selector(alertDidEnd:code:context:)
contextInfo:NULL];
[alert release];
}
- (void)handleDeleteSign:(NSDictionary *)attr
{
MMTextView *view = [[windowController vimView] textView];
[view deleteSign:[attr objectForKey:@"imgName"]];
}
- (void)setToolTipDelay:(NSTimeInterval)seconds
{
// HACK! NSToolTipManager is an AppKit private class.
static Class TTM = nil;
if (!TTM)
TTM = NSClassFromString(@"NSToolTipManager");
if (seconds < 0)
seconds = 0;
if (TTM) {
[[TTM sharedToolTipManager] setInitialToolTipDelay:seconds];
} else {
ASLogNotice(@"Failed to get NSToolTipManager");
}
}
@end // MMVimController (Private)
@implementation MMAlert
- (void)dealloc
{
ASLogDebug(@"");
[textField release]; textField = nil;
[super dealloc];
}
- (void)setTextFieldString:(NSString *)textFieldString
{
[textField release];
textField = [[NSTextField alloc] init];
[textField setStringValue:textFieldString];
}
- (NSTextField *)textField
{
return textField;
}
- (void)setInformativeText:(NSString *)text
{
if (textField) {
// HACK! Add some space for the text field.
[super setInformativeText:[text stringByAppendingString:@"\n\n\n"]];
} else {
[super setInformativeText:text];
}
}
- (void)beginSheetModalForWindow:(NSWindow *)window
modalDelegate:(id)delegate
didEndSelector:(SEL)didEndSelector
contextInfo:(void *)contextInfo
{
[super beginSheetModalForWindow:window
modalDelegate:delegate
didEndSelector:didEndSelector
contextInfo:contextInfo];
// HACK! Place the input text field at the bottom of the informative text
// (which has been made a bit larger by adding newline characters).
NSView *contentView = [[self window] contentView];
NSRect rect = [contentView frame];
rect.origin.y = rect.size.height;
NSArray *subviews = [contentView subviews];
unsigned i, count = [subviews count];
for (i = 0; i < count; ++i) {
NSView *view = [subviews objectAtIndex:i];
if ([view isKindOfClass:[NSTextField class]]
&& [view frame].origin.y < rect.origin.y) {
// NOTE: The informative text field is the lowest NSTextField in
// the alert dialog.
rect = [view frame];
}
}
rect.size.height = MMAlertTextFieldHeight;
[textField setFrame:rect];
[contentView addSubview:textField];
[textField becomeFirstResponder];
}
@end // MMAlert
static BOOL
isUnsafeMessage(int msgid)
{
// Messages that may release Cocoa objects must be added to this list. For
// example, UpdateTabBarMsgID may delete NSTabViewItem objects so it goes
// on this list.
static int unsafeMessages[] = { // REASON MESSAGE IS ON THIS LIST:
//OpenWindowMsgID, // Changes lots of state
UpdateTabBarMsgID, // May delete NSTabViewItem
RemoveMenuItemMsgID, // Deletes NSMenuItem
DestroyScrollbarMsgID, // Deletes NSScroller
ExecuteActionMsgID, // Impossible to predict
ShowPopupMenuMsgID, // Enters modal loop
ActivateMsgID, // ?
EnterFullScreenMsgID, // Modifies delegate of window controller
LeaveFullScreenMsgID, // Modifies delegate of window controller
CloseWindowMsgID, // See note below
BrowseForFileMsgID, // Enters modal loop
ShowDialogMsgID, // Enters modal loop
};
// NOTE about CloseWindowMsgID: If this arrives at the same time as say
// ExecuteActionMsgID, then the "execute" message will be lost due to it
// being queued and handled after the "close" message has caused the
// controller to cleanup...UNLESS we add CloseWindowMsgID to the list of
// unsafe messages. This is the _only_ reason it is on this list (since
// all that happens in response to it is that we schedule another message
// for later handling).
int i, count = sizeof(unsafeMessages)/sizeof(unsafeMessages[0]);
for (i = 0; i < count; ++i)
if (msgid == unsafeMessages[i])
return YES;
return NO;
}
Jump to Line
Something went wrong with that request. Please try again.