Сигнал GtkTextBuffer mark_set запускает обратный вызов 3-4 раза для клавиш со стрелками или щелчка мыши

Я пытаюсь использовать сигнал 'mark_set' для обновления значений row:col в моем GtkTextBuffer. Для тестирования у меня есть простая настройка с textview внутри прокручиваемого окна внутри окна, например:

window
  scrolled window
    textview

Я использую структуру для хранения различных значений для моего приложения, например:

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

Я пытаюсь обновить текущие значения line и col в экземпляре структуры, используемой моим приложением для отслеживания позиций строки и столбца в буфере. В функции 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->line и app->col устанавливаются правильно (только один раз) после каждого нажатия клавиши, когда ввод передается в буфер. например ввод 'abc' в текстовое представление приводит к:

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

Однако, когда я использую arrow keys для перемещения курсора ввода или использую mouse для изменения его положения, срабатывает тройной или четырехкратный обратный вызов. например нажатие стрелки влево для резервного копирования одной позиции приводит к следующему:

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' — это единственный сигнал, который может охватывать ввод->обработку положения курсора, независимо от того, поступает ли ввод для позиционирования с помощью нажатия клавиши или щелчка мыши. Цель состоит в том, чтобы использовать сигнал 'mark_set' для обработки всех обновлений row:col, но я должен найти способ предотвратить запуск обратного вызова более одного раза для каждого события нажатия клавиши или щелчка мыши.

При использовании 'key_press_event' с виджетом textview вы можете передать функцию обратного вызова gboolean и передать GdkEventKey и вручную обработать event->keyval для обработки изменения положения курсора. с помощью клавиатуры (включая клавиши со стрелками), а с помощью return сообщите обработчикам ввода по умолчанию, что никаких дальнейших действий не требуется для любого данного нажатия клавиши, но это не так и не может работать с щелчками мыши. Так что, если я могу сделать все это с помощью сигнала 'mark_set', это будет мой выбор.

Есть ли способ сделать то же самое с событием 'mark_set', чтобы гарантировать, что обратный вызов on_mark_set() выполняется только один раз, независимо от нажатия клавиши или щелчка мыши? Я разместил сообщение в gtk-app-devel-list, но не получил ответа. ТАК. вероятно, более активен в темах gtk, чем сам gtk-list. Любая помощь с этой загадкой будет принята с благодарностью.

MCVE для тестирования

Ниже приведен 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 с минимальными предупреждениями об устаревании.


Решение

Поскольку решение этой проблемы недоступно в Интернете с помощью обычных средств поиска, я опубликую решение, к которому пришел после того, как ответ ниже указал мне правильное направление. Предложение в ответе после изменения прототипа обратного вызова заключалось в том, чтобы сравнивать свойства метки каждый раз, когда генерируется сигнал mark_set, и отбрасывать все вызовы mark_set, которые не соответствуют требуемому свойству. Хотя этот подход не работает из-за отсутствия используемого свойства unique для идентификации (любое или все 3 варианта имени: null, insert или selection_bound могут быть сгенерированы для любого заданного перемещения курсора вставки), это дала возможность различать новые и текущие сигналы 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);

Зная текущие значения line:col, вы можете затем сравнить их с новыми значениями line:col, которые будут созданы на основе значения GtkTextIter *iter, переданного в качестве параметра функции обратного вызова on_mark_set(). Это обеспечило простое сравнение текущего значения с новым, позволяя вам реагировать только на сигнал 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) {}
}

(примечание: операторы отладки g_print оставлены выше, чтобы предоставить контекст для вывода ниже). Кроме того, обратите внимание, что сравнение между mark и gtk_text_buffer_get_insert (buffer) невозможно, поскольку gtk_text_buffer_get_insert (buffer) возвращает только совпадающие значения, когда стрелка -key или щелчок мышью. (сравнение не выполняется для обычного ввода текста).

