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 }