Настройка завершения GtkComboBoxText

Как я могу настроить завершение GtkComboBoxText как с "статическим" аспектом, так и с "динамическим"? Статический аспект заключается в том, что некоторые записи известны и добавляются в текст со списком во время построения с gtk_combo_box_text_append_text. Динамический аспект заключается в том, что мне также необходимо выполнить некоторые функции обратного вызова (-ов), которые должны выполняться динамически - после создания виджета GtkComboBoxText - после ввода нескольких символов.

Мое приложение использует Boehm GC (за исключением GTK-объектов, конечно), таких как Guile или SCM или Bigloo. Его можно рассматривать как экспериментальную персистентную динамически типизированную реализацию языка программирования с интегрированным редактором, закодированным на и для Debian/Linux/x86-64 с помощью библиотеки GTK3.21, он закодирован на C99 (некоторые из которых сгенерированы) и скомпилирован с GCC6.

(меня не интересуют системы, отличные от Linux, библиотеки GTK3 старше GTK3.20, компилятор GCC старше GCC6)

вопрос вопросов

Я ввожу (вводя в GtkComboBoxText) либо имя, либо идентификатор объекта.

  • Имя является C-идентификатором, но начинается с буквы и не может заканчиваться символом подчеркивания. Например, comment, if, the_GUI, the_system, payload_json или x1 являются допустимыми именами (но _a0bcd или foobar_ являются недопустимыми именами, потому что они начинаются или заканчиваются знаком подчеркивания). В настоящее время у меня есть большая дюжина имен, но у меня может быть несколько тысяч. Поэтому было бы разумно предложить завершение, только когда набрали только одну или, возможно, две буквы, а завершение для имен может происходить статически, потому что их не так много (поэтому я считаю разумным называть gtk_combo_box_append_text для каждого имени).

  • Идентификатор объекта начинается с символа подчеркивания, за которым следует цифра, и имеет ровно 18 буквенно-цифровых (произвольных) символов. Например, _5Hf0fFKvRVa71ZPM0, _8261sbF1f9ohzu2Iu, _0BV96V94PJIn9si1K являются объектами-объектами. На самом деле это 96 почти случайных бит (возможно, возможно только 2 94). Идентификатор объекта играет роль UUID (в том смысле, что он считается уникальным для всего мира для разных объектов), но имеет C дружественный синтаксис. В настоящее время у меня есть несколько десятков объектов-идентификаторов, но у меня может быть несколько сотен тысяч (или, может быть, миллионов) из них. Но, учитывая префикс из четырех символов, таких как _6S3 или _22z, я предполагаю, что в моем приложении с этим префиксом существует только разумное число (вероятно, не более тысячи и, конечно, не более тысячи) объектов-идентификаторов. Конечно, было бы необоснованным регистрировать (статически) априори все идентификаторы объектов (завершение должно происходить после ввода четырех символов и должно происходить динамически).

Итак, я хочу, чтобы завершение, которое работает как над именами (например, для ввода одной буквы, за которой следует другой символ алфавита, должно быть достаточно, чтобы предлагать не более ста выборов), так и на объектных идентификаторах (набрав четыре символа, например _826 должно быть достаточно, чтобы вызвать завершение, вероятно, не более нескольких десятков вариантов, возможно, тысячи, если не повезет).

Следовательно, набрав три клавиши p a tab, вы получите завершение с несколькими именами, такими как payload_json или payload_vectval и т.д.... и набрав пять клавиш _ 5 H f tab предложит завершение с очень небольшим количеством объектов-идентификаторов, особенно _5Hf0fFKvRVa71ZPM0

образец неполного кода

До сих пор я кодировал следующее:

static GtkWidget *
mom_objectentry (void)
{
  GtkWidget *obent = gtk_combo_box_text_new_with_entry ();
  gtk_widget_set_size_request (obent, 30, 10);
  mo_value_t namsetv = mo_named_objects_set ();

У меня есть значения, полученные от Boehm-garbage, а mo_value_t - указатель на любой из них. Значения могут быть помечены целыми числами, указателями на строки, объекты или кортежи или наборы объектов. Итак, namesetv теперь содержит множество именованных объектов (возможно, менее нескольких тысяч названных объектов).

  int nbnam = mo_set_size (namsetv);
  MOM_ASSERTPRINTF (nbnam > 0, "bad nbnam");
  mo_value_t *namarr = mom_gc_alloc (nbnam * sizeof (mo_value_t));
  int cntnam = 0;
  for (int ix = 0; ix < nbnam; ix++)
    {
      mo_objref_t curobr = mo_set_nth (namsetv, ix);
      mo_value_t curnamv = mo_objref_namev (curobr);
      if (mo_dyncast_string (curnamv))
        namarr[cntnam++] = curnamv;
    }
  qsort (namarr, cntnam, sizeof (mo_value_t), mom_obname_cmp);
  for (int ix = 0; ix < cntnam; ix++)
    gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (obent),
                    mo_string_cstr (namarr[ix]));

в этот момент я отсортировал все имена (несколько тысяч) и добавил "статически" их с помощью gtk_combo_box_text_append_text.

  GtkWidget *combtextent = gtk_bin_get_child (GTK_BIN (obent));
  MOM_ASSERTPRINTF (GTK_IS_ENTRY (combtextent), "bad combtextent");
  MOM_ASSERTPRINTF (gtk_entry_get_completion (GTK_ENTRY (combtextent)) ==
                    NULL, "got completion in combtextent");

Я с удивлением заметил, что gtk_entry_get_completion (GTK_ENTRY (combtextent)) имеет значение null.

Но я застрял здесь. Я думаю:

  • Наличие некоторого mom_set_complete_objectid(const char*prefix), который дал prefix как "_47n" не менее четырех символов, возвратил собранный мусором mo_value_t, представляющий набор объектов с этим префиксом. Это очень легко кодировать для меня, и это почти сделано.

  • Создайте собственный локальный GtkEntryCompletion* mycompl =..., который будет завершен, как я хочу. Затем я поместил бы его в текстовую запись combtextent моего gtk-combo-box-text, используя gtk_entry_set_completion(GTK_ENTRY(combtextent), mycompl);

Должны ли они использовать записи, добавленные с помощью gtk_combo_box_text_append_text для роли статического имени? Как я должен динамически завершить использование значения динамического набора, возвращаемого с моего mom_set_complete_objectid; учитывая некоторый объект-указатель obr и некоторый char bufid[20];, я легко и быстро могу заполнить его объектным идентификатором этого объекта obr с помощью mo_cstring_from_hi_lo_ids(bufid, obr->mo_ob_hid, obr->mo_ob_loid)..

Я не знаю, как закодировать выше. Для справки, теперь я просто возвращаю combo-box-текст:

  // if the entered text starts with a letter, I want it to be
  // completed with the appended text above if the entered text starts
  // with an undersore, then a digit, then two alphanum (like _0BV or
  // _6S3 for example), I want to call a completion function.
#warning objectentry: what should I code here?
  return obent;
}  /* end mom_objectentry */

Мой подход правильный?

Функция mom_objectentry выше используется для заполнения модальных диалогов с коротким временем жизни.

Я предпочитаю простой код по эффективности. На самом деле мой код временный (я надеялся загрузить мой язык и сгенерировать все его C-код!), И на практике у меня, вероятно, будет всего несколько сотен имен и самое большее несколько десятков тысяч объектов-идентификаторов. Таким образом, производительность не очень важна, но более важна простота кодирования (какой-то концептуально "выброшенный" код).

Я не хочу (если возможно) добавлять свои собственные классы GTK. Я предпочитаю использовать существующие классы и виджеты GTK, настраивая их с помощью сигналов GTK и обратных вызовов.

Контекст

