Gfret Gtk4 Port Part 2

Current status

At the moment the gtk4 port has acheived basic functionality in terms of being able to save the svg to disk and display the current filename and saved status in the window title. Templates are currently disabled, as are preferences. In summary, there's a few things left to get back feature parity with the Gtk3 version.

Dialog window changes

    /// Opens a [gtk::FileChooserDialog] and sets the path to the output file.
    fn get_output(&self) -> Option<String> {
        let currentfile = if *self.saved_once.borrow() {
            self.filename.borrow().to_string()
        } else {
            String::from("untitled.svg")
        };
        let dialog = gtk::FileChooserDialog::with_buttons::<Window>(
            Some("Save As"),

You can replace calls to gtk_dialog_run() by specifying that the GtkDialog must be modal using gtk_window_set_modal() or the GTK_DIALOG_MODAL flag, and connecting to the GtkDialog::response signal.

It's funny how sometimes a seemingly innocuos change can really throw a wrench in your architecture. Previously, dialog windows could return a basic value, signaling whether the user had clicked "Accept" vs. "Cancel" for instance. This lent itself well to tucking extra windows and their definitions into separate modules, almost treating them as a smaller sub-program.

Attempting to do something similar by connecting to the GtkDialog::response signal in Rust is problematic at best. Seeing as gtk-rs implements callbacks using Rust closures, and any data sent into a closure is swallowed up whole and not returned, it is not really possible to create a new dialog window in a separate function and return a value back to the caller, or at least not in a clean and straightforward way. So my previous method of keeping track of whether a file has ever been saved, has changes since save, and it's name in RefCell's that are a member of the Gui struct became fundamentally broken if the save dialog is created and destroyed as needed.

What works instead, at least for a relatively simple program like Gfret, is to create all of the dialogs at the same time the main window is created, let them live for the life of the program, and then just show() and hide() them as required. I'm not a huge fan of this in terms of memory usage, but it makes the code nice and neat. So I've tucked the Save As, Open Template and Preferences dialogs into their own module and made their associated objects members of the Gui struct.

// Connecting to the `response` signal
    gui.dialogs.save_as.connect_response(clone!(@strong gui => move |dlg,res| {
        if res == ResponseType::Accept {
            if let Some(file) = dlg.file() {
                if let Some(mut path) = file.path() {
                    path.set_extension("svg");
                    if let Some(filename) = path.to_str() {
                        let document = gui.get_specs().create_document(None);
                        gui.file.do_save(filename.to_string(), &document);
                        gui.set_window_title();
                    }
                }
            }
        }
        dlg.hide();
    }));
// The `Gui` struct in `gui/mod.rs`
struct Gui {
    window: gtk::Window,
    image_preview: gtk::Picture,
    // some fields omitted
    menu: Menu,
    file: File,
    dialogs: Dialogs,
}
// The `Dialogs` struct in `dialogs.rs`
pub struct Dialogs {
    pub save_as: gtk::FileChooserDialog,
    pub open_template: gtk::FileChooserDialog,
    pub preferences: PrefWidgets,
}
// Dialogs are initialized with the main window
impl Dialogs {
    pub fn init(window: &gtk::Window, builder: &gtk::Builder) -> Dialogs {
        Dialogs {
            save_as: Dialogs::init_save_as(window),
            open_template: Dialogs::init_open_template(window),
            preferences: Dialogs::init_preferences(window, builder),
        }
    }

    fn init_save_as(window: &gtk::Window) -> gtk::FileChooserDialog {
        let dlg = gtk::FileChooserDialog::builder()
            .action(gtk::FileChooserAction::Save)
            .name("Gfret - Save As")
            .use_header_bar(1)
            .create_folders(true)
            .select_multiple(false)
            .modal(true)
            .destroy_with_parent(true)
            .transient_for(window)
            .build();
        let accept = gtk::Button::with_label("Accept");
        let cancel = gtk::Button::with_label("Cancel");
        dlg.add_action_widget(&cancel, gtk::ResponseType::Cancel);
        dlg.add_action_widget(&accept, gtk::ResponseType::Accept);
        dlg
    }

GtkFilechooserDialog bug

This one is tricky due to when it manifests. When I first got the save dialog to open again I encountered a bug that makes it almost impossible to enter a filename for which to save a file. The Gtk filechooser widget has a feature where it starts searching for a matching file name when you start typing. This is actually incredibly useful when opening a file, but it's supposed to turn off when you click on, and thus focus, the path bar in order to allow you to enter a filename. The bug manifests itself by automatically opening the search entry and transferring focus to it after every keypress, even after focusing the path entry box. This makes it almost impossible to use the dialog, as the path entry box must be re-focused after every keypress.

The bug has been reported previously, confirmed by multiple people, but was never reproducable by the maintainers. I think I found out why, and I've reported my findings to the project. The reason is that the bug only manifests if the dialog has the use-headerbar property set to false. And since a Gnome developer and maintainer is likely going to be following the current design guidelines they're going to be using headerbars everywhere, as do most of the current Gnome applications.

Partially as a result of this I've changed the various dialogs that Gfret uses so that they all have headerbars. I'm on the fence about doing so with the main window, but I don't hate how the dialogs look. I'm leaning towards not doing so on the main window because quite a lot of people set their WM's to undecorate a window when it is maximized, and tiling wm's tend to have no title bar by default on every window. The Gtk headerbar completely defeats the space saving nature of these features. Additionally, when using an environment other than Gnome it leads to a situation where headerbar windows have different window decorations than all other windows, which is a visual annoyance for a lot of users.

I think these trade offs are OK for a transient window like a Save As dialog, especially as there is an actual space savings when putting the Accept and Cancel buttons into the headerbar. For Gfret's main window, however, I don't believe that there would be any benefit and would be the above mentioned drawbacks.