/* * R : A Computer Language for Statistical Data Analysis * Copyright (C) 2007 The R Foundation * * 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, a copy is available at * http://www.r-project.org/Licenses/ * * Cocoa Quartz device module * */ #include "QuartzView.h" #include #include #include #include #include #include /* --- userInfo structure for the CocoaDevice --- */ #define histsize 16 struct sQuartzCocoaDevice { QuartzDesc_t qd; QuartzView *view; NSWindow *window; CGLayerRef layer; /* layer */ CGContextRef layerContext; /* layer context */ CGContextRef context; /* window drawing context */ NSRect bounds; /* set along with context */ BOOL closing; BOOL pdfMode; /* this flag is set when printing, bypassing CGLayer to avoid rasterization */ int inLocator; double locator[2]; /* locaton click position (x,y) */ BOOL inHistoryRecall; int inHistory; SEXP history[histsize]; int histptr; const char *title; QuartzParameters_t pars; /* initial parameters */ }; static QuartzFunctions_t *qf; #pragma mark --- QuartzView class --- static QuartzView *mainView; @implementation QuartzView /* we define them manually so we don't have to deal with GraphicsDevice/GraphicsEngine issues */ #define R_RED(col) (((col) )&255) #define R_GREEN(col) (((col)>> 8)&255) #define R_BLUE(col) (((col)>>16)&255) #define R_ALPHA(col) (((col)>>24)&255) - (NSColor *) canvasColor { int canvas = ci->pars.canvas; 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]; } - (void) setInfo: (QuartzCocoaDevice*) info { ci = info; ci->view = self; ci->closing = NO; ci->inLocator = NO; ci->inHistoryRecall = NO; ci->inHistory = -1; ci->histptr = 0; memset(ci->history, 0, sizeof(ci->history)); } /* can return nil on an error */ + (QuartzView*) quartzWindowWithRect: (NSRect) rect andInfo: (void*) info { QuartzCocoaDevice *ci = (QuartzCocoaDevice*) info; if (!mainView) return nil; // if there is no view yet, we're out of luck QuartzView *view = mainView; [view setInfo:ci]; return view; } - (id) initWithFrame: (NSRect) frame andInfo: (void*) info { self = [super initWithFrame: frame]; if (self) { ci = (QuartzCocoaDevice*) info; ci->view = self; ci->closing = NO; ci->inLocator = NO; ci->inHistoryRecall = NO; ci->inHistory = -1; ci->histptr = 0; memset(ci->history, 0, sizeof(ci->history)); } return self; } - (void) awakeFromNib { ci = 0; ptr_QuartzBackend = QuartzCocoa_DeviceCreate; mainView = self; } - (BOOL)isFlipped { return YES; } /* R uses flipped coordinates */ - (IBAction) activateQuartzDevice:(id) sender { if (qf && ci && ci-> qd) qf->Activate(ci->qd); } - (void)drawRect:(NSRect)aRect { if (!ci) return; CGRect rect; CGContextRef ctx = [[NSGraphicsContext currentContext] graphicsPort]; ci->context = ctx; ci->bounds = [self bounds]; rect = CGRectMake(0.0, 0.0, ci->bounds.size.width, ci->bounds.size.height); if (ci->pdfMode) { qf->ReplayDisplayList(ci->qd); return; } /* Rprintf("drawRect, ctx=%p, bounds=(%f x %f)\n", ctx, ci->bounds.size.width, ci->bounds.size.height); */ if (!ci->layer) { CGSize size = CGSizeMake(ci->bounds.size.width, ci->bounds.size.height); /* Rprintf(" - have no layer, creating one (%f x %f)\n", ci->bounds.size.width, ci->bounds.size.height); */ ci->layer = CGLayerCreateWithContext(ctx, size, 0); ci->layerContext = CGLayerGetContext(ci->layer); qf->ResetContext(ci->qd); if (ci->inHistoryRecall && ci->inHistory >= 0) { qf->RestoreSnapshot(ci->qd, ci->history[ci->inHistory]); ci->inHistoryRecall = NO; } else qf->ReplayDisplayList(ci->qd); } else { CGSize size = CGLayerGetSize(ci->layer); /* Rprintf(" - have layer %p\n", ci->layer); */ if (size.width != rect.size.width || size.height != rect.size.height) { /* resize */ /* 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); */ /* if we are in live resize, skip this all */ if (![self inLiveResize]) { /* first draw a rescaled version */ CGContextDrawLayerInRect(ctx, rect, ci->layer); /* release old layer */ CGLayerRelease(ci->layer); ci->layer = 0; ci->layerContext = 0; /* set size */ qf->SetScaledSize(ci->qd, ci->bounds.size.width, ci->bounds.size.height); /* issue replay */ if (ci->inHistoryRecall && ci->inHistory >= 0) { qf->RestoreSnapshot(ci->qd, ci->history[ci->inHistory]); ci->inHistoryRecall = NO; } else qf->ReplayDisplayList(ci->qd); } } } if ([self inLiveResize]) CGContextSetAlpha(ctx, 0.6); if (ci->layer) CGContextDrawLayerInRect(ctx, rect, ci->layer); if ([self inLiveResize]) CGContextSetAlpha(ctx, 1.0); } - (void)mouseDown:(NSEvent *)theEvent { if (!ci) return; if (ci->inLocator) { NSPoint pt = [theEvent locationInWindow]; NSUInteger mf = [theEvent modifierFlags]; ci->locator[0] = pt.x; ci->locator[1] = pt.y; /* Note: we still use menuForEvent: because no other events than left click get here ..*/ if (mf&(NSControlKeyMask|NSRightMouseDownMask|NSOtherMouseDownMask)) ci->locator[0] = -1.0; ci->inLocator = NO; } } /* right-click does NOT generate mouseDown: events, sadly, so we have to (ab)use menuForEvent: */ - (NSMenu *)menuForEvent:(NSEvent *)theEvent { if (!ci) return nil; if (ci->inLocator) { ci->locator[0] = -1.0; ci->inLocator = NO; return nil; } return [super menuForEvent:theEvent]; } /* is caught before so keyDown: won't work */ - (BOOL)performKeyEquivalent:(NSEvent *)theEvent { if (!ci) return FALSE; if (ci->inLocator && [theEvent keyCode] == 53 /* ESC - can't find the proper constant for this */) { ci->locator[0] = -1.0; ci->inLocator = NO; return TRUE; } return FALSE; } static void QuartzCocoa_SaveHistory(QuartzCocoaDevice *ci, int last) { SEXP ss = (SEXP) qf->GetSnapshot(ci->qd, last); if (ss) { /* ss will be NULL if there is no content, e.g. during the first call */ R_PreserveObject(ss); if (ci->inHistory != -1) { /* if we are editing an existing snapshot, replace it */ /* Rprintf("(updating plot in history at %d)\n", ci->inHistory); */ if (ci->history[ci->inHistory]) R_ReleaseObject(ci->history[ci->inHistory]); ci->history[ci->inHistory] = ss; } else { /* Rprintf("(adding plot to history at %d)\n", ci->histptr); */ if (ci->history[ci->histptr]) R_ReleaseObject(ci->history[ci->histptr]); ci->history[ci->histptr++] = ss; ci->histptr &= histsize - 1; } } } - (void)historyBack: (id) sender { if (!ci) return; int hp = ci->inHistory - 1; if (ci->inHistory == -1) hp = (ci->histptr - 1); hp &= histsize - 1; if (hp == ci->histptr || !ci->history[hp]) return; if (qf->GetDirty(ci->qd)) /* save the current snapshot if it is dirty */ QuartzCocoa_SaveHistory(ci, 0); ci->inHistory = hp; ci->inHistoryRecall = YES; /* Rprintf("(activating history entry %d) ", hp); */ /* get rid of the current layer and force a repaint which will fetch the right entry */ CGLayerRelease(ci->layer); ci->layer = 0; ci->layerContext = 0; [self setNeedsDisplay:YES]; } - (void)historyForward: (id) sender { if (!ci) return; int hp = ci->inHistory + 1; if (ci->inHistory == -1) return; hp &= histsize - 1; if (hp == ci->histptr || !ci->history[hp]) /* we can't really get past the last entry */ return; if (qf->GetDirty(ci->qd)) /* save the current snapshot if it is dirty */ QuartzCocoa_SaveHistory(ci, 0); ci->inHistory = hp; /* Rprintf("(activating history entry %d)\n", hp); */ ci->inHistoryRecall = YES; CGLayerRelease(ci->layer); ci->layer = 0; ci->layerContext = 0; [self setNeedsDisplay:YES]; } - (void)historyFlush: (id) sender { if (!ci) return; int i = 0; ci->inHistory = -1; ci->inHistoryRecall = NO; ci->histptr = 0; while (i < histsize) { if (ci->history[i]) { R_ReleaseObject(ci->history[i]); ci->history[i]=0; } i++; } } - (void)viewDidEndLiveResize { [self setNeedsDisplay: YES]; } - (void)windowWillClose:(NSNotification *)aNotification { if (!ci) return; ci->closing = YES; qf->Kill(ci->qd); } - (void)resetCursorRects { if (!ci) return; if (ci->inLocator) [self addCursorRect:[self bounds] cursor:[NSCursor crosshairCursor]]; } @end #pragma mark --- R Quartz interface --- static CGContextRef QuartzCocoa_GetCGContext(QuartzDesc_t dev, void *userInfo) { QuartzCocoaDevice *qd = (QuartzCocoaDevice*)userInfo; return qd->pdfMode ? qd->context : qd->layerContext; } static void QuartzCocoa_Close(QuartzDesc_t dev,void *userInfo) { QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; /* cancel any locator events */ ci->inLocator = NO; ci->locator[0] = -1.0; /* release all history objects */ ci->inHistory = -1; ci->inHistoryRecall = NO; ci->histptr = 0; { int i = 0; while (i < histsize) { if (ci->history[i]) { R_ReleaseObject(ci->history[i]); ci->history[i] = 0; } i++; } } if (ci->pars.family) free((void*)ci->pars.family); if (ci->pars.title) free((void*)ci->pars.title); if (ci->pars.file) free((void*)ci->pars.file); /* close the window (if it's not already closing) */ if (ci && ci->view && !ci->closing) [[ci->view window] close]; if (ci->view) [ci->view release]; /* this is our own release, the window should still have a copy */ if (ci->window) [ci->window release]; /* that should close it all */ ci->view = nil; ci->window = nil; } static int QuartzCocoa_Locator(QuartzDesc_t dev, void* userInfo, double *x, double*y) { QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; if (!ci || !ci->view || ci->inLocator) return FALSE; ci->locator[0] = -1.0; ci->inLocator = YES; [[ci->view window] invalidateCursorRectsForView: ci->view]; while (ci->inLocator && !ci->closing) { NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate dateWithTimeIntervalSinceNow:0.2] inMode:NSDefaultRunLoopMode dequeue:YES]; if (event) [NSApp sendEvent:event]; } [[ci->view window] invalidateCursorRectsForView: ci->view]; *x = ci->locator[0]; *y = ci->bounds.size.height - ci->locator[1]; return (*x >= 0.0)?TRUE:FALSE; } static void QuartzCocoa_NewPage(QuartzDesc_t dev,void *userInfo, int flags) { QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; if (!ci) return; if (ci->pdfMode) { if (ci->context) qf->ResetContext(dev); return; } if ((flags&QNPF_REDRAW)==0) { /* no redraw -> really new page */ QuartzCocoa_SaveHistory(ci, 1); ci->inHistory = -1; } if (ci->layer) { CGLayerRelease(ci->layer); ci->layer = 0; ci->layerContext = 0; } if (ci->context) { CGSize size = CGSizeMake(ci->bounds.size.width, ci->bounds.size.height); ci->layer = CGLayerCreateWithContext(ci->context, size, 0); ci->layerContext = CGLayerGetContext(ci->layer); qf->ResetContext(dev); /* Rprintf(" - creating new layer (%p - ctx: %p, %f x %f)\n", ci->layer, ci->layerContext, size.width, size.height); */ } } static void QuartzCocoa_Sync(QuartzDesc_t dev,void *userInfo) { QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; if (!ci || !ci->view || ci->pdfMode) return; /* we have to force display now, enqueuing it on the event loop * via setNeedsDisplay: YES has issues since dev.flush() won't * be synchronous and thus animation using dev.flush(); dev.hold() * will break by the time the event loop is run */ [ci->view display]; } static void QuartzCocoa_State(QuartzDesc_t dev, void *userInfo, int state) { QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; NSString *title; if (!ci || !ci->view) return; if (!ci->title) ci->title=strdup("Quartz %d"); title = [NSString stringWithFormat: [NSString stringWithUTF8String: ci->title], qf->DevNumber(dev)]; if (state) title = [title stringByAppendingString: @" [*]"]; [[ci->view window] setTitle: title]; } static void* QuartzCocoa_Cap(QuartzDesc_t dev, void *userInfo) { QuartzCocoaDevice *ci = (QuartzCocoaDevice*)userInfo; SEXP raster = R_NilValue; if (!ci || !ci->view) { return (void*) raster; } else { unsigned int i, pixels, stride, j = 0; unsigned int *rint; SEXP dim; NSSize size = [ci->view frame].size; pixels = size.width * size.height; // make sure the view is up-to-date (fix for PR#14260) [ci->view display]; if (![ci->view canDraw]) warning("View not able to draw!?"); [ci->view lockFocus]; NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithFocusedViewRect: NSMakeRect(0, 0, size.width, size.height)]; int bpp = (int) [rep bitsPerPixel]; NSBitmapFormat bf = [rep bitmapFormat]; /* Rprintf("format: bpp=%d, bf=0x%x, bps=%d, planar=%s, colorspace=%s\n", bpp, (int) bf, [rep bitsPerSample], [rep isPlanar] ? "YES" : "NO", [[rep colorSpaceName] UTF8String]); */ /* we only support meshed (=interleaved) formats of 8 bits/component with 3 or 4 components. We should really check for RGB/RGBA as well.. */ if ([rep isPlanar] || [rep bitsPerSample] != 8 || (bf & NSFloatingPointSamplesBitmapFormat) || (bpp != 24 && bpp != 32)) { warning("Unsupported image format"); return (void*) raster; } unsigned char *screenData = [rep bitmapData]; PROTECT(raster = allocVector(INTSXP, pixels)); /* 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... */ /* Copy each byte of screen to an R matrix. * The ARGB32 needs to be converted to an R ABGR32 */ rint = (unsigned int *) INTEGER(raster); stride = (bpp == 24) ? 3 : 4; /* convers bpp to stride in bytes */ for (i = 0; i < pixels; i++, j += stride) rint[i] = ((screenData[j + 0]) | (screenData[j + 1] << 8) | (screenData[j + 2] << 16) | 0xFF000000); /* alpha is currently ignored and set to 1.0 (why?) */ [rep release]; PROTECT(dim = allocVector(INTSXP, 2)); INTEGER(dim)[0] = size.height; INTEGER(dim)[1] = size.width; setAttrib(raster, R_DimSymbol, dim); UNPROTECT(2); [ci->view unlockFocus]; } return (void *) raster; } QuartzDesc_t QuartzCocoa_DeviceCreate(void *dd, QuartzFunctions_t *fn, QuartzParameters_t *par) { QuartzDesc_t qd; double *dpi = par->dpi, width = par->width, height = par->height; double mydpi[2] = { 72.0, 72.0 }; double scalex = 1.0, scaley = 1.0; QuartzCocoaDevice *dev; if (!qf) qf = fn; { /* check whether we have access to a display at all */ CGDisplayCount dcount = 0; CGGetOnlineDisplayList(255, NULL, &dcount); if (dcount < 1) { warning("No displays are available"); return NULL; } } if (!dpi) { CGDirectDisplayID md = CGMainDisplayID(); if (md) { CGSize ds = CGDisplayScreenSize(md); double width = (double)CGDisplayPixelsWide(md); double height = (double)CGDisplayPixelsHigh(md); /* landscape screen, portrait resolution -> rotated screen */ if (ds.width > ds.height && width < height) { mydpi[0] = width / ds.height * 25.4; mydpi[1] = height / ds.width * 25.4; } else { mydpi[0] = width / ds.width * 25.4; mydpi[1] = height / ds.height * 25.4; } /* Rprintf("screen resolution %f x %f\n", mydpi[0], mydpi[1]); */ } dpi = mydpi; } scalex = dpi[0] / 72.0; scaley = dpi[1] / 72.0; if (width * height > 20736.0) { warning("Requested on-screen area is too large (%.1f by %.1f inches).", width, height); return NULL; } /* FIXME: check allocations [better now, but strdups below are not covered; also check dev->pars] */ dev = malloc(sizeof(QuartzCocoaDevice)); memset(dev, 0, sizeof(QuartzCocoaDevice)); QuartzBackend_t qdef = { sizeof(qdef), width, height, scalex, scaley, par->pointsize, par->bg, par->canvas, par->flags | QDFLAG_INTERACTIVE | QDFLAG_DISPLAY_LIST | QDFLAG_RASTERIZED, dev, QuartzCocoa_GetCGContext, QuartzCocoa_Locator, QuartzCocoa_Close, QuartzCocoa_NewPage, QuartzCocoa_State, NULL,/* par */ QuartzCocoa_Sync, QuartzCocoa_Cap, }; qd = qf->Create(dd, &qdef); if (!qd) { free(dev); return NULL; } dev->qd = qd; /* copy parameters for later */ memcpy(&dev->pars, par, (par->size < sizeof(QuartzParameters_t))? par->size : sizeof(QuartzParameters_t)); if (par->size > sizeof(QuartzParameters_t)) dev->pars.size = sizeof(QuartzParameters_t); if (par->family) dev->pars.family = strdup(par->family); if (par->title) dev->pars.title = strdup(par->title); if (par->file) dev->pars.file = strdup(par->file); /* we cannot substitute the device number as it is not yet known at this point */ dev->title = strdup(par->title); { /* this view ignores it anyway ... */ NSRect rect = NSMakeRect(20.0, 20.0, qf->GetScaledWidth(qd), qf->GetScaledHeight(qd)); /* Rprintf("scale=%f/%f; size=%f x %f\n", scalex, scaley, rect.size.width, rect.size.height); */ if (![QuartzView quartzWindowWithRect: rect andInfo: dev]) { free((char*)dev->title); free(qd); free(dev); return NULL; } } if (dev->view) { // unlike normal Quartz device we enforce the size of the view and not the device NSRect bounds = [dev->view bounds]; qf->SetScaledSize(qd, bounds.size.width, bounds.size.height); } return qd; }