Actions

By now, we've already learned many ways to glue our widgets together. We can send messages through channels, emit signals, share reference-counted state and bind properties. Now, we will complete our set by learning about actions.

An action is a piece of functionality bound to a certain GObject. Let's check out the simplest case where we activate an action without a parameter.

Filename: listings/actions/1/main.rs

use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};

const APP_ID: &str = "org.gtk_rs.Actions1";

fn main() -> glib::ExitCode {
    // Create a new application
    let app = Application::builder().application_id(APP_ID).build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Set keyboard accelerator to trigger "win.close".
    app.set_accels_for_action("win.close", &["<Ctrl>W"]);

    // Run the application
    app.run()
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    // Add action "close" to `window` taking no parameter
    let action_close = ActionEntry::builder("close")
        .activate(|window: &ApplicationWindow, _, _| {
            window.close();
        })
        .build();
    window.add_action_entries([action_close]);

    // Present window
    window.present();
}

First, we created a new gio::ActionEntry which is named "close" and takes no parameter. We also connected a callback which closes the window when the action is activated. Finally, we add the action entry to the window via add_action_entries.

Filename: listings/actions/1/main.rs

use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};

const APP_ID: &str = "org.gtk_rs.Actions1";

fn main() -> glib::ExitCode {
    // Create a new application
    let app = Application::builder().application_id(APP_ID).build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Set keyboard accelerator to trigger "win.close".
    app.set_accels_for_action("win.close", &["<Ctrl>W"]);

    // Run the application
    app.run()
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    // Add action "close" to `window` taking no parameter
    let action_close = ActionEntry::builder("close")
        .activate(|window: &ApplicationWindow, _, _| {
            window.close();
        })
        .build();
    window.add_action_entries([action_close]);

    // Present window
    window.present();
}

One of the most popular reasons to use actions are keyboard accelerators, so we added one here. With set_accels_for_action one can assign one or more accelerators to a certain action. Check the documentation of accelerator_parse in order to learn more about its syntax.

Before we move on to other aspects of actions, let's appreciate a few things that are curious here. The "win" part of "win.close" is the group of the action. But how does GTK know that "win" is the action group of our window? The answer is that it is so common to add actions to windows and applications that there are already two predefined groups available:

  • "app" for actions global to the application, and
  • "win" for actions tied to an application window.

We can add an action group to any widget via the method insert_action_group. Let's add our action to the action group "custom-group" and add the group then to our window. The action entry isn't specific to our window anymore, the first parameter of the "activate" callback is of type SimpleActionGroup instead of ApplicationWindow. This means we have to clone window into the closure.

Filename: listings/actions/2/main.rs

use gio::ActionEntry;
use glib::clone;
use gtk::gio::SimpleActionGroup;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};

const APP_ID: &str = "org.gtk_rs.Actions2";

fn main() -> glib::ExitCode {
    // Create a new application
    let app = Application::builder().application_id(APP_ID).build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);
    // Set keyboard accelerator to trigger "custom-group.close".
    app.set_accels_for_action("custom-group.close", &["<Ctrl>W"]);

    // Run the application
    app.run()
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    // Add action "close" to `window` taking no parameter
    let action_close = ActionEntry::builder("close")
        .activate(clone!(@weak window => move |_, _, _| {
            window.close();
        }))
        .build();

    // Create a new action group and add actions to it
    let actions = SimpleActionGroup::new();
    actions.add_action_entries([action_close]);
    window.insert_action_group("custom-group", Some(&actions));

    // Present window
    window.present();
}

If we bind the accelerator to "custom-group.close", it works just as before.

Filename: listings/actions/2/main.rs

use gio::ActionEntry;
use glib::clone;
use gtk::gio::SimpleActionGroup;
use gtk::prelude::*;
use gtk::{gio, glib, Application, ApplicationWindow};

const APP_ID: &str = "org.gtk_rs.Actions2";

