From c9e24d6bf4d3d5baae9bb4dade97cfcb8a5ecafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vivek=20Das=C2=A0Mohapatra?= Date: Sun, 15 Jul 2018 18:59:59 +0100 Subject: [PATCH 1/3] GTK3 menu bars force frame resizing (Bug#22000) Menu bars force the frame they are in to resize when the menu bar width exceeds the frame width, both at the point the menu bar grows past the frame width and whenever the gtk idle resize callback is triggered. The effect is that the user's frame width is effectively ignored, and emacs will semi-predictably resize itself to accommodate the menu bar. This effect can be suppressed by wrapping the menu bar in a scrollable window. This behaviour is controlled by the `menu-bar-scrollbar' parameter which may have a value of 'automatic, 'always, 'forced-resize or anything else (equivalent to nil). Limitations in earlier versions of GTK mean that there are some version-dependent differences in behaviour. GTK 3.16 and later: The menu bar is always in a scrolled window . In the default mode the menu bar is truncated when it tries to grow wider than the frame. CSS is used to strip away the excess space this introduces. In 'always or 'automatic mode, the CSS is relaxed slightly to work around a GTK focus glitch, but otherwise the widget setup is identical. The menubar will have a scrollbar either always, or when it tries to grow too wide. In 'forced-resize mode the frame-size-jitter behaviour described in bug #22000 is preserved. Before GTK 3.16: When in 'always or 'automatic mode, the menu bar will be in a scrolled window. The extra space cannot be properly ameliorated with CSS styling as this does not seem to work well. In the default mode, the scrolled window is not present - the menu bar is dynamically re-parented between the scrolled window (which is created on demand) and the emacs pane (vbox widget) when the menu bar scrolling mode is changed. In these versions of GTK the menu-bar truncation behaviour is not easily achievable, so the default mode is identical to 'forced-resize mode. --- src/frame.c | 7 ++ src/gtkutil.c | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- src/gtkutil.h | 3 + src/xfns.c | 12 +++- src/xterm.h | 4 ++ 5 files changed, 220 insertions(+), 9 deletions(-) diff --git a/src/frame.c b/src/frame.c index 0a6ca26f5d..e4e430de8f 100644 --- a/src/frame.c +++ b/src/frame.c @@ -3550,6 +3550,7 @@ static const struct frame_parm_table frame_parms[] = {"right-divider-width", SYMBOL_INDEX (Qright_divider_width)}, {"bottom-divider-width", SYMBOL_INDEX (Qbottom_divider_width)}, {"menu-bar-lines", SYMBOL_INDEX (Qmenu_bar_lines)}, + {"menu-bar-scrollbar", SYMBOL_INDEX (Qmenu_bar_scrollbar)}, {"mouse-color", SYMBOL_INDEX (Qmouse_color)}, {"name", SYMBOL_INDEX (Qname)}, {"scroll-bar-width", SYMBOL_INDEX (Qscroll_bar_width)}, @@ -5660,6 +5661,11 @@ syms_of_frame (void) DEFSYM (Qx_resource_name, "x-resource-name"); DEFSYM (Qx_frame_parameter, "x-frame-parameter"); + /* Values for menu-bar-scrollbar frame parameter (GTK only) */ + DEFSYM (Qautomatic, "automatic"); + DEFSYM (Qalways, "always"); + DEFSYM (Qforced_resize, "forced-resize"); + DEFSYM (Qworkarea, "workarea"); DEFSYM (Qmm_size, "mm-size"); DEFSYM (Qframes, "frames"); @@ -5675,6 +5681,7 @@ syms_of_frame (void) DEFSYM (Qtitle_bar_size, "title-bar-size"); DEFSYM (Qmenu_bar_external, "menu-bar-external"); DEFSYM (Qmenu_bar_size, "menu-bar-size"); + DEFSYM (Qmenu_bar_scrollbar, "menu-bar-scrollbar"); DEFSYM (Qtool_bar_external, "tool-bar-external"); DEFSYM (Qtool_bar_size, "tool-bar-size"); /* The following are used for frame_size_history. */ diff --git a/src/gtkutil.c b/src/gtkutil.c index 83b306a730..c4637e0cba 100644 --- a/src/gtkutil.c +++ b/src/gtkutil.c @@ -1136,6 +1136,10 @@ delete_cb (GtkWidget *widget, return TRUE; } +#define MENUBAR_STYLESHEET \ + ".mbtrunc * { border-width: 1px; margin-top: -2px; margin-bottom: -2px; }\n" \ + ".mbscroll * { border-width: 1px; margin-top: -1px; margin-bottom: 0px; }\n" + /* Create and set up the GTK widgets for frame F. Return true if creation succeeded. */ @@ -1147,6 +1151,9 @@ xg_create_frame_widgets (struct frame *f) GtkWidget *wfixed; #ifndef HAVE_GTK3 GtkRcStyle *style; +#else + GtkCssProvider *css; + GdkScreen *screen; #endif char *title = 0; @@ -1213,6 +1220,17 @@ xg_create_frame_widgets (struct frame *f) store_frame_param (f, Qundecorated, Qt); } + /* Add a CSS provider for the frame which will be used for dynamic styling + when we change widget behaviour (eg menubar scrollbars). */ + css = gtk_css_provider_new (); + screen = gtk_widget_get_screen (wtop); + /* This should probably be moved ino the filesystem. */ + gtk_css_provider_load_from_data (css, MENUBAR_STYLESHEET, -1, NULL); + gtk_style_context_add_provider_for_screen (screen, + GTK_STYLE_PROVIDER (css), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_object_unref (css); + FRAME_GTK_OUTER_WIDGET (f) = wtop; FRAME_GTK_WIDGET (f) = wfixed; f->output_data.x->vbox_widget = wvbox; @@ -3434,7 +3452,11 @@ menubar_map_cb (GtkWidget *w, gpointer user_data) { GtkRequisition req; struct frame *f = user_data; - gtk_widget_get_preferred_size (w, NULL, &req); + struct x_output *x = f->output_data.x; + + /* Use the menubar viewport for size if there is one: */ + gtk_widget_get_preferred_size (x->menubar_visible_widget ?: w, NULL, &req); + if (FRAME_MENUBAR_HEIGHT (f) != req.height) { FRAME_MENUBAR_HEIGHT (f) = req.height; @@ -3442,6 +3464,150 @@ menubar_map_cb (GtkWidget *w, gpointer user_data) } } +/* Remove a gtk widget from any parent it may belong to. + Ensures that this does not change target's ref count. */ +static void +orphan_widget (GtkWidget *w) +{ + GtkWidget *parent = gtk_widget_get_parent (w); + + if (parent && GTK_IS_CONTAINER (parent)) + { + g_object_ref (w); + gtk_container_remove (GTK_CONTAINER (parent), w); + +#if !GTK_CHECK_VERSION(3, 8, 0) + /* gtk 3.8 and earlier: viewport must be managed manually + but we don't need to save it, unlike the scrolled window + or the menubar. */ + if (GTK_IS_VIEWPORT (parent)) + { + GtkWidget *grandparent = gtk_widget_get_parent (parent); + + if (parent && GTK_IS_CONTAINER (grandparent)) + gtk_container_remove (GTK_CONTAINER (grandparent), parent); + } +#endif + return; + } +} + +/* As per orphan_widget but we also want to get rid of it and clean up. */ +static void +discard_widget (GtkWidget *w) +{ + orphan_widget (w); + if (g_object_is_floating (w)) + g_object_ref_sink (w); + g_object_unref (w); +} + +/* Sets up the menubar style for scrolling/non-scrolling modes. + Reparents the menubar directly into the vbox for non-scrolling + mode and adds a scrolledwindow+viewport for scrolling modes. */ +static void +menubar_scrollbox (struct frame *f, int scroll) +{ + struct x_output *x = f->output_data.x; + + if (scroll) + { + if (x->menubar_visible_widget == x->menubar_viewport) + return; + + x->menubar_visible_widget = x->menubar_viewport; + orphan_widget (x->menubar_widget); + +#if GTK_CHECK_VERSION (3, 8, 0) + gtk_container_add (GTK_CONTAINER (x->menubar_viewport), x->menubar_widget); +#else + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (x->menubar_viewport), x->menubar_widget); +#endif + } + else + { + if (x->menubar_visible_widget == x->menubar_widget) + return; + + x->menubar_visible_widget = x->menubar_widget; + orphan_widget (x->menubar_widget); + orphan_widget (x->menubar_viewport); + } + + gtk_box_pack_start (GTK_BOX (x->vbox_widget), + GTK_WIDGET (x->menubar_visible_widget), FALSE, FALSE, 0); + gtk_box_reorder_child (GTK_BOX (x->vbox_widget), + GTK_WIDGET (x->menubar_visible_widget), 0); + gtk_widget_show_all (x->menubar_visible_widget); +} + +#if GTK_CHECK_VERSION (3, 16, 0) +#define MENUBAR_SCROLLBAR_DEFAULT_POLICY GTK_POLICY_EXTERNAL +#else +#define MENUBAR_SCROLLBAR_DEFAULT_POLICY GTK_POLICY_NEVER +#endif + +/* Apply the scrollbar mode and related style settings. + This also inserts or removes the intervening viewport as necessary + and maps any widgets that need mapping. Note that after gtk 3.16 + we don't need (or want) to remove the scrolledwindow or viewport, + it suffices to change their style. */ +void +xg_update_frame_menubar_scrollbar_mode (struct frame *f, Lisp_Object mode) +{ + GtkStyleContext *style; + struct x_output *x = f->output_data.x; + GtkScrolledWindow *sw; + GtkPolicyType scroll_policy = MENUBAR_SCROLLBAR_DEFAULT_POLICY; + + if (!x->menubar_viewport) + return; + + if (EQ (mode, Qautomatic)) + scroll_policy = GTK_POLICY_AUTOMATIC; + else if (EQ (mode, Qalways)) + scroll_policy = GTK_POLICY_ALWAYS; + else if (EQ (mode, Qforced_resize)) + scroll_policy = GTK_POLICY_NEVER; + + sw = GTK_SCROLLED_WINDOW (x->menubar_viewport); + style = gtk_widget_get_style_context (x->menubar_viewport); + +#if GTK_CHECK_VERSION(3, 16, 0) + /* Always want the scrollable container for capable-enough GTK versions */ + menubar_scrollbox (f, 1); + + switch (scroll_policy) + { + case GTK_POLICY_AUTOMATIC: + case GTK_POLICY_ALWAYS: + gtk_style_context_add_class (style, "mbscroll"); + gtk_style_context_remove_class (style, "mbtrunc"); + break; + default: + gtk_style_context_remove_class (style, "mbscroll"); + gtk_style_context_add_class (style, "mbtrunc"); + } +#else + /* In older GTK versions we need to swap out the scrollable container + instead since we can't get truncating behaviour and CSS styling is + not well supported. */ + switch (scroll_policy) + { + case GTK_POLICY_AUTOMATIC: + case GTK_POLICY_ALWAYS: + menubar_scrollbox (f, 1); + gtk_style_context_remove_class (style, "mbtrunc"); + break; + default: + menubar_scrollbox (f, 0); + gtk_style_context_add_class (style, "mbtrunc"); + } +#endif + + gtk_scrolled_window_set_policy (sw, scroll_policy, GTK_POLICY_AUTOMATIC); +} + /* Recompute all the widgets of frame F, when the menu bar has been changed. */ @@ -3450,6 +3616,7 @@ xg_update_frame_menubar (struct frame *f) { struct x_output *x = f->output_data.x; GtkRequisition req; + Lisp_Object menuscroll; if (!x->menubar_widget || gtk_widget_get_mapped (x->menubar_widget)) return; @@ -3459,13 +3626,29 @@ xg_update_frame_menubar (struct frame *f) block_input (); - gtk_box_pack_start (GTK_BOX (x->vbox_widget), x->menubar_widget, - FALSE, FALSE, 0); - gtk_box_reorder_child (GTK_BOX (x->vbox_widget), x->menubar_widget, 0); + menuscroll = get_frame_param (f, Qmenu_bar_scrollbar); + + /* Put the menu bar inside a scrolled window so that adding items + to the menu bar (such as when entering dired mode or activating + a minor more) does not trigger a frame resize:*/ + x->menubar_viewport = gtk_scrolled_window_new(NULL, NULL); + + /* Leave the keyboard focus where it is when clicking the scrollwindow: */ +#if GTK_CHECK_VERSION (3, 20, 0) + gtk_widget_set_focus_on_click (x->menubar_viewport, FALSE); +#endif + +#if GTK_CHECK_VERSION (3, 16, 0) + /* If we don't set this then the scrollable keeps focus when the user + interacts with the scrollbar, at least until the menubar is clicked. + Overlay scrolling is more compact but until the focus problem is fixed + it's not livable with. */ + gtk_scrolled_window_set_overlay_scrolling (GTK_SCROLLED_WINDOW (x->menubar_viewport), FALSE); +#endif g_signal_connect (x->menubar_widget, "map", G_CALLBACK (menubar_map_cb), f); - gtk_widget_show_all (x->menubar_widget); - gtk_widget_get_preferred_size (x->menubar_widget, NULL, &req); + xg_update_frame_menubar_scrollbar_mode (f, menuscroll); + gtk_widget_get_preferred_size (x->menubar_visible_widget, NULL, &req); if (FRAME_MENUBAR_HEIGHT (f) != req.height) { @@ -3487,10 +3670,14 @@ free_frame_menubar (struct frame *f) { block_input (); - gtk_container_remove (GTK_CONTAINER (x->vbox_widget), x->menubar_widget); + discard_widget (x->menubar_widget); + discard_widget (x->menubar_viewport); + /* The menubar and its children shall be deleted when removed from the container. */ - x->menubar_widget = 0; + x->menubar_visible_widget = NULL; + x->menubar_viewport = NULL; + x->menubar_widget = NULL; FRAME_MENUBAR_HEIGHT (f) = 0; adjust_frame_size (f, -1, -1, 2, 0, Qmenu_bar_lines); unblock_input (); diff --git a/src/gtkutil.h b/src/gtkutil.h index 7dcd549f5c..96220f27c8 100644 --- a/src/gtkutil.h +++ b/src/gtkutil.h @@ -103,6 +103,9 @@ extern void xg_modify_menubar_widgets (GtkWidget *menubar, GCallback deactivate_cb, GCallback highlight_cb); +extern void xg_update_frame_menubar_scrollbar_mode (struct frame *f, + Lisp_Object mode); + extern void xg_update_frame_menubar (struct frame *f); extern bool xg_event_is_for_menubar (struct frame *, const XEvent *); diff --git a/src/xfns.c b/src/xfns.c index 3da853ede8..9503ae1e1d 100644 --- a/src/xfns.c +++ b/src/xfns.c @@ -1601,6 +1601,13 @@ x_set_menu_bar_lines (struct frame *f, Lisp_Object value, Lisp_Object oldval) adjust_frame_glyphs (f); } +static void +x_set_menu_bar_scrollbar (struct frame *f, Lisp_Object value, Lisp_Object oldval) +{ +#ifdef USE_GTK /* menubar resize/scrolling only happens under GTK. */ + xg_update_frame_menubar_scrollbar_mode (f, value); +#endif +} /* Set the number of lines used for the tool bar of frame F to VALUE. VALUE not an integer, or < 0 means set the lines to zero. OLDVAL @@ -3888,7 +3895,9 @@ This function is an internal primitive--use `make-frame' instead. */) NILP (Vtool_bar_mode) ? make_number (0) : make_number (1), NULL, NULL, RES_TYPE_NUMBER); - + /* How scrolling is handled for oversized (too wide) menu bars. */ + x_default_parameter (f, parms, Qmenu_bar_scrollbar, Qnil, + NULL, NULL, RES_TYPE_SYMBOL); x_default_parameter (f, parms, Qbuffer_predicate, Qnil, "bufferPredicate", "BufferPredicate", RES_TYPE_SYMBOL); @@ -7536,6 +7545,7 @@ frame_parm_handler x_frame_parm_handlers[] = x_set_right_divider_width, x_set_bottom_divider_width, x_set_menu_bar_lines, + x_set_menu_bar_scrollbar, x_set_mouse_color, x_explicitly_set_name, x_set_scroll_bar_width, diff --git a/src/xterm.h b/src/xterm.h index f73dd0e25a..ac5f7f08da 100644 --- a/src/xterm.h +++ b/src/xterm.h @@ -581,7 +581,11 @@ struct x_output /* The widget used for laying out widgets horizontally. */ GtkWidget *hbox_widget; /* The menubar in this frame. */ + GtkWidget *menubar_viewport; GtkWidget *menubar_widget; + /* If the viewport is in usem this will be the viewport, otherwise it + will be the menubar_widget. Used to get height calculations right. */ + GtkWidget *menubar_visible_widget; /* The tool bar in this frame */ GtkWidget *toolbar_widget; /* True if tool bar is packed into the hbox widget (i.e. vertical). */ -- 2.11.0