Open MichaelChirico opened 4 years ago
Created attachment 1480 [details] Changed/Added files in grDevices
Here is a revised set of new files that addresses several drawbacks with the previous approach.
Instead of programmatically creating the main menu, it is now loaded from a nib file. This is an officially supported way of creating a main menu and should be much more future proof. The nib file is stored in a bundle and could be localized if desired. The makefiles were updated to generate the nib file a xib file. If used the use of the nib obsoletes quite a bit of C code that could be removed. The largest blocks that are candidates for removal are marked with comments.
In addition, this makes the application menu more functional by including both an "About R" menu item and a working "Quit R" menu item. The "About R" menu item could be enhanced by including version/copyright information. I don't know how to get this information through C code. The "Quit R" currently switches to the Terminal (if being used) and displays a message about how to quit R. This could also be enhanced to actually quit and/or show the save workspace image prompt, but I don't know how to do that through C code.
The loading the mani menu also instantiates a new application delegate class, QuartzAppDelegate, which handles the quit and about actions. In addition, it handles the AppleScript code. Checking whether R is run from the terminal is now delayed from Cocoa initialization to subsequent run loop iterations, which removes the startup delay. The compiled show terminal AppleScript is cached so that subsequent uses are cached.
The diff against the current trunk is below:
Index: grDevices/Makefile.in =================================================================== --- grDevices/Makefile.in (revision 63830) +++ grDevices/Makefile.in (working copy) @@ -25,6 +25,8 @@ @$(MKINSTALLDIRS) $(top_builddir)/library/$(pkg) @$(MAKE) mkR1 mkdesc mkdemos instdirs @$(MAKE) mksrc +@BUILD_AQUA_TRUE@ @cp -R $(srcdir)/inst/aqua.bundle $(top_builddir)/library/$(pkg) +@BUILD_AQUA_TRUE@ @cp -R $(srcdir)/src/MainMenu.nib $(top_builddir)/library/$(pkg)/aqua.bundle/Contents/Resources @BYTE_COMPILE_PACKAGES_FALSE@ @$(MAKE) mklazy @BYTE_COMPILE_PACKAGES_TRUE@ @$(MAKE) mklazycomp @$(R_GZIPCMD) -9f $(top_builddir)/library/grDevices/afm/*.afm Index: grDevices/src/Makefile.in =================================================================== --- grDevices/src/Makefile.in (revision 63830) +++ grDevices/src/Makefile.in (working copy) @@ -35,6 +35,7 @@ all: Makefile Makedeps @$(MAKE) Makedeps @$(MAKE) shlib @BUILD_DEVCAIRO_TRUE@ cairodevice +@BUILD_AQUA_TRUE@ @$(MAKE) MainMenu.nib
Makefile: $(srcdir)/Makefile.in $(top_builddir)/config.status @cd $(top_builddir) && $(SHELL) ./config.status $(subdir)/$@ @@ -52,6 +53,9 @@ cairodevice: @(cd cairo && $(MAKE))
+MainMenu.nib: MainMenu.xib + ibtool --compile MainMenu.nib MainMenu.xib + include $(R_HOME)/etc${R_ARCH}/Makeconf include $(top_srcdir)/share/make/shlib.mk LTO = @LTO@ @@ -59,7 +63,7 @@ mostlyclean: clean clean: @-rm -rf .libs _libs
@-rm -f Makedeps .d .o $(SHLIB_EXT) + @-rm -f Makedeps .d .o $(SHLIB_EXT) *.nib @(cd cairo && $(MAKE) clean) distclean: clean @-rm -f Makefile Index: grDevices/src/qdCocoa.h =================================================================== --- grDevices/src/qdCocoa.h (revision 63830) +++ grDevices/src/qdCocoa.h (working copy) @@ -40,7 +40,7 @@
typedef struct sQuartzCocoaDevice QuartzCocoaDevice;
-@interface QuartzCocoaView : NSView
+@interface QuartzCocoaView : NSView
@end
+@interface QuartzAppDelegate : NSObject
Index: grDevices/src/qdCocoa.m =================================================================== --- grDevices/src/qdCocoa.m (revision 63830) +++ grDevices/src/qdCocoa.m (working copy) @@ -30,6 +30,7 @@
+#include
@@ -94,6 +95,7 @@ NSColor *canvasColor = [view canvasColor]; [window setBackgroundColor:canvasColor ? canvasColor : [NSColor colorWithCalibratedRed:1.0 green:1.0 blue:1.0 alpha:0.5]]; [window setOpaque:NO]; + [window setReleasedWhenClosed:NO]; ci->window = window;
[window setDelegate: view];
@@ -110,6 +112,7 @@ if (soleMenu) [NSApp setMainMenu:[[NSMenu alloc] init]]; mainMenu = [NSApp mainMenu];
+ / This code is replaced by the MainMenu nib and can be removed /
/ File menu is tricky - it may have a different name in different localizations. Hence we use a trick - the File menu should be first and have the
if ([mainMenu indexOfItemWithTitle:@"Quartz"] < 0) { /* Quartz menu - if it doesn't exist, add it */
unichar leftArrow = NSLeftArrowFunctionKey, rightArrow = NSRightArrowFunctionKey;
@@ -186,6 +190,7 @@ [[NSApp mainMenu] addItem:menuItem]; } } + / This code is replaced by the MainMenu nib and can be removed / if (soleMenu) { / those should be standard if we have some menu / menu = [[NSMenu alloc] initWithTitle:@"Window"];
@@ -199,6 +204,7 @@
[NSApp setWindowsMenu:menu];
[menu release];
[menuItem release];
+ / End code that can be removed /
}
} @catch (NSException ex) {
/ on error release what we know about, issue a warning and return nil */
@@ -512,6 +518,8 @@
+static NSAutoreleasePool global_pool = 0; + / --- Cocoa event loop This EL is enabled upon the first use of Quartz or alternatively using the QuartzCocoa_SetupEventLoop function / @@ -548,7 +556,12 @@ untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])) + { [NSApp sendEvent:event]; + [NSApp updateWindows]; + [global_pool release]; + global_pool = [[NSAutoreleasePool alloc] init]; + } el_pe_serial = el_serial; } } @@ -638,7 +651,7 @@ /----- R Quartz interface ------*/
static int cocoa_initialized = 0; -static NSAutoreleasePool global_pool = 0; +static NSBundle aquaBundle = nil;
static void initialize_cocoa() { / check embedding parameters to see if Rapp (or other Cocoa app) didn't do the work for us / @@ -652,7 +665,14 @@ return; }
NSApplicationLoad(); + / Initialize a global NSBundle instance for retrieving Mac OS X resources / + NSString bundlePath = [[NSString alloc] initWithFormat:@"%s/library/grDevices/aqua.bundle", R_HomeDir()]; + aquaBundle = [[NSBundle alloc] initWithPath:bundlePath]; + [bundlePath release]; + / Instantiate the main NSMenu and QuartzAppDelegate objects from the MainMenu nib / + NSApplication application = [NSApplication sharedApplication]; + NSNib *mainNib = [[NSNib alloc] initWithNibNamed:@"MainMenu" bundle:aquaBundle]; + [mainNib instantiateNibWithOwner:application topLevelObjects:nil]; global_pool = [[NSAutoreleasePool alloc] init]; if (eflags & QP_Flags_CFLoop) { cocoa_initialized = 1; @@ -663,9 +683,19 @@ QuartzCocoa_SetupEventLoop(QCF_SET_PEPTR|QCF_SET_FRONT, 100);
[NSApplication sharedApplication]; + / Close All seems to be added automatically, remove it because it currently crashes / + NSMenu fileMenu = [[[NSApp mainMenu] itemWithTitle:@"File"] submenu]; + NSMenuItem closeAllMenuItem = [fileMenu itemWithTitle:@"Close All"]; + if (closeAllMenuItem) [fileMenu removeItem:closeAllMenuItem]; + / Set the dock icon to be the R logo / + NSImage iconImage = [[NSImage alloc] initWithContentsOfFile:[aquaBundle pathForResource:@"RLogo" ofType:@"png"]]; + if (iconImage) [NSApp setApplicationIconImage:iconImage]; + [iconImage release]; + [application finishLaunching]; cocoa_process_events(); cocoa_initialized = 1; + / During the next call to the run loop, check if we are running in the Terminal */ + [[NSApp delegate] performSelector:@selector(checkIsRunningInTerminal) withObject:nil afterDelay:0.0]; }
static CGContextRef QuartzCocoa_GetCGContext(QuartzDesc_t dev, void userInfo) {
@@ -937,3 +967,132 @@
[[dev->view window] makeKeyAndOrderFront: dev->view];
return qd;
}
+
+#pragma mark --- Application Delegate ---
+
+@implementation QuartzAppDelegate
+
+- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication )sender {
+
+ printf("\nType 'q()' to quit R.\n> ");
+ fflush(stdout);
+ [self showTerminal:self];
+
+ return NSTerminateCancel;
+}
+
+- (IBAction)showAboutPanel:(id)sender {
+
+ NSImage iconImage = [[NSImage alloc] initWithContentsOfFile:[aquaBundle pathForResource:@"RLogo" ofType:@"png"]];
+
+ NSDictionary optionsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
+ @"R", @"ApplicationName",
+ iconImage, @"ApplicationIcon",
+ nil
+ ];
+
+ [NSApp orderFrontStandardAboutPanelWithOptions:optionsDictionary];
+
+ [iconImage release];
+}
+
+- (void)checkIsRunningInTerminal
+{
+ if (!isatty(STDOUT_FILENO)) return;
+ char tty = ttyname(STDOUT_FILENO);
+
+ NSString appleScriptSource = [[NSString alloc] initWithFormat:
+ @"tell application \"Terminal\"\n"
+ " if it is running then\n"
+ " repeat with theWindow in windows\n"
+ " repeat with theTab in (tabs of theWindow)\n"
+ " if tty of theTab is \"%s\" then\n"
+ " return true\n"
+ " end if\n"
+ " end repeat\n"
+ " end repeat\n"
+ " end if\n"
+ "end tell\n"
+ "return false", tty];
+
+ NSAppleScript appleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource];
+ NSDictionary errorInfo = nil;
+ NSAppleEventDescriptor appleEventDescriptor = [appleScript executeAndReturnError:&errorInfo];
+
+ [appleScriptSource release];
+ [appleScript release];
+
+ if (errorInfo) return;
+
+ if ([appleEventDescriptor booleanValue])
+ {
+ _isRunningInTerminal = YES;
+ NSMenu windowMenu = [[[NSApp mainMenu] itemWithTitle:@"Window"] submenu];
+ NSMenuItem showTerminalMenuItem = [windowMenu itemWithTitle:@"Show Terminal"];
+ [showTerminalMenuItem setTarget:self];
+ [showTerminalMenuItem setAction:@selector(showTerminal:)];
+ }
+}
+
+- (BOOL)isRunningInTerminal
+{
+ return _isRunningInTerminal;
+}
+
+- (NSAppleScript )showTerminalAppleScript
+{
+ if (_showTerminalAppleScript) return _showTerminalAppleScript;
+
+ if (!self.isRunningInTerminal) return nil;
+
+ if (!isatty(STDOUT_FILENO)) return nil;
+ char tty = ttyname(STDOUT_FILENO);
+
+ NSString appleScriptSource = [[NSString alloc] initWithFormat:
+ @"tell application \"Terminal\"\n"
+ " repeat with theWindow in windows\n"
+ " repeat with theTab in (tabs of theWindow)\n"
+ " if tty of theTab is \"%s\" then\n"
+ " if selected tab of theWindow is not theTab then\n"
+ " set selected tab of theWindow to theTab\n"
+ " end if\n"
+ " if index of theWindow is not 1 then\n"
+ " set frontmost of theWindow to true\n"
+ " end if\n"
+ " activate\n"
+ " return true\n"
+ " end if\n"
+ " end repeat\n"
+ " end repeat\n"
+ "end tell\n"
+ "return false", tty];
+
+ _showTerminalAppleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource];
+ NSDictionary errorInfo = nil;
+ if (![_showTerminalAppleScript compileAndReturnError:&errorInfo])
+ {
+ _showTerminalAppleScript = nil;
+ NSLog(@"Error Compiling Show Terminal AppleScript: %@", errorInfo);
+ }
+
+ [appleScriptSource release];
+
+ return _showTerminalAppleScript;
+}
+
+- (IBAction)showTerminal:(id)sender
+{
+ if (self.isRunningInTerminal)
+ {
+ NSDictionary errorInfo = nil;
+ [self.showTerminalAppleScript executeAndReturnError:&errorInfo];
+ }
+}
+
+- (void)dealloc
+{
+ [_showTerminalAppleScript release];
+ [super dealloc];
+}
+
+@end
NA
NA
NA
NA
NA
Created attachment 1479 [details] Changed files in grDevices
Here are a few changes in grDevices to improve the Cocoa/Quartz device appearance/functionality:
Getting a real application menu with About, Hide, Quit, etc. requires loading from an application package, which command line R obviously doesn't do. There are workarounds that call functions that were removed around the time of Mac OS X 10.4. (Google setAppleMenu for more details.) They can still be called in an unsupported fashion, but I don't think it's a good idea to be calling APIs that are now private.
This patch simply adds an empty menu item to the menu bar at index 0. (The File menu used to be at index 0, it is now index 1.) Under OS X 10.8, this results in correct menu names, although it is not guaranteed to work in the future. However, if the behavior changes it will hopefully not be worse than it was before this patch.
The R logo was taken from the R.app icon file. It is a 128x128 pixel PNG file located in a new directory, grDevices/inst/aqua. Makefile.in is set up to only install this directory if aqua is enabled. When a Cocoa/Quartz device is initially created, it finds this directory using R_HomeDir().
This is done using embedded AppleScripts in two class methods. Class (as opposed to instance) methods were used so the menu item is functional even if all Quartz device windows are closed. The menu item is only added if R was run from Terminal.app (as opposed to xterm, ssh, etc. Compiling/running the AppleScript that tests for this causes a small delay in opening the Quartz device. On my laptop this is only noticeable if you compare the old and new code side by side. If this very slight lag is unacceptable, the check could be removed, or potentially delayed until after the run loop starts (but that would increase complexity).
The attached archive has the files modified from the latest version of trunk.
The diff is below:
Index: grDevices/Makefile.in =================================================================== --- grDevices/Makefile.in (revision 63776) +++ grDevices/Makefile.in (working copy) @@ -17,7 +17,7 @@ R_EXE = $(top_builddir)/bin/R --vanilla --slave
RSRC =
LC_COLLATE=C ls $(srcdir)/R/*.R $(srcdir)/R/$(R_OSTYPE)/*.R
-INSTDIRS = afm enc icc +INSTDIRS = afm enc icc @BUILD_AQUA_TRUE@ aqua DEFPKGS = NULLall: Makefile DESCRIPTION Index: grDevices/src/qdCocoa.m =================================================================== --- grDevices/src/qdCocoa.m (revision 63776) +++ grDevices/src/qdCocoa.m (working copy) @@ -30,6 +30,7 @@
include <R.h>
include
+#include
include <R_ext/QuartzDevice.h>
include <R_ext/eventloop.h>
@@ -113,14 +114,25 @@ / File menu is tricky - it may have a different name in different localizations. Hence we use a trick - the File menu should be first and have the shortcut for "Close Window" by convenience /
BOOL hasFileMenu = NO;
if (!soleMenu) { / in the case of a soleMenu we already know that we don't have it. Otherwise look for it. /
if (!hasFileMenu && [mainMenu numberOfItems] > 0 && (menuItem = [mainMenu itemAtIndex:0]) && (menu = [menuItem submenu])) { / potentially a File menu / + if (!hasFileMenu && [mainMenu indexOfItemWithTitle:@"File"] >= 0) hasFileMenu = YES; / first shot is cheap - it will succeed if we added the menu ourself / + if (!hasFileMenu && [mainMenu numberOfItems] > 0 && (menuItem = [mainMenu itemAtIndex:1]) && (menu = [menuItem submenu])) { / potentially a File menu / int i = 0, n = [menu numberOfItems]; while (i < n) { NSString ke = [[menu itemAtIndex: i++] keyEquivalent]; if (ke && [ke isEqualToString:@"w"]) { hasFileMenu = YES; break; } } } + } else { + / Create an empty menu item that becomes the application menu / + menuItem = [[NSMenuItem alloc] initWithTitle:@"R" action:nil keyEquivalent:@""]; + [mainMenu addItem:menuItem]; + [menuItem release]; + / Set the dock icon to be the R logo / + NSString iconPath = [[NSString alloc] initWithFormat:@"%s/library/grDevices/aqua/RLogo.png", R_HomeDir()]; + NSImage iconImage = [[NSImage alloc] initWithContentsOfFile:iconPath]; + if (iconImage) [NSApp setApplicationIconImage:iconImage]; + [iconImage release]; + [iconPath release]; } if (!hasFileMenu) { / No file menu? Add it. */ menu = [[NSMenu alloc] initWithTitle:@"File"]; @@ -132,14 +144,14 @@
[mainMenu insertItem: menuItem atIndex:0]; + [mainMenu insertItem: menuItem atIndex:1]; }
/ same trick for Edit / BOOL hasEditMenu = NO; if (!soleMenu) { / in the case of a soleMenu we already know that we don't have it. Otherwise look for it. /
[mainMenu insertItem: menuItem atIndex:1]; + [mainMenu insertItem: menuItem atIndex:2]; else / this should never be the case because we have added "File" menu, but just in case something goes wrong ... / [mainMenu addItem: menuItem]; } @@ -191,6 +203,11 @@
+ / If R is running within a Terminal window tab, add a menu item to show it / + if ([self canShowTerminal]) { + [menu addItem:[NSMenuItem separatorItem]]; + menuItem = [[NSMenuItem alloc] initWithTitle:@"Show Terminal" action:@selector(showTerminal:) keyEquivalent:@"t"]; [menuItem setTarget:[QuartzCocoaView class]]; [menu addItem:menuItem]; [menuItem release]; + }
@@ -508,6 +525,72 @@ [self addCursorRect:[self bounds] cursor:[NSCursor crosshairCursor]]; }
++ (BOOL)canShowTerminal +{ + if (!isatty(STDOUT_FILENO)) return NO; + char tty = ttyname(STDOUT_FILENO); +
+ NSString appleScriptSource = [[NSString alloc] initWithFormat: + @"tell application \"Terminal\"\n" + " if it is running then\n" + " repeat with theWindow in windows\n" + " repeat with theTab in (tabs of theWindow)\n" + " if tty of theTab is \"%s\" then\n" + " return true\n" + " end if\n" + " end repeat\n" + " end repeat\n" + " end if\n" + "end tell\n" + "return false", tty]; +
+ NSAppleScript appleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource]; + NSDictionary errorInfo = nil; + NSAppleEventDescriptor appleEventDescriptor = [appleScript executeAndReturnError:&errorInfo]; +
+ [appleScriptSource release]; + [appleScript release]; +
+ if (errorInfo) return NO; + return [appleEventDescriptor booleanValue]; +} + ++ (void)showTerminal: (id) sender +{ + if (!isatty(STDOUT_FILENO)) return; + char tty = ttyname(STDOUT_FILENO); +
+ NSString appleScriptSource = [[NSString alloc] initWithFormat: + @"tell application \"Terminal\"\n" + " repeat with theWindow in windows\n" + " repeat with theTab in (tabs of theWindow)\n" + " if tty of theTab is \"%s\" then\n" + " if selected tab of theWindow is not theTab then\n" + " set selected tab of theWindow to theTab\n" + " end if\n" + " if index of theWindow is not 1 then\n" + " set frontmost of theWindow to true\n" + " end if\n" + " activate\n" + " return true\n" + " end if\n" + " end repeat\n" + " end repeat\n" + "end tell\n" + "return false", tty]; +
+ NSAppleScript appleScript = [[NSAppleScript alloc] initWithSource:appleScriptSource]; + NSDictionary *errorInfo = nil; + if ([appleScript compileAndReturnError:&errorInfo]) { + [appleScript executeAndReturnError:&errorInfo]; + } else { + NSLog(@"Error Compiling AppleScript: %@", errorInfo); + } +
+ [appleScriptSource release]; + [appleScript release]; +} + @end
pragma mark --- Cocoa event loop ---
METADATA