fn main() -> glib::ExitCode {
    // Create a new application
    let app = Application::builder().application_id(APP_ID).build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);
    // Set keyboard accelerator to trigger "custom-group.close".
    app.set_accels_for_action("custom-group.close", &["<Ctrl>W"]);

    // Run the application
    app.run()
}

fn build_ui(app: &Application) {
    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .build();

    // Add action "close" to `window` taking no parameter
    let action_close = ActionEntry::builder("close")
        .activate(clone!(@weak window => move |_, _, _| {
            window.close();
        }))
        .build();

    // Create a new action group and add actions to it
    let actions = SimpleActionGroup::new();
    actions.add_action_entries([action_close]);
    window.insert_action_group("custom-group", Some(&actions));

    // Present window
    window.present();
}

Also, if we had multiple instances of the same windows, we would expect that only the currently focused window will be closed when activating "win.close". And indeed, the "win.close" will be dispatched to the currently focused window. However, that also means that we actually define one action per window instance. If we want to have a single globally accessible action instead, we call add_action_entries on our application instead.

Adding "win.close" was useful as a simple example. However, in the future we will use the pre-defined "window.close" action which does exactly the same thing.

Parameter and State

An action, like most functions, can take a parameter. However, unlike most functions it can also be stateful. Let's see how this works.

Filename: listings/actions/3/main.rs

use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{
    gio, glib, Align, Application, ApplicationWindow, Button, Label, Orientation,
};

const APP_ID: &str = "org.gtk_rs.Actions3";

fn main() -> glib::ExitCode {
    // Create a new application
    let app = Application::builder().application_id(APP_ID).build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Run the application
    app.run()
}

fn build_ui(app: &Application) {
    let original_state = 0;
    let label = Label::builder()
        .label(format!("Counter: {original_state}"))
        .build();

    // Create a button with label
    let button = Button::builder().label("Press me!").build();

    // Connect to "clicked" signal of `button`
    button.connect_clicked(move |button| {
        // Activate "win.count" and pass "1" as parameter
        let parameter = 1;
        button
            .activate_action("win.count", Some(&parameter.to_variant()))
            .expect("The action does not exist.");
    });

    // Create a `gtk::Box` and add `button` and `label` to it
    let gtk_box = gtk::Box::builder()
        .orientation(Orientation::Vertical)
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .spacing(12)
        .halign(Align::Center)
        .build();
    gtk_box.append(&button);
    gtk_box.append(&label);

    // Create a window, set the title and add `gtk_box` to it
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .child(&gtk_box)
        .build();

    // Add action "count" to `window` taking an integer as parameter
    let action_count = ActionEntry::builder("count")
        .parameter_type(Some(&i32::static_variant_type()))
        .state(original_state.to_variant())
        .activate(move |_, action, parameter| {
            // Get state
            let mut state = action
                .state()
                .expect("Could not get state.")
                .get::<i32>()
                .expect("The variant needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The variant needs to be of type `i32`.");

            // Increase state by parameter and store state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {state}"));
        })
        .build();
    window.add_action_entries([action_count]);

    // Present window
    window.present();
}

Here, we created a "win.count" action that increases its state by the given parameter every time it is activated. It also takes care of updating the label with the current state. The button activates the action with each click while passing "1" as parameter. This is how our app works:

Actionable

Connecting actions to the "clicked" signal of buttons is a typical use case, which is why all buttons implement the Actionable interface. This way, the action can be specified by setting the "action-name" property. If the action accepts a parameter, it can be set via the "action-target" property. With ButtonBuilder, we can set everything up by calling its methods.

Filename: listings/actions/4/main.rs

use gio::ActionEntry;
use gtk::prelude::*;
use gtk::{
    gio, glib, Align, Application, ApplicationWindow, Button, Label, Orientation,
};

const APP_ID: &str = "org.gtk_rs.Actions4";

fn main() -> glib::ExitCode {
    // Create a new application
    let app = Application::builder().application_id(APP_ID).build();

    // Connect to "activate" signal of `app`
    app.connect_activate(build_ui);

    // Run the application
    app.run()
}

