GtkTextBuffer "mark_set" 信号为箭头键或鼠标单击触发回调 3-4 次

GtkTextBuffer "mark_set" signal fires callback 3-4 times for Arrow-Keys or Mouse-Click

我正在尝试使用 'mark_set' 信号更新 GtkTextBuffer 中的 row:col 值。为了测试我有一个简单的设置 textview inside a scrolled window inside a window ,例如:

window
  scrolled window
    textview

我使用一个结构来保存我的应用程序的各种值,例如:

typedef struct {
    GtkWidget *window;
    GtkWidget *view;
    GtkTextBuffer *buffer;
    GtkTextMark *cursor;
    gint line;
    gint col;
    gint winwidth;
    gint winheight;
} context;

我正在尝试更新我的应用程序使用的结构实例中的当前 linecol 值,以跟踪 column 缓冲区内的位置。在 create_window 函数中,我初始化 context *app; 的值(在 main() 中定义)并将 'mark_set' 信号连接到 on_mark_set() 回调,将结构的实例作为数据传递给回调。例如:

g_signal_connect (app->buffer, "mark_set",
                  G_CALLBACK (on_mark_set), app);

on_mark_set() 回调(使用 g_print 作为示例和调试目的)是:

void on_mark_set (GtkTextBuffer *buffer, context *app)
{
    GtkTextIter iter;

    app->cursor = gtk_text_buffer_get_insert (buffer);

    gtk_text_buffer_get_iter_at_mark (buffer, &iter, app->cursor);

    app->line = gtk_text_iter_get_line (&iter);
    app->col = gtk_text_iter_get_line_offset (&iter);

    g_print (" line: %3d col: %d\n", app->line + 1, app->col + 1);
}

app->lineapp->col 的值在每次按键后正确设置(仅一次),其中 输入 被提供给缓冲区。例如在 textview 中输入 'abc' 结果:

$ ./bin/text_mcve
 line:   1 col: 2
 line:   1 col: 3
 line:   1 col: 4

但是,当我使用 arrow keys 移动输入光标或使用 mouse 重新定位时,回调 tripple-触发或 四重-发射。例如按 左箭头 后退一个位置会导致以下结果:

line:   1 col: 3
line:   1 col: 3
line:   1 col: 3

通过单击鼠标重新定位到最后会导致 四重-触发回调:

line:   1 col: 4
line:   1 col: 4
line:   1 col: 4
line:   1 col: 4

如何将 on_mark_set() 回调的执行限制为单个调用,而不管是否正在输入数据或者是否正在使用箭头键或鼠标移动光标?鉴于 'mark_set' 是唯一可以涵盖输入-> 光标位置处理的信号,无论定位输入是来自 keypress 还是 mouse-click。目标是利用 'mark_set' 信号来处理所有 row:col 更新,但我必须找到一种方法来防止每次按键或鼠标单击事件多次触发回调。

'key_press_event'textview 小部件一起使用时,您可以通过创建 gboolean 回调并通过 GdkEventKey 并手动处理 event->keyval 以使用键盘(包括箭头键)处理光标重新定位,并通过 return 告诉默认输入处理程序不需要进一步操作被用于任何给定的按键,但这不会,也不能与鼠标点击一起使用。所以如果我可以通过 'mark_set' 信号完成这一切,那将是我的选择。

有没有什么方法可以对 'mark_set' 事件做同样的事情,以确保 on_mark_set() 回调只执行一次,而不管按键或鼠标点击?我已经post编辑到gtk-app-devel-list,但没有收到回复。 S.O。在 gtk 主题上可能比 gtk-list 本身更活跃。非常感谢任何解决这个难题的帮助。

用于测试的 MCVE

下面提供了用于测试目的的

A MCVE。用 gcc -o progname progname.c $(pkg-config --cflags --libs gtk+-2.0)

编译
#include <gtk/gtk.h>

typedef struct {
    GtkWidget *window;
    GtkWidget *view;
    GtkTextBuffer *buffer;
    GtkTextMark *cursor;
    gint line;
    gint col;
    gint winwidth;
    gint winheight;
} context;

GtkWidget *create_window (context *app);
void on_window_destroy (GtkWidget *widget, context *app);
void on_mark_set (GtkTextBuffer *buffer, context *app);

