apprt/gtk-ng: hook up all the refs to show the dialog

This commit is contained in:
Mitchell Hashimoto
2025-07-17 08:28:58 -07:00
parent b253e2efe2
commit 0da47903f4
5 changed files with 171 additions and 34 deletions

View File

@ -10,6 +10,8 @@ pub const Application = @import("gtk-ng/class/application.zig").Application;
pub const Window = @import("gtk-ng/class/window.zig").Window;
pub const Config = @import("gtk-ng/class/config.zig").Config;
pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef;
test {
@import("std").testing.refAllDecls(@This());
}

View File

@ -19,6 +19,7 @@ const CoreConfig = configpkg.Config;
const adw_version = @import("../adw_version.zig");
const gtk_version = @import("../gtk_version.zig");
const WeakRef = @import("../weak_ref.zig").WeakRef;
const Config = @import("config.zig").Config;
const Window = @import("window.zig").Window;
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
@ -72,6 +73,9 @@ pub const Application = extern struct {
/// only be set by the main loop thread.
running: bool = false,
/// If non-null, we're currently showing a config errors dialog.
config_errors_dialog: WeakRef(ConfigErrorsDialog) = .{},
var offset: c_int = 0;
};
@ -107,14 +111,10 @@ pub const Application = extern struct {
// us to startup.
var default: CoreConfig = try .default(alloc);
errdefer default.deinit();
const config_arena = default._arena.?.allocator();
try default._diagnostics.append(config_arena, .{
.message = try std.fmt.allocPrintZ(
config_arena,
"error loading user configuration: {}",
.{err},
),
});
try default.addDiagnosticFmt(
"error loading user configuration: {}",
.{err},
);
break :err default;
};
@ -190,8 +190,10 @@ pub const Application = extern struct {
// callback that GObject calls, but we can't pass this data through
// to there (and we don't need it there directly) so this is here.
const priv = self.private();
priv.core_app = core_app;
priv.config = config_obj;
priv.* = .{
.core_app = core_app,
.config = config_obj,
};
return self;
}
@ -303,22 +305,6 @@ pub const Application = extern struct {
}
}
pub fn as(app: *Self, comptime T: type) *T {
return gobject.ext.as(T, app);
}
pub fn unref(self: *Self) void {
gobject.Object.unref(self.as(gobject.Object));
}
fn private(self: *Self) *Private {
return gobject.ext.impl_helpers.getPrivate(
self,
Private,
Private.offset,
);
}
fn startup(self: *Self) callconv(.C) void {
log.debug("startup", .{});
@ -334,19 +320,26 @@ pub const Application = extern struct {
self.startupStyleManager();
// Setup our cgroup for the application.
self.startupCgroup() catch {
log.warn("TODO", .{});
self.startupCgroup() catch |err| {
log.warn("cgroup initialization failed err={}", .{err});
// Add it to our config diagnostics so it shows up in a GUI dialog.
// Admittedly this has two issues: (1) we shuldn't be using the
// config errors dialog for this long term and (2) using a mut
// ref to the config wouldn't propagate changes to UI properly,
// but we're in startup mode so its okay.
const config = self.private().config.getMut();
config.addDiagnosticFmt(
"cgroup initialization failed: {}",
.{err},
) catch {};
};
// If we have any config diagnostics from loading, then we
// show the diagnostics dialog. We show this one as a general
// modal (not to any specific window) because we don't even
// know if the window will load.
const priv = self.private();
if (priv.config.hasDiagnostics()) {
const dialog: *ConfigErrorsDialog = .new(priv.config);
dialog.present(null);
}
self.showConfigErrorsDialog();
}
/// Configure libxev to use a specific backend.
@ -490,6 +483,19 @@ pub const Application = extern struct {
// gtk.Window.present(win.as(gtk.Window));
}
fn dispose(self: *Self) callconv(.C) void {
const priv = self.private();
if (priv.config_errors_dialog.get()) |diag| {
diag.close();
diag.unref(); // strong ref from get()
}
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.C) void {
self.deinit();
gobject.Object.virtual_methods.finalize.call(
@ -513,10 +519,62 @@ pub const Application = extern struct {
log.debug("style manager changed scheme={}", .{color_scheme});
}
/// Show the config errors dialog if the config on our application
/// has diagnostics.
fn showConfigErrorsDialog(self: *Self) void {
const priv = self.private();
// If we already have a dialog, just update the config.
if (priv.config_errors_dialog.get()) |diag| {
defer diag.unref(); // get gets a strong ref
var value = gobject.ext.Value.newFrom(priv.config);
defer value.unset();
gobject.Object.setProperty(
diag.as(gobject.Object),
"config",
&value,
);
if (!priv.config.hasDiagnostics()) {
diag.close();
} else {
diag.present(null);
}
return;
}
// No diagnostics, do nothing.
if (!priv.config.hasDiagnostics()) return;
// No dialog yet, initialize a new one. There's no need to unref
// here because the widget that it becomes a part of takes ownership.
const dialog: *ConfigErrorsDialog = .new(priv.config);
dialog.present(null);
priv.config_errors_dialog.set(dialog);
}
fn allocator(self: *Self) std.mem.Allocator {
return self.private().core_app.alloc;
}
pub fn as(app: *Self, comptime T: type) *T {
return gobject.ext.as(T, app);
}
pub fn unref(self: *Self) void {
gobject.Object.unref(self.as(gobject.Object));
}
fn private(self: *Self) *Private {
return gobject.ext.impl_helpers.getPrivate(
self,
Private,
Private.offset,
);
}
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
@ -542,6 +600,7 @@ pub const Application = extern struct {
// Virtual methods
gio.Application.virtual_methods.activate.implement(class, &activate);
gio.Application.virtual_methods.startup.implement(class, &startup);
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
};

View File

@ -7,7 +7,7 @@ const gresource = @import("../build/gresource.zig");
const adw_version = @import("../adw_version.zig");
const Config = @import("config.zig").Config;
const log = std.log.scoped(.gtk_ghostty_window);
const log = std.log.scoped(.gtk_ghostty_config_errors_dialog);
pub const ConfigErrorsDialog = extern struct {
const Self = @This();
@ -78,6 +78,13 @@ pub const ConfigErrorsDialog = extern struct {
}
}
pub fn close(self: *Self) void {
switch (Parent) {
adw.AlertDialog => self.as(adw.Dialog).forceClose(),
else => unreachable,
}
}
fn response(
self: *Self,
response_id: [*:0]const u8,
@ -92,6 +99,7 @@ pub const ConfigErrorsDialog = extern struct {
}
fn dispose(self: *Self) callconv(.C) void {
log.warn("DISPOSE", .{});
gtk.Widget.disposeTemplate(self.as(gtk.Widget), getGObjectType());
const priv = self.private();
@ -121,6 +129,14 @@ pub const ConfigErrorsDialog = extern struct {
return gobject.ext.as(T, win);
}
pub fn ref(self: *Self) *Self {
return @ptrCast(@alignCast(gobject.Object.ref(self.as(gobject.Object))));
}
pub fn unref(self: *Self) void {
gobject.Object.unref(self.as(gobject.Object));
}
fn private(self: *Self) *Private {
return gobject.ext.impl_helpers.getPrivate(
self,

View File

@ -0,0 +1,43 @@
const std = @import("std");
const gtk = @import("gtk");
const gobject = @import("gobject");
/// A lightweight wrapper around gobject.WeakRef to make it type-safe
/// to hold a single type of value.
pub fn WeakRef(comptime T: type) type {
return struct {
const Self = @This();
ref: gobject.WeakRef = std.mem.zeroes(gobject.WeakRef),
/// Set the weak reference to the given object. This will not
/// increase the reference count of the object.
pub fn set(self: *Self, v: *T) void {
self.ref.set(v.as(gobject.Object));
}
/// Get a strong reference to the object, or null if the object
/// has been finalized. This increases the reference count by one.
pub fn get(self: *Self) ?*T {
// The GIR of g_weak_ref_get has a bug where the optional
// is not encoded. Or, it may be a bug in zig-gobject.
const obj_: ?*gobject.Object = @ptrCast(self.ref.get());
const obj = obj_ orelse return null;
// We can't use `as` because `as` guarantees conversion and
// that can't be statically guaranteed.
return gobject.ext.cast(T, obj);
}
};
}
test WeakRef {
const testing = std.testing;
var ref: WeakRef(gtk.TextBuffer) = .{};
const obj: *gtk.TextBuffer = .new(null);
ref.set(obj);
ref.get().?.unref(); // The "?" asserts non-null
obj.unref();
try testing.expect(ref.get() == null);
}

View File

@ -4068,6 +4068,23 @@ fn compatBoldIsBright(
return true;
}
/// Add a diagnostic message to the config with the given string.
/// This is always added with a location of "none".
pub fn addDiagnosticFmt(
self: *Config,
comptime fmt: []const u8,
args: anytype,
) Allocator.Error!void {
const alloc = self._arena.?.allocator();
try self._diagnostics.append(alloc, .{
.message = try std.fmt.allocPrintZ(
alloc,
fmt,
args,
),
});
}
/// Create a shallow copy of this config. This will share all the memory
/// allocated with the previous config but will have a new arena for
/// any changes or new allocations. The config should have `deinit`