diff --git a/src/apprt/gtk-ng/class/surface.zig b/src/apprt/gtk-ng/class/surface.zig index 591a1ff5e..afcc9ffd0 100644 --- a/src/apprt/gtk-ng/class/surface.zig +++ b/src/apprt/gtk-ng/class/surface.zig @@ -284,6 +284,9 @@ pub const Surface = extern struct { /// True when we have a precision scroll in progress precision_scroll: bool = false, + // Template binds + drop_target: *gtk.DropTarget, + pub var offset: c_int = 0; }; @@ -793,6 +796,16 @@ pub const Surface = extern struct { priv.im_composing = false; priv.im_len = 0; + // Set up to handle items being dropped on our surface. Files can be dropped + // from Nautilus and strings can be dropped from many programs. The order + // of these types matter. + var drop_target_types = [_]gobject.Type{ + gdk.FileList.getGObjectType(), + gio.File.getGObjectType(), + gobject.ext.types.string, + }; + priv.drop_target.setGtypes(&drop_target_types, drop_target_types.len); + // Initialize our GLArea. We only set the values we can't set // in our blueprint file. const gl_area = priv.gl_area; @@ -1002,6 +1015,100 @@ pub const Surface = extern struct { //--------------------------------------------------------------- // Signal Handlers + fn dtDrop( + _: *gtk.DropTarget, + value: *gobject.Value, + _: f64, + _: f64, + self: *Self, + ) callconv(.c) c_int { + const alloc = Application.default().allocator(); + + if (g_value_holds( + value, + gdk.FileList.getGObjectType(), + )) { + var data = std.ArrayList(u8).init(alloc); + defer data.deinit(); + + var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ + .child_writer = data.writer(), + }; + const writer = shell_escape_writer.writer(); + + const list: ?*glib.SList = list: { + const unboxed = value.getBoxed() orelse return 0; + const fl: *gdk.FileList = @ptrCast(@alignCast(unboxed)); + break :list fl.getFiles(); + }; + defer if (list) |v| v.free(); + + { + var current: ?*glib.SList = list; + while (current) |item| : (current = item.f_next) { + const file: *gio.File = @ptrCast(@alignCast(item.f_data orelse continue)); + const path = file.getPath() orelse continue; + const slice = std.mem.span(path); + defer glib.free(path); + + writer.writeAll(slice) catch |err| { + log.err("unable to write path to buffer: {}", .{err}); + continue; + }; + writer.writeAll("\n") catch |err| { + log.err("unable to write to buffer: {}", .{err}); + continue; + }; + } + } + + const string = data.toOwnedSliceSentinel(0) catch |err| { + log.err("unable to convert to a slice: {}", .{err}); + return 0; + }; + defer alloc.free(string); + Clipboard.paste(self, string); + return 1; + } + + if (g_value_holds(value, gio.File.getGObjectType())) { + const object = value.getObject() orelse return 0; + const file = gobject.ext.cast(gio.File, object) orelse return 0; + const path = file.getPath() orelse return 0; + var data = std.ArrayList(u8).init(alloc); + defer data.deinit(); + + var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{ + .child_writer = data.writer(), + }; + const writer = shell_escape_writer.writer(); + writer.writeAll(std.mem.span(path)) catch |err| { + log.err("unable to write path to buffer: {}", .{err}); + return 0; + }; + writer.writeAll("\n") catch |err| { + log.err("unable to write to buffer: {}", .{err}); + return 0; + }; + + const string = data.toOwnedSliceSentinel(0) catch |err| { + log.err("unable to convert to a slice: {}", .{err}); + return 0; + }; + defer alloc.free(string); + return 1; + } + + if (g_value_holds(value, gobject.ext.types.string)) { + if (value.getString()) |string| { + Clipboard.paste(self, std.mem.span(string)); + } + return 1; + } + + return 1; + } + fn ecKeyPressed( ec_key: *gtk.EventControllerKey, keyval: c_uint, @@ -1669,6 +1776,7 @@ pub const Surface = extern struct { class.bindTemplateChildPrivate("url_left", .{}); class.bindTemplateChildPrivate("url_right", .{}); class.bindTemplateChildPrivate("resize_overlay", .{}); + class.bindTemplateChildPrivate("drop_target", .{}); class.bindTemplateChildPrivate("im_context", .{}); // Template Callbacks @@ -1683,6 +1791,7 @@ pub const Surface = extern struct { class.bindTemplateCallback("scroll", &ecMouseScroll); class.bindTemplateCallback("scroll_begin", &ecMouseScrollPrecisionBegin); class.bindTemplateCallback("scroll_end", &ecMouseScrollPrecisionEnd); + class.bindTemplateCallback("drop", &dtDrop); class.bindTemplateCallback("gl_realize", &glareaRealize); class.bindTemplateCallback("gl_unrealize", &glareaUnrealize); class.bindTemplateCallback("gl_render", &glareaRender); @@ -1758,17 +1867,6 @@ fn translateMouseButton(button: c_uint) input.MouseButton { /// A namespace for our clipboard-related functions so Surface isn't SO large. const Clipboard = struct { - /// Get the specific type of clipboard for a widget. - pub fn get( - widget: *gtk.Widget, - clipboard: apprt.Clipboard, - ) ?*gdk.Clipboard { - return switch (clipboard) { - .standard => widget.getClipboard(), - .selection, .primary => widget.getPrimaryClipboard(), - }; - } - /// Set the clipboard contents. pub fn set( self: *Surface, @@ -1837,6 +1935,52 @@ const Clipboard = struct { ); } + /// Paste explicit text directly into the surface, regardless of the + /// actual clipboard contents. + pub fn paste( + self: *Surface, + text: [:0]const u8, + ) void { + if (text.len == 0) return; + + const surface = self.private().core_surface orelse return; + surface.completeClipboardRequest( + .paste, + text, + false, + ) catch |err| switch (err) { + error.UnsafePaste, + error.UnauthorizedPaste, + => { + showClipboardConfirmation( + self, + .paste, + text, + ); + return; + }, + + else => { + log.warn( + "failed to complete clipboard request err={}", + .{err}, + ); + return; + }, + }; + } + + /// Get the specific type of clipboard for a widget. + fn get( + widget: *gtk.Widget, + clipboard: apprt.Clipboard, + ) ?*gdk.Clipboard { + return switch (clipboard) { + .standard => widget.getClipboard(), + .selection, .primary => widget.getPrimaryClipboard(), + }; + } + fn showClipboardConfirmation( self: *Surface, req: apprt.ClipboardRequest, @@ -2007,3 +2151,13 @@ const Clipboard = struct { state: apprt.ClipboardRequest, }; }; + +/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's +/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it. +fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool { + if (value_) |value| { + if (value.f_g_type == g_type) return true; + return gobject.typeCheckValueHolds(value, g_type) != 0; + } + return false; +} diff --git a/src/apprt/gtk-ng/ui/1.2/surface.blp b/src/apprt/gtk-ng/ui/1.2/surface.blp index e18b4d85d..faa2daea5 100644 --- a/src/apprt/gtk-ng/ui/1.2/surface.blp +++ b/src/apprt/gtk-ng/ui/1.2/surface.blp @@ -89,6 +89,11 @@ template $GhosttySurface: Adw.Bin { released => $mouse_up(); button: 0; } + + DropTarget drop_target { + drop => $drop(); + actions: copy; + } } IMMulticontext im_context { diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 159b3df54..231ab0c09 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2232,22 +2232,30 @@ fn gtkDrop( }; const writer = shell_escape_writer.writer(); - const unboxed = value.getBoxed() orelse return 0; - const fl: *gdk.FileList = @ptrCast(@alignCast(unboxed)); - var list: ?*glib.SList = fl.getFiles(); + const list: ?*glib.SList = list: { + const unboxed = value.getBoxed() orelse return 0; + const fl: *gdk.FileList = @ptrCast(@alignCast(unboxed)); + break :list fl.getFiles(); + }; + defer if (list) |v| v.free(); - while (list) |item| : (list = item.f_next) { - const file: *gio.File = @ptrCast(@alignCast(item.f_data orelse continue)); - const path = file.getPath() orelse continue; + { + var current: ?*glib.SList = list; + while (current) |item| : (current = item.f_next) { + const file: *gio.File = @ptrCast(@alignCast(item.f_data orelse continue)); + const path = file.getPath() orelse continue; + const slice = std.mem.span(path); + defer glib.free(path); - writer.writeAll(std.mem.span(path)) catch |err| { - log.err("unable to write path to buffer: {}", .{err}); - continue; - }; - writer.writeAll("\n") catch |err| { - log.err("unable to write to buffer: {}", .{err}); - continue; - }; + writer.writeAll(slice) catch |err| { + log.err("unable to write path to buffer: {}", .{err}); + continue; + }; + writer.writeAll("\n") catch |err| { + log.err("unable to write to buffer: {}", .{err}); + continue; + }; + } } const string = data.toOwnedSliceSentinel(0) catch |err| { diff --git a/valgrind.supp b/valgrind.supp index 6b7320b75..645b35b17 100644 --- a/valgrind.supp +++ b/valgrind.supp @@ -13,6 +13,75 @@ # You must gracefully exit Ghostty (do not SIGINT) by closing all windows # and quitting. Otherwise, we leave a number of GTK resources around. +{ + GDK Drag and Drop Leaks Data + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:g_malloc + fun:g_slice_alloc + fun:g_slice_alloc0 + fun:g_error_allocate + fun:g_error_new_literal + fun:g_set_error_literal + fun:g_output_stream_set_pending + fun:g_output_stream_write + fun:portal_file_deserializer_finish + fun:g_task_return_now + fun:g_task_return + fun:async_ready_splice_callback_wrapper + fun:g_task_return_now + fun:g_task_return + fun:real_splice_async_complete_cb + fun:async_ready_close_callback_wrapper + fun:g_task_return_now + fun:complete_in_idle_cb + fun:g_idle_dispatch + fun:g_main_context_dispatch_unlocked + fun:g_main_context_iterate_unlocked.isra.0 + fun:g_main_context_iteration + fun:apprt.gtk-ng.class.application.Application.run + fun:apprt.gtk-ng.App.run + fun:main_ghostty.main + fun:callMain + fun:callMainWithArgs + fun:main +} + +{ + GDK Drag and Drop Leaks Task + Memcheck:Leak + match-leak-kinds: definite + fun:calloc + fun:g_malloc0 + fun:g_type_create_instance + fun:g_object_new_internal.part.0 + fun:g_object_new_with_properties + fun:g_object_new + fun:g_task_new + fun:file_transfer_portal_retrieve_files + fun:portal_file_deserializer_finish + fun:g_task_return_now + fun:g_task_return + fun:async_ready_splice_callback_wrapper + fun:g_task_return_now + fun:g_task_return + fun:real_splice_async_complete_cb + fun:async_ready_close_callback_wrapper + fun:g_task_return_now + fun:complete_in_idle_cb + fun:g_idle_dispatch + fun:g_main_context_dispatch_unlocked + fun:g_main_context_iterate_unlocked.isra.0 + fun:g_main_context_iteration + fun:apprt.gtk-ng.class.application.Application.run + fun:apprt.gtk-ng.App.run + fun:main_ghostty.main + fun:callMain + fun:callMainWithArgs + fun:main +} + { GSK Renderer GPU Stuff Memcheck:Leak