diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 483e7821e..9195355db 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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); diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig new file mode 100644 index 000000000..9bfedc8bc --- /dev/null +++ b/src/apprt/gtk/ConfigErrorsDialog.zig @@ -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; + }; + } +} diff --git a/src/apprt/gtk/ConfigErrorsWindow.zig b/src/apprt/gtk/ConfigErrorsWindow.zig deleted file mode 100644 index 5fbf8e835..000000000 --- a/src/apprt/gtk/ConfigErrorsWindow.zig +++ /dev/null @@ -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(>kDestroy), 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(>kIgnoreClick), - root, - null, - c.G_CONNECT_DEFAULT, - ); - _ = c.g_signal_connect_data( - reload_button, - "clicked", - c.G_CALLBACK(>kReloadClick), - 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-|", - }; -}; diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 44a1e00bf..64067c199 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -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" }, diff --git a/src/apprt/gtk/ui/1.2/config-errors-dialog.blp b/src/apprt/gtk/ui/1.2/config-errors-dialog.blp new file mode 100644 index 000000000..4f750a3be --- /dev/null +++ b/src/apprt/gtk/ui/1.2/config-errors-dialog.blp @@ -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 { }; + } + }; +} diff --git a/src/apprt/gtk/ui/1.2/config-errors-dialog.ui b/src/apprt/gtk/ui/1.2/config-errors-dialog.ui new file mode 100644 index 000000000..1d7517f7a --- /dev/null +++ b/src/apprt/gtk/ui/1.2/config-errors-dialog.ui @@ -0,0 +1,36 @@ + + + + + + Configuration Errors + One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors. + + Ignore + Reload Configuration + + + + 500 + 100 + + + false + false + 8 + 8 + 8 + 8 + + + + + + + + + diff --git a/src/apprt/gtk/ui/1.5/config-errors-dialog.blp b/src/apprt/gtk/ui/1.5/config-errors-dialog.blp new file mode 100644 index 000000000..e6d81c0df --- /dev/null +++ b/src/apprt/gtk/ui/1.5/config-errors-dialog.blp @@ -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 { }; + } + }; +}