eclipse.platform.ui icon indicating copy to clipboard operation
eclipse.platform.ui copied to clipboard

SWT org.eclipse.swt.widgets.Menu: a very tall menu exceeding the screen when scrolled has the effect to blank the neighboring menus

Open backwindj opened this issue 1 year ago • 11 comments

Hello,

Issue description: In a GUI application made with "Eclipse IDE for RCP and RAP Developers" on Linux Fedora 40, I have a menu attached to a tree. For one of the TreeItems, the menu has a very large number of MenuItems and consequently exceeds the screen height. For the next TreeItem, the menu has only four MenuItems. If I open and close the menus of these two TreeItems, the rendering of both is perfectly fine.

If I open the large menu then scroll inside it with the mouse to see off-screen MenuItems then close the large menu and then open the small menu next to it, the small menu is rendered as 4 four blank lines plus a top handle as if it was a menu exceeding the screen. If I scroll down long enough inside the small menu, the four expected MenuItems finally will appear, the top handle is not shown any more and scrolling in the small menu has no effect any more.

I verified I can reproduce this issue against Eclipse Version: 2024-09 (4.33.0) Build id: 20240905-0614

I did not expect the small menu to be blank and to have to scroll inside it.

I understand reporting an issue to this OSS project does not mandate anyone to fix it. Other contributors may consider the issue, or not, at their own convenience. The most efficient way to get it fixed is that I fix it myself and contribute it back as a good quality patch to the project.

Best regards, François

backwindj avatar Oct 01 '24 19:10 backwindj

Screenshot would help. I believe it's a known GTK issue we reported some years ago to GTK, @trancexpress ?

iloveeclipse avatar Oct 01 '24 20:10 iloveeclipse

Copie d'écran_20241001_220639

backwindj avatar Oct 01 '24 20:10 backwindj

I will rework my menu to fit the screen.

backwindj avatar Oct 01 '24 20:10 backwindj

Screenshot would help. I believe it's a known GTK issue we reported some years ago to GTK, @trancexpress ?

We had/have: https://bugs.eclipse.org/bugs/show_bug.cgi?id=564910

Without a video its hard to tell if its the same issue.

trancexpress avatar Oct 02 '24 03:10 trancexpress

https://github.com/user-attachments/assets/b526cbc3-a9a4-4322-b340-6f96bbada976

The issue is shown twice in this recording.

Best regards, François

backwindj avatar Oct 02 '24 20:10 backwindj

We've not ran into this issue yet. If I find time I'll try to come up with a GTK3 repoducer. Maybe its again some weird combo behavior (menus share code with combo drop downs) that is supposed to make to make a combo entry easy to click (due to mouse pointer proximity)...

trancexpress avatar Oct 02 '24 20:10 trancexpress

I tried a few reproducers, I couldn't reproduce the problem with GTK3.

Next step here should be an SWT snippet that reproduces the problem, maybe with that we can reduce to a GTK3 snippet. I doubt its an SWT bug, I assume its some GTK3 bug.

trancexpress avatar Oct 03 '24 09:10 trancexpress

Hello,

I have verified that the issue was not due to X11 but the behaviour is exactly the same with Wayland.

Best regards, François

backwindj avatar Oct 03 '24 18:10 backwindj

Hello,

Please find below a code snippet reproducing the issue.

package org.eclipse.swt.snippets;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MenuListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

public class Snippet
{
   public static void main (final String [] args)
   {
      final var display = new Display ();

      final var shell = new Shell (display);

      shell.setText ("Snippet");
      shell.setLayout (new FillLayout ());

      final var tree = new Tree (shell, SWT.FULL_SELECTION);

      final var menu = new Menu (tree);

      tree.setMenu (menu);

      menu.addMenuListener (MenuListener.menuShownAdapter (e -> build_menu (tree, menu)));

      final var nb_menu_entry = new int [] {4, 80, 8};

      for (int i = 0; i < 2; i++)
      {
         final var item_I = new TreeItem (tree, SWT.NONE);

         item_I.setText ("Level 1 - " + i);

         for (int j = 0; j < nb_menu_entry.length; j++)
         {
            final var item_J = new TreeItem (item_I, SWT.NONE);

            item_J.setText ("Level 2 - " + j);

            item_J.setData (Integer.valueOf (nb_menu_entry [j]));
         }
      }

      shell.setSize (800, 600);
      shell.open ();

      while (! shell.isDisposed ())
      {
         if (! display.readAndDispatch ())
         {
            display.sleep ();
         }
      }

      display.dispose ();
   }

