2

So when writing UI in GTK it's generally preferrable to handle reading of files, etc. in an Async Method. things such as listboxes, are generally bound to a ListModel, the items in the ListBox updated in accordance with the items_changed signal.

So if I have some class, that implements ListModel, and has an add function, and some FileReader that holds a reference to said ListModel, and call add from an async function, how do i make that in essence triggering the items_changed and having GTK update accordingly?

I've tried list.items_changed.connect(message("Items changed!")); but it never triggers.

I saw this: How can one update GTK+ UI in Vala from a long operation without blocking the UI but in this example, it's just the button label that is changed, no signal is actually triggered.

EDIT: (Codesample added at the request of @Michael Gratton

//Disclaimer: everything here is still very much a work in progress, and will, as soon as I'm confident that what I have is not total crap, be released under some GPL or other open license.

//Note: for the sake of readability, I adopted the C# naming convention for interfaces, namely, putting a capital 'I' in front of them, a decision i do not feel quite as confident in as I did earlier.
//Note: the calls to message(..) was put in here to help debugging    

public class AsyncFileContext : Object{


    private int64 offset;
    private bool start_read;
    private bool read_to_end;

    private Factories.IVCardFactory factory;
    private File file;
    private FileMonitor monitor;

    private Gee.Set<IVCard> vcard_buffer;

    private IObservableSet<IVCard> _vCards;
    public IObservableSet<IVCard> vCards { 
        owned get{
            return this._vCards;
        } 
    }

    construct{
        //We want to start fileops at the beginning of the file
        this.offset = (int64)0;
        this.start_read = true;
        this.read_to_end = false;
        this.vcard_buffer = new Gee.HashSet<IVCard>();
        this.factory = new Factories.GenericVCardFactory();
    }

    public void add_vcard(IVCard card){
        //TODO: implement
    }

    public AsyncFileContext(IObservableSet<IVCard> vcards, string path){
        this._vCards = vcards;
        this._vCards = IObservableSet.wrap_set<IVCard>(new Gee.HashSet<IVCard>());
        this.file = File.new_for_path(path);
        this.monitor = file.monitor_file(FileMonitorFlags.NONE, null);
        message("1");
        //TODO: add connect
        this.monitor.changed.connect((file, otherfile, event) => {
            if(event != FileMonitorEvent.DELETED){
                bool changes_done = event == FileMonitorEvent.CHANGES_DONE_HINT;
                Idle.add(() => {
                    read_file_async.begin(changes_done);
                    return false;
                });
            }
        });
        message("2");
        //We don't know that changes are done yet
        //TODO: Consider carefully how you want this to work when it is NOT called from an event

        Idle.add(() => {
            read_file_async.begin(false);
            return false;
        });
    }


    //Changes done should only be true if the FileMonitorEvent that triggers the call was CHANGES_DONE_HINT
    private async void read_file_async(bool changes_done) throws IOError{
        if(!this.start_read){
            return;
        }
        this.start_read = false;
        var dis = new DataInputStream(yield file.read_async());
        message("3");
        //If we've been reading this file, and there's then a change, we assume we need to continue where we let off
        //TODO: assert that the offset isn't at the very end of the file, if so reset to 0 so we can reread the file
        if(offset > 0){
            dis.seek(offset, SeekType.SET);
        }

        string line;
        int vcards_added = 0;
        while((line = yield dis.read_line_async()) != null){
            message("position: %s".printf(dis.tell().to_string()));
            this.offset = dis.tell();
            message("4");
            message(line);
            //if the line is empty, we want to jump to next line, and ignore the input here entirely
            if(line.chomp().chug() == ""){
                continue;
            }

            this.factory.add_line(line);

            if(factory.vcard_ready){
                message("creating...");
                this.vcard_buffer.add(factory.create());
                vcards_added++;
                //If we've read-in and created an entire vcard, it's time to yield
                message("Yielding...");
                Idle.add(() => {
                    _vCards.add_all(vcard_buffer);
                    vcard_buffer.remove_all(_vCards);
                    return false;
                });
                Idle.add(read_file_async.callback);
                yield;
                message("Resuming");
            }
        }
        //IF we expect there will be no more writing, or if we expect that we read ALL the vcards, and did not add any, it's time to go back and read through the whole thing again.
        if(changes_done){ //|| vcards_added == 0){
            this.offset = 0;
        }
        this.start_read = true;
    }

}

//The main idea in this class is to just bind the IObservableCollection's item_added, item_removed and cleared signals to the items_changed of the ListModel. IObservableCollection is a class I have implemented that merely wraps Gee.Collection, it is unittested, and works as intended
public class VCardListModel : ListModel, Object{

    private Gee.List<IVCard> vcard_list;
    private IObservableCollection<IVCard> vcard_collection;

    public VCardListModel(IObservableCollection<IVCard> vcard_collection){
        this.vcard_collection = vcard_collection;
        this.vcard_list = new Gee.ArrayList<IVCard>.wrap(vcard_collection.to_array());

        this.vcard_collection.item_added.connect((vcard) => {
            vcard_list.add(vcard);
            int pos = vcard_list.index_of(vcard);
            items_changed(pos, 0, 1);
        });

        this.vcard_collection.item_removed.connect((vcard) => {
            int pos = vcard_list.index_of(vcard);
            vcard_list.remove(vcard);
            items_changed(pos, 1, 0);
        });
        this.vcard_collection.cleared.connect(() => {
            items_changed(0, vcard_list.size, 0);
            vcard_list.clear();
        });

    }

    public Object? get_item(uint position){
        if((vcard_list.size - 1) < position){
            return null;
        }
        return this.vcard_list.get((int)position);
    }

    public Type get_item_type(){
        return Type.from_name("VikingvCardIVCard");
    }

    public uint get_n_items(){
        return (uint)this.vcard_list.size;
    }

    public Object? get_object(uint position){
        return this.get_item((int)position);
    }

}

//The IObservableCollection parsed to this classes constructor, is the one from the AsyncFileContext
public class ContactList : Gtk.ListBox{

    private ListModel list_model;

    public ContactList(IObservableCollection<IVCard> ivcards){
        this.list_model = new VCardListModel(ivcards);

        bind_model(this.list_model, create_row_func);
        list_model.items_changed.connect(() => {
            message("Items Changed!");
            base.show_all();
        });
    }

    private Gtk.Widget create_row_func(Object item){
        return new ContactRow((IVCard)item);
    }

}
rasmus91
  • 3,024
  • 3
  • 20
  • 32
  • Does your class call [`g_list_model_items_changed()`](https://developer.gnome.org/gio/stable/GListModel.html#g-list-model-items-changed)? – Alexander Dmitriev Sep 29 '19 at 12:45
  • Yes! And when adding items to it, from a syncronous method (a not async one), they show up in the UI. – rasmus91 Sep 29 '19 at 12:48
  • 1
    Looks like the problem is that you call `add` right from your async method. GTK is not threadsafe and it's UB to call any of it's functions **not** from mainloop. Here is what happens: you call `add` from another thread, `"items_changed"` is emitted and GtkListBox callback gets called immidiately (which is an error). That's why `Idle.add (test_async.callback);` is used in your link. – Alexander Dmitriev Sep 29 '19 at 13:11
  • Sure, but how/where should I call `add` the? – rasmus91 Sep 29 '19 at 14:06
  • You can call `idle_add` from anywhere, it's thread safe. The function which is idle_add'ed will be called from mainloop. Or you can use `GTask`, which has a callback which is *guaranteed to be invoked in a later iteration of the thread-default main context* [(link)](https://developer.gnome.org/gio/stable/GAsyncResult.html#GAsyncReadyCallback) – Alexander Dmitriev Sep 29 '19 at 14:14
  • So what, something like: `async void readfile{ /*lots of filereading*/ Idle.add(() => list_model.add(element); yield; }` should I do `Idle.add(readfile.callback);` before yielding too, to make certain the function is resumed eventually? – rasmus91 Sep 29 '19 at 14:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/200124/discussion-between-user3801839-and-alexander-dmitriev). – rasmus91 Sep 29 '19 at 15:54
  • I don't think the problem is with the fact you're adding items via async methods, since unless you are doing something weird, the async code will be executed from the main loop anyway. So the problem is likely with some other aspect of your code. It would help if you posted a minimal example that demonstrates the problem. – Michael Gratton Oct 01 '19 at 01:49
  • @MichaelGratton sure. I'll do that, as soon as I have ten minutes today – rasmus91 Oct 01 '19 at 04:36
  • Thanks for the code sample, but it's definitely not minimal and doesn't even compile. Since you are providing the implementation of GLib.List, if the items_changed signal is not being emitted, it's because your impl isn't invoking it. I'd add some debug lines to your vcard_collection signal handlers to check they are being emitted - I suspect they aren't. – Michael Gratton Oct 01 '19 at 12:21
  • @MichaelGratton I had that thought as well, and had connected a message to the items_changed event of the ListModel. It printed in the console exactly as it should. – rasmus91 Oct 01 '19 at 14:58
  • Okay, well the issue must be in how you're running the async code. Where's the main loop being launched, etc? – Michael Gratton Oct 03 '19 at 03:48
  • It's a granite application. I assume that means it's the standard gtk mainloop being launched? – rasmus91 Oct 03 '19 at 20:49
  • How did you solve this? – aggsol Feb 26 '20 at 12:58
  • 1
    @aggsol gee, its been a while. I honestly can't recall, but I'll give the code a glance, and let you know if I solved it or just made a workaround (I believe, sadly it ended up just being a workaround) – rasmus91 Feb 26 '20 at 16:43
  • @rasmus91 That would be great. Maybe it is even worth an answer! – aggsol Feb 28 '20 at 08:11

1 Answers1

0

Heres the way i 'solved' it.

I'm not particularly proud of this solution, but there are a couple of awful things about the Gtk ListBox, one of them being (and this might really be more of a ListModel issue) if the ListBox is bound to a ListModel, the ListBox will NOT be sortable by using the sort method, and to me at least, that is a dealbreaker. I've solved it by making a class which is basically a List wrapper, which has an 'added' signal and a 'remove' signal. Upon adding an element to the list, the added signal is then wired, so it will create a new Row object and add it to the list box. That way, data is control in a manner Similar to ListModel binding. I can not make it work without calling the ShowAll method though.

private IObservableCollection<IVCard> _ivcards;
        public IObservableCollection<IVCard> ivcards {
            get{
                return _ivcards;
            }
            set{
                this._ivcards = value;

                foreach(var card in this._ivcards){
                    base.prepend(new ContactRow(card));
                }

                this._ivcards.item_added.connect((item) => {
                    base.add(new ContactRow(item));
                    base.show_all();
                });

                base.show_all();

            }
        }

Even though this is by no means the best code I've come up with, it works very well.

rasmus91
  • 3,024
  • 3
  • 20
  • 32