// // $Id$ // // SPFieldEditorController.m // sequel-pro // // Created by Hans-Jörg Bibiko on July 16, 2009 // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // More info at <http://code.google.com/p/sequel-pro/> #import "SPFieldEditorController.h" #import "SPStringAdditions.h" #import "SPArrayAdditions.h" #import "SPTextViewAdditions.h" #import "SPDataAdditions.h" #import "QLPreviewPanel.h" #import "SPDataCellFormatter.h" #import "RegexKitLite.h" #import "SPDataCellFormatter.h" #import "SPTooltip.h" #import "SPConstants.h" @implementation SPFieldEditorController - (id)init { if ((self = [super initWithWindowNibName:@"FieldEditorSheet"])) { // force the nib to be loaded (void) [self window]; counter = 0; maxTextLength = 0; stringValue = nil; prefs = [NSUserDefaults standardUserDefaults]; // Used for max text length recognition if last typed char is a non-space char editTextViewWasChanged = NO; // Allow the user to enter cmd+return to close the edit sheet in addition to fn+return [editSheetOkButton setKeyEquivalentModifierMask:NSCommandKeyMask]; allowUndo = NO; selectionChanged = NO; tmpDirPath = NSTemporaryDirectory(); tmpFileName = nil; NSMenu *menu = [editSheetQuickLookButton menu]; [menu setAutoenablesItems:NO]; NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Interpret data as:", @"Interpret data as:") action:NULL keyEquivalent:@""]; [item setTag:1]; [item setEnabled:NO]; [menu addItem:item]; [item release]; NSUInteger tag = 2; // Load default QL types NSMutableArray *qlTypesItems = [[NSMutableArray alloc] init]; NSError *readError = nil; NSString *convError = nil; NSPropertyListFormat format; NSData *defaultTypeData = [NSData dataWithContentsOfFile:[NSBundle pathForResource:@"EditorQuickLookTypes.plist" ofType:nil inDirectory:[[NSBundle mainBundle] bundlePath]] options:NSMappedRead error:&readError]; NSDictionary *defaultQLTypes = [NSPropertyListSerialization propertyListFromData:defaultTypeData mutabilityOption:NSPropertyListImmutable format:&format errorDescription:&convError]; if(defaultQLTypes == nil || readError != nil || convError != nil) NSLog(@"Error while reading 'EditorQuickLookTypes.plist':\n%@\n%@", [readError localizedDescription], convError); if(defaultQLTypes != nil && [defaultQLTypes objectForKey:@"QuickLookTypes"]) { for(id type in [defaultQLTypes objectForKey:@"QuickLookTypes"]) { NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithString:[type objectForKey:@"MenuLabel"]] action:NULL keyEquivalent:@""]; [item setTag:tag]; [item setAction:@selector(quickLookFormatButton:)]; [menu addItem:item]; [item release]; tag++; [qlTypesItems addObject:type]; } } // Load user-defined QL types if([prefs objectForKey:SPQuickLookTypes]) { for(id type in [prefs objectForKey:SPQuickLookTypes]) { NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithString:[type objectForKey:@"MenuLabel"]] action:NULL keyEquivalent:@""]; [item setTag:tag]; [item setAction:@selector(quickLookFormatButton:)]; [menu addItem:item]; [item release]; tag++; [qlTypesItems addObject:type]; } } qlTypes = [NSDictionary dictionaryWithObject:qlTypesItems forKey:SPQuickLookTypes]; [qlTypesItems release]; } return self; } - (void)dealloc { [NSObject cancelPreviousPerformRequestsWithTarget:self]; // On Mac OSX 10.6 QuickLook runs non-modal thus order out the panel // if still visible if([[NSClassFromString(@"QLPreviewPanel") sharedPreviewPanel] isVisible]) [[NSClassFromString(@"QLPreviewPanel") sharedPreviewPanel] orderOut:nil]; if ( esUndoManager ) [esUndoManager release]; if ( sheetEditData ) [sheetEditData release]; [super dealloc]; } - (void)setTextMaxLength:(unsigned long long)length { maxTextLength = length; } - (id)editWithObject:(id)data fieldName:(NSString*)fieldName usingEncoding:(NSStringEncoding)anEncoding isObjectBlob:(BOOL)isFieldBlob isEditable:(BOOL)isEditable withWindow:(NSWindow *)theWindow { // If required, use monospaced fonts if (![prefs objectForKey:SPFieldEditorSheetFont]) { [editTextView setFont:([prefs boolForKey:SPUseMonospacedFonts]) ? [NSFont fontWithName:SPDefaultMonospacedFontName size:[NSFont smallSystemFontSize]] : [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; } else { [editTextView setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:@"FieldEditorSheetFont"]]]; } [editTextView setContinuousSpellCheckingEnabled:[prefs boolForKey:SPBlobTextEditorSpellCheckingEnabled]]; [hexTextView setFont:[NSFont fontWithName:SPDefaultMonospacedFontName size:[NSFont smallSystemFontSize]]]; [editSheetFieldName setStringValue:[NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"Field", @"Field"), fieldName]]; // hide all views in editSheet [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [editImage setHidden:YES]; [editTextView setHidden:YES]; [editTextScrollView setHidden:YES]; if (!isEditable) { [editSheetOkButton setHidden:YES]; [editSheetCancelButton setHidden:YES]; [editSheetIsNotEditableCancelButton setHidden:NO]; [editSheetOpenButton setEnabled:NO]; } editSheetWillBeInitialized = YES; encoding = anEncoding; isBlob = isFieldBlob; sheetEditData = [data retain]; // hide all views in editSheet [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [editImage setHidden:YES]; [editTextView setHidden:YES]; [editTextScrollView setHidden:YES]; // Hide QuickLook button and text/iamge/hex control for text data [editSheetQuickLookButton setHidden:(!isBlob)]; [editSheetSegmentControl setHidden:(!isBlob)]; // Set window's min size since no segment and quicklook buttons are hidden if (isBlob) { [editSheet setFrameAutosaveName:@"SPFieldEditorBlobSheet"]; [editSheet setMinSize:NSMakeSize(560, 200)]; } else { [editSheet setFrameAutosaveName:@"SPFieldEditorTextSheet"]; [editSheet setMinSize:NSMakeSize(340, 150)]; } [editTextView setEditable:isEditable]; [editImage setEditable:isEditable]; [NSApp beginSheet:editSheet modalForWindow:theWindow modalDelegate:self didEndSelector:nil contextInfo:nil]; [editSheetProgressBar startAnimation:self]; NSImage *image = nil; if ( [sheetEditData isKindOfClass:[NSData class]] ) { image = [[[NSImage alloc] initWithData:sheetEditData] autorelease]; // Set hex view to "" - load on demand only [hexTextView setString:@""]; stringValue = [[NSString alloc] initWithData:sheetEditData encoding:encoding]; if (stringValue == nil) stringValue = [[NSString alloc] initWithData:sheetEditData encoding:NSASCIIStringEncoding]; [hexTextView setHidden:NO]; [hexTextScrollView setHidden:NO]; [editImage setHidden:YES]; [editTextView setHidden:YES]; [editTextScrollView setHidden:YES]; [editSheetSegmentControl setSelectedSegment:2]; } else { stringValue = [sheetEditData retain]; [hexTextView setString:@""]; [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [editImage setHidden:YES]; [editTextView setHidden:NO]; [editTextScrollView setHidden:NO]; [editSheetSegmentControl setSelectedSegment:0]; } if (image) { [editImage setImage:image]; [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [editImage setHidden:NO]; [editTextView setHidden:YES]; [editTextScrollView setHidden:YES]; [editSheetSegmentControl setSelectedSegment:1]; } else { [editImage setImage:nil]; } if (stringValue) { [editTextView setString:stringValue]; if(image == nil) { [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [editImage setHidden:YES]; [editTextView setHidden:NO]; [editTextScrollView setHidden:NO]; [editSheetSegmentControl setSelectedSegment:0]; } // Locate the caret in editTextView // (to select all takes a bit time for large data) [editTextView setSelectedRange:NSMakeRange(0,0)]; // If the string content is NULL select NULL for convenience if([stringValue isEqualToString:[prefs objectForKey:SPNullValue]]) [editTextView setSelectedRange:NSMakeRange(0,[[editTextView string] length])]; // Set focus if(image == nil) [editSheet makeFirstResponder:editTextView]; else [editSheet makeFirstResponder:editImage]; [stringValue release], stringValue = nil; } editSheetWillBeInitialized = NO; [editSheetProgressBar stopAnimation:self]; // wait for editSheet NSModalSession session = [NSApp beginModalSessionForWindow:editSheet]; NSInteger cycleCounter = 0; BOOL doGroupDueToChars = NO; for (;;) { // Break the run loop if editSheet was closed if ([NSApp runModalSession:session] != NSRunContinuesResponse || ![editSheet isVisible]) break; // Execute code on DefaultRunLoop (like displaying a tooltip) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; // Allow undo grouping if user typed a ' ' (for word level undo) // or a RETURN but not for each char due to writing speed if([[NSApp currentEvent] type] == NSKeyDown && ( [[[NSApp currentEvent] charactersIgnoringModifiers] isEqualToString:@" "] || [[NSApp currentEvent] keyCode] == 36 || [[NSApp currentEvent] modifierFlags] & (NSCommandKeyMask|NSControlKeyMask|NSAlternateKeyMask) )) { doGroupDueToChars=YES; } // If conditions match create an undo group if( ( wasCutPaste || allowUndo || doGroupDueToChars ) && ![esUndoManager isUndoing] && ![esUndoManager isRedoing] ) { allowUndo = NO; wasCutPaste = NO; doGroupDueToChars = NO; selectionChanged = NO; cycleCounter = 0; while([esUndoManager groupingLevel] > 0) { [esUndoManager endUndoGrouping]; cycleCounter++; } while([esUndoManager groupingLevel] < cycleCounter) [esUndoManager beginUndoGrouping]; cycleCounter = 0; } } [NSApp endModalSession:session]; [editSheet orderOut:nil]; [NSApp endSheet:editSheet]; // For safety reasons inform QuickLook to quit quickLookCloseMarker = 1; // Remember spell cheecker status [prefs setBool:[editTextView isContinuousSpellCheckingEnabled] forKey:SPBlobTextEditorSpellCheckingEnabled]; return ( editSheetReturnCode && isEditable ) ? sheetEditData : nil; } /* * Establish and return an UndoManager for editTextView */ - (NSUndoManager*)undoManagerForTextView:(NSTextView*)aTextView { if (!esUndoManager) esUndoManager = [[NSUndoManager alloc] init]; return esUndoManager; } - (void)setWasCutPaste { wasCutPaste = YES; } - (IBAction)closeEditSheet:(id)sender { editSheetReturnCode = 0; // Validate the sheet data before saving them. // - for max text length (except for NULL value string) select the part which won't be saved // and suppress closing the sheet if(sender == editSheetOkButton) { if (maxTextLength > 0 && [[editTextView textStorage] length] > maxTextLength && ![[[editTextView textStorage] string] isEqualToString:[prefs objectForKey:SPNullValue]]) { [editTextView setSelectedRange:NSMakeRange(maxTextLength, [[editTextView textStorage] length] - maxTextLength)]; [editTextView scrollRangeToVisible:NSMakeRange([editTextView selectedRange].location,0)]; [SPTooltip showWithObject:[NSString stringWithFormat:NSLocalizedString(@"Text is too long. Maximum text length is set to %llu.", @"Text is too long. Maximum text length is set to %llu."), maxTextLength]]; return; } [NSApp stopModal]; editSheetReturnCode = 1; } // Delete all QuickLook temp files if it was invoked if(tmpFileName != nil) { NSArray *dirContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:tmpDirPath error:nil]; for (NSString *file in dirContents) { if ([file hasPrefix:@"SequelProQuickLook"]) { if(![[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@/%@", tmpDirPath, file] error:NULL]) { NSLog(@"QL: Couldn't delete temporary file '%@/%@'.", tmpDirPath, file); } } } } [NSApp abortModal]; } - (IBAction)openEditSheet:(id)sender { [[NSOpenPanel openPanel] beginSheetForDirectory:nil file:@"" modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo:NULL]; } /* * Segement controller for text/image/hex buttons in editSheet */ - (IBAction)segmentControllerChanged:(id)sender { switch([sender selectedSegment]){ case 0: // text [editTextView setHidden:NO]; [editTextScrollView setHidden:NO]; [editImage setHidden:YES]; [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [[self window] makeFirstResponder:editTextView]; break; case 1: // image [editTextView setHidden:YES]; [editTextScrollView setHidden:YES]; [editImage setHidden:NO]; [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [[self window] makeFirstResponder:editImage]; break; case 2: // hex - load on demand [[self window] makeFirstResponder:hexTextView]; if([[hexTextView string] isEqualToString:@""]) { [editSheetProgressBar startAnimation:self]; if([sheetEditData isKindOfClass:[NSData class]]) { [hexTextView setString:[sheetEditData dataToFormattedHexString]]; } else { [hexTextView setString:[[sheetEditData dataUsingEncoding:encoding allowLossyConversion:YES] dataToFormattedHexString]]; } [editSheetProgressBar stopAnimation:self]; } [editTextView setHidden:YES]; [editTextScrollView setHidden:YES]; [editImage setHidden:YES]; [hexTextView setHidden:NO]; [hexTextScrollView setHidden:NO]; break; } } /* * Saves a file containing the content of the editSheet */ - (IBAction)saveEditSheet:(id)sender { [[NSSavePanel savePanel] beginSheetForDirectory:nil file:@"" modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:) contextInfo:NULL]; } /** * Save panel didEndSelector. Writes the current content to disk. */ - (void)savePanelDidEnd:(NSSavePanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { if (returnCode == NSOKButton) { [editSheetProgressBar startAnimation:self]; NSString *fileName = [panel filename]; // Write binary field types directly to the file if ( [sheetEditData isKindOfClass:[NSData class]] ) { [sheetEditData writeToFile:fileName atomically:YES]; // Write other field types' representations to the file via the current encoding } else { [[sheetEditData description] writeToFile:fileName atomically:YES encoding:encoding error:NULL]; } [editSheetProgressBar stopAnimation:self]; } } /** * Open panel didEndSelector. Opens the selected file. */ - (void)openPanelDidEnd:(NSOpenPanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { if (returnCode == NSOKButton) { NSString *fileName = [panel filename]; NSString *contents = nil; editSheetWillBeInitialized = YES; [editSheetProgressBar startAnimation:self]; // free old data if ( sheetEditData != nil ) { [sheetEditData release]; } // load new data/images sheetEditData = [[NSData alloc] initWithContentsOfFile:fileName]; NSImage *image = [[NSImage alloc] initWithData:sheetEditData]; contents = [[NSString alloc] initWithData:sheetEditData encoding:encoding]; if (contents == nil) contents = [[NSString alloc] initWithData:sheetEditData encoding:NSASCIIStringEncoding]; // set the image preview, string contents and hex representation [editImage setImage:image]; if(contents) [editTextView setString:contents]; else [editTextView setString:@""]; // Load hex data only if user has already displayed them if(![[hexTextView string] isEqualToString:@""]) [hexTextView setString:[sheetEditData dataToFormattedHexString]]; // If the image cell now contains a valid image, select the image view if (image) { [editSheetSegmentControl setSelectedSegment:1]; [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [editImage setHidden:NO]; [editTextView setHidden:YES]; [editTextScrollView setHidden:YES]; // Otherwise deselect the image view } else { [editSheetSegmentControl setSelectedSegment:0]; [hexTextView setHidden:YES]; [hexTextScrollView setHidden:YES]; [editImage setHidden:YES]; [editTextView setHidden:NO]; [editTextScrollView setHidden:NO]; } [image release]; if(contents) [contents release]; [editSheetProgressBar stopAnimation:self]; editSheetWillBeInitialized = NO; } } #pragma mark - #pragma mark QuickLook - (IBAction)quickLookFormatButton:(id)sender { if(qlTypes != nil && [[qlTypes objectForKey:@"QuickLookTypes"] count] > [sender tag] - 2) { NSDictionary *type = [[qlTypes objectForKey:@"QuickLookTypes"] objectAtIndex:[sender tag] - 2]; [self invokeQuickLookOfType:[type objectForKey:@"Extension"] treatAsText:([[type objectForKey:@"treatAsText"] integerValue])]; } } - (void)createTemporaryQuickLookFileOfType:(NSString *)type treatAsText:(BOOL)isText { // Create a temporary file name to store the data as file // since QuickLook only works on files. // Alternate the file name to suppress caching by using counter%2. tmpFileName = [NSString stringWithFormat:@"%@SequelProQuickLook%d.%@", tmpDirPath, counter%2, type]; // if data are binary if ( [sheetEditData isKindOfClass:[NSData class]] && !isText) { [sheetEditData writeToFile:tmpFileName atomically:YES]; // write other field types' representations to the file via the current encoding } else { // if "html" type try to set the HTML charset - not yet completed if([type isEqualToString:@"html"]) { NSString *enc; switch(encoding) { case NSASCIIStringEncoding: enc = @"US-ASCII";break; case NSUTF8StringEncoding: enc = @"UTF-8";break; case NSISOLatin1StringEncoding: enc = @"ISO-8859-1";break; default: enc = @"US-ASCII"; } [[NSString stringWithFormat:@"<META HTTP-EQUIV='Content-Type' CONTENT='text/html; charset=%@'>%@", enc, [editTextView string]] writeToFile:tmpFileName atomically:YES encoding:encoding error:NULL]; } else { [[sheetEditData description] writeToFile:tmpFileName atomically:YES encoding:encoding error:NULL]; } } } /* * Opens QuickLook for current data if QuickLook is available */ - (void)invokeQuickLookOfType:(NSString *)type treatAsText:(BOOL)isText { // Load QL via private framework (SDK 10.5) if([[NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/QuickLookUI.framework"] load]) { [editSheetProgressBar startAnimation:self]; [self createTemporaryQuickLookFileOfType:type treatAsText:isText]; counter++; // Init QuickLook id ql = [NSClassFromString(@"QLPreviewPanel") sharedPreviewPanel]; [[ql delegate] setDelegate:self]; [ql setURLs:[NSArray arrayWithObject: [NSURL fileURLWithPath:tmpFileName]] currentIndex:0 preservingDisplayState:YES]; // TODO: No interaction with iChat and iPhoto due to .scriptSuite warning: // unknown image format [ql setShowsAddToiPhotoButton:NO]; [ql setShowsiChatTheaterButton:NO]; // Since we are inside of editSheet we have to avoid full-screen zooming // otherwise QuickLook hangs [ql setShowsFullscreenButton:NO]; [ql setEnableDragNDrop:NO]; // Order out QuickLook with animation effect according to self:previewPanel:frameForURL: [ql makeKeyAndOrderFrontWithEffect:2]; // 1 = fade in // quickLookCloseMarker == 1 break the modal session quickLookCloseMarker = 0; [editSheetProgressBar stopAnimation:self]; // Run QuickLook in its own modal seesion for event handling NSModalSession session = [NSApp beginModalSessionForWindow:ql]; for (;;) { // Conditions for closing QuickLook if ([NSApp runModalSession:session] != NSRunContinuesResponse || quickLookCloseMarker == 1 || ![ql isVisible]) break; [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } [NSApp endModalSession:session]; // set ql's delegate to nil for dealloc [[ql windowController] setDelegate:nil]; } // Load QL via framework (SDK 10.5 but SP runs on 10.6) // TODO: This is an hack in order to be able to support QuickLook on Mac OS X 10.5 and 10.6 // as long as SP will be compiled against SDK 10.5. // If SP will be compiled against SDK 10.6 we can use the standard way by using // the QuickLookUI which is part of the Quartz.framework. See Developer example "QuickLookDownloader" // file:///Developer/Documentation/DocSets/com.apple.adc.documentation.AppleSnowLeopard.CoreReference.docset/Contents/Resources/Documents/samplecode/QuickLookDownloader/index.html#//apple_ref/doc/uid/DTS40009082 else if([[NSBundle bundleWithPath:@"/System/Library/Frameworks/Quartz.framework/Frameworks/QuickLookUI.framework"] load]) { [editSheetProgressBar startAnimation:self]; [self createTemporaryQuickLookFileOfType:type treatAsText:isText]; counter++; // TODO: If QL is visible reload it - but how? // Up to now QL will close and the user has to redo it. if([[NSClassFromString(@"QLPreviewPanel") sharedPreviewPanel] isVisible]) { [[NSClassFromString(@"QLPreviewPanel") sharedPreviewPanel] orderOut:nil]; } [[NSClassFromString(@"QLPreviewPanel") sharedPreviewPanel] makeKeyAndOrderFront:nil]; [editSheetProgressBar stopAnimation:self]; } else { [SPTooltip showWithObject:[NSString stringWithFormat:@"QuickLook is not available on that platform."]]; } } // QuickLook delegates for SDK 10.6 - (void)beginPreviewPanelControl:(id)panel { // This document is now responsible of the preview panel [panel setDelegate:self]; [panel setDataSource:self]; // Due to the unknown image format disable image sharing [panel setShowsAddToiPhotoButton:NO]; } // QuickLook delegates for SDK 10.6 - (void)endPreviewPanelControl:(id)panel { // This document loses its responsisibility on the preview panel // Until the next call to -beginPreviewPanelControl: it must not // change the panel's delegate, data source or refresh it. } // QuickLook delegates for SDK 10.6 - (BOOL)acceptsPreviewPanelControl:(id)panel; { return YES; } // QuickLook delegates for SDK 10.6 // - (BOOL)previewPanel:(QLPreviewPanel *)panel handleEvent:(NSEvent *)event // { // } // QuickLook delegates for SDK 10.6 - (NSInteger)numberOfPreviewItemsInPreviewPanel:(id)panel { return 1; } // QuickLook delegates for SDK 10.6 - (id)previewPanel:(id)panel previewItemAtIndex:(NSInteger)index { return [NSURL fileURLWithPath:tmpFileName]; } // QuickLook delegates for SDK 10.5 // It should return the frame for the item represented by the URL // If an empty frame is returned then the panel will fade in/out instead - (NSRect)previewPanel:(NSPanel*)panel frameForURL:(NSURL*)URL { // Close modal session defined in invokeQuickLookOfType: // if user closes the QuickLook view quickLookCloseMarker = 1; // Return the App's middle point NSRect mwf = [[NSApp mainWindow] frame]; return NSMakeRect( mwf.origin.x+mwf.size.width/2, mwf.origin.y+mwf.size.height/2, 5, 5); } // QuickLook delegates for SDK 10.6 // It should return the frame for the item represented by the URL // If an empty frame is returned then the panel will fade in/out instead - (NSRect)previewPanel:(id)panel sourceFrameOnScreenForPreviewItem:(id)item { // Return the App's middle point NSRect mwf = [[NSApp mainWindow] frame]; return NSMakeRect( mwf.origin.x+mwf.size.width/2, mwf.origin.y+mwf.size.height/2, 5, 5); } // QuickLook delegates for SDK 10.6 // - (id)previewPanel:(id)panel transitionImageForPreviewItem:(id)item contentRect:(NSRect *)contentRect // { // return [NSImage imageNamed:@"database"]; // } -(void)processPasteImageData { editSheetWillBeInitialized = YES; NSImage *image = nil; image = [[[NSImage alloc] initWithPasteboard:[NSPasteboard generalPasteboard]] autorelease]; if (image) { if (nil != sheetEditData) [sheetEditData release]; [editImage setImage:image]; if( sheetEditData ) [sheetEditData release]; sheetEditData = [[NSData alloc] initWithData:[image TIFFRepresentationUsingCompression:NSTIFFCompressionLZW factor:1]]; NSString *contents = [[NSString alloc] initWithData:sheetEditData encoding:encoding]; if (contents == nil) contents = [[NSString alloc] initWithData:sheetEditData encoding:NSASCIIStringEncoding]; // Set the string contents and hex representation if(contents) [editTextView setString:contents]; if(![[hexTextView string] isEqualToString:@""]) [hexTextView setString:[sheetEditData dataToFormattedHexString]]; [contents release]; } editSheetWillBeInitialized = NO; } /* * Invoked when the imageView in the connection sheet has the contents deleted * or a file dragged and dropped onto it. */ - (void)processUpdatedImageData:(NSData *)data { editSheetWillBeInitialized = YES; if (nil != sheetEditData) [sheetEditData release]; // If the image was not processed, set a blank string as the contents of the edit and hex views. if ( data == nil ) { sheetEditData = [[NSData alloc] init]; [editTextView setString:@""]; [hexTextView setString:@""]; editSheetWillBeInitialized = NO; return; } // Process the provided image sheetEditData = [[NSData alloc] initWithData:data]; NSString *contents = [[NSString alloc] initWithData:data encoding:encoding]; if (contents == nil) contents = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; // Set the string contents and hex representation if(contents) [editTextView setString:contents]; if(![[hexTextView string] isEqualToString:@""]) [hexTextView setString:[sheetEditData dataToFormattedHexString]]; [contents release]; editSheetWillBeInitialized = NO; } - (IBAction)dropImage:(id)sender { // If the image was deleted, set a blank string as the contents of the edit and hex views. // The actual dropped image processing is handled by processUpdatedImageData:. if ( [editImage image] == nil ) { if (nil != sheetEditData) [sheetEditData release]; sheetEditData = [[NSData alloc] init]; [editTextView setString:@""]; [hexTextView setString:@""]; return; } } #pragma mark - #pragma mark Delegates /* Validate editTextView for max text length except for NULL value string */ - (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)r replacementString:(NSString *)replacementString { if(textView == editTextView && maxTextLength > 0 && ![ [[[editTextView textStorage] string] stringByAppendingString:replacementString] isEqualToString:[prefs objectForKey:SPNullValue]]) { NSInteger newLength; // Auxilary to ensure that eg textViewDidChangeSelection: // saves a non-space char + base char if that combination // occurs at the end of a sequence of typing before saving // (OK button). editTextViewWasChanged = ([replacementString length] == 1); // Pure attribute changes are ok. if (!replacementString) return YES; // The exact change isn't known. Disallow the change to be safe. if (r.location==NSNotFound) return NO; // Length checking while using the Input Manager (eg for Japanese) if ([textView hasMarkedText] && maxTextLength > 0 && r.location < maxTextLength) // User tries to insert a new char but max text length was already reached - return NO if( !r.length && [[textView textStorage] length] >= maxTextLength ) { [SPTooltip showWithObject:[NSString stringWithFormat:NSLocalizedString(@"Maximum text length is set to %llu.", @"Maximum text length is set to %llu."), maxTextLength]]; [textView unmarkText]; return NO; } // otherwise allow it if insertion point is valid for eg // a VARCHAR(3) field filled with two Chinese chars and one inserts the // third char by typing its pronounciation "wo" - 2 Chinese chars plus "wo" would give // 4 which is larger than max length. // TODO this doesn't solve the problem of inserting more than one char. For now // that part which won't be saved will be hilited if user pressed the OK button. else if (r.location < maxTextLength) return YES; // Calculate the length of the text after the change. newLength=[[textView textStorage] length]+[replacementString length]-r.length; // If it's too long, disallow the change but try // to insert a text chunk partially to maxTextLength. if (newLength>maxTextLength) { if(maxTextLength-[[textView textStorage] length]+[textView selectedRange].length <= [replacementString length]) { if(maxTextLength-[[textView textStorage] length]+[textView selectedRange].length) [SPTooltip showWithObject:[NSString stringWithFormat:NSLocalizedString(@"Maximum text length is set to %llu. Inserted text was truncated.", @"Maximum text length is set to %llu. Inserted text was truncated."), maxTextLength]]; else [SPTooltip showWithObject:[NSString stringWithFormat:NSLocalizedString(@"Maximum text length is set to %llu.", @"Maximum text length is set to %llu."), maxTextLength]]; [textView insertText:[replacementString substringToIndex:maxTextLength-[[textView textStorage] length]+[textView selectedRange].length]]; } return NO; } // Otherwise, allow it. return YES; } return YES; } /* invoked when the user changes the string in the editSheet */ - (void)textViewDidChangeSelection:(NSNotification *)notification { // Do nothing if user really didn't changed text (e.g. for font size changing return) if(!editTextViewWasChanged && (editSheetWillBeInitialized || (([[[notification object] textStorage] editedRange].length == 0) && ([[[notification object] textStorage] changeInLength] == 0)))) { // Inform the undo-grouping about the caret movement selectionChanged = YES; return; } // clear the image and hex (since i doubt someone can "type" a gif) [editImage setImage:nil]; [hexTextView setString:@""]; // free old data if ( sheetEditData != nil ) { [sheetEditData release]; } // set edit data to text sheetEditData = [[NSString stringWithString:[editTextView string]] retain]; } #pragma - #pragma TextView delegate methods /** * Traps enter and return key and closes editSheet instead of inserting a linebreak when user hits return. */ - (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector { if ( aTextView == editTextView ) { if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] && [[[NSApp currentEvent] characters] isEqualToString:@"\003"] ) { [self closeEditSheet:editSheetOkButton]; return YES; } } return NO; } - (void)setAllowedUndo { allowUndo = YES; } - (void)textDidChange:(NSNotification *)aNotification { // Allow undo grouping only if the text buffer was really changed. Inform // the run loop delayed for larger undo groups. [self performSelector:@selector(setAllowedUndo) withObject:nil afterDelay:0.2]; } @end