   static private void build_menu (final Tree tree, final Menu menu)
   {
      final TreeItem [] selection = tree.getSelection ();

      if (selection.length != 0)
      {
         final TreeItem tree_item = selection [0];

         final var nb_entry = (Integer) tree_item.getData ();

         clear_menu (menu);

         if (nb_entry != null)
         {
            for (int i = 0; i < nb_entry.intValue (); i++)
            {
               final var item = new MenuItem (menu, SWT.PUSH);

               item.setText ("Menu entry " + i);

               item.addSelectionListener (SelectionListener.widgetSelectedAdapter (e -> System.out.println ("Coucou")));
            }
         }
      }
   }

   static private void clear_menu (final Menu menu)
   {
      for (final MenuItem menu_item : menu.getItems ())
      {
         final Menu child = menu_item.getMenu ();

         if (child != null)
         {
            clear_menu (child);
         }

         menu_item.dispose ();
      }
   }

}

Best regards, François

backwindj avatar Oct 06 '24 18:10 backwindj

The behavior is reproducible with this GTK+ snippet:

// gcc -g menu_scroll_bug2.c  `pkg-config --cflags --libs gtk+-3.0`  -o MenuScrollBug && ./MenuScrollBug

#include <stdio.h>
#include <gtk/gtk.h>


static int menu_item_count = 60;

static GtkWidget *text;
static GtkWidget *menu;
static GtkWidget *tmp_item;

static gboolean b = TRUE;

void view_popup_menu_onDoSomething (GtkWidget *menuitem, gpointer userdata)
{
  g_print ("menu item pressed!\n");
}

void view_popup_menu (GtkWidget *widget, GdkEventButton *event, gpointer userdata)
{
  GList *children = gtk_container_get_children(GTK_CONTAINER(menu));
  GList *l;
  for (l = children; l; l = l->next)
  {
    gtk_container_remove (GTK_CONTAINER (menu), GTK_WIDGET (l->data));
  }

  if (b)
  {
    GtkWidget *menuitem;
    char buf[128];
    for (int i = 0; i < menu_item_count; ++i)
    {
      sprintf (buf, "menu 1 item %d", i);
      menuitem = gtk_menu_item_new_with_label(buf);
      g_signal_connect(menuitem, "activate", (GCallback) view_popup_menu_onDoSomething, text);
      gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
      gtk_widget_show(menuitem);
    }
  }
  else
  {
    GtkWidget *menuitem;
    char buf[128];
    for (int i = 0; i < 5; ++i)
    {
      sprintf (buf, "menu 2 item %d", i);
      menuitem = gtk_menu_item_new_with_label(buf);
      g_signal_connect(menuitem, "activate", (GCallback) view_popup_menu_onDoSomething, text);
      gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
      gtk_widget_show(menuitem);
    }
  }
  gtk_widget_show_all(menu);
  gtk_menu_popup_at_pointer(GTK_MENU(menu), NULL);
  b = !b;
}

gboolean view_onPopupMenu (GtkWidget *widget, gpointer userdata)
{
  view_popup_menu(widget, NULL, userdata);
  return TRUE;
}


gboolean view_onButtonPressed (GtkWidget *widget, GdkEventButton *event, gpointer userdata)
{
  if (event->type == GDK_BUTTON_PRESS && event->button == 3)
  {
    view_popup_menu(widget, event, userdata);
    return TRUE;
  }
  return FALSE;
}

gboolean view_onKeyPressed (GtkWidget *widget, GdkEventButton *event, gpointer userdata)
{
  if (event->type == GDK_KEY_PRESS)
  {
    if (tmp_item != NULL)
    {
      gtk_widget_destroy(tmp_item);
      tmp_item = NULL;
    }
    else
    {
      tmp_item = gtk_menu_item_new_with_label("temp item");
      g_signal_connect(tmp_item, "activate", (GCallback) view_popup_menu_onDoSomething, text);
      gtk_menu_shell_append(GTK_MENU_SHELL(menu), tmp_item);
      gtk_widget_show(tmp_item);
    }
    return TRUE;
  }
  return FALSE;
}

