Gtk4. Migration Label and tooltip from gtk3

Original issue is gtk4: missbehavior of the Gtk::Label tooltip (#6674) · Issues · GNOME / gtk · GitLab
The problem here is: When application have a lot of labels there are separate threads which can change tooltips for any label. In UI for those unchanged Labels - tooltips are flickering or even disappeared
Code in gtk3

#include <thread>
#include <iomanip>

void setTlpMarkup(GtkWidget *wg, char* markUp) {
  if (wg) {
    while(true) {
      auto t = std::time(nullptr);
      auto tm = *std::localtime(&t);
      std::ostringstream oss;
      oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
      auto str = std::string(markUp) + ' ' + oss.str();

      gtk_widget_set_tooltip_markup(wg, str.c_str());
      std::this_thread::sleep_for(std::chrono::seconds(2));
    }
  }
}

static GtkWidget *lLabel, *rLabel;
std::thread lT, rT;

static void
activate (GtkApplication *app,
          gpointer        user_data)
{
  GtkWidget *window;
  GtkWidget *mainBox, *lBox, *rBox;

  mainBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
  lBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
  rBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

  gtk_box_pack_start(GTK_BOX(mainBox), lBox, true, true, 0);
  gtk_box_pack_end(GTK_BOX(mainBox), rBox, true, true, 0);

  // Labels
  lLabel = gtk_label_new("Left Label");
  rLabel = gtk_label_new("Right Label");
  gtk_box_pack_start(GTK_BOX(lBox), lLabel, true, true, 0);
  gtk_box_pack_start(GTK_BOX(rBox), rLabel, true, true, 0);

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Hello");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  gtk_container_add (GTK_CONTAINER (window), mainBox);
  gtk_window_present (GTK_WINDOW (window));

  gtk_widget_set_tooltip_markup(lLabel, (char*)"Left tooltip");

//  lT = std::thread(setTlpMarkup, lLabel, (char*)"Left tooltip");
  rT = std::thread(setTlpMarkup, rLabel, (char*)"Right tooltip");
  gtk_widget_show_all (window);
}

int main(int argc, char* argv[]) {
  GtkApplication *app;
  int status{0};

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

Code in gtk4

#include <gtk/gtk.h>
#include <thread>
#include <iomanip>

void setTlpMarkup(GtkWidget *wg, char* markUp) {
  if (wg) {
    while(true) {
      auto t = std::time(nullptr);
      auto tm = *std::localtime(&t);
      std::ostringstream oss;
      oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
      auto str = std::string(markUp) + ' ' + oss.str();

      gtk_widget_set_tooltip_markup(wg, str.c_str());
      std::this_thread::sleep_for(std::chrono::seconds(2));
    }
  }
}

static GtkWidget *lLabel, *rLabel;
std::thread lT, rT;

static void
activate (GtkApplication *app,
          gpointer        user_data)
{
  GtkWidget *window;
  GtkWidget *mainBox, *lBox, *rBox;

  mainBox = gtk_center_box_new();
  lBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
  rBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

  gtk_center_box_set_start_widget(GTK_CENTER_BOX(mainBox), lBox);
  gtk_center_box_set_end_widget(GTK_CENTER_BOX(mainBox), rBox);

  // Labels
  lLabel = gtk_label_new("Left Label");
  rLabel = gtk_label_new("Right Label");
  gtk_box_append(GTK_BOX(lBox), lLabel);
  gtk_box_append(GTK_BOX(rBox), rLabel);

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Hello");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  gtk_window_set_child (GTK_WINDOW (window), mainBox);
  gtk_window_present (GTK_WINDOW (window));

  gtk_widget_set_tooltip_markup(lLabel, (char*)"Left tooltip");

//  lT = std::thread(setTlpMarkup, lLabel, (char*)"Left tooltip");
  rT = std::thread(setTlpMarkup, rLabel, (char*)"Right tooltip");
}

int main(int argc, char* argv[]) {
  GtkApplication *app;
  int status{0};

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

As I said on GitLab:

You cannot call GTK API in a thread. If you are doing busy work in a thread, and you wish to update the UI, you have to schedule a call into the main loop, by using API like g_idle_add_once() or g_main_context_invoke().

The reason why you’re seeing odd behaviour is that you’re using GTK API from multiple threads, and you’re blocking in the middle on a thread.

GTK is thread aware, but not thread safe: only the thread that initialised GTK and is spinning the main loop can use GTK API; every other thread must schedule UI updates into the main loop.

For more information:

Thank you , let me try another approach

Hi @ebassi
I’ve rewritten incoming example for gtk4. Now I’m using Glib::Dispatcher but unfortunately no any luck. Even separate thread now is just doing emits … and tooltip is refreshed by the callback function the problem with tooltip flickering still persists

#include <gtk/gtk.h>
#include <thread>
#include <iomanip>
#include <glibmm/dispatcher.h>
#include <spdlog/spdlog.h>

static GtkWidget *lLabel, *rLabel;
std::thread lT, rT;
/// Emitting on this dispatcher triggers a update() call
Glib::Dispatcher dp;

void setTlpMarkup(GtkWidget *wg, char* markUp) {
  if (wg) {
      auto t = std::time(nullptr);
      auto tm = *std::localtime(&t);
      std::ostringstream oss;
      oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
      auto str = std::string(markUp) + ' ' + oss.str();
      gtk_widget_set_tooltip_markup(wg, str.c_str());
    }
}

static void
activate (GtkApplication *app,
          gpointer        user_data)
{
  GtkWidget *window;
  GtkWidget *mainBox, *lBox, *rBox;

  mainBox = gtk_center_box_new();
  lBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
  rBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

  gtk_center_box_set_start_widget(GTK_CENTER_BOX(mainBox), lBox);
  gtk_center_box_set_end_widget(GTK_CENTER_BOX(mainBox), rBox);

  // Labels
  lLabel = gtk_label_new("Left Label");
  rLabel = gtk_label_new("Right Label");
  gtk_box_append(GTK_BOX(lBox), lLabel);
  gtk_box_append(GTK_BOX(rBox), rLabel);

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Hello");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  gtk_window_set_child (GTK_WINDOW (window), mainBox);
  gtk_window_present (GTK_WINDOW (window));

  gtk_widget_set_tooltip_markup(lLabel, (char*)"Left tooltip");

//  lT = std::thread(setTlpMarkup, lLabel, (char*)"Left tooltip");
//  rT = std::thread(setTlpMarkup, rLabel, (char*)"Right tooltip");
  rT = std::thread([](){
    while(true) {
      std::this_thread::sleep_for(std::chrono::seconds(2));
      dp.emit();
    }
  });
}

int main(int argc, char* argv[]) {
  GtkApplication *app;
  int status{0};

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  dp.connect([] {
          try {
            setTlpMarkup(rLabel, (char*)"Right tooltip");
          } catch (const std::exception& e) {
            spdlog::error("{}: {}", "setTlpMarkup", e.what());
          }
        });
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

I have no idea what Glib::Dispatcher is, so I’ll leave somebody else fluent in the C++ bindings take over from here.

Hi,

Can you try replacing

dp.emit();

by

g_idle_add_once([] {
          try {
            setTlpMarkup(rLabel, (char*)"Right tooltip");
          } catch (const std::exception& e) {
            spdlog::error("{}: {}", "setTlpMarkup", e.what());
          }
        }, NULL);

?

I never used Glib::Dispatcher, seems to be some C++ specific API, but the examples I found by googling use a lot of mutextes… I suspect it’s not thread-safe.

Hi @gwillems and @ebassi , unfortunately no luck with provided replacing.
To me it seems like gtk recreates tooltip instance which leads to tooltip flickering … but it just my guesses …
UPD: If I use set_markup method - Labels work OK without any flickering. The problem persist only for tooltip due to I believe it belongs to Widget class … not to Label.
UPD2: I tried the same example with mutex

void setTlpMarkup(GtkWidget *wg, char* markUp) {
  if (wg) {
      auto t = std::time(nullptr);
      auto tm = *std::localtime(&t);
      std::ostringstream oss;
      oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
      auto str = std::string(markUp) + ' ' + oss.str();
      std::lock_guard<std::mutex> lock(m_Mutex);
      gtk_widget_set_tooltip_markup(wg, str.c_str());
    }
}

But again no luck

Hi @ebassi and @gwillems
Finally I’ve rewritten this code on pure C using pure GTK API using signals. And the problem still persists. So separate thread now just sends “thread_signal” signals. In the main loop I’m using subscription on this signals with the callback function * notify_cb*

See the code now

#include <gtk/gtk.h>
#include <thread>
#include <iomanip>
#include <spdlog/spdlog.h>

static GtkWidget *lLabel, *rLabel;
std::thread lT, rT;
guint mySig{g_signal_new("thread_finished",
             G_TYPE_OBJECT, G_SIGNAL_RUN_FIRST,
             0, NULL, NULL,
             g_cclosure_marshal_VOID__BOXED,
             G_TYPE_NONE, 1, GDK_TYPE_EVENT)};

void setTlpMarkup(GtkWidget *wg, char* markUp) {
  if (wg) {
      auto t = std::time(nullptr);
      auto tm = *std::localtime(&t);
      std::ostringstream oss;
      oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
      auto str = std::string(markUp) + ' ' + oss.str();
      gtk_widget_set_tooltip_markup(wg, str.c_str());
    }
}

void notify_cb() {
  try {
    setTlpMarkup(rLabel, (char*)"Right tooltip");
  } catch (const std::exception& e) {
    spdlog::error("{}: {}", "setTlpMarkup", e.what());
  }
}


static void
activate (GtkApplication *app,
          gpointer        user_data)
{
  GtkWidget *window;
  GtkWidget *mainBox, *lBox, *rBox;

  mainBox = gtk_center_box_new();
  lBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
  rBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

  gtk_center_box_set_start_widget(GTK_CENTER_BOX(mainBox), lBox);
  gtk_center_box_set_end_widget(GTK_CENTER_BOX(mainBox), rBox);

  // Labels
  lLabel = gtk_label_new("Left Label");
  rLabel = gtk_label_new("Right Label");
  gtk_box_append(GTK_BOX(lBox), lLabel);
  gtk_box_append(GTK_BOX(rBox), rLabel);

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Hello");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  gtk_window_set_child (GTK_WINDOW (window), mainBox);
  gtk_window_present (GTK_WINDOW (window));

  gtk_widget_set_tooltip_markup(lLabel, (char*)"Left tooltip");

  rT = std::thread([app](){
    while(true) {
      std::this_thread::sleep_for(std::chrono::seconds(2));
      g_signal_emit_by_name(app, "thread_finished");
    }
  });
}

int main(int argc, char* argv[]) {
  GtkApplication *app;
  int status{0};

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  g_signal_connect(G_OBJECT(app), "thread_finished", G_CALLBACK(notify_cb), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

I made some tests with gdb and the version of your program with Glib::Dispatcher.
The pure C version would probably lead to the same conclusion.

The call to gtk_widget_set_tooltip_markup() results, among many other things, in a
call to gtk_tooltip_handle_event_internal() with the following code:

	    /* Is the pointer above another widget now? */
	    if (GTK_TOOLTIP_VISIBLE (tooltip))
	      hide_tooltip |= target_widget != tooltip->tooltip_widget;

If the pointer (i.e. the mouse cursor) is above the widget whose tooltip is being
set, the tooltip is not hidden.

If the cursor is, for instance, over the left label when the right label’s tooltip
is being changed, the tooltip is hidden. In this case the code snippet above is
executed only every second time the tooltip changes. The remaining changes execute
code in gtk_tooltip_handle_event_internal() that shows the tooltip.

I haven’t tried to understand exactly how this works, but I doubt that you can
avoid the flickering tooltip by fixes outside gtk itself.

1 Like

Hi @kjellahl , @gwillems , @ebassi
Even thought the application can handle this issue itself what is I see this is pure Gdk issue. In order to do migration of the application from gtk3 to gtk4 I have to do some additional trick, hacks and revise the whole application architecture. Where in original gtk3 it was not even an issue.
In the same time If application uses Gtk::Label which derives from Gtk:Widget it looks awkward when it’s possible to change Label text using set_text/set_markup but in the same time experiences the problem with set_tooltip_text/set_tooltip_markup

Is it possible to fix the issue in GTK framework ?

I agree the behavior is weird and annoying.

I’ll have a look and try to fix this.

1 Like

Thank you @gwillems .

1 Like

Hi @Viktar ,

As pointed by devs in gitlab, your code above is not thread-safe: GObject.signal_emit_by_name is emitted synchronously, i.e. in thread context, so you may experience corruptions and crashes.

This one should be better, can you try if it solves your issues?

#include <gtk/gtk.h>
#include <thread>
#include <iomanip>
//#include <spdlog/spdlog.h>

static GtkWidget *lLabel, *rLabel;
std::thread lT, rT;

void setTlpMarkup(GtkWidget *wg, char* markUp) {
  if (wg) {
      auto t = std::time(nullptr);
      auto tm = *std::localtime(&t);
      std::ostringstream oss;
      oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
      auto str = std::string(markUp) + ' ' + oss.str();
      gtk_widget_set_tooltip_markup(wg, str.c_str());
    }
}

void notify_cb(gpointer data) {
  try {
    setTlpMarkup(rLabel, (char*)"Right tooltip");
  } catch (const std::exception& e) {
    //spdlog::error("{}: {}", "setTlpMarkup", e.what());
  }
}

static void
activate (GtkApplication *app,
          gpointer        user_data)
{
  GtkWidget *window;
  GtkWidget *mainBox, *lBox, *rBox;

  mainBox = gtk_center_box_new();
  lBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
  rBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

  gtk_center_box_set_start_widget(GTK_CENTER_BOX(mainBox), lBox);
  gtk_center_box_set_end_widget(GTK_CENTER_BOX(mainBox), rBox);

  // Labels
  lLabel = gtk_label_new("Left Label");
  rLabel = gtk_label_new("Right Label");
  gtk_box_append(GTK_BOX(lBox), lLabel);
  gtk_box_append(GTK_BOX(rBox), rLabel);

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Hello");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  gtk_window_set_child (GTK_WINDOW (window), mainBox);
  gtk_window_present (GTK_WINDOW (window));

  gtk_widget_set_tooltip_markup(lLabel, (char*)"Left tooltip");

  rT = std::thread([app](){
    while(true) {
      std::this_thread::sleep_for(std::chrono::seconds(2));
      g_idle_add_once(notify_cb, NULL);
    }
  });
}

int main(int argc, char* argv[]) {
  GtkApplication *app;
  int status{0};

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}

Hi @gwillems , unfortunately no luck

./build/testapp
fish: Job 1, './build/testapp' terminated by signal SIGABRT (Abort)

When I use gtk <=4.15.1 - at least it works, even with the topic issue. Once I use 4.15.2 I have SIGABRT

Can you please try this?

#include <gtk/gtk.h>
#include <thread>
#include <iomanip>
#include <atomic>
//#include <spdlog/spdlog.h>

static GtkWidget *lLabel, *rLabel;
std::thread lT, rT;
std::atomic<bool> stop_thread = false;

void setTlpMarkup(GtkWidget *wg, const char* markUp) {
  if (wg) {
      auto t = std::time(nullptr);
      auto tm = *std::localtime(&t);
      std::ostringstream oss;
      oss << std::put_time(&tm, "%d-%m-%Y %H-%M-%S");
      auto str = std::string(markUp) + ' ' + oss.str();
      gtk_widget_set_tooltip_markup(wg, str.c_str());
    }
}

void notify_cb(gpointer data) {
  try {
    setTlpMarkup(rLabel, "Right tooltip");
  } catch (const std::exception& e) {
    //spdlog::error("{}: {}", "setTlpMarkup", e.what());
  }
}

static void
activate (GtkApplication *app,
          gpointer        user_data)
{
  GtkWidget *window;
  GtkWidget *mainBox, *lBox, *rBox;

  mainBox = gtk_center_box_new();
  lBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
  rBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);

  gtk_center_box_set_start_widget(GTK_CENTER_BOX(mainBox), lBox);
  gtk_center_box_set_end_widget(GTK_CENTER_BOX(mainBox), rBox);

  // Labels
  lLabel = gtk_label_new("Left Label");
  rLabel = gtk_label_new("Right Label");
  gtk_box_append(GTK_BOX(lBox), lLabel);
  gtk_box_append(GTK_BOX(rBox), rLabel);

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Hello");
  gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);

  gtk_window_set_child (GTK_WINDOW (window), mainBox);
  gtk_window_present (GTK_WINDOW (window));

  gtk_widget_set_tooltip_markup(lLabel, "Left tooltip");

  rT = std::thread([app](){
    while(true) {
      std::this_thread::sleep_for(std::chrono::seconds(2));
      if (stop_thread)
        break;
      else
        g_idle_add_once(notify_cb, NULL);
    }
  });
}

int main(int argc, char* argv[]) {
  GtkApplication *app;
  int status{0};

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  stop_thread = true;
  rT.join();
  g_object_unref (app);

  return status;
}

Hi @gwillems , sorry for long delay .

Unfortunately the same error. (((

░▒▓   …/ /git/gtk_label    main    v0.0.1  meson setup --wipe build
The Meson build system
Version: 1.4.0
Source dir: /home/viktar/Documents/git/gtk_label
Build dir: /home/viktar/Documents/git/gtk_label/build
Build type: native build
Project name: testapp
Project version: 0.0.1
C compiler for the host machine: cc (gcc 13.2.1 "cc (Gentoo 13.2.1_p20240210 p14) 13.2.1 20240210")
C linker for the host machine: cc ld.bfd 2.41
C++ compiler for the host machine: c++ (gcc 13.2.1 "c++ (Gentoo 13.2.1_p20240210 p14) 13.2.1 20240210")
C++ linker for the host machine: c++ ld.bfd 2.41
Host machine cpu family: x86_64
Host machine cpu: x86_64
Found pkg-config: YES (/usr/bin/pkg-config) 2.2.0
Run-time dependency gtk4 found: YES 4.15.2
Run-time dependency spdlog found: YES 1.12.0
Build targets in project: 1

Found ninja-1.11.1 at /usr/bin/ninja
░▒▓   …/ /git/gtk_label    main    v0.0.1  make
meson build
Directory already configured.

Just run your build command (e.g. ninja) and Meson will regenerate as necessary.
Run "meson setup --reconfigure to force Meson to regenerate.

If build failures persist, run "meson setup --wipe" to rebuild from scratch
using the same options as passed when configuring the build.
WARNING: Running the setup command as `meson [options]` instead of `meson setup [options]` is ambiguous and deprecated.
ninja -C build
ninja: Entering directory `build'
[2/2] Linking target testapp
░▒▓   …/ /git/gtk_label    main    v0.0.1  ./build/testapp
fish: Job 1, './build/testapp' terminated by signal SIGABRT (Abort)

In which environment do you run the testapp? Wayland , X11 ? which distro?

Hi @gwillems ,

> uname -a
Linux tuxedo 6.6.30-gentoo-x86_64 #1 SMP PREEMPT_DYNAMIC Tue May  7 14:38:32 +03 2024 x86_64 12th Gen Intel(R) Core(TM) i7-1260P GenuineIntel GNU/Linux
> gcc --version
gcc (Gentoo 13.2.1_p20240210 p14) 13.2.1 20240210
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE
> sway --version
sway version 1.9

Actually Gentoo, Kernel 6.6.30.
Sway + Wayland.

Will try to compile and run in virtual machine using arch, opensuse, fedora on Monday.