fn build_ui(app: &Application) {
    let original_state = 0;
    let label = Label::builder()
        .label(format!("Counter: {original_state}"))
        .build();
    // Create a button with label and action
    let button = Button::builder()
        .label("Press me!")
        .action_name("win.count")
        .action_target(&1.to_variant())
        .build();

    // Create `gtk_box` and add `button` and `label` to it
    let gtk_box = gtk::Box::builder()
        .orientation(Orientation::Vertical)
        .margin_top(12)
        .margin_bottom(12)
        .margin_start(12)
        .margin_end(12)
        .spacing(12)
        .halign(Align::Center)
        .build();
    gtk_box.append(&button);
    gtk_box.append(&label);

    // Create a window and set the title
    let window = ApplicationWindow::builder()
        .application(app)
        .title("My GTK App")
        .width_request(360)
        .child(&gtk_box)
        .build();

    // Add action "count" to `window` taking an integer as parameter
    let action_count = ActionEntry::builder("count")
        .parameter_type(Some(&i32::static_variant_type()))
        .state(original_state.to_variant())
        .activate(move |_, action, parameter| {
            // Get state
            let mut state = action
                .state()
                .expect("Could not get state.")
                .get::<i32>()
                .expect("The variant needs to be of type `i32`.");

            // Get parameter
            let parameter = parameter
                .expect("Could not get parameter.")
                .get::<i32>()
                .expect("The variant needs to be of type `i32`.");

            // Increase state by parameter and store state
            state += parameter;
            action.set_state(&state.to_variant());

            // Update label with new state
            label.set_label(&format!("Counter: {state}"));
        })
        .build();
    window.add_action_entries([action_count]);

    // Present window
    window.present();
}

Actionable widgets are also easily accessible through the interface builder. As usual, we build up the window via a composite template. Within the template we can then set the "action-name" and "action-target" properties.

Filename: listings/actions/5/resources/window.ui

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="MyGtkAppWindow" parent="GtkApplicationWindow">
    <property name="title">My GTK App</property>
    <child>
      <object class="GtkBox" id="gtk_box">
        <property name="orientation">vertical</property>
        <property name="margin-top">12</property>
        <property name="margin-bottom">12</property>
        <property name="margin-start">12</property>
        <property name="margin-end">12</property>
        <property name="spacing">12</property>
        <property name="halign">center</property>
        <child>
          <object class="GtkButton" id="button">
            <property name="label">Press me!</property>
            <property name="action-name">win.count</property>
            <property name="action-target">1</property>
          </object>
        </child>
        <child>
          <object class="GtkLabel" id="label">
            <property name="label">Counter: 0</property>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

We will connect the actions and add them to the window in the Window::setup_actions method.

Filename: listings/actions/5/window/mod.rs

mod imp;

use gio::ActionEntry;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application};

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::builder().property("application", app).build()
    }

    fn setup_actions(&self) {
        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = ActionEntry::builder("count")
            .parameter_type(Some(&i32::static_variant_type()))
            .state(original_state.to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Increase state by parameter and store state
                state += parameter;
                action.set_state(&state.to_variant());

                // Update label with new state
                window.imp().label.set_label(&format!("Counter: {state}"));
            })
            .build();
        self.add_action_entries([action_count]);
    }
}

Finally, setup_actions will be called within constructed.

Filename: listings/actions/5/window/imp.rs

use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{glib, CompositeTemplate, Label};

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
    #[template_child]
    pub label: TemplateChild<Label>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        klass.bind_template();
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self) {
        // Call "constructed" on parent
        self.parent_constructed();

        // Add actions
        self.obj().setup_actions();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}

This app behaves the same as our previous example, but it will make it simpler for us to add a menu in the following section.

If you want to create a menu, you have to use actions, and you will want to use the interface builder. Typically, a menu entry has an action fitting one of these three descriptions:

  • no parameter and no state, or
  • no parameter and boolean state, or
  • string parameter and string state.

