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