自定义完成 GtkComboBoxText

customizing completion of GtkComboBoxText

如何自定义 GtkComboBoxText 的 "static" 方面和 "dynamic" 方面的完成?静态方面是因为一些条目是已知的,并在构建时使用 gtk_combo_box_text_append_text 添加到组合框文本中。动态方面是因为我还需要通过一些回调函数来完成,即完成 动态 - 在创建 GtkComboBoxText 小部件之后 - 一旦几个字符被输入键入。

我的应用程序使用 Boehm 的 GC(当然除了 GTK 对象),就像 Guile 或 SCM 或 Bigloo 正在做的那样。它可以被看作是一个实验性的 persistent 动态类型的编程语言实现,带有一个集成的编辑器,针对 Debian/Linux/x86-64 和系统 GTK3.21 库进行编码,它是用 C99 编码的(其中一些是生成的)并使用 GCC6 编译。

(我不关心非 Linux 系统,GTK3 库早于 GTK3.20,GCC 编译器早于 GCC6)

问题详情

我正在输入(输入 GtkComboBoxText)一个 name,或一个 object-id

所以我想要一个既适用于名称(例如键入一个字母后跟另一个字母数字字符应该足以建议完成最多一百个选择)和对象 ID(键入四个字符)的完成像 _826 应该足以触发最多可能有几十个选择的完成,如果运气不好的话可能有一千个)。

因此键入三个键 p a tab 将提供几个名称的补全像 payload_jsonpayload_vectval 等...并输入五个键 _ 5 H f tab 将提供非常少的对象 ID 的完成,特别是 _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-collected 值,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 添加了 "statically"。

  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)) 为空。

但我被困在这里了。我在想:

  1. 有一些 mom_set_complete_objectid(const char*prefix) 给定一个 prefix 至少四个字符的 "_47n" 会 return 垃圾收集 mo_value_t表示具有该前缀的对象集。这对我来说很容易编写代码,并且快完成了。

  2. 制作我自己的本地 GtkEntryCompletion* mycompl = ...,它会像我想要的那样完成。然后我会使用 gtk_entry_set_completion(GTK_ENTRY(combtextent), mycompl);

  3. 将其放入 gtk-combo-box-text 的文本条目 combtextent

它是否应该使用添加了 gtk_combo_box_text_append_text 的条目作为 "static" 名称完成角色?我应该如何使用 mom_set_complete_objectid 中的动态设置值 return 动态完成;给定一些对象指针 obr 和一些 char bufid[20]; 我可以轻松快速地用该对象的对象 ID obrmo_cstring_from_hi_lo_ids(bufid, obr->mo_ob_hid, obr->mo_ob_loid)..[=61 填充它=]

我不知道如何编写上面的代码。作为参考,我现在只是 returning 组合框文本:

  // 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函数用于填充生命周期较短的模态对话框。

我更喜欢简单的代码而不是效率。实际上,我的代码是临时的(我希望 bootstrap 我的语言,并生成它的所有 C 代码!)实际上我可能只有几百个名字,最多只有几十万个对象 ID。所以性能不是很重要,但编码的简单性(一些概念上 "throw away" 代码)更重要。

我不想(如果可能的话)添加我自己的 GTK 类。我更喜欢使用现有的 GTK 类 和小部件,使用 GTK 信号和回调对其进行自定义。

上下文

