Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot use GListStore - Boxed Any Type #29

Open
HyperSphereStudio opened this issue May 24, 2023 · 9 comments
Open

Cannot use GListStore - Boxed Any Type #29

HyperSphereStudio opened this issue May 24, 2023 · 9 comments

Comments

@HyperSphereStudio
Copy link

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.

@jwahlstrand
Copy link
Member

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.

@HyperSphereStudio
Copy link
Author

HyperSphereStudio commented May 25, 2023

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

@jwahlstrand
Copy link
Member

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...

@HyperSphereStudio
Copy link
Author

HyperSphereStudio commented May 29, 2023

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

@jwahlstrand
Copy link
Member

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.

@HyperSphereStudio
Copy link
Author

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

@jwahlstrand
Copy link
Member

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...

@HyperSphereStudio
Copy link
Author

HyperSphereStudio commented May 30, 2023

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

@jwahlstrand
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants