diff --git a/Source/Engine/Platform/SDL/SDLPlatform.Mac.cpp b/Source/Engine/Platform/SDL/SDLPlatform.Mac.cpp index 5770b56d5..e2f510785 100644 --- a/Source/Engine/Platform/SDL/SDLPlatform.Mac.cpp +++ b/Source/Engine/Platform/SDL/SDLPlatform.Mac.cpp @@ -1,5 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. +#include "SDLInput.h" #if PLATFORM_SDL && PLATFORM_MAC #include "SDLWindow.h" @@ -20,7 +21,10 @@ #include "Engine/Platform/Unix/UnixFile.h" #include "Engine/Profiler/ProfilerCPU.h" -#include "Engine/Platform/Linux/IncludeX11.h" +#include "Engine/Platform/Apple/AppleUtils.h" +#include +#include +#include #include #include @@ -30,6 +34,43 @@ #include #include +namespace MacImpl +{ + Window* DraggedWindow = nullptr; + String DraggingData = String(); + Float2 DraggingPosition; + Nullable LastMouseDragPosition; +#if USE_EDITOR + //CriticalSection MacDragLocker; + bool DraggingActive = false; + bool DraggingIgnoreEvent = false; + NSDraggingSession* MacDragSession = nullptr; + //DoDragDropJob* MacDragJob = nullptr; + int64 MacDragExitFlag = 0; +#endif +} + +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; @@ -50,16 +91,287 @@ 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) + { + LOG(Info, "Window exposed event"); + // 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 + else + { + char eventName[64]; + SDL_GetEventDescription(event, eventName, 64); + + LOG(Info, "Unhandled event: {} ({})", event->type, String(StringAnsi(eventName))); + } + } + return false; + } + return true; + } + return true; + + // When the window is being dragged on Windows, the internal message loop is blocking + // the SDL event queue. We need to handle all relevant events in this event watch callback + // to ensure dragging related functionality doesn't break due to engine not getting updated. + // This also happens to fix the engine freezing during the dragging operation. +#if false + SDLWindow* window = SDLWindow::GetWindowFromEvent(*event); + if (event->type == SDL_EVENT_WINDOW_EXPOSED) + { + // The internal timer is sending exposed events every ~16ms + Engine::OnUpdate(); // For docking updates + Engine::OnDraw(); + return false; + } + else if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) + { + if (window) + { + bool result = false; + window->OnLeftButtonHit(WindowHitCodes::Caption, result); + //if (result) + // return false; + window->HandleEvent(*event); + } + return false; + } + else if (event->type == SDL_EVENT_WINDOW_MOVED) + { + if (window) + window->HandleEvent(*event); + + /*if (WinImpl::DraggedWindowSize != window->GetClientSize()) + { + // The window size changed while dragging, most likely due to maximized window restoring back to previous size. + WinImpl::DraggedWindowMousePosition = WinImpl::DraggedWindowStartPosition + WinImpl::DraggedWindowMousePosition - window->GetClientPosition(); + WinImpl::DraggedWindowStartPosition = window->GetClientPosition(); + WinImpl::DraggedWindowSize = window->GetClientSize(); + } + Float2 windowPosition = Float2(static_cast(event->window.data1), static_cast(event->window.data2)); + Float2 mousePosition = WinImpl::DraggedWindowMousePosition; + + // Generate mouse movement events while dragging the window around + SDL_Event mouseMovedEvent { 0 }; + mouseMovedEvent.motion.type = SDL_EVENT_MOUSE_MOTION; + mouseMovedEvent.motion.windowID = SDL_GetWindowID(WinImpl::DraggedWindow->GetSDLWindow()); + mouseMovedEvent.motion.timestamp = SDL_GetTicksNS(); + mouseMovedEvent.motion.state = SDL_BUTTON_LEFT; + mouseMovedEvent.motion.x = mousePosition.X; + mouseMovedEvent.motion.y = mousePosition.Y; + if (window) + window->HandleEvent(mouseMovedEvent);*/ + + return false; + } + if (window) + window->HandleEvent(*event); + + return false; +#endif +} + 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_CLIPBOARD_UPDATE: + 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: + { + { + // HACK: We can't use Wayland listeners due to SDL also using them at the same time causes + // some of the events to drop and make it impossible to implement dragging on application side. + // We can get enough information through SDL_EVENT_DROP_* events to fill in the blanks for the + // drag and drop implementation. + + 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; } @@ -68,9 +380,207 @@ 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; +} + +void GetDragDropData(const SDLWindow* window, id sender, Float2& mousePos, MacDropData& dropData) +{ + NSRect frame = [(NSWindow*)window->GetNativePtr() frame]; + NSPoint point = [sender draggingLocation]; + mousePos = Float2(point.x, frame.size.height - point.y) * MacPlatform::ScreenScale - GetWindowTitleSize(window); + NSPasteboard* pasteboard = [sender draggingPasteboard]; + if ([[pasteboard types] containsObject:NSPasteboardTypeString]) + { + dropData.CurrentType = IGuiData::Type::Text; + dropData.AsText = AppleUtils::ToString((CFStringRef)[pasteboard stringForType:NSPasteboardTypeString]); + } + else + { + dropData.CurrentType = IGuiData::Type::Files; + NSArray* files = [pasteboard readObjectsForClasses:@[[NSURL class]] options:nil]; + for (int32 i = 0; i < [files count]; i++) + { + NSString* url = [[files objectAtIndex:i] path]; + NSString* file = [NSURL URLWithString:url].path; + dropData.AsFiles.Add(AppleUtils::ToString((CFStringRef)file)); + } + } +} + +NSDragOperation GetDragDropOperation(DragDropEffect dragDropEffect) +{ + NSDragOperation result = NSDragOperationCopy; + switch (dragDropEffect) + { + case DragDropEffect::None: + //result = NSDragOperationNone; + break; + case DragDropEffect::Copy: + result = NSDragOperationCopy; + break; + case DragDropEffect::Move: + result = NSDragOperationMove; + break; + case DragDropEffect::Link: + result = NSDragOperationLink; + break; + } + return result; +} + +#undef INCLUDED_IN_SDL + + +@interface ClipboardDataProviderImpl : NSObject +{ +@public + SDLWindow* Window; +} +@end + +@implementation ClipboardDataProviderImpl + +// NSPasteboardItemDataProvider +// --- + +- (void)pasteboard:(nullable NSPasteboard*)pasteboard item:(NSPasteboardItem*)item provideDataForType:(NSPasteboardType)type +{ + LOG(Info, "pasteboard"); + if (IsWindowInvalid(Window)) return; + [pasteboard setString:(NSString*)AppleUtils::ToString(MacImpl::DraggingData) forType:NSPasteboardTypeString]; +} + +- (void)pasteboardFinishedWithDataProvider:(NSPasteboard*)pasteboard +{ + LOG(Info, "pasteboardFinishedWithDataProvider"); +} + +// NSDraggingSource +// --- + +- (NSDragOperation)draggingSession:(NSDraggingSession*)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context +{ + if (IsWindowInvalid(Window)) + return NSDragOperationNone; + + switch(context) + { + case NSDraggingContextOutsideApplication: + LOG(Info, "draggingSession sourceOperationMaskForDraggingContext: outside"); + return NSDragOperationCopy; + case NSDraggingContextWithinApplication: + LOG(Info, "draggingSession sourceOperationMaskForDraggingContext: inside"); + return NSDragOperationCopy; + default: + LOG(Info, "draggingSession sourceOperationMaskForDraggingContext: unknown"); + return NSDragOperationMove; + } +} + +- (void)draggingSession:(NSDraggingSession*)session willBeginAtPoint:(NSPoint)screenPoint +{ + LOG(Info, "draggingSession willBeginAtPoint"); + MacImpl::DraggingPosition = GetMousePosition(Window, screenPoint); +} + +- (void)draggingSession:(NSDraggingSession*)session movedToPoint:(NSPoint)screenPoint +{ + //LOG(Info, "draggingSession movedToPoint"); + MacImpl::DraggingPosition = GetMousePosition(Window, screenPoint); +} + +- (void)draggingSession:(NSDraggingSession*)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation +{ + LOG(Info, "draggingSession endedAtPoint"); + 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) { - return DragDropEffect::None; + 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 + // Create background worker that will keep updating GUI (perform rendering) + ASSERT(!MacImpl::MacDragSession); + 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)