int main (int argc, char **argv)
{
    context *app = NULL;
    app = g_slice_new (context);

    gtk_init (&argc, &argv);

    if ((app->window = create_window (app))) {
        gtk_widget_show (app->window);
        gtk_main();
    }
    else
        g_print ("\nerror: create_window returned NULL\n\n");

    g_slice_free (context, app);

    return 0;
}

GtkWidget *create_window (context *app)
{
    GtkWidget *scrolled_window;
    GtkWidget *vbox;
    PangoFontDescription *font_desc;

    app->winwidth = 500;    /* window width x height */
    app->winheight = 350;

    app->line = 0;          /* initialize beginning pos line/col  */
    app->col = 0;

    app->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title (GTK_WINDOW (app->window), "mark_set MCVE");
    gtk_window_set_default_size (GTK_WINDOW (app->window),
                                 app->winwidth, app->winheight);
    gtk_container_set_border_width (GTK_CONTAINER (app->window), 5);

    vbox = gtk_vbox_new (FALSE, 0);
    gtk_container_add (GTK_CONTAINER (app->window), vbox);

    app->buffer = gtk_text_buffer_new (NULL);

    app->view = gtk_text_view_new_with_buffer (app->buffer);
    gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (app->view), GTK_WRAP_WORD);
    gtk_text_view_set_left_margin (GTK_TEXT_VIEW (app->view), 10);
    font_desc = pango_font_description_from_string ("DejaVu Sans Mono 8");
    gtk_widget_modify_font (app->view, font_desc);
    pango_font_description_free (font_desc);

    scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window),
                                    GTK_POLICY_AUTOMATIC,
                                    GTK_POLICY_AUTOMATIC);

    gtk_container_add (GTK_CONTAINER (scrolled_window),  app->view);
    gtk_box_pack_start (GTK_BOX (vbox), scrolled_window, TRUE, TRUE, 5);

    g_signal_connect (app->window, "destroy",
                      G_CALLBACK (on_window_destroy), app);
    g_signal_connect (app->buffer, "mark_set",
                      G_CALLBACK (on_mark_set), app);

    gtk_widget_show (app->view);
    gtk_widget_show (scrolled_window);
    gtk_widget_show (vbox);

    return app->window;
}

void on_window_destroy (GtkWidget *widget, context *app)
{
    GtkTextIter start, end;
    gtk_text_buffer_get_bounds (app->buffer, &start, &end);
    g_print ("Exiting... buffer contained:\n%s\n",
             gtk_text_buffer_get_text (app->buffer, &start, &end, FALSE));
    gtk_main_quit ();
    if (widget) {}
}

void on_mark_set (GtkTextBuffer *buffer, context *app)
{
    GtkTextIter iter;

    app->cursor = gtk_text_buffer_get_insert (buffer);

    gtk_text_buffer_get_iter_at_mark (buffer, &iter, app->cursor);

    app->line = gtk_text_iter_get_line (&iter);
    app->col = gtk_text_iter_get_line_offset (&iter);

    g_print (" line: %3d col: %d\n", app->line + 1, app->col + 1);
}

再次感谢您的任何建议或帮助。 注意:此代码是 gtk+2 应用程序的一部分,但也可以使用 gtk+3 进行编译,并带有最少的弃用警告。


解决方案

由于无法通过正常搜索方式在网络上轻松获得此问题的解决方案,因此我将 post 在以下答案指向正确方向后得出的解决方案。在更改回调原型后,答案中的建议是每次生成 mark_set 信号时比较标记属性,并丢弃所有 mark_set 不满足要求的调用 属性 .由于没有可用的 unique 属性 来识别(任何或所有 3 种可能的名称:nullinsertselection_bound 可以为任何给定的插入光标移动生成),这确实提供了区分 newcurrent[=224 的方法的火花=] mark_set 信号。

关键是在创建缓冲区时初始化和存储缓冲区中的当前 line:col 位置。这可以在 create_window() 函数中完成:

GtkTextIter iterfirst;
...
app->cursor = gtk_text_buffer_get_insert (app->buffer);
gtk_text_buffer_get_iter_at_mark (app->buffer, &iterfirst, app->cursor);
app->line = gtk_text_iter_get_line (&iterfirst);
app->col = gtk_text_iter_get_line_offset (&iterfirst);

了解 current line:col 值,然后您可以与 new line:col 值进行比较基于作为参数传递给 on_mark_set() 回调的 GtkTextIter *iter 值生成。这提供了 currentnew 值的简单比较,允许您仅响应导致变化的 mark_set 信号line:col 值:

void on_mark_set (GtkTextBuffer *buffer, GtkTextIter *iter,
                GtkTextMark *mark, context *app)
{
    gint line, col;

    line = gtk_text_iter_get_line (iter);
    col = gtk_text_iter_get_line_offset (iter);

    if (line == app->line && col == app->col) return;

    app->line = line;
    app->col = col;

    g_print (" line: %3d col: %d\n", app->line + 1, app->col + 1);

    if (mark) {}
}

注意: debug g_print 语句留在上面为下面的输出提供上下文)进一步注意,markgtk_text_buffer_get_insert (buffer) 无法创建,因为 gtk_text_buffer_get_insert (buffer) 仅在 arrow-keymouse-click 输入时返回匹配值. (正常文本输入比较失败)。

现在重复原始问题中记录的相同事件序列(例如输入 'abc',然后使用 向左箭头 备份 1,然后左键单击在重新定位光标的末尾)显示 on_mark_set 现在仅正确响应更新的 line:col 值:

输出

$ ./bin/text_mcve
 line:   1 col: 2
 line:   1 col: 3
 line:   1 col: 4
 line:   1 col: 3
 line:   1 col: 4
Exiting... buffer contained:
abc

每个 iter 位置有多个标记,无特定顺序

为了留下这个问题作为一个很好的参考,如果不是一个好的解决方案,我会把进一步调试的结果包含在这个主题中。在其他任何地方都找不到关于这个特定主题的信息。

进一步的调试说明了为什么在这方面存在困难,以及为什么无法可靠地单独使用 markgtk_text_buffer_get_insert (buffer) 之间的简单比较来确定是否响应 "mark_set" 信号或不。为什么?

每次"mark_set"信号产生,在任何给定的iter位置可以有多个marks。在正常输入的情况下(例如 'a''b' 等...)传递给 on_mark_set() 回调的 mark 不一定是 "insert" 标记,但显然只是该 iter location 处出现的最后一个标记。 (在每种情况下都在 匿名标记 下方)任何给定 iter 位置的标记列表可以通过 [=86= 返回的标记的 GSList 找到]. (注意: 返回列表中的标记 没有特定的 顺序——这可能是整个问题开始的基础。见: gtk_text_iter_get_marks() ) 例如,您可以使用以下调试代码检查标记:

void on_mark_set (GtkTextBuffer *buffer, GtkTextIter *iter,
                GtkTextMark *mark, context *app)
{
    gint line, col;

#ifdef DEBUG
    g_print ("  mark: %p  - gtbgi (buffer): %p  mark->name: %s\n", mark, 
            gtk_text_buffer_get_insert (buffer), 
            gtk_text_mark_get_name (mark));

    GSList *marks = gtk_text_iter_get_marks (iter);
    GSList *p = marks;
    gint i = 0;
    while (p) {
        const gchar *name = gtk_text_mark_get_name (GTK_TEXT_MARK(p->data));
        g_print ("    mark[%d] : %p : %s\n", i++, GTK_TEXT_MARK(p->data), name);
        p = p->next;
    }
    g_slist_free (marks);
#endif

    line = gtk_text_iter_get_line (iter);
    col = gtk_text_iter_get_line_offset (iter);

    if (line == app->line && col == app->col) return;

    app->line = line;
    app->col = col;

#ifdef DEBUG
    g_print (" line: %3d col: %d\n\n", app->line + 1, app->col + 1);
#endif

    if (mark) {}
}

编译然后使用相同的(输入'abc',然后左箭头键,然后鼠标单击 end) 为每个 'abc' 输入触发 on_mark_set() 回调:

$ ./bin/text_mcve_dbg
  mark: 0x2458880  - gtbgi (buffer): 0x237d600  mark->name: (null)
    mark[0] : 0x237d600 : insert
    mark[1] : 0x237d620 : selection_bound
    mark[2] : 0x237d7a0 : gtk_drag_target
    mark[3] : 0x2458880 : (null)
 line:   1 col: 2

  mark: 0x24792c0  - gtbgi (buffer): 0x237d600  mark->name: (null)
    mark[0] : 0x237d600 : insert
    mark[1] : 0x237d620 : selection_bound
    mark[2] : 0x237d7a0 : gtk_drag_target
    mark[3] : 0x24792c0 : (null)
 line:   1 col: 3

  mark: 0x24797a0  - gtbgi (buffer): 0x237d600  mark->name: (null)
    mark[0] : 0x237d600 : insert
    mark[1] : 0x237d620 : selection_bound
    mark[2] : 0x237d7a0 : gtk_drag_target
    mark[3] : 0x24797a0 : (null)
 line:   1 col: 4

