Skip to content

Commit

Permalink
add a bit of documentation for actions
Browse files Browse the repository at this point in the history
  • Loading branch information
jwahlstrand committed Feb 18, 2024
1 parent 57e2848 commit a23b2c3
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ makedocs(
"manual/layout.md",
"manual/signals.md",
"manual/builder.md",
"manual/actions.md",
"manual/textwidgets.md",
"manual/combobox.md",
"manual/listtreeview.md",
Expand Down
12 changes: 11 additions & 1 deletion docs/src/doc/GLib_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Gtk4.GLib.on_notify
Gtk4.GLib.bind_property
Gtk4.GLib.unbind_property
Gtk4.GLib.setproperties!
Gtk4.GLib.set_gtk_property!
Gtk4.GLib.get_gtk_property
```

## Signals
Expand All @@ -45,12 +47,20 @@ Gtk4.GLib.signal_handler_is_connected
Gtk4.GLib.signal_handler_disconnect
Gtk4.GLib.signal_handler_block
Gtk4.GLib.signal_handler_unblock
Gtk4.GLib.signal_emit
Gtk4.GLib.waitforsignal
```

## Actions and action groups
```@docs
Gtk4.GLib.GSimpleAction
Gtk4.GLib.add_action
Gtk4.GLib.add_stateful_action
Gtk4.GLib.set_state
```

## GObject type system
```@docs
Gtk4.GLib.g_type
Gtk4.GLib.find_leaf_type
```

3 changes: 3 additions & 0 deletions docs/src/doc/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Gtk4.visible
Gtk4.display
Gtk4.monitor
Gtk4.size_request
Gtk4.isrealized
```

## Windows
Expand All @@ -31,6 +32,7 @@ Gtk4.default_size
Gtk4.fullscreen
Gtk4.unfullscreen
Gtk4.isfullscreen
Gtk4.isactive
Gtk4.maximize
Gtk4.unmaximize
Gtk4.present
Expand Down Expand Up @@ -73,6 +75,7 @@ Gtk4.queue_render
```@docs
Gtk4.find_controller
Gtk4.widget
Gtk4.add_action_shortcut
```

## GtkTextView
Expand Down
104 changes: 104 additions & 0 deletions docs/src/manual/actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Actions

Actions are another way of associating callbacks with widgets. While they are a bit more complicated to set up, they have a few advantages over connecting to widget signals such as "clicked".

Actions are based on an interface `GAction`, the most straightforward implementation of which is `GSimpleAction`. Actions have a property "name" which must be a non-empty string consisting of alphanumeric characters, dashes ('-'), and dots ('.'). Actions also have a method `activate` that takes an optional parameter argument (more on those later).

Some widgets, including `GtkButton`, `GtkToggleButton`, and `GtkCheckButton`, (to be precise, widgets that implement the `GtkActionable` interface) have a property "action-name" that sets an associated action.

Let's say we want to define an action that closes a window. We could use
```julia
using Gtk4, Gtk4.GLib

win = GtkWindow("Action demo", 400, 200)

b = GtkButton("Click to close this window")
b.action_name = "win.close"
push!(win,b)

function close_action_cb(a, par)::Nothing # important to return nothing!
close(win)
end

action_group = GSimpleActionGroup()
add_action(GActionMap(action_group), "close", close_action_cb)
push!(win, Gtk4.GLib.GActionGroup(action_group), "win")
```
Let's go through this. First we created the window and button and set the button's action name to "win.close". Next we created an action group. Then we used the helper method `add_action` to create an action with the name "close", connect a callback for the action's "activate" signal, and add the action to the action group. Finally we added the action group to the window and associated it with a prefix "win".

For what it accomplishes, this code looks a bit convoluted.
Why did we set the button's action name to "win.close" rather than "close", which is the action's name? Each action is put in a group, which has its own prefix, in this case "win". The action's global name is "win.close" because it was added to the "win" group. Often applications define an "app" group for application-wide actions, and each window has its own "win" group for window-specific actions. You can even define action groups for individual widgets. When we set an action name for the button, GTK looks for a group named "win", starting with the button itself and then working its way up the widget hierarchy. In our case it found that group associated with the window.

You are probably asking yourself, why do it this way when we could have just connected `close_cb` to the button's "clicked" signal? Here are a few reasons to use actions:

1. Multiple widgets can point to the same action. For example, we could have a menu item for closing the window. Actions can reduce code duplication.
2. Actions can be disabled. Let's say we want to temporarily turn off the "close" action. We can do this by setting the action's property "enabled" to false. Any widgets that are connected to this action will be greyed out automatically, indicating to users that they don't do anything.
3. If you use `GtkApplication` and `GtkApplicationWindow`, you get action groups associated with the application and windows for free since these two widget classes define the interface `GActionGroup`. So that saves you from having to create a `GSimpleActionGroup` and add it to the window.
4. If you use Builder, you can set the action names and targets for widgets in XML.

## Stateful actions

The action "close" in the example above was a stateless action.
All one can do is "activate" it, which calls a function.
It's possible to associate a state with an action too.

Here is a simple example of a stateful action:
```julia
using Gtk4, Gtk4.GLib

win = GtkWindow("Fullscreen action demo", 400, 200)

b = GtkButton("Click to change fullscreen state"; action_name="win.fullscreen")
push!(win,b)

function fullscreen_action_cb(a, v)::Nothing
if v[Bool]
Gtk4.fullscreen(win)
else
Gtk4.unfullscreen(win)
end
Gtk4.GLib.set_state(a, v)
end

action_group = GSimpleActionGroup()
add_stateful_action(GActionMap(action_group), "fullscreen", false, fullscreen_action_cb)
push!(win, Gtk4.GLib.GActionGroup(action_group), "win")
```

Here we used `add_stateful_action` to create an action named "fullscreen" that has a boolean state associated with it. The type of the state is taken from the initial state argument, which in this case was `false`.
The callback is connected to the action's "change-state" signal.

In the callback, the argument `v` is a `GVariant`, which is sort of a container type in GLib that is used in the action system.
To read the state value from the container we used `v[Bool]`.

Note that you have to call `set_state` in the callback or else the state will not be updated!

## A more complicated example

```julia
using Gtk4, Gtk4.GLib

win = GtkWindow("Radio button demo", 400, 200)
win[] = box = GtkBox(:v)

b1 = GtkToggleButton("Option 1"; action_name = "win.option", action_target=GVariant("1"))
push!(box, b1)
push!(box, GtkToggleButton("Option 2"; action_name = "win.option", action_target=GVariant("2"), group = b1))
push!(box, GtkToggleButton("Option 3"; action_name = "win.option", action_target=GVariant("3"), group = b1))

lbl = GtkLabel("1")
push!(box, lbl)

function opt_action_cb(a,v)::Nothing
lbl.label = v[String]
Gtk4.GLib.set_state(a,v)
end

action_group = GSimpleActionGroup()
add_stateful_action(GActionMap(action_group), "option", String, "1", opt_action_cb)
push!(win, Gtk4.GLib.GActionGroup(action_group), "win")
```

## Actions in an application

As mentioned above, the objects `GtkApplication` and `GtkApplicationWindow` implement the `GActionMap` interface, so there is no need to create a `GSimpleActionGroup` and add it to the window. For `GtkApplicationWindow`s, you can add window-associated actions using `add_action(GActionMap(win), "fullscreen", fullscreen_cb)`. Assuming you have created a `GtkApplication` called `app`, you can add actions to the application using `add_action(GActionMap(app), "fullscreen", fullscreen_cb)`.
15 changes: 11 additions & 4 deletions docs/src/manual/builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ There is an alternative way to design user interfaces that strictly separates th
from the code. This is done by an XML based file format that allows for describing any arrangement of widgets.
In order to use the interface in your Julia Gtk4 application you will need `GtkBuilder`.

For GTK version 3 and earlier, Glade is often used as a GUI tool for creating GtkBuilder XML files in a WYSIWYG (what you see is what you get) manner, but Glade wasn't ported to GTK version 4. Instead [Cambalache](https://flathub.org/apps/details/ar.xjuan.Cambalache) can be used (or the XML can be created by hand).
For GTK version 3 and earlier, Glade is often used as a GUI tool for creating GtkBuilder XML files in a WYSIWYG (what you see is what you get) manner, but Glade wasn't ported to GTK version 4. Instead [Cambalache](https://flathub.org/apps/details/ar.xjuan.Cambalache) can be used or the XML can be created in an editor.

Once we have created the XML interface the result can be stored in an XML file that usually has
the extension `.ui`. Let's assume we have created a file `myapp.ui` that looks like
Expand Down Expand Up @@ -36,6 +36,12 @@ connector between the XML definition and our Julia code.
```julia
b = GtkBuilder("path/to/myapp.ui")
```

!!! note
If you are developing the code in a package you can get the package directory using the `@__DIR__` macro.
For instance, if your UI file is located at `MyPackage/src/builder/myuifile.ui`, you can get the full path using
`uifile = joinpath(@__DIR__, "builder", "myuifile.ui")`.

Alternatively, if we store the above XML definition in a Julia string `myapp` we can initialize
the builder by
```julia
Expand All @@ -53,9 +59,9 @@ to be loaded. You can thus see your builder as a kind of a widget store that you
when you need access to your widgets.

!!! note
If you are developing the code in a package you can get the package directory using the `@__DIR__` macro.
For instance, if your UI file is located at `MyPackage/src/builder/myuifile.ui`, you can get the full path using
`uifile = joinpath(@__DIR__, "builder", "myuifile.ui")`.
Fetching an object from `GtkBuilder` is type unstable since the Julia compiler has no way of knowing the type of the object.
A type assertion can be used to set a concrete type (ending in "Leaf") and potentially improve performance.
In the example above, the correct assertion would be `win = b["window1"]::GtkWindowLeaf`.

In Gtk4.jl a macro `@load_builder` is defined that iterates over the `GtkWidget`s in
a `GtkBuilder` object and automatically assigns them to Julia variables with the same id. For
Expand All @@ -77,3 +83,4 @@ Note that this only works for `GtkWidget`s that implement the interface `GtkBuil
The XML file lets us only describe the visual structure of our widgets and not their behavior when the using
is interacting with it. For this reason, we will have to add callbacks to the widgets which we do in Julia code
as it was described in [Signals and Callbacks](@ref).
Alternatively you can use [Actions](@ref), which are described in the next section.
5 changes: 4 additions & 1 deletion docs/src/manual/dialogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ Dialogs are transient windows that show information or ask the user for informat
!!! note "Example"
Some of the code on this page can be found in "dialogs.jl" in the "example" subdirectory.

!!! tip "Creating dialogs in callbacks"
When creating dialogs in signal or action callbacks, you have to use the methods that take a function as the first argument (equivalently the `do` syntax).

## Message dialogs

Gtk4.jl supports `GtkMessageDialog` and provides several convenience functions: `info_dialog`, `ask_dialog`, `warn_dialog`, and `error_dialog`. Each takes a string for a message to show and an optional parent container, and returns nothing, except for `ask_dialog` which returns `true` if the user clicks the button corresponding to yes.
Expand All @@ -18,7 +21,7 @@ warn_dialog("Oops!... I did it again")
```
These take an optional argument `timeout` (in seconds) that can be used to make the dialog disappear after a certain time.

In callbacks (for example when a user clicks a button in a GUI), you can use a different form, which takes a callback as the first argument that will be called when the user closes the dialog. A full example:
In callbacks (for example when a user clicks a button in a GUI), you _must_ use a different form, which takes a callback as the first argument that will be called when the user closes the dialog. A full example:
```julia
b = GtkButton("Click me")
win = GtkWindow(b,"Info dialog example")
Expand Down

0 comments on commit a23b2c3

Please sign in to comment.