const std = @import("std"); const assert = std.debug.assert; const adw = @import("adw"); const glib = @import("glib"); const gobject = @import("gobject"); const gtk = @import("gtk"); const apprt = @import("../../../apprt.zig"); const gresource = @import("../build/gresource.zig"); const i18n = @import("../../../os/main.zig").i18n; const adw_version = @import("../adw_version.zig"); const Common = @import("../class.zig").Common; const Dialog = @import("dialog.zig").Dialog; const log = std.log.scoped(.gtk_ghostty_clipboard_confirmation); /// Whether we're able to have the remember switch const can_remember = adw_version.supportsSwitchRow(); pub const ClipboardConfirmationDialog = extern struct { const Self = @This(); parent_instance: Parent, pub const Parent = Dialog; pub const getGObjectType = gobject.ext.defineClass(Self, .{ .name = "GhosttyClipboardConfirmationDialog", .instanceInit = &init, .classInit = &Class.init, .parent_class = &Class.parent, .private = .{ .Type = Private, .offset = &Private.offset }, }); pub const properties = struct { pub const @"can-remember" = struct { pub const name = "can-remember"; const impl = gobject.ext.defineProperty( name, Self, bool, .{ .nick = "Can Remember", .blurb = "Allow remembering the choice.", .default = false, .accessor = gobject.ext.privateFieldAccessor( Self, Private, &Private.offset, "can_remember", ), }, ); }; pub const request = struct { pub const name = "request"; const impl = gobject.ext.defineProperty( name, Self, ?*apprt.ClipboardRequest, .{ .nick = "Request", .blurb = "The clipboard request.", .accessor = gobject.ext.privateFieldAccessor( Self, Private, &Private.offset, "request", ), }, ); }; pub const @"clipboard-contents" = struct { pub const name = "clipboard-contents"; const impl = gobject.ext.defineProperty( name, Self, ?*gtk.TextBuffer, .{ .nick = "Clipboard Contents", .blurb = "The clipboard contents being read/written.", .accessor = gobject.ext.privateFieldAccessor( Self, Private, &Private.offset, "clipboard_contents", ), }, ); }; pub const blur = struct { pub const name = "blur"; const impl = gobject.ext.defineProperty( name, Self, bool, .{ .nick = "Blur", .blurb = "Blur the contents, allowing the user to reveal.", .default = false, .accessor = gobject.ext.privateFieldAccessor( Self, Private, &Private.offset, "blur", ), }, ); }; }; pub const signals = struct { pub const deny = struct { pub const name = "deny"; pub const connect = impl.connect; const impl = gobject.ext.defineSignal( name, Self, &.{bool}, void, ); }; pub const confirm = struct { pub const name = "confirm"; pub const connect = impl.connect; const impl = gobject.ext.defineSignal( name, Self, &.{bool}, void, ); }; }; const Private = struct { /// The request that this dialog is for. request: ?*apprt.ClipboardRequest = null, /// The clipboard contents being read/written. clipboard_contents: ?*gtk.TextBuffer = null, /// Whether the contents should be blurred. blur: bool = false, /// Whether the user can remember the choice. can_remember: bool = false, // Template bindings text_view_scroll: *gtk.ScrolledWindow, text_view: *gtk.TextView, reveal_button: *gtk.Button, hide_button: *gtk.Button, remember_choice: if (can_remember) *adw.SwitchRow else void, pub var offset: c_int = 0; }; pub fn new() *Self { return gobject.ext.newInstance(Self, .{}); } fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); const priv = self.private(); // Signals _ = gtk.Button.signals.clicked.connect( priv.reveal_button, *Self, revealButtonClicked, self, .{}, ); _ = gtk.Button.signals.clicked.connect( priv.hide_button, *Self, hideButtonClicked, self, .{}, ); // Some property signals _ = gobject.Object.signals.notify.connect( self, ?*anyopaque, &propBlur, null, .{ .detail = "blur" }, ); _ = gobject.Object.signals.notify.connect( self, ?*anyopaque, &propRequest, null, .{ .detail = "request" }, ); // Trigger initial values self.propBlur(undefined, null); self.propRequest(undefined, null); } pub fn present(self: *Self, parent: ?*gtk.Widget) void { self.as(Dialog).present(parent); } /// Get the clipboard request without copying. pub fn getRequest(self: *Self) ?*apprt.ClipboardRequest { return self.private().request; } /// Get the clipboard contents without copying. pub fn getClipboardContents(self: *Self) ?*gtk.TextBuffer { return self.private().clipboard_contents; } //--------------------------------------------------------------- // Signal Handlers fn propBlur( self: *Self, _: *gobject.ParamSpec, _: ?*anyopaque, ) callconv(.c) void { const priv = self.private(); if (priv.blur) { priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false)); priv.text_view.as(gtk.Widget).addCssClass("blurred"); priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true)); priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false)); } else { priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true)); priv.text_view.as(gtk.Widget).removeCssClass("blurred"); priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false)); priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false)); } } fn propRequest( self: *Self, _: *gobject.ParamSpec, _: ?*anyopaque, ) callconv(.c) void { const priv = self.private(); const req = priv.request orelse return; switch (req.*) { .osc_52_write => { self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access")); self.as(Dialog.Parent).setBody(i18n._("An application is attempting to write to the clipboard. The current clipboard contents are shown below.")); }, .osc_52_read => { self.as(Dialog.Parent).setHeading(i18n._("Authorize Clipboard Access")); self.as(Dialog.Parent).setBody(i18n._("An application is attempting to read from the clipboard. The current clipboard contents are shown below.")); }, .paste => { self.as(Dialog.Parent).setHeading(i18n._("Warning: Potentially Unsafe Paste")); self.as(Dialog.Parent).setBody(i18n._("Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.")); }, } } fn revealButtonClicked(_: *gtk.Button, self: *Self) callconv(.c) void { const priv = self.private(); priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true)); priv.text_view.as(gtk.Widget).removeCssClass("blurred"); priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(true)); priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false)); } fn hideButtonClicked(_: *gtk.Button, self: *Self) callconv(.c) void { const priv = self.private(); priv.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false)); priv.text_view.as(gtk.Widget).addCssClass("blurred"); priv.hide_button.as(gtk.Widget).setVisible(@intFromBool(false)); priv.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true)); } //--------------------------------------------------------------- // Virtual methods fn response( self: *Self, response_id: [*:0]const u8, ) callconv(.C) void { const remember: bool = if (comptime can_remember) remember: { const priv = self.private(); break :remember priv.remember_choice.getActive() != 0; } else false; if (std.mem.orderZ(u8, response_id, "cancel") == .eq) { signals.deny.impl.emit( self, null, .{remember}, null, ); } else if (std.mem.orderZ(u8, response_id, "ok") == .eq) { signals.confirm.impl.emit( self, null, .{remember}, null, ); } } fn dispose(self: *Self) callconv(.C) void { const priv = self.private(); if (priv.clipboard_contents) |v| { v.unref(); priv.clipboard_contents = null; } gtk.Widget.disposeTemplate( self.as(gtk.Widget), getGObjectType(), ); gobject.Object.virtual_methods.dispose.call( Class.parent, self.as(Parent), ); } fn finalize(self: *Self) callconv(.C) void { const priv = self.private(); if (priv.request) |v| { glib.ext.destroy(v); priv.request = null; } gobject.Object.virtual_methods.finalize.call( Class.parent, self.as(Parent), ); } const C = Common(Self, Private); pub const as = C.as; pub const ref = C.ref; pub const unref = C.unref; const private = C.private; pub const Class = extern struct { parent_class: Parent.Class, var parent: *Parent.Class = undefined; pub const Instance = Self; fn init(class: *Class) callconv(.C) void { gtk.Widget.Class.setTemplateFromResource( class.as(gtk.Widget.Class), if (comptime adw_version.atLeast(1, 4, 0)) comptime gresource.blueprint(.{ .major = 1, .minor = 4, .name = "clipboard-confirmation-dialog", }) else comptime gresource.blueprint(.{ .major = 1, .minor = 0, .name = "clipboard-confirmation-dialog", }), ); // Bindings class.bindTemplateChildPrivate("text_view_scroll", .{}); class.bindTemplateChildPrivate("text_view", .{}); class.bindTemplateChildPrivate("hide_button", .{}); class.bindTemplateChildPrivate("reveal_button", .{}); if (comptime can_remember) { class.bindTemplateChildPrivate("remember_choice", .{}); } // Properties gobject.ext.registerProperties(class, &.{ properties.blur.impl, properties.@"can-remember".impl, properties.@"clipboard-contents".impl, properties.request.impl, }); // Signals signals.confirm.impl.register(.{}); signals.deny.impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); gobject.Object.virtual_methods.finalize.implement(class, &finalize); Dialog.virtual_methods.response.implement(class, &response); } pub const as = C.Class.as; pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate; }; };