检查,每个 iter 位置都有 4 个标记,回调传递的 markmark[3],尽管所有 4 个标记实际上都出现在 iter 位置。

当按下 向左箭头 键时,回调触发 3 次,每次出现每个标记:

  mark: 0x237d600  - gtbgi (buffer): 0x237d600  mark->name: insert
    mark[0] : 0x237d600 : insert
    mark[1] : 0x237d620 : selection_bound
 line:   1 col: 3

  mark: 0x237d620  - gtbgi (buffer): 0x237d600  mark->name: selection_bound
    mark[0] : 0x237d600 : insert
    mark[1] : 0x237d620 : selection_bound
  mark: 0x2479700  - gtbgi (buffer): 0x237d600  mark->name: (null)
    mark[0] : 0x237d600 : insert
    mark[1] : 0x237d620 : selection_bound
    mark[2] : 0x2479700 : (null)

对于回调的第一次触发,"insert" 标记被传递,第二次触发 "selection_bound" 标记被传递,最后,匿名 'null' 标记被传递。本质上,当按下 Left-Arrow 键时,iter 位置的每个标记都会触发一次回调。

当鼠标点击定位到缓冲区末尾的插入点时,回调触发4次如下:

  mark: 0x237d600  - gtbgi (buffer): 0x237d600  mark->name: insert
    mark[0] : 0x237d7a0 : gtk_drag_target
    mark[1] : 0x237d600 : insert
    mark[2] : 0x237d620 : selection_bound
 line:   1 col: 4

  mark: 0x237d620  - gtbgi (buffer): 0x237d600  mark->name: selection_bound
    mark[0] : 0x237d7a0 : gtk_drag_target
    mark[1] : 0x237d600 : insert
    mark[2] : 0x237d620 : selection_bound
  mark: 0x24792a0  - gtbgi (buffer): 0x237d600  mark->name: (null)
    mark[0] : 0x237d7a0 : gtk_drag_target
    mark[1] : 0x237d600 : insert
    mark[2] : 0x237d620 : selection_bound
    mark[3] : 0x24792a0 : (null)
  mark: 0x2479200  - gtbgi (buffer): 0x237d600  mark->name: (null)
    mark[0] : 0x237d7a0 : gtk_drag_target
    mark[1] : 0x237d600 : insert
    mark[2] : 0x237d620 : selection_bound
    mark[3] : 0x2479200 : (null)
    mark[4] : 0x24792a0 : (null)

点击鼠标时有一个'gtk_drag_target'标记,除此之外,除了附加标记和附加匿名标记外,它的行为类似于左箭头 按键。

所以最重要的是,由于 "insert" 标记作为该位置的标记之一包含在每次射击中,但 不是 作为参数传递 mark 到正常文本输入的回调,那么在任何情况下都没有办法防止多次触发回调。最好的办法是有效地确定回调是否需要响应 "mark_set" 信号。在这种情况下,检查 "insert" 标记是否存在以及 line:col 位置是否有任何变化就差不多了。

另一种选择是在 on_mark_set() 回调和输入处理程序回调之间分担更新 line:col 位置的责任,并让您的输入处理程序更新 line:col 以获取普通文本输入和 on_mark_set() 仅在 "insert" mark 作为参数传递时响应。但是,我不确定这是更好的解决方案。

mark_set 信号是为了更改 任何 标记而发出的,而不仅仅是光标位置。鼠标移动也可能影响选择标记,这就是为什么它看起来好像收到多个信号的原因。 mark-set 信号的正确处理程序应该检查 mark 参数并忽略它不关心的标记,在这种情况下,除了 gtk_text_buffer_get_insert(buffer) 返回的插入标记之外的所有标记。

最后一点导致最初出现的代码出现问题。根据documentation,应该声明为

void user_function (GtkTextBuffer *textbuffer, GtkTextIter *location,
                    GtkTextMark *mark, gpointer user_data)

...而您的处理程序只接受缓冲区和用户数据。写入为 location 迭代器保留的内存可能会导致代码的不相关部分损坏并使问题复杂化。

修复处理程序原型并忽略非光标标记后,多重调用问题应该消失。