Let's modify our small app to demonstrate these cases. First, we extend setup_actions. For the action without parameter or state, we can use the pre-defined "window.close" action. Therefore, we don't have to add anything here.

With the action "button-frame", we manipulate the "has-frame" property of button. Here, the convention is that actions with no parameter and boolean state should behave like toggle actions. This means that the caller can expect the boolean state to toggle after activating the action. Luckily for us, that is the default behavior for gio::PropertyAction with a boolean property.

Filename: listings/actions/6/window/mod.rs

mod imp;

use gio::{ActionEntry, PropertyAction};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::builder().property("application", app).build()
    }

    fn setup_actions(&self) {
        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = ActionEntry::builder("count")
            .parameter_type(Some(&i32::static_variant_type()))
            .state(original_state.to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Increase state by parameter and store state
                state += parameter;
                action.set_state(&state.to_variant());

                // Update label with new state
                window.imp().label.set_label(&format!("Counter: {state}"));
            })
            .build();
        // Add property action "button-frame" to `window`
        let button = self.imp().button.get();
        let action_button_frame =
            PropertyAction::new("button-frame", &button, "has-frame");
        self.add_action(&action_button_frame);

        // Add stateful action "orientation" to `window` taking a string as parameter
        let action_orientation = ActionEntry::builder("orientation")
            .parameter_type(Some(&String::static_variant_type()))
            .state("Vertical".to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<String>()
                    .expect("The value needs to be of type `String`.");

                let orientation = match parameter.as_str() {
                    "Horizontal" => Orientation::Horizontal,
                    "Vertical" => Orientation::Vertical,
                    _ => unreachable!(),
                };

                // Set orientation and save state
                window.imp().gtk_box.set_orientation(orientation);
                action.set_state(&parameter.to_variant());
            })
            .build();
        self.add_action_entries([action_count, action_orientation]);
    }
}

A PropertyAction is useful when you need an action that manipulates the property of a GObject. The property then acts as the state of the action. As mentioned above, if the property is a boolean the action has no parameter and toggles the property on activation. In all other cases, the action has a parameter of the same type as the property. When activating the action, the property gets set to the same value as the parameter of the action.

Finally, we add "win.orientation", an action with string parameter and string state. This action can be used to change the orientation of gtk_box. Here the convention is that the state should be set to the given parameter. We don't need the action state to implement orientation switching, however it is useful for making the menu display the current orientation.

Filename: listings/actions/6/window/mod.rs

mod imp;

use gio::{ActionEntry, PropertyAction};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::builder().property("application", app).build()
    }

    fn setup_actions(&self) {
        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = ActionEntry::builder("count")
            .parameter_type(Some(&i32::static_variant_type()))
            .state(original_state.to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Increase state by parameter and store state
                state += parameter;
                action.set_state(&state.to_variant());

                // Update label with new state
                window.imp().label.set_label(&format!("Counter: {state}"));
            })
            .build();
        // Add property action "button-frame" to `window`
        let button = self.imp().button.get();
        let action_button_frame =
            PropertyAction::new("button-frame", &button, "has-frame");
        self.add_action(&action_button_frame);

        // Add stateful action "orientation" to `window` taking a string as parameter
        let action_orientation = ActionEntry::builder("orientation")
            .parameter_type(Some(&String::static_variant_type()))
            .state("Vertical".to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<String>()
                    .expect("The value needs to be of type `String`.");

                let orientation = match parameter.as_str() {
                    "Horizontal" => Orientation::Horizontal,
                    "Vertical" => Orientation::Vertical,
                    _ => unreachable!(),
                };

                // Set orientation and save state
                window.imp().gtk_box.set_orientation(orientation);
                action.set_state(&parameter.to_variant());
            })
            .build();
        self.add_action_entries([action_count, action_orientation]);
    }
}

Even though gio::Menu can also be created with the bindings, the most convenient way is to use the interface builder for that. We do that by adding the menu in front of the template.

