gtk: port ConfigErrorsWindow to dialogs

pull/6792/head
Leah Amelia Chen 2025-02-25 08:55:56 +01:00
parent 1ee9c85954
commit 73341b052b
No known key found for this signature in database
7 changed files with 188 additions and 256 deletions

View File

@ -36,7 +36,7 @@ const CoreSurface = @import("../../Surface.zig");
const cgroup = @import("cgroup.zig");
const Surface = @import("Surface.zig");
const Window = @import("Window.zig");
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
const ConfigErrorsDialog = @import("ConfigErrorsDialog.zig");
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
const CloseDialog = @import("CloseDialog.zig");
const Split = @import("Split.zig");
@ -71,9 +71,6 @@ single_instance: bool,
/// The "none" cursor. We use one that is shared across the entire app.
cursor_none: ?*gdk.Cursor,
/// The configuration errors window, if it is currently open.
config_errors_window: ?*ConfigErrorsWindow = null,
/// The clipboard confirmation window, if it is currently open.
clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
@ -956,16 +953,22 @@ fn configChange(
log.warn("error cloning configuration err={}", .{err});
}
self.syncConfigChanges() catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
// App changes needs to show a toast that our configuration
// has reloaded.
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded();
}
const window = window: {
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| {
window.onConfigReloaded();
break :window window;
}
}
break :window null;
};
self.syncConfigChanges(window) catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
},
}
}
@ -1001,8 +1004,8 @@ pub fn reloadConfig(
}
/// Call this anytime the configuration changes.
fn syncConfigChanges(self: *App) !void {
try self.updateConfigErrors();
fn syncConfigChanges(self: *App, window: ?*Window) !void {
ConfigErrorsDialog.maybePresent(self, window);
try self.syncActionAccelerators();
// Load our runtime and custom CSS. If this fails then our window is just stuck
@ -1018,23 +1021,6 @@ fn syncConfigChanges(self: *App) !void {
};
}
/// This should be called whenever the configuration changes to update
/// the state of our config errors window. This will show the window if
/// there are new configuration errors and hide the window if the errors
/// are resolved.
fn updateConfigErrors(self: *App) !void {
if (!self.config._diagnostics.empty()) {
if (self.config_errors_window == null) {
try ConfigErrorsWindow.create(self);
assert(self.config_errors_window != null);
}
}
if (self.config_errors_window) |window| {
window.update();
}
}
fn syncActionAccelerators(self: *App) !void {
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
try self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
@ -1309,13 +1295,6 @@ pub fn run(self: *App) !void {
// Setup our actions
self.initActions();
// On startup, we want to check for configuration errors right away
// so we can show our error window. We also need to setup other initial
// state.
self.syncConfigChanges() catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
while (self.running) {
_ = glib.MainContext.iteration(self.ctx, 1);

View File

@ -0,0 +1,77 @@
/// Configuration errors window.
const ConfigErrorsDialog = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gobject = @import("gobject");
const gio = @import("gio");
const gtk = @import("gtk");
const adw = @import("adw");
const build_config = @import("../../build_config.zig");
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
const App = @import("App.zig");
const Window = @import("Window.zig");
const Builder = @import("Builder.zig");
const adwaita = @import("adwaita.zig");
const log = std.log.scoped(.gtk);
const DialogType = if (adwaita.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
builder: Builder,
dialog: *DialogType,
error_message: *gtk.TextBuffer,
pub fn maybePresent(app: *App, window: ?*Window) void {
if (app.config._diagnostics.empty()) return;
var builder = switch (DialogType) {
adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5, .blp),
adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2, .ui),
else => unreachable,
};
defer builder.deinit();
const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
var msg_buf: [4095:0]u8 = undefined;
var fbs = std.io.fixedBufferStream(&msg_buf);
for (app.config._diagnostics.items()) |diag| {
fbs.reset();
diag.write(fbs.writer()) catch |err| {
log.warn(
"error writing diagnostic to buffer err={}",
.{err},
);
continue;
};
error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
error_message.insertAtCursor("\n", 1);
}
_ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
const parent: ?*gtk.Widget = if (window) |w| @ptrCast(w.window) else null;
switch (DialogType) {
adw.AlertDialog => dialog.as(adw.Dialog).present(parent),
adw.MessageDialog => dialog.as(gtk.Window).present(),
else => unreachable,
}
}
fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.C) void {
if (std.mem.orderZ(u8, response, "reload") == .eq) {
app.reloadConfig(.app, .{}) catch |err| {
log.warn("error reloading config error={}", .{err});
return;
};
}
}

View File

@ -1,218 +0,0 @@
/// Configuration errors window.
const ConfigErrors = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
const App = @import("App.zig");
const View = @import("View.zig");
const c = @import("c.zig").c;
const log = std.log.scoped(.gtk);
app: *App,
window: *c.GtkWindow,
view: PrimaryView,
pub fn create(app: *App) !void {
if (app.config_errors_window != null) return error.InvalidOperation;
const alloc = app.core_app.alloc;
const self = try alloc.create(ConfigErrors);
errdefer alloc.destroy(self);
try self.init(app);
app.config_errors_window = self;
}
pub fn update(self: *ConfigErrors) void {
if (self.app.config._diagnostics.empty()) {
c.gtk_window_destroy(@ptrCast(self.window));
return;
}
self.view.update(&self.app.config);
_ = c.gtk_window_present(self.window);
_ = c.gtk_widget_grab_focus(@ptrCast(self.window));
}
/// Not public because this should be called by the GTK lifecycle.
fn destroy(self: *ConfigErrors) void {
const alloc = self.app.core_app.alloc;
self.app.config_errors_window = null;
alloc.destroy(self);
}
fn init(self: *ConfigErrors, app: *App) !void {
// Create the window
const window = c.gtk_window_new();
const gtk_window: *c.GtkWindow = @ptrCast(window);
errdefer c.gtk_window_destroy(gtk_window);
c.gtk_window_set_title(gtk_window, "Configuration Errors");
c.gtk_window_set_default_size(gtk_window, 600, 275);
c.gtk_window_set_resizable(gtk_window, 0);
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "config-errors-window");
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
// Set some state
self.* = .{
.app = app,
.window = gtk_window,
.view = undefined,
};
// Show the window
const view = try PrimaryView.init(self);
self.view = view;
c.gtk_window_set_child(@ptrCast(window), view.root);
c.gtk_widget_show(window);
}
fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self = userdataSelf(ud.?);
self.destroy();
}
fn userdataSelf(ud: *anyopaque) *ConfigErrors {
return @ptrCast(@alignCast(ud));
}
const PrimaryView = struct {
root: *c.GtkWidget,
text: *c.GtkTextView,
pub fn init(root: *ConfigErrors) !PrimaryView {
// All our widgets
const label = c.gtk_label_new(
"One or more configuration errors were found while loading " ++
"the configuration. Please review the errors below and reload " ++
"your configuration or ignore the erroneous lines.",
);
const buf = contentsBuffer(&root.app.config);
defer c.g_object_unref(buf);
const buttons = try ButtonsView.init(root);
const text_scroll = c.gtk_scrolled_window_new();
errdefer c.g_object_unref(text_scroll);
const text = c.gtk_text_view_new_with_buffer(buf);
errdefer c.g_object_unref(text);
c.gtk_scrolled_window_set_child(@ptrCast(text_scroll), text);
// Create our view
const view = try View.init(&.{
.{ .name = "label", .widget = label },
.{ .name = "text", .widget = text_scroll },
.{ .name = "buttons", .widget = buttons.root },
}, &vfl);
errdefer view.deinit();
// We can do additional settings once the layout is setup
c.gtk_label_set_wrap(@ptrCast(label), 1);
c.gtk_text_view_set_editable(@ptrCast(text), 0);
c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0);
c.gtk_text_view_set_top_margin(@ptrCast(text), 8);
c.gtk_text_view_set_bottom_margin(@ptrCast(text), 8);
c.gtk_text_view_set_left_margin(@ptrCast(text), 8);
c.gtk_text_view_set_right_margin(@ptrCast(text), 8);
return .{ .root = view.root, .text = @ptrCast(text) };
}
pub fn update(self: *PrimaryView, config: *const Config) void {
const buf = contentsBuffer(config);
defer c.g_object_unref(buf);
c.gtk_text_view_set_buffer(@ptrCast(self.text), buf);
}
/// Returns the GtkTextBuffer for the config errors that we want to show.
fn contentsBuffer(config: *const Config) *c.GtkTextBuffer {
const buf = c.gtk_text_buffer_new(null);
errdefer c.g_object_unref(buf);
var msg_buf: [4096]u8 = undefined;
var fbs = std.io.fixedBufferStream(&msg_buf);
for (config._diagnostics.items()) |diag| {
fbs.reset();
diag.write(fbs.writer()) catch |err| {
log.warn(
"error writing diagnostic to buffer err={}",
.{err},
);
continue;
};
const msg = fbs.getWritten();
c.gtk_text_buffer_insert_at_cursor(buf, msg.ptr, @intCast(msg.len));
c.gtk_text_buffer_insert_at_cursor(buf, "\n", -1);
}
return buf;
}
const vfl = [_][*:0]const u8{
"H:|-8-[label]-8-|",
"H:|[text]|",
"H:|[buttons]|",
"V:|[label(<=80)][text(>=100)]-[buttons]-|",
};
};
const ButtonsView = struct {
root: *c.GtkWidget,
pub fn init(root: *ConfigErrors) !ButtonsView {
const ignore_button = c.gtk_button_new_with_label("Ignore");
errdefer c.g_object_unref(ignore_button);
const reload_button = c.gtk_button_new_with_label("Reload Configuration");
errdefer c.g_object_unref(reload_button);
// Create our view
const view = try View.init(&.{
.{ .name = "ignore", .widget = ignore_button },
.{ .name = "reload", .widget = reload_button },
}, &vfl);
// Signals
_ = c.g_signal_connect_data(
ignore_button,
"clicked",
c.G_CALLBACK(&gtkIgnoreClick),
root,
null,
c.G_CONNECT_DEFAULT,
);
_ = c.g_signal_connect_data(
reload_button,
"clicked",
c.G_CALLBACK(&gtkReloadClick),
root,
null,
c.G_CONNECT_DEFAULT,
);
return .{ .root = view.root };
}
fn gtkIgnoreClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *ConfigErrors = @ptrCast(@alignCast(ud));
c.gtk_window_destroy(@ptrCast(self.window));
}
fn gtkReloadClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self: *ConfigErrors = @ptrCast(@alignCast(ud));
self.app.reloadConfig(.app, .{}) catch |err| {
log.warn("error reloading config error={}", .{err});
return;
};
}
const vfl = [_][*:0]const u8{
"H:[ignore]-8-[reload]-8-|",
};
};