Мое приложение представляет собой экспериментальный постоянный язык программирования и реализацию с близким Scheme или Python (или JavaScript, игнорируя прототип,...) семантика, но с совершенно другим (еще не реализованным в сентябре 7 th 2016) синтаксисом (который будет показан и введен в виджеты GTK), используя сборщик мусора Boehm для значения (включая объекты, наборы, кортежи, строки...)... Значения (включая объекты), как правило, постоянны (за исключением связанных с GTK данных: приложение начинается с почти пустого окна). Вся языковая куча сохраняется в JSON-подобном синтаксисе в некоторой базе данных Sqlite (сгенерированной при выходе приложения), сбрасываемой в _momstate.sql, которая повторяется - загружается при запуске приложения. Идентификаторы объектов полезны для отображения ссылок объектов на пользователя в виджетах GTK, настойчивости и для генерации C-кода, связанного с объектами (например, объект id _76f7e2VcL8IJC1hq6 может быть связан с идентификатором mo_76f7e2VcL8IJC1hq6 в некотором сгенерированном C код, отчасти потому, что у меня есть свой формат идентификатора объекта вместо использования UUID).

PS. Мой код C - бесплатное программное обеспечение GPLv3 и доступно на github. Это монитор MELT, ветвь expjs, commit e2b3b99ef66394...

NB: Объекты, упомянутые здесь, неявно являются моими языковыми объектами, а не объектами GTK. У всех есть уникальный идентификатор объекта, а некоторые, но не большинство из них, называются.

Ответ 1

Вот мое предложение:

Используйте GtkListStore, чтобы содержать список управляемых GTK строк (по существу, копии вашей строки идентификатора), которые соответствуют текущему префиксу строка.

(Как описано в gtk_list_store_set(), скопирован элемент G_TYPE_STRING. Я считаю, что накладные расходы дополнительной копии приемлемы здесь; в любом случае, не стоит влиять на реальную производительность, и, в свою очередь, GTK + будет управлять подсчетом ссылок для нас.)

Вышеупомянутое реализовано в функции обратного вызова GTK +, которая получает дополнительный указатель как полезную нагрузку (установленную во время создания или активации GUI, я предлагаю вам использовать некоторую структуру для хранения ссылок, необходимых для создания совпадений). Обратный вызов подключается к сигналу combobox popup, так что он вызывается всякий раз, когда список расширяется.

Обратите внимание, что как отмечено в комментарии B8vrede, a GtkComboBoxText не следует изменять с помощью своей модели; поэтому нужно/следует использовать вместо GtkComboBox.

Практический пример

Для простоты предположим, что все данные, необходимые для поиска или генерации всех известных идентификаторов, сопоставленных с ними, хранятся в структуре, скажем

struct generator {
    /* Whatever data you need to generate prefix matches */
};

а вспомогательная функция помощника со списком - это что-то вроде

static void combo_box_populator(GtkComboBox *combobox, gpointer genptr)
{
    struct generator *const generator = genptr;

    GtkListStore *combo_list = GTK_LIST_STORE(gtk_combo_box_get_model(combobox));

    GtkWidget *entry = gtk_bin_get_child(GTK_BIN(combobox));
    const char *prefix = gtk_entry_get_text(GTK_ENTRY(entry));
    const size_t prefix_len = (prefix) ? strlen(prefix) : 0;

    GtkTreeIter iterator;

    /* Clear the current store */
    gtk_list_store_clear(combo_list);

    /* Initialize the list iterator */
    gtk_tree_model_get_iter_first(GTK_TREE_MODEL(combo_list), &iterator);

    /* Find all you want to have in the combo box;
       for each  const char *match, do:
    */

        gtk_list_store_append(combo_list, &iterator);
        gtk_list_store_set(combo_list, &iterator, 0, match, -1);

    /* Note that the string pointed to by match is copied;
       match is not referred to after the _set() returns.
    */
}

Когда пользовательский интерфейс построен или активирован, вы должны убедиться, что GtkComboBox имеет запись (чтобы пользователь мог писать в нее текст) и GtkListStore model:

    struct generator *generator;
    GtkWidget *combobox;
    GtkListStore *combo_list;

    combo_list = gtk_list_store_new(1, G_TYPE_STRING);
    combobox = gtk_combo_box_new_with_model_and_entry(GTK_TREE_MODEL(combo_list));
    gtk_combo_box_set_id_column(GTK_COMBO_BOX(combobox), 0);
    gtk_combo_box_set_entry_text_column(GTK_COMBO_BOX(combobox), 0);
    gtk_combo_box_set_button_sensitivity(GTK_COMBO_BOX(combobox), GTK_SENSITIVITY_ON);

    g_signal_connect(combobox, "popup", G_CALLBACK(combo_box_populator), generator);

В моей системе всплывающий ускоритель по умолчанию - Alt + Down, но я предполагаю, что вы уже изменили его на Tab.

У меня есть грубый рабочий пример здесь (a .tar.xz tarball, CC0): он читает строки со стандартного ввода и перечисляет те, которые соответствуют префиксу пользователя в обратном порядке в списке со списком (при всплывании). Если запись пуста, в поле со списком будут входить все строки ввода. Я не изменил ускорители по умолчанию, поэтому вместо Tab попробуйте Alt+Down.

У меня также есть тот же пример, но вместо этого GtkComboBoxText (также СС0). Это не использует модель GtkListStore, но использует gtk_combo_box_text_remove_all() и gtk_combo_box_text_append_text(), чтобы напрямую манипулировать содержимым списка. (В двух примерах всего несколько строк.) К сожалению, документация не является явной, ссылается ли этот интерфейс на копии или копирует строки. Хотя копирование является единственным вариантом, который имеет смысл, и это можно проверить из текущих источников Gtk +, отсутствие явной документации заставляет меня колебаться.

Сравнивая два примера, с которыми я связан выше (оба захвата около 500 случайных слов из /usr/share/dict/words, если вы компилируете и запускаете его с помощью make), я не вижу разницы в скорости. Оба используют один и тот же наивный способ выбора префиксных совпадений из связанного списка, что означает, что два метода (GtkComboBox + model, или GtkComboBoxText) должны быть примерно одинаковыми.

На моей собственной машине оба становятся раздражающе медленными с более чем 1000 или около того совпадений во всплывающем окне; всего лишь сотню или меньше матчей, он чувствует себя мгновенно. Это, по моему мнению, указывает на то, что медленный/наивный способ выбора префиксных совпадений из связанного списка не является виновником (потому что весь список перемещается в обоих случаях), но что комбинированные поля GTK + просто не предназначены для больших списков. (Замедление, безусловно, намного, намного хуже линейного.)

Ответ 2

Я не буду показывать точный код о том, как это сделать, потому что я никогда не делал GTK и C только GTK и Python, но это должно быть хорошо, поскольку функции в функциях C и Python могут быть легко переведены.

Подход OP фактически является правильным, поэтому я попытаюсь заполнить пробелы. Поскольку количество статических опций ограничено, вероятно, не будет сильно изменяться, действительно имеет смысл добавить их с помощью gtk_combo_box_text_append, который добавит их во внутреннюю модель GtkComboBoxText.

Thats покрывает статическую часть, для динамической части это было бы идеально, если бы мы могли просто сохранить эту статическую модель и заменить ее временной моделью с помощью gtk_combo_box_set_model(), когда _ был найден в начале строки. Но мы не должны этого делать, поскольку в документации говорится:

Вы не должны вызывать gtk_combo_box_set_model() или пытаться упаковать больше ячеек в это поле со списком через свой интерфейс GtkCellLayout.

Итак, нам нужно обойти это, один из способов сделать это - добавить GtkEntryCompletion к записи GtkComboBoxText. Это сделает попытку ввода для завершения текущей строки на основе ее текущей модели. В качестве дополнительного бонуса он также может добавить все символы, которые имеют все параметры, как это:

введите описание изображения здесь

Поскольку мы не хотим загружать все динамические параметры перед обработкой, я думаю, что лучшим подходом будет подключение прослушивателя changed к GtkEntry, таким образом мы можем загружать динамические параметры, когда у нас есть символ подчеркивания и некоторые персонажи.

