)info row:(int)row dropOperation:(NSTableViewDropOperation)operation
{
int originalRow;
int destinationRow;
NSMutableDictionary *draggedRow;
if ( aTableView == queryFavoritesView ) {
originalRow = [[[info draggingPasteboard] stringForType:@"SequelProPasteboard"] intValue];
destinationRow = row;
if ( destinationRow > originalRow )
destinationRow--;
draggedRow = [queryFavorites objectAtIndex:originalRow];
[queryFavorites removeObjectAtIndex:originalRow];
[queryFavorites insertObject:draggedRow atIndex:destinationRow];
[queryFavoritesView reloadData];
[queryFavoritesView selectRowIndexes:[NSIndexSet indexSetWithIndex:destinationRow] byExtendingSelection:NO];
return YES;
} else {
return NO;
}
}*/
#pragma mark -
#pragma mark TableView delegate methods
/**
* Show the table cell content as tooltip
* - for text displays line breaks and tabs as well
* - if blob data can be interpret as image data display the image as transparent thumbnail
* (up to now using base64 encoded HTML data)
*/
- (NSString *)tableView:(NSTableView *)aTableView toolTipForCell:(SPTextAndLinkCell *)aCell rect:(NSRectPointer)rect tableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)row mouseLocation:(NSPoint)mouseLocation
{
if([[aCell stringValue] length] < 2) return nil;
NSImage *image;
NSPoint pos = [NSEvent mouseLocation];
pos.y -= 20;
// Try to get the original data. If not possible return nil.
// @try clause is used due to the multifarious cases of
// possible exceptions (eg for reloading tables etc.)
id theValue;
@try{
theValue = NSArrayObjectAtIndex(NSArrayObjectAtIndex(fullResult, row), [[aTableColumn identifier] intValue]);
}
@catch(id ae) {
return nil;
}
// Get the original data for trying to display the blob data as an image
if ([theValue isKindOfClass:[NSData class]]) {
image = [[[NSImage alloc] initWithData:theValue] autorelease];
if(image) {
[SPTooltip showWithObject:image atLocation:pos ofType:@"image"];
return nil;
}
}
// Show the cell string value as tooltip (including line breaks and tabs)
[SPTooltip showWithObject:[aCell stringValue] atLocation:pos];
return nil;
}
/*
* Double-click action on a field
*/
- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
// Check if the field can identified bijectively
if ( aTableView == customQueryView ) {
NSDictionary *columnDefinition;
BOOL noTableName = NO;
BOOL isFieldEditable;
BOOL isBlob;
int numberOfPossibleUpdateRows = -1;
// Retrieve the column defintion
for(id c in cqColumnDefinition) {
if([[c objectForKey:@"datacolumnindex"] isEqualToNumber:[aTableColumn identifier]]) {
columnDefinition = [NSDictionary dictionaryWithDictionary:c];
break;
}
}
// Check if current field is a blob
if([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"textdata"]
|| [[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"blobdata"])
isBlob = YES;
else
isBlob = NO;
// Resolve the original table name for current column if AS was used
NSString *tableForColumn = [columnDefinition objectForKey:@"org_table"];
// Get the database name which the field belongs to
NSString *dbForColumn = [columnDefinition objectForKey:@"db"];
// No table/database name found indicates that the field's column contains data from more than one table as for UNION
// or the field data are not bound to any table as in SELECT 1 or if column database is unset
if(!tableForColumn || ![tableForColumn length] || ![dbForColumn length])
noTableName = YES;
if(!noTableName) {
// if table and database name are given check if field can be identified unambiguously
fieldIDQueryString = [self argumentForRow:rowIndex ofTable:tableForColumn andDatabase:[columnDefinition objectForKey:@"db"]];
// Actual check whether field can be identified bijectively
numberOfPossibleUpdateRows = [[[[mySQLConnection queryString:[NSString stringWithFormat:@"SELECT COUNT(*) FROM %@.%@ %@", [[columnDefinition objectForKey:@"db"] backtickQuotedString], [tableForColumn backtickQuotedString], fieldIDQueryString]] fetchRowAsArray] objectAtIndex:0] intValue];
isFieldEditable = (numberOfPossibleUpdateRows == 1) ? YES : NO;
if(!isFieldEditable)
if(numberOfPossibleUpdateRows == 0)
[errorText setStringValue:[NSString stringWithFormat:@"Field is not editable. No matching record found. Try to add the primary key field or more fields in your SELECT statement for table '%@' to identify field origin unambiguously.", tableForColumn]];
else
[errorText setStringValue:[NSString stringWithFormat:@"Field is not editable. Couldn't identify field origin unambiguously (%d match%@).", numberOfPossibleUpdateRows, (numberOfPossibleUpdateRows>1)?@"es":@""]];
} else {
// no table/databse name are given
isFieldEditable = NO;
fieldIDQueryString = nil;
[errorText setStringValue:NSLocalizedString(@"Field is not editable. Field has no or multiple table or database origin(s).",@"field is not editable due to no table/database")];
}
SPFieldEditorController *fieldEditor = [[SPFieldEditorController alloc] init];
// Set max text length
if ([[columnDefinition objectForKey:@"typegrouping"] isEqualToString:@"string"]
&& [columnDefinition valueForKey:@"char_length"])
[fieldEditor setTextMaxLength:[[columnDefinition valueForKey:@"char_length"] intValue]];
id editData = [[fieldEditor editWithObject:[[fullResult objectAtIndex:rowIndex] objectAtIndex:[[aTableColumn identifier] intValue]]
fieldName:[columnDefinition objectForKey:@"name"]
usingEncoding:[mySQLConnection encoding]
isObjectBlob:isBlob
isEditable:isFieldEditable
withWindow:tableWindow] retain];
if ( editData )
[self tableView:aTableView setObjectValue:[editData copy] forTableColumn:aTableColumn row:rowIndex];
[fieldEditor release];
if ( editData ) [editData release];
return NO;
} else {
return YES;
}
}
#pragma mark -
#pragma mark TableView notifications
/**
* Saves the new column size in the preferences for columns which map to fields
*/
- (void)tableViewColumnDidResize:(NSNotification *)aNotification
{
// Abort if still loading the table
if (![cqColumnDefinition count]) return;
// Retrieve the original index of the column from the identifier
int columnIndex = [[[[aNotification userInfo] objectForKey:@"NSTableColumn"] identifier] intValue];
NSDictionary *columnDefinition = NSArrayObjectAtIndex(cqColumnDefinition, columnIndex);
// Don't save if the column doesn't map to an underlying SQL field
if (![columnDefinition objectForKey:@"org_name"] || ![[columnDefinition objectForKey:@"org_name"] length])
return;
NSMutableDictionary *tableColumnWidths;
NSString *host_db = [NSString stringWithFormat:@"%@@%@", [columnDefinition objectForKey:@"db"], [tableDocumentInstance host]];
NSString *table = [columnDefinition objectForKey:@"org_table"];
NSString *col = [columnDefinition objectForKey:@"org_name"];
// Retrieve or instantiate the tableColumnWidths object
if ([prefs objectForKey:@"tableColumnWidths"] != nil) {
tableColumnWidths = [NSMutableDictionary dictionaryWithDictionary:[prefs objectForKey:@"tableColumnWidths"]];
} else {
tableColumnWidths = [NSMutableDictionary dictionary];
}
// Edit or create database object
if ([tableColumnWidths objectForKey:host_db] == nil) {
[tableColumnWidths setObject:[NSMutableDictionary dictionary] forKey:host_db];
} else {
[tableColumnWidths setObject:[NSMutableDictionary dictionaryWithDictionary:[tableColumnWidths objectForKey:host_db]] forKey:host_db];
}
// Edit or create table object
if ([[tableColumnWidths objectForKey:host_db] objectForKey:table] == nil) {
[[tableColumnWidths objectForKey:host_db] setObject:[NSMutableDictionary dictionary] forKey:table];
} else {
[[tableColumnWidths objectForKey:host_db] setObject:[NSMutableDictionary dictionaryWithDictionary:[[tableColumnWidths objectForKey:host_db] objectForKey:table]] forKey:table];
}
// Save the column size
[[[tableColumnWidths objectForKey:host_db] objectForKey:table] setObject:[NSNumber numberWithFloat:[[[aNotification userInfo] objectForKey:@"NSTableColumn"] width]] forKey:col];
[prefs setObject:tableColumnWidths forKey:@"tableColumnWidths"];
}
#pragma mark -
#pragma mark TextView delegate methods
/*
* Traps enter key and performs query instead of inserting a line break if aTextView == textView
* closes valueSheet if aTextView == valueTextField
*/
- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector
{
if ( aTextView == textView ) {
if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] &&
[[[NSApp currentEvent] characters] isEqualToString:@"\003"] )
{
[self runAllQueries:self];
return YES;
} else {
return NO;
}
} else if ( aTextView == valueTextField ) {
if ( [aTextView methodForSelector:aSelector] == [aTextView methodForSelector:@selector(insertNewline:)] )
{
[self closeSheet:self];
return YES;
} else {
return NO;
}
}
return NO;
}
#pragma mark -
#pragma mark TextView notifications
/*
* A notification posted when the selection changes within the text view;
* used to control the run-currentrun-selection button state and action.
*/
- (void)textViewDidChangeSelection:(NSNotification *)aNotification
{
// Ensure that the notification is from the custom query text view
if ( [aNotification object] != textView ) return;
// Remove all background color attributes used by highlighting the current query
if([prefs boolForKey:@"CustomQueryHighlightCurrentQuery"]) {
// Remove only the background attribute for the current range if still valid
NSRange textRange = NSMakeRange(0,[[textView string] length]);
NSRange r = NSIntersectionRange(currentQueryRange, textRange);
if(r.length)
[[textView textStorage] removeAttribute:NSBackgroundColorAttributeName range:r];
else
[[textView textStorage] removeAttribute:NSBackgroundColorAttributeName range:textRange];
} else {
// ensure that we do it only once
if(hasBackgroundAttribute) {
[[textView textStorage] removeAttribute:NSBackgroundColorAttributeName range:NSMakeRange(0,[[textView string] length])];
hasBackgroundAttribute = NO;
}
}
BOOL isLookBehind = YES;
NSRange currentSelection = [textView selectedRange];
long caretPosition = currentSelection.location;
NSRange qRange = [self queryRangeAtPosition:caretPosition lookBehind:&isLookBehind];
// Highlight by setting a background color the current query
// if nothing is selected
if(qRange.length && !currentSelection.length) {
if([prefs boolForKey:@"CustomQueryHighlightCurrentQuery"]) {
[[textView textStorage] addAttribute: NSBackgroundColorAttributeName
value: [NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:@"CustomQueryEditorHighlightQueryColor"]]
range: qRange ];
hasBackgroundAttribute = YES;
}
currentQueryRange = qRange;
} else {
currentQueryRange = NSMakeRange(0, 0);
}
// disable "Comment Current Query" meun item if no current query is selectable
[commentCurrentQueryMenuItem setEnabled:(currentQueryRange.length) ? YES : NO];
// If no text is selected, disable the button and action menu.
if ( caretPosition == NSNotFound ) {
[runSelectionButton setEnabled:NO];
[runSelectionMenuItem setEnabled:NO];
return;
}
// If the current selection is a single caret position, update the button based on
// whether the caret is inside a valid query.
if (!currentSelection.length) {
[runSelectionButton setTitle:NSLocalizedString(@"Run Current", @"Title of button to run current query in custom query view")];
[runSelectionMenuItem setTitle:NSLocalizedString(@"Run Current Query", @"Title of action menu item to run current query in custom query view")];
// If a valid query is present at the cursor position, enable the button
if (qRange.length) {
if (isLookBehind) {
[runSelectionButton setTitle:NSLocalizedString(@"Run Previous", @"Title of button to run query just before text caret in custom query view")];
[runSelectionMenuItem setTitle:NSLocalizedString(@"Run Previous Query", @"Title of action menu item to run query just before text caret in custom query view")];
}
[runSelectionButton setEnabled:YES];
[runSelectionMenuItem setEnabled:YES];
} else {
[runSelectionButton setEnabled:NO];
[runSelectionMenuItem setEnabled:NO];
}
[commentLineOrSelectionMenuItem setTitle:NSLocalizedString(@"Comment Line", @"Title of action menu item to comment line")];
// For selection ranges, enable the button.
} else {
[runSelectionButton setTitle:NSLocalizedString(@"Run Selection", @"Title of button to run selected text in custom query view")];
[runSelectionButton setEnabled:YES];
[runSelectionMenuItem setTitle:NSLocalizedString(@"Run Selected Text", @"Title of action menu item to run selected text in custom query view")];
[runSelectionMenuItem setEnabled:YES];
[commentLineOrSelectionMenuItem setTitle:NSLocalizedString(@"Comment Selection", @"Title of action menu item to comment selection")];
}
}
/*
* Save the custom query editor font if it is changed.
*/
- (void)textViewDidChangeTypingAttributes:(NSNotification *)aNotification
{
// Only save the font if prefs have been loaded, ensuring the saved font has been applied once.
// And check for [textView font] != nil which occurs while awaking from nib.
if (prefs && [textView font] != nil)
[prefs setObject:[NSArchiver archivedDataWithRootObject:[textView font]] forKey:@"CustomQueryEditorFont"];
}
#pragma mark -
#pragma mark TextField delegate methods
/**
* Called whenever the user changes the name of the new query favorite or
* the user changed the query favorite search string.
*/
- (void)controlTextDidChange:(NSNotification *)notification
{
if ([notification object] == queryFavoriteNameTextField)
[saveQueryFavoriteButton setEnabled:[[queryFavoriteNameTextField stringValue] length]];
else if ([notification object] == queryFavoritesSearchField){
[self filterQueryFavorites:nil];
}
else if ([notification object] == queryHistorySearchField) {
[self filterQueryHistory:nil];
}
}
#pragma mark -
#pragma mark SplitView delegate methods
/*
* Tells the splitView that it can collapse views
*/
- (BOOL)splitView:(NSSplitView *)sender canCollapseSubview:(NSView *)subview
{
return YES;
}
/*
* Defines max position of splitView
*/
- (float)splitView:(NSSplitView *)sender constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)offset
{
if ( offset == 0 ) {
return proposedMax - 100;
} else {
return proposedMax - 73;
}
}
/*
* Defines min position of splitView
*/
- (float)splitView:(NSSplitView *)sender constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)offset
{
if ( offset == 0 ) {
return proposedMin + 100;
} else {
return proposedMin + 100;
}
}
#pragma mark -
#pragma mark MySQL Help
/*
* Set the MySQL version as X.Y for Help window title and online search
*/
- (void)setMySQLversion:(NSString *)theVersion
{
mySQLversion = [[theVersion substringToIndex:3] retain];
[textView setConnection:mySQLConnection withVersion:[[[mySQLversion componentsSeparatedByString:@"."] objectAtIndex:0] intValue]];
}
/*
* Return the Help window.
*/
- (NSWindow *)helpWebViewWindow
{
return helpWebViewWindow;
}
/*
* Show the data for "HELP 'searchString'".
*/
- (void)showHelpFor:(NSString *)searchString addToHistory:(BOOL)addToHistory
{
NSString * helpString = [self getHTMLformattedMySQLHelpFor:searchString];
// Order out resp. init the Help window if not visible
if(![helpWebViewWindow isVisible])
{
// set title of the Help window
[helpWebViewWindow setTitle:[NSString stringWithFormat:@"%@ (%@ %@)", NSLocalizedString(@"MySQL Help", @"mysql help"), NSLocalizedString(@"version", @"version"), mySQLversion]];
// init goback/forward buttons
if([[helpWebView backForwardList] backListCount] < 1)
{
[helpNavigator setEnabled:NO forSegment:SP_HELP_GOBACK_BUTTON];
[helpNavigator setEnabled:NO forSegment:SP_HELP_GOFORWARD_BUTTON];
} else {
[helpNavigator setEnabled:[[helpWebView backForwardList] backListCount] forSegment:SP_HELP_GOBACK_BUTTON];
[helpNavigator setEnabled:[[helpWebView backForwardList] forwardListCount] forSegment:SP_HELP_GOFORWARD_BUTTON];
}
// set default to search in MySQL help
helpTarget = SP_HELP_SEARCH_IN_MYSQL;
[helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_MYSQL];
[self helpTargetValidation];
// order out Help window if Help is available
if(![helpString isEqualToString:SP_HELP_NOT_AVAILABLE])
[helpWebViewWindow orderFront:helpWebView];
}
// close Help window if no Help avaiable
if([helpString isEqualToString:SP_HELP_NOT_AVAILABLE])
[helpWebViewWindow close];
if(![helpString length]) return;
// add searchString to history list
if(addToHistory)
{
WebHistoryItem *aWebHistoryItem = [[WebHistoryItem alloc] initWithURLString:[NSString stringWithFormat:@"applewebdata://%@", searchString] title:searchString lastVisitedTimeInterval:[[NSDate date] timeIntervalSinceDate:[NSDate distantFuture]]];
[[helpWebView backForwardList] addItem:aWebHistoryItem];
[aWebHistoryItem release];
}
// validate goback/forward buttons
[helpNavigator setEnabled:[[helpWebView backForwardList] backListCount] forSegment:SP_HELP_GOBACK_BUTTON];
[helpNavigator setEnabled:[[helpWebView backForwardList] forwardListCount] forSegment:SP_HELP_GOFORWARD_BUTTON];
// load HTML formatted help into the webview
[[helpWebView mainFrame] loadHTMLString:helpString baseURL:nil];
}
/*
* Show the data for "HELP 'search word'" according to helpTarget
*/
- (IBAction)showHelpForSearchString:(id)sender
{
NSString *searchString = [[helpSearchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
switch(helpTarget)
{
case SP_HELP_SEARCH_IN_PAGE:
if(![helpWebView searchFor:searchString direction:YES caseSensitive:NO wrap:YES])
if([searchString length]) NSBeep();
break;
case SP_HELP_SEARCH_IN_WEB:
if(![searchString length])
break;
[self openMySQLonlineDocumentationWithString:searchString];
break;
case SP_HELP_SEARCH_IN_MYSQL:
[self showHelpFor:searchString addToHistory:YES];
break;
}
}
/*
* Show the Help for the selected text in the webview
*/
- (IBAction)showHelpForWebViewSelection:(id)sender
{
[self showHelpFor:[[helpWebView selectedDOMRange] text] addToHistory:YES];
}
/*
* Show MySQL's online documentation for the selected text in the webview
*/
- (IBAction)searchInDocForWebViewSelection:(id)sender
{
NSString *searchString = [[[helpWebView selectedDOMRange] text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if(![searchString length])
{
NSBeep();
return;
}
[self openMySQLonlineDocumentationWithString:searchString];
}
/*
* Show the data for "HELP 'currentWord'"
*/
- (IBAction)showHelpForCurrentWord:(id)sender
{
NSString *searchString = [[sender string] substringWithRange:[sender getRangeForCurrentWord]];
[self showHelpFor:searchString addToHistory:YES];
}
/*
* Find Next/Previous in current page
*/
- (IBAction)helpSearchFindNextInPage:(id)sender
{
if(helpTarget == SP_HELP_SEARCH_IN_PAGE)
if(![helpWebView searchFor:[[helpSearchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] direction:YES caseSensitive:NO wrap:YES])
NSBeep();
}
- (IBAction)helpSearchFindPreviousInPage:(id)sender
{
if(helpTarget == SP_HELP_SEARCH_IN_PAGE)
if(![helpWebView searchFor:[[helpSearchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] direction:NO caseSensitive:NO wrap:YES])
NSBeep();
}
/*
* Navigation for back/TOC/forward
*/
- (IBAction)helpSegmentDispatcher:(id)sender
{
switch([helpNavigator selectedSegment])
{
case SP_HELP_GOBACK_BUTTON:
[helpWebView goBack];
break;
case SP_HELP_SHOW_TOC_BUTTON:
[self showHelpFor:SP_HELP_TOC_SEARCH_STRING addToHistory:YES];
break;
case SP_HELP_GOFORWARD_BUTTON:
[helpWebView goForward];
break;
}
// validate goback and goforward buttons according history
[helpNavigator setEnabled:[[helpWebView backForwardList] backListCount] forSegment:SP_HELP_GOBACK_BUTTON];
[helpNavigator setEnabled:[[helpWebView backForwardList] forwardListCount] forSegment:SP_HELP_GOFORWARD_BUTTON];
}
/*
* Set helpTarget according user choice via mouse and keyboard short-cuts.
*/
- (IBAction)helpSelectHelpTargetMySQL:(id)sender
{
helpTarget = SP_HELP_SEARCH_IN_MYSQL;
[helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_MYSQL];
[self helpTargetValidation];
}
- (IBAction)helpSelectHelpTargetPage:(id)sender
{
helpTarget = SP_HELP_SEARCH_IN_PAGE;
[helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_PAGE];
[self helpTargetValidation];
}
- (IBAction)helpSelectHelpTargetWeb:(id)sender
{
helpTarget = SP_HELP_SEARCH_IN_WEB;
[helpTargetSelector setSelectedSegment:SP_HELP_SEARCH_IN_WEB];
[self helpTargetValidation];
}
- (IBAction)helpTargetDispatcher:(id)sender
{
helpTarget = [helpTargetSelector selectedSegment];
[self helpTargetValidation];
}
/*
* Control the help search field behaviour.
*/
- (void)helpTargetValidation
{
switch(helpTarget)
{
case SP_HELP_SEARCH_IN_PAGE:
case SP_HELP_SEARCH_IN_WEB:
[helpSearchFieldCell setSendsWholeSearchString:YES];
break;
case SP_HELP_SEARCH_IN_MYSQL:
[helpSearchFieldCell setSendsWholeSearchString:NO];
break;
}
}
- (void)openMySQLonlineDocumentationWithString:(NSString *)searchString
{
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:
[[NSString stringWithFormat:
SP_MYSQL_DEV_SEARCH_URL,
searchString,
[mySQLversion stringByReplacingOccurrencesOfString:@"." withString:@""]]
stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding]]];
}
/*
* Return the help string HTML formatted from executing "HELP 'searchString'".
* If more than one help topic was found return a link list.
*/
- (NSString *)getHTMLformattedMySQLHelpFor:(NSString *)searchString
{
if(![searchString length]) return @"";
NSRange aRange;
MCPResult *theResult = nil;
NSDictionary *tableDetails;
NSMutableString *theHelp = [NSMutableString string];
[theHelp setString:@""];
// search via: HELP 'searchString'
theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"HELP '%@'", [searchString stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]]];
if ( ![[mySQLConnection getLastErrorMessage] isEqualToString:@""])
{
// if an error or HELP is not supported fall back to online search
NSLog(@"Error in HELP statement for '%@'", searchString);
[self openMySQLonlineDocumentationWithString:searchString];
return SP_HELP_NOT_AVAILABLE;
}
// nothing found?
if(![theResult numOfRows]) {
// try to search via: HELP 'searchString%'
theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"HELP '%@%%'", [searchString stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]]];
// really nothing found?
if(![theResult numOfRows])
return @"";
}
tableDetails = [[NSDictionary alloc] initWithDictionary:[theResult fetchRowAsDictionary]];
if ([tableDetails objectForKey:@"description"]) { // one single help topic found
if ([tableDetails objectForKey:@"name"]) {
[theHelp appendString:@""];
}
if ([tableDetails objectForKey:@"description"]) {
NSMutableString *desc = [NSMutableString string];
NSError *err1 = NULL;
NSString *aUrl;
[desc setString:[[[tableDetails objectForKey:@"description"] copy] autorelease]];
//[desc replaceOccurrencesOfString:[searchString uppercaseString] withString:[NSString stringWithFormat:@"%@", [searchString uppercaseString]] options:NSLiteralSearch range:NSMakeRange(0,[desc length])];
// detect and generate http links
aRange = NSMakeRange(0,0);
int safeCnt = 0; // safety counter - not more than 200 loops allowed
while(1){
aRange = [desc rangeOfRegex:@"\\s((https?|ftp|file)://.*?html)" options:RKLNoOptions inRange:NSMakeRange(aRange.location+aRange.length, [desc length]-aRange.location-aRange.length) capture:1 error:&err1];
if(aRange.location != NSNotFound) {
aUrl = [desc substringWithRange:aRange];
[desc replaceCharactersInRange:aRange withString:[NSString stringWithFormat:@"%@", aUrl, aUrl]];
}
else
break;
safeCnt++;
if(safeCnt > 200)
break;
}
// detect and generate mysql links for "[HELP keyword]"
aRange = NSMakeRange(0,0);
safeCnt = 0;
while(1){
// TODO how to catch in HELP 'grant' last see [HELP SHOW GRANTS] ?? it's ridiculous
aRange = [desc rangeOfRegex:@"\\[HELP ([^ ]*?)\\]" options:RKLNoOptions inRange:NSMakeRange(aRange.location+aRange.length+53, [desc length]-53-aRange.location-aRange.length) capture:1 error:&err1];
if(aRange.location != NSNotFound) {
aUrl = [[desc substringWithRange:aRange] stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
[desc replaceCharactersInRange:aRange withString:[NSString stringWithFormat:@"%@", NSLocalizedString(@"Show MySQL help for", @"show mysql help for"), aUrl, aUrl, aUrl]];
}
else
break;
safeCnt++;
if(safeCnt > 200)
break;
}
// detect and generate mysql links for capitalzed letters
// aRange = NSMakeRange(0,0);
// safeCnt = 0;
// while(1){
// aRange = [desc rangeOfRegex:@"(?%@", NSLocalizedString(@"Show MySQL help for", @"show mysql help for"), aUrl, aUrl, aUrl]];
// }
// else
// break;
// safeCnt++;
// if(safeCnt > 200)
// break;
// }
[theHelp appendString:@""];
[theHelp appendString:desc];
[theHelp appendString:@"
"];
}
// are examples available?
if([tableDetails objectForKey:@"example"]){
NSString *examples = [[[tableDetails objectForKey:@"example"] copy] autorelease];
if([examples length]){
[theHelp appendString:@"
Example:
"];
[theHelp appendString:examples];
[theHelp appendString:@"
"];
}
}
} else { // list all found topics
int i;
int r = [theResult numOfRows];
if (r) [theResult dataSeek:0];
// check if HELP 'contents' is called
if(![searchString isEqualToString:SP_HELP_TOC_SEARCH_STRING])
[theHelp appendString:[NSString stringWithFormat:@"
%@ “%@”
", NSLocalizedString(@"Help topics for", @"help topics for"), searchString]];
else
[theHelp appendString:[NSString stringWithFormat:@"
%@:
", NSLocalizedString(@"MySQL Help – Categories", @"mysql help categories"), searchString]];
// iterate through all found rows and print them as HTML ul/li list
[theHelp appendString:@""];
for ( i = 0 ; i < r ; i++ ) {
NSArray *anArray = [theResult fetchRowAsArray];
NSString *topic = [anArray objectAtIndex:[anArray count]-2];
[theHelp appendString:
[NSString stringWithFormat:@"- %@
", NSLocalizedString(@"Show MySQL help for", @"show mysql help for"), topic, topic, topic]];
}
[theHelp appendString:@"
"];
}
[tableDetails release];
return [NSString stringWithFormat:helpHTMLTemplate, theHelp];
}
//////////////////////////////
// WebView delegate methods //
//////////////////////////////
/*
* Link detector: If user clicked at an http link open it in the default browser,
* otherwise search for it in the MySQL help. Additionally handle back/forward events from
* keyboard and context menu.
*/
- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener
{
int navigationType = [[actionInformation objectForKey:WebActionNavigationTypeKey] intValue];
if([[[request URL] scheme] isEqualToString:@"applewebdata"] && navigationType == WebNavigationTypeLinkClicked){
[self showHelpFor:[[[request URL] path] lastPathComponent] addToHistory:YES];
[listener ignore];
} else {
if (navigationType == WebNavigationTypeOther) {
// catch reload event
// if([[[actionInformation objectForKey:WebActionOriginalURLKey] absoluteString] isEqualToString:@"about:blank"])
// [listener use];
// else
[listener use];
} else if (navigationType == WebNavigationTypeLinkClicked) {
// show http in browser
[[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]];
[listener ignore];
} else if (navigationType == WebNavigationTypeBackForward) {
// catch back/forward events from contextual menu
[self showHelpFor:[[[[actionInformation objectForKey:WebActionOriginalURLKey] absoluteString] lastPathComponent] stringByReplacingPercentEscapesUsingEncoding:NSASCIIStringEncoding] addToHistory:NO];
[listener ignore];
} else if (navigationType == WebNavigationTypeReload) {
// just in case
[listener ignore];
} else {
// Ignore WebNavigationTypeFormSubmitted, WebNavigationTypeFormResubmitted.
[listener ignore];
}
}
}
/*
* Manage contextual menu in helpWebView
* Ignore "Reload", "Open Link", "Open Link in new Window", "Download link" etc.
*/
- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
{
NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
if (webViewMenuItems)
{
// Remove all needless default menu items
NSEnumerator *itemEnumerator = [defaultMenuItems objectEnumerator];
NSMenuItem *menuItem = nil;
while (menuItem = [itemEnumerator nextObject])
{
int tag = [menuItem tag];
switch (tag)
{
case 2000: // WebMenuItemTagOpenLink
case WebMenuItemTagOpenLinkInNewWindow:
case WebMenuItemTagDownloadLinkToDisk:
case WebMenuItemTagOpenImageInNewWindow:
case WebMenuItemTagDownloadImageToDisk:
case WebMenuItemTagCopyImageToClipboard:
case WebMenuItemTagOpenFrameInNewWindow:
case WebMenuItemTagStop:
case WebMenuItemTagReload:
case WebMenuItemTagCut:
case WebMenuItemTagPaste:
case WebMenuItemTagSpellingGuess:
case WebMenuItemTagNoGuessesFound:
case WebMenuItemTagIgnoreSpelling:
case WebMenuItemTagLearnSpelling:
case WebMenuItemTagOther:
case WebMenuItemTagOpenWithDefaultApplication:
[webViewMenuItems removeObjectIdenticalTo: menuItem];
break;
}
}
}
// Add two menu items for a selection if no link is given
if(webViewMenuItems
&& [[element objectForKey:@"WebElementIsSelected"] boolValue]
&& ![[element objectForKey:@"WebElementLinkIsLive"] boolValue])
{
NSMenuItem *searchInMySQL;
NSMenuItem *searchInMySQLonline;
[webViewMenuItems insertObject:[NSMenuItem separatorItem] atIndex:0];
searchInMySQLonline = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Search in MySQL Documentation", @"Search in MySQL Documentation") action:@selector(searchInDocForWebViewSelection:) keyEquivalent:@""];
[searchInMySQLonline setEnabled:YES];
[searchInMySQLonline setTarget:self];
[webViewMenuItems insertObject:searchInMySQLonline atIndex:0];
[searchInMySQLonline release];
searchInMySQL = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Search in MySQL Help", @"Search in MySQL Help") action:@selector(showHelpForWebViewSelection:) keyEquivalent:@""];
[searchInMySQL setEnabled:YES];
[searchInMySQL setTarget:self];
[webViewMenuItems insertObject:searchInMySQL atIndex:0];
[searchInMySQL release];
}
return webViewMenuItems;
}
#pragma mark -
#pragma mark Query favorites manager delegate methods
/**
* Called by the query favorites manager whenever the query favorites have been updated.
*/
- (void)queryFavoritesHaveBeenUpdated:(id)manager
{
NSMutableArray *favorites = [[NSMutableArray alloc] init];
// Invoked after adding a favorite from inside CustomQuery
if(manager == nil) {
[favorites addObjectsFromArray:[prefs objectForKey:@"queryFavorites"]];
}
// Invoked by the query favorite manager
else if(manager == favoritesManager){
// Update global query favorites
[favorites addObjectsFromArray:[favoritesManager globalQueryFavorites]];
[prefs setObject:favorites forKey:@"queryFavorites"];
[prefs synchronize];
}
// Remove all favorites beginning from the end
while([queryFavoritesButton numberOfItems] > 6)
[queryFavoritesButton removeItemAtIndex:[queryFavoritesButton numberOfItems]-1];
// Re-add favorites and allow menu items with the same name
NSMenu *menu = [queryFavoritesButton menu];
int i = 6;
for (NSDictionary *favorite in favorites) {
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithString:[favorite objectForKey:@"name"]] action:NULL keyEquivalent:@""];
[item setTag:i++];
[item setToolTip:[NSString stringWithString:[favorite objectForKey:@"query"]]];
[menu addItem:item];
[item release];
}
[favorites release];
[prefs synchronize];
[queryFavoritesSearchField setStringValue:@""];
}
#pragma mark -
#pragma mark Other
/**
* Returns the number of queries.
*/
- (NSUInteger)numberOfQueries
{
return numberOfQueries;
}
/*
* This method is called as part of Key Value Observing which is used to watch for prefernce changes which effect the interface.
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"DisplayTableViewVerticalGridlines"]) {
[customQueryView setGridStyleMask:([[change objectForKey:NSKeyValueChangeNewKey] boolValue]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone];
}
}
/**
* Called when the save query favorite sheet is dismissed.
*/
- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo
{
if ([contextInfo isEqualToString:@"addAllToNewQueryFavorite"] || [contextInfo isEqualToString:@"addSelectionToNewQueryFavorite"]) {
if (returnCode == NSOKButton) {
// Add the new query favorite directly the user's preferences here instead of asking the manager to do it
// as it may not have been fully initialized yet.
NSMutableArray *favorites = [NSMutableArray arrayWithArray:[prefs objectForKey:@"queryFavorites"]];
// What should be saved
NSString *queryToBeAddded;
if([contextInfo isEqualToString:@"addSelectionToNewQueryFavorite"]) {
// First check for a selection
if([textView selectedRange].length)
queryToBeAddded = [[textView string] substringWithRange:[textView selectedRange]];
// then for a current query
else if(currentQueryRange.length)
queryToBeAddded = [[textView string] substringWithRange:currentQueryRange];
// otherwise take the entire string
else
queryToBeAddded = [textView string];
} else {
queryToBeAddded = [textView string];
}
[favorites addObject:[NSDictionary dictionaryWithObjects:
[NSArray arrayWithObjects:[queryFavoriteNameTextField stringValue], queryToBeAddded, nil]
forKeys:[NSArray arrayWithObjects:@"name", @"query", nil]]];
[prefs setObject:favorites forKey:@"queryFavorites"];
[prefs synchronize];
[self queryFavoritesHaveBeenUpdated:nil];
}
}
[queryFavoriteNameTextField setStringValue:@""];
}
/**
* Menu item validation.
*/
- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
// Control "Save ... to Favorites" = tag is set to 1000
if ( [menuItem tag] == 1000 ) {
if ([[textView string] length] < 1) return NO;
if([textView selectedRange].length)
[menuItem setTitle:NSLocalizedString(@"Save Selection to Favorites",@"Save Selection to Favorites")];
else if(currentQueryRange.length)
[menuItem setTitle:NSLocalizedString(@"Save Current Query to Favorites",@"Save Current Query to Favorites")];
else
[menuItem setTitle:NSLocalizedString(@"Save All to Favorites",@"Save All to Favorites")];
}
// Control "Save All to Favorites" = tag is set to 1001
if ( [menuItem tag] == 1001 ) {
if ([[textView string] length] < 1) return NO;
}
return YES;
}
#pragma mark -
// Last but not least
- (id)init
{
if ((self = [super init])) {
usedQuery = [[NSString stringWithString:@""] retain];
sortField = nil;
isDesc = NO;
// init helpHTMLTemplate
NSError *error;
helpHTMLTemplate = [[NSString alloc]
initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"sequel-pro-mysql-help-template" ofType:@"html"]
encoding:NSUTF8StringEncoding
error:&error];
// an error occurred while reading
if (helpHTMLTemplate == nil) {
NSLog(@"%@", [NSString stringWithFormat:@"Error reading “sequel-pro-mysql-help-template.html”!
%@", [error localizedFailureReason]]);
NSBeep();
}
// init search history
[helpWebView setMaintainsBackForwardList:YES];
[[helpWebView backForwardList] setCapacity:20];
// init tableView's data source
fullResult = [[NSMutableArray alloc] init];
// init local favorite and history item array
localHistoryItems = [[NSMutableArray alloc] init];
localFavorites = [[NSMutableArray alloc] init];
prefs = [NSUserDefaults standardUserDefaults];
}
return self;
}
- (IBAction)filterQueryFavorites:(id)sender
{
int i;
NSMenu *menu = [queryFavoritesButton menu];
for (i=6; i< [menu numberOfItems]; i++)
[[menu itemAtIndex:i] setHidden:(![[[menu itemAtIndex:i] title] isMatchedByRegex:[NSString stringWithFormat:@"(?i).*%@.*", [queryFavoritesSearchField stringValue]]])];
}
- (IBAction)filterQueryHistory:(id)sender
{
int i;
NSMenu *menu = [queryHistoryButton menu];
for (i=2; i< [menu numberOfItems]; i++)
[[menu itemAtIndex:i] setHidden:(![[[menu itemAtIndex:i] title] isMatchedByRegex:[NSString stringWithFormat:@"(?i).*%@.*", [queryHistorySearchField stringValue]]])];
}
- (void)awakeFromNib
{
// Set the structure and index view's vertical gridlines if required
[customQueryView setGridStyleMask:([prefs boolForKey:@"DisplayTableViewVerticalGridlines"]) ? NSTableViewSolidVerticalGridLineMask : NSTableViewGridNone];
// Populate the query favorites popup button
NSMenu *menu = [queryFavoritesButton menu];
int i = 6;
for (NSDictionary *favorite in [prefs objectForKey:@"queryFavorites"])
{
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[favorite objectForKey:@"name"] action:NULL keyEquivalent:@""];
[item setTag:i++];
[item setToolTip:[favorite objectForKey:@"query"]];
[menu addItem:item];
[item release];
}
}
- (void)dealloc
{
[usedQuery release];
[fullResult release];
[favoritesManager release];
if (localFavorites) [localFavorites release];
if (localHistoryItems) [localHistoryItems release];
if (sortField) [sortField release];
[super dealloc];
}
@end