From 001dfcf3d688ec3ce45cfdb2e0ef0d1fc96599a6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Jul 2025 14:43:00 -0700 Subject: [PATCH] apprt/gtk-ng: abstract our alert vs msg dialog into a superclass This introduces a new `GhosttyDialog` class that either inherits from `adw.MessageDialog` or `adw.AlertDialog`, depending on the version of libadwaita we compile against. This is the same logic we used previously. This lets us have a single libadw 1.2 blueprint file for all dialogs and we just do the right thing at compile time! --- src/apprt/gtk-ng/build/gresource.zig | 1 - .../gtk-ng/class/config_errors_dialog.zig | 43 +++------ src/apprt/gtk-ng/class/dialog.zig | 93 +++++++++++++++++++ .../gtk-ng/ui/1.2/config-errors-dialog.blp | 3 +- .../gtk-ng/ui/1.5/config-errors-dialog.blp | 27 ------ 5 files changed, 106 insertions(+), 61 deletions(-) create mode 100644 src/apprt/gtk-ng/class/dialog.zig delete mode 100644 src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp diff --git a/src/apprt/gtk-ng/build/gresource.zig b/src/apprt/gtk-ng/build/gresource.zig index ca3f762cc..103e8a063 100644 --- a/src/apprt/gtk-ng/build/gresource.zig +++ b/src/apprt/gtk-ng/build/gresource.zig @@ -32,7 +32,6 @@ pub const icon_sizes: []const comptime_int = &.{ 16, 32, 128, 256, 512, 1024 }; pub const blueprints: []const Blueprint = &.{ .{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 2, .name = "surface" }, - .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 5, .name = "window" }, }; diff --git a/src/apprt/gtk-ng/class/config_errors_dialog.zig b/src/apprt/gtk-ng/class/config_errors_dialog.zig index b8b0880d1..52591e622 100644 --- a/src/apprt/gtk-ng/class/config_errors_dialog.zig +++ b/src/apprt/gtk-ng/class/config_errors_dialog.zig @@ -7,18 +7,14 @@ const gresource = @import("../build/gresource.zig"); const adw_version = @import("../adw_version.zig"); const Common = @import("../class.zig").Common; const Config = @import("config.zig").Config; +const Dialog = @import("dialog.zig").Dialog; const log = std.log.scoped(.gtk_ghostty_config_errors_dialog); pub const ConfigErrorsDialog = extern struct { const Self = @This(); parent_instance: Parent, - - pub const Parent = if (adw_version.supportsDialogs()) - adw.AlertDialog - else - adw.MessageDialog; - + pub const Parent = Dialog; pub const getGObjectType = gobject.ext.defineClass(Self, .{ .name = "GhosttyConfigErrorsDialog", .instanceInit = &init, @@ -76,19 +72,11 @@ pub const ConfigErrorsDialog = extern struct { } pub fn present(self: *Self, parent: ?*gtk.Widget) void { - switch (Parent) { - adw.AlertDialog => self.as(adw.Dialog).present(parent), - adw.MessageDialog => self.as(gtk.Window).present(), - else => comptime unreachable, - } + self.as(Dialog).present(parent); } pub fn close(self: *Self) void { - switch (Parent) { - adw.AlertDialog => self.as(adw.Dialog).forceClose(), - adw.MessageDialog => self.as(gtk.Window).close(), - else => comptime unreachable, - } + self.as(Dialog).close(); } fn response( @@ -147,23 +135,14 @@ pub const ConfigErrorsDialog = extern struct { pub const Instance = Self; fn init(class: *Class) callconv(.C) void { + gobject.ext.ensureType(Dialog); gtk.Widget.Class.setTemplateFromResource( class.as(gtk.Widget.Class), - switch (Parent) { - adw.AlertDialog => comptime gresource.blueprint(.{ - .major = 1, - .minor = 5, - .name = "config-errors-dialog", - }), - - adw.MessageDialog => comptime gresource.blueprint(.{ - .major = 1, - .minor = 2, - .name = "config-errors-dialog", - }), - - else => comptime unreachable, - }, + comptime gresource.blueprint(.{ + .major = 1, + .minor = 2, + .name = "config-errors-dialog", + }), ); // Properties @@ -176,7 +155,7 @@ pub const ConfigErrorsDialog = extern struct { // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); - Parent.virtual_methods.response.implement(class, &response); + Dialog.virtual_methods.response.implement(class, &response); } pub fn as(class: *Class, comptime T: type) *T { diff --git a/src/apprt/gtk-ng/class/dialog.zig b/src/apprt/gtk-ng/class/dialog.zig new file mode 100644 index 000000000..fd7fed21b --- /dev/null +++ b/src/apprt/gtk-ng/class/dialog.zig @@ -0,0 +1,93 @@ +const std = @import("std"); +const adw = @import("adw"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const gresource = @import("../build/gresource.zig"); +const adw_version = @import("../adw_version.zig"); +const Common = @import("../class.zig").Common; +const Config = @import("config.zig").Config; + +const log = std.log.scoped(.gtk_ghostty_dialog); + +/// Dialog is a simple abstraction over the `adw.AlertDialog` and +/// `adw.MessageDialog` widgets, chosen at comptime based on the linked +/// Adwaita version. +/// +/// Once we drop support for Adwaita <= 1.2, this can be fully removed +/// and we can use `adw.AlertDialog` directly. +pub const Dialog = extern struct { + const Self = @This(); + parent_instance: Parent, + + pub const Parent = if (adw_version.supportsDialogs()) + adw.AlertDialog + else + adw.MessageDialog; + + pub const getGObjectType = gobject.ext.defineClass(Self, .{ + .name = "GhosttyDialog", + .classInit = &Class.init, + .parent_class = &Class.parent, + }); + + pub const virtual_methods = struct { + /// Forwarded from parent so subclasses can reference this + /// directly. This will make it easier to remove Dialog in the future. + pub const response = Parent.virtual_methods.response; + }; + + pub fn present(self: *Self, parent: ?*gtk.Widget) void { + switch (Parent) { + adw.AlertDialog => self.as(adw.Dialog).present(parent), + + adw.MessageDialog => { + // Reset the previous parent window + self.as(gtk.Window).setTransientFor(null); + + // We need to get the window for the parent in order + // to set the transient-for property on the MessageDialog. + if (parent) |widget| parent: { + const root = gtk.Widget.getRoot(widget) orelse break :parent; + const window = gobject.ext.cast( + gtk.Window, + root, + ) orelse break :parent; + self.as(gtk.Window).setTransientFor(window); + } + + self.as(gtk.Window).present(); + }, + + else => comptime unreachable, + } + } + + pub fn close(self: *Self) void { + switch (Parent) { + adw.AlertDialog => self.as(adw.Dialog).forceClose(), + adw.MessageDialog => self.as(gtk.Window).close(), + else => comptime unreachable, + } + } + + const C = Common(Self, null); + 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 { + _ = class; + } + + pub fn as(class: *Class, comptime T: type) *T { + return gobject.ext.as(T, class); + } + }; +}; diff --git a/src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp b/src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp index 7c16c3363..845909eb3 100644 --- a/src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp +++ b/src/apprt/gtk-ng/ui/1.2/config-errors-dialog.blp @@ -1,7 +1,8 @@ using Gtk 4.0; +// This is unused but if we remove it we get a blueprint-compiler error. using Adw 1; -template $GhosttyConfigErrorsDialog: Adw.MessageDialog { +template $GhosttyConfigErrorsDialog: $GhosttyDialog { heading: _("Configuration Errors"); body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors."); diff --git a/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp b/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp deleted file mode 100644 index 17ecac4e4..000000000 --- a/src/apprt/gtk-ng/ui/1.5/config-errors-dialog.blp +++ /dev/null @@ -1,27 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template $GhosttyConfigErrorsDialog: Adw.AlertDialog { - heading: _("Configuration Errors"); - body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors."); - - responses [ - ignore: _("Ignore"), - reload: _("Reload Configuration") suggested, - ] - - extra-child: ScrolledWindow { - min-content-width: 500; - min-content-height: 100; - - TextView { - editable: false; - cursor-visible: false; - top-margin: 8; - bottom-margin: 8; - left-margin: 8; - right-margin: 8; - buffer: bind (template.config as <$GhosttyConfig>).diagnostics-buffer; - } - }; -}