Filename: listings/actions/6/resources/window.ui

 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
+  <menu id="main-menu">
+    <item>
+      <attribute name="label" translatable="yes">_Close window</attribute>
+      <attribute name="action">window.close</attribute>
+    </item>
+    <item>
+      <attribute name="label" translatable="yes">_Toggle button frame</attribute>
+      <attribute name="action">win.button-frame</attribute>
+    </item>
+    <section>
+      <attribute name="label" translatable="yes">Orientation</attribute>
+      <item>
+        <attribute name="label" translatable="yes">_Horizontal</attribute>
+        <attribute name="action">win.orientation</attribute>
+        <attribute name="target">Horizontal</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Vertical</attribute>
+        <attribute name="action">win.orientation</attribute>
+        <attribute name="target">Vertical</attribute>
+      </item>
+    </section>
+  </menu>
   <template class="MyGtkAppWindow" parent="GtkApplicationWindow">
     <property name="title">My GTK App</property>
+    <property name="width-request">360</property>
+    <child type="titlebar">
+      <object class="GtkHeaderBar">
+        <child type ="end">
+          <object class="GtkMenuButton">
+            <property name="icon-name">open-menu-symbolic</property>
+            <property name="menu-model">main-menu</property>
+          </object>
+        </child>
+      </object>
+    </child>
     <child>
       <object class="GtkBox" id="gtk_box">
         <property name="orientation">vertical</property>

Since we connect the menu to the gtk::MenuButton via the menu-model property, the Menu is expected to be a gtk::PopoverMenu. The documentation for PopoverMenu also explains its xml syntax for the interface builder.

Also note how we specified the target:

<attribute name="target">Horizontal</attribute>

String is the default type of the target which is why we did not have to specify a type. With targets of other types you need to manually specify the correct GVariant format string. For example, an i32 variable with value "5" would correspond to this:

<attribute name="target" type="i">5</attribute>

This is how the app looks in action:

We changed the icon of the MenuButton by setting its property "icon-name" to "open-menu-symbolic". You can find more icons with the Icon Library. They can be embedded with gio::Resource and then be referenced within the composite templates (or other places).

Settings

The menu entries nicely display the state of our stateful actions, but after the app is closed, all changes to that state are lost. As usual, we solve this problem with gio::Settings. First we create a schema with settings corresponding to the stateful actions we created before.

Filename: listings/actions/7/org.gtk_rs.Actions7.gschema.xml

<?xml version="1.0" encoding="utf-8"?>
<schemalist>
  <schema id="org.gtk_rs.Actions7" path="/org/gtk_rs/Actions7/">
    <key name="button-frame" type="b">
      <default>true</default>
      <summary>Whether the button has a frame</summary>
    </key>
    <key name="orientation" type="s">
      <choices>
        <choice value='Horizontal'/>
        <choice value='Vertical'/>
      </choices>
      <default>'Vertical'</default>
      <summary>Orientation of GtkBox</summary>
    </key>
  </schema>
</schemalist>

Again, we install the schema as described in the settings chapter. Then we add the settings to imp::Window. Since gio::Settings does not implement Default, we wrap it in a std::cell::OnceCell.

Filename: listings/actions/7/window/imp.rs

use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Button, CompositeTemplate, Label};
use std::cell::OnceCell;

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
    #[template_child]
    pub gtk_box: TemplateChild<gtk::Box>,
    #[template_child]
    pub button: TemplateChild<Button>,
    #[template_child]
    pub label: TemplateChild<Label>,
    pub settings: OnceCell<Settings>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        klass.bind_template();
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self) {
        // Call "constructed" on parent
        self.parent_constructed();

        // Setup
        let obj = self.obj();
        obj.setup_settings();
        obj.setup_actions();
        obj.bind_settings();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}

Now we create functions to make it easier to access settings.

Filename: listings/actions/7/window/mod.rs

mod imp;

use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};

