1/* 2 * R : A Computer Language for Statistical Data Analysis 3 * Copyright (C) 2007 The R Foundation 4 * Copyright (C) 2007--2020 The R Core Team 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, a copy is available at 18 * https://www.R-project.org/Licenses/ 19 * 20 * Cocoa Quartz device module 21 * 22 * This file should be compiled only if AQUA is enabled 23 */ 24 25#include "qdCocoa.h" 26#include "qdPDF.h" /* we use qdPDF for clipboard export and Save As */ 27 28#include <sys/types.h> 29#include <sys/time.h> 30#include <unistd.h> 31 32#include <R.h> 33#include <Rinternals.h> 34#include <R_ext/QuartzDevice.h> 35#include <R_ext/eventloop.h> 36 37/* --- userInfo structure for the CocoaDevice --- */ 38#define histsize 16 39 40struct sQuartzCocoaDevice { 41 QuartzDesc_t qd; 42 QuartzCocoaView *view; 43 NSWindow *window; 44 CGLayerRef layer; /* layer */ 45 CGContextRef layerContext; /* layer context */ 46 CGContextRef context; /* window drawing context */ 47 NSRect bounds; /* set along with context */ 48 BOOL closing; 49 BOOL pdfMode; /* this flag is set when printing, bypassing CGLayer to avoid rasterization */ 50 int inLocator; 51 double locator[2]; /* locaton click position (x,y) */ 52 BOOL inHistoryRecall; 53 int inHistory; 54 SEXP history[histsize]; 55 int histptr; 56 const char *title; 57 QuartzParameters_t pars; /* initial parameters */ 58}; 59 60static QuartzFunctions_t *qf; 61 62#pragma mark --- QuartzCocoaView class --- 63 64@implementation QuartzCocoaView 65 66/* we define them manually so we don't have to deal with GraphicsDevice/GraphicsEngine issues */ 67#define R_RED(col) (((col) )&255) 68#define R_GREEN(col) (((col)>> 8)&255) 69#define R_BLUE(col) (((col)>>16)&255) 70#define R_ALPHA(col) (((col)>>24)&255) 71#define R_RGB(r,g,b) ((r)|((g)<<8)|((b)<<16)|0xFF000000) 72#define R_RGBA(r,g,b,a) ((r)|((g)<<8)|((b)<<16)|((a)<<24)) 73 74- (NSColor *) canvasColor 75{ 76 int canvas = ci->pars.canvas; 77 return [NSColor colorWithCalibratedRed: R_RED(canvas)/255.0 green:R_GREEN(canvas)/255.0 blue:R_BLUE(canvas)/255.0 alpha:R_ALPHA(canvas)/255.0]; 78} 79 80/* can return nil on an error */ 81+ (QuartzCocoaView*) quartzWindowWithRect: (NSRect) rect andInfo: (void*) info 82{ 83 QuartzCocoaDevice *ci = (QuartzCocoaDevice*) info; 84 QuartzCocoaView* view = nil; 85 NSWindow* window = nil; 86 NSColor* canvasColor = nil; 87 88 /* do everything in a try block -- this is not merely theoretical, 89 for example NSWindow will throw an expection when the supplied 90 rect is too big */ 91 @try { 92 view = [[QuartzCocoaView alloc] initWithFrame: rect andInfo: info]; 93 window = [[NSWindow alloc] initWithContentRect: rect 94 styleMask: NSTitledWindowMask|NSClosableWindowMask| 95 NSMiniaturizableWindowMask|NSResizableWindowMask//|NSTexturedBackgroundWindowMask 96 backing:NSBackingStoreBuffered defer:NO]; 97 NSColor *canvasColor = [view canvasColor]; 98 [window setBackgroundColor:canvasColor ? canvasColor : [NSColor colorWithCalibratedRed:1.0 green:1.0 blue:1.0 alpha:0.5]]; 99 [window setOpaque:NO]; 100 ci->window = window; 101 102 [window setDelegate: view]; 103 [window setContentView: view]; 104 [window setInitialFirstResponder: view]; 105 /* [window setAcceptsMouseMovedEvents:YES]; not neeed now, maybe later */ 106 [window setTitle: [NSString stringWithUTF8String: ((QuartzCocoaDevice*)info)->title]]; 107 108 NSMenu *menu, *mainMenu; 109 NSMenuItem *menuItem; 110 /* soleMenu is set if we have no menu at all, so we have to create it. Otherwise we are loading into an application that has already some menu, so we need only our specific stuff. */ 111 BOOL soleMenu = ([NSApp mainMenu] == NULL); 112 113 if (soleMenu) [NSApp setMainMenu:[[NSMenu alloc] init]]; 114 mainMenu = [NSApp mainMenu]; 115 116 /* 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 <Cmd><W> shortcut for "Close Window" by convenience */ 117 BOOL hasFileMenu = NO; 118 if (!soleMenu) { /* in the case of a soleMenu we already know that we don't have it. Otherwise look for it. */ 119 if (!hasFileMenu && [mainMenu indexOfItemWithTitle:@"File"]) hasFileMenu = YES; /* first shot is cheap - it will succeed if we added the menu ourself */ 120 if (!hasFileMenu && [mainMenu numberOfItems] > 0 && (menuItem = [mainMenu itemAtIndex:0]) && (menu = [menuItem submenu])) { /* potentially a File menu */ 121 int i = 0, n = [menu numberOfItems]; 122 while (i < n) { 123 NSString *ke = [[menu itemAtIndex: i++] keyEquivalent]; 124 if (ke && [ke isEqualToString:@"w"]) { hasFileMenu = YES; break; } 125 } 126 } 127 } 128 if (!hasFileMenu) { /* No file menu? Add it. */ 129 menu = [[NSMenu alloc] initWithTitle:@"File"]; 130 menuItem = [[NSMenuItem alloc] initWithTitle:@"Close Window" action:@selector(performClose:) keyEquivalent:@"w"]; [menu addItem:menuItem]; [menuItem release]; 131 menuItem = [[NSMenuItem alloc] initWithTitle:@"Save" action:@selector(saveDocument:) keyEquivalent:@"s"]; [menu addItem:menuItem]; [menuItem release]; 132 [menu addItem:[NSMenuItem separatorItem]]; 133 menuItem = [[NSMenuItem alloc] initWithTitle:@"Page Setup…" action:@selector(runPageLayout:) keyEquivalent:@"P"]; [menu addItem:menuItem]; [menuItem release]; 134 menuItem = [[NSMenuItem alloc] initWithTitle:@"Print" action:@selector(printDocument:) keyEquivalent:@"p"]; [menu addItem:menuItem]; [menuItem release]; 135 136 menuItem = [[NSMenuItem alloc] initWithTitle:[menu title] action:nil keyEquivalent:@""]; /* the "Quartz" item in the main menu */ 137 [menuItem setSubmenu:menu]; 138 [mainMenu insertItem: menuItem atIndex:0]; 139 } 140 141 /* same trick for Edit */ 142 BOOL hasEditMenu = NO; 143 if (!soleMenu) { /* in the case of a soleMenu we already know that we don't have it. Otherwise look for it. */ 144 if (!hasEditMenu && [mainMenu indexOfItemWithTitle:@"Edit"]) hasEditMenu = YES; /* first shot is cheap - it will succeed if we added the menu ourself */ 145 if (!hasEditMenu && [mainMenu numberOfItems] > 1 && (menuItem = [mainMenu itemAtIndex:1]) && (menu = [menuItem submenu])) { /* potentially a Edit menu */ 146 int i = 0, n = [menu numberOfItems]; 147 while (i < n) { 148 NSString *ke = [[menu itemAtIndex: i++] keyEquivalent]; 149 if (ke && [ke isEqualToString:@"c"]) { hasEditMenu = YES; break; } 150 } 151 } 152 } 153 if (!hasEditMenu) { /* We really use just Copy, but we add some more to be consistent with other apps */ 154 menu = [[NSMenu alloc] initWithTitle:@"Edit"]; 155 menuItem = [[NSMenuItem alloc] initWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"]; [menu addItem:menuItem]; [menuItem release]; 156 menuItem = [[NSMenuItem alloc] initWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"]; [menu addItem:menuItem]; [menuItem release]; 157 [menu addItem:[NSMenuItem separatorItem]]; 158 menuItem = [[NSMenuItem alloc] initWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"]; [menu addItem:menuItem]; [menuItem release]; 159 menuItem = [[NSMenuItem alloc] initWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"]; [menu addItem:menuItem]; [menuItem release]; 160 menuItem = [[NSMenuItem alloc] initWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""]; [menu addItem:menuItem]; [menuItem release]; 161 [menu addItem:[NSMenuItem separatorItem]]; 162 menuItem = [[NSMenuItem alloc] initWithTitle:@"Activate" action:@selector(activateQuartzDevice:) keyEquivalent:@"A"]; [menu addItem:menuItem]; [menuItem release]; 163 164 menuItem = [[NSMenuItem alloc] initWithTitle:[menu title] action:nil keyEquivalent:@""]; /* the "Quartz" item in the main menu */ 165 [menuItem setSubmenu:menu]; 166 if ([mainMenu numberOfItems] > 0) 167 [mainMenu insertItem: menuItem atIndex:1]; 168 else /* this should never be the case because we have added "File" menu, but just in case something goes wrong ... */ 169 [mainMenu addItem: menuItem]; 170 } 171 172 if ([mainMenu indexOfItemWithTitle:@"Quartz"] < 0) { /* Quartz menu - if it doesn't exist, add it */ 173 unichar leftArrow = NSLeftArrowFunctionKey, rightArrow = NSRightArrowFunctionKey; 174 menu = [[NSMenu alloc] initWithTitle:@"Quartz"]; 175 menuItem = [[NSMenuItem alloc] initWithTitle:@"Back" action:@selector(historyBack:) keyEquivalent:[NSString stringWithCharacters:&leftArrow length:1]]; [menu addItem:menuItem]; [menuItem release]; 176 menuItem = [[NSMenuItem alloc] initWithTitle:@"Forward" action:@selector(historyForward:) keyEquivalent:[NSString stringWithCharacters:&rightArrow length:1]]; [menu addItem:menuItem]; [menuItem release]; 177 menuItem = [[NSMenuItem alloc] initWithTitle:@"Clear History" action:@selector(historyFlush:) keyEquivalent:@"L"]; [menu addItem:menuItem]; [menuItem release]; 178 179 menuItem = [[NSMenuItem alloc] initWithTitle:[menu title] action:nil keyEquivalent:@""]; /* the "Quartz" item in the main menu */ 180 [menuItem setSubmenu:menu]; 181 182 if (soleMenu) 183 [[NSApp mainMenu] addItem:menuItem]; 184 else { 185 int wmi; /* put us just before the Windows menu if possible */ 186 if ([NSApp windowsMenu] && ((wmi = [[NSApp mainMenu] indexOfItemWithSubmenu: [NSApp windowsMenu]])>=0)) 187 [[NSApp mainMenu] insertItem: menuItem atIndex: wmi]; 188 else 189 [[NSApp mainMenu] addItem:menuItem]; 190 } 191 } 192 if (soleMenu) { /* those should be standard if we have some menu */ 193 menu = [[NSMenu alloc] initWithTitle:@"Window"]; 194 195 menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; [menu addItem:menuItem]; 196 menuItem = [[NSMenuItem alloc] initWithTitle:@"Zoom" action:@selector(performZoom:) keyEquivalent:@""]; [menu addItem:menuItem]; 197 198 /* Add to menubar */ 199 menuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; 200 [menuItem setSubmenu:menu]; 201 [[NSApp mainMenu] addItem:menuItem]; 202 [NSApp setWindowsMenu:menu]; 203 [menu release]; 204 [menuItem release]; 205 } 206 } @catch (NSException *ex) { 207 /* on error release what we know about, issue a warning and return nil */ 208 if (window) { 209 ci->window = nil; 210 [window release]; 211 } 212 if (view) 213 [view release]; 214 if (ex) { 215 /* we don't bother localizing this since the exception is likely in English anyway */ 216 warning("Unable to create Cocoa Quartz window: %s (%s)", 217 [[ex reason] UTF8String], [[ex name] UTF8String]); 218 } 219 return nil; 220 } 221 222 return view; 223} 224 225- (id) initWithFrame: (NSRect) frame andInfo: (void*) info 226{ 227 self = [super initWithFrame: frame]; 228 if (self) { 229 ci = (QuartzCocoaDevice*) info; 230 ci->view = self; 231 ci->closing = NO; 232 ci->inLocator = NO; 233 ci->inHistoryRecall = NO; 234 ci->inHistory = -1; 235 ci->histptr = 0; 236 memset(ci->history, 0, sizeof(ci->history)); 237 } 238 return self; 239} 240 241- (BOOL)isFlipped { return YES; } /* R uses flipped coordinates */ 242 243- (IBAction) activateQuartzDevice:(id) sender 244{ 245 if (qf && ci && ci-> qd) qf->Activate(ci->qd); 246} 247 248- (BOOL) writeAsPDF: (NSString*) fileName 249{ 250 QuartzParameters_t qpar = ci->pars; 251 qpar.file = [fileName UTF8String]; 252 qpar.connection = 0; 253 qpar.parv = NULL; 254 qpar.flags = 0; 255 qpar.width = qf->GetWidth(ci->qd); 256 qpar.height = qf->GetHeight(ci->qd); 257 qpar.canvas = 0; /* disable canvas */ 258 QuartzDesc_t qd = Quartz_C(&qpar, QuartzPDF_DeviceCreate, NULL); 259 if (qd == NULL) return NO; 260 void *ss = qf->GetSnapshot(ci->qd, 0); 261 qf->RestoreSnapshot(qd, ss); 262 qf->Kill(qd); 263 return YES; 264} 265 266- (IBAction) saveDocumentAs: (id) sender 267{ 268 NSSavePanel *sp = [NSSavePanel savePanel]; 269 [sp setRequiredFileType:@"pdf"]; 270 [sp setTitle:@"Save Quartz To PDF File"]; 271 int answer = [sp runModalForDirectory:nil file:@"Rplot.pdf"]; 272 if(answer == NSOKButton) 273 if (![self writeAsPDF:[sp filename]]) NSBeep(); 274} 275 276- (IBAction) saveDocument: (id) sender 277{ 278 [self saveDocumentAs:sender]; 279} 280 281- (IBAction) copy: (id) sender 282{ 283 /* currently we use qdPDF to create the PDF for the clipboard. 284 Now that we have pdfMode we could use it instead, saving some memory ... */ 285 NSPasteboard *pb = [NSPasteboard generalPasteboard]; 286 QuartzParameters_t qpar = ci->pars; 287 qpar.file = 0; 288 qpar.connection = 0; 289 CFMutableDataRef data = CFDataCreateMutable(NULL, 0); 290 if (!data) { NSBeep(); return; } /* cannot copy */ 291 qpar.parv = data; 292 qpar.flags = 0; 293 qpar.width = qf->GetWidth(ci->qd); 294 qpar.height = qf->GetHeight(ci->qd); 295 qpar.canvas = 0; /* have to disable canvas */ 296 /* replay our snapshot and close the PDF device */ 297 QuartzDesc_t qd = Quartz_C(&qpar, QuartzPDF_DeviceCreate, NULL); 298 if (qd == NULL) { 299 CFRelease(data); 300 NSBeep(); 301 return; 302 } 303 void *ss = qf->GetSnapshot(ci->qd, 0); 304 qf->RestoreSnapshot(qd, ss); 305 qf->Kill(qd); 306 /* the result should be in the data by now */ 307 [pb declareTypes: [NSArray arrayWithObjects: NSPDFPboardType, nil ] owner:nil]; 308 [pb setData: (NSMutableData*) data forType:NSPDFPboardType]; 309 CFRelease(data); 310} 311 312- (IBAction)printDocument:(id)sender 313{ 314 NSPrintInfo *printInfo; 315 NSPrintOperation *printOp; 316 317 printInfo = [[NSPrintInfo alloc] initWithDictionary: [[NSPrintInfo sharedPrintInfo] dictionary]]; 318 [printInfo setHorizontalPagination: NSFitPagination]; 319 [printInfo setVerticalPagination: NSAutoPagination]; 320 [printInfo setVerticallyCentered:NO]; 321 322 ci->pdfMode = YES; 323 @try { 324 printOp = [NSPrintOperation printOperationWithView:self 325 printInfo:printInfo]; 326 [printOp setShowsPrintPanel:YES]; 327 [printOp setShowsProgressPanel:NO]; 328 [printOp runOperation]; 329 } 330 @catch (NSException *ex) {} 331 ci->pdfMode = NO; 332} 333 334- (void)drawRect:(NSRect)aRect 335{ 336 CGRect rect; 337 CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; 338 /* we have to retain our copy, beause we may need to create a layer 339 based on the context in NewPage outside of drawRect: */ 340 if (ci->context != ctx) { 341 if (ci->context) 342 CGContextRelease(ci->context); 343 CGContextRetain(ctx); 344 } 345 ci->context = ctx; 346 ci->bounds = [self bounds]; 347 rect = CGRectMake(0.0, 0.0, ci->bounds.size.width, ci->bounds.size.height); 348 349 if (ci->pdfMode) { 350 qf->ReplayDisplayList(ci->qd); 351 return; 352 } 353 354 /* Rprintf("drawRect, ctx=%p, bounds=(%f x %f)\n", ctx, ci->bounds.size.width, ci->bounds.size.height); */ 355 if (!ci->layer) { 356 CGSize size = CGSizeMake(ci->bounds.size.width, ci->bounds.size.height); 357 /* Rprintf(" - have no layer, creating one (%f x %f)\n", ci->bounds.size.width, ci->bounds.size.height); */ 358 ci->layer = CGLayerCreateWithContext(ctx, size, 0); 359 ci->layerContext = CGLayerGetContext(ci->layer); 360 qf->ResetContext(ci->qd); 361 if (ci->inHistoryRecall && ci->inHistory >= 0) { 362 qf->RestoreSnapshot(ci->qd, ci->history[ci->inHistory]); 363 ci->inHistoryRecall = NO; 364 } else 365 qf->ReplayDisplayList(ci->qd); 366 } else { 367 CGSize size = CGLayerGetSize(ci->layer); 368 /* Rprintf(" - have layer %p\n", ci->layer); */ 369 if (size.width != rect.size.width || size.height != rect.size.height) { /* resize */ 370 /* Rprintf(" - but wrong size (%f x %f vs %f x %f; drawing scaled version\n", size.width, size.height, rect.size.width, rect.size.height); */ 371 372 /* if we are in live resize, skip this all */ 373 if (![self inLiveResize]) { 374 /* first draw a rescaled version */ 375 CGContextDrawLayerInRect(ctx, rect, ci->layer); 376 /* release old layer */ 377 CGLayerRelease(ci->layer); 378 ci->layer = 0; 379 ci->layerContext = 0; 380 /* set size */ 381 qf->SetScaledSize(ci->qd, ci->bounds.size.width, ci->bounds.size.height); 382 /* issue replay */ 383 if (ci->inHistoryRecall && ci->inHistory >= 0) { 384 qf->RestoreSnapshot(ci->qd, ci->history[ci->inHistory]); 385 ci->inHistoryRecall = NO; 386 } else 387 qf->ReplayDisplayList(ci->qd); 388 } 389 } 390 } 391 if ([self inLiveResize]) CGContextSetAlpha(ctx, 0.6); 392 if (ci->layer) 393 CGContextDrawLayerInRect(ctx, rect, ci->layer); 394 if ([self inLiveResize]) CGContextSetAlpha(ctx, 1.0); 395} 396 397- (void)mouseDown:(NSEvent *)theEvent 398{ 399 if (ci->inLocator) { 400 NSPoint pt = [theEvent locationInWindow]; 401 unsigned int mf = [theEvent modifierFlags]; 402 ci->locator[0] = pt.x; 403 ci->locator[1] = pt.y; 404 /* Note: we still use menuForEvent: because no other events than left click get here ..*/ 405 if (mf&(NSControlKeyMask|NSRightMouseDownMask|NSOtherMouseDownMask)) 406 ci->locator[0] = -1.0; 407 ci->inLocator = NO; 408 } 409} 410 411/* right-click does NOT generate mouseDown: events, sadly, so we have to (ab)use menuForEvent: */ 412- (NSMenu *)menuForEvent:(NSEvent *)theEvent 413{ 414 if (ci->inLocator) { 415 ci->locator[0] = -1.0; 416 ci->inLocator = NO; 417 return nil; 418 } 419 return [super menuForEvent:theEvent]; 420} 421 422/* <Esc> is caught before so keyDown: won't work */ 423- (BOOL)performKeyEquivalent:(NSEvent *)theEvent 424{ 425 if (ci->inLocator && [theEvent keyCode] == 53 /* ESC - can't find the proper constant for this */) { 426 ci->locator[0] = -1.0; 427 ci->inLocator = NO; 428 return TRUE; 429 } 430 return FALSE; 431} 432 433static void QuartzCocoa_SaveHistory(QuartzCocoaDevice *ci, int last) { 434 SEXP ss = (SEXP) qf->GetSnapshot(ci->qd, last); 435 if (ss) { /* ss will be NULL if there is no content, e.g. during the first call */ 436 R_PreserveObject(ss); 437 if (ci->inHistory != -1) { /* if we are editing an existing snapshot, replace it */ 438 /* Rprintf("(updating plot in history at %d)\n", ci->inHistory); */ 439 if (ci->history[ci->inHistory]) R_ReleaseObject(ci->history[ci->inHistory]); 440 ci->history[ci->inHistory] = ss; 441 } else { 442 /* Rprintf("(adding plot to history at %d)\n", ci->histptr); */ 443 if (ci->history[ci->histptr]) R_ReleaseObject(ci->history[ci->histptr]); 444 ci->history[ci->histptr++] = ss; 445 ci->histptr &= histsize - 1; 446 } 447 } 448} 449 450- (void)historyBack: (id) sender 451{ 452 int hp = ci->inHistory - 1; 453 if (ci->inHistory == -1) 454 hp = (ci->histptr - 1); 455 hp &= histsize - 1; 456 if (hp == ci->histptr || !ci->history[hp]) 457 return; 458 if (qf->GetDirty(ci->qd)) /* save the current snapshot if it is dirty */ 459 QuartzCocoa_SaveHistory(ci, 0); 460 ci->inHistory = hp; 461 ci->inHistoryRecall = YES; 462 /* Rprintf("(activating history entry %d) ", hp); */ 463 /* get rid of the current layer and force a repaint which will fetch the right entry */ 464 CGLayerRelease(ci->layer); 465 ci->layer = 0; 466 ci->layerContext = 0; 467 [self setNeedsDisplay:YES]; 468} 469 470- (void)historyForward: (id) sender 471{ 472 int hp = ci->inHistory + 1; 473 if (ci->inHistory == -1) return; 474 hp &= histsize - 1; 475 if (hp == ci->histptr || !ci->history[hp]) /* we can't really get past the last entry */ 476 return; 477 if (qf->GetDirty(ci->qd)) /* save the current snapshot if it is dirty */ 478 QuartzCocoa_SaveHistory(ci, 0); 479 480 ci->inHistory = hp; 481 /* Rprintf("(activating history entry %d)\n", hp); */ 482 ci->inHistoryRecall = YES; 483 484 CGLayerRelease(ci->layer); 485 ci->layer = 0; 486 ci->layerContext = 0; 487 [self setNeedsDisplay:YES]; 488} 489 490- (void)historyFlush: (id) sender 491{ 492 int i = 0; 493 ci->inHistory = -1; 494 ci->inHistoryRecall = NO; 495 ci->histptr = 0; 496 while (i < histsize) { 497 if (ci->history[i]) { 498 R_ReleaseObject(ci->history[i]); 499 ci->history[i]=0; 500 } 501 i++; 502 } 503} 504 505- (void)viewDidEndLiveResize 506{ 507 [self setNeedsDisplay: YES]; 508} 509 510- (void)windowWillClose:(NSNotification *)aNotification { 511 if (ci) { 512 ci->closing = YES; 513 qf->Kill(ci->qd); 514 } 515} 516 517- (void)resetCursorRects 518{ 519 if (ci->inLocator) 520 [self addCursorRect:[self bounds] cursor:[NSCursor crosshairCursor]]; 521} 522 523@end 524 525#pragma mark --- Cocoa event loop --- 526 527/* --- Cocoa event loop 528 This EL is enabled upon the first use of Quartz or alternatively using 529 the QuartzCocoa_SetupEventLoop function */ 530 531static BOOL el_active = YES; /* the worker thread work until this is NO */ 532static BOOL el_fired = NO; /* flag set when an event was fired */ 533static int el_ofd, el_ifd; /* communication file descriptors */ 534static unsigned long el_sleep; /* latency in ms */ 535static long el_serial = 0; /* serial number for the time slice */ 536static long el_pe_serial = 0; /* ProcessEvents serial number, event are 537 only when the serial number changes */ 538static BOOL el_inhibit = NO; /* this flag is used by special code that 539 needs to inhibit running the event loop */ 540 541/* helper function - sleep X milliseconds */ 542static void millisleep(unsigned long tout) { 543 struct timeval tv; 544 tv.tv_usec = (tout%1000)*1000; 545 tv.tv_sec = tout/1000; 546 select(0, 0, 0, 0, &tv); 547} 548 549/* from aqua.c */ 550extern void (*ptr_R_ProcessEvents)(void); 551/* from Defn.h */ 552extern Rboolean R_isForkedChild; 553 554static void cocoa_process_events() { 555 /* this is a precaution if cocoa_process_events is called 556 via R_ProcessEvents and the R code calls it too often */ 557 if (!R_isForkedChild && !el_inhibit && el_serial != el_pe_serial) { 558 NSEvent *event; 559 while ((event = [NSApp nextEventMatchingMask:NSAnyEventMask 560 untilDate:nil 561 inMode:NSDefaultRunLoopMode 562 dequeue:YES])) 563 [NSApp sendEvent:event]; 564 el_pe_serial = el_serial; 565 } 566} 567 568static void input_handler(void *data) { 569 char buf[16]; 570 571 read(el_ifd, buf, 16); 572 cocoa_process_events(); 573 el_fired = NO; 574} 575 576@interface ELThread : NSObject 577- (int) eventsThread: (id) args; 578@end 579 580@implementation ELThread 581- (int) eventsThread: (id) arg 582{ 583 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 584 char buf[16]; 585 586 while (el_active) { 587 millisleep(el_sleep); 588 el_serial++; 589 if (!el_fired) { 590 el_fired = YES; *buf=0; 591 write(el_ofd, buf, 1); 592 } 593 } 594 595 [pool release]; 596 return 0; 597} 598@end 599 600static ELThread* el_obj = nil; 601 602/* setup Cocoa event loop */ 603void QuartzCocoa_SetupEventLoop(int flags, unsigned long latency) { 604 if (!el_obj) { 605 int fds[2]; 606 pipe(fds); 607 el_ifd = fds[0]; 608 el_ofd = fds[1]; 609 610 if (flags&QCF_SET_PEPTR) 611 ptr_R_ProcessEvents = cocoa_process_events; 612 613 el_sleep = latency; 614 615 addInputHandler(R_InputHandlers, el_ifd, &input_handler, 31); 616 617 el_obj = [[ELThread alloc] init]; 618 [NSThread detachNewThreadSelector:@selector(eventsThread:) toTarget:el_obj withObject:nil]; 619 } 620 if (flags&QCF_SET_FRONT) { 621 void CPSEnableForegroundOperation(ProcessSerialNumber* psn); 622 ProcessSerialNumber myProc, frProc; 623 Boolean sameProc; 624 625 if (GetFrontProcess(&frProc) == noErr) { 626 if (GetCurrentProcess(&myProc) == noErr) { 627 if (SameProcess(&frProc, &myProc, &sameProc) == noErr && !sameProc) { 628 CPSEnableForegroundOperation(&myProc); 629 } 630 SetFrontProcess(&myProc); 631 } 632 } 633 } 634 635} 636 637/* set Cocoa event loop latency in ms */ 638int QuartzCocoa_SetLatency(unsigned long latency) { 639 el_sleep = latency; 640 return (el_obj)?YES:NO; 641} 642 643/* inhibit Cocoa from running the event loop (e.g., when R is forked) */ 644void QuartzCocoa_InhibitEventLoop(int flag) { 645 el_inhibit = flag ? YES : NO; 646} 647 648#pragma mark --- R Quartz interface --- 649 650/*----- R Quartz interface ------*/ 651 652static int cocoa_initialized = 0; 653static NSAutoreleasePool *global_pool = 0; 654 655static void initialize_cocoa() { 656 /* check embedding parameters to see if Rapp (or other Cocoa app) didn't do the work for us */ 657 int eflags = 0; 658 if (qf) { 659 int *p_eflags = (int*) qf->GetParameter(NULL, QuartzParam_EmbeddingFlags); 660 if (p_eflags) eflags = p_eflags[0]; 661 } 662 if ((eflags & QP_Flags_CFLoop) && (eflags & QP_Flags_Cocoa) && (eflags & QP_Flags_Front)) { 663 cocoa_initialized = 1; 664 return; 665 } 666 667 NSApplicationLoad(); 668 global_pool = [[NSAutoreleasePool alloc] init]; 669 if (eflags & QP_Flags_CFLoop) { 670 cocoa_initialized = 1; 671 return; 672 } 673 674 if (!ptr_R_ProcessEvents) 675 QuartzCocoa_SetupEventLoop(QCF_SET_PEPTR|QCF_SET_FRONT, 100); 676 677 678 [NSApplication sharedApplication]; 679 cocoa_process_events(); 680 cocoa_initialized = 1; 681} 682 683static CGContextRef QuartzCocoa_GetCGContext(QuartzDesc_t dev, void *userInfo) { 684 QuartzCocoaDevice *qd = (QuartzCocoaDevice*)userInfo; 685 return qd->pdfMode ? qd->context : qd->layerContext; 686} 687 688static void QuartzCocoa_Close(QuartzDesc_t dev,void *userInfo) { 689 QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; 690 691 /* cancel any locator events */ 692 ci->inLocator = NO; 693 ci->locator[0] = -1.0; 694 695 /* release all history objects */ 696 ci->inHistory = -1; 697 ci->inHistoryRecall = NO; 698 ci->histptr = 0; 699 { 700 int i = 0; 701 while (i < histsize) { 702 if (ci->history[i]) { 703 R_ReleaseObject(ci->history[i]); 704 ci->history[i] = 0; 705 } 706 i++; 707 } 708 } 709 710 if (ci->pars.family) free((void*)ci->pars.family); 711 if (ci->pars.title) free((void*)ci->pars.title); 712 if (ci->pars.file) free((void*)ci->pars.file); 713 714 if (ci->layer) 715 CGLayerRelease(ci->layer); 716 717 /* release context (if we had one) */ 718 if (ci->context) { 719 CGContextRelease(ci->context); 720 ci->context = 0; 721 } 722 723 /* close the window (if it's not already closing) */ 724 if (ci && ci->view && !ci->closing) 725 [[ci->view window] close]; 726 727 if (ci->view) [ci->view release]; /* this is our own release, the window should still have a copy */ 728 if (ci->window) [ci->window release]; /* that should close it all */ 729 ci->view = nil; 730 ci->window = nil; 731} 732 733static int QuartzCocoa_Locator(QuartzDesc_t dev, void* userInfo, double *x, double*y) { 734 QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; 735 736 if (!ci || !ci->view || ci->inLocator) return FALSE; 737 738 ci->locator[0] = -1.0; 739 ci->inLocator = YES; 740 [[ci->view window] invalidateCursorRectsForView: ci->view]; 741 742 while (ci->inLocator && !ci->closing) { 743 NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask 744 untilDate:[NSDate dateWithTimeIntervalSinceNow:0.2] 745 inMode:NSDefaultRunLoopMode 746 dequeue:YES]; 747 if (event) [NSApp sendEvent:event]; 748 } 749 [[ci->view window] invalidateCursorRectsForView: ci->view]; 750 *x = ci->locator[0]; 751 *y = ci->bounds.size.height - ci->locator[1]; 752 return (*x >= 0.0)?TRUE:FALSE; 753} 754 755static void QuartzCocoa_NewPage(QuartzDesc_t dev,void *userInfo, int flags) { 756 QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; 757 if (!ci) return; 758 if (ci->pdfMode) { 759 if (ci->context) 760 qf->ResetContext(dev); 761 return; 762 } 763 if ((flags&QNPF_REDRAW)==0) { /* no redraw -> really new page */ 764 QuartzCocoa_SaveHistory(ci, 1); 765 ci->inHistory = -1; 766 } 767 if (ci->layer) { 768 CGLayerRelease(ci->layer); 769 ci->layer = 0; 770 ci->layerContext = 0; 771 } 772 if (ci->context) { 773 CGSize size = CGSizeMake(ci->bounds.size.width, ci->bounds.size.height); 774 ci->layer = CGLayerCreateWithContext(ci->context, size, 0); 775 ci->layerContext = CGLayerGetContext(ci->layer); 776 qf->ResetContext(dev); 777 /* Rprintf(" - creating new layer (%p - ctx: %p, %f x %f)\n", ci->layer, ci->layerContext, size.width, size.height); */ 778 } 779} 780 781static void QuartzCocoa_Sync(QuartzDesc_t dev,void *userInfo) { 782 QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; 783 if (!ci || !ci->view || ci->pdfMode) return; 784 /* we have to force display now, enqueuing it on the event loop 785 * via setNeedsDisplay: YES has issues since dev.flush() won't 786 * be synchronous and thus animation using dev.flush(); dev.hold() 787 * will break by the time the event loop is run */ 788 [ci->view display]; 789} 790 791static void QuartzCocoa_State(QuartzDesc_t dev, void *userInfo, int state) { 792 QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; 793 NSString *title; 794 if (!ci || !ci->view) return; 795 if (!ci->title) ci->title=strdup("Quartz %d"); 796 title = [NSString stringWithFormat: [NSString stringWithUTF8String: ci->title], qf->DevNumber(dev)]; 797 if (state) title = [title stringByAppendingString: @" [*]"]; 798 [[ci->view window] setTitle: title]; 799} 800 801static void* QuartzCocoa_Cap(QuartzDesc_t dev, void *userInfo) { 802 QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; 803 SEXP raster = R_NilValue; 804 805 if (!ci || !ci->view) { 806 return (void*) raster; 807 } else { 808 unsigned int i, pixels, stride, j = 0; 809 unsigned int *rint; 810 SEXP dim; 811 NSSize size = [ci->view frame].size; 812 pixels = size.width * size.height; 813 814 // make sure the view is up-to-date (fix for PR#14260) 815 [ci->view display]; 816 817 if (![ci->view canDraw]) 818 warning("View not able to draw!?"); 819 820 [ci->view lockFocus]; 821 NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] 822 initWithFocusedViewRect: 823 NSMakeRect(0, 0, 824 size.width, size.height)]; 825 826 int bpp = (int) [rep bitsPerPixel]; 827 int spp = (int) [rep samplesPerPixel]; 828 NSBitmapFormat bf = [rep bitmapFormat]; 829 /* Rprintf("format: bpp=%d, bf=0x%x, bps=%d, spp=%d, planar=%s, colorspace=%s\n", bpp, (int) bf, [rep bitsPerSample], spp, [rep isPlanar] ? "YES" : "NO", [[rep colorSpaceName] UTF8String]); */ 830 /* we only support meshed (=interleaved) formats of 8 bits/component with 3 or 4 components. We should really check for RGB/RGBA as well.. */ 831 if ([rep isPlanar] || [rep bitsPerSample] != 8 || (bf & NSFloatingPointSamplesBitmapFormat) || (bpp != 24 && bpp != 32)) { 832 warning("Unsupported image format"); 833 return (void*) raster; 834 } 835 836 unsigned char *screenData = [rep bitmapData]; 837 838 PROTECT(raster = allocVector(INTSXP, pixels)); 839 840 /* FIXME: the current implementation of rasters seems to be endianness-dependent which is deadly (whether that is intentional or not). It needs to be fixed before it can work properly. The code below is sort of ok in little-endian machines, but the resulting raster is interpreted wrongly on big-endian machines. This needs to be discussed with Paul as all details are missing from his write-up... */ 841 /* Copy each byte of screen to an R matrix. 842 * The ARGB32 needs to be converted to an R ABGR32 */ 843 rint = (unsigned int *) INTEGER(raster); 844 stride = (bpp == 24) ? 3 : 4; /* convers bpp to stride in bytes */ 845 846 if (bf & NSAlphaFirstBitmapFormat) /* ARGB */ 847 for (i = 0; i < pixels; i++, j += stride) 848 rint[i] = R_RGBA(screenData[j + 1], screenData[j + 2], screenData[j + 3], screenData[j]); 849 else if (spp == 4) /* RGBA */ 850 for (i = 0; i < pixels; i++, j += stride) 851 rint[i] = R_RGBA(screenData[j], screenData[j + 1], screenData[j + 2], screenData[j + 3]); 852 else /* RGB */ 853 for (i = 0; i < pixels; i++, j += stride) 854 rint[i] = R_RGB(screenData[j + 0], screenData[j + 1], screenData[j + 2]); 855 856 [rep release]; 857 858 PROTECT(dim = allocVector(INTSXP, 2)); 859 INTEGER(dim)[0] = size.height; 860 INTEGER(dim)[1] = size.width; 861 setAttrib(raster, R_DimSymbol, dim); 862 863 UNPROTECT(2); 864 865 [ci->view unlockFocus]; 866 } 867 868 return (void *) raster; 869} 870 871QuartzDesc_t QuartzCocoa_DeviceCreate(void *dd, QuartzFunctions_t *fn, QuartzParameters_t *par) 872{ 873 QuartzDesc_t qd; 874 double *dpi = par->dpi, width = par->width, height = par->height; 875 double mydpi[2] = { 72.0, 72.0 }; 876 double scalex = 1.0, scaley = 1.0; 877 QuartzCocoaDevice *dev; 878 879 if (!qf) qf = fn; 880 881 { /* check whether we have access to a display at all */ 882 CGDisplayCount dcount = 0; 883 CGGetOnlineDisplayList(255, NULL, &dcount); 884 if (dcount < 1) { 885 warning("No displays are available"); 886 return NULL; 887 } 888 } 889 890 if (!dpi) { 891 CGDirectDisplayID md = CGMainDisplayID(); 892 if (md) { 893 CGSize ds = CGDisplayScreenSize(md); 894 double width = (double)CGDisplayPixelsWide(md); 895 double height = (double)CGDisplayPixelsHigh(md); 896 /* landscape screen, portrait resolution -> rotated screen */ 897 if (ds.width > ds.height && width < height) { 898 mydpi[0] = width / ds.height * 25.4; 899 mydpi[1] = height / ds.width * 25.4; 900 } else { 901 mydpi[0] = width / ds.width * 25.4; 902 mydpi[1] = height / ds.height * 25.4; 903 } 904 /* Rprintf("screen resolution %f x %f\n", mydpi[0], mydpi[1]); */ 905 } 906 dpi = mydpi; 907 } 908 909 scalex = dpi[0] / 72.0; 910 scaley = dpi[1] / 72.0; 911 912 if (width * height > 20736.0) { 913 warning("Requested on-screen area is too large (%.1f by %.1f inches).", width, height); 914 return NULL; 915 } 916 917 dev = malloc(sizeof(QuartzCocoaDevice)); 918 if (dev == NULL) error("allocation failure in QuartzCocoa_DeviceCreate"); 919 memset(dev, 0, sizeof(QuartzCocoaDevice)); 920 921 QuartzBackend_t qdef = { 922 sizeof(qdef), width, height, scalex, scaley, par->pointsize, 923 par->bg, par->canvas, par->flags | QDFLAG_INTERACTIVE | QDFLAG_DISPLAY_LIST | QDFLAG_RASTERIZED, 924 dev, 925 QuartzCocoa_GetCGContext, 926 QuartzCocoa_Locator, 927 QuartzCocoa_Close, 928 QuartzCocoa_NewPage, 929 QuartzCocoa_State, 930 NULL,/* par */ 931 QuartzCocoa_Sync, 932 QuartzCocoa_Cap, 933 }; 934 935 qd = qf->Create(dd, &qdef); 936 if (!qd) { 937 free(dev); 938 return NULL; 939 } 940 dev->qd = qd; 941 942 /* copy parameters for later */ 943 memcpy(&dev->pars, par, (par->size < sizeof(QuartzParameters_t))? par->size : sizeof(QuartzParameters_t)); 944 if (par->size > sizeof(QuartzParameters_t)) dev->pars.size = sizeof(QuartzParameters_t); 945 /* FIXME: strdup can return NULL */ 946 if (par->family) dev->pars.family = strdup(par->family); 947 if (par->title) dev->pars.title = strdup(par->title); 948 if (par->file) dev->pars.file = strdup(par->file); 949 950 /* we cannot substitute the device number as it is not yet known at this point */ 951 dev->title = strdup(par->title); 952 { 953 NSRect rect = NSMakeRect(20.0, 20.0, /* FIXME: proper position */ 954 qf->GetScaledWidth(qd), qf->GetScaledHeight(qd)); 955 if (!cocoa_initialized) initialize_cocoa(); 956 /* Rprintf("scale=%f/%f; size=%f x %f\n", scalex, scaley, rect.size.width, rect.size.height); */ 957 if (![QuartzCocoaView quartzWindowWithRect: rect andInfo: dev]) { 958 free((char*)dev->title); 959 free(qd); 960 free(dev); 961 return NULL; 962 } 963 } 964 if (dev->view) 965 [[dev->view window] makeKeyAndOrderFront: dev->view]; 966 return qd; 967} 968