Browse Source

UI/AppKit: Process drag-and-drop events through the web view

This forwards all drag-and-drop events from the UI to the WebContent
process. If the page accepts the events, the UI does not handle them.
Otherwise, we will open the dropped files as file:// URLs.
Timothy Flynn 11 months ago
parent
commit
ac062d0c97

+ 7 - 1
Ladybird/AppKit/UI/Event.h

@@ -1,11 +1,13 @@
 /*
- * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
 #pragma once
 
+#include <AK/Vector.h>
+#include <LibURL/Forward.h>
 #include <LibWeb/Page/InputEvent.h>
 
 #import <Cocoa/Cocoa.h>
@@ -13,6 +15,10 @@
 namespace Ladybird {
 
 Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type, NSEvent*, NSView*, NSScrollView*, Web::UIEvents::MouseButton);
+
+Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type, id<NSDraggingInfo>, NSView*);
+Vector<URL::URL> drag_event_url_list(Web::DragEvent const&);
+
 Web::KeyEvent ns_event_to_key_event(Web::KeyEvent::Type, NSEvent*);
 NSEvent* key_event_to_ns_event(Web::KeyEvent const&);
 

+ 64 - 1
Ladybird/AppKit/UI/Event.mm

@@ -1,11 +1,14 @@
 /*
- * Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
+ * Copyright (c) 2023-2024, Tim Flynn <trflynn89@serenityos.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
 #include <AK/TypeCasts.h>
 #include <AK/Utf8View.h>
+#include <LibURL/URL.h>
+#include <LibWeb/HTML/SelectedFile.h>
+#include <LibWeb/UIEvents/KeyCode.h>
 
 #import <Carbon/Carbon.h>
 #import <UI/Event.h>
@@ -70,6 +73,66 @@ Web::MouseEvent ns_event_to_mouse_event(Web::MouseEvent::Type type, NSEvent* eve
     return { type, device_position, device_screen_position, button, button, modifiers, wheel_delta_x, wheel_delta_y, nullptr };
 }
 
+struct DragData : public Web::ChromeInputData {
+    explicit DragData(Vector<URL::URL> urls)
+        : urls(move(urls))
+    {
+    }
+
+    Vector<URL::URL> urls;
+};
+
+Web::DragEvent ns_event_to_drag_event(Web::DragEvent::Type type, id<NSDraggingInfo> event, NSView* view)
+{
+    auto position = [view convertPoint:event.draggingLocation fromView:nil];
+    auto device_position = ns_point_to_gfx_point(position).to_type<Web::DevicePixels>();
+
+    auto screen_position = [NSEvent mouseLocation];
+    auto device_screen_position = ns_point_to_gfx_point(screen_position).to_type<Web::DevicePixels>();
+
+    auto button = Web::UIEvents::MouseButton::Primary;
+    auto modifiers = ns_modifiers_to_key_modifiers([NSEvent modifierFlags], button);
+
+    Vector<Web::HTML::SelectedFile> files;
+    OwnPtr<DragData> chrome_data;
+
+    auto for_each_file = [&](auto callback) {
+        NSArray* file_list = [[event draggingPasteboard] readObjectsForClasses:@[ [NSURL class] ]
+                                                                       options:nil];
+
+        for (NSURL* file in file_list) {
+            auto file_path = Ladybird::ns_string_to_byte_string([file path]);
+            callback(file_path);
+        }
+    };
+
+    if (type == Web::DragEvent::Type::DragStart) {
+        for_each_file([&](ByteString const& file_path) {
+            if (auto file = Web::HTML::SelectedFile::from_file_path(file_path); file.is_error())
+                warnln("Unable to open file {}: {}", file_path, file.error());
+            else
+                files.append(file.release_value());
+        });
+    } else if (type == Web::DragEvent::Type::Drop) {
+        Vector<URL::URL> urls;
+
+        for_each_file([&](ByteString const& file_path) {
+            if (auto url = URL::create_with_url_or_path(file_path); url.is_valid())
+                urls.append(move(url));
+        });
+
+        chrome_data = make<DragData>(move(urls));
+    }
+
+    return { type, device_position, device_screen_position, button, button, modifiers, move(files), move(chrome_data) };
+}
+
+Vector<URL::URL> drag_event_url_list(Web::DragEvent const& event)
+{
+    auto& chrome_data = verify_cast<DragData>(*event.chrome_data);
+    return move(chrome_data.urls);
+}
+
 NSEvent* create_context_menu_mouse_event(NSView* view, Gfx::IntPoint position)
 {
     return create_context_menu_mouse_event(view, gfx_point_to_ns_point(position));

+ 59 - 1
Ladybird/AppKit/UI/LadybirdWebView.mm

@@ -48,7 +48,7 @@ struct HideCursor {
     }
 };
 
-@interface LadybirdWebView ()
+@interface LadybirdWebView () <NSDraggingDestination>
 {
     OwnPtr<Ladybird::WebViewBridge> m_web_view_bridge;
 
@@ -114,6 +114,8 @@ struct HideCursor {
                                                     owner:self
                                                  userInfo:nil];
         [self addTrackingArea:area];
+
+        [self registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeFileURL, nil]];
     }
 
     return self;
@@ -417,6 +419,25 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
         self.event_being_redispatched = nil;
     };
 
+    m_web_view_bridge->on_finish_handling_drag_event = [weak_self](auto const& event) {
+        LadybirdWebView* self = weak_self;
+        if (self == nil) {
+            return;
+        }
+
+        if (event.type != Web::DragEvent::Type::Drop) {
+            return;
+        }
+
+        if (auto urls = Ladybird::drag_event_url_list(event); !urls.is_empty()) {
+            [self.observer loadURL:urls[0]];
+
+            for (size_t i = 1; i < urls.size(); ++i) {
+                [self.observer onCreateNewTab:urls[i] activateTab:Web::HTML::ActivateTab::No];
+            }
+        }
+    };
+
     m_web_view_bridge->on_cursor_change = [weak_self](auto cursor) {
         LadybirdWebView* self = weak_self;
         if (self == nil) {
@@ -1669,4 +1690,41 @@ static void copy_data_to_clipboard(StringView data, NSPasteboardType pasteboard_
     m_web_view_bridge->enqueue_input_event(move(key_event));
 }
 
+#pragma mark - NSDraggingDestination
+
+- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)event
+{
+    auto drag_event = Ladybird::ns_event_to_drag_event(Web::DragEvent::Type::DragStart, event, self);
+    m_web_view_bridge->enqueue_input_event(move(drag_event));
+
+    return NSDragOperationCopy;
+}
+
+- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)event
+{
+    auto drag_event = Ladybird::ns_event_to_drag_event(Web::DragEvent::Type::DragMove, event, self);
+    m_web_view_bridge->enqueue_input_event(move(drag_event));
+
+    return NSDragOperationCopy;
+}
+
+- (void)draggingExited:(id<NSDraggingInfo>)event
+{
+    auto drag_event = Ladybird::ns_event_to_drag_event(Web::DragEvent::Type::DragEnd, event, self);
+    m_web_view_bridge->enqueue_input_event(move(drag_event));
+}
+
+- (BOOL)performDragOperation:(id<NSDraggingInfo>)event
+{
+    auto drag_event = Ladybird::ns_event_to_drag_event(Web::DragEvent::Type::Drop, event, self);
+    m_web_view_bridge->enqueue_input_event(move(drag_event));
+
+    return YES;
+}
+
+- (BOOL)wantsPeriodicDraggingUpdates
+{
+    return NO;
+}
+
 @end

+ 7 - 0
Ladybird/AppKit/UI/LadybirdWebViewBridge.cpp

@@ -93,6 +93,13 @@ void WebViewBridge::enqueue_input_event(Web::MouseEvent event)
     ViewImplementation::enqueue_input_event(move(event));
 }
 
+void WebViewBridge::enqueue_input_event(Web::DragEvent event)
+{
+    event.position = to_content_position(event.position.to_type<int>()).to_type<Web::DevicePixels>();
+    event.screen_position = to_content_position(event.screen_position.to_type<int>()).to_type<Web::DevicePixels>();
+    ViewImplementation::enqueue_input_event(move(event));
+}
+
 void WebViewBridge::enqueue_input_event(Web::KeyEvent event)
 {
     ViewImplementation::enqueue_input_event(move(event));

+ 1 - 0
Ladybird/AppKit/UI/LadybirdWebViewBridge.h

@@ -44,6 +44,7 @@ public:
     void set_preferred_motion(Web::CSS::PreferredMotion);
 
     void enqueue_input_event(Web::MouseEvent);
+    void enqueue_input_event(Web::DragEvent);
     void enqueue_input_event(Web::KeyEvent);
 
     struct Paintable {