int main(int argc, char *argv[]) {
  GtkWidget *window, *scrolled_window;
  GtkTextBuffer *buffer;
  GtkTextIter iter;

  gtk_init(&argc, &argv);

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 400, 500);
  gtk_window_set_title(GTK_WINDOW(window), "Menu scroll bug");
  g_signal_connect(window, "delete_event", gtk_main_quit, NULL);

  text = gtk_text_view_new();
  menu = gtk_menu_new();

  scrolled_window = gtk_scrolled_window_new (NULL, NULL);

  gtk_container_add(GTK_CONTAINER(window), scrolled_window);

  buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text));

  gtk_text_buffer_get_iter_at_offset(buffer, &iter, 0);

  char buf[128];
  for (int i = 0; i < 120; ++i)
  {
    sprintf (buf, "row %d - abcdefghijklmnopqrstuvwxyz0123456789\n", i);
    gtk_text_buffer_insert(buffer, &iter, buf, -1);
  }

  gtk_container_add(GTK_CONTAINER(scrolled_window), text);

  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

  g_signal_connect(text, "button-press-event", G_CALLBACK(view_onButtonPressed), NULL);
  g_signal_connect(text, "popup-menu", G_CALLBACK(view_onPopupMenu), NULL);
  g_signal_connect (text, "key-press-event", G_CALLBACK(view_onKeyPressed), NULL);

  gtk_widget_show_all(window);

  gtk_main();
}

I assume when reusing the same menu, the scroll state is preserved despite removing items. I don't find any option in SWT to scroll the menu programmatically.

@backwindj are you able to use several SWT menu objects? Likely this will be the easiest option for a workaround. I assume the GTK3 behavior here is expected, to fix this behavior some sort of a workaround will be needed.

trancexpress avatar Oct 07 '24 07:10 trancexpress

Hello,

I think I have found a general workaround with only one menu. The menu has to be disposed and recreated when closed:

package org.eclipse.swt.snippets;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MenuListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

public class Snippet
{
   public static void main (String [] args)
   {
      final var display = new Display ();

      final var shell = new Shell (display);

      shell.setText ("Snippet");
      shell.setLayout (new FillLayout ());

      final var tree = new Tree (shell, SWT.FULL_SELECTION);

      final var menu = new Menu (tree);

      menu.addMenuListener (MenuListener.menuShownAdapter (e -> build_menu (tree)));

      tree.setMenu (menu);

      final var nb_menu_entry = new int [] {4, 80, 8};

      for (int i = 0; i < 2; i++)
      {
         final var item_I = new TreeItem (tree, SWT.NONE);

         item_I.setText ("Level 1 - " + i);

         for (int j = 0; j < nb_menu_entry.length; j++)
         {
            final var item_J = new TreeItem (item_I, SWT.NONE);

            item_J.setText ("Level 2 - " + j);

            item_J.setData (Integer.valueOf (nb_menu_entry [j]));
         }
      }

      shell.setSize (800, 600);
      shell.open ();

      while (! shell.isDisposed ())
      {
         if (! display.readAndDispatch ())
         {
            display.sleep ();
         }
      }

      display.dispose ();
   }

   static private void build_menu (final Tree tree)
   {
      final TreeItem [] selection = tree.getSelection ();

      if (selection.length != 0)
      {
         final TreeItem tree_item = selection [0];

         final var nb_entry = (Integer) tree_item.getData ();

         if (nb_entry != null)
         {
            final Menu menu = tree.getMenu ();

            clear_menu (menu);

            menu.addMenuListener (MenuListener.menuHiddenAdapter (e -> dispose_menu (tree)));

            for (int i = 0; i < nb_entry.intValue (); i++)
            {
               final var item = new MenuItem (menu, SWT.PUSH);

               item.setText ("Menu entry " + i);

               item.addSelectionListener (SelectionListener.widgetSelectedAdapter (e -> System.out.println ("Coucou")));
            }
         }
      }
   }

