/* ============================================================================== This file is part of the JUCE 7 technical preview. Copyright (c) 2022 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For the technical preview this file cannot be licensed commercially. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { void LookAndFeel::playAlertSound() { NSBeep(); } //============================================================================== class OSXMessageBox : private AsyncUpdater { public: OSXMessageBox (const MessageBoxOptions& opts, std::unique_ptr&& c) : options (opts), callback (std::move (c)) { } int getResult() const { return convertResult ([getAlert() runModal]); } using AsyncUpdater::triggerAsyncUpdate; private: static int convertResult (NSModalResponse response) { switch (response) { case NSAlertFirstButtonReturn: return 0; case NSAlertSecondButtonReturn: return 1; case NSAlertThirdButtonReturn: return 2; default: break; } jassertfalse; return 0; } void handleAsyncUpdate() override { if (auto* comp = options.getAssociatedComponent()) { if (auto* peer = comp->getPeer()) { if (auto* view = static_cast (peer->getNativeHandle())) { if (auto* window = [view window]) { if (@available (macOS 10.9, *)) { [getAlert() beginSheetModalForWindow: window completionHandler: ^(NSModalResponse result) { handleModalFinished (result); }]; return; } } } } } handleModalFinished ([getAlert() runModal]); } void handleModalFinished (NSModalResponse result) { if (callback != nullptr) callback->modalStateFinished (convertResult (result)); delete this; } static void addButton (NSAlert* alert, const String& button) { if (! button.isEmpty()) [alert addButtonWithTitle: juceStringToNS (button)]; } NSAlert* getAlert() const { NSAlert* alert = [[[NSAlert alloc] init] autorelease]; [alert setMessageText: juceStringToNS (options.getTitle())]; [alert setInformativeText: juceStringToNS (options.getMessage())]; [alert setAlertStyle: options.getIconType() == MessageBoxIconType::WarningIcon ? NSAlertStyleCritical : NSAlertStyleInformational]; const auto button1Text = options.getButtonText (0); addButton (alert, button1Text.isEmpty() ? "OK" : button1Text); addButton (alert, options.getButtonText (1)); addButton (alert, options.getButtonText (2)); return alert; } MessageBoxOptions options; std::unique_ptr callback; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OSXMessageBox) }; static int showDialog (const MessageBoxOptions& options, ModalComponentManager::Callback* callbackIn, AlertWindowMappings::MapFn mapFn) { #if JUCE_MODAL_LOOPS_PERMITTED if (callbackIn == nullptr) { jassert (mapFn != nullptr); OSXMessageBox messageBox (options, nullptr); return mapFn (messageBox.getResult()); } #endif auto messageBox = std::make_unique (options, AlertWindowMappings::getWrappedCallback (callbackIn, mapFn)); messageBox->triggerAsyncUpdate(); messageBox.release(); return 0; } #if JUCE_MODAL_LOOPS_PERMITTED void JUCE_CALLTYPE NativeMessageBox::showMessageBox (MessageBoxIconType iconType, const String& title, const String& message, Component* associatedComponent) { showDialog (MessageBoxOptions() .withIconType (iconType) .withTitle (title) .withMessage (message) .withButton (TRANS("OK")) .withAssociatedComponent (associatedComponent), nullptr, AlertWindowMappings::messageBox); } int JUCE_CALLTYPE NativeMessageBox::show (const MessageBoxOptions& options) { return showDialog (options, nullptr, AlertWindowMappings::noMapping); } #endif void JUCE_CALLTYPE NativeMessageBox::showMessageBoxAsync (MessageBoxIconType iconType, const String& title, const String& message, Component* associatedComponent, ModalComponentManager::Callback* callback) { showDialog (MessageBoxOptions() .withIconType (iconType) .withTitle (title) .withMessage (message) .withButton (TRANS("OK")) .withAssociatedComponent (associatedComponent), callback, AlertWindowMappings::messageBox); } bool JUCE_CALLTYPE NativeMessageBox::showOkCancelBox (MessageBoxIconType iconType, const String& title, const String& message, Component* associatedComponent, ModalComponentManager::Callback* callback) { return showDialog (MessageBoxOptions() .withIconType (iconType) .withTitle (title) .withMessage (message) .withButton (TRANS("OK")) .withButton (TRANS("Cancel")) .withAssociatedComponent (associatedComponent), callback, AlertWindowMappings::okCancel) != 0; } int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (MessageBoxIconType iconType, const String& title, const String& message, Component* associatedComponent, ModalComponentManager::Callback* callback) { return showDialog (MessageBoxOptions() .withIconType (iconType) .withTitle (title) .withMessage (message) .withButton (TRANS("Yes")) .withButton (TRANS("No")) .withButton (TRANS("Cancel")) .withAssociatedComponent (associatedComponent), callback, AlertWindowMappings::yesNoCancel); } int JUCE_CALLTYPE NativeMessageBox::showYesNoBox (MessageBoxIconType iconType, const String& title, const String& message, Component* associatedComponent, ModalComponentManager::Callback* callback) { return showDialog (MessageBoxOptions() .withIconType (iconType) .withTitle (title) .withMessage (message) .withButton (TRANS("Yes")) .withButton (TRANS("No")) .withAssociatedComponent (associatedComponent), callback, AlertWindowMappings::okCancel); } void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, ModalComponentManager::Callback* callback) { showDialog (options, callback, AlertWindowMappings::noMapping); } void JUCE_CALLTYPE NativeMessageBox::showAsync (const MessageBoxOptions& options, std::function callback) { showAsync (options, ModalCallbackFunction::create (callback)); } //============================================================================== static NSRect getDragRect (NSView* view, NSEvent* event) { auto eventPos = [event locationInWindow]; return [view convertRect: NSMakeRect (eventPos.x - 16.0f, eventPos.y - 16.0f, 32.0f, 32.0f) fromView: nil]; } static NSView* getNSViewForDragEvent (Component* sourceComp) { if (sourceComp == nullptr) if (auto* draggingSource = Desktop::getInstance().getDraggingMouseSource (0)) sourceComp = draggingSource->getComponentUnderMouse(); if (sourceComp != nullptr) return (NSView*) sourceComp->getWindowHandle(); jassertfalse; // This method must be called in response to a component's mouseDown or mouseDrag event! return nil; } struct NSDraggingSourceHelper : public ObjCClass> { NSDraggingSourceHelper() : ObjCClass> ("JUCENSDraggingSourceHelper_") { addIvar*> ("callback"); addIvar ("text"); addIvar ("operation"); addMethod (@selector (dealloc), dealloc); addMethod (@selector (pasteboard:item:provideDataForType:), provideDataForType); addMethod (@selector (draggingSession:sourceOperationMaskForDraggingContext:), sourceOperationMaskForDraggingContext); addMethod (@selector (draggingSession:endedAtPoint:operation:), draggingSessionEnded); addProtocol (@protocol (NSPasteboardItemDataProvider)); registerClass(); } static void setText (id self, const String& text) { object_setInstanceVariable (self, "text", new String (text)); } static void setCompletionCallback (id self, std::function cb) { object_setInstanceVariable (self, "callback", new std::function (cb)); } static void setDragOperation (id self, NSDragOperation op) { object_setInstanceVariable (self, "operation", new NSDragOperation (op)); } private: static void dealloc (id self, SEL) { delete getIvar (self, "text"); delete getIvar*> (self, "callback"); delete getIvar (self, "operation"); sendSuperclassMessage (self, @selector (dealloc)); } static void provideDataForType (id self, SEL, NSPasteboard* sender, NSPasteboardItem*, NSString* type) { if ([type compare: NSPasteboardTypeString] == NSOrderedSame) if (auto* text = getIvar (self, "text")) [sender setData: [juceStringToNS (*text) dataUsingEncoding: NSUTF8StringEncoding] forType: NSPasteboardTypeString]; } static NSDragOperation sourceOperationMaskForDraggingContext (id self, SEL, NSDraggingSession*, NSDraggingContext) { return *getIvar (self, "operation"); } static void draggingSessionEnded (id self, SEL, NSDraggingSession*, NSPoint p, NSDragOperation) { // Our view doesn't receive a mouse up when the drag ends so we need to generate one here and send it... if (auto* view = getNSViewForDragEvent (nullptr)) if (auto* cgEvent = CGEventCreateMouseEvent (nullptr, kCGEventLeftMouseUp, CGPointMake (p.x, p.y), kCGMouseButtonLeft)) if (id e = [NSEvent eventWithCGEvent: cgEvent]) [view mouseUp: e]; if (auto* cb = getIvar*> (self, "callback")) cb->operator()(); } }; static NSDraggingSourceHelper draggingSourceHelper; bool DragAndDropContainer::performExternalDragDropOfText (const String& text, Component* sourceComponent, std::function callback) { if (text.isEmpty()) return false; if (auto* view = getNSViewForDragEvent (sourceComponent)) { JUCE_AUTORELEASEPOOL { if (auto event = [[view window] currentEvent]) { id helper = [draggingSourceHelper.createInstance() init]; NSDraggingSourceHelper::setText (helper, text); NSDraggingSourceHelper::setDragOperation (helper, NSDragOperationCopy); if (callback != nullptr) NSDraggingSourceHelper::setCompletionCallback (helper, callback); auto pasteboardItem = [[NSPasteboardItem new] autorelease]; [pasteboardItem setDataProvider: helper forTypes: [NSArray arrayWithObjects: NSPasteboardTypeString, nil]]; auto dragItem = [[[NSDraggingItem alloc] initWithPasteboardWriter: pasteboardItem] autorelease]; NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: nsEmptyString()]; [dragItem setDraggingFrame: getDragRect (view, event) contents: image]; if (auto session = [view beginDraggingSessionWithItems: [NSArray arrayWithObject: dragItem] event: event source: helper]) { session.animatesToStartingPositionsOnCancelOrFail = YES; session.draggingFormation = NSDraggingFormationNone; return true; } } } } return false; } bool DragAndDropContainer::performExternalDragDropOfFiles (const StringArray& files, bool canMoveFiles, Component* sourceComponent, std::function callback) { if (files.isEmpty()) return false; if (auto* view = getNSViewForDragEvent (sourceComponent)) { JUCE_AUTORELEASEPOOL { if (auto event = [[view window] currentEvent]) { auto dragItems = [[[NSMutableArray alloc] init] autorelease]; for (auto& filename : files) { auto* nsFilename = juceStringToNS (filename); auto fileURL = [NSURL fileURLWithPath: nsFilename]; auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: fileURL]; auto eventPos = [event locationInWindow]; auto dragRect = [view convertRect: NSMakeRect (eventPos.x - 16.0f, eventPos.y - 16.0f, 32.0f, 32.0f) fromView: nil]; auto dragImage = [[NSWorkspace sharedWorkspace] iconForFile: nsFilename]; [dragItem setDraggingFrame: dragRect contents: dragImage]; [dragItems addObject: dragItem]; [dragItem release]; } auto helper = [draggingSourceHelper.createInstance() autorelease]; if (callback != nullptr) NSDraggingSourceHelper::setCompletionCallback (helper, callback); NSDraggingSourceHelper::setDragOperation (helper, canMoveFiles ? NSDragOperationMove : NSDragOperationCopy); return [view beginDraggingSessionWithItems: dragItems event: event source: helper] != nullptr; } } } return false; } //============================================================================== bool Desktop::canUseSemiTransparentWindows() noexcept { return true; } Point MouseInputSource::getCurrentRawMousePosition() { JUCE_AUTORELEASEPOOL { auto p = [NSEvent mouseLocation]; return { (float) p.x, (float) (getMainScreenHeight() - p.y) }; } } void MouseInputSource::setRawMousePosition (Point newPosition) { // this rubbish needs to be done around the warp call, to avoid causing a // bizarre glitch.. CGAssociateMouseAndMouseCursorPosition (false); CGWarpMouseCursorPosition (convertToCGPoint (newPosition)); CGAssociateMouseAndMouseCursorPosition (true); } double Desktop::getDefaultMasterScale() { return 1.0; } Desktop::DisplayOrientation Desktop::getCurrentOrientation() const { return upright; } bool Desktop::isDarkModeActive() const { return [[[NSUserDefaults standardUserDefaults] stringForKey: nsStringLiteral ("AppleInterfaceStyle")] isEqualToString: nsStringLiteral ("Dark")]; } class Desktop::NativeDarkModeChangeDetectorImpl { public: NativeDarkModeChangeDetectorImpl() { static DelegateClass delegateClass; delegate = [delegateClass.createInstance() init]; object_setInstanceVariable (delegate, "owner", this); JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") [[NSDistributedNotificationCenter defaultCenter] addObserver: delegate selector: @selector (darkModeChanged:) name: @"AppleInterfaceThemeChangedNotification" object: nil]; JUCE_END_IGNORE_WARNINGS_GCC_LIKE } ~NativeDarkModeChangeDetectorImpl() { object_setInstanceVariable (delegate, "owner", nullptr); [[NSDistributedNotificationCenter defaultCenter] removeObserver: delegate]; [delegate release]; } void darkModeChanged() { Desktop::getInstance().darkModeChanged(); } private: struct DelegateClass : public ObjCClass { DelegateClass() : ObjCClass ("JUCEDelegate_") { addIvar ("owner"); JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wundeclared-selector") addMethod (@selector (darkModeChanged:), darkModeChanged); JUCE_END_IGNORE_WARNINGS_GCC_LIKE registerClass(); } static void darkModeChanged (id self, SEL, NSNotification*) { if (auto* owner = getIvar (self, "owner")) owner->darkModeChanged(); } }; id delegate = nil; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeDarkModeChangeDetectorImpl) }; std::unique_ptr Desktop::createNativeDarkModeChangeDetectorImpl() { return std::make_unique(); } //============================================================================== class ScreenSaverDefeater : public Timer { public: ScreenSaverDefeater() { startTimer (5000); timerCallback(); } void timerCallback() override { if (Process::isForegroundProcess()) { if (assertion == nullptr) assertion.reset (new PMAssertion()); } else { assertion.reset(); } } struct PMAssertion { PMAssertion() : assertionID (kIOPMNullAssertionID) { IOReturn res = IOPMAssertionCreateWithName (kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, CFSTR ("JUCE Playback"), &assertionID); jassert (res == kIOReturnSuccess); ignoreUnused (res); } ~PMAssertion() { if (assertionID != kIOPMNullAssertionID) IOPMAssertionRelease (assertionID); } IOPMAssertionID assertionID; }; std::unique_ptr assertion; }; static std::unique_ptr screenSaverDefeater; void Desktop::setScreenSaverEnabled (const bool isEnabled) { if (isEnabled) screenSaverDefeater.reset(); else if (screenSaverDefeater == nullptr) screenSaverDefeater.reset (new ScreenSaverDefeater()); } bool Desktop::isScreenSaverEnabled() { return screenSaverDefeater == nullptr; } //============================================================================== struct DisplaySettingsChangeCallback : private DeletedAtShutdown { DisplaySettingsChangeCallback() { CGDisplayRegisterReconfigurationCallback (displayReconfigurationCallback, this); } ~DisplaySettingsChangeCallback() { CGDisplayRemoveReconfigurationCallback (displayReconfigurationCallback, this); clearSingletonInstance(); } static void displayReconfigurationCallback (CGDirectDisplayID, CGDisplayChangeSummaryFlags, void* userInfo) { if (auto* thisPtr = static_cast (userInfo)) if (thisPtr->forceDisplayUpdate != nullptr) thisPtr->forceDisplayUpdate(); } std::function forceDisplayUpdate; JUCE_DECLARE_SINGLETON (DisplaySettingsChangeCallback, false) JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DisplaySettingsChangeCallback) }; JUCE_IMPLEMENT_SINGLETON (DisplaySettingsChangeCallback) static Rectangle convertDisplayRect (NSRect r, CGFloat mainScreenBottom) { r.origin.y = mainScreenBottom - (r.origin.y + r.size.height); return convertToRectInt (r); } static Displays::Display getDisplayFromScreen (NSScreen* s, CGFloat& mainScreenBottom, const float masterScale) { Displays::Display d; d.isMain = (mainScreenBottom == 0); if (d.isMain) mainScreenBottom = [s frame].size.height; d.userArea = convertDisplayRect ([s visibleFrame], mainScreenBottom) / masterScale; d.totalArea = convertDisplayRect ([s frame], mainScreenBottom) / masterScale; d.scale = masterScale; if ([s respondsToSelector: @selector (backingScaleFactor)]) d.scale *= s.backingScaleFactor; NSSize dpi = [[[s deviceDescription] objectForKey: NSDeviceResolution] sizeValue]; d.dpi = (dpi.width + dpi.height) / 2.0; return d; } void Displays::findDisplays (const float masterScale) { JUCE_AUTORELEASEPOOL { if (DisplaySettingsChangeCallback::getInstanceWithoutCreating() == nullptr) DisplaySettingsChangeCallback::getInstance()->forceDisplayUpdate = [this] { refresh(); }; CGFloat mainScreenBottom = 0; for (NSScreen* s in [NSScreen screens]) displays.add (getDisplayFromScreen (s, mainScreenBottom, masterScale)); } } //============================================================================== bool juce_areThereAnyAlwaysOnTopWindows() { for (NSWindow* window in [NSApp windows]) if ([window level] > NSNormalWindowLevel) return true; return false; } //============================================================================== static void selectImageForDrawing (const Image& image) { [NSGraphicsContext saveGraphicsState]; if (@available (macOS 10.10, *)) { [NSGraphicsContext setCurrentContext: [NSGraphicsContext graphicsContextWithCGContext: juce_getImageContext (image) flipped: false]]; return; } JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") [NSGraphicsContext setCurrentContext: [NSGraphicsContext graphicsContextWithGraphicsPort: juce_getImageContext (image) flipped: false]]; JUCE_END_IGNORE_WARNINGS_GCC_LIKE } static void releaseImageAfterDrawing() { [[NSGraphicsContext currentContext] flushGraphics]; [NSGraphicsContext restoreGraphicsState]; } Image juce_createIconForFile (const File& file) { JUCE_AUTORELEASEPOOL { NSImage* image = [[NSWorkspace sharedWorkspace] iconForFile: juceStringToNS (file.getFullPathName())]; Image result (Image::ARGB, (int) [image size].width, (int) [image size].height, true); selectImageForDrawing (result); [image drawAtPoint: NSMakePoint (0, 0) fromRect: NSMakeRect (0, 0, [image size].width, [image size].height) operation: NSCompositingOperationSourceOver fraction: 1.0f]; releaseImageAfterDrawing(); return result; } } static Image createNSWindowSnapshot (NSWindow* nsWindow) { JUCE_AUTORELEASEPOOL { CGImageRef screenShot = CGWindowListCreateImage (CGRectNull, kCGWindowListOptionIncludingWindow, (CGWindowID) [nsWindow windowNumber], kCGWindowImageBoundsIgnoreFraming); NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage: screenShot]; Image result (Image::ARGB, (int) [bitmapRep size].width, (int) [bitmapRep size].height, true); selectImageForDrawing (result); [bitmapRep drawAtPoint: NSMakePoint (0, 0)]; releaseImageAfterDrawing(); [bitmapRep release]; CGImageRelease (screenShot); return result; } } Image createSnapshotOfNativeWindow (void* nativeWindowHandle) { if (id windowOrView = (id) nativeWindowHandle) { if ([windowOrView isKindOfClass: [NSWindow class]]) return createNSWindowSnapshot ((NSWindow*) windowOrView); if ([windowOrView isKindOfClass: [NSView class]]) return createNSWindowSnapshot ([(NSView*) windowOrView window]); } return {}; } //============================================================================== void SystemClipboard::copyTextToClipboard (const String& text) { NSPasteboard* pb = [NSPasteboard generalPasteboard]; [pb declareTypes: [NSArray arrayWithObject: NSPasteboardTypeString] owner: nil]; [pb setString: juceStringToNS (text) forType: NSPasteboardTypeString]; } String SystemClipboard::getTextFromClipboard() { return nsStringToJuce ([[NSPasteboard generalPasteboard] stringForType: NSPasteboardTypeString]); } void Process::setDockIconVisible (bool isVisible) { ProcessSerialNumber psn { 0, kCurrentProcess }; OSStatus err = TransformProcessType (&psn, isVisible ? kProcessTransformToForegroundApplication : kProcessTransformToUIElementApplication); jassert (err == 0); ignoreUnused (err); } } // namespace juce