use crate::APP_ID;

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::builder().property("application", app).build()
    }

    fn setup_settings(&self) {
        let settings = Settings::new(APP_ID);
        self.imp()
            .settings
            .set(settings)
            .expect("`settings` should not be set before calling `setup_settings`.");
    }

    fn settings(&self) -> &Settings {
        self.imp()
            .settings
            .get()
            .expect("`settings` should be set in `setup_settings`.")
    }

    fn setup_actions(&self) {
        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = ActionEntry::builder("count")
            .parameter_type(Some(&i32::static_variant_type()))
            .state(original_state.to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Increase state by parameter and store state
                state += parameter;
                action.set_state(&state.to_variant());

                // Update label with new state
                window.imp().label.set_label(&format!("Counter: {state}"));
            })
            .build();
        self.add_action_entries([action_count]);

        // Create action from key "button-frame" and add to action group "win"
        let action_button_frame = self.settings().create_action("button-frame");
        self.add_action(&action_button_frame);

        // Create action from key "orientation" and add to action group "win"
        let action_orientation = self.settings().create_action("orientation");
        self.add_action(&action_orientation);
    }

    fn bind_settings(&self) {
        // Bind setting "button-frame" to "has-frame" property of `button`
        let button = self.imp().button.get();
        self.settings()
            .bind("button-frame", &button, "has-frame")
            .build();

        // Bind setting "orientation" to "orientation" property of `button`
        let gtk_box = self.imp().gtk_box.get();
        self.settings()
            .bind("orientation", &gtk_box, "orientation")
            .mapping(|variant, _| {
                let orientation = variant
                    .get::<String>()
                    .expect("The variant needs to be of type `String`.");

                let orientation = match orientation.as_str() {
                    "Horizontal" => Orientation::Horizontal,
                    "Vertical" => Orientation::Vertical,
                    _ => unreachable!(),
                };

                Some(orientation.to_value())
            })
            .build();
    }
}

Creating stateful actions from setting entries is so common that Settings provides a method for that exact purpose. We create actions with the create_action method and then add them to the action group of our window.

Filename: listings/actions/7/window/mod.rs

mod imp;

use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};

use crate::APP_ID;

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::builder().property("application", app).build()
    }

    fn setup_settings(&self) {
        let settings = Settings::new(APP_ID);
        self.imp()
            .settings
            .set(settings)
            .expect("`settings` should not be set before calling `setup_settings`.");
    }

    fn settings(&self) -> &Settings {
        self.imp()
            .settings
            .get()
            .expect("`settings` should be set in `setup_settings`.")
    }

    fn setup_actions(&self) {
        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = ActionEntry::builder("count")
            .parameter_type(Some(&i32::static_variant_type()))
            .state(original_state.to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Increase state by parameter and store state
                state += parameter;
                action.set_state(&state.to_variant());

                // Update label with new state
                window.imp().label.set_label(&format!("Counter: {state}"));
            })
            .build();
        self.add_action_entries([action_count]);

        // Create action from key "button-frame" and add to action group "win"
        let action_button_frame = self.settings().create_action("button-frame");
        self.add_action(&action_button_frame);

        // Create action from key "orientation" and add to action group "win"
        let action_orientation = self.settings().create_action("orientation");
        self.add_action(&action_orientation);
    }

    fn bind_settings(&self) {
        // Bind setting "button-frame" to "has-frame" property of `button`
        let button = self.imp().button.get();
        self.settings()
            .bind("button-frame", &button, "has-frame")
            .build();

        // Bind setting "orientation" to "orientation" property of `button`
        let gtk_box = self.imp().gtk_box.get();
        self.settings()
            .bind("orientation", &gtk_box, "orientation")
            .mapping(|variant, _| {
                let orientation = variant
                    .get::<String>()
                    .expect("The variant needs to be of type `String`.");

                let orientation = match orientation.as_str() {
                    "Horizontal" => Orientation::Horizontal,
                    "Vertical" => Orientation::Vertical,
                    _ => unreachable!(),
                };

                Some(orientation.to_value())
            })
            .build();
    }
}