   static private void clear_menu (final Menu menu)
   {
      for (final MenuItem menu_item : menu.getItems ())
      {
         final Menu child = menu_item.getMenu ();

         if (child != null)
         {
            clear_menu (child);
         }

         menu_item.dispose ();
      }
   }

   static private void dispose_menu (final Tree tree)
   {
      tree.getMenu ().dispose ();

      final var menu = new Menu (tree);

      menu.addMenuListener (MenuListener.menuShownAdapter (e -> build_menu (tree)));

      tree.setMenu (menu);
   }

}

Best regards, François

backwindj avatar Oct 07 '24 20:10 backwindj

Hello,

I was wrong. Sadly, it does not work. The selection listener of the selected menu item is never called because the menu and all of its menu items are disposed before.

Best regards, François

backwindj avatar Oct 08 '24 20:10 backwindj

May be, it could work if the menu is not disposed immediately but only replaced by a new menu and later disposed when the replacing menu becomes itself replaced by a new replacing menu.

backwindj avatar Oct 08 '24 20:10 backwindj

Yes, it does work with a fifo to differ the menu disposal. Here is the snippet.

package org.eclipse.swt.snippets;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MenuListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;

public class Snippet
{
   public static void main (String [] args)
   {
      final var display = new Display ();

      final var shell = new Shell (display);

      shell.setText ("Snippet");
      shell.setLayout (new FillLayout ());

      final var fifo = new ArrayList <Menu> ();

      final var tree = new Tree (shell, SWT.FULL_SELECTION);

      final var menu = new Menu (tree);

      menu.addMenuListener (MenuListener.menuShownAdapter (e -> build_menu (tree, fifo)));

      tree.setMenu (menu);

      final var nb_menu_entry = new int [] {4, 80, 8};

      for (int i = 0; i < 2; i++)
      {
         final var item_I = new TreeItem (tree, SWT.NONE);

         item_I.setText ("Level 1 - " + i);

         for (int j = 0; j < nb_menu_entry.length; j++)
         {
            final var item_J = new TreeItem (item_I, SWT.NONE);

            item_J.setText ("Level 2 - " + j);

            item_J.setData (Integer.valueOf (nb_menu_entry [j]));
         }
      }

      shell.setSize (800, 600);
      shell.open ();

      while (! shell.isDisposed ())
      {
         if (! display.readAndDispatch ())
         {
            display.sleep ();
         }
      }

      display.dispose ();
   }

   static private void build_menu (final Tree tree, final List <Menu> fifo)
   {
      final TreeItem [] selection = tree.getSelection ();

      if (selection.length != 0)
      {
         final TreeItem tree_item = selection [0];

         final var nb_entry = (Integer) tree_item.getData ();

         if (nb_entry != null)
         {
            final Menu menu = tree.getMenu ();

            menu.addMenuListener (MenuListener.menuHiddenAdapter (e -> dispose_menu (tree, fifo)));

            for (int i = 0; i < nb_entry.intValue (); i++)
            {
               final var item = new MenuItem (menu, SWT.PUSH);

               final String text = "Menu entry " + i;

               item.setText (text);

               item.addSelectionListener (SelectionListener.widgetSelectedAdapter (e -> System.out.println (text)));
            }
         }
      }
   }

   static private void clear_menu (final Menu menu)
   {
      for (final MenuItem menu_item : menu.getItems ())
      {
         final Menu child = menu_item.getMenu ();

         if (child != null)
         {
            clear_menu (child);
         }

         menu_item.dispose ();
      }
   }

   static private void dispose_menu (final Tree tree, final List <Menu> fifo)
   {
      final Menu current_menu = tree.getMenu ();

      fifo.add (current_menu);

      final var menu = new Menu (tree);

      menu.addMenuListener (MenuListener.menuShownAdapter (e -> build_menu (tree, fifo)));

      tree.setMenu (menu);

      if (fifo.size () >= 2)
      {
         final Menu old_menu = fifo.remove (0);

         clear_menu (old_menu);

         old_menu.dispose ();
      }
   }

}

Best regards, François

backwindj avatar Oct 09 '24 19:10 backwindj