From bd3b5d533269088a6e0bdfde533486f7c9985293 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 17 Sep 2023 21:37:57 -0700 Subject: [PATCH] apprt/gtk: working on config errors window --- src/apprt/gtk/App.zig | 35 +++++- src/apprt/gtk/ConfigErrorsWindow.zig | 163 +++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 src/apprt/gtk/ConfigErrorsWindow.zig diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index fba7bf89e..f2377248e 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -11,6 +11,7 @@ const App = @This(); const std = @import("std"); +const assert = std.debug.assert; const builtin = @import("builtin"); const glfw = @import("glfw"); const configpkg = @import("../../config.zig"); @@ -20,6 +21,7 @@ const CoreSurface = @import("../../Surface.zig"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); +const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); const c = @import("c.zig"); 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. 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. running: bool = true, @@ -80,7 +85,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { _ = c.g_signal_connect_data( app, "activate", - c.G_CALLBACK(&activate), + c.G_CALLBACK(>kActivate), core_app, null, c.G_CONNECT_DEFAULT, @@ -155,9 +160,27 @@ pub fn reloadConfig(self: *App) !?*const Config { self.config.deinit(); self.config = config; + // If there were errors, report them + self.updateConfigErrors() catch |err| { + log.warn("error handling configuration errors err={}", .{err}); + }; + 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. pub fn wakeup(self: App) void { _ = self; @@ -166,6 +189,14 @@ pub fn wakeup(self: App) void { /// Run the event loop. This doesn't return until the app exits. 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) { _ = 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 /// startup and also when a secondary instance launches and requests /// a new window. -fn activate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { +fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { _ = app; const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); diff --git a/src/apprt/gtk/ConfigErrorsWindow.zig b/src/apprt/gtk/ConfigErrorsWindow.zig new file mode 100644 index 000000000..2228205f5 --- /dev/null +++ b/src/apprt/gtk/ConfigErrorsWindow.zig @@ -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(>kDestroy), 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]", +};