Since actions from create_action follow the aforementioned conventions, we can keep further changes to a minimum. The action "win.button-frame" toggles its state with each activation and the state of the "win.orientation" action follows the given parameter.

We still have to specify what should happen when the actions are activated though. For the stateful actions, instead of adding callbacks to their "activate" signals, we bind the settings to properties we want to manipulate.

Filename: listings/actions/7/window/mod.rs

mod imp;

use gio::{ActionEntry, Settings};
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, Orientation};

use crate::APP_ID;

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        // Create new window
        Object::builder().property("application", app).build()
    }

    fn setup_settings(&self) {
        let settings = Settings::new(APP_ID);
        self.imp()
            .settings
            .set(settings)
            .expect("`settings` should not be set before calling `setup_settings`.");
    }

    fn settings(&self) -> &Settings {
        self.imp()
            .settings
            .get()
            .expect("`settings` should be set in `setup_settings`.")
    }

    fn setup_actions(&self) {
        // Add stateful action "count" to `window` taking an integer as parameter
        let original_state = 0;
        let action_count = ActionEntry::builder("count")
            .parameter_type(Some(&i32::static_variant_type()))
            .state(original_state.to_variant())
            .activate(move |window: &Self, action, parameter| {
                // Get state
                let mut state = action
                    .state()
                    .expect("Could not get state.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Get parameter
                let parameter = parameter
                    .expect("Could not get parameter.")
                    .get::<i32>()
                    .expect("The variant needs to be of type `i32`.");

                // Increase state by parameter and store state
                state += parameter;
                action.set_state(&state.to_variant());

                // Update label with new state
                window.imp().label.set_label(&format!("Counter: {state}"));
            })
            .build();
        self.add_action_entries([action_count]);

        // Create action from key "button-frame" and add to action group "win"
        let action_button_frame = self.settings().create_action("button-frame");
        self.add_action(&action_button_frame);

        // Create action from key "orientation" and add to action group "win"
        let action_orientation = self.settings().create_action("orientation");
        self.add_action(&action_orientation);
    }

    fn bind_settings(&self) {
        // Bind setting "button-frame" to "has-frame" property of `button`
        let button = self.imp().button.get();
        self.settings()
            .bind("button-frame", &button, "has-frame")
            .build();

        // Bind setting "orientation" to "orientation" property of `button`
        let gtk_box = self.imp().gtk_box.get();
        self.settings()
            .bind("orientation", &gtk_box, "orientation")
            .mapping(|variant, _| {
                let orientation = variant
                    .get::<String>()
                    .expect("The variant needs to be of type `String`.");

                let orientation = match orientation.as_str() {
                    "Horizontal" => Orientation::Horizontal,
                    "Vertical" => Orientation::Vertical,
                    _ => unreachable!(),
                };

                Some(orientation.to_value())
            })
            .build();
    }
}

Finally, we make sure that bind_settings is called within constructed.

Filename: listings/actions/7/window/imp.rs

use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Button, CompositeTemplate, Label};
use std::cell::OnceCell;

// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/example/window.ui")]
pub struct Window {
    #[template_child]
    pub gtk_box: TemplateChild<gtk::Box>,
    #[template_child]
    pub button: TemplateChild<Button>,
    #[template_child]
    pub label: TemplateChild<Label>,
    pub settings: OnceCell<Settings>,
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
    // `NAME` needs to match `class` attribute of template
    const NAME: &'static str = "MyGtkAppWindow";
    type Type = super::Window;
    type ParentType = gtk::ApplicationWindow;

    fn class_init(klass: &mut Self::Class) {
        klass.bind_template();
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self) {
        // Call "constructed" on parent
        self.parent_constructed();

        // Setup
        let obj = self.obj();
        obj.setup_settings();
        obj.setup_actions();
        obj.bind_settings();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {}

// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}

Actions are extremely powerful, and we are only scratching the surface here. If you want to learn more about them, the GNOME developer documentation is a good place to start.