Gtk4.jl icon indicating copy to clipboard operation
Gtk4.jl copied to clipboard

Cannot use GListStore - Boxed Any Type

Open HyperSphereStudio opened this issue 1 year ago • 9 comments

Gtk4 Utilizes a new storage model GListStore which takes in a GObject.

I cannot find any documentation on how to insert in any data. I was wondering if a boxed julia value could be introduced.


mutable struct MyWrappedType
	x::Int
end

model = GLib.GListStore(MyWrappedType)

factory = GtkSignalListItemFactory()

signal_connect((f, li) -> set_child(li, GtkLabel("")), factory, "setup")
signal_connect((f, li) -> get_child(li).label = string(li[].x), factory, "bind")

push!(model, MyWrappedType(1)) #Cannot due for any data type atm

cv = GtkColumnView(model = GtkSelectionModel(GtkSingleSelection(Gtk4.GListModel(model))))
cvc = GtkColumnViewColumn("Test", factory)

append!(cv, cvc)

My thinking on implementation is to create a G_TYPE that wraps an integer that points back to an Any[] on the julia side of things.

Let me know if you can do something like this at the moment.

HyperSphereStudio avatar May 24 '23 13:05 HyperSphereStudio

I've only used the new list views using GtkStringList, which wraps strings as GObjects. You can use this to store a list of keys of a Julia Dict, and then use the key to look up Julia values in the "bind" callback. I'm not sure how to define a GObject wrapper for Julia values without writing some C code... There's probably a way that I'm not seeing. If you come up with something that works, I'd be happy to discuss further.

jwahlstrand avatar May 25 '23 11:05 jwahlstrand

Thanks! Ill try this :)

For others pursuing this deeper, I reccommend just creating a custom GListModel that is backed by a Julia Array for better memory & performance.

It looks like gtk is having more emphasis towards using their type system so it might be worth it to take a look at the define class macro as I am sure more stuff will use it in the future

HyperSphereStudio avatar May 25 '23 12:05 HyperSphereStudio

It seems extreme to have to define a new GObject to properly use the new list views, but it looks like that's what you do in the Javascript bindings: https://rmnvgr.gitlab.io/gtk4-gjs-book/application/list-widgets/

Being able to do this would also allow defining custom widgets. I wonder if it could be done in pure Julia...

jwahlstrand avatar May 29 '23 12:05 jwahlstrand

I built an efficient manager with what we have with the GString idea. On my computer it can create a 2x100k row columnview in ~.5 seconds. I can PR it if anyone is interested in it.

Below is the current version:

append!(cv::GtkColumnView, cvc::GtkColumnViewColumn) = G_.append_column(cv, cvc)

GtkNoSelection(model) = G_.NoSelection_new(model)

