Smooth text input field with autocompleted units?



I'm working on a settings dialog for a little app and I needed a custom
field for the user to enter a time, which can be either minutes, seconds
or hours. Also, I needed decent i18n. Finally, I'm really picky and I
didn't want a combobox to choose a unit.

I came up with an approach that uses a single GtkEntry with a
GtkEntryCompletion, and here it is in Valaâ

        private class TimeEntryDialog : Gtk.Dialog {
                private Gtk.Widget ok_button;
                private Gtk.Entry time_entry;
                
                private Gtk.ListStore completion_store;
                
                public signal void time_entered(int time_seconds);
                
                public TimeEntryDialog(Gtk.Window? parent, string title) {
                        Object();
                        
                        this.set_title(title);
                        
                        this.set_modal(true);
                        this.set_destroy_with_parent(true);
                        this.set_transient_for(parent);
                        
                        this.ok_button = this.add_button(Gtk.Stock.OK, Gtk.ResponseType.OK);
                        this.response.connect((response_id) => {
                                if (response_id == Gtk.ResponseType.OK) this.submit();
                        });
                        
                        Gtk.Container content_area = (Gtk.Container)this.get_content_area();
                        
                        Gtk.Grid content_grid = new Gtk.Grid();
                        content_grid.margin = 6;
                        content_grid.set_row_spacing(4);
                        content_area.add(content_grid);
                        
                        Gtk.Label entry_label = new Gtk.Label(title);
                        content_grid.attach(entry_label, 0, 0, 1, 1);
                        
                        this.time_entry = new Gtk.Entry();
                        this.time_entry.activate.connect(this.submit);
                        this.time_entry.changed.connect(this.time_entry_changed);
                        content_grid.attach(this.time_entry, 0, 1, 1, 1);
                        
                        Gtk.EntryCompletion completion = new Gtk.EntryCompletion();
                        this.completion_store = new Gtk.ListStore(1, typeof(string));
                        completion.set_model(this.completion_store);
                        completion.set_text_column(0);
                        completion.set_inline_completion(true);
                        completion.set_popup_completion(true);
                        completion.set_popup_single_match(false);
                        
                        this.time_entry.set_completion(completion);
                        
                        content_area.show_all();
                }
                
                public void time_entry_changed() {
                        string text = this.time_entry.get_text();
                        string[] text_parts = text.split(" ");
                        
                        int time = 1;
                        // You might notice this is totally not good i18n.
                        // It'll use a regex in NaturalTime soon!
                        if (text_parts.length > 0) {
                                time = int.parse(text_parts[0]);
                        }
                        if (time < 1) time = 1;
                        
                        // replace completion options without deleting rows
                        // if we delete rows, gtk throws some unhappy error messages
                        Gtk.TreeIter iter;
                        bool iter_valid = this.completion_store.get_iter_first(out iter);
                        if (!iter_valid) this.completion_store.append(out iter);
                        
                        string[] completions = NaturalTime.get_completions_for_time(time);
                        foreach (string completion in completions) {
                                this.completion_store.set(iter, 0, completion, -1);
                                
                                iter_valid = this.completion_store.iter_next(ref iter);
                                if (!iter_valid) this.completion_store.append(out iter);
                        }
                        
                        if (NaturalTime.get_seconds_for_label(text) > 0) {
                                this.ok_button.set_sensitive(true);
                        } else {
                                this.ok_button.set_sensitive(false);
                        }
                }
                
                public void submit() {
                        int time = NaturalTime.get_seconds_for_label(this.time_entry.get_text());
                        if (time > 0) {
                                this.time_entered(time);
                                this.destroy();
                        } else {
                                Gdk.beep();
                        }
                }
        }

The full source belongs to break_settings in
<https://code.launchpad.net/~dylanmccall/brainbreak/trunk>.

Now, this feels pretty smooth in practice, but I'm not thrilled with how
it works. Whenever the time field is changed, time_entry_changed
generates a list of possible completions based on the time that has been
entered (if there is one). So, in English if you enter "12 " it'll add
"12 hours," "12 seconds" and "12 minutes" to the completion's model. The
main push of the ugliness comes when we're adding those completions to
the completion_store. If I remove a row that is being used for the
inline completion, I get an unhappy-looking error in the console. So, I
have a loop that manually goes through and _updates_ each row, adding
more rows as necessary. It feels pretty ugly, and it probably wastes a
few thousand CPU cycles.

I think this would be much tidier if I could do any of the following:

      * Detect whether the EntryCompletion is already showing a match
        (and therefore don't add to the model).
      * Call Gtk.EntryCompletion.clear() at any time without making gtk
        go âGLib-CRITICAL **: g_sequence_remove: assertion `iter !=
        NULL' failedâ.
      * Build an EntryCompletion that matches individual words instead
        of the entire thing. Close enough to what I have here, and if I
        put it in those terms maybe it'll light up some distant
        recollections?
      * Build an EntryCompletion that uses regex (or a wildcard) for its
        matching, replacing a group with the user's input. I know we
        have gtk_entry_completion_set_match_func but I don't see a way
        to tell EntryCompletion to use a different string in its
        completion list. It will just use exactly what is in that
        TreeIter we returned true for.

If anyone has an idea, please share. Thank you :)

--
Dylan McCall

dylanmccall gmail com
http://www.dylanmccall.com






[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]