Skip to content

Commit

Permalink
GI-generated signals continued
Browse files Browse the repository at this point in the history
rethink REPL helper functions, add a bit about these to docs
  • Loading branch information
jwahlstrand committed Oct 29, 2023
1 parent 8eca099 commit f3285f2
Show file tree
Hide file tree
Showing 14 changed files with 4,097 additions and 4,164 deletions.
80 changes: 66 additions & 14 deletions GI/src/giimport.jl
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,28 @@ function prop_dict_incl_parents(objectinfo::GIObjectInfo)
end
end

function signal_dict(info)
signals=get_signals(info)
d=Dict{Symbol,Tuple{Any,Any}}()
for s in signals
rettyp = get_ret_type(s)
argtyps = get_arg_types(s)
name=Symbol(replace(String(get_name(s)),"-"=>"_"))
d[name]=(rettyp,argtyps)
end
d
end

function signal_dict_incl_parents(objectinfo::GIObjectInfo)
d=signal_dict(objectinfo)
parentinfo=get_parent(objectinfo)
if parentinfo!==nothing
return merge(signal_dict_incl_parents(parentinfo),d)
else
return d
end
end

get_toplevel(o::GIInterfaceInfo) = :GObject
function get_toplevel(o)
p = o
Expand Down Expand Up @@ -284,6 +306,33 @@ function gobject_decl(objectinfo)
ccall(($type_init, $slib), GType, ())
end
push!(exprs, decl)
# if there are signals, add "signal_return_type" method, and "signal_arg_types" method
signals = get_signals(objectinfo)
sigdict = signal_dict_incl_parents(objectinfo)
if length(signals)>0
signalnames = [Symbol(replace(String(sname),"-"=>"_")) for sname in get_name.(signals)]
signalexprs = quote
function GLib.signalnames(::Type{$oname})
vcat($signalnames,signalnames(supertype($oname)))
end
let d = $sigdict
function GLib.signal_return_type(::Type{T},name::Symbol) where T<:$oname
eval(d[name][1])
end
function GLib.signal_argument_types(::Type{T},name::Symbol) where T<:$oname
Tuple(eval.(d[name][2]))
end
end
end
push!(exprs,signalexprs)
else
signalexprs = quote
function GLib.signalnames(::Type{$oname})
signalnames(supertype($oname))
end
end
push!(exprs,signalexprs)
end
exprs
end

Expand Down Expand Up @@ -385,6 +434,21 @@ function decl(callbackinfo::GICallbackInfo)
unblock(d)
end

function get_ret_type(signalinfo::GISignalInfo)
rettypeinfo=get_return_type(signalinfo)
extract_type(rettypeinfo).ctype
end

function get_arg_types(signalinfo::GISignalInfo)
args=get_args(signalinfo)
argctypes_arr=[]
for arg in args
argtype = extract_type(arg)
push!(argctypes_arr, argtype.ctype)
end
argctypes_arr
end

## Signal output
function decl(signalinfo::GISignalInfo)
name = get_name(signalinfo)
Expand All @@ -396,25 +460,13 @@ function decl(signalinfo::GISignalInfo)
object = get_container(signalinfo)
@assert object !== nothing
objtypeinfo = extract_type(InstanceType,object)
rettypeinfo=get_return_type(signalinfo)
rettype = extract_type(rettypeinfo).ctype
args=get_args(signalinfo)
argctypes_arr=[]
for arg in args
argtype = extract_type(arg)
push!(argctypes_arr, argtype.ctype)
end
rettype = get_ret_type(signalinfo)
argctypes_arr = get_arg_types(signalinfo)
argctypes = Expr(:tuple, argctypes_arr...)
d = quote
function $oname(f, object::$(objtypeinfo.jtype), user_data=object, after=false)
GLib.signal_connect_generic(f, object, $(String(name)), $rettype, $argctypes, after, user_data)
end
function $rettypefunc(object::$(objtypeinfo.jtype))
$rettype
end
function $typeargsfunc(object::$(objtypeinfo.jtype))
$argctypes
end
end
unblock(d)
end
Expand Down
105 changes: 85 additions & 20 deletions docs/src/manual/signals.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,93 @@ signal_connect(win, "notify::title") do obj, pspec # here `obj` is the GObjec
end
```

## Alternative approach to signals and signal-handlers
## Alternative approach to signals and signal handlers

In addition to the "simple" interface, `signal_connect` supports an approach that allows your callback function to be directly compiled to machine code.
In addition to the "simple" interface described above, Gtk4 includes an approach that allows your callback function to be directly compiled to machine code. Gtk4 makes this easier by using GObject introspection data to look up the return type and parameter types, saving the user the hassle of doing this themselves.

This alternative syntax is as follows:
For the "clicked" signal of a `GtkButton`, the equivalent to the example at the beginning of this page is as follows:
```julia
b = GtkButton("Press me")
win = GtkWindow(b, "Callbacks")
function button_cb(::Ptr, b)
println(b, " was clicked!")
end