View File

@ -60,6 +60,7 @@ pub const VersionedBuilderXML = struct {
};
pub const ui_files = [_]VersionedBuilderXML{
.{ .major = 1, .minor = 2, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" },
.{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" },
.{ .major = 1, .minor = 2, .name = "ccw-paste" },
@ -73,6 +74,7 @@ pub const VersionedBlueprint = struct {
pub const blueprint_files = [_]VersionedBlueprint{
.{ .major = 1, .minor = 5, .name = "prompt-title-dialog" },
.{ .major = 1, .minor = 5, .name = "config-errors-dialog" },
.{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" },
.{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" },
.{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" },

View File

@ -0,0 +1,28 @@
using Gtk 4.0;
using Adw 1;
Adw.MessageDialog config_errors_dialog {
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: TextBuffer error_message { };
}
};
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
DO NOT EDIT!
This file was @generated by blueprint-compiler. Instead, edit the
corresponding .blp file and regenerate this file with blueprint-compiler.
-->
<interface>
<requires lib="gtk" version="4.0"/>
<object class="AdwMessageDialog" id="config_errors_dialog">
<property name="heading" translatable="yes">Configuration Errors</property>
<property name="body" translatable="yes">One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.</property>
<responses>
<response id="ignore" translatable="yes">Ignore</response>
<response id="reload" translatable="yes" appearance="suggested">Reload Configuration</response>
</responses>
<property name="extra-child">
<object class="GtkScrolledWindow">
<property name="min-content-width">500</property>
<property name="min-content-height">100</property>
<child>
<object class="GtkTextView">
<property name="editable">false</property>
<property name="cursor-visible">false</property>
<property name="top-margin">8</property>
<property name="bottom-margin">8</property>
<property name="left-margin">8</property>
<property name="right-margin">8</property>
<property name="buffer">
<object class="GtkTextBuffer" id="error_message"></object>
</property>
</object>
</child>
</object>
</property>
</object>
</interface>

View File

@ -0,0 +1,28 @@
using Gtk 4.0;
using Adw 1;
Adw.AlertDialog config_errors_dialog {
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: TextBuffer error_message { };
}
};
}