From a7f414bba7f4fa306af30cdf52df6df85278ed62 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Sun, 8 Dec 2019 16:50:23 +0100 Subject: [PATCH] LibGUI+WindowServer: Start fleshing out drag&drop functionality This patch enables basic drag&drop between applications. You initiate a drag by creating a GDragOperation object and calling exec() on it. This creates a nested event loop in the calling program that only returns once the drag operation has ended. On the receiving side, you get a call to GWidget::drop_event() with a GDropEvent containing information about the dropped data. The only data passed right now is a piece of text that's also used to visually indicate that a drag is happening (by showing the text in a little box that follows the mouse cursor around.) There are things to fix here, but we're off to a nice start. :^) --- Libraries/LibGUI/GDragOperation.cpp | 53 ++++++++++++++++ Libraries/LibGUI/GDragOperation.h | 36 +++++++++++ Libraries/LibGUI/GEvent.h | 20 +++++- Libraries/LibGUI/GItemView.cpp | 42 +++++++++++++ Libraries/LibGUI/GItemView.h | 3 + Libraries/LibGUI/GWidget.cpp | 8 ++- Libraries/LibGUI/GWidget.h | 1 + Libraries/LibGUI/GWindow.cpp | 10 +++ Libraries/LibGUI/GWindowServerConnection.cpp | 17 +++++ Libraries/LibGUI/GWindowServerConnection.h | 3 + Libraries/LibGUI/Makefile | 1 + Servers/WindowServer/WSClientConnection.cpp | 23 ++++++- Servers/WindowServer/WSClientConnection.h | 1 + Servers/WindowServer/WSCompositor.cpp | 16 +++++ Servers/WindowServer/WSCompositor.h | 1 + Servers/WindowServer/WSWindowManager.cpp | 66 ++++++++++++++++++++ Servers/WindowServer/WSWindowManager.h | 13 ++++ Servers/WindowServer/WindowClient.ipc | 5 ++ Servers/WindowServer/WindowServer.ipc | 2 + 19 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 Libraries/LibGUI/GDragOperation.cpp create mode 100644 Libraries/LibGUI/GDragOperation.h diff --git a/Libraries/LibGUI/GDragOperation.cpp b/Libraries/LibGUI/GDragOperation.cpp new file mode 100644 index 00000000000..b88b863f841 --- /dev/null +++ b/Libraries/LibGUI/GDragOperation.cpp @@ -0,0 +1,53 @@ +#include +#include + +static GDragOperation* s_current_drag_operation; + +GDragOperation::GDragOperation(CObject* parent) + : CObject(parent) +{ +} + +GDragOperation::~GDragOperation() +{ +} + +GDragOperation::Outcome GDragOperation::exec() +{ + ASSERT(!s_current_drag_operation); + ASSERT(!m_event_loop); + + auto response = GWindowServerConnection::the().send_sync(m_text, -1, Size()); + if (!response->started()) { + m_outcome = Outcome::Cancelled; + return m_outcome; + } + + s_current_drag_operation = this; + m_event_loop = make(); + auto result = m_event_loop->exec(); + m_event_loop = nullptr; + dbgprintf("%s: event loop returned with result %d\n", class_name(), result); + remove_from_parent(); + s_current_drag_operation = nullptr; + return m_outcome; +} + +void GDragOperation::done(Outcome outcome) +{ + ASSERT(m_outcome == Outcome::None); + m_outcome = outcome; + m_event_loop->quit(0); +} + +void GDragOperation::notify_accepted(Badge) +{ + ASSERT(s_current_drag_operation); + s_current_drag_operation->done(Outcome::Accepted); +} + +void GDragOperation::notify_cancelled(Badge) +{ + ASSERT(s_current_drag_operation); + s_current_drag_operation->done(Outcome::Cancelled); +} diff --git a/Libraries/LibGUI/GDragOperation.h b/Libraries/LibGUI/GDragOperation.h new file mode 100644 index 00000000000..e21b9d4470d --- /dev/null +++ b/Libraries/LibGUI/GDragOperation.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +class GWindowServerConnection; + +class GDragOperation : public CObject { + C_OBJECT(GDragOperation) +public: + enum class Outcome { + None, + Accepted, + Cancelled, + }; + + virtual ~GDragOperation() override; + + void set_text(const String& text) { m_text = text; } + + Outcome exec(); + Outcome outcome() const { return m_outcome; } + + static void notify_accepted(Badge); + static void notify_cancelled(Badge); + +protected: + explicit GDragOperation(CObject* parent = nullptr); + +private: + void done(Outcome); + + OwnPtr m_event_loop; + Outcome m_outcome { Outcome::None }; + String m_text; +}; diff --git a/Libraries/LibGUI/GEvent.h b/Libraries/LibGUI/GEvent.h index d58e4a80438..66171c0e31f 100644 --- a/Libraries/LibGUI/GEvent.h +++ b/Libraries/LibGUI/GEvent.h @@ -2,9 +2,9 @@ #include #include -#include #include #include +#include class CObject; @@ -34,6 +34,7 @@ public: WindowCloseRequest, ContextMenu, EnabledChange, + Drop, __Begin_WM_Events, WM_WindowRemoved, @@ -278,3 +279,20 @@ private: unsigned m_modifiers { 0 }; int m_wheel_delta { 0 }; }; + +class GDropEvent final : public GEvent { +public: + GDropEvent(const Point& position, const String& text) + : GEvent(GEvent::Drop) + , m_position(position) + , m_text(text) + { + } + + const Point& position() const { return m_position; } + const String& text() const { return m_text; } + +private: + Point m_position; + String m_text; +}; diff --git a/Libraries/LibGUI/GItemView.cpp b/Libraries/LibGUI/GItemView.cpp index f6e83ed8453..042c0f164a4 100644 --- a/Libraries/LibGUI/GItemView.cpp +++ b/Libraries/LibGUI/GItemView.cpp @@ -1,4 +1,6 @@ +#include #include +#include #include #include #include @@ -83,6 +85,7 @@ void GItemView::mousedown_event(GMouseEvent& event) int item_index = item_at_event_position(event.position()); if (event.button() == GMouseButton::Left) { + m_left_mousedown_position = event.position(); if (item_index == -1) { selection().clear(); } else { @@ -97,6 +100,45 @@ void GItemView::mousedown_event(GMouseEvent& event) GAbstractView::mousedown_event(event); } +void GItemView::mousemove_event(GMouseEvent& event) +{ + if (!model()) + return GAbstractView::mousemove_event(event); + + if (event.buttons() & GMouseButton::Left && !selection().is_empty()) { + auto diff = event.position() - m_left_mousedown_position; + auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y(); + constexpr int drag_distance_threshold = 5; + if (distance_travelled_squared > (drag_distance_threshold)) { + dbg() << "Initiate drag!"; + auto drag_operation = GDragOperation::construct(); + + StringBuilder builder; + selection().for_each_index([&](auto& index) { + auto data = model()->data(index); + builder.append(data.to_string()); + builder.append(" "); + }); + + drag_operation->set_text(builder.to_string()); + auto outcome = drag_operation->exec(); + switch (outcome) { + case GDragOperation::Outcome::Accepted: + dbg() << "Drag was accepted!"; + break; + case GDragOperation::Outcome::Cancelled: + dbg() << "Drag was cancelled!"; + break; + default: + ASSERT_NOT_REACHED(); + break; + } + } + } + + GAbstractView::mousemove_event(event); +} + void GItemView::context_menu_event(GContextMenuEvent& event) { if (!model()) diff --git a/Libraries/LibGUI/GItemView.h b/Libraries/LibGUI/GItemView.h index dc67ea8fe43..7650775d06a 100644 --- a/Libraries/LibGUI/GItemView.h +++ b/Libraries/LibGUI/GItemView.h @@ -29,6 +29,7 @@ private: virtual void paint_event(GPaintEvent&) override; virtual void resize_event(GResizeEvent&) override; virtual void mousedown_event(GMouseEvent&) override; + virtual void mousemove_event(GMouseEvent&) override; virtual void keydown_event(GKeyEvent&) override; virtual void doubleclick_event(GMouseEvent&) override; virtual void context_menu_event(GContextMenuEvent&) override; @@ -43,5 +44,7 @@ private: int m_visual_column_count { 0 }; int m_visual_row_count { 0 }; + Point m_left_mousedown_position; + Size m_effective_item_size { 80, 80 }; }; diff --git a/Libraries/LibGUI/GWidget.cpp b/Libraries/LibGUI/GWidget.cpp index 1859d84ecd9..dbd7b0cd83c 100644 --- a/Libraries/LibGUI/GWidget.cpp +++ b/Libraries/LibGUI/GWidget.cpp @@ -156,6 +156,8 @@ void GWidget::event(CEvent& event) return handle_mouseup_event(static_cast(event)); case GEvent::MouseWheel: return mousewheel_event(static_cast(event)); + case GEvent::Drop: + return drop_event(static_cast(event)); case GEvent::Enter: return handle_enter_event(event); case GEvent::Leave: @@ -350,6 +352,11 @@ void GWidget::change_event(GEvent&) { } +void GWidget::drop_event(GDropEvent& event) +{ + dbg() << class_name() << "{" << this << "} DROP position: " << event.position() << ", text: '" << event.text() << "'"; +} + void GWidget::update() { if (rect().is_empty()) @@ -686,4 +693,3 @@ Vector GWidget::child_widgets() const } return widgets; } - diff --git a/Libraries/LibGUI/GWidget.h b/Libraries/LibGUI/GWidget.h index f339ff03d8d..59a336c9477 100644 --- a/Libraries/LibGUI/GWidget.h +++ b/Libraries/LibGUI/GWidget.h @@ -254,6 +254,7 @@ protected: virtual void leave_event(CEvent&); virtual void child_event(CChildEvent&) override; virtual void change_event(GEvent&); + virtual void drop_event(GDropEvent&); private: void handle_paint_event(GPaintEvent&); diff --git a/Libraries/LibGUI/GWindow.cpp b/Libraries/LibGUI/GWindow.cpp index 9386ae607b1..91db8d9de41 100644 --- a/Libraries/LibGUI/GWindow.cpp +++ b/Libraries/LibGUI/GWindow.cpp @@ -154,6 +154,16 @@ void GWindow::set_override_cursor(GStandardCursor cursor) void GWindow::event(CEvent& event) { + if (event.type() == GEvent::Drop) { + auto& drop_event = static_cast(event); + if (!m_main_widget) + return; + auto result = m_main_widget->hit_test(drop_event.position()); + auto local_event = make(result.local_position, drop_event.text()); + ASSERT(result.widget); + return result.widget->dispatch_event(*local_event, this); + } + if (event.type() == GEvent::MouseUp || event.type() == GEvent::MouseDown || event.type() == GEvent::MouseDoubleClick || event.type() == GEvent::MouseMove || event.type() == GEvent::MouseWheel) { auto& mouse_event = static_cast(event); if (m_global_cursor_tracking_widget) { diff --git a/Libraries/LibGUI/GWindowServerConnection.cpp b/Libraries/LibGUI/GWindowServerConnection.cpp index b04ed1c7d0d..aba2bcdba5b 100644 --- a/Libraries/LibGUI/GWindowServerConnection.cpp +++ b/Libraries/LibGUI/GWindowServerConnection.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -261,3 +262,19 @@ void GWindowServerConnection::handle(const WindowClient::AsyncSetWallpaperFinish { // This is handled manually by GDesktop::set_wallpaper(). } + +void GWindowServerConnection::handle(const WindowClient::DragDropped& message) +{ + if (auto* window = GWindow::from_window_id(message.window_id())) + CEventLoop::current().post_event(*window, make(message.mouse_position(), message.text())); +} + +void GWindowServerConnection::handle(const WindowClient::DragAccepted&) +{ + GDragOperation::notify_accepted({}); +} + +void GWindowServerConnection::handle(const WindowClient::DragCancelled&) +{ + GDragOperation::notify_cancelled({}); +} diff --git a/Libraries/LibGUI/GWindowServerConnection.h b/Libraries/LibGUI/GWindowServerConnection.h index a20b902dbb9..56fc8b0c216 100644 --- a/Libraries/LibGUI/GWindowServerConnection.h +++ b/Libraries/LibGUI/GWindowServerConnection.h @@ -41,4 +41,7 @@ private: virtual void handle(const WindowClient::WM_WindowIconBitmapChanged&) override; virtual void handle(const WindowClient::WM_WindowRectChanged&) override; virtual void handle(const WindowClient::AsyncSetWallpaperFinished&) override; + virtual void handle(const WindowClient::DragDropped&) override; + virtual void handle(const WindowClient::DragAccepted&) override; + virtual void handle(const WindowClient::DragCancelled&) override; }; diff --git a/Libraries/LibGUI/Makefile b/Libraries/LibGUI/Makefile index 1993aabf737..0d872557459 100644 --- a/Libraries/LibGUI/Makefile +++ b/Libraries/LibGUI/Makefile @@ -59,6 +59,7 @@ OBJS = \ GLazyWidget.o \ GCommand.o \ GUndoStack.o \ + GDragOperation.o \ GWindow.o LIBRARY = libgui.a diff --git a/Servers/WindowServer/WSClientConnection.cpp b/Servers/WindowServer/WSClientConnection.cpp index 74a59319158..6c684267428 100644 --- a/Servers/WindowServer/WSClientConnection.cpp +++ b/Servers/WindowServer/WSClientConnection.cpp @@ -1,12 +1,12 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -678,3 +678,24 @@ OwnPtr WSClientConnection::handl it->value->invalidate(message.rect()); return make(); } + +OwnPtr WSClientConnection::handle(const WindowServer::StartDrag& message) +{ + auto& wm = WSWindowManager::the(); + if (wm.dnd_client()) + return make(false); + + RefPtr bitmap; + if (message.bitmap_id() != -1) { + auto shared_buffer = SharedBuffer::create_from_shared_buffer_id(message.bitmap_id()); + ssize_t size_in_bytes = message.bitmap_size().area() * sizeof(RGBA32); + if (size_in_bytes > shared_buffer->size()) { + did_misbehave("SetAppletBackingStore: Shared buffer is too small for applet size"); + return nullptr; + } + bitmap = GraphicsBitmap::create_with_shared_buffer(GraphicsBitmap::Format::RGBA32, *shared_buffer, message.bitmap_size()); + } + + wm.start_dnd_drag(*this, message.text(), bitmap); + return make(true); +} diff --git a/Servers/WindowServer/WSClientConnection.h b/Servers/WindowServer/WSClientConnection.h index b7d3bc4987b..707c882d2f5 100644 --- a/Servers/WindowServer/WSClientConnection.h +++ b/Servers/WindowServer/WSClientConnection.h @@ -92,6 +92,7 @@ private: virtual OwnPtr handle(const WindowServer::DestroyMenuApplet&) override; virtual OwnPtr handle(const WindowServer::SetMenuAppletBackingStore&) override; virtual OwnPtr handle(const WindowServer::InvalidateMenuAppletRect&) override; + virtual OwnPtr handle(const WindowServer::StartDrag&) override; HashMap> m_menu_applets; HashMap> m_windows; diff --git a/Servers/WindowServer/WSCompositor.cpp b/Servers/WindowServer/WSCompositor.cpp index 6590330957a..83692179355 100644 --- a/Servers/WindowServer/WSCompositor.cpp +++ b/Servers/WindowServer/WSCompositor.cpp @@ -93,6 +93,7 @@ void WSCompositor::compose() dirty_rects.add(Rect::intersection(m_last_geometry_label_rect, WSScreen::the().rect())); dirty_rects.add(Rect::intersection(m_last_cursor_rect, WSScreen::the().rect())); + dirty_rects.add(Rect::intersection(m_last_dnd_rect, WSScreen::the().rect())); dirty_rects.add(Rect::intersection(current_cursor_rect(), WSScreen::the().rect())); #ifdef DEBUG_COUNTERS dbgprintf("[WM] compose #%u (%u rects)\n", ++m_compose_count, dirty_rects.rects().size()); @@ -387,6 +388,9 @@ Rect WSCompositor::current_cursor_rect() const void WSCompositor::invalidate_cursor() { + auto& wm = WSWindowManager::the(); + if (wm.dnd_client()) + invalidate(wm.dnd_rect()); invalidate(current_cursor_rect()); } @@ -417,5 +421,17 @@ void WSCompositor::draw_cursor() auto& wm = WSWindowManager::the(); Rect cursor_rect = current_cursor_rect(); m_back_painter->blit(cursor_rect.location(), wm.active_cursor().bitmap(), wm.active_cursor().rect()); + + if (wm.dnd_client()) { + auto dnd_rect = wm.dnd_rect(); + if (!wm.dnd_text().is_empty()) { + m_back_painter->fill_rect(dnd_rect, Color(110, 34, 9, 200)); + m_back_painter->draw_text(dnd_rect, wm.dnd_text(), TextAlignment::Center, Color::White); + } + // FIXME: Also do the drag_bitmap if present + m_last_dnd_rect = dnd_rect; + } else { + m_last_dnd_rect = {}; + } m_last_cursor_rect = cursor_rect; } diff --git a/Servers/WindowServer/WSCompositor.h b/Servers/WindowServer/WSCompositor.h index 4a6a64d2638..b6fe538a572 100644 --- a/Servers/WindowServer/WSCompositor.h +++ b/Servers/WindowServer/WSCompositor.h @@ -61,6 +61,7 @@ private: DisjointRectSet m_dirty_rects; Rect m_last_cursor_rect; + Rect m_last_dnd_rect; Rect m_last_geometry_label_rect; String m_wallpaper_path; diff --git a/Servers/WindowServer/WSWindowManager.cpp b/Servers/WindowServer/WSWindowManager.cpp index 4f20d54350f..2bb4c12bd37 100644 --- a/Servers/WindowServer/WSWindowManager.cpp +++ b/Servers/WindowServer/WSWindowManager.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -625,6 +626,36 @@ bool WSWindowManager::process_ongoing_window_resize(const WSMouseEvent& event, W return true; } +bool WSWindowManager::process_ongoing_drag(WSMouseEvent& event, WSWindow*& hovered_window) +{ + if (!m_dnd_client) + return false; + if (!(event.type() == WSEvent::MouseUp && event.button() == MouseButton::Left)) + return true; + + hovered_window = nullptr; + for_each_visible_window_from_front_to_back([&](auto& window) { + if (window.frame().rect().contains(event.position())) { + hovered_window = &window; + return IterationDecision::Break; + } + return IterationDecision::Continue; + }); + + if (hovered_window) { + m_dnd_client->post_message(WindowClient::DragAccepted()); + if (hovered_window->client()) { + auto translated_event = event.translated(-hovered_window->position()); + hovered_window->client()->post_message(WindowClient::DragDropped(hovered_window->window_id(), translated_event.position(), m_dnd_text)); + } + } else { + m_dnd_client->post_message(WindowClient::DragCancelled()); + } + + end_dnd_drag(); + return true; +} + void WSWindowManager::set_cursor_tracking_button(WSButton* button) { m_cursor_tracking_button = button ? button->make_weak_ptr() : nullptr; @@ -708,6 +739,9 @@ void WSWindowManager::process_mouse_event(WSMouseEvent& event, WSWindow*& hovere { hovered_window = nullptr; + if (process_ongoing_drag(event, hovered_window)) + return; + if (process_ongoing_window_drag(event, hovered_window)) return; @@ -943,6 +977,12 @@ void WSWindowManager::event(CEvent& event) auto& key_event = static_cast(event); m_keyboard_modifiers = key_event.modifiers(); + if (key_event.type() == WSEvent::KeyDown && key_event.key() == Key_Escape && m_dnd_client) { + m_dnd_client->post_message(WindowClient::DragCancelled()); + end_dnd_drag(); + return; + } + if (key_event.type() == WSEvent::KeyDown && key_event.modifiers() == Mod_Logo && key_event.key() == Key_Tab) m_switcher.show(); if (m_switcher.is_visible()) { @@ -1138,3 +1178,29 @@ WSMenu* WSWindowManager::find_internal_menu_by_id(int menu_id) } return nullptr; } + +void WSWindowManager::start_dnd_drag(WSClientConnection& client, const String& text, GraphicsBitmap* bitmap) +{ + ASSERT(!m_dnd_client); + m_dnd_client = client.make_weak_ptr(); + m_dnd_text = text; + m_dnd_bitmap = bitmap; + WSCompositor::the().invalidate_cursor(); +} + +void WSWindowManager::end_dnd_drag() +{ + ASSERT(m_dnd_client); + WSCompositor::the().invalidate_cursor(); + m_dnd_client = nullptr; + m_dnd_text = {}; + m_dnd_bitmap = nullptr; +} + +Rect WSWindowManager::dnd_rect() const +{ + int width = font().width(m_dnd_text); + int height = font().glyph_height(); + auto location = WSCompositor::the().current_cursor_rect().center().translated(8, 8); + return Rect(location, { width, height }).inflated(4, 4); +} diff --git a/Servers/WindowServer/WSWindowManager.h b/Servers/WindowServer/WSWindowManager.h index b0ef9fcbf8b..eba7e8a44b1 100644 --- a/Servers/WindowServer/WSWindowManager.h +++ b/Servers/WindowServer/WSWindowManager.h @@ -66,6 +66,14 @@ public: Rect maximized_window_rect(const WSWindow&) const; + WSClientConnection* dnd_client() { return m_dnd_client.ptr(); } + const String& dnd_text() const { return m_dnd_text; } + const GraphicsBitmap* dnd_bitmap() const { return m_dnd_bitmap; } + Rect dnd_rect() const; + + void start_dnd_drag(WSClientConnection&, const String& text, GraphicsBitmap*); + void end_dnd_drag(); + WSWindow* active_window() { return m_active_window.ptr(); } const WSClientConnection* active_client() const; bool active_window_is_modal() const { return m_active_window && m_active_window->is_modal(); } @@ -156,6 +164,7 @@ private: void deliver_mouse_event(WSWindow& window, WSMouseEvent& event); bool process_ongoing_window_resize(const WSMouseEvent&, WSWindow*& hovered_window); bool process_ongoing_window_drag(WSMouseEvent&, WSWindow*& hovered_window); + bool process_ongoing_drag(WSMouseEvent&, WSWindow*& hovered_window); void start_window_drag(WSWindow&, const WSMouseEvent&); void set_hovered_window(WSWindow*); template @@ -268,6 +277,10 @@ private: }; Vector m_apps; HashMap> m_app_category_menus; + + WeakPtr m_dnd_client; + String m_dnd_text; + RefPtr m_dnd_bitmap; }; template diff --git a/Servers/WindowServer/WindowClient.ipc b/Servers/WindowServer/WindowClient.ipc index d4a09179213..245af0ba792 100644 --- a/Servers/WindowServer/WindowClient.ipc +++ b/Servers/WindowServer/WindowClient.ipc @@ -27,4 +27,9 @@ endpoint WindowClient = 4 WM_WindowRectChanged(i32 client_id, i32 window_id, Rect rect) =| AsyncSetWallpaperFinished(bool success) =| + + DragAccepted() =| + DragCancelled() =| + + DragDropped(i32 window_id, Point mouse_position, String text) =| } diff --git a/Servers/WindowServer/WindowServer.ipc b/Servers/WindowServer/WindowServer.ipc index 61ab691b9e2..cafa36fee3c 100644 --- a/Servers/WindowServer/WindowServer.ipc +++ b/Servers/WindowServer/WindowServer.ipc @@ -71,4 +71,6 @@ endpoint WindowServer = 2 GetWallpaper() => (String path) SetWindowOverrideCursor(i32 window_id, i32 cursor_type) => () + + StartDrag(String text, i32 bitmap_id, Size bitmap_size) => (bool started) }