mutable struct GtkJuliaStore
    items::Dict{Ptr{GObject}, Any}
    store::GListStore
    freeNames::Array{Ptr{GObject}}

    GtkJuliaStore() = new(Dict{Ptr{GObject}, Any}(), GLib.GListStore(:GObject), Ptr{GObject}[])
    GtkJuliaStore(items::AbstractArray) = (g = GtkJuliaStore(); append!(g, items))
    GtkJuliaStore(items...) = GtkJuliaStore(collect(items))

    Gtk4.GListModel(g::GtkJuliaStore) = Gtk4.GListModel(g.store)
    Base.getindex(g::GtkJuliaStore, i::Integer) = g.items[unsafegetname(g.store, i)]
    Base.setindex!(g::GtkJuliaStore, v, i::Integer) = g.items[unsafegetname(g.store, i)] = v
    Base.keys(lm::GtkJuliaStore) = keys(g.store)
    Base.eltype(::Type{GtkJuliaStore}) = Any
    Base.iterate(g::GtkJuliaStore, i=0) = (i == length(g) ? nothing : (getindex(g, i + 1), i + 1))
    Base.length(g::GtkJuliaStore) = length(g.store)
    Base.empty!(g::GtkJuliaStore) = (empty!(g.items); empty!(g.store); empty!(freeNames))
    Base.pushfirst!(g::GtkJuliaStore, item) = insert!(g, 1, item)
    Base.append!(g::GtkJuliaStore, items) = foreach(x -> push!(g, x), items)
    Base.getindex(g::GtkJuliaStore, i::GtkListItem) = g.items[ccall(("gtk_list_item_get_item", libgtk4), Ptr{GObject}, (Ptr{GObject},), i)]
    Base.setindex!(g::GtkJuliaStore, v, i::GtkListItem) = g.items[ccall(("gtk_list_item_get_item", libgtk4), Ptr{GObject}, (Ptr{GObject},), i)] = v
    unsafegetname(ls::GListStore, i) = ccall(("g_list_model_get_object", libgio), Ptr{GObject}, (Ptr{GObject}, UInt32), ls, i-1)

    function nextname(g::GtkJuliaStore)
        name = length(g.freeNames) == 0 ? Symbol("$(length(g))") : pop!(g.freeNames)
        return ccall(("gtk_string_object_new", libgtk4), Ptr{GObject}, (Cstring,), name)
    end

    function Base.push!(g::GtkJuliaStore, item)
        name = nextname(g)
        ccall(("g_list_store_append", libgio), Nothing, (Ptr{GObject}, Ptr{GObject}), g.store, name)
        g.items[name] = item
        return nothing
    end

    function Base.insert!(g::GtkJuliaStore, i::Integer, item)
        name = nextname(g)
        ccall(("g_list_store_insert", libgio), Nothing, (Ptr{GObject}, UInt32, Ptr{GObject}), g.store, i-1, name)
        g.items[name] = item
        return nothing
    end

    function Base.deleteat!(g::GtkJuliaStore, i::Integer)
        name = unsafegetname(g.store, i)
        push!(freeNames, name)
        delete!(g.items, name)
        ccall(("g_list_store_remove", libgio), Nothing, (Ptr{GObject}, UInt32), g.store, i-1)
        return nothing
    end   
end

function GtkJuliaColumnViewColumn(store::GtkJuliaStore, name::String, @nospecialize(init_child::Function), @nospecialize(update_child::Function))
    factory = GtkSignalListItemFactory()
    signal_connect((f, li) -> set_child(li, init_child()), factory, "setup")
    signal_connect((f, li) -> update_child(get_child(li), store[li]), factory, "bind")
    return GtkColumnViewColumn(name, factory)
end

mutable struct MyTestStruct
    num::Integer
    name::String
end

##Testing
win = GtkWindow()
sw = GtkScrolledWindow()
win[] = sw

store = GtkJuliaStore()

@time for w in 1:100000
    push!(store, MyTestStruct(w, "Number:$w"))
end

name_c = GtkJuliaColumnViewColumn(store, "Name", () -> GtkLabel(""), (c, i) -> c.label = i.name)
num_c = GtkJuliaColumnViewColumn(store, "Number", () -> GtkLabel(""), (c, i) -> c.label = string(i.num))

cv = GtkColumnView(model = GtkSelectionModel(GtkSingleSelection(Gtk4.GListModel(store))))

append!(cv, name_c)
append!(cv, num_c)

sw[] = cv

image

HyperSphereStudio avatar May 29 '23 19:05 HyperSphereStudio

What I had in mind was to use GtkStringList as the model and use the string items to look up items from a Julia dictionary in the bind callback, like this (adapted from the listview.jl example):

using Gtk4, Gtk4.GLib

win = GtkWindow("Listview demo")
win[] = sw = GtkScrolledWindow()

struct MyTestStruct
    num::Int
    name::String
end

dict = Dict{String,MyTestStruct}()
for w=1:100000
    dict[string(w)] = MyTestStruct(w,"Number:$w")
end

model = GtkStringList(string.(1:100000))
factory = GtkSignalListItemFactory()

setup_cb(f, li) = set_child(li,GtkLabel(""))

function bind_cb(f, li)
    text = li[].string
    label = get_child(li)
    label.label = string(dict[text].name)
end

signal_connect(setup_cb, factory, "setup")
signal_connect(bind_cb, factory, "bind")

sw[] = GtkListView(GtkSelectionModel(GtkSingleSelection(GLib.GListModel(model))), factory)

Looking at the constructor for GtkStringList I'm concerned about the list of strings being garbage collected. But this seems to work.