on_clicked(cb, b)
```

Note that the main difference here, other than the name of the function being called to connect the signal, is the argument list of the callback. The first argument here is always a pointer to the GObject that sends the signal, which in this case is the `GtkButton`.

The full definition of the function `on_clicked` is
```julia
on_clicked(cb::Function, widget::GtkButton, user_data = widget, after = false)
```
where:
- `cb` is your callback function. This will be compiled with `@cfunction`, and you need to follow its rules. In particular, you should use a generic function
(i.e., one defined as `function foo(x,y,z) ... end`), and the
arguments and return type should match the GTK+ documentation for
the widget and signal ([see
examples](https://docs.gtk.org/gtk4/signal.Widget.query-tooltip.html)).
**In contrast with the simpler interface, when writing these callbacks you must include the `user_data` argument**. See examples below.
- `widget` is the widget that will send the signal
- `user_data` contains any additional information your callback needs
to operate. For example, you can pass other widgets, tuples of
values, etc. If omitted (as it was in the example above), it defaults to `widget`.
- `after` is a boolean, `true` if you want your callback to run after
the default handler for your signal. When in doubt, specify `false`.

Functions like this are defined for every signal of every widget supported by Gtk4.jl. They are named `on_signalname`, where signals with `-` in their names have them replaced by underscores `_`. So to connect to `GtkWindow`'s "close-request" signal, you would use `on_close_request`.

When you define the callback, you still have to use the correct argument list or else the call to `@cfunction` will throw an error. It should be `Ptr{GObject}`, `param_types...`, `user_data`. The callback should also return the right type. Functions `signal_return_type(WidgetType, signame)` and `signal_argument_types(WidgetType, signame)` are defined that return the needed types for the signal "signame" of the type "WidgetType".

For example, consider a GUI in which pressing a button updates
a counter:

```julia
box = GtkBox(:h)
button = GtkButton("click me")
label = GtkLabel("0")
push!(box, button)
push!(box, label)
win = GtkWindow(box, "Callbacks")

const counter = [0] # Pack counter value inside array to make it a reference

# "clicked" callback declaration is
# void user_function(GtkButton *button, gpointer user_data)
# But user_data gets converted into a Julia object automatically
function button_cb(widgetptr::Ptr, user_data)
widget = convert(Gtk4.GtkButtonLeaf, widgetptr) # pointer -> object
lbl, cntr = user_data # unpack the user_data tuple
cntr[] = cntr[]+1 # increment counter[1]
lbl.label = string(cntr[])
nothing # return type is void
end