Поскольку GtkEntryCompletion использует GtkListStore внутренне, мы можем повторно использовать часть кода Nominal Animal, предоставленную в его ответе. Основное различие заключается в том, что connect выполняется на GtkEntry и замене GtkComboText на GtkEntryCompletion внутри наполнителя. Тогда все должно быть хорошо, я бы хотел написать приличную C, тогда я бы предоставил вам код, но это должно будет сделать.

Изменить: Небольшая демонстрация в Python с GTK3

import gi

gi.require_version('Gtk', '3.0')

import gi.repository.Gtk as Gtk

class CompletingComboBoxText(Gtk.ComboBoxText):
    def __init__(self, static_options, populator, **kwargs):
        # Set up the ComboBox with the Entry
        Gtk.ComboBoxText.__init__(self, has_entry=True, **kwargs)

        # Store the populator reference in the object
        self.populator = populator

        # Create the completion
        completion = Gtk.EntryCompletion(inline_completion=True)

        # Specify that we want to use the first col of the model for completion
        completion.set_text_column(0)
        completion.set_minimum_key_length(2)

        # Set the completion model to the combobox model such that we can also autocomplete these options
        self.static_options_model = self.get_model()
        completion.set_model(self.static_options_model)

        # The child of the combobox is the entry if 'has_entry' was set to True
        entry = self.get_child()
        entry.set_completion(completion)

        # Set the active option of the combobox to 0 (which is an empty field)
        self.set_active(0)

        # Fill the model with the static options (could also be used for a history or something)
        for option in static_options:
            self.append_text(option)

        # Connect a listener to adjust the model when the user types something
        entry.connect("changed", self.update_completion, True)


    def update_completion(self, entry, editable):
        # Get the current content of the entry
        text = entry.get_text()

        # Get the completion which needs to be updated
        completion = entry.get_completion()

        if text.startswith("_") and len(text) >= completion.get_minimum_key_length():
            # Fetch the options from the populator for a given text
            completion_options = self.populator(text)

            # Create a temporary model for the completion and fill it
            dynamic_model = Gtk.ListStore.new([str])
            for completion_option in completion_options:
                dynamic_model.append([completion_option])
            completion.set_model(dynamic_model)
        else:
            # Restore the default static options
            completion.set_model(self.static_options_model)


def demo():
    # Create the window
    window = Gtk.Window()

    # Add some static options
    fake_static_options = [
        "comment",
        "if",
        "the_GUI",
        "the_system",
        "payload_json",
        "x1",
        "payload_json",
        "payload_vectval"
    ]

    # Add the the Combobox
    ccb = CompletingComboBoxText(fake_static_options, dynamic_option_populator)
    window.add(ccb)

    # Show it
    window.show_all()
    Gtk.main()


def dynamic_option_populator(text):
    # Some fake returns for the populator
    fake_dynamic_options = [
        "_5Hf0fFKvRVa71ZPM0",
        "_8261sbF1f9ohzu2Iu",
        "_0BV96V94PJIn9si1K",
        "_0BV1sbF1f9ohzu2Iu",
        "_0BV0fFKvRVa71ZPM0",
        "_0Hf0fF4PJIn9si1Ks",
        "_6KvRVa71JIn9si1Kw",
        "_5HKvRVa71Va71ZPM0",
        "_8261sbF1KvRVa71ZP",
        "_0BKvRVa71JIn9si1K",
        "_0BV1KvRVa71ZPu2Iu",
        "_0BV0fKvRVa71ZZPM0",
        "_0Hf0fF4PJIbF1f9oh",
        "_61sbFV0fFKn9si1Kw",
        "_5Hf0fFKvRVa71ozu2",
    ]

    # Only return those that start with the text
    return [fake_dynamic_option for fake_dynamic_option in fake_dynamic_options if fake_dynamic_option.startswith(text)]


if __name__ == '__main__':
    demo()
    Gtk.main()