我的应用程序是一个实验性的 persistent programming language and implementation with a near Scheme or Python (or JavaScript, ignoring the prototype aspect, ...) semantics but with a widely different (not yet implemented in september 7th, 2016) syntax (to be shown & input in GTK widgets), using the Boehm garbage collector for values (including objects, sets, tuples, strings...)... Values (including objects) are generally persistent (except the GTK related data : the application starts with a nearly empty window). The entire language heap is persisted in JSON-like syntax in some Sqlite "database" (generated at application exit) dumped into _momstate.sql,它会在应用程序启动时重新加载。对象 ID 可用于在 GTK 小部件中向用户显示对象引用,以实现持久性,并生成与对象相关的 C 代码(例如,id _76f7e2VcL8IJC1hq6 的对象可能与 mo_76f7e2VcL8IJC1hq6 标识符相关在一些生成的 C 代码中;这就是为什么我使用我的对象 ID 格式而不是使用 UUID 的部分原因。

PS。我的 C 代码是 GPLv3 免费软件,可在 github 上获得。它是 MELT 监视器,分支 expjs, commit e2b3b99ef66394...

注意:这里提到的对象隐式是我的语言对象,而不是 GTK 对象。它们都有一个唯一的对象 ID,其中一些(但不是大多数)被命名。

这是我的建议:

使用 GtkListStore 来包含与当前前缀字符串匹配的 GTK 管理的字符串列表(本质上是您的标识符字符串的副本)。

(如 gtk_list_store_set() 所记录,一个 G_TYPE_STRING 项目被复制。我认为额外复制的开销在这里是可以接受的;它应该不会对实际性能产生太大影响,我认为,在 return 中,GTK+ 将为我们管理引用计数。)

上面是在GTK+回调函数中实现的,它得到一个额外的指针作为负载(在创建或激活GUI时设置;我建议你使用一些结构来保留你需要的引用生成匹配项)。回调连接到组合框 popup 信号,因此每当列表展开时都会调用它。

请注意,正如 B8vrede 在评论中指出的那样,不应通过其模型修改 GtkComboBoxText;这就是为什么 should/must 改用 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.
    */
}

当 UI 构建或激活时,您需要确保 GtkComboBox 有一个条目(以便用户可以在其中写入文本)和一个 GtkListStore 模型:

    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.

我有一个粗略的工作示例here (a .tar.xz tarball, CC0):它从标准输入读取行,并在组合框列表中以相反的顺序列出与用户前缀匹配的行(弹出时)。如果条目为空,组合框将包含所有输入行。我没有更改默认加速器,所以尝试使用 Alt+Down.

而不是 Tab

我也有同样的例子,但是使用GtkComboBoxText代替,here (also CC0). This does not use a GtkListStore model, but uses gtk_combo_box_text_remove_all() and gtk_combo_box_text_append_text()函数直接操作列表内容。 (两个示例中只有几行不同。)不幸的是,文档没有明确说明此接口是引用还是复制字符串。虽然复制是唯一有意义的选择,并且可以从当前的 Gtk+ 源代码中验证这一点,但缺乏明确的文档让我犹豫不决。

比较我上面链接的两个例子(如果你编译和 运行 它从 /usr/share/dict/words 中抓取大约 500 个随机单词 make),我没有看到任何速度差异。两者都使用相同的从链表中挑选前缀匹配的简单方法,这意味着这两种方法(GtkComboBox + 模型,或 GtkComboBoxText)应该差不多快。

在我自己的机器上,弹出窗口中超过 1000 个左右的匹配项都变得非常慢;只有一百或更少的比赛,感觉瞬间。对我来说,这表明 slow/naive 从链表中选择前缀匹配的方式不是罪魁祸首(因为在这两种情况下都会遍历整个列表),但 GTK+ 组合框并不是为大名单。 (减速肯定比线性减速要严重得多。)

我不会展示具体的代码,因为我从来没有用过 GTK & C,只用过 GTK & Python,但它应该没问题,因为 C 中的函数和 Python 函数可以轻松翻译。

OP的做法其实是对的,所以我会尽量填补空白。由于静态选项的数量有限,可能不会改变太多,因此使用 gtk_combo_box_text_append 添加它们确实有意义,这会将它们添加到 GtkComboBoxText 的内部模型中。

这涵盖了静态部分,对于动态部分,如果我们可以只存储这个静态模型并使用 gtk_combo_box_set_model() 将其替换为临时模型,当 _ 被发现时,这将是完美的字符串的开头。但是我们不应该像文档中说的那样这样做:

You should not call gtk_combo_box_set_model() or attempt to pack more cells into this combo box via its GtkCellLayout interface.

所以我们需要解决这个问题,一种方法是在 GtkComboBoxText 的条目中添加 GtkEntryCompletion。这将使条目尝试根据其当前模型完成当前字符串。作为额外的奖励,它还可以添加所有选项的所有字符,如下所示:

由于我们不想事先加载所有动态选项,我认为最好的方法是将 changed 侦听器连接到 GtkEntry,这样我们就可以加载动态当我们有下划线和一些字符时的选项。

由于 GtkEntryCompletion 在内部使用 GtkListStore,我们可以 重用他的回答 中提供的部分代码 Nominal Animal。主要区别在于:connect 是在 GtkEntry 上完成的,并且在 populator 中用 GtkEntryCompletion 替换了 GtkComboText。然后一切都应该没问题,我希望我能够编写像样的 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()