on_clicked(button_cb, button, (label, counter))
```
Here, the tuple `(label, counter)` was passed in as `user_data`. Note that the value of `counter[]` matches the display in the GUI.

### `@guarded`

The "simple" callback interface includes protections against
corrupting Gtk state from errors, but this `@cfunction`-based approach
does not. Consequently, you may wish to use `@guarded` when writing
these functions. ([Canvas](../manual/canvas.md) draw functions and
mouse event-handling are called through this interface, which is why
you should use `@guarded` there.) For functions that should return a
value, you can specify the value to be returned on error as the first
argument. For example:

```julia
const unhandled = convert(Int32, false)
@guarded unhandled function my_callback(widgetptr, ...)
...
end
```

### Old approach to @cfunction based signals
The approach taken by Gtk.jl and earlier versions of Gtk4.jl is still supported, where you supply the return type and parameter types:
```julia
signal_connect(cb, widget, signalname, return_type, parameter_type_tuple, after, user_data=widget)
```
Expand Down Expand Up @@ -164,20 +246,3 @@ write a callback using the "simple" interface, e.g.,

and then use the reported type in `parameter_type_tuple`.

### `@guarded`

The "simple" callback interface includes protections against
corrupting Gtk state from errors, but this `@cfunction`-based approach
does not. Consequently, you may wish to use `@guarded` when writing
these functions. ([Canvas](../manual/canvas.md) draw functions and
mouse event-handling are called through this interface, which is why
you should use `@guarded` there.) For functions that should return a
value, you can specify the value to be returned on error as the first
argument. For example:

```julia
const unhandled = convert(Int32, false)
@guarded unhandled function my_callback(widgetptr, ...)
...
end
```
4 changes: 2 additions & 2 deletions examples/glarea.jl
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ function on_realized(a)
@async println(state.program)
end

function on_render(a, c, user_data)
function render(a, c, user_data)
glUseProgram(state.program)
positionAttribute = glGetAttribLocation(state.program, "position");
glEnableVertexAttribArray(positionAttribute)
Expand All @@ -201,7 +201,7 @@ w = GtkWindow("GL example", wh, wh)
glarea = GtkGLArea()

signal_connect(on_realized, glarea, "realize")
signal_connect(on_render, glarea, "render", Cint, (Ptr{Gtk4.GdkGLContext},))
Gtk4.on_render(render, glarea)

w[]=glarea

Expand Down
2 changes: 1 addition & 1 deletion src/GLib/GLib.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export GList, GSList, glist_iter, _GSList, _GList, GError, GVariant, GType, GBox
export GObject, GInitiallyUnowned, GInterface, GTypeInterface, _GTypeInterface, GParam, GTypeInstance
export GByteArray, GHashTable, GPtrArray
export g_timeout_add, g_idle_add, @idle_add, @guarded, g_source_remove
export cfunction_
export cfunction_, on_notify, signalnames, signal_return_type, signal_argument_types
export gobject_ref, signal_connect, signal_emit, signal_handler_disconnect
export signal_handler_block, signal_handler_unblock
export add_action, add_stateful_action
Expand Down
43 changes: 43 additions & 0 deletions src/GLib/signals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# end
function signal_connect(@nospecialize(cb::Function), w::GObject, sig::AbstractStringLike,
::Type{RT}, param_types::Tuple, after::Bool = false, user_data::CT = w) where {CT, RT}
# could use signal_query to check RT and param_types and throw an error if not correct
signal_connect_generic(cb, w, sig, RT, param_types, after, user_data)
end

Expand Down Expand Up @@ -401,3 +402,45 @@ function waitforsignal(obj::GObject,signal)
end
wait(c)
end

"""
on_notify(f, object::GObject, property::Symbol, user_data = object, after = false)
Connect a callback `f` to the object's "notify::property" signal that will be
called whenever the property changes. The callback signature should be
`f(::Ptr, param::GParam, user_data)` and should return `nothing`.
"""
function on_notify(f, object::GObject, property::Symbol, user_data = object, after = false)
signal_connect_generic(f, object, "notify::$property", Nothing, (GParam,), after, user_data)
end

# The following are overridden by GI for each subtype of GObject

"""
signalnames(::Type{T}) where T <: GObject
Returns a list of the names of supported signals for T.
"""
function signalnames(::Type{GObject})
[:notify]
end

"""
signal_return_type(::Type{T}, name::Symbol) where T <: GObject
Gets the return type for the callback for the signal `name` of a `GObject` type
(for example `GtkWidget`).
"""
function signal_return_type(::Type{GObject},name::Symbol)
Nothing
end

"""
signal_argument_types(::Type{T}, name::Symbol) where T <: GObject
Gets the argument types for the callback for the signal `name` of a `GObject` type
(for example `GtkWidget`).
"""
function signal_argument_types(::Type{GObject},name::Symbol)
(GParam,)
end
2 changes: 1 addition & 1 deletion src/Gtk4.jl
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ end
end

import .GLib: set_gtk_property!, get_gtk_property, run,
signal_handler_is_connected
signal_handler_is_connected, signalnames

# define accessor methods in Gtk4

Expand Down
Loading

0 comments on commit f3285f2

Please sign in to comment.