//
// $Id$
//
// SPAppController.m
// sequel-pro
//
// Created by lorenz textor (lorenz@textor.ch) on Wed May 01 2002.
// Copyright (c) 2002-2003 Lorenz Textor. All rights reserved.
//
// 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 "SPKeychain.h"
#import "SPAppController.h"
#import "SPDatabaseDocument.h"
#import "SPPreferenceController.h"
#import "SPAboutController.h"
#import "TableDump.h"
#import "SPEncodingPopupAccessory.h"
#import "SPConstants.h"
#import "SPWindowController.h"
#import
@implementation SPAppController
/**
* Initialise the application's main controller, setting itself as the app delegate.
*/
- (id)init
{
if ((self = [super init])) {
_sessionURL = nil;
_spfSessionDocData = [[NSMutableDictionary alloc] init];
[NSApp setDelegate:self];
}
return self;
}
/**
* Called even before init so we can register our preference defaults
*/
+ (void)initialize
{
// Register application defaults
[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"PreferenceDefaults" ofType:@"plist"]]];
}
/**
* Initialisation stuff upon nib awakening
*/
- (void)awakeFromNib
{
// Set up the prefs controller
prefsController = [[SPPreferenceController alloc] init];
aboutController = nil;
// Set Sparkle delegate
[[SUUpdater sharedUpdater] setDelegate:self];
// Register SPAppController as services provider
[NSApp setServicesProvider:self];
// Register SPAppController for AppleScript events
[[NSScriptExecutionContext sharedScriptExecutionContext] setTopLevelObject:self];
isNewFavorite = NO;
}
/**
* Initialisation stuff after launch is complete
*/
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
// Set ourselves as the crash reporter delegate
[[FRFeedbackReporter sharedReporter] setDelegate:self];
// Report any crashes
[[FRFeedbackReporter sharedReporter] reportIfCrash];
}
/**
* Menu item validation.
*/
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
if ([menuItem action] == @selector(openCurrentConnectionInNewWindow:))
{
[menuItem setTitle:NSLocalizedString(@"Open in New Window", @"menu item open in new window")];
return NO;
}
return YES;
}
#pragma mark -
#pragma mark Open methods
/**
* NSOpenPanel delegate to control encoding popup and allowMultipleSelection
*/
- (void)panelSelectionDidChange:(id)sender
{
if ([sender isKindOfClass:[NSOpenPanel class]]) {
if([[[[sender filename] pathExtension] lowercaseString] isEqualToString:SPFileExtensionSQL]) {
[encodingPopUp setEnabled:YES];
} else {
[encodingPopUp setEnabled:NO];
}
}
}
/**
* NSOpenPanel for selecting sql or spf file
*/
- (IBAction)openConnectionSheet:(id)sender
{
// Avoid opening more than NSOpenPanel
if(encodingPopUp){
NSBeep();
return;
}
NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setDelegate:self];
[panel setCanSelectHiddenExtension:YES];
[panel setCanChooseDirectories:NO];
[panel setAllowsMultipleSelection:NO];
[panel setResolvesAliases:YES];
// If no lastSqlFileEncoding in prefs set it to UTF-8
if(![[NSUserDefaults standardUserDefaults] integerForKey:SPLastSQLFileEncoding]) {
[[NSUserDefaults standardUserDefaults] setInteger:4 forKey:SPLastSQLFileEncoding];
[[NSUserDefaults standardUserDefaults] synchronize];
}
[panel setAccessoryView:[SPEncodingPopupAccessory encodingAccessory:[[NSUserDefaults standardUserDefaults] integerForKey:SPLastSQLFileEncoding]
includeDefaultEntry:NO encodingPopUp:&encodingPopUp]];
// it will enabled if user selects a *.sql file
[encodingPopUp setEnabled:NO];
// Check if at least one document exists, if so show a sheet
if ([self frontDocumentWindow]) {
[panel beginSheetForDirectory:nil
file:@""
types:[NSArray arrayWithObjects:SPFileExtensionDefault, SPFileExtensionSQL, nil]
modalForWindow:[self frontDocumentWindow]
modalDelegate:self
didEndSelector:@selector(openConnectionPanelDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
}
else {
NSInteger returnCode = [panel runModalForDirectory:nil file:nil types:[NSArray arrayWithObjects:SPFileExtensionDefault, SPFileExtensionSQL, nil]];
if (returnCode) [self application:nil openFiles:[panel filenames]];
encodingPopUp = nil;
}
}
/**
* Invoked when the open connection panel is dismissed.
*/
- (void)openConnectionPanelDidEnd:(NSOpenPanel *)panel returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
if (returnCode) {
[panel orderOut:self];
[self application:nil openFiles:[panel filenames]];
}
encodingPopUp = nil;
}
/**
* Called if user drag and drops files on Sequel Pro's dock item or double-clicked
* at files *.spf or *.sql
*/
- (void)application:(NSApplication *)app openFiles:(NSArray *)filenames
{
for (NSString *filename in filenames)
{
// Opens a sql file and insert its content into the Custom Query editor
if([[[filename pathExtension] lowercaseString] isEqualToString:SPFileExtensionSQL]) {
// Check size and NSFileType
NSDictionary *attr = [[NSFileManager defaultManager] fileAttributesAtPath:filename traverseLink:YES];
if(attr)
{
NSNumber *filesize = [attr objectForKey:NSFileSize];
NSString *filetype = [attr objectForKey:NSFileType];
if(filetype == NSFileTypeRegular && filesize)
{
// Ask for confirmation if file content is larger than 1MB
if([filesize unsignedLongValue] > 1000000)
{
NSAlert *alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")];
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")];
// Show 'Import' button only if there's a connection available
if ([self frontDocument])
[alert addButtonWithTitle:NSLocalizedString(@"Import", @"import button")];
[alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Do you really want to load a SQL file with %@ of data into the Query Editor?", @"message of panel asking for confirmation for loading large text into the query editor"),
[NSString stringForByteSize:[filesize longLongValue]]]];
[alert setHelpAnchor:filename];
[alert setMessageText:NSLocalizedString(@"Warning",@"warning")];
[alert setAlertStyle:NSWarningAlertStyle];
NSUInteger returnCode = [alert runModal];
[alert release];
if(returnCode == NSAlertSecondButtonReturn) return; // Cancel
else if(returnCode == NSAlertThirdButtonReturn) { // Import
// begin import process
[[[self frontDocument] valueForKeyPath:@"tableDumpInstance"] startSQLImportProcessWithFile:filename];
return;
}
}
}
}
// Attempt to open the file into a string.
NSString *sqlString = nil;
// If the user came from an openPanel use the chosen encoding
if (encodingPopUp) {
NSError *error = nil;
sqlString = [NSString stringWithContentsOfFile:filename encoding:[[encodingPopUp selectedItem] tag] error:&error];
if(error != nil) {
NSAlert *errorAlert = [NSAlert alertWithError:error];
[errorAlert runModal];
return;
}
// Otherwise, read while attempting to autodetect the encoding
} else {
sqlString = [self contentOfFile:filename];
}
// if encodingPopUp is defined the filename comes from an openPanel and
// the encodingPopUp contains the chosen encoding; otherwise autodetect encoding
if(encodingPopUp)
[[NSUserDefaults standardUserDefaults] setInteger:[[encodingPopUp selectedItem] tag] forKey:SPLastSQLFileEncoding];
// Check if at least one document exists. If not, open one.
if (![self frontDocument]) {
[self newWindow:self];
[[self frontDocument] initQueryEditorWithString:sqlString];
} else {
// Pass query to the Query editor of the current document
[[self frontDocument] doPerformLoadQueryService:[self contentOfFile:filename]];
}
break; // open only the first SQL file
}
else if([[[filename pathExtension] lowercaseString] isEqualToString:SPFileExtensionDefault]) {
SPWindowController *frontController = nil;
for (NSWindow *aWindow in [self orderedWindows]) {
if ([[aWindow windowController] isMemberOfClass:[SPWindowController class]]) {
frontController = [aWindow windowController];
break;
}
}
// If no window was found or the front most window has no tabs, create a new one
if (!frontController || [[frontController valueForKeyPath:@"tabView"] numberOfTabViewItems] == 1) {
[self newWindow:self];
// Open the spf file in a new tab if the tab bar is visible
} else if ([[frontController valueForKeyPath:@"tabView"] numberOfTabViewItems] != 1) {
if ([[frontController window] isMiniaturized]) [[frontController window] deminiaturize:self];
[frontController addNewConnection:self];
}
[[self frontDocument] initWithConnectionFile:filename];
}
else if([[[filename pathExtension] lowercaseString] isEqualToString:@"spfs"]) {
}
else {
NSLog(@"Only files with the extensions ‘spf’ or ‘sql’ are allowed.");
}
}
}
#pragma mark -
#pragma mark Window management
/**
* Create a new window, containing a single tab.
*/
- (IBAction)newWindow:(id)sender
{
static NSPoint cascadeLocation = {.x = 0, .y = 0};
// Create a new window controller, and set up a new connection view within it.
SPWindowController *newWindowController = [[SPWindowController alloc] initWithWindowNibName:@"MainWindow"];
[newWindowController addNewConnection:self];
NSWindow *newWindow = [newWindowController window];
// Cascading defaults to on - retrieve the window origin automatically assigned by cascading,
// and convert to a top left point.
NSPoint topLeftPoint = [newWindow frame].origin;
topLeftPoint.y += [newWindow frame].size.height;
// The first window should use autosaving; subsequent windows should cascade.
// So attempt to set the frame autosave name; this will succeed for the very
// first window, and fail for others.
BOOL usedAutosave = [newWindow setFrameAutosaveName:@"DBView"];
if (!usedAutosave) {
[newWindow setFrameUsingName:@"DBView"];
}
// Cascade according to the statically stored cascade location.
cascadeLocation = [newWindow cascadeTopLeftFromPoint:cascadeLocation];
// Set the window controller as the window's delegate
[newWindow setDelegate:newWindowController];
// Show the window
[newWindowController showWindow:self];
}
/**
* Create a new tab in the frontmost window.
*/
- (IBAction)newTab:(id)sender
{
SPWindowController *frontController = nil;
for (NSWindow *aWindow in [self orderedWindows]) {
if ([[aWindow windowController] isMemberOfClass:[SPWindowController class]]) {
frontController = [aWindow windowController];
break;
}
}
// If no window was found, create a new one
if (!frontController) {
[self newWindow:self];
} else {
if ([[frontController window] isMiniaturized]) [[frontController window] deminiaturize:self];
[frontController addNewConnection:self];
}
}
/**
* Retrieve the frontmost document window; returns nil if not found.
*/
- (NSWindow *) frontDocumentWindow
{
for (NSWindow *aWindow in [self orderedWindows]) {
if ([[aWindow windowController] isMemberOfClass:[SPWindowController class]]) {
return aWindow;
}
}
return nil;
}
#pragma mark -
#pragma mark IBAction methods
/**
* Opens the about panel.
*/
- (IBAction)openAboutPanel:(id)sender
{
if (!aboutController) aboutController = [[SPAboutController alloc] init];
[aboutController showWindow:self];
}
/**
* Opens the preferences window.
*/
- (IBAction)openPreferences:(id)sender
{
[prefsController showWindow:self];
}
#pragma mark -
#pragma mark Accessors
/**
* Provide a method to retrieve the prefs controller
*/
- (SPPreferenceController *)preferenceController
{
return prefsController;
}
/**
* Provide a method to retrieve an ordered list of the database
* connection windows currently open in the application.
*/
- (NSArray *) orderedDatabaseConnectionWindows
{
NSMutableArray *orderedDatabaseConnectionWindows = [NSMutableArray array];
for (NSWindow *aWindow in [NSApp orderedWindows]) {
if ([[aWindow windowController] isMemberOfClass:[SPWindowController class]]) [orderedDatabaseConnectionWindows addObject:aWindow];
}
return orderedDatabaseConnectionWindows;
}
/**
* Retrieve the frontmost document; returns nil if not found.
*/
- (SPDatabaseDocument *) frontDocument
{
for (NSWindow *aWindow in [self orderedWindows]) {
if ([[aWindow windowController] isMemberOfClass:[SPWindowController class]]) {
return [[aWindow windowController] selectedTableDocument];
}
}
return nil;
}
/**
* Retrieve the session URL. Return nil if no session is opened
*/
- (NSURL *)sessionURL
{
return _sessionURL;
}
/**
* Set the global session URL used for Save (As) Session.
*/
- (void)setSessionURL:(NSString *)urlString
{
if(_sessionURL) [_sessionURL release], _sessionURL = nil;
if(urlString)
_sessionURL = [[NSURL fileURLWithPath:urlString] retain];
}
- (NSDictionary *)spfSessionDocData
{
return _spfSessionDocData;
}
- (void)setSpfSessionDocData:(NSDictionary *)data
{
[_spfSessionDocData removeAllObjects];
if(data)
[_spfSessionDocData addEntriesFromDictionary:data];
}
#pragma mark -
#pragma mark Services menu methods
/**
* Passes the query to the frontmost document
*/
- (void)doPerformQueryService:(NSPasteboard *)pboard userData:(NSString *)data error:(NSString **)error
{
NSString *pboardString;
NSArray *types = [pboard types];
if ((![types containsObject:NSStringPboardType]) || (!(pboardString = [pboard stringForType:NSStringPboardType]))) {
*error = @"Pasteboard couldn't give string.";
return;
}
// Check if at least one document exists
if (![self frontDocument]) {
*error = @"No Documents open!";
return;
}
// Pass query to front document
[[self frontDocument] doPerformQueryService:pboardString];
return;
}
#pragma mark -
#pragma mark Sequel Pro menu methods
/**
* Opens donate link in default browser
*/
- (IBAction)donate:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPDonationsURL]];
}
/**
* Opens website link in default browser
*/
- (IBAction)visitWebsite:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPHomePageURL]];
}
/**
* Opens help link in default browser
*/
- (IBAction)visitHelpWebsite:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPDocumentationURL]];
}
/**
* Opens FAQ help link in default browser
*/
- (IBAction)visitFAQWebsite:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPFAQURL]];
}
/**
* Opens the 'Contact the developers' page in the default browser
*/
- (IBAction)provideFeedback:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPContactURL]];
}
/**
* Opens the 'Translation Feedback' page in the default browser.
*/
- (IBAction)provideTranslationFeedback:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPTranslationFeedbackURL]];
}
/**
* Opens the 'Keyboard Shortcuts' page in the default browser.
*/
- (IBAction)viewKeyboardShortcuts:(id)sender
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:SPKeyboardShortcutsURL]];
}
#pragma mark -
#pragma mark Feedback reporter delegate methods
/**
* Anonymises the preferences dictionary before feedback submission
*/
- (NSMutableDictionary*)anonymizePreferencesForFeedbackReport:(NSMutableDictionary *)preferences
{
[preferences removeObjectsForKeys:[NSArray arrayWithObjects:@"ContentFilters",
@"favorites",
@"lastSqlFileName",
@"NSNavLastRootDirectory",
@"openPath",
@"queryFavorites",
@"queryHistory",
@"tableColumnWidths",
@"savePath",
@"NSRecentDocumentRecords",
nil]];
return preferences;
}
#pragma mark -
#pragma mark Other methods
/**
* Override the default open-blank-document methods to automatically connect automatically opened windows
* if the preference is set
*/
- (BOOL)applicationShouldOpenUntitledFile:(NSApplication *)sender
{
// Manually open a table document
[self newWindow:self];
// Set autoconnection if appropriate
if ([[NSUserDefaults standardUserDefaults] boolForKey:SPAutoConnectToDefault]) {
[[self frontDocument] connect];
}
// Return NO to the automatic opening
return NO;
}
/**
* Implement this method to prevent the above being called in the case of a reopen (for example, clicking
* the dock icon) where we don't want the auto-connect to kick in.
*/
- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)flag
{
// Only create a new document (without auto-connect) when there are already no documents open.
if (![self frontDocument]) {
[self newWindow:self];
return NO;
}
// Return YES to the automatic opening
return YES;
}
/**
* Insert content of a plain text file for a given path.
* In addition it tries to figure out the file's text encoding heuristically.
*/
- (NSString *)contentOfFile:(NSString *)aPath
{
NSError *err = nil;
NSStringEncoding enc;
NSString *content = nil;
// Make usage of the UNIX command "file" to get an info
// about file type and encoding.
NSTask *task=[[NSTask alloc] init];
NSPipe *pipe=[[NSPipe alloc] init];
NSFileHandle *handle;
NSString *result;
[task setLaunchPath:@"/usr/bin/file"];
[task setArguments:[NSArray arrayWithObjects:aPath, @"-Ib", nil]];
[task setStandardOutput:pipe];
handle=[pipe fileHandleForReading];
[task launch];
result=[[NSString alloc] initWithData:[handle readDataToEndOfFile]
encoding:NSASCIIStringEncoding];
[pipe release];
[task release];
// UTF16/32 files are detected as application/octet-stream resp. audio/mpeg
if( [result hasPrefix:@"text/plain"]
|| [[[aPath pathExtension] lowercaseString] isEqualToString:SPFileExtensionSQL]
|| [[[aPath pathExtension] lowercaseString] isEqualToString:@"txt"]
|| [result hasPrefix:@"audio/mpeg"]
|| [result hasPrefix:@"application/octet-stream"]
)
{
// if UTF16/32 cocoa will try to find the correct encoding
if([result hasPrefix:@"application/octet-stream"] || [result hasPrefix:@"audio/mpeg"] || [result rangeOfString:@"utf-16"].length)
enc = 0;
else if([result rangeOfString:@"utf-8"].length)
enc = NSUTF8StringEncoding;
else if([result rangeOfString:@"iso-8859-1"].length)
enc = NSISOLatin1StringEncoding;
else if([result rangeOfString:@"us-ascii"].length)
enc = NSASCIIStringEncoding;
else
enc = 0;
if(enc == 0) // cocoa tries to detect the encoding
content = [NSString stringWithContentsOfFile:aPath usedEncoding:&enc error:&err];
else
content = [NSString stringWithContentsOfFile:aPath encoding:enc error:&err];
if(content)
{
[result release];
return content;
}
// If UNIX "file" failed try cocoa's encoding detection
content = [NSString stringWithContentsOfFile:aPath encoding:enc error:&err];
if(content)
{
[result release];
return content;
}
}
[result release];
NSLog(@"%@ ‘%@’.", NSLocalizedString(@"Couldn't read the file content of", @"Couldn't read the file content of"), aPath);
return @"";
}
/**
* Sparkle updater delegate method. Called just before the updater relaunches Sequel Pro and we need to make
* sure that no sheets are currently open, which will prevent the app from being quit.
*/
- (void)updaterWillRelaunchApplication:(SUUpdater *)updater
{
// Get all the currently open windows and their attached sheets if any
NSArray *windows = [NSApp windows];
for (NSWindow *window in windows)
{
NSWindow *attachedSheet = [window attachedSheet];
if (attachedSheet) {
[NSApp endSheet:attachedSheet returnCode:0];
[attachedSheet orderOut:nil];
}
}
}
#pragma mark -
#pragma mark AppleScript support
//////////////// Examples to catch AS core events - maybe for further stuff
// - (void)handleQuitEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
// {
// [NSApp terminate:self];
// }
// - (void)handleOpenEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
// {
// NSLog(@"OPEN %@", [event description]);
// }
//
// - (void)applicationWillFinishLaunching:(NSNotification *)aNotification
// {
// NSAppleEventManager *aeManager = [NSAppleEventManager sharedAppleEventManager];
// [aeManager setEventHandler:self andSelector:@selector(handleQuitEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEQuitApplication];
// [aeManager setEventHandler:self andSelector:@selector(handleOpenEvent:withReplyEvent:) forEventClass:kCoreEventClass andEventID:kAEOpenApplication];
// }
//
/**
* Is needed to interact with AppleScript for set/get internal SP variables
*/
- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key
{
NSLog(@"Not yet implemented.");
return NO;
}
/**
* AppleScript calls that method to get the available documents
*/
- (NSArray *)orderedDocuments
{
NSMutableArray *orderedDocuments = [NSMutableArray array];
for (NSWindow *aWindow in [self orderedWindows]) {
if ([[aWindow windowController] isMemberOfClass:[SPWindowController class]]) {
[orderedDocuments addObjectsFromArray:[[aWindow windowController] documents]];
}
}
return orderedDocuments;
}
/**
* Support for 'make new document'.
* TODO: following tab support this has been disabled - need to discuss reimplmenting vs syntax.
*/
- (void)insertInOrderedDocuments:(SPDatabaseDocument *)doc
{
[self newWindow:self];
/* if ([[NSUserDefaults standardUserDefaults] boolForKey:SPAutoConnectToDefault])
[doc setShouldAutomaticallyConnect:YES];
[[NSDocumentController sharedDocumentController] addDocument:doc];
[doc makeWindowControllers];
[doc showWindows];*/
}
/**
* AppleScript calls that method to get the available windows.
*/
- (NSArray *)orderedWindows
{
return [NSApp orderedWindows];
}
/**
* AppleScript handler to quit Sequel Pro
* This handler is needed to allow to quit SP via the Dock or AppleScript after activating it by using AppleScript
*/
- (id)handleQuitScriptCommand:(NSScriptCommand *)command
{
[NSApp terminate:self];
return nil;
}
#pragma mark -
/**
* Deallocate
*/
- (void)dealloc
{
if(_spfSessionDocData) [_spfSessionDocData release], _spfSessionDocData = nil;
[prefsController release], prefsController = nil;
[aboutController release], aboutController = nil;
if(_sessionURL) [_sessionURL release], _sessionURL = nil;
[super dealloc];
}
@end