//
// $Id$
//
// SPBundleHTMLOutputController.m
// sequel-pro
//
// Created by Hans-Jörg Bibiko on November 22, 2010
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// More info at
#import "SPBundleHTMLOutputController.h"
#import "SPAlertSheets.h"
#import "SPPrintAccessory.h"
#import "SPAppController.h"
#import "SPBundleCommandRunner.h"
@class WebScriptCallFrame;
#pragma mark -
@interface WebView (WebViewPrivate)
- (void) setScriptDebugDelegate:(id) delegate;
@end
@interface WebScriptCallFrame : NSObject
- (id)userInfo;
- (WebScriptCallFrame *)caller;
- (NSString *)functionName;
- (id)exception;
@end
#pragma mark -
@implementation SPBundleHTMLOutputController
@synthesize docTitle;
@synthesize initHTMLSourceString;
@synthesize windowUUID;
@synthesize docUUID;
@synthesize suppressExceptionAlerting;
/**
* Initialisation
*/
- (id)init
{
if ((self = [super initWithWindowNibName:@"BundleHTMLOutput"])) {
[webView setContinuousSpellCheckingEnabled:NO];
[webView setGroupName:@"SequelProBundleHTMLOutput"];
[webView setDrawsBackground:YES];
[webView setEditable:NO];
[webView setShouldCloseWithWindow:YES];
#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1060
[webView setShouldUpdateWhileOffscreen:NO];
#endif
suppressExceptionAlerting = NO;
}
return self;
}
- (NSString *)windowNibName
{
return @"BundleHTMLOutput";
}
- (void)displayHTMLContent:(NSString *)content withOptions:(NSDictionary *)displayOptions
{
[[self window] orderFront:nil];
[self setInitHTMLSourceString:content];
[[webView mainFrame] loadHTMLString:content baseURL:nil];
}
- (void)displayURLString:(NSString *)url withOptions:(NSDictionary *)displayOptions
{
[[self window] makeKeyAndOrderFront:nil];
[[webView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
}
- (id)webView
{
return webView;
}
- (void)updateWindow
{
if (docTitle != nil)
[[webView window] setTitle:docTitle];
else
[[webView window] setTitle:@""];
}
- (BOOL)canMakeTextLarger
{
return YES;
}
- (BOOL)canMakeTextSmaller
{
return YES;
}
- (void)dealloc
{
if(webPreferences) [webPreferences release];
[super dealloc];
}
- (void)keyDown:(NSEvent *)theEvent
{
long allFlags = (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask);
NSString *charactersIgnMod = [theEvent charactersIgnoringModifiers];
long curFlags = ([theEvent modifierFlags] & allFlags);
if(curFlags & NSCommandKeyMask) {
if([charactersIgnMod isEqualToString:@"+"] || [charactersIgnMod isEqualToString:@"="]) // increase text size by 1; ⌘+, ⌘=, and ⌘ numpad +
{
[webView makeTextLarger:nil];
return;
}
if([charactersIgnMod isEqualToString:@"-"]) // decrease text size by 1; ⌘- and numpad -
{
[webView makeTextSmaller:nil];
return;
}
if([charactersIgnMod isEqualToString:@"0"]) // return the text size to the default size
{
[webView makeTextStandardSize:nil];
return;
}
if([theEvent keyCode] == 123) // goBack
{
if([webView canGoBack])
[webView goBack:nil];
else
[[webView mainFrame] loadHTMLString:[self initHTMLSourceString] baseURL:nil];
return;
}
if([theEvent keyCode] == 124) // goForward
{
[webView goForward:nil];
return;
}
}
[super keyDown: theEvent];
}
/**
* Sheet did end method
*/
- (void)sheetDidEnd:(id)sheet returnCode:(NSInteger)returnCode contextInfo:(NSString *)contextInfo
{
// Order out current sheet to suppress overlapping of sheets
if ([sheet respondsToSelector:@selector(orderOut:)])
[sheet orderOut:nil];
else if ([sheet respondsToSelector:@selector(window)])
[[sheet window] orderOut:nil];
if([contextInfo isEqualToString:@"saveDocument"]) {
if (returnCode == NSOKButton) {
NSString *sourceCode = [webView stringByEvaluatingJavaScriptFromString:@"document.getElementsByTagName('html')[0].outerHTML"];
NSError *err = nil;
[sourceCode writeToURL:[sheet URL]
atomically:YES
encoding:NSUTF8StringEncoding
error:&err];
if(err != nil) {
SPBeginAlertSheet(NSLocalizedString(@"Error", @"error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil,
[NSString stringWithFormat:@"%@", [err localizedDescription]]);
}
}
}
}
- (IBAction)printDocument:(id)sender
{
NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];
NSSize paperSize = [printInfo paperSize];
NSRect printableRect = [printInfo imageablePageBounds];
// Calculate page margins
CGFloat marginL = printableRect.origin.x;
CGFloat marginR = paperSize.width - (printableRect.origin.x + printableRect.size.width);
CGFloat marginB = printableRect.origin.y;
CGFloat marginT = paperSize.height - (printableRect.origin.y + printableRect.size.height);
// Make sure margins are symetric and positive
CGFloat marginLR = MAX(0, MAX(marginL, marginR));
CGFloat marginTB = MAX(0, MAX(marginT, marginB));
// Set the margins
[printInfo setLeftMargin:marginLR];
[printInfo setRightMargin:marginLR];
[printInfo setTopMargin:marginTB];
[printInfo setBottomMargin:marginTB];
[printInfo setHorizontalPagination:NSFitPagination];
[printInfo setVerticalPagination:NSFitPagination];
[printInfo setVerticallyCentered:NO];
NSPrintOperation *op = [NSPrintOperation printOperationWithView:[[[webView mainFrame] frameView] documentView] printInfo:printInfo];
// Perform the print operation on a background thread
[op setCanSpawnSeparateThread:YES];
// Add the ability to select the orientation to print panel
NSPrintPanel *printPanel = [op printPanel];
[printPanel setOptions:[printPanel options] + NSPrintPanelShowsOrientation + NSPrintPanelShowsScaling + NSPrintPanelShowsPaperSize];
[op setPrintPanel:printPanel];
SPPrintAccessory *printAccessory = [[SPPrintAccessory alloc] initWithNibName:@"PrintAccessory" bundle:nil];
[printAccessory setPrintView:webView];
[printPanel addAccessoryController:printAccessory];
[[NSPageLayout pageLayout] addAccessoryController:printAccessory];
[printAccessory release];
[op runOperationModalForWindow:[self window]
delegate:self
didRunSelector:nil
contextInfo:nil];
}
- (void)showSourceCode
{
NSString *sourceCode = [webView stringByEvaluatingJavaScriptFromString:@"document.getElementsByTagName('html')[0].outerHTML"];
SPBundleHTMLOutputController *c = [[SPBundleHTMLOutputController alloc] init];
[c displayHTMLContent:[NSString stringWithFormat:@"
%@
", [sourceCode HTMLEscapeString]] withOptions:nil];
[[NSApp delegate] addHTMLOutputController:c];
}
- (void)saveDocument
{
NSSavePanel *panel = [NSSavePanel savePanel];
[panel setAllowedFileTypes:[NSArray arrayWithObject:@"html"]];
[panel setExtensionHidden:NO];
[panel setAllowsOtherFileTypes:YES];
[panel setCanSelectHiddenExtension:YES];
[panel setCanCreateDirectories:YES];
[panel beginSheetForDirectory:nil file:@"output" modalForWindow:[self window] modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:@"saveDocument"];
}
#pragma mark -
- (void)windowWillClose:(NSNotification *)notification
{
[[webView mainFrame] loadHTMLString:@"" baseURL:nil];
[webView close];
[self setInitHTMLSourceString:@""];
windowUUID = @"";
docUUID = @"";
[[NSApp delegate] removeHTMLOutputController:self];
[self release];
}
#pragma mark -
- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
{
NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
[webViewMenuItems addObject:[NSMenuItem separatorItem]];
NSMenuItem *anItem;
anItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"View Source", @"view html source code menu item title") action:@selector(showSourceCode) keyEquivalent:@""];
[anItem setEnabled:YES];
[anItem setTarget:self];
[webViewMenuItems addObject:anItem];
[anItem release];
anItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Save Page As…", @"save page as menu item title") action:@selector(saveDocument) keyEquivalent:@""];
[anItem setEnabled:YES];
[anItem setTarget:self];
[webViewMenuItems addObject:anItem];
[anItem release];
anItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Print Page…", @"print page menu item title") action:@selector(printDocument:) keyEquivalent:@""];
[anItem setEnabled:YES];
[anItem setTarget:self];
[webViewMenuItems addObject:anItem];
[anItem release];
return webViewMenuItems;
}
- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request
{
if(request != nil) {
SPBundleHTMLOutputController *c = [[SPBundleHTMLOutputController alloc] init];
[c displayURLString:[[request URL] absoluteString] withOptions:nil];
[[NSApp delegate] addHTMLOutputController:c];
return [c webView];
}
return nil;
}
- (void)webViewShow:(WebView *)sender
{
id newWebView = [[NSDocumentController sharedDocumentController] documentForWindow:[sender window]];
[newWebView showWindows];
}
- (void)webView:(WebView *)aWebView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener
{
NSInteger navigationType = [[actionInformation objectForKey:WebActionNavigationTypeKey] integerValue];
// sequelpro:// handler
if([[[request URL] scheme] isEqualToString:@"sequelpro"] && navigationType == WebNavigationTypeLinkClicked) {
[[NSApp delegate] handleEventWithURL:[request URL]];
[listener ignore];
}
// sp-reveal-file://a_file_path reveals the file in Finder
else if([[[request URL] scheme] isEqualToString:@"sp-reveal-file"] && navigationType == WebNavigationTypeLinkClicked) {
[[NSWorkspace sharedWorkspace] selectFile:[[[request mainDocumentURL] absoluteString] substringFromIndex:16] inFileViewerRootedAtPath:nil];
[listener ignore];
}
// sp-open-file://a_file_path opens the file with the default
else if([[[request URL] scheme] isEqualToString:@"sp-open-file"] && navigationType == WebNavigationTypeLinkClicked) {
[[NSWorkspace sharedWorkspace] openFile:[[[request mainDocumentURL] absoluteString] substringFromIndex:14]];
[listener ignore];
}
else {
switch(navigationType) {
case WebNavigationTypeLinkClicked:
[[aWebView mainFrame] loadRequest:request];
[listener use];
break;
case WebNavigationTypeReload:
[[aWebView mainFrame] loadHTMLString:[self initHTMLSourceString] baseURL:nil];
break;
default:
[listener use];
}
}
}
- (void)webView:(WebView *)sender didReceiveTitle:(NSString *)title forFrame:(WebFrame *)frame
{
// Only report feedback for the main frame.
if (frame == [sender mainFrame]) {
[self setDocTitle:title];
[self updateWindow];
}
}
- (void)webView:(WebView*)sender didFinishLoadForFrame:(WebFrame*)frame;
{
// Only report feedback for the main frame.
if (frame == [sender mainFrame]) {
[self updateWindow];
}
}
- (void)webView:(WebView *)sender didFailProvisionalLoadWithError:(NSError *)error forFrame:(WebFrame *)frame
{
if(error) {
NSLog(@"%@", [error localizedDescription]);
}
}
- (void)webView:(WebView *)webView didFailLoadWithError:(NSError*)error forFrame:(WebFrame *)frame
{
if(error) {
NSLog(@"%@", [error localizedDescription]);
}
}
#pragma mark -
#pragma mark JS support
- (void)webView:(WebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame
{
NSAlert *alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")];
[alert setInformativeText:(message)?:@""];
[alert setMessageText:@"JavaScript"];
[alert runModal];
[alert release];
}
- (BOOL)webView:(WebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame
{
NSAlert *alert = [[NSAlert alloc] init];
[alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")];
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")];
[alert setInformativeText:(message)?:@""];
[alert setMessageText:@"JavaScript"];
NSUInteger returnCode = [alert runModal];
[alert release];
if(returnCode == NSAlertFirstButtonReturn) return YES;
return NO;
}
- (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject
{
[windowScriptObject setValue:self forKey:@"system"];
[webView setScriptDebugDelegate:self];
}
+ (NSString *)webScriptNameForSelector:(SEL)aSelector
{
if (aSelector == @selector(run:))
return @"run";
if (aSelector == @selector(getShellEnvironmentForName:))
return @"getShellEnvironmentForName";
if (aSelector == @selector(insertText:))
return @"insertText";
if (aSelector == @selector(setText:))
return @"setText";
if (aSelector == @selector(setSelectedTextRange:))
return @"setSelectedTextRange";
if (aSelector == @selector(makeHTMLOutputWindowKeyWindow))
return @"makeHTMLOutputWindowKeyWindow";
if (aSelector == @selector(closeHTMLOutputWindow))
return @"closeHTMLOutputWindow";
if (aSelector == @selector(suppressExceptionAlert))
return @"suppressExceptionAlert";
return @"";
}
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)selector {
if (selector == @selector(run:)) {
return NO;
}
if (selector == @selector(getShellEnvironmentForName:)) {
return NO;
}
if (selector == @selector(insertText:)) {
return NO;
}
if (selector == @selector(setText:)) {
return NO;
}
if (selector == @selector(setSelectedTextRange:)) {
return NO;
}
if (selector == @selector(makeHTMLOutputWindowKeyWindow)) {
return NO;
}
if (selector == @selector(closeHTMLOutputWindow)) {
return NO;
}
if (selector == @selector(suppressExceptionAlert)) {
return NO;
}
return YES;
}
+ (BOOL)isKeyExcludedFromWebScript:(const char *)property {
if (strcmp(property, "run") == 0) {
return NO;
}
if (strcmp(property, "getShellEnvironmentForName") == 0) {
return NO;
}
if (strcmp(property, "insertText") == 0) {
return NO;
}
if (strcmp(property, "setText") == 0) {
return NO;
}
if (strcmp(property, "setSelectedTextRange") == 0) {
return NO;
}
if (strcmp(property, "makeHTMLOutputWindowKeyWindow") == 0) {
return NO;
}
return YES;
}
- (void)webView:(WebView *)webView failedToParseSource:(NSString *)source baseLineNumber:(NSUInteger)lineNumber fromURL:(NSURL *)url withError:(NSError *)error forWebFrame:(WebFrame *)webFrame
{
NSString *mes = [NSString stringWithFormat:@"Failed to parse JavaScript source:\nline = %ld\nerror = %@ with\n%@\nfor source = \n%@", lineNumber, [error localizedDescription], [error userInfo], source];
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"JavaScript Parsing Error", @"javascript parsing error")
defaultButton:NSLocalizedString(@"OK", @"OK button")
alternateButton:nil
otherButton:nil
informativeTextWithFormat:mes];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert runModal];
}
- (void)webView:(WebView *)webView exceptionWasRaised:(WebScriptCallFrame *)frame sourceId:(NSInteger)sid line:(NSInteger)lineno forWebFrame:(WebFrame *)webFrame
{
NSString *mes = [NSString stringWithFormat:@"Exception:\nline = %ld\nfunction = %@\ncaller = %@\nexception = %@", lineno, [frame functionName], [frame caller], [frame userInfo], [frame exception]];
if([self suppressExceptionAlerting]) {
NSLog(@"%@", mes);
return;
}
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"JavaScript Exception", @"javascript exception")
defaultButton:NSLocalizedString(@"OK", @"OK button")
alternateButton:nil
otherButton:nil
informativeTextWithFormat:mes];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert runModal];
}
/**
* JavaScript window.system.getShellEnvironmentForName('a_key') function to
* return the value for key keyName
*/
- (NSString *)getShellEnvironmentForName:(NSString*)keyName
{
return [[[NSApp delegate] shellEnvironmentForDocument:nil] objectForKey:keyName];
}
/**
* JavaScript window.system.makeHTMLOutputWindowKeyWindow() function
* to make the HTML output window the first responder
*/
- (void)makeHTMLOutputWindowKeyWindow
{
[[self window] makeKeyAndOrderFront:nil];
}
/**
* JavaScript window.system.makeHTMLOutputWindowKeyWindow() function
* to close the HTML window
*/
- (void)closeHTMLOutputWindow
{
[[self window] close];
}
/**
* JavaScript window.system.insertText(text) function
* to insert text into the first responder
*/
- (void)insertText:(NSString*)text
{
id firstResponder = [[NSApp keyWindow] firstResponder];
if([firstResponder isKindOfClass:[NSTextView class]]) {
[firstResponder insertText:text];
return;
}
NSBeep();
}
/**
* JavaScript window.system.setText(text) function
* to set the content of the first responder to text
*/
- (void)setText:(NSString*)text
{
id firstResponder = [[NSApp keyWindow] firstResponder];
if([firstResponder isKindOfClass:[NSTextView class]]) {
[firstResponder setSelectedRange:NSMakeRange(0, [[firstResponder string] length])];
[firstResponder insertText:text];
return;
}
NSBeep();
}
/**
* JavaScript window.system.setSelectedRange({location,length}) function
* to set the selection range of the first responder
*/
- (void)setSelectedTextRange:(NSString*)range
{
id firstResponder = [[NSApp keyWindow] firstResponder];
if([firstResponder isKindOfClass:[NSTextView class]]) {
NSRange theRange = NSIntersectionRange(NSRangeFromString(range), NSMakeRange(0, [[firstResponder string] length]));
if(theRange.location != NSNotFound) {
[firstResponder setSelectedRange:theRange];
}
return;
}
NSBeep();
}
/**
* JavaScript window.system.suppressExceptionAlert() function
* to suppress an exception alert, instead write the message to NSLog
*/
- (void)suppressExceptionAlert
{
[self setSuppressExceptionAlerting:YES];
}
/**
* JavaScript window.system.run('a_command'|new Array('a_command', 'uuid')) function
* to return the result of the BASH command a_command
*/
- (NSString *)run:(id)call
{
NSError *err = nil;
NSString *command = nil;
NSString *uuid = nil;
if([self docUUID] && [[self docUUID] length])
uuid = [self docUUID];
if([call isKindOfClass:[NSString class]])
command = [NSString stringWithString:call];
else if([[[call class] description] isEqualToString:@"WebScriptObject"]){
command = [call webScriptValueAtIndex:0];
uuid = [call webScriptValueAtIndex:1];
}
else {
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Error while executing JavaScript BASH command", @"error while executing javascript bash command")
defaultButton:NSLocalizedString(@"OK", @"OK button")
alternateButton:nil
otherButton:nil
informativeTextWithFormat:NSLocalizedString(@"Passed parameter couldn't be interpreted. Only string or array (with 2 elements) are allowed.", @"Passed parameter couldn't be interpreted. Only string or array (with 2 elements) are allowed.")];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert runModal];
return @"";
}
if(!command) return @"No JavaScript command found.";
NSString *output = nil;
if(uuid == nil)
output = [SPBundleCommandRunner runBashCommand:command withEnvironment:nil atCurrentDirectoryPath:nil error:&err];
else {
NSMutableDictionary *theEnv = [NSMutableDictionary dictionary];
[theEnv addEntriesFromDictionary:[[NSApp delegate] shellEnvironmentForDocument:nil]];
[theEnv setObject:uuid forKey:SPBundleShellVariableProcessID];
[theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryInputPathHeader, uuid] forKey:SPBundleShellVariableQueryFile];
[theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryResultPathHeader, uuid] forKey:SPBundleShellVariableQueryResultFile];
[theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryResultStatusPathHeader, uuid] forKey:SPBundleShellVariableQueryResultStatusFile];
[theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryResultMetaPathHeader, uuid] forKey:SPBundleShellVariableQueryResultMetaFile];
output = [SPBundleCommandRunner runBashCommand:command
withEnvironment:theEnv
atCurrentDirectoryPath:nil
callerInstance:[NSApp delegate]
contextInfo:[NSDictionary dictionaryWithObjectsAndKeys:
@"JavaScript", @"name",
NSLocalizedString(@"General", @"general menu item label"), @"scope",
uuid, SPBundleFileInternalexecutionUUID, nil]
error:&err];
}
if(err != nil) {
NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Error while executing JavaScript BASH command", @"error while executing javascript bash command")
defaultButton:NSLocalizedString(@"OK", @"OK button")
alternateButton:nil
otherButton:nil
informativeTextWithFormat:[err localizedDescription]];
[alert setAlertStyle:NSCriticalAlertStyle];
[alert runModal];
return @"";
}
if(output)
return output;
else {
NSLog(@"No valid output for JavaScript command found.");
NSBeep();
return @"";
}
}
#pragma mark -
#pragma mark multi-touch trackpad support
/**
* Trackpad two-finger zooming gesture for in/decreasing the font size
*/
- (void)magnifyWithEvent:(NSEvent *)anEvent
{
if([anEvent deltaZ]>2.0)
[webView makeTextLarger:nil];
else if([anEvent deltaZ]<-2.0)
[webView makeTextSmaller:nil];
}
@end