// Copyright (c) Wojciech Figat. All rights reserved. #if PLATFORM_SDL && PLATFORM_MAC #include "SDLWindow.h" #include "Engine/Core/Log.h" #include "Engine/Core/Collections/Array.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Engine/Engine.h" #include "Engine/Engine/Time.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Input/Input.h" #include "Engine/Input/Mouse.h" #include "Engine/Platform/IGuiData.h" #include "Engine/Platform/Platform.h" #include "Engine/Platform/WindowsManager.h" #include "Engine/Platform/Base/DragDropHelper.h" #include "Engine/Platform/SDL/SDLClipboard.h" #include "Engine/Platform/Apple/AppleUtils.h" #include #include #include #include #include #include #include #include #include namespace MacImpl { Window* DraggedWindow = nullptr; String DraggingData = String(); Float2 DraggingPosition; Nullable LastMouseDragPosition; bool DraggingActive = false; bool DraggingIgnoreEvent = false; NSDraggingSession* MacDragSession = nullptr; int64 MacDragExitFlag = 0; } class MacDropData : public IGuiData { public: Type CurrentType; String AsText; Array AsFiles; Type GetType() const override { return CurrentType; } String GetAsText() const override { return AsText; } void GetAsFiles(Array* files) const override { files->Add(AsFiles); } }; bool SDLPlatform::InitInternal() { return false; } bool SDLPlatform::UsesWindows() { return false; } bool SDLPlatform::UsesWayland() { return false; } bool SDLPlatform::UsesX11() { return false; } bool SDLPlatform::EventFilterCallback(void* userdata, SDL_Event* event) { Window* draggedWindow = *(Window**)userdata; if (draggedWindow == nullptr) { if (MacImpl::DraggingActive) { // Handle events during drag operation here since the normal event loop is blocked if (event->type == SDL_EVENT_WINDOW_EXPOSED) { // The internal timer is sending exposed events every ~16ms #if USE_EDITOR // Flush any single-frame shapes to prevent memory leaking (eg. via terrain collision debug during scene drawing with PhysicsColliders or PhysicsDebug flag) DebugDraw::UpdateContext(nullptr, 0.0f); #endif Engine::OnUpdate(); // For docking updates Engine::OnDraw(); } else { SDLWindow* window = SDLWindow::GetWindowFromEvent(*event); if (window) window->HandleEvent(*event); // We do not receive events at steady rate to keep the engine updated... #if USE_EDITOR // Flush any single-frame shapes to prevent memory leaking (eg. via terrain collision debug during scene drawing with PhysicsColliders or PhysicsDebug flag) DebugDraw::UpdateContext(nullptr, 0.0f); #endif Engine::OnUpdate(); // For docking updates Engine::OnDraw(); if (event->type == SDL_EVENT_DROP_BEGIN || event->type == SDL_EVENT_DROP_FILE || event->type == SDL_EVENT_DROP_TEXT) return true; // Filtering these event stops other following events from getting added to the queue } return false; } return true; } return true; } void SDLPlatform::PreHandleEvents() { SDL_SetEventFilter(EventFilterCallback, &MacImpl::DraggedWindow); } void SDLPlatform::PostHandleEvents() { SDL_SetEventFilter(EventFilterCallback, &MacImpl::DraggedWindow); // Handle window dragging release here if (MacImpl::DraggedWindow != nullptr) { Float2 mousePosition; auto buttons = SDL_GetGlobalMouseState(&mousePosition.X, &mousePosition.Y); bool buttonReleased = (buttons & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) == 0; if (buttonReleased) { // Send simulated mouse up event SDL_Event buttonUpEvent { 0 }; buttonUpEvent.motion.type = SDL_EVENT_MOUSE_BUTTON_UP; buttonUpEvent.button.down = false; buttonUpEvent.motion.windowID = SDL_GetWindowID(MacImpl::DraggedWindow->GetSDLWindow()); buttonUpEvent.motion.timestamp = SDL_GetTicksNS(); buttonUpEvent.motion.state = SDL_BUTTON_LEFT; buttonUpEvent.button.clicks = 1; buttonUpEvent.motion.x = mousePosition.X; buttonUpEvent.motion.y = mousePosition.Y; MacImpl::DraggedWindow->HandleEvent(buttonUpEvent); MacImpl::DraggedWindow = nullptr; } } } bool SDLWindow::HandleEventInternal(SDL_Event& event) { switch (event.type) { case SDL_EVENT_WINDOW_MOVED: { // Quartz doesn't report any mouse events when mouse is over the caption area, send a simulated event instead... Float2 mousePosition; auto buttons = SDL_GetGlobalMouseState(&mousePosition.X, &mousePosition.Y); if ((buttons & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) != 0) { if (MacImpl::DraggedWindow == nullptr) { // TODO: verify mouse position, window focus bool result = false; OnLeftButtonHit(WindowHitCodes::Caption, result); if (result) MacImpl::DraggedWindow = this; } else { Float2 mousePos = Platform::GetMousePosition(); Input::Mouse->OnMouseMove(mousePos, this); } } break; } case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_BUTTON_DOWN: { if (MacImpl::LastMouseDragPosition.HasValue()) { // SDL reports wrong mouse position after dragging has ended Float2 mouseClientPosition = ScreenToClient(MacImpl::LastMouseDragPosition.GetValue()); event.button.x = mouseClientPosition.X; event.button.y = mouseClientPosition.Y; } break; } case SDL_EVENT_MOUSE_MOTION: { if (MacImpl::LastMouseDragPosition.HasValue()) MacImpl::LastMouseDragPosition.Reset(); if (MacImpl::DraggedWindow != nullptr) return true; break; } case SDL_EVENT_WINDOW_MOUSE_LEAVE: { OnDragLeave(); // Check for release of mouse button too? break; } case SDL_EVENT_DROP_BEGIN: case SDL_EVENT_DROP_POSITION: case SDL_EVENT_DROP_FILE: case SDL_EVENT_DROP_TEXT: case SDL_EVENT_DROP_COMPLETE: { auto dpiScale = GetDpiScale(); Float2 mousePos = Float2(event.drop.x * dpiScale, event.drop.y * dpiScale); DragDropEffect effect = DragDropEffect::None; String text(event.drop.data); MacDropData dropData; if (MacImpl::DraggingActive) { // We don't have the window dragging data during these events... text = MacImpl::DraggingData; mousePos = ScreenToClient(MacImpl::DraggingPosition); // Ensure mouse position is updated while dragging Input::Mouse->OnMouseMove(MacImpl::DraggingPosition, this); MacImpl::LastMouseDragPosition = MacImpl::DraggingPosition; } dropData.AsText = text; if (event.type == SDL_EVENT_DROP_BEGIN) { // We don't know the type of dragged data at this point, so call the events for both types if (!MacImpl::DraggingActive) { dropData.CurrentType = IGuiData::Type::Files; OnDragEnter(&dropData, mousePos, effect); } if (effect == DragDropEffect::None) { dropData.CurrentType = IGuiData::Type::Text; OnDragEnter(&dropData, mousePos, effect); } } else if (event.type == SDL_EVENT_DROP_POSITION) { Input::Mouse->OnMouseMove(ClientToScreen(mousePos), this); // We don't know the type of dragged data at this point, so call the events for both types if (!MacImpl::DraggingActive) { dropData.CurrentType = IGuiData::Type::Files; OnDragOver(&dropData, mousePos, effect); } if (effect == DragDropEffect::None) { dropData.CurrentType = IGuiData::Type::Text; OnDragOver(&dropData, mousePos, effect); } } else if (event.type == SDL_EVENT_DROP_FILE) { text.Split('\n', dropData.AsFiles); dropData.CurrentType = IGuiData::Type::Files; OnDragDrop(&dropData, mousePos, effect); } else if (event.type == SDL_EVENT_DROP_TEXT) { dropData.CurrentType = IGuiData::Type::Text; OnDragDrop(&dropData, mousePos, effect); } else if (event.type == SDL_EVENT_DROP_COMPLETE) { OnDragLeave(); if (MacImpl::DraggingActive) { // The previous drop events needs to be flushed to avoid processing them twice SDL_FlushEvents(SDL_EVENT_DROP_FILE, SDL_EVENT_DROP_POSITION); } } // TODO: Implement handling for feedback effect result (https://github.com/libsdl-org/SDL/issues/10448) break; } } return false; } void SDLPlatform::SetHighDpiAwarenessEnabled(bool enable) { // TODO: This is now called before Platform::Init, ensure the scaling is changed accordingly during Platform::Init (see ApplePlatform::SetHighDpiAwarenessEnabled) } inline bool IsWindowInvalid(Window* win) { WindowsManager::WindowsLocker.Lock(); const bool hasWindow = WindowsManager::Windows.Contains(win); WindowsManager::WindowsLocker.Unlock(); return !hasWindow || !win; } Float2 GetWindowTitleSize(const SDLWindow* window) { Float2 size = Float2::Zero; if (window->GetSettings().HasBorder) { NSRect frameStart = [(NSWindow*)window->GetNativePtr() frameRectForContentRect:NSMakeRect(0, 0, 0, 0)]; size.Y = frameStart.size.height; } return size * MacPlatform::ScreenScale; } Float2 GetMousePosition(SDLWindow* window, NSEvent* event) { NSRect frame = [(NSWindow*)window->GetNativePtr() frame]; NSPoint point = [event locationInWindow]; return Float2(point.x, frame.size.height - point.y) * MacPlatform::ScreenScale - GetWindowTitleSize(window); } Float2 GetMousePosition(SDLWindow* window, const NSPoint& point) { NSRect frame = [(NSWindow*)window->GetNativePtr() frame]; CGRect screenBounds = CGDisplayBounds(CGMainDisplayID()); return Float2(point.x, screenBounds.size.height - point.y) * MacPlatform::ScreenScale; } @interface ClipboardDataProviderImpl : NSObject { @public SDLWindow* Window; } @end @implementation ClipboardDataProviderImpl // NSPasteboardItemDataProvider // --- - (void)pasteboard:(nullable NSPasteboard*)pasteboard item:(NSPasteboardItem*)item provideDataForType:(NSPasteboardType)type { if (IsWindowInvalid(Window)) return; [pasteboard setString:(NSString*)AppleUtils::ToString(MacImpl::DraggingData) forType:NSPasteboardTypeString]; } - (void)pasteboardFinishedWithDataProvider:(NSPasteboard*)pasteboard { } // NSDraggingSource // --- - (NSDragOperation)draggingSession:(NSDraggingSession*)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context { if (IsWindowInvalid(Window)) return NSDragOperationNone; switch(context) { case NSDraggingContextOutsideApplication: return NSDragOperationCopy; case NSDraggingContextWithinApplication: return NSDragOperationCopy; default: return NSDragOperationMove; } } - (void)draggingSession:(NSDraggingSession*)session willBeginAtPoint:(NSPoint)screenPoint { MacImpl::DraggingPosition = GetMousePosition(Window, screenPoint); } - (void)draggingSession:(NSDraggingSession*)session movedToPoint:(NSPoint)screenPoint { MacImpl::DraggingPosition = GetMousePosition(Window, screenPoint); } - (void)draggingSession:(NSDraggingSession*)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation { MacImpl::DraggingPosition = GetMousePosition(Window, screenPoint); #if USE_EDITOR // Stop background worker once the drag ended if (MacImpl::MacDragSession && MacImpl::MacDragSession == session) Platform::AtomicStore(&MacImpl::MacDragExitFlag, 1); #endif } @end DragDropEffect SDLWindow::DoDragDrop(const StringView& data) { NSWindow* window = (NSWindow*)_handle; ClipboardDataProviderImpl* clipboardDataProvider = [ClipboardDataProviderImpl alloc]; clipboardDataProvider->Window = this; // Create mouse drag event NSEvent* event = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDragged location:window.mouseLocationOutsideOfEventStream modifierFlags:0 timestamp:NSApp.currentEvent.timestamp windowNumber:window.windowNumber context:nil eventNumber:0 clickCount:1 pressure:1.0]; // Create drag item NSPasteboardItem* pasteItem = [NSPasteboardItem new]; [pasteItem setDataProvider:clipboardDataProvider forTypes:[NSArray arrayWithObjects:NSPasteboardTypeString, nil]]; NSDraggingItem* dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter:pasteItem]; [dragItem setDraggingFrame:NSMakeRect(event.locationInWindow.x, event.locationInWindow.y, 100, 100) contents:nil]; // Start dragging session NSDraggingSession* draggingSession = [window.contentView beginDraggingSessionWithItems:[NSArray arrayWithObject:dragItem] event:event source:clipboardDataProvider]; DragDropEffect result = DragDropEffect::None; #if USE_EDITOR //ASSERT(!MacImpl::MacDragSession); // TODO: Dragging item from dropdown box in a floating window attempts to init dragging twice if (MacImpl::MacDragSession != nullptr) return result; MacImpl::MacDragSession = draggingSession; MacImpl::MacDragExitFlag = 0; MacImpl::DraggingData = data; MacImpl::DraggingActive = true; while (Platform::AtomicRead(&MacImpl::MacDragExitFlag) == 0) { // The internal event loop will block here during the drag operation, // events are processed in the event filter callback instead. SDLPlatform::Tick(); Platform::Sleep(1); } MacImpl::DraggingActive = false; MacImpl::DraggingData.Clear(); MacImpl::MacDragSession = nullptr; #endif return result; } DragDropEffect SDLWindow::DoDragDrop(const StringView& data, const Float2& offset, Window* dragSourceWindow) { Show(); return DragDropEffect::None; } #endif