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 }