I think the API for GtkListView, GtkColumnView, etc. should be wrapped in a nicer way and I like how you created a constructor that takes "setup" and "bind" functions.

jwahlstrand avatar May 29 '23 23:05 jwahlstrand

Ah I see what you meant

I think my version my be safe from collecting since I am using handles to the GString objects as the keys.

I use unsafe functions to bypass all collecting so its not boxing/unboxing many times between calls.

I think the GtkStringStore is also immutable (or atleast not meant to be changed often due to the internal structure) which is why I used the GtkListStore

HyperSphereStudio avatar May 30 '23 01:05 HyperSphereStudio

Yeah, I think GtkStringList is mostly meant to support simple situations like GtkDropDown. For optimal efficiency it seems like you're supposed to create your own GListModel that produces GObjects on command, rather than creating a ton of GObjects ahead of time. I think the purpose of the new list views is to avoid having to copy your data into a special model data structure (like GtkListStore or GtkTreeStore), which is pretty redundant. Instead you fetch the data needed to render the row in a callback. It sounds nice but they want the data in the form of a GObject...

jwahlstrand avatar May 30 '23 02:05 jwahlstrand

I have also been experimenting with building powerful observables.


on_update_signal_name(::GtkButton) = "clicked"
on_update_signal_name(::GtkComboBoxText) = "changed"
on_update_signal_name(::GtkAdjustment) = "value-changed"
on_update_signal_name(::GtkEntry) = "activate"

Observables.on(@nospecialize(cb::Function), w::GtkWidget) = signal_connect(cb, w, on_update_signal_name(w))
Observables.connect!(w::GtkWidget, o::AbstractObservable) = on(v -> w[] = v, o)

Base.getindex(g::GtkEntry, ::Type{String}) = g.text
Base.getindex(g::GtkLabel, ::Type{String}) = g.label
Base.getindex(g::GtkComboBoxText, ::Type{String}) = Gtk4.active_text(g)
Base.getindex(g::GtkAdjustment, ::Type{Number}) = Gtk4.value(g)

Base.getindex(g::Union{GtkEntry, GtkLabel, GtkComboBoxText}, t::Type = String) = parse(g, t)

Base.setindex!(g::GtkLabel, v) = g.label = string(v)
Base.setindex!(g::GtkEntry, v) = g.text = string(v)
Base.setindex!(g::GtkAdjustment, v) = Gtk4.value(g, v)

function Gtk4.set_gtk_property!(o::GObject, name::String, value::AbstractObservable) 
    set_gtk_property!(o, name, value[])
    on(v -> set_gtk_property!(o, name, v), value)
end

function Observables.ObservablePair(w::GtkWidget, o::AbstractObservable{T}) where T
    done = Ref(false)
    on(w) do w
        if !done[]
            done[] = true
            o[] = w[T]
            done[] = false
        end
    end
    on(o) do val
        if !done[]
            done[] = true
            w[] = val
            done[] = false
        end
    end
end

What this snippet allows you to do is something like this....


w = GtkWindow()
v = GtkEntry()
w[] = v
o = Observable(3)
on(v -> println("Changed:$v"), o)


connect!(w, o) 		#Update the widget entry when the observable changes (not when widget changes)

Observables.ObservablePair(v, o)  	#Update the the observable if the entry changes, update the entry if the observable changes.


You can also do stuff on non widgets since I overrided the set_gtk_prop.

my_random_g_object.prop = o #Will update the property when o is updated

Not sure if this is something that others may want to use in the future / something for this library, just something that may be good discussion

HyperSphereStudio avatar May 30 '23 14:05 HyperSphereStudio

Thanks, PR's are generally welcome. My goal for this package is to keep the layers of stuff on top of GObject introspection relatively light. If we can make the GtkListViews easier to use in Julia without defining new types, that would be ideal IMO. Other packages could do more sophisticated stuff on top of this one.

In Gtk.jl, Observable support is in GtkObservables.jl, and I was thinking of keeping the same separation here to keep dependencies to a minimum (I'm not thrilled with the Graphics.jl dependency inherited from Gtk.jl). I have ported GtkObservables.jl to Gtk4 to test Gtk4.jl, but haven't used it much myself yet.

jwahlstrand avatar Jun 03 '23 13:06 jwahlstrand