mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-25 13:16:11 +03:00
apprt/gtk-ng: fix focus deadlock
This commit is contained in:
@ -33,6 +33,7 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 };
|
|||||||
///
|
///
|
||||||
/// These will be asserted to exist at runtime.
|
/// These will be asserted to exist at runtime.
|
||||||
pub const blueprints: []const Blueprint = &.{
|
pub const blueprints: []const Blueprint = &.{
|
||||||
|
.{ .major = 1, .minor = 2, .name = "clipboard-confirmation-dialog" },
|
||||||
.{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" },
|
.{ .major = 1, .minor = 2, .name = "close-confirmation-dialog" },
|
||||||
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
|
||||||
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
|
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
|
||||||
|
118
src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig
Normal file
118
src/apprt/gtk-ng/class/clipboard_confirmation_dialog.zig
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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 Common = @import("../class.zig").Common;
|
||||||
|
const Dialog = @import("dialog.zig").Dialog;
|
||||||
|
|
||||||
|
const log = std.log.scoped(.gtk_ghostty_clipboard_confirmation);
|
||||||
|
|
||||||
|
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 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",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const Private = struct {
|
||||||
|
/// The request that this dialog is for.
|
||||||
|
request: ?*apprt.ClipboardRequest = null,
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
|
||||||
|
self.as(Dialog).present(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
//---------------------------------------------------------------
|
||||||
|
// Virtual methods
|
||||||
|
|
||||||
|
fn dispose(self: *Self) callconv(.C) void {
|
||||||
|
gtk.Widget.disposeTemplate(
|
||||||
|
self.as(gtk.Widget),
|
||||||
|
getGObjectType(),
|
||||||
|
);
|
||||||
|
|
||||||
|
gobject.Object.virtual_methods.dispose.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),
|
||||||
|
comptime gresource.blueprint(.{
|
||||||
|
.major = 1,
|
||||||
|
.minor = 2,
|
||||||
|
.name = "clipboard-confirmation-dialog",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bindings
|
||||||
|
//class.bindTemplateChildPrivate("label", .{});
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
gobject.ext.registerProperties(class, &.{
|
||||||
|
properties.request.impl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Virtual methods
|
||||||
|
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const as = C.Class.as;
|
||||||
|
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
|
||||||
|
};
|
||||||
|
};
|
@ -21,6 +21,7 @@ const Common = @import("../class.zig").Common;
|
|||||||
const Application = @import("application.zig").Application;
|
const Application = @import("application.zig").Application;
|
||||||
const Config = @import("config.zig").Config;
|
const Config = @import("config.zig").Config;
|
||||||
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
|
const ResizeOverlay = @import("resize_overlay.zig").ResizeOverlay;
|
||||||
|
const ClipboardConfirmationDialog = @import("clipboard_confirmation_dialog.zig").ClipboardConfirmationDialog;
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_ghostty_surface);
|
const log = std.log.scoped(.gtk_ghostty_surface);
|
||||||
|
|
||||||
@ -56,6 +57,26 @@ pub const Surface = extern struct {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const focused = struct {
|
||||||
|
pub const name = "focused";
|
||||||
|
const impl = gobject.ext.defineProperty(
|
||||||
|
name,
|
||||||
|
Self,
|
||||||
|
bool,
|
||||||
|
.{
|
||||||
|
.nick = "Focused",
|
||||||
|
.blurb = "The focused state of the surface.",
|
||||||
|
.default = false,
|
||||||
|
.accessor = gobject.ext.privateFieldAccessor(
|
||||||
|
Self,
|
||||||
|
Private,
|
||||||
|
&Private.offset,
|
||||||
|
"focused",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
pub const @"mouse-hidden" = struct {
|
pub const @"mouse-hidden" = struct {
|
||||||
pub const name = "mouse-hidden";
|
pub const name = "mouse-hidden";
|
||||||
const impl = gobject.ext.defineProperty(
|
const impl = gobject.ext.defineProperty(
|
||||||
@ -196,6 +217,10 @@ pub const Surface = extern struct {
|
|||||||
/// The title of this surface, if any has been set.
|
/// The title of this surface, if any has been set.
|
||||||
title: ?[:0]const u8 = null,
|
title: ?[:0]const u8 = null,
|
||||||
|
|
||||||
|
/// The current focus state of the terminal based on the
|
||||||
|
/// focus events.
|
||||||
|
focused: bool = true,
|
||||||
|
|
||||||
/// The overlay we use for things such as the URL hover label
|
/// The overlay we use for things such as the URL hover label
|
||||||
/// or resize box. Bound from the template.
|
/// or resize box. Bound from the template.
|
||||||
overlay: *gtk.Overlay = undefined,
|
overlay: *gtk.Overlay = undefined,
|
||||||
@ -731,6 +756,7 @@ pub const Surface = extern struct {
|
|||||||
priv.cursor_pos = .{ .x = 0, .y = 0 };
|
priv.cursor_pos = .{ .x = 0, .y = 0 };
|
||||||
priv.mouse_shape = .text;
|
priv.mouse_shape = .text;
|
||||||
priv.mouse_hidden = false;
|
priv.mouse_hidden = false;
|
||||||
|
priv.focused = true;
|
||||||
priv.size = .{
|
priv.size = .{
|
||||||
// Funky numbers on purpose so they stand out if for some reason
|
// Funky numbers on purpose so they stand out if for some reason
|
||||||
// our size doesn't get properly set.
|
// our size doesn't get properly set.
|
||||||
@ -1242,30 +1268,41 @@ pub const Surface = extern struct {
|
|||||||
|
|
||||||
fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
fn ecFocusEnter(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
||||||
const priv = self.private();
|
const priv = self.private();
|
||||||
|
priv.focused = true;
|
||||||
|
|
||||||
if (priv.im_context) |im_context| {
|
if (priv.im_context) |im_context| {
|
||||||
im_context.as(gtk.IMContext).focusIn();
|
im_context.as(gtk.IMContext).focusIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priv.core_surface) |surface| {
|
_ = glib.idleAddOnce(idleFocus, self.ref());
|
||||||
surface.focusCallback(true) catch |err| {
|
|
||||||
log.warn("error in focus callback err={}", .{err});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
|
||||||
const priv = self.private();
|
const priv = self.private();
|
||||||
|
priv.focused = false;
|
||||||
|
|
||||||
if (priv.im_context) |im_context| {
|
if (priv.im_context) |im_context| {
|
||||||
im_context.as(gtk.IMContext).focusOut();
|
im_context.as(gtk.IMContext).focusOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priv.core_surface) |surface| {
|
_ = glib.idleAddOnce(idleFocus, self.ref());
|
||||||
surface.focusCallback(false) catch |err| {
|
}
|
||||||
log.warn("error in focus callback err={}", .{err});
|
|
||||||
};
|
/// The focus callback must be triggerd on an idle loop source because
|
||||||
}
|
/// there are actions within libghostty callbacks (such as showing close
|
||||||
|
/// confirmation dialogs) that can trigger focus loss and cause a deadlock
|
||||||
|
/// because the lock may be held during the callback.
|
||||||
|
///
|
||||||
|
/// Userdata should be a `*Surface`. This will unref once.
|
||||||
|
fn idleFocus(ud: ?*anyopaque) callconv(.c) void {
|
||||||
|
const self: *Self = @ptrCast(@alignCast(ud orelse return));
|
||||||
|
defer self.unref();
|
||||||
|
|
||||||
|
const priv = self.private();
|
||||||
|
const surface = priv.core_surface orelse return;
|
||||||
|
surface.focusCallback(priv.focused) catch |err| {
|
||||||
|
log.warn("error in focus callback err={}", .{err});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn gcMouseDown(
|
fn gcMouseDown(
|
||||||
@ -1880,6 +1917,7 @@ pub const Surface = extern struct {
|
|||||||
// Properties
|
// Properties
|
||||||
gobject.ext.registerProperties(class, &.{
|
gobject.ext.registerProperties(class, &.{
|
||||||
properties.config.impl,
|
properties.config.impl,
|
||||||
|
properties.focused.impl,
|
||||||
properties.@"mouse-shape".impl,
|
properties.@"mouse-shape".impl,
|
||||||
properties.@"mouse-hidden".impl,
|
properties.@"mouse-hidden".impl,
|
||||||
properties.@"mouse-hover-url".impl,
|
properties.@"mouse-hover-url".impl,
|
||||||
@ -1951,10 +1989,31 @@ const Clipboard = struct {
|
|||||||
clipboard_type: apprt.Clipboard,
|
clipboard_type: apprt.Clipboard,
|
||||||
confirm: bool,
|
confirm: bool,
|
||||||
) void {
|
) void {
|
||||||
_ = self;
|
const priv = self.private();
|
||||||
_ = val;
|
|
||||||
_ = clipboard_type;
|
// If no confirmation is necessary, set the clipboard.
|
||||||
_ = confirm;
|
if (!confirm and false) {
|
||||||
|
const clipboard = get(
|
||||||
|
priv.gl_area.as(gtk.Widget),
|
||||||
|
clipboard_type,
|
||||||
|
) orelse return;
|
||||||
|
clipboard.setText(val);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
const diag = gobject.ext.newInstance(
|
||||||
|
ClipboardConfirmationDialog,
|
||||||
|
.{ .request = &apprt.ClipboardRequest{
|
||||||
|
.osc_52_write = clipboard_type,
|
||||||
|
} },
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need to trigger the dialog
|
||||||
|
|
||||||
|
diag.present(self.as(gtk.Widget));
|
||||||
|
|
||||||
|
log.warn("TODO: confirmation window", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Request data from the clipboard (read the clipboard). This
|
/// Request data from the clipboard (read the clipboard). This
|
||||||
|
81
src/apprt/gtk-ng/ui/1.2/clipboard-confirmation-dialog.blp
Normal file
81
src/apprt/gtk-ng/ui/1.2/clipboard-confirmation-dialog.blp
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
using Gtk 4.0;
|
||||||
|
// This is unused but if we remove it we get a blueprint-compiler error.
|
||||||
|
using Adw 1;
|
||||||
|
|
||||||
|
template $GhosttyClipboardConfirmationDialog: $GhosttyDialog {
|
||||||
|
heading: _("Authorize Clipboard Access");
|
||||||
|
body: _("An application is attempting to write to the clipboard. The current clipboard contents are shown below.");
|
||||||
|
|
||||||
|
responses [
|
||||||
|
cancel: _("Deny") suggested,
|
||||||
|
ok: _("Allow") destructive,
|
||||||
|
]
|
||||||
|
|
||||||
|
default-response: "cancel";
|
||||||
|
close-response: "cancel";
|
||||||
|
|
||||||
|
extra-child: ListBox {
|
||||||
|
selection-mode: none;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"boxed-list-separate",
|
||||||
|
]
|
||||||
|
|
||||||
|
Overlay {
|
||||||
|
styles [
|
||||||
|
"osd",
|
||||||
|
"clipboard-overlay",
|
||||||
|
]
|
||||||
|
|
||||||
|
ScrolledWindow text_view_scroll {
|
||||||
|
width-request: 500;
|
||||||
|
height-request: 200;
|
||||||
|
|
||||||
|
TextView text_view {
|
||||||
|
cursor-visible: false;
|
||||||
|
editable: false;
|
||||||
|
monospace: true;
|
||||||
|
top-margin: 8;
|
||||||
|
left-margin: 8;
|
||||||
|
bottom-margin: 8;
|
||||||
|
right-margin: 8;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"clipboard-content-view",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[overlay]
|
||||||
|
Button reveal_button {
|
||||||
|
visible: false;
|
||||||
|
halign: end;
|
||||||
|
valign: start;
|
||||||
|
margin-end: 12;
|
||||||
|
margin-top: 12;
|
||||||
|
|
||||||
|
Image {
|
||||||
|
icon-name: "view-reveal-symbolic";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[overlay]
|
||||||
|
Button hide_button {
|
||||||
|
visible: false;
|
||||||
|
halign: end;
|
||||||
|
valign: start;
|
||||||
|
margin-end: 12;
|
||||||
|
margin-top: 12;
|
||||||
|
|
||||||
|
styles [
|
||||||
|
"opaque",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Adw.SwitchRow remember_choice {
|
||||||
|
title: _("Remember choice for this split");
|
||||||
|
subtitle: _("Reload configuration to show this prompt again");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
const build_config = @import("../build_config.zig");
|
||||||
|
|
||||||
/// ContentScale is the ratio between the current DPI and the platform's
|
/// ContentScale is the ratio between the current DPI and the platform's
|
||||||
/// default DPI. This is used to determine how much certain rendered elements
|
/// default DPI. This is used to determine how much certain rendered elements
|
||||||
/// need to be scaled up or down.
|
/// need to be scaled up or down.
|
||||||
@ -50,6 +52,16 @@ pub const ClipboardRequest = union(ClipboardRequestType) {
|
|||||||
|
|
||||||
/// A request to write clipboard contents via OSC 52.
|
/// A request to write clipboard contents via OSC 52.
|
||||||
osc_52_write: Clipboard,
|
osc_52_write: Clipboard,
|
||||||
|
|
||||||
|
/// Make this a valid gobject if we're in a GTK environment.
|
||||||
|
pub const getGObjectType = switch (build_config.app_runtime) {
|
||||||
|
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
|
||||||
|
ClipboardRequest,
|
||||||
|
.{ .name = "GhosttyClipboardRequest" },
|
||||||
|
),
|
||||||
|
|
||||||
|
.none => void,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The color scheme in use (light vs dark).
|
/// The color scheme in use (light vs dark).
|
||||||
|
Reference in New Issue
Block a user