//
// $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
#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"
@implementation SPFieldEditorController
- (id)init
{
if ((self = [super initWithWindowNibName:@"FieldEditorSheet"])) {
// force the nib to be loaded
(void) [self window];
counter = 0;
maxTextLength = 0;
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;
if([prefs objectForKey:@"QuickLookTypes"]) {
for(id type in [prefs 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++;
}
}
}
return self;
}
- (void)dealloc
{
// 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 *)tableWindow
{
if ( ![prefs objectForKey:@"FieldEditorSheetFont"] )
if ( [prefs boolForKey:@"UseMonospacedFonts"] ) {
[editTextView setFont:[NSFont fontWithName:@"Monaco" size:[NSFont smallSystemFontSize]]];
} else {
[editTextView setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
// [prefs setObject:[NSArchiver archivedDataWithRootObject:[editTextView font]] forKey:@"FieldEditorSheetFont"];
}
else
[editTextView setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:@"FieldEditorSheetFont"]]];
[hexTextView setFont:[NSFont fontWithName:@"Monaco" 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:tableWindow 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:@"NullValue"]])
[editTextView setSelectedRange:NSMakeRange(0,[[editTextView string] length])];
// Set focus
if(image == nil)
[editSheet makeFirstResponder:editTextView];
else
[editSheet makeFirstResponder:editImage];
[stringValue release];
}
editSheetWillBeInitialized = NO;
[editSheetProgressBar stopAnimation:self];
// wait for editSheet
NSModalSession session = [NSApp beginModalSessionForWindow:editSheet];
int 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;
return ( editSheetReturnCode && isEditable ) ? [sheetEditData retain] : 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 select the part which won't be saved
// and suppress closing the sheet
if(sender == editSheetOkButton) {
if (maxTextLength > 0 && [[editTextView textStorage] length] > maxTextLength) {
[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 %d.", @"Text is too long. Maximum text length is set to %d."), 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([sheetEditData length] && [[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:(int)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:(int)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
{
id types = [prefs objectForKey:@"QuickLookTypes"];
if(types != nil && [types isKindOfClass:[NSArray class]] && [types count] > [sender tag] - 2) {
NSDictionary *type = [types objectAtIndex:[sender tag] - 2];
[self invokeQuickLookOfType:[type objectForKey:@"Extension"] treatAsText:([[type objectForKey:@"treatAsText"] intValue])];
}
}
- (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:@"%@", 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
*/
- (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)r replacementString:(NSString *)replacementString
{
if(textView == editTextView && maxTextLength > 0) {
int 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 %d.", @"Maximum text length is set to %d."), 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 %d. Inserted text was truncated.", @"Maximum text length is set to %d. Inserted text was truncated."), maxTextLength]];
else
[SPTooltip showWithObject:[NSString stringWithFormat:NSLocalizedString(@"Maximum text length is set to %d.", @"Maximum text length is set to %d."), 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