Теперь при повторении той же последовательности событий, отмеченной в исходном вопросе (например, введите '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 месте в произвольном порядке

Чтобы оставить эти вопросы в качестве хорошей ссылки, если не хорошего решения, я включу результаты дальнейшей отладки в эту тему. Существует полный дефицит информации по этой конкретной теме, которую можно найти где-либо еще.

Дальнейшая отладка показывает, почему в этой области существуют трудности и почему простое сравнение между mark и gtk_text_buffer_get_insert (buffer) не может надежно использоваться само по себе, чтобы определить, следует ли реагировать на сигнал "mark_set" или нет. Почему?

Каждый раз, когда генерируется сигнал "mark_set", может быть несколько marks в любом заданном iter местоположении. В случае обычного ввода (например, 'a', 'b' и т. д.) mark, передаваемое обратному вызову on_mark_set(), не обязательно является меткой "insert", а, по-видимому, просто последней из меток, присутствующих в этом местоположении. (В каждом случае под анонимной отметкой) Список отметок в любой заданной позиции iter можно найти по GSList отметкам, возвращенным gtk_text_iter_get_marks (iter). (примечание: метки в возвращаемом списке расположены в неопределенном порядке, что, вероятно, и является основой для всей этой проблемы. См.: 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', затем Стрелка влево, затем щелкните мышью в конце) вызывается обратный вызов on_mark_set() для каждого введенного 'abc':

$ ./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 метки, а mark, переданное обратным вызовом, равно mark[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'. По сути, обратный вызов срабатывает один раз для каждой метки в позиции 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 обратному вызову при обычном вводе текста, то t способ предотвратить многократное срабатывание обратного вызова в любом случае. Лучшее, что можно сделать, это эффективно определить, должен ли обратный вызов реагировать на сигнал "mark_set". В этом случае проверка наличия метки "insert" и наличия каких-либо изменений в местоположении line:col почти настолько хороша, насколько это возможно.

Другой альтернативой является разделение ответственности за обновление местоположения line:col между обратным вызовом on_mark_set() и обратным вызовом обработчика ввода и обновлением обработчика ввода line:col для обычного ввода текста, а on_mark_set() отвечает только тогда, когда "insert" mark передается в качестве параметра. Однако я не уверен, что это лучшее решение.


person David C. Rankin    schedule 27.12.2015    source источник


Ответы (1)


Сигнал mark_set выдается при изменении любой метки, а не только положения курсора. Движения мыши, вероятно, также влияют на метку выбора, поэтому кажется, что вы получаете несколько сигналов. Правильный обработчик сигнала mark-set должен проверять аргумент mark и игнорировать метки, которые ему не нужны, в данном случае все метки, кроме метки вставки, возвращаемой gtk_text_buffer_get_insert(buffer).

Последние пункты приводят к проблеме в исходном коде. Согласно документации, он должен быть объявлен как

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

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

Как только прототип обработчика будет исправлен, а некурсорные метки проигнорированы, проблема многократного вызова должна исчезнуть.

person user4815162342    schedule 27.12.2015
comment
Вы знаете, я, наверное, провел день на этой самой странице и посмотрел на эту самую функцию, но без какой-либо документации на странице, касающейся использования функции, было непонятно, как она могла различать ключ нажатия и щелчки мышью. Спасибо, что указали мне правильное направление. - person David C. Rankin; 27.12.2015
comment
У меня есть решение, но оно не включает сравнение свойства mark-name. Вы не можете. Почему? Имена меток, связанные с перемещениями курсора вставки: (null), insert и selection_bound. Для любого заданного mark_set не существует уникальной метки, которую можно было бы идентифицировать (например, вы можете получить все 3 имени для движений клавиш со стрелками и selection_bound плюс 2-null для движений мыши. Единственное, что возможно, это сохранить текущие значения line:col и сравните новые значения line:col на основе переданного итератора location и отвечайте только на новые значения. Я добавлю редактирование к вопросу. - person David C. Rankin; 27.12.2015
comment
@DavidCRankin, вас не должны волновать имена меток, вы должны сравнить объект метки, который вы получаете в качестве третьего аргумента, с обратным вызовом с меткой вставки, полученной путем спекания gtk_text_buffer_get_insert (). Как написано, ваш вызов реагирует на все изменения меток, что неверно, поскольку буфер может содержать любой буфер меток, не связанных с текущей позицией курсора. - person user4815162342; 27.12.2015
comment
Ах, это то, чего я не знал, было возможно, учитывая, что сравнение значений указателя повышено до возможности повторного использования указателя, вызывающего ложное сравнение на равенство. Пойду переделывать и снова проверять значения. Я проверил значения gtk_text_mark_get_name (mark)) (именно здесь я использовал анонимные имена). Я попробую сравнить mark. Но я все еще должен что-то упустить, потому что мне все равно придется сравнивать mark object в обратном вызове. Похоже, это спасет только один вызов функции. Я еще немного покопаюсь, но сравнение текущее и новое работает хорошо. - person David C. Rankin; 27.12.2015
comment
Хорошо, теперь я в замешательстве. Если я добавляю к началу on_mark_set() префикс if (mark != gtk_text_buffer_get_insert (buffer)) return;, функция реагирует только на arrow-key и mouse-click, так как обычный текстовый ввод не генерирует метку insert (только null). Вы предлагаете что-то другое? - person David C. Rankin; 27.12.2015
comment
@DavidCRankin Прямое сравнение - это то, что я имел в виду, игнорируя отметки, которые ему не нужны, - игнорируя все, кроме отметки, возвращаемой gtk_text_buffer_get_insert. Я изменю ответ, чтобы быть более явным по этому поводу. Повторное использование указателя не является проблемой, поскольку метка вставки существует до тех пор, пока буфер. (И даже если бы это было не так, вызов gtk_text_buffer_get_insert при каждом вызове обратного вызова все равно гарантировал бы, что вы сравниваете метку с текущей меткой вставки.) - person user4815162342; 27.12.2015
comment
Я понял. Мне просто нужно было встать, немного пройтись и дать ему понять. Вы говорите, что on_mark_set() несет ответственность за проверку того, что он отвечает только на буфер textview при установке значений line:col. В противном случае он будет реагировать на любое mark_set, которое может быть сгенерировано любым буфером в коде. Понятно. Спасибо! - person David C. Rankin; 28.12.2015
comment
То, что я выбрал в этом случае, чтобы выполнить требуемую проверку (поскольку app->buffer ранее сохранено), представляет собой простое сравнение между buffer и app->buffer, которое гарантирует, что mark_set был сгенерирован желаемым буфером. (if (buffer != app->buffer) return;). Это позволяет избежать проблемы mark != gtk_text_buffer_get_insert (buffer) для обычного ввода текста. - person David C. Rankin; 28.12.2015
comment
@DavidC.Rankin Не какой-либо буфер в коде, а любая метка в буфере. Поскольку обработчик сигнала привязан к конкретному буферу, его не следует вызывать для других буферов. Если сравнение buffer с app->buffer изменяет поведение программы, это может указывать на ошибку в другом месте кода. - person user4815162342; 28.12.2015
comment
В конце концов я разберусь с этим. Я вижу различные соображения, несколько меток в одном и том же буфере и т. д., но вся эта проверка нарушается тем фактом, что при обычном вводе (например, 'a', 'b', ...) mark != gtk_text_buffer_get_insert (buffer). Я сбросил все метки и указатели, чтобы посмотреть, что сработает. При этом между mark и gtk_text_buffer_get_insert (buffer) нет никакой связи. Что мне не хватает в том, как я пытаюсь провести сравнение для обычного ввода текста, который оставляет mark != gtk_text_buffer_get_insert (buffer)? Спасибо за помощь! - person David C. Rankin; 28.12.2015
comment
Понятно! Каждый раз, когда генерируется сигнал "mark_set", в каждом месте может быть от 2 до 5+ меток. Вы можете исследовать с gtk_text_iter_get_marks (iter). gtk_text_buffer_get_insert (buffer) возвращает только один из многих. mark, переданный в качестве параметра, может быть или не быть меткой insert. Чтобы сделать это правильно, вы должны получить GSList отметок в переданном iter местоположении, а затем обновить line:col только в том случае, если insert есть среди отметок в этом iter местоположении. Шиш, как это сделать... - person David C. Rankin; 29.12.2015