diff options
Diffstat (limited to 'Source')
-rw-r--r-- | Source/SPCustomQuery.h | 40 | ||||
-rw-r--r-- | Source/SPCustomQuery.m | 531 | ||||
-rw-r--r-- | Source/SPDatabaseDocument.h | 4 | ||||
-rw-r--r-- | Source/SPDatabaseDocument.m | 14 | ||||
-rw-r--r-- | Source/SPHelpViewerClient.h | 66 | ||||
-rw-r--r-- | Source/SPHelpViewerClient.m | 262 | ||||
-rw-r--r-- | Source/SPHelpViewerController.h | 99 | ||||
-rw-r--r-- | Source/SPHelpViewerController.m | 415 | ||||
-rw-r--r-- | Source/SPTextView.m | 3 |
9 files changed, 876 insertions, 558 deletions
diff --git a/Source/SPCustomQuery.h b/Source/SPCustomQuery.h index 24695ba2..f06b63dc 100644 --- a/Source/SPCustomQuery.h +++ b/Source/SPCustomQuery.h @@ -31,17 +31,6 @@ #import "SPDatabaseContentViewDelegate.h" -#import <WebKit/WebKit.h> - -#define SP_HELP_TOC_SEARCH_STRING @"contents" -#define SP_HELP_SEARCH_IN_MYSQL 0 -#define SP_HELP_SEARCH_IN_PAGE 1 -#define SP_HELP_SEARCH_IN_WEB 2 -#define SP_HELP_GOBACK_BUTTON 0 -#define SP_HELP_SHOW_TOC_BUTTON 1 -#define SP_HELP_GOFORWARD_BUTTON 2 -#define SP_HELP_NOT_AVAILABLE @"__no_help_available" - #define SP_SAVE_ALL_FAVORTITE_MENUITEM_TAG 100001 #define SP_SAVE_SELECTION_FAVORTITE_MENUITEM_TAG 100000 #define SP_FAVORITE_HEADER_MENUITEM_TAG 200000 @@ -122,12 +111,6 @@ #ifndef SP_CODA IBOutlet NSMenuItem *previousHistoryMenuItem; IBOutlet NSMenuItem *nextHistoryMenuItem; - IBOutlet NSWindow *helpWebViewWindow; - IBOutlet WebView *helpWebView; - IBOutlet NSSearchField *helpSearchField; - IBOutlet NSSearchFieldCell *helpSearchFieldCell; - IBOutlet NSSegmentedControl *helpNavigator; - IBOutlet NSSegmentedControl *helpTargetSelector; #endif IBOutlet NSButton *queryInfoButton; @@ -145,17 +128,10 @@ NSArray *currentQueryRanges; BOOL currentQueryBeforeCaret; - NSString *mySQLversion; NSTableColumn *sortColumn; NSUInteger queryStartPosition; -#ifndef SP_CODA - NSUInteger helpTarget; - WebHistory *helpHistory; - NSString *helpHTMLTemplate; -#endif - SPDataStorage *resultData; pthread_mutex_t resultDataLock; NSArray *cqColumnDefinition; @@ -213,17 +189,6 @@ - (IBAction)chooseQueryHistory:(id)sender; - (IBAction)closeSheet:(id)sender; - (IBAction)gearMenuItemSelected:(id)sender; -#ifndef SP_CODA -- (IBAction)showHelpForCurrentWord:(id)sender; -- (IBAction)showHelpForSearchString:(id)sender; -- (IBAction)helpSegmentDispatcher:(id)sender; -- (IBAction)helpTargetDispatcher:(id)sender; -- (IBAction)helpSearchFindNextInPage:(id)sender; -- (IBAction)helpSearchFindPreviousInPage:(id)sender; -- (IBAction)helpSelectHelpTargetMySQL:(id)sender; -- (IBAction)helpSelectHelpTargetPage:(id)sender; -- (IBAction)helpSelectHelpTargetWeb:(id)sender; -#endif - (IBAction)filterQueryFavorites:(id)sender; - (IBAction)filterQueryHistory:(id)sender; - (IBAction)saveQueryHistory:(id)sender; @@ -268,11 +233,6 @@ #ifndef SP_CODA // MySQL Help - (void)showAutoHelpForCurrentWord:(id)sender; -- (NSString *)getHTMLformattedMySQLHelpFor:(NSString *)searchString calledByAutoHelp:(BOOL)autoHelp; -- (void)showHelpFor:(NSString *)aString addToHistory:(BOOL)addToHistory calledByAutoHelp:(BOOL)autoHelp; -- (void)helpTargetValidation; -- (void)openMySQLonlineDocumentationWithString:(NSString *)searchString; -- (NSWindow *)helpWebViewWindow; #endif - (void)setMySQLversion:(NSString *)theVersion; diff --git a/Source/SPCustomQuery.m b/Source/SPCustomQuery.m index e8c1d6ad..62ff3e18 100644 --- a/Source/SPCustomQuery.m +++ b/Source/SPCustomQuery.m @@ -61,6 +61,8 @@ #import "SPBundleHTMLOutputController.h" #endif #import "SPFunctions.h" +#import "SPHelpViewerClient.h" +#import "SPHelpViewerController.h" #import <pthread.h> #import <SPMySQL/SPMySQL.h> @@ -68,10 +70,10 @@ @interface SPCustomQuery () - (id)_resultDataItemAtRow:(NSInteger)row columnIndex:(NSUInteger)column preserveNULLs:(BOOL)preserveNULLs asPreview:(BOOL)asPreview; -+ (NSString *)linkToHelpTopic:(NSString *)aTopic; - (void)documentWillClose:(NSNotification *)notification; - (void)queryFavoritesHaveBeenUpdated:(NSNotification *)notification; - (void)historyItemsHaveBeenUpdated:(NSNotification *)notification; +- (void)helpWindowClosedByUser:(NSNotification *)notification; @end @@ -2830,221 +2832,14 @@ #endif -#pragma mark - -#pragma mark MySQL Help - /** - * Set the MySQL version as X.Y for Help window title and online search + * Set the MySQL version as X.Y */ - (void)setMySQLversion:(NSString *)theVersion { - mySQLversion = [[theVersion substringToIndex:3] retain]; - [textView setConnection:mySQLConnection withVersion:[[[mySQLversion componentsSeparatedByString:@"."] objectAtIndex:0] integerValue]]; -} - -#ifndef SP_CODA -/** - * Return the Help window. - */ -- (NSWindow *)helpWebViewWindow -{ - return helpWebViewWindow; + [textView setConnection:mySQLConnection withVersion:[[[[theVersion substringToIndex:3] componentsSeparatedByString:@"."] objectAtIndex:0] integerValue]]; } -/** - * Show the data for "HELP 'searchString'". - */ -- (void)showHelpFor:(NSString *)searchString addToHistory:(BOOL)addToHistory calledByAutoHelp:(BOOL)autoHelp -{ - - // If there's no search string, show nothing if called by autohelp, and the index otherwise - if (![searchString length]) { - if (autoHelp) return; - searchString = SP_HELP_TOC_SEARCH_STRING; - } - - NSString *helpString = [self getHTMLformattedMySQLHelpFor:searchString calledByAutoHelp:autoHelp]; - - if(autoHelp && [helpString isEqualToString:SP_HELP_NOT_AVAILABLE]) { - [helpWebViewWindow orderOut:nil]; - return; - } - - // 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 available - 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 calledByAutoHelp:NO]; - break; - } -} - -/** - * Show the Help for the selected text in the webview - */ -- (IBAction)showHelpForWebViewSelection:(id)sender -{ - [self showHelpFor:[[helpWebView selectedDOMRange] text] addToHistory:YES calledByAutoHelp:NO]; -} - -/* - * 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 calledByAutoHelp:NO]; -} - -/** - * 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 calledByAutoHelp:NO]; - 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]; -} -#endif - - (IBAction)showCompletionList:(id)sender { NSRange insertRange = NSMakeRange([textView selectedRange].location, 0); @@ -3068,294 +2863,22 @@ - (void)showAutoHelpForCurrentWord:(id)sender { NSString *searchString = [[sender string] substringWithRange:[sender getRangeForCurrentWord]]; - [self showHelpFor:searchString addToHistory:YES calledByAutoHelp:YES]; -} - -/** - * 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 -{ - NSString *version = nil; - if([[mySQLversion stringByReplacingOccurrencesOfString:@"." withString:@""] integerValue] < 42) - version = @"4.1"; - else - version = [NSString stringWithString:mySQLversion]; - - [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString: - [[NSString stringWithFormat: - SPMySQLSearchURL, - version, - NSLocalizedString(@"en", @"MySQL search language code - eg in http://search.mysql.com/search?q=select&site=refman-50&lr=lang_en"), - searchString] - 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 calledByAutoHelp:(BOOL)autoHelp -{ - - if(![searchString length]) return @""; - - // Don't escape % when being used as a wildcard, but escape it when it's being used by itself. - if ([searchString isEqualToString:@"%"]) searchString = @"\\%"; - - NSRange aRange; - SPMySQLResult *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 queryErrored]) - { - // if an error or HELP is not supported fall back to online search but - // don't open it if autoHelp is enabled - if(!autoHelp) - [self openMySQLonlineDocumentationWithString:searchString]; - - [helpWebViewWindow close]; - return SP_HELP_NOT_AVAILABLE; - } - - // nothing found? - if(![theResult numberOfRows]) { - - // try to search via: HELP 'searchString%' - theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"HELP '%@%%'", [searchString stringByReplacingOccurrencesOfString:@"'" withString:@"\\'"]]]; - - // really nothing found? - if(![theResult numberOfRows]) - return [NSString stringWithFormat:@"<em style='color: gray'>%@</em>", NSLocalizedString(@"No results found.", @"Mysql Help Viewer : Search : No results")]; - } - - // Ensure rows are returned as strings to prevent data problems with older 4.1 servers - [theResult setReturnDataAsStrings:YES]; - - tableDetails = [[NSDictionary alloc] initWithDictionary:[theResult getRowAsDictionary]]; - - if ([tableDetails objectForKey:@"description"]) { // one single help topic found - if ([tableDetails objectForKey:@"name"]) { - [theHelp appendString:@"<h2 class='header'>"]; - [theHelp appendString:[[[tableDetails objectForKey:@"name"] copy] autorelease]]; - [theHelp appendString:@"</h2>"]; - - } - 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:@"<span class='searchstring'>%@</span>", [searchString uppercaseString]] options:NSLiteralSearch range:NSMakeRange(0,[desc length])]; - - // detect and generate http links - aRange = NSMakeRange(0,0); - NSInteger safeCnt = 0; // safety counter - not more than 200 loops allowed - while(1){ - aRange = [desc rangeOfRegex:@"\\s((https?|ftp|file)://.*?html)" options:RKLNoOptions inRange:NSMakeRange(NSMaxRange(aRange), [desc length]-aRange.location-aRange.length) capture:1 error:&err1]; - if(aRange.location != NSNotFound) { - aUrl = [desc substringWithRange:aRange]; - [desc replaceCharactersInRange:aRange withString:[NSString stringWithFormat:@"<a href='%@'>%@</a>", aUrl, aUrl]]; - } - else - break; - safeCnt++; - if(safeCnt > 200) - break; - } - - // Detect and generate cross-links. First, handle the old-style [HELP ...] text. - [desc replaceOccurrencesOfRegex:@"(\\[HELP ([^\\]]*?)\\]" withString:[SPCustomQuery linkToHelpTopic:@"$1"]]; - - // Handle "see [...]" and "in [...]"-style 5.x links. - //look-behind won't work here because of the \s+ - [desc replaceOccurrencesOfRegex:@"(See|see|In|in|and)\\s+\\[(?:HELP\\s+)?([^\\]]*?)\\]" withString:[NSString stringWithFormat:@"$1 %@",[SPCustomQuery linkToHelpTopic:@"$2"]]]; - - [theHelp appendFormat:@"<pre class='description'>%@</pre>", desc]; - } - // are examples available? - if([tableDetails objectForKey:@"example"]){ - NSString *examples = [[[tableDetails objectForKey:@"example"] copy] autorelease]; - if([examples length]) - [theHelp appendFormat:@"<br><i><b>%1$@</b></i><br><pre class='example'>%2$@</pre>",NSLocalizedString(@"Example:",@"Mysql Help Viewer : Help Topic: Example section title"), examples]; - - } - } else { // list all found topics - - // check if HELP 'contents' is called - if(![searchString isEqualToString:SP_HELP_TOC_SEARCH_STRING]) - [theHelp appendFormat:@"<br><i>%@</i><br>", [NSString stringWithFormat:NSLocalizedString(@"Help topics for “%@”", @"MySQL Help Viewer : Results list : Page title"), searchString]]; - else - [theHelp appendFormat:@"<br><b>%@:</b><br>", NSLocalizedString(@"MySQL Help – Categories", @"mysql help categories")]; - - // iterate through all found rows and print them as HTML ul/li list - [theHelp appendString:@"<ul>"]; - [theResult setDefaultRowReturnType:SPMySQLResultRowAsArray]; - for (NSArray *eachRow in theResult) { - NSString *topic = [eachRow objectAtIndex:[eachRow count]-2]; - [theHelp appendFormat:@"<li>%@</li>",[SPCustomQuery linkToHelpTopic:topic]]; - } - [theHelp appendString:@"</ul>"]; - } - - [tableDetails release]; - - return [NSString stringWithFormat:helpHTMLTemplate, theHelp]; -} - -+ (NSString *)linkToHelpTopic:(NSString *)aTopic -{ - NSString *linkTitle = [NSString stringWithFormat:NSLocalizedString(@"Show MySQL help for “%@”", @"MySQL Help Viewer : Results list : Link tooltip"),aTopic]; - return [NSString stringWithFormat:@"<a title='%2$@' href='%1$@' class='internallink'>%1$@</a>", aTopic, linkTitle]; -} - -#pragma mark - -#pragma mark 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<WebPolicyDecisionListener>)listener -{ - NSInteger navigationType = [[actionInformation objectForKey:WebActionNavigationTypeKey] integerValue]; - - if([[[request URL] scheme] isEqualToString:@"applewebdata"] && navigationType == WebNavigationTypeLinkClicked){ - [self showHelpFor:[[[request URL] path] lastPathComponent] addToHistory:YES calledByAutoHelp:NO]; - [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 calledByAutoHelp: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])) - { - NSInteger 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; + [[tableDocumentInstance helpViewerClient] showHelpFor:searchString addToHistory:YES calledByAutoHelp:YES]; } /** * Detect when the help window is closed (manually) and disable autohelp to ensure it * isn't reopened on keypresses. */ -- (BOOL)windowShouldClose:(id)sender +- (void)helpWindowClosedByUser:(NSNotification *)notification { - if (sender == helpWebViewWindow) { - [prefs setBool:NO forKey:SPCustomQueryUpdateAutoHelp]; - [prefs synchronize]; - [autohelpMenuItem setState:NSOffState]; - [textView setAutohelp:NO]; - } + if ([notification object] != [tableDocumentInstance helpViewerClient]) return; - return YES; + //TODO: this doesn't belong in the document context, since multiple open documents can become out of sync through this + [prefs setBool:NO forKey:SPCustomQueryUpdateAutoHelp]; + [prefs synchronize]; + [autohelpMenuItem setState:NSOffState]; + [textView setAutohelp:NO]; } #endif @@ -3749,26 +3272,6 @@ selectionIndexToRestore = nil; selectionViewportToRestore = NSZeroRect; -#ifndef SP_CODA - // init helpHTMLTemplate - NSError *error; - - helpHTMLTemplate = [[NSString alloc] - initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:SPHTMLHelpTemplate ofType:@"html"] - encoding:NSUTF8StringEncoding - error:&error]; - - // an error occurred while reading - if (helpHTMLTemplate == nil) { - NSLog(@"%@", [NSString stringWithFormat:@"Error reading “%@.html”!<br>%@", SPHTMLHelpTemplate, [error localizedFailureReason]]); - NSBeep(); - } - - // init search history - [helpWebView setMaintainsBackForwardList:YES]; - [[helpWebView backForwardList] setCapacity:20]; -#endif - // init tableView's data source resultData = [[SPDataStorage alloc] init]; editedRow = -1; @@ -4003,6 +3506,10 @@ selector:@selector(historyItemsHaveBeenUpdated:) name:SPHistoryItemsHaveBeenUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(helpWindowClosedByUser:) + name:SPUserClosedHelpViewerNotification + object:[tableDocumentInstance helpViewerClient]]; #ifndef SP_CODA [prefs addObserver:self forKeyPath:SPGlobalResultTableFont options:NSKeyValueObservingOptionNew context:NULL]; @@ -4088,10 +3595,6 @@ if(fieldEditor) SPClear(fieldEditor); -#ifndef SP_CODA - if (helpHTMLTemplate) SPClear(helpHTMLTemplate); -#endif - if (mySQLversion) SPClear(mySQLversion); if (sortField) SPClear(sortField); if (cqColumnDefinition) SPClear(cqColumnDefinition); if (selectionIndexToRestore) SPClear(selectionIndexToRestore); diff --git a/Source/SPDatabaseDocument.h b/Source/SPDatabaseDocument.h index 20fb34d1..ceaca039 100644 --- a/Source/SPDatabaseDocument.h +++ b/Source/SPDatabaseDocument.h @@ -60,6 +60,7 @@ @class SPExtendedTableInfo; @class SPTableTriggers; @class SPTableRelations; +@class SPHelpViewerClient; #import "SPDatabaseContentViewDelegate.h" #import "SPConnectionControllerDelegateProtocol.h" @@ -92,6 +93,7 @@ IBOutlet id spHistoryControllerInstance; IBOutlet id exportControllerInstance; #endif + IBOutlet SPHelpViewerClient *helpViewerClientInstance; IBOutlet id statusTableAccessoryView; IBOutlet id statusTableView; @@ -325,6 +327,8 @@ @property (readonly) SPDatabaseStructure *databaseStructureRetrieval; @property (readonly) int64_t instanceId; +- (SPHelpViewerClient *)helpViewerClient; + #ifndef SP_CODA /* method decls */ - (BOOL)isUntitled; #endif diff --git a/Source/SPDatabaseDocument.m b/Source/SPDatabaseDocument.m index 4bdd0aad..4909282c 100644 --- a/Source/SPDatabaseDocument.m +++ b/Source/SPDatabaseDocument.m @@ -48,7 +48,6 @@ #import "SPSQLParser.h" #import "SPTableData.h" #import "SPDatabaseData.h" -#import "SPDatabaseStructure.h" #import "SPExtendedTableInfo.h" #import "SPHistoryController.h" #import "SPPreferenceController.h" @@ -85,6 +84,8 @@ #import "ICUTemplateMatcher.h" #import "SPFavoritesOutlineView.h" #import "SPSSHTunnel.h" +#import "SPHelpViewerClient.h" +#import "SPHelpViewerController.h" #import <SPMySQL/SPMySQL.h> @@ -511,6 +512,8 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; // Set the custom query editor's MySQL version [customQueryInstance setMySQLversion:mySQLVersion]; + [helpViewerClientInstance setConnection:mySQLConnection]; + #ifndef SP_CODA [self updateWindowTitle:self]; @@ -2661,6 +2664,11 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; } } +- (SPHelpViewerClient *)helpViewerClient +{ + return helpViewerClientInstance; +} + /** * Is current document Untitled? */ @@ -3602,8 +3610,8 @@ static int64_t SPDatabaseDocumentInstanceCounter = 0; */ - (IBAction)showMySQLHelp:(id)sender { - [customQueryInstance showHelpFor:SP_HELP_TOC_SEARCH_STRING addToHistory:YES calledByAutoHelp:NO]; - [[customQueryInstance helpWebViewWindow] makeKeyWindow]; + [helpViewerClientInstance showHelpFor:SPHelpViewerSearchTOC addToHistory:YES calledByAutoHelp:NO]; + [[helpViewerClientInstance helpWebViewWindow] makeKeyWindow]; } #endif diff --git a/Source/SPHelpViewerClient.h b/Source/SPHelpViewerClient.h new file mode 100644 index 00000000..b169e99f --- /dev/null +++ b/Source/SPHelpViewerClient.h @@ -0,0 +1,66 @@ +// +// SPHelpViewerClient.h +// sequel-pro +// +// Created by Max Lohrmann on 25.05.18. +// Copyright (c) 2018 Max Lohrmann. All rights reserved. +// Parts relocated from existing files. Previous copyright applies. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <https://github.com/sequelpro/sequelpro> + +@class SPHelpViewerController; +@class SPMySQLConnection; +@class MGTemplateEngine; + +/** + * This is the client side of the Help Viewer window, i.e. this class + * can be instantiated from within an xib file as a custom object. + * + * It also contains the logic to look up the help in the mysql database + * using the mySQLConnection (which does not belong into the Help Viewer's + * window controller). + * + * Notifications posted: + * * SPUserClosedHelpViewerNotification + * When the user triggered closing the help viewer window + */ +@interface SPHelpViewerClient : NSObject +{ + SPHelpViewerController *controller; + + NSString *helpHTMLTemplate; + SPMySQLConnection *mySQLConnection; + + MGTemplateEngine *engine; +} + +- (void)setConnection:(SPMySQLConnection *)theConnection; + +- (NSWindow *)helpWebViewWindow; + +- (void)showHelpFor:(NSString *)aString addToHistory:(BOOL)addToHistory calledByAutoHelp:(BOOL)autoHelp; + +// this is not bound in Interface Builder, but used by the SPTextView context menu +- (IBAction)showHelpForCurrentWord:(id)sender; +@end diff --git a/Source/SPHelpViewerClient.m b/Source/SPHelpViewerClient.m new file mode 100644 index 00000000..54d8633d --- /dev/null +++ b/Source/SPHelpViewerClient.m @@ -0,0 +1,262 @@ +// +// SPHelpViewerClient.m +// sequel-pro +// +// Created by Max Lohrmann on 25.05.18. +// Copyright (c) 2018 Max Lohrmann. All rights reserved. +// Parts relocated from existing files. Previous copyright applies. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <https://github.com/sequelpro/sequelpro> + +#import "SPHelpViewerClient.h" +#import "SPHelpViewerController.h" +#import <SPMySQL/SPMySQL.h> +#import "RegexKitLite.h" +#import "MGTemplateEngine.h" +#import "ICUTemplateMatcher.h" + +@interface SPHelpViewerClient () <SPHelpViewerDataSource> + ++ (NSString *)linkToHelpTopic:(NSString *)aTopic; + +- (void)helpViewerClosed:(NSNotification *)notification; + +@end + +@implementation SPHelpViewerClient + +- (instancetype)init +{ + if (self = [super init]) { + controller = [[SPHelpViewerController alloc] init]; + [controller setDataSource:self]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(helpViewerClosed:) name:SPUserClosedHelpViewerNotification object:controller]; + + // init helpHTMLTemplate + NSError *error; + + helpHTMLTemplate = [[NSString alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:SPHTMLHelpTemplate ofType:@"html"] + encoding:NSUTF8StringEncoding + error:&error]; + + // Set up template engine with your chosen matcher + engine = [[MGTemplateEngine alloc] init]; + [engine setMatcher:[ICUTemplateMatcher matcherWithTemplateEngine:engine]]; + + // an error occurred while reading + if (helpHTMLTemplate == nil) { + helpHTMLTemplate = [@"<html><body>{{body}}</body></html>" copy]; //fallback + NSLog(@"%@", [NSString stringWithFormat:@"Error reading “%@.html”!<br>%@", SPHTMLHelpTemplate, [error localizedFailureReason]]); + NSBeep(); + } + } + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [controller setDataSource:nil]; // we are the (unretained) datasource, but the controller may outlive us (if retained by other objects) + [controller close]; // hide the window if it is still visible (can't update anymore without delegate anyway) + + mySQLConnection = nil; + SPClear(controller); + SPClear(helpHTMLTemplate); + SPClear(engine); + [super dealloc]; +} + +- (void)helpViewerClosed:(NSNotification *)notification +{ + //we'll just proxy that notification because outsiders can't/shouldn't access the controller + [[NSNotificationCenter defaultCenter] postNotificationName:SPUserClosedHelpViewerNotification object:self]; +} + +- (void)openOnlineHelpForTopic:(NSString *)searchString +{ + NSString *version = nil; + if(![mySQLConnection serverVersionIsGreaterThanOrEqualTo:4 minorVersion:1 releaseVersion:0]) + version = @"4.1"; + else + version = [NSString stringWithFormat:@"%u.%u",(unsigned int)[mySQLConnection serverMajorVersion], (unsigned int)[mySQLConnection serverMinorVersion]]; + + NSString *url = [[NSString stringWithFormat: + SPMySQLSearchURL, + version, + NSLocalizedString(@"en", @"MySQL search language code - eg in http://search.mysql.com/search?q=select&site=refman-50&lr=lang_en"), + searchString] + stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding]; + + if([url length]) [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; +} + +- (NSString *)HTMLHelpContentsForSearchString:(NSString *)searchString autoHelp:(BOOL)autoHelp +{ + if(![searchString length]) return @""; + + NSMutableString *theTitle = [NSMutableString stringWithFormat:NSLocalizedString(@"Version %@", @"Mysql Help Viewer : window title : mysql server version"),[mySQLConnection serverVersionString]]; + NSMutableString *theHelp = [NSMutableString string]; + + // Don't escape % when being used as a wildcard, but escape it when it's being used by itself. + if ([searchString isEqualToString:@"%"]) searchString = @"\\%"; + + // search via: HELP 'searchString' + SPMySQLResult *theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"HELP %@", [searchString tickQuotedString]]]; + if ([mySQLConnection queryErrored]) { + [theTitle setString:NSLocalizedString(@"Error", @"Mysql Help Viewer : window title : query error")]; + NSString *errMsg = [NSString stringWithFormat:@"ERROR %lu (%@): %@", (unsigned long)[mySQLConnection lastErrorID], [mySQLConnection lastSqlstate], [mySQLConnection lastErrorMessage]]; + [theHelp appendFormat:@"<b>%@:</b><br><p class='error'>%@</p>", NSLocalizedString(@"MySQL Help Query Failed", @"Mysql Help Viewer : title of error message"), errMsg]; + goto generate_help; + } + + // nothing found? + if(![theResult numberOfRows]) { + // try to search via: HELP 'searchString%' + theResult = [mySQLConnection queryString:[NSString stringWithFormat:@"HELP %@", [[searchString stringByAppendingString:@"%"] tickQuotedString]]]; + + // really nothing found? + if(![theResult numberOfRows]) { + [theTitle appendFormat:@": %@", NSLocalizedString(@"No Results", @"Mysql Help Viewer : window title : nothing found")]; + [theHelp appendFormat:@"<em class='nothing'>%@</em>", NSLocalizedString(@"No results found.", @"Mysql Help Viewer : Search : No results")]; + goto generate_help; + } + } + + // Ensure rows are returned as strings to prevent data problems with older 4.1 servers + [theResult setReturnDataAsStrings:YES]; + + NSDictionary *tableDetails = [[NSDictionary alloc] initWithDictionary:[theResult getRowAsDictionary]]; + + if ([tableDetails objectForKey:@"description"]) { // one single help topic found + if ([tableDetails objectForKey:@"name"]) { + [theTitle appendFormat:@": %@", [tableDetails objectForKey:@"name"]]; + [theHelp appendString:@"<h2 class='header'>"]; + [theHelp appendString:[tableDetails objectForKey:@"name"]]; + [theHelp appendString:@"</h2>"]; + } + if ([tableDetails objectForKey:@"description"]) { + NSMutableString *desc = [NSMutableString string]; + NSError *err1 = NULL; + NSString *aUrl; + + [desc setString:[tableDetails objectForKey:@"description"]]; + + //[desc replaceOccurrencesOfString:[searchString uppercaseString] withString:[NSString stringWithFormat:@"<span class='searchstring'>%@</span>", [searchString uppercaseString]] options:NSLiteralSearch range:NSMakeRange(0,[desc length])]; + + // detect and generate http links + NSRange aRange = NSMakeRange(0,0); + NSInteger safeCnt = 0; // safety counter - not more than 200 loops allowed + while(1) { + aRange = [desc rangeOfRegex:@"\\s((https?|ftp|file)://.*?html)" options:RKLNoOptions inRange:NSMakeRange(NSMaxRange(aRange), [desc length]-aRange.location-aRange.length) capture:1 error:&err1]; + if(aRange.location != NSNotFound) { + aUrl = [desc substringWithRange:aRange]; + [desc replaceCharactersInRange:aRange withString:[NSString stringWithFormat:@"<a href='%@'>%@</a>", aUrl, aUrl]]; + } + else { + break; + } + safeCnt++; + if(safeCnt > 200) break; + } + + // Detect and generate cross-links. First, handle the old-style [HELP ...] text. + [desc replaceOccurrencesOfRegex:@"(\\[HELP ([^\\]]*?)\\]" withString:[[self class] linkToHelpTopic:@"$1"]]; + + // Handle "see [...]" and "in [...]"-style 5.x links. + //look-behind won't work here because of the \s+ + [desc replaceOccurrencesOfRegex:@"(See|see|In|in|and)\\s+\\[(?:HELP\\s+)?([^\\]]*?)\\]" withString:[NSString stringWithFormat:@"$1 %@",[[self class] linkToHelpTopic:@"$2"]]]; + + [theHelp appendFormat:@"<pre class='description'>%@</pre>", desc]; + } + // are examples available? + if([tableDetails objectForKey:@"example"]){ + NSString *examples = [[[tableDetails objectForKey:@"example"] copy] autorelease]; + if([examples length]) [theHelp appendFormat:@"<br><i><b>%1$@</b></i><br><pre class='example'>%2$@</pre>",NSLocalizedString(@"Example:",@"Mysql Help Viewer : Help Topic: Example section title"), examples]; + } + } + else { // list all found topics + // check if HELP 'contents' is called + if(![searchString isEqualToString:SPHelpViewerSearchTOC]) { + [theTitle appendString:@": "]; + [theTitle appendFormat:NSLocalizedString(@"Multiple Results for “%@”", @"Mysql Help Viewer : window title : multiple topics found"), searchString]; + [theHelp appendFormat:@"<br><i>%@</i><br>", [NSString stringWithFormat:NSLocalizedString(@"Help topics for “%@”", @"MySQL Help Viewer : Results list : Page title"), searchString]]; + } + else { + [theTitle appendFormat:@": %@", NSLocalizedString(@"Table of Contents", @"Mysql Help Viewer : window title : TOC")]; + [theHelp appendFormat:@"<br><b>%@:</b><br>", NSLocalizedString(@"MySQL Help – Categories", @"mysql help categories")]; + } + + // iterate through all found rows and print them as HTML ul/li list + [theHelp appendString:@"<ul>"]; + [theResult setDefaultRowReturnType:SPMySQLResultRowAsArray]; + for (NSArray *eachRow in theResult) { + NSString *topic = [eachRow objectAtIndex:[eachRow count]-2]; + [theHelp appendFormat:@"<li>%@</li>",[[self class] linkToHelpTopic:topic]]; + } + [theHelp appendString:@"</ul>"]; + } + + [tableDetails release]; + +generate_help: + return [engine processTemplate:helpHTMLTemplate withVariables:@{ + @"title": theTitle, + @"body": theHelp, + }]; +} + ++ (NSString *)linkToHelpTopic:(NSString *)aTopic +{ + NSString *linkTitle = [NSString stringWithFormat:NSLocalizedString(@"Show MySQL help for “%@”", @"MySQL Help Viewer : Results list : Link tooltip"),aTopic]; + return [NSString stringWithFormat:@"<a title='%2$@' href='%1$@' class='internallink'>%1$@</a>", aTopic, linkTitle]; +} + +- (void)setConnection:(SPMySQLConnection *)theConnection +{ + mySQLConnection = theConnection; +} + +/** + * Return the Help window. + */ +- (NSWindow *)helpWebViewWindow +{ + return [controller window]; +} + +- (void)showHelpFor:(NSString *)aString addToHistory:(BOOL)addToHistory calledByAutoHelp:(BOOL)autoHelp +{ + [controller showHelpFor:aString addToHistory:addToHistory calledByAutoHelp:autoHelp]; +} + +/** + * Show the data for "HELP 'currentWord'" + */ +- (IBAction)showHelpForCurrentWord:(id)sender +{ + NSString *searchString = [[sender string] substringWithRange:[sender getRangeForCurrentWord]]; + [controller showHelpFor:searchString addToHistory:YES calledByAutoHelp:NO]; +} + +@end diff --git a/Source/SPHelpViewerController.h b/Source/SPHelpViewerController.h new file mode 100644 index 00000000..c4de0d54 --- /dev/null +++ b/Source/SPHelpViewerController.h @@ -0,0 +1,99 @@ +// +// SPHelpViewerController.h +// sequel-pro +// +// Created by Max Lohrmann on 21.05.18. +// Copyright (c) 2018 Max Lohrmann. All rights reserved. +// Parts relocated from existing files. Previous copyright applies. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <https://github.com/sequelpro/sequelpro> + +@class WebView; + +//private +typedef NS_ENUM(NSUInteger, HelpTarget) { + HelpTargetMySQL = 0, + HelpTargetPage = 1, + HelpTargetWeb = 2, +}; + +NSString * const SPHelpViewerSearchTOC; + +/** + * This notification is posted by the SPHelpViewerController when the user + * triggered closing the help viewer window (or by -performClose:). + * The window is not guaranteed to be off screen already, when the notification is sent. + * + * It will NOT be sent when the window was closed or hidden by code (including app termination). + */ +NSString * const SPUserClosedHelpViewerNotification; + +@protocol SPHelpViewerDataSource <NSObject> + +@required +/** + * When called with a search string this method should open the user's default browser + * with an URL to the MySQL online manual for the page that explains the search string. + */ +- (void)openOnlineHelpForTopic:(NSString *)searchString; + +/** + * This method is called by the SPHelpViewerController when it wants to receive the HTML + * page to display in response to a search string. + * + * The implementation has to handle the magic search string SPHelpViewerSearchTOC to + * return a table of contents document. + */ +- (NSString *)HTMLHelpContentsForSearchString:(NSString *)searchString autoHelp:(BOOL)autoHelp; + +@end + +/** + * This is the window controller class for the MySQL Help Viewer panel. + * + * See SPHelpViewerClient for the class that provides data for this controller and which + * can be instantiated from within an XIB. + * + * - Do NOT instantiate this class from within an XIB. + * - None of the methods in this class are thread-safe - always use the UI thread! + */ +@interface SPHelpViewerController : NSWindowController +{ + IBOutlet WebView *helpWebView; + + IBOutlet NSSearchField *helpSearchField; + IBOutlet NSSearchFieldCell *helpSearchFieldCell; + IBOutlet NSSegmentedControl *helpNavigator; + IBOutlet NSSegmentedControl *helpTargetSelector; + + HelpTarget helpTarget; + + id<SPHelpViewerDataSource> dataSource; +} + +@property (assign, nonatomic) id <SPHelpViewerDataSource> dataSource; + +- (void)showHelpFor:(NSString *)aString addToHistory:(BOOL)addToHistory calledByAutoHelp:(BOOL)autoHelp; + +@end diff --git a/Source/SPHelpViewerController.m b/Source/SPHelpViewerController.m new file mode 100644 index 00000000..a2c13000 --- /dev/null +++ b/Source/SPHelpViewerController.m @@ -0,0 +1,415 @@ +// +// SPHelpViewerController.m +// sequel-pro +// +// Created by Max Lohrmann on 21.05.18. +// Copyright (c) 2018 Max Lohrmann. All rights reserved. +// Parts relocated from existing files. Previous copyright applies. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <https://github.com/sequelpro/sequelpro> + +#import "SPHelpViewerController.h" + +#import <WebKit/WebKit.h> + +NSString * const SPHelpViewerSearchTOC = @"contents"; + +NSString * const SPUserClosedHelpViewerNotification = @"SPUserClosedHelpViewer"; + +typedef NS_ENUM(NSInteger, HelpNavButton) { + HelpNavButtonGoBack = 0, + HelpNavButtonShowTOC = 1, + HelpNavButtonGoForward = 2, +}; + +static void *HelpViewerControllerKVOContext = &HelpViewerControllerKVOContext; + +@interface SPHelpViewerController () <WebPolicyDelegate, WebUIDelegate, NSWindowDelegate> +- (IBAction)showHelpForSearchString:(id)sender; +- (IBAction)helpSegmentDispatcher:(id)sender; +- (IBAction)helpSearchFindNextInPage:(id)sender; +- (IBAction)helpSearchFindPreviousInPage:(id)sender; +- (IBAction)helpTargetDispatcher:(id)sender; +- (IBAction)helpSelectHelpTargetMySQL:(id)sender; +- (IBAction)helpSelectHelpTargetPage:(id)sender; +- (IBAction)helpSelectHelpTargetWeb:(id)sender; + +- (IBAction)showHelpForWebViewSelection:(id)sender; +- (IBAction)searchInDocForWebViewSelection:(id)sender; +- (void)helpTargetValidation; +- (void)updateWindowTitle; +@end + +#pragma mark - + +@implementation SPHelpViewerController + +@synthesize dataSource = dataSource; + +- (instancetype)init +{ + if ((self = [super initWithWindowNibName:@"HelpViewer"])) { + //force window to be loaded for simplicity + [self window]; + } + return self; +} + +- (void)dealloc +{ + [helpWebView removeObserver:self forKeyPath:@"mainFrameTitle"]; //TODO: update to ...context: variant after 10.6 + [super dealloc]; +} + +- (void)windowDidLoad +{ + // init search history + [helpWebView setMaintainsBackForwardList:YES]; + [[helpWebView backForwardList] setCapacity:20]; + + [self updateWindowTitle]; + + [helpWebView addObserver:self forKeyPath:@"mainFrameTitle" options:0 context:HelpViewerControllerKVOContext]; +} + +- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context +{ + if(context == HelpViewerControllerKVOContext) { + if([@"mainFrameTitle" isEqualToString:keyPath] && object == helpWebView) { + [self updateWindowTitle]; + } + } + else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)updateWindowTitle +{ + NSString *title = NSLocalizedString(@"MySQL Help", @"mysql help"); + + NSString *webTitle = [helpWebView mainFrameTitle]; + if([webTitle length]) title = [title stringByAppendingFormat:@" (%@)", webTitle]; + + [[self window] setTitle:title]; +} + +#pragma mark - +#pragma mark MySQL Help + +/** + * Show the data for "HELP 'searchString'". + */ +- (void)showHelpFor:(NSString *)searchString addToHistory:(BOOL)addToHistory calledByAutoHelp:(BOOL)autoHelp +{ + // If there's no search string, ignore if called by autohelp, show the index otherwise + if (![searchString length]) { + if (autoHelp) return; + searchString = SPHelpViewerSearchTOC; + } + + NSString *helpString = [dataSource HTMLHelpContentsForSearchString:searchString autoHelp:autoHelp]; + + // init the Help window if not visible + if(![[self window] isVisible]) { + // init goback/forward buttons + if([[helpWebView backForwardList] backListCount] < 1) { + [helpNavigator setEnabled:NO forSegment:HelpNavButtonGoBack]; + [helpNavigator setEnabled:NO forSegment:HelpNavButtonGoForward]; + } + else { + [helpNavigator setEnabled:([[helpWebView backForwardList] backListCount] != 0) forSegment:HelpNavButtonGoBack]; + [helpNavigator setEnabled:([[helpWebView backForwardList] forwardListCount] != 0) forSegment:HelpNavButtonGoForward]; + } + + // set default to search in MySQL help + helpTarget = HelpTargetMySQL; + [helpTargetSelector setSelectedSegment:HelpTargetMySQL]; + [self helpTargetValidation]; + + // show Help window + [[self window] orderFront:helpWebView]; + } + + 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] != 0) forSegment:HelpNavButtonGoBack]; + [helpNavigator setEnabled:([[helpWebView backForwardList] forwardListCount] != 0) forSegment:HelpNavButtonGoForward]; + + // 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 HelpTargetPage: + if(![helpWebView searchFor:searchString direction:YES caseSensitive:NO wrap:YES]) { + if([searchString length]) NSBeep(); + } + break; + case HelpTargetWeb: + if(![searchString length]) break; + [dataSource openOnlineHelpForTopic:searchString]; + break; + case HelpTargetMySQL: + [self showHelpFor:searchString addToHistory:YES calledByAutoHelp:NO]; + break; + } +} + +/** + * Show the Help for the selected text in the webview + */ +- (IBAction)showHelpForWebViewSelection:(id)sender +{ + [self showHelpFor:[[helpWebView selectedDOMRange] text] addToHistory:YES calledByAutoHelp:NO]; +} + +/** + * 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; + } + [dataSource openOnlineHelpForTopic:searchString]; +} + +/** + * Find Next/Previous in current page + */ +- (IBAction)helpSearchFindNextInPage:(id)sender +{ + if(helpTarget == HelpTargetPage) { + if(![helpWebView searchFor:[[helpSearchField stringValue] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] direction:YES caseSensitive:NO wrap:YES]) NSBeep(); + } +} + +- (IBAction)helpSearchFindPreviousInPage:(id)sender +{ + if(helpTarget == HelpTargetPage) { + 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((HelpNavButton)[helpNavigator selectedSegment]) { + case HelpNavButtonGoBack: + [helpWebView goBack]; + break; + case HelpNavButtonShowTOC: + [self showHelpFor:SPHelpViewerSearchTOC addToHistory:YES calledByAutoHelp:NO]; + break; + case HelpNavButtonGoForward: + [helpWebView goForward]; + break; + } + + // validate goback and goforward buttons according history + [helpNavigator setEnabled:([[helpWebView backForwardList] backListCount] != 0) forSegment:HelpNavButtonGoBack]; + [helpNavigator setEnabled:([[helpWebView backForwardList] forwardListCount] != 0) forSegment:HelpNavButtonGoForward]; +} + +/** + * Set helpTarget according user choice via mouse and keyboard short-cuts. + */ +- (IBAction)helpSelectHelpTargetMySQL:(id)sender +{ + helpTarget = HelpTargetMySQL; + [helpTargetSelector setSelectedSegment:HelpTargetMySQL]; + [self helpTargetValidation]; +} + +- (IBAction)helpSelectHelpTargetPage:(id)sender +{ + helpTarget = HelpTargetPage; + [helpTargetSelector setSelectedSegment:HelpTargetPage]; + [self helpTargetValidation]; +} + +- (IBAction)helpSelectHelpTargetWeb:(id)sender +{ + helpTarget = HelpTargetWeb; + [helpTargetSelector setSelectedSegment:HelpTargetWeb]; + [self helpTargetValidation]; +} + +- (IBAction)helpTargetDispatcher:(id)sender +{ + helpTarget = (HelpTarget)[helpTargetSelector selectedSegment]; + [self helpTargetValidation]; +} + +/** + * Control the help search field behaviour. + */ +- (void)helpTargetValidation +{ + switch(helpTarget) { + case HelpTargetPage: + case HelpTargetWeb: + [helpSearchFieldCell setSendsWholeSearchString:YES]; + break; + case HelpTargetMySQL: + [helpSearchFieldCell setSendsWholeSearchString:NO]; + break; + } +} + +- (BOOL)windowShouldClose:(NSWindow *)sender +{ + // -windowShouldClose: is the only method that will ONLY be invoked when the user closes the window (or by -performClose:) + [[NSNotificationCenter defaultCenter] postNotificationName:SPUserClosedHelpViewerNotification object:self]; + return YES; +} + +#pragma mark - +#pragma mark 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<WebPolicyDecisionListener>)listener +{ + NSInteger navigationType = [[actionInformation objectForKey:WebActionNavigationTypeKey] integerValue]; + + if([[[request URL] scheme] isEqualToString:@"applewebdata"] && navigationType == WebNavigationTypeLinkClicked) { + [self showHelpFor:[[[request URL] path] lastPathComponent] addToHistory:YES calledByAutoHelp:NO]; + [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 calledByAutoHelp:NO]; + [listener ignore]; + } + else { + // Ignore WebNavigationTypeFormSubmitted, WebNavigationTypeFormResubmitted, WebNavigationTypeReload. + [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])) { + NSInteger 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; +} + +@end diff --git a/Source/SPTextView.m b/Source/SPTextView.m index de1ddc04..6b98217c 100644 --- a/Source/SPTextView.m +++ b/Source/SPTextView.m @@ -50,6 +50,7 @@ #import "SPCopyTable.h" #import "SPEditorTokens.h" #import "SPSyntaxParser.h" +#import "SPHelpViewerClient.h" #import <SPMySQL/SPMySQL.h> @@ -1107,7 +1108,7 @@ retry: */ - (IBAction) showMySQLHelpForCurrentWord:(id)sender { - [customQueryInstance showHelpForCurrentWord:self]; + [[tableDocumentInstance helpViewerClient] showHelpForCurrentWord:self]; } #endif |