apprt/gtk: working on config errors window

This commit is contained in:
Mitchell Hashimoto
2023-09-17 21:37:57 -07:00
parent dbd8add23e
commit bd3b5d5332
2 changed files with 196 additions and 2 deletions

View File

@ -11,6 +11,7 @@
const App = @This(); const App = @This();
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert;
const builtin = @import("builtin"); const builtin = @import("builtin");
const glfw = @import("glfw"); const glfw = @import("glfw");
const configpkg = @import("../../config.zig"); const configpkg = @import("../../config.zig");
@ -20,6 +21,7 @@ const CoreSurface = @import("../../Surface.zig");
const Surface = @import("Surface.zig"); const Surface = @import("Surface.zig");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
const c = @import("c.zig"); const c = @import("c.zig");
const log = std.log.scoped(.gtk); const log = std.log.scoped(.gtk);
@ -35,6 +37,9 @@ ctx: *c.GMainContext,
/// The "none" cursor. We use one that is shared across the entire app. /// The "none" cursor. We use one that is shared across the entire app.
cursor_none: ?*c.GdkCursor, cursor_none: ?*c.GdkCursor,
/// The configuration errors window, if it is currently open.
config_errors_window: ?*ConfigErrorsWindow = null,
/// This is set to false when the main loop should exit. /// This is set to false when the main loop should exit.
running: bool = true, running: bool = true,
@ -80,7 +85,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
_ = c.g_signal_connect_data( _ = c.g_signal_connect_data(
app, app,
"activate", "activate",
c.G_CALLBACK(&activate), c.G_CALLBACK(&gtkActivate),
core_app, core_app,
null, null,
c.G_CONNECT_DEFAULT, c.G_CONNECT_DEFAULT,
@ -155,9 +160,27 @@ pub fn reloadConfig(self: *App) !?*const Config {
self.config.deinit(); self.config.deinit();
self.config = config; self.config = config;
// If there were errors, report them
self.updateConfigErrors() catch |err| {
log.warn("error handling configuration errors err={}", .{err});
};
return &self.config; return &self.config;
} }
/// 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._errors.empty()) {
if (self.config_errors_window == null) {
try ConfigErrorsWindow.create(self);
assert(self.config_errors_window != null);
}
}
}
/// Called by CoreApp to wake up the event loop. /// Called by CoreApp to wake up the event loop.
pub fn wakeup(self: App) void { pub fn wakeup(self: App) void {
_ = self; _ = self;
@ -166,6 +189,14 @@ pub fn wakeup(self: App) void {
/// Run the event loop. This doesn't return until the app exits. /// Run the event loop. This doesn't return until the app exits.
pub fn run(self: *App) !void { pub fn run(self: *App) !void {
if (!self.running) return;
// On startup, we want to check for configuration errors right away
// so we can show our error window.
self.updateConfigErrors() catch |err| {
log.warn("error handling configuration errors err={}", .{err});
};
while (self.running) { while (self.running) {
_ = c.g_main_context_iteration(self.ctx, 1); _ = c.g_main_context_iteration(self.ctx, 1);
@ -286,7 +317,7 @@ fn gtkQuitConfirmation(
/// This is called by the "activate" signal. This is sent on program /// This is called by the "activate" signal. This is sent on program
/// startup and also when a secondary instance launches and requests /// startup and also when a secondary instance launches and requests
/// a new window. /// a new window.
fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
_ = app; _ = app;
const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return));

View File

@ -0,0 +1,163 @@
/// Configuration errors window.
const ConfigErrors = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
const App = @import("App.zig");
const c = @import("c.zig");
const log = std.log.scoped(.gtk);
app: *App,
layout: *c.GtkConstraintLayout,
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;
}
/// Not public because this should be called by the GTK lifecycle.
fn destroy(self: *ConfigErrors) void {
c.g_object_unref(self.layout);
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_application_window_new(app.app);
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, 300);
// Box to store our widgets
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 12);
c.gtk_widget_set_vexpand(box, 1);
c.gtk_widget_set_hexpand(box, 1);
c.gtk_window_set_child(@ptrCast(window), box);
// We use a constraint-based layout so the window is resizeable
const layout = c.gtk_constraint_layout_new();
errdefer c.g_object_unref(layout);
c.gtk_widget_set_layout_manager(@ptrCast(box), layout);
// Create all of 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.",
);
c.gtk_label_set_wrap(@ptrCast(label), 1);
const buf = try contentsBuffer(&app.config);
defer c.g_object_unref(buf);
const text = c.gtk_text_view_new_with_buffer(buf);
errdefer c.g_object_unref(text);
c.gtk_text_view_set_editable(@ptrCast(text), 0);
c.gtk_text_view_set_cursor_visible(@ptrCast(text), 0);
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);
// This hooks up all our widgets to the window so they can be laid out
// using the constraint-based layout.
c.gtk_widget_set_parent(label, box);
c.gtk_widget_set_name(label, "label");
c.gtk_widget_set_parent(text, box);
c.gtk_widget_set_name(text, "text");
c.gtk_widget_set_parent(ignore_button, box);
c.gtk_widget_set_name(ignore_button, "ignorebutton");
c.gtk_widget_set_parent(reload_button, box);
c.gtk_widget_set_name(reload_button, "reloadbutton");
var gerr: ?*c.GError = null;
const list = c.gtk_constraint_layout_add_constraints_from_description(
@ptrCast(layout),
&vfl,
vfl.len,
8,
8,
&gerr,
"label",
label,
"text",
text,
"ignorebutton",
ignore_button,
"reloadbutton",
reload_button,
@as(?*anyopaque, null),
);
if (gerr) |err| {
defer c.g_error_free(err);
log.warn("error building window message={s}", .{err.message});
return error.OperationFailed;
}
c.g_list_free(list);
// We can do additional settings once the layout is setup
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);
// Signals
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
// Show the window
c.gtk_widget_show(window);
// Set some state
self.* = .{
.app = app,
.layout = @ptrCast(layout),
};
}
fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
const self = userdataSelf(ud.?);
self.destroy();
}
fn userdataSelf(ud: *anyopaque) *ConfigErrors {
return @ptrCast(@alignCast(ud));
}
/// 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);
for (config._errors.list.items) |err| {
c.gtk_text_buffer_insert_at_cursor(buf, err.message, @intCast(err.message.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:[ignorebutton]-8-[reloadbutton]-8-|",
"V:|[label(<=100)][text(>=100)]-[ignorebutton]-|",
"V:|[label(<=100)][text(>=100)]-[reloadbutton]-|",
"V:[label][text]-[ignorebutton]",
"V:[label][text]-[reloadbutton]",
};