1 /**
2 * Copyright: Copyright Auburn Sounds 2015 and later.
3 * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
4 * Authors:   Guillaume Piolat
5 */
6 module dplug.window.cocoawindow;
7 
8 import core.stdc.stdlib;
9 
10 import std.string;
11 import std.uuid;
12 
13 import gfm.math.vector;
14 import gfm.math.box;
15 import dplug.graphics.image;
16 import dplug.graphics.view;
17 
18 import dplug.core.sync;
19 import dplug.core.runtime;
20 import dplug.core.nogc;
21 import dplug.core.random;
22 import dplug.window.window;
23 
24 import derelict.cocoa;
25 
26 final class CocoaWindow : IWindow
27 {
28 nothrow:
29 @nogc:
30 private:
31     IWindowListener _listener;
32 
33     NSColorSpace _nsColorSpace;
34     CGColorSpaceRef _cgColorSpaceRef;
35     NSData _imageData;
36     NSString _logFormatStr;
37 
38     // Only used by host windows
39     NSWindow _nsWindow;
40     NSApplication _nsApplication;
41 
42     DPlugCustomView _view = null;
43 
44     bool _terminated = false;
45 
46     int _lastMouseX, _lastMouseY;
47     bool _firstMouseMove = true;
48 
49     int _width;
50     int _height;
51 
52     int _askedWidth;
53     int _askedHeight;
54 
55     ImageRef!RGBA _wfb;
56 
57     uint _timeAtCreationInMs;
58     uint _lastMeasturedTimeInMs;
59     bool _dirtyAreasAreNotYetComputed;
60 
61 public:
62 
63     // If listener is null, this window is a host window who doesn't need to register a view
64     // Else it's a client window.
65     this(void* parentWindow, IWindowListener listener, int width, int height)
66     {
67         bool isHostWindow = listener is null;
68 
69         _listener = listener;
70 
71         acquireCocoaFunctions();
72         NSApplicationLoad(); // to use Cocoa in Carbon applications
73         bool parentViewExists = parentWindow !is null;
74 
75         _width = 0;
76         _height = 0;
77 
78         _askedWidth = width;
79         _askedHeight = height;
80 
81         _nsColorSpace = NSColorSpace.sRGBColorSpace();
82         // hopefully not null else the colors will be brighter
83         _cgColorSpaceRef = _nsColorSpace.CGColorSpace();
84 
85         _logFormatStr = NSString.stringWith("%@");
86 
87         _timeAtCreationInMs = getTimeMs();
88         _lastMeasturedTimeInMs = _timeAtCreationInMs;
89 
90         _dirtyAreasAreNotYetComputed = true;
91 
92         if (!isHostWindow)
93         {
94             DPlugCustomView.generateClassName();
95             DPlugCustomView.registerSubclass();
96 
97             _view = DPlugCustomView.alloc();
98             _view.initialize(this, width, height);
99 
100             // In VST, add the view to the parent view.
101             // In AU (parentWindow == null), a reference to the view is returned instead and the host does it.
102             if (parentWindow !is null)
103             {
104                 NSView parentView = NSView(cast(id)parentWindow);
105                 parentView.addSubview(_view);
106             }
107         }
108         else
109         {
110             _nsApplication = NSApplication.sharedApplication;
111             _nsApplication.setActivationPolicy(NSApplicationActivationPolicyRegular);
112             _nsWindow = NSWindow.alloc();
113             _nsWindow.initWithContentRect(NSMakeRect(0, 0, width, height),
114                                             NSBorderlessWindowMask, NSBackingStoreBuffered, NO);
115             _nsWindow.makeKeyAndOrderFront();
116 
117                 _nsApplication.activateIgnoringOtherApps(YES);
118         }
119     }
120 
121     ~this()
122     {
123         bool isHostWindow = _listener is null;
124 
125         if (!isHostWindow)
126         {
127             _terminated = true;
128 
129             {
130                 _view.killTimer();
131             }
132 
133             _view.removeFromSuperview();
134             _view.release();
135             _view = DPlugCustomView(null);
136 
137             DPlugCustomView.unregisterSubclass();
138         }
139         else
140         {
141             _nsWindow.destroy();
142         }
143 
144         releaseCocoaFunctions();
145     }
146 
147     // Implements IWindow
148     override void waitEventAndDispatch()
149     {
150         bool isHostWindow = _listener is null;
151         if (!isHostWindow)
152             assert(false); // only valid for a host window
153 
154         NSEvent event = _nsWindow.nextEventMatchingMask(cast(NSUInteger)-1);
155         _nsApplication.sendEvent(event);
156     }
157 
158     override bool terminated()
159     {
160         return _terminated;
161     }
162 
163     override uint getTimeMs()
164     {
165         return cast(uint)(NSDate.timeIntervalSinceReferenceDate() * 1000.0);
166     }
167 
168     override void* systemHandle()
169     {
170         bool isHostWindow = _listener is null;
171         if (isHostWindow)
172             return _nsWindow.contentView()._id; // return the main NSView
173         else
174             return _view._id;
175     }
176 
177 private:
178 
179     MouseState getMouseState(NSEvent event)
180     {
181         // not working
182         MouseState state;
183         uint pressedMouseButtons = event.pressedMouseButtons();
184         if (pressedMouseButtons & 1)
185             state.leftButtonDown = true;
186         if (pressedMouseButtons & 2)
187             state.rightButtonDown = true;
188         if (pressedMouseButtons & 4)
189             state.middleButtonDown = true;
190 
191         NSEventModifierFlags mod = event.modifierFlags();
192         if (mod & NSControlKeyMask)
193             state.ctrlPressed = true;
194         if (mod & NSShiftKeyMask)
195             state.shiftPressed = true;
196         if (mod & NSAlternateKeyMask)
197             state.altPressed = true;
198 
199         return state;
200     }
201 
202     void handleMouseWheel(NSEvent event)
203     {
204         int deltaX = cast(int)(0.5 + 10 * event.deltaX);
205         int deltaY = cast(int)(0.5 + 10 * event.deltaY);
206         vec2i mousePos = getMouseXY(_view, event, _height);
207         _listener.onMouseWheel(mousePos.x, mousePos.y, deltaX, deltaY, getMouseState(event));
208     }
209 
210     bool handleKeyEvent(NSEvent event, bool released)
211     {
212         uint keyCode = event.keyCode();
213         Key key;
214         switch (keyCode)
215         {
216             case kVK_ANSI_Keypad0: key = Key.digit0; break;
217             case kVK_ANSI_Keypad1: key = Key.digit1; break;
218             case kVK_ANSI_Keypad2: key = Key.digit2; break;
219             case kVK_ANSI_Keypad3: key = Key.digit3; break;
220             case kVK_ANSI_Keypad4: key = Key.digit4; break;
221             case kVK_ANSI_Keypad5: key = Key.digit5; break;
222             case kVK_ANSI_Keypad6: key = Key.digit6; break;
223             case kVK_ANSI_Keypad7: key = Key.digit7; break;
224             case kVK_ANSI_Keypad8: key = Key.digit8; break;
225             case kVK_ANSI_Keypad9: key = Key.digit9; break;
226             case kVK_Return: key = Key.enter; break;
227             case kVK_Escape: key = Key.escape; break;
228             case kVK_LeftArrow: key = Key.leftArrow; break;
229             case kVK_RightArrow: key = Key.rightArrow; break;
230             case kVK_DownArrow: key = Key.downArrow; break;
231             case kVK_UpArrow: key = Key.upArrow; break;
232             default: key = Key.unsupported;
233         }
234 
235         bool handled = false;
236 
237         if (released)
238         {
239             if (_listener.onKeyDown(key))
240                 handled = true;
241         }
242         else
243         {
244             if (_listener.onKeyUp(key))
245                 handled = true;
246         }
247 
248         return handled;
249     }
250 
251     void handleMouseMove(NSEvent event)
252     {
253         vec2i mousePos = getMouseXY(_view, event, _height);
254 
255         if (_firstMouseMove)
256         {
257             _firstMouseMove = false;
258             _lastMouseX = mousePos.x;
259             _lastMouseY = mousePos.y;
260         }
261 
262         _listener.onMouseMove(mousePos.x, mousePos.y, mousePos.x - _lastMouseX, mousePos.y - _lastMouseY,
263             getMouseState(event));
264 
265         _lastMouseX = mousePos.x;
266         _lastMouseY = mousePos.y;
267     }
268 
269     void handleMouseClicks(NSEvent event, MouseButton mb, bool released)
270     {
271         vec2i mousePos = getMouseXY(_view, event, _height);
272 
273         if (released)
274             _listener.onMouseRelease(mousePos.x, mousePos.y, mb, getMouseState(event));
275         else
276         {
277             int clickCount = event.clickCount();
278             bool isDoubleClick = clickCount >= 2;
279             _listener.onMouseClick(mousePos.x, mousePos.y, mb, isDoubleClick, getMouseState(event));
280         }
281     }
282 
283     enum scanLineAlignment = 4; // could be anything
284 
285     // given a width, how long in bytes should scanlines be
286     int byteStride(int width)
287     {
288         int widthInBytes = width * 4;
289         return (widthInBytes + (scanLineAlignment - 1)) & ~(scanLineAlignment-1);
290     }
291 
292     void drawRect(NSRect rect)
293     {
294         NSGraphicsContext nsContext = NSGraphicsContext.currentContext();
295 
296         CIContext ciContext = nsContext.getCIContext();
297 
298         // Updates internal buffers in case of startup/resize
299         // FUTURE: why is the bounds rect too large? It creates havoc in AU even without resizing.
300         {
301             /*
302             NSRect boundsRect = _view.bounds();
303             int width = cast(int)(boundsRect.size.width);   // truncating down the dimensions of bounds
304             int height = cast(int)(boundsRect.size.height);
305             */
306             updateSizeIfNeeded(_askedWidth, _askedHeight);
307         }
308 
309         // The first drawRect callback occurs before the timer triggers.
310         // But because recomputeDirtyAreas() wasn't called before there is nothing to draw.
311         // Hence, do it.
312         if (_dirtyAreasAreNotYetComputed)
313         {
314             _dirtyAreasAreNotYetComputed = false;
315             _listener.recomputeDirtyAreas();
316         }
317 
318         _listener.onDraw(WindowPixelFormat.ARGB8);
319 
320         size_t sizeNeeded = _wfb.pitch * _wfb.h;
321         _imageData = NSData.dataWithBytesNoCopy(_wfb.pixels, sizeNeeded, false);
322 
323         CIImage image = CIImage.imageWithBitmapData(_imageData,
324                                                     byteStride(_width),
325                                                     CGSize(_width, _height),
326                                                     kCIFormatARGB8,
327                                                     _cgColorSpaceRef);
328 
329         ciContext.drawImage(image, rect, rect);
330     }
331 
332     /// Returns: true if window size changed.
333     bool updateSizeIfNeeded(int newWidth, int newHeight)
334     {
335         // only do something if the client size has changed
336         if ( (newWidth != _width) || (newHeight != _height) )
337         {
338             _width = newWidth;
339             _height = newHeight;
340             _wfb = _listener.onResized(_width, _height);
341             return true;
342         }
343         else
344             return false;
345     }
346 
347     void doAnimation()
348     {
349         uint now = getTimeMs();
350         double dt = (now - _lastMeasturedTimeInMs) * 0.001;
351         double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days
352         _lastMeasturedTimeInMs = now;
353         _listener.onAnimate(dt, time);
354     }
355 
356     void onTimer()
357     {
358         // Deal with animation
359         doAnimation();
360 
361         _listener.recomputeDirtyAreas();
362         _dirtyAreasAreNotYetComputed = false;
363         box2i dirtyRect = _listener.getDirtyRectangle();
364         if (!dirtyRect.empty())
365         {
366 
367             NSRect boundsRect = _view.bounds();
368             int height = cast(int)(boundsRect.size.height);
369             NSRect r = NSMakeRect(dirtyRect.min.x,
370                                     height - dirtyRect.min.y - dirtyRect.height,
371                                     dirtyRect.width,
372                                     dirtyRect.height);
373             _view.setNeedsDisplayInRect(r);
374         }
375     }
376 }
377 
378 struct DPlugCustomView
379 {
380 public:
381 nothrow:
382 @nogc:
383 
384     NSView parent;
385     alias parent this;
386 
387     // create from an id
388     this (id id_)
389     {
390         this._id = id_;
391     }
392 
393     /// Allocates, but do not init
394     static DPlugCustomView alloc()
395     {
396         alias fun_t = extern(C) id function (id obj, SEL sel) nothrow @nogc;
397         return DPlugCustomView( (cast(fun_t)objc_msgSend)(getClassID(), sel!"alloc") );
398     }
399 
400     static Class getClass()
401     {
402         return cast(Class)( getClassID() );
403     }
404 
405     static id getClassID()
406     {
407         return objc_getClass(customClassName.ptr);
408     }
409 
410     // This class uses a unique class name for each plugin instance
411     static __gshared char[16 + 36 + 1] customClassName;
412 
413     static void generateClassName() nothrow @nogc
414     {
415         generateNullTerminatedRandomUUID!char(customClassName, "DPlugCustomView_");
416     }
417 
418 private:
419 
420     CocoaWindow _window;
421     NSTimer _timer = null;
422     NSString _runLoopMode;
423 
424     void initialize(CocoaWindow window, int width, int height)
425     {
426         // Warning: taking this address is fishy since DPlugCustomView is a struct and thus could be copied
427         // we rely on the fact it won't :|
428         void* thisPointer = cast(void*)(&this);
429         object_setInstanceVariable(_id, "this", thisPointer);
430 
431         this._window = window;
432 
433         NSRect r = NSRect(NSPoint(0, 0), NSSize(width, height));
434         initWithFrame(r);
435 
436         _timer = NSTimer.timerWithTimeInterval(1 / 60.0, this, sel!"onTimer:", null, true);
437         _runLoopMode = NSString.stringWith("kCFRunLoopCommonModes"w);
438         NSRunLoop.currentRunLoop().addTimer(_timer, _runLoopMode);
439     }
440 
441     static __gshared Class clazz;
442 
443     static void registerSubclass()
444     {
445         clazz = objc_allocateClassPair(cast(Class) lazyClass!"NSView", customClassName.ptr, 0);
446 
447         class_addMethod(clazz, sel!"keyDown:", cast(IMP) &keyDown, "v@:@");
448         class_addMethod(clazz, sel!"keyUp:", cast(IMP) &keyUp, "v@:@");
449         class_addMethod(clazz, sel!"mouseDown:", cast(IMP) &mouseDown, "v@:@");
450         class_addMethod(clazz, sel!"mouseUp:", cast(IMP) &mouseUp, "v@:@");
451         class_addMethod(clazz, sel!"rightMouseDown:", cast(IMP) &rightMouseDown, "v@:@");
452         class_addMethod(clazz, sel!"rightMouseUp:", cast(IMP) &rightMouseUp, "v@:@");
453         class_addMethod(clazz, sel!"otherMouseDown:", cast(IMP) &otherMouseDown, "v@:@");
454         class_addMethod(clazz, sel!"otherMouseUp:", cast(IMP) &otherMouseUp, "v@:@");
455         class_addMethod(clazz, sel!"mouseMoved:", cast(IMP) &mouseMoved, "v@:@");
456         class_addMethod(clazz, sel!"mouseDragged:", cast(IMP) &mouseMoved, "v@:@");
457         class_addMethod(clazz, sel!"rightMouseDragged:", cast(IMP) &mouseMoved, "v@:@");
458         class_addMethod(clazz, sel!"otherMouseDragged:", cast(IMP) &mouseMoved, "v@:@");
459         class_addMethod(clazz, sel!"acceptsFirstResponder", cast(IMP) &acceptsFirstResponder, "b@:");
460         class_addMethod(clazz, sel!"isOpaque", cast(IMP) &isOpaque, "b@:");
461         class_addMethod(clazz, sel!"acceptsFirstMouse:", cast(IMP) &acceptsFirstMouse, "b@:@");
462         class_addMethod(clazz, sel!"viewDidMoveToWindow", cast(IMP) &viewDidMoveToWindow, "v@:");
463         class_addMethod(clazz, sel!"drawRect:", cast(IMP) &drawRect, "v@:" ~ encode!NSRect);
464         class_addMethod(clazz, sel!"onTimer:", cast(IMP) &onTimer, "v@:@");
465 
466         class_addMethod(clazz, sel!"mouseEntered:", cast(IMP) &mouseEntered, "v@:@");
467         class_addMethod(clazz, sel!"mouseExited:", cast(IMP) &mouseExited, "v@:@");
468 
469         // This ~ is to avoid a strange DMD ICE. Didn't succeed in isolating it.
470         class_addMethod(clazz, sel!("scroll" ~ "Wheel:") , cast(IMP) &scrollWheel, "v@:@");
471 
472         // very important: add an instance variable for the this pointer so that the D object can be
473         // retrieved from an id
474         class_addIvar(clazz, "this", (void*).sizeof, (void*).sizeof == 4 ? 2 : 3, "^v");
475 
476         objc_registerClassPair(clazz);
477     }
478 
479     static void unregisterSubclass()
480     {
481         // For some reason the class need to continue to exist, so we leak it
482         //  objc_disposeClassPair(clazz);
483         // TODO: remove this crap
484     }
485 
486     void killTimer()
487     {
488         if (_timer)
489         {
490             _timer.invalidate();
491             _timer = NSTimer(null);
492         }
493     }
494 }
495 
496 DPlugCustomView getInstance(id anId) nothrow @nogc
497 {
498     // strange thing: object_getInstanceVariable definition is odd (void**)
499     // and only works for pointer-sized values says SO
500     void* thisPointer = null;
501     Ivar var = object_getInstanceVariable(anId, "this", &thisPointer);
502     assert(var !is null);
503     assert(thisPointer !is null);
504     return *cast(DPlugCustomView*)thisPointer;
505 }
506 
507 vec2i getMouseXY(NSView view, NSEvent event, int windowHeight) nothrow @nogc
508 {
509     NSPoint mouseLocation = event.locationInWindow();
510     mouseLocation = view.convertPoint(mouseLocation, NSView(null));
511     int px = cast(int)(mouseLocation.x) - 2;
512     int py = windowHeight - cast(int)(mouseLocation.y) - 3;
513     return vec2i(px, py);
514 }
515 
516 
517 
518 alias CocoaScopedCallback = ScopedForeignCallback!(true, true);
519 
520 // Overridden function gets called with an id, instead of the self pointer.
521 // So we have to get back the D class object address.
522 // Big thanks to Mike Ash (@macdev)
523 // MAYDO: why are these methods members???
524 extern(C)
525 {
526     void keyDown(id self, SEL selector, id event) nothrow @nogc
527     {
528         CocoaScopedCallback scopedCallback;
529         scopedCallback.enter();
530 
531         DPlugCustomView view = getInstance(self);
532         bool handled = view._window.handleKeyEvent(NSEvent(event), false);
533 
534         // send event to superclass if event not handled
535         if (!handled)
536         {
537             objc_super sup;
538             sup.receiver = self;
539             sup.clazz = cast(Class) lazyClass!"NSView";
540             alias fun_t = extern(C) void function (objc_super*, SEL, id) nothrow @nogc;
541             (cast(fun_t)objc_msgSendSuper)(&sup, selector, event);
542         }
543     }
544 
545     void keyUp(id self, SEL selector, id event) nothrow @nogc
546     {
547         CocoaScopedCallback scopedCallback;
548         scopedCallback.enter();
549 
550         DPlugCustomView view = getInstance(self);
551         view._window.handleKeyEvent(NSEvent(event), true);
552     }
553 
554     void mouseDown(id self, SEL selector, id event) nothrow @nogc
555     {
556         CocoaScopedCallback scopedCallback;
557         scopedCallback.enter();
558 
559         DPlugCustomView view = getInstance(self);
560         view._window.handleMouseClicks(NSEvent(event), MouseButton.left, false);        
561     }
562 
563     void mouseUp(id self, SEL selector, id event) nothrow @nogc
564     {
565         CocoaScopedCallback scopedCallback;
566         scopedCallback.enter();
567 
568         DPlugCustomView view = getInstance(self);
569         view._window.handleMouseClicks(NSEvent(event), MouseButton.left, true);        
570     }
571 
572     void rightMouseDown(id self, SEL selector, id event) nothrow @nogc
573     {       
574         CocoaScopedCallback scopedCallback;
575         scopedCallback.enter();
576 
577         DPlugCustomView view = getInstance(self);
578         view._window.handleMouseClicks(NSEvent(event), MouseButton.right, false);
579     }
580 
581     void rightMouseUp(id self, SEL selector, id event) nothrow @nogc
582     {        
583         CocoaScopedCallback scopedCallback;
584         scopedCallback.enter();
585 
586         DPlugCustomView view = getInstance(self);
587         view._window.handleMouseClicks(NSEvent(event), MouseButton.right, true);
588     }
589 
590     void otherMouseDown(id self, SEL selector, id event) nothrow @nogc
591     {
592         CocoaScopedCallback scopedCallback;
593         scopedCallback.enter();
594 
595         DPlugCustomView view = getInstance(self);
596         auto nsEvent = NSEvent(event);
597         if (nsEvent.buttonNumber == 2)
598             view._window.handleMouseClicks(nsEvent, MouseButton.middle, false);
599     }
600 
601     void otherMouseUp(id self, SEL selector, id event) nothrow @nogc
602     {
603         CocoaScopedCallback scopedCallback;
604         scopedCallback.enter();
605 
606         DPlugCustomView view = getInstance(self);
607         auto nsEvent = NSEvent(event);
608         if (nsEvent.buttonNumber == 2)
609             view._window.handleMouseClicks(nsEvent, MouseButton.middle, true);    
610     }
611 
612     void mouseMoved(id self, SEL selector, id event) nothrow @nogc
613     {
614         CocoaScopedCallback scopedCallback;
615         scopedCallback.enter();
616 
617         DPlugCustomView view = getInstance(self);
618         view._window.handleMouseMove(NSEvent(event));        
619     }
620 
621     void mouseEntered(id self, SEL selector, id event) nothrow @nogc
622     {
623         CocoaScopedCallback scopedCallback;
624         scopedCallback.enter();
625         NSCursor.arrowCursor().push();
626     }
627 
628     void mouseExited(id self, SEL selector, id event) nothrow @nogc
629     {
630         CocoaScopedCallback scopedCallback;
631         scopedCallback.enter();
632         NSCursor.pop();    
633     }
634 
635 
636     void scrollWheel(id self, SEL selector, id event) nothrow @nogc
637     {
638         CocoaScopedCallback scopedCallback;
639         scopedCallback.enter();
640         DPlugCustomView view = getInstance(self);
641         view._window.handleMouseWheel(NSEvent(event));
642     }
643 
644     bool acceptsFirstResponder(id self, SEL selector) nothrow @nogc
645     {
646         return YES;
647     }
648 
649     bool acceptsFirstMouse(id self, SEL selector, id pEvent) nothrow @nogc
650     {
651         return YES;
652     }
653 
654     bool isOpaque(id self, SEL selector) nothrow @nogc
655     {
656         return YES;
657     }
658 
659     void viewDidMoveToWindow(id self, SEL selector) nothrow @nogc
660     {
661         CocoaScopedCallback scopedCallback;
662         scopedCallback.enter();
663 
664         DPlugCustomView view = getInstance(self);
665         NSWindow parentWindow = view.window();
666         if (parentWindow)
667         {
668             parentWindow.makeFirstResponder(view);
669             parentWindow.setAcceptsMouseMovedEvents(true);
670         }
671     }
672 
673     void drawRect(id self, SEL selector, NSRect rect) nothrow @nogc
674     {
675         CocoaScopedCallback scopedCallback;
676         scopedCallback.enter();
677 
678         DPlugCustomView view = getInstance(self);
679         view._window.drawRect(rect);   
680     }
681 
682     void onTimer(id self, SEL selector, id timer) nothrow @nogc
683     {
684         CocoaScopedCallback scopedCallback;
685         scopedCallback.enter();
686 
687         DPlugCustomView view = getInstance(self);
688         view._window.onTimer();
689     }
690 }