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