gtkmenubutton.c 34.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/* GTK - The GIMP Toolkit
 *
 * Copyright (C) 2003 Ricardo Fernandez Pascual
 * Copyright (C) 2004 Paolo Borelli
 * Copyright (C) 2012 Bastien Nocera
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:gtkmenubutton
23
 * @short_description: A widget that shows a popup when clicked on
Matthias Clasen's avatar
Matthias Clasen committed
24
 * @title: GtkMenuButton
25
 *
26 27 28
 * The #GtkMenuButton widget is used to display a popup when clicked on.
 * This popup can be provided either as a #GtkMenu, a #GtkPopover or an
 * abstract #GMenuModel.
29
 *
30 31
 * The #GtkMenuButton widget can hold any valid child widget. That is, it
 * can hold almost any other standard #GtkWidget. The most commonly used
32 33 34 35
 * child is #GtkImage. If no widget is explicitely added to the #GtkMenuButton,
 * a #GtkImage is automatically created, using an arrow image oriented
 * according to #GtkMenuButton:direction or the generic "view-context-menu"
 * icon if the direction is not set.
36
 *
37 38 39 40 41 42 43 44 45 46 47
 * The positioning of the popup is determined by the #GtkMenuButton:direction
 * property of the menu button.
 *
 * For menus, the #GtkWidget:halign and #GtkWidget:valign properties of the
 * menu are also taken into account. For example, when the direction is
 * %GTK_ARROW_DOWN and the horizontal alignment is %GTK_ALIGN_START, the
 * menu will be positioned below the button, with the starting edge
 * (depending on the text direction) of the menu aligned with the starting
 * edge of the button. If there is not enough space below the button, the
 * menu is popped up above the button instead. If the alignment would move
 * part of the menu offscreen, it is “pushed in”.
48
 *
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
 * ## Direction = Down
 *
 * - halign = start
 *
 *     ![](down-start.png)
 *
 * - halign = center
 *
 *     ![](down-center.png)
 *
 * - halign = end
 *
 *     ![](down-end.png)
 *
 * ## Direction = Up
 *
 * - halign = start
 *
 *     ![](up-start.png)
 *
 * - halign = center
 *
 *     ![](up-center.png)
 *
 * - halign = end
 *
 *     ![](up-end.png)
 *
 * ## Direction = Left
 *
 * - valign = start
 *
 *     ![](left-start.png)
 *
 * - valign = center
 *
 *     ![](left-center.png)
 *
 * - valign = end
 *
 *     ![](left-end.png)
 *
 * ## Direction = Right
 *
 * - valign = start
 *
 *     ![](right-start.png)
 *
 * - valign = center
 *
 *     ![](right-center.png)
 *
 * - valign = end
 *
 *     ![](right-end.png)
 *
105 106 107
 * # CSS nodes
 *
 * GtkMenuButton has a single CSS node with name button. To differentiate
108
 * it from a plain #GtkButton, it gets the .popup style class.
109 110 111 112 113 114
 */

#include "config.h"

#include "gtkmenubutton.h"
#include "gtkmenubuttonprivate.h"
115
#include "gtkbuttonprivate.h"
116 117 118
#include "gtktypebuiltins.h"
#include "gtkwindow.h"
#include "gtkmain.h"
119
#include "gtkaccessible.h"
120
#include "gtkpopover.h"
Matthias Clasen's avatar
Matthias Clasen committed
121
#include "a11y/gtkmenubuttonaccessible.h"
122 123 124 125 126 127

#include "gtkprivate.h"
#include "gtkintl.h"

struct _GtkMenuButtonPrivate
{
128 129
  GtkWidget *menu;    /* The menu and the popover are mutually exclusive */
  GtkWidget *popover; /* Only one at a time can be set */
130 131 132 133 134 135
  GMenuModel *model;

  GtkMenuButtonShowMenuCallback func;
  gpointer user_data;

  GtkWidget *align_widget;
136
  GtkWidget *arrow_widget;
137
  GtkArrowType arrow_type;
138
  gboolean use_popover;
139
  guint press_handled : 1;
140
  guint in_click : 1;
141 142 143 144 145
};

enum
{
  PROP_0,
146
  PROP_POPUP,
147
  PROP_MENU_MODEL,
148
  PROP_ALIGN_WIDGET,
149 150
  PROP_DIRECTION,
  PROP_USE_POPOVER,
151 152
  PROP_POPOVER,
  LAST_PROP
153 154
};

155 156
static GParamSpec *menu_button_props[LAST_PROP];

157
G_DEFINE_TYPE_WITH_PRIVATE (GtkMenuButton, gtk_menu_button, GTK_TYPE_TOGGLE_BUTTON)
158

159
static void gtk_menu_button_dispose (GObject *object);
160 161 162

static void
gtk_menu_button_set_property (GObject      *object,
Matthias Clasen's avatar
Matthias Clasen committed
163 164 165
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
166 167 168 169 170
{
  GtkMenuButton *self = GTK_MENU_BUTTON (object);

  switch (property_id)
    {
171 172
      case PROP_POPUP:
        gtk_menu_button_set_popup (self, g_value_get_object (value));
173
        break;
174
      case PROP_MENU_MODEL:
175 176 177 178 179 180 181 182
        gtk_menu_button_set_menu_model (self, g_value_get_object (value));
        break;
      case PROP_ALIGN_WIDGET:
        gtk_menu_button_set_align_widget (self, g_value_get_object (value));
        break;
      case PROP_DIRECTION:
        gtk_menu_button_set_direction (self, g_value_get_enum (value));
        break;
183 184 185 186 187 188
      case PROP_USE_POPOVER:
        gtk_menu_button_set_use_popover (self, g_value_get_boolean (value));
        break;
      case PROP_POPOVER:
        gtk_menu_button_set_popover (self, g_value_get_object (value));
        break;
189 190 191 192 193 194 195
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

static void
gtk_menu_button_get_property (GObject    *object,
Matthias Clasen's avatar
Matthias Clasen committed
196 197 198
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
199 200 201 202 203
{
  GtkMenuButtonPrivate *priv = GTK_MENU_BUTTON (object)->priv;

  switch (property_id)
    {
204
      case PROP_POPUP:
205
        g_value_set_object (value, priv->menu);
206
        break;
207
      case PROP_MENU_MODEL:
208 209 210 211 212 213 214 215
        g_value_set_object (value, priv->model);
        break;
      case PROP_ALIGN_WIDGET:
        g_value_set_object (value, priv->align_widget);
        break;
      case PROP_DIRECTION:
        g_value_set_enum (value, priv->arrow_type);
        break;
216 217 218 219 220 221
      case PROP_USE_POPOVER:
        g_value_set_boolean (value, priv->use_popover);
        break;
      case PROP_POPOVER:
        g_value_set_object (value, priv->popover);
        break;
222 223 224 225 226 227 228 229 230 231 232 233
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

static void
gtk_menu_button_state_flags_changed (GtkWidget    *widget,
                                     GtkStateFlags previous_state_flags)
{
  GtkMenuButton *button = GTK_MENU_BUTTON (widget);
  GtkMenuButtonPrivate *priv = button->priv;

234 235 236 237 238 239 240
  if (!gtk_widget_is_sensitive (widget))
    {
      if (priv->menu)
        gtk_menu_shell_deactivate (GTK_MENU_SHELL (priv->menu));
      else if (priv->popover)
        gtk_widget_hide (priv->popover);
    }
241 242 243
}

static void
244 245 246 247 248
menu_position_up_down_func (GtkMenu       *menu,
                            gint          *x,
                            gint          *y,
                            gboolean      *push_in,
                            GtkMenuButton *menu_button)
249 250 251
{
  GtkMenuButtonPrivate *priv = menu_button->priv;
  GtkWidget *widget = GTK_WIDGET (menu_button);
252
  GtkWidget *toplevel;
253 254 255 256 257
  GtkTextDirection direction;
  GdkRectangle monitor;
  gint monitor_num;
  GdkScreen *screen;
  GdkWindow *window;
258
  GtkAllocation menu_allocation, allocation, arrow_allocation;
259
  GtkAlign align;
260

261 262 263 264 265 266
  /* In the common case the menu button is showing a dropdown menu, set the
   * corresponding type hint on the toplevel, so the WM can omit the top side
   * of the shadows.
   */
  if (priv->arrow_type == GTK_ARROW_DOWN)
    {
267
      toplevel = gtk_widget_get_toplevel (priv->menu);
268 269 270
      gtk_window_set_type_hint (GTK_WINDOW (toplevel), GDK_WINDOW_TYPE_HINT_DROPDOWN_MENU);
    }

271
  align = gtk_widget_get_halign (priv->menu);
272 273 274 275 276 277 278 279 280 281 282
  direction = gtk_widget_get_direction (widget);
  window = gtk_widget_get_window (priv->align_widget ? priv->align_widget : widget);

  screen = gtk_widget_get_screen (GTK_WIDGET (menu));
  monitor_num = gdk_screen_get_monitor_at_window (screen, window);
  if (monitor_num < 0)
    monitor_num = 0;
  gdk_screen_get_monitor_workarea (screen, monitor_num, &monitor);

  gtk_widget_get_allocation (priv->align_widget ? priv->align_widget : widget, &allocation);
  gtk_widget_get_allocation (widget, &arrow_allocation);
283
  gtk_widget_get_allocation (priv->menu, &menu_allocation);
284 285 286 287 288

  gdk_window_get_origin (window, x, y);
  *x += allocation.x;
  *y += allocation.y;

289 290 291
  /* treat the default align value like START */
  if (align == GTK_ALIGN_FILL)
    align = GTK_ALIGN_START;
292

293
  if (align == GTK_ALIGN_CENTER)
294
    *x -= (menu_allocation.width - allocation.width) / 2;
295 296
  else if ((align == GTK_ALIGN_START && direction == GTK_TEXT_DIR_LTR) ||
           (align == GTK_ALIGN_END && direction == GTK_TEXT_DIR_RTL))
297 298 299
    *x += MAX (allocation.width - menu_allocation.width, 0);
  else if (menu_allocation.width > allocation.width)
    *x -= menu_allocation.width - allocation.width;
300

301
  if (priv->arrow_type == GTK_ARROW_UP && *y - menu_allocation.height >= monitor.y)
302
    {
303
      *y -= menu_allocation.height;
304 305
    }
  else
306
    {
307
      if ((*y + arrow_allocation.height + menu_allocation.height) <= monitor.y + monitor.height)
308
        *y += arrow_allocation.height;
309 310
      else if ((*y - menu_allocation.height) >= monitor.y)
        *y -= menu_allocation.height;
311 312 313
      else if (monitor.y + monitor.height - (*y + arrow_allocation.height) > *y)
        *y += arrow_allocation.height;
      else
314
        *y -= menu_allocation.height;
315 316 317 318 319 320
    }

  *push_in = FALSE;
}

static void
Matthias Clasen's avatar
Matthias Clasen committed
321 322 323 324 325
menu_position_side_func (GtkMenu       *menu,
                         gint          *x,
                         gint          *y,
                         gboolean      *push_in,
                         GtkMenuButton *menu_button)
326 327
{
  GtkMenuButtonPrivate *priv = menu_button->priv;
328
  GtkAllocation allocation;
329
  GtkAllocation menu_allocation;
330 331 332 333 334
  GtkWidget *widget = GTK_WIDGET (menu_button);
  GdkRectangle monitor;
  gint monitor_num;
  GdkScreen *screen;
  GdkWindow *window;
335
  GtkAlign align;
336
  GtkTextDirection direction;
337 338 339

  window = gtk_widget_get_window (widget);

340
  direction = gtk_widget_get_direction (widget);
341
  align = gtk_widget_get_valign (GTK_WIDGET (menu));
342 343 344 345 346 347 348 349
  screen = gtk_widget_get_screen (GTK_WIDGET (menu));
  monitor_num = gdk_screen_get_monitor_at_window (screen, window);
  if (monitor_num < 0)
    monitor_num = 0;
  gdk_screen_get_monitor_workarea (screen, monitor_num, &monitor);

  gdk_window_get_origin (gtk_button_get_event_window (GTK_BUTTON (menu_button)), x, y);

350
  gtk_widget_get_allocation (widget, &allocation);
351
  gtk_widget_get_allocation (priv->menu, &menu_allocation);
352

353 354 355
  if ((priv->arrow_type == GTK_ARROW_RIGHT && direction == GTK_TEXT_DIR_LTR) ||
      (priv->arrow_type == GTK_ARROW_LEFT && direction == GTK_TEXT_DIR_RTL))

356
    {
357
      if (*x + allocation.width + menu_allocation.width <= monitor.x + monitor.width)
358 359
        *x += allocation.width;
      else
360
        *x -= menu_allocation.width;
361
    }
362
  else
363
    {
364 365
      if (*x - menu_allocation.width >= monitor.x)
        *x -= menu_allocation.width;
366 367 368 369 370 371 372
      else
        *x += allocation.width;
    }

  /* treat the default align value like START */
  if (align == GTK_ALIGN_FILL)
    align = GTK_ALIGN_START;
373

374
  if (align == GTK_ALIGN_CENTER)
375
    *y -= (menu_allocation.height - allocation.height) / 2;
376
  else if (align == GTK_ALIGN_END)
377
    *y -= menu_allocation.height - allocation.height;
378 379 380 381 382

  *push_in = FALSE;
}

static void
383 384
popup_menu (GtkMenuButton *menu_button,
            GdkEvent      *event)
385 386 387
{
  GtkMenuButtonPrivate *priv = menu_button->priv;
  GtkMenuPositionFunc func;
388 389 390
  GdkDevice *device;
  guint button;
  guint32 time;
391 392 393 394

  if (priv->func)
    priv->func (priv->user_data);

395
  if (!priv->menu)
396 397 398 399 400 401 402 403 404
    return;

  switch (priv->arrow_type)
    {
      case GTK_ARROW_LEFT:
      case GTK_ARROW_RIGHT:
        func = (GtkMenuPositionFunc) menu_position_side_func;
        break;
      default:
405
        func = (GtkMenuPositionFunc) menu_position_up_down_func;
406 407 408
        break;
  }

409 410
  if (event != NULL &&
      gdk_event_get_screen (event) == gtk_widget_get_screen (GTK_WIDGET (menu_button)))
411 412 413 414 415 416 417 418 419 420 421 422
    {
      device = gdk_event_get_device (event);
      gdk_event_get_button (event, &button);
      time = gdk_event_get_time (event);
    }
  else
    {
      device = NULL;
      button = 0;
      time = gtk_get_current_event_time ();
    }

423
  gtk_menu_popup_for_device (GTK_MENU (priv->menu),
424
                             device,
425
                             NULL, NULL,
426 427 428
                             func,
                             GTK_WIDGET (menu_button),
                             NULL,
429 430
                             button,
                             time);
431 432 433
}

static void
434
gtk_menu_button_clicked (GtkButton *button)
435 436 437
{
  GtkMenuButton *menu_button = GTK_MENU_BUTTON (button);
  GtkMenuButtonPrivate *priv = menu_button->priv;
438
  gboolean active;
439

440 441 442 443 444
  if (priv->in_click)
    return;

  priv->in_click = TRUE;

445
  if (priv->menu)
446
    {
447 448 449 450
      active = !gtk_widget_get_visible (priv->menu);
      if (active)
        {
          GdkEvent *event;
451

452
          event = gtk_get_current_event ();
453

454
          popup_menu (menu_button, event);
455

456 457 458 459
          if (!event ||
              event->type == GDK_KEY_PRESS ||
              event->type == GDK_KEY_RELEASE)
            gtk_menu_shell_select_first (GTK_MENU_SHELL (priv->menu), FALSE);
460

461 462 463 464 465 466 467 468 469 470 471
          if (event)
            gdk_event_free (event);
        }
    }
  else if (priv->popover)
    {
      active = !gtk_widget_get_visible (priv->popover);
      if (active)
        gtk_widget_show (priv->popover);
      else
        gtk_widget_hide (priv->popover);
472
    }
473 474 475 476
  else
    active = FALSE;

  GTK_BUTTON_CLASS (gtk_menu_button_parent_class)->clicked (button);
477

478 479
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), active);
  gtk_toggle_button_toggled (GTK_TOGGLE_BUTTON (button));
480
  priv->in_click = FALSE;
481 482
}

483 484 485 486 487 488 489
static void
gtk_menu_button_add (GtkContainer *container,
                     GtkWidget    *child)
{
  GtkMenuButton *button = GTK_MENU_BUTTON (container);

  if (button->priv->arrow_widget)
490
    gtk_container_remove (container, button->priv->arrow_widget);
491 492 493 494

  GTK_CONTAINER_CLASS (gtk_menu_button_parent_class)->add (container, child);
}

495 496 497 498 499 500 501 502 503 504 505 506
static void
gtk_menu_button_remove (GtkContainer *container,
                        GtkWidget    *child)
{
  GtkMenuButton *button = GTK_MENU_BUTTON (container);

  if (child == button->priv->arrow_widget)
    button->priv->arrow_widget = NULL;

  GTK_CONTAINER_CLASS (gtk_menu_button_parent_class)->remove (container, child);
}

507 508 509 510 511
static void
gtk_menu_button_class_init (GtkMenuButtonClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
512
  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
513
  GtkButtonClass *button_class = GTK_BUTTON_CLASS (klass);
514 515 516

  gobject_class->set_property = gtk_menu_button_set_property;
  gobject_class->get_property = gtk_menu_button_get_property;
517
  gobject_class->dispose = gtk_menu_button_dispose;
518 519 520

  widget_class->state_flags_changed = gtk_menu_button_state_flags_changed;

521
  container_class->add = gtk_menu_button_add;
522
  container_class->remove = gtk_menu_button_remove;
523

524
  button_class->clicked = gtk_menu_button_clicked;
525 526

  /**
527 528 529 530 531 532
   * GtkMenuButton:popup:
   *
   * The #GtkMenu that will be popped up when the button is clicked.
   *
   * Since: 3.6
   */
533 534 535 536 537 538
  menu_button_props[PROP_POPUP] =
      g_param_spec_object ("popup",
                           P_("Popup"),
                           P_("The dropdown menu."),
                           GTK_TYPE_MENU,
                           GTK_PARAM_READWRITE);
539

540 541 542
  /**
   * GtkMenuButton:menu-model:
   *
543 544 545 546
   * The #GMenuModel from which the popup will be created.
   * Depending on the #GtkMenuButton:use-popover property, that may
   * be a menu or a popover.
   *
547
   * See gtk_menu_button_set_menu_model() for the interaction with the
Matthias Clasen's avatar
Matthias Clasen committed
548
   * #GtkMenuButton:popup property.
549 550 551
   *
   * Since: 3.6
   */
552 553 554 555 556 557 558
  menu_button_props[PROP_MENU_MODEL] =
      g_param_spec_object ("menu-model",
                           P_("Menu model"),
                           P_("The model from which the popup is made."),
                           G_TYPE_MENU_MODEL,
                           GTK_PARAM_READWRITE);

559 560 561
  /**
   * GtkMenuButton:align-widget:
   *
562
   * The #GtkWidget to use to align the menu with.
563 564 565
   *
   * Since: 3.6
   */
566 567 568 569 570 571 572
  menu_button_props[PROP_ALIGN_WIDGET] =
      g_param_spec_object ("align-widget",
                           P_("Align with"),
                           P_("The parent widget which the menu should align with."),
                           GTK_TYPE_CONTAINER,
                           GTK_PARAM_READWRITE);

573 574 575 576
  /**
   * GtkMenuButton:direction:
   *
   * The #GtkArrowType representing the direction in which the
577
   * menu or popover will be popped out.
578 579 580
   *
   * Since: 3.6
   */
581 582 583 584 585 586 587
  menu_button_props[PROP_DIRECTION] =
      g_param_spec_enum ("direction",
                         P_("Direction"),
                         P_("The direction the arrow should point."),
                         GTK_TYPE_ARROW_TYPE,
                         GTK_ARROW_DOWN,
                         GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
588

589 590 591 592 593 594 595 596
  /**
   * GtkMenuButton:use-popover:
   *
   * Whether to construct a #GtkPopover from the menu model,
   * or a #GtkMenu.
   *
   * Since: 3.12
   */
597 598 599 600 601 602
  menu_button_props[PROP_USE_POPOVER] =
      g_param_spec_boolean ("use-popover",
                            P_("Use a popover"),
                            P_("Use a popover instead of a menu"),
                            TRUE,
                            GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
603 604 605 606 607 608 609 610

  /**
   * GtkMenuButton:popover:
   *
   * The #GtkPopover that will be popped up when the button is clicked.
   *
   * Since: 3.12
   */
611 612 613 614 615 616
  menu_button_props[PROP_POPOVER] =
      g_param_spec_object ("popover",
                           P_("Popover"),
                           P_("The popover"),
                           GTK_TYPE_POPOVER,
                           G_PARAM_READWRITE);
617

618
  g_object_class_install_properties (gobject_class, LAST_PROP, menu_button_props);
Matthias Clasen's avatar
Matthias Clasen committed
619 620

  gtk_widget_class_set_accessible_type (widget_class, GTK_TYPE_MENU_BUTTON_ACCESSIBLE);
621
  gtk_widget_class_set_css_name (widget_class, "button");
622 623
}

624 625 626 627 628 629 630
static void
set_arrow_type (GtkImage     *image,
                GtkArrowType  arrow_type)
{
  switch (arrow_type)
    {
    case GTK_ARROW_NONE:
631
      gtk_image_set_from_icon_name (image, "open-menu-symbolic", GTK_ICON_SIZE_BUTTON);
632
      break;
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647
    case GTK_ARROW_DOWN:
      gtk_image_set_from_icon_name (image, "pan-down-symbolic", GTK_ICON_SIZE_BUTTON);
      break;
    case GTK_ARROW_UP:
      gtk_image_set_from_icon_name (image, "pan-up-symbolic", GTK_ICON_SIZE_BUTTON);
      break;
    case GTK_ARROW_LEFT:
      gtk_image_set_from_icon_name (image, "pan-start-symbolic", GTK_ICON_SIZE_BUTTON);
      break;
    case GTK_ARROW_RIGHT:
      gtk_image_set_from_icon_name (image, "pan-end-symbolic", GTK_ICON_SIZE_BUTTON);
      break;
    }
}

648 649 650 651
static void
add_arrow (GtkMenuButton *menu_button)
{
  GtkWidget *arrow;
652

653 654
  arrow = gtk_image_new ();
  set_arrow_type (GTK_IMAGE (arrow), menu_button->priv->arrow_type);
655 656 657 658 659 660 661 662 663
  gtk_container_add (GTK_CONTAINER (menu_button), arrow);
  gtk_widget_show (arrow);
  menu_button->priv->arrow_widget = arrow;
}

static void
gtk_menu_button_init (GtkMenuButton *menu_button)
{
  GtkMenuButtonPrivate *priv;
664
  GtkStyleContext *context;
665

666
  priv = gtk_menu_button_get_instance_private (menu_button);
667 668
  menu_button->priv = priv;
  priv->arrow_type = GTK_ARROW_DOWN;
669
  priv->use_popover = TRUE;
670

671 672
  add_arrow (menu_button);

673
  gtk_widget_set_sensitive (GTK_WIDGET (menu_button), FALSE);
674 675

  context = gtk_widget_get_style_context (GTK_WIDGET (menu_button));
676
  gtk_style_context_add_class (context, "popup");
677 678 679 680 681 682 683 684 685
}

/**
 * gtk_menu_button_new:
 *
 * Creates a new #GtkMenuButton widget with downwards-pointing
 * arrow as the only child. You can replace the child widget
 * with another #GtkWidget should you wish to.
 *
686
 * Returns: The newly created #GtkMenuButton widget
687 688 689 690 691 692 693 694 695 696 697 698
 *
 * Since: 3.6
 */
GtkWidget *
gtk_menu_button_new (void)
{
  return g_object_new (GTK_TYPE_MENU_BUTTON, NULL);
}

/* Callback for the "deactivate" signal on the pop-up menu.
 * This is used so that we unset the state of the toggle button
 * when the pop-up menu disappears.
699
 * Also used for the "close" signal on the popover.
700
 */
701 702
static gboolean
menu_deactivate_cb (GtkMenuButton *menu_button)
703 704 705 706 707 708 709 710 711 712 713 714
{
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (menu_button), FALSE);

  return TRUE;
}

static void
menu_detacher (GtkWidget *widget,
               GtkMenu   *menu)
{
  GtkMenuButtonPrivate *priv = GTK_MENU_BUTTON (widget)->priv;

715
  g_return_if_fail (priv->menu == (GtkWidget *) menu);
716

717
  priv->menu = NULL;
718 719
}

720 721 722 723 724 725 726 727 728 729 730 731
static void
update_sensitivity (GtkMenuButton *menu_button)
{
  GtkMenuButtonPrivate *priv = menu_button->priv;

  if (GTK_BUTTON (menu_button)->priv->action_helper)
    return;

  gtk_widget_set_sensitive (GTK_WIDGET (menu_button),
                            priv->menu != NULL || priv->popover != NULL);
}

732
/* This function is used in GtkMenuToolButton, the call back will
William Jon McCann's avatar
William Jon McCann committed
733
 * be called when GtkMenuToolButton would have emitted the “show-menu”
734 735 736
 * signal.
 */
void
737 738 739 740
_gtk_menu_button_set_popup_with_func (GtkMenuButton                 *menu_button,
                                      GtkWidget                     *menu,
                                      GtkMenuButtonShowMenuCallback  func,
                                      gpointer                       user_data)
741 742 743 744 745 746 747 748 749 750
{
  GtkMenuButtonPrivate *priv;

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
  g_return_if_fail (GTK_IS_MENU (menu) || menu == NULL);

  priv = menu_button->priv;
  priv->func = func;
  priv->user_data = user_data;

751
  if (priv->menu == GTK_WIDGET (menu))
752 753
    return;

754
  if (priv->menu)
755
    {
756 757
      if (gtk_widget_get_visible (priv->menu))
        gtk_menu_shell_deactivate (GTK_MENU_SHELL (priv->menu));
758

759
      g_signal_handlers_disconnect_by_func (priv->menu,
760 761
                                            menu_deactivate_cb,
                                            menu_button);
762
      gtk_menu_detach (GTK_MENU (priv->menu));
763 764
    }

765
  priv->menu = menu;
766

767
  if (priv->menu)
768
    {
769
      gtk_menu_attach_to_widget (GTK_MENU (priv->menu), GTK_WIDGET (menu_button),
770 771
                                 menu_detacher);

772
      gtk_widget_set_visible (priv->menu, FALSE);
773

774 775
      g_signal_connect_swapped (priv->menu, "deactivate",
                                G_CALLBACK (menu_deactivate_cb), menu_button);
776 777
    }

778
  update_sensitivity (menu_button);
Matthias Clasen's avatar
Matthias Clasen committed
779

780 781
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_POPUP]);
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_MENU_MODEL]);
782 783 784
}

/**
785
 * gtk_menu_button_set_popup:
786
 * @menu_button: a #GtkMenuButton
787
 * @menu: (allow-none): a #GtkMenu
788 789
 *
 * Sets the #GtkMenu that will be popped up when the button is clicked,
790 791
 * or %NULL to disable the button. If #GtkMenuButton:menu-model or
 * #GtkMenuButton:popover are set, they will be set to %NULL.
792 793 794 795
 *
 * Since: 3.6
 */
void
796
gtk_menu_button_set_popup (GtkMenuButton *menu_button,
797
                           GtkWidget     *menu)
798
{
799
  GtkMenuButtonPrivate *priv = menu_button->priv;
800 801

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
802
  g_return_if_fail (GTK_IS_MENU (menu) || menu == NULL);
803

804 805
  g_object_freeze_notify (G_OBJECT (menu_button));

806 807
  g_clear_object (&priv->model);

808 809 810 811 812
  _gtk_menu_button_set_popup_with_func (menu_button, menu, NULL, NULL);

  if (menu && priv->popover)
    gtk_menu_button_set_popover (menu_button, NULL);

813
  update_sensitivity (menu_button);
814 815

  g_object_thaw_notify (G_OBJECT (menu_button));
816 817 818
}

/**
819
 * gtk_menu_button_get_popup:
820 821 822
 * @menu_button: a #GtkMenuButton
 *
 * Returns the #GtkMenu that pops out of the button.
823 824
 * If the button does not use a #GtkMenu, this function
 * returns %NULL.
825
 *
826
 * Returns: (transfer none): a #GtkMenu or %NULL
827 828 829 830
 *
 * Since: 3.6
 */
GtkMenu *
831
gtk_menu_button_get_popup (GtkMenuButton *menu_button)
832 833 834
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

835
  return GTK_MENU (menu_button->priv->menu);
836 837
}

838 839 840 841 842
/**
 * gtk_menu_button_set_menu_model:
 * @menu_button: a #GtkMenuButton
 * @menu_model: (allow-none): a #GMenuModel
 *
843 844
 * Sets the #GMenuModel from which the popup will be constructed,
 * or %NULL to disable the button.
845
 *
846 847 848 849
 * Depending on the value of #GtkMenuButton:use-popover, either a
 * #GtkMenu will be created with gtk_menu_new_from_model(), or a
 * #GtkPopover with gtk_popover_new_from_model(). In either case,
 * actions will be connected as documented for these functions.
850
 *
851 852
 * If #GtkMenuButton:popup or #GtkMenuButton:popover are already set,
 * their content will be lost and replaced by the newly created popup.
853 854 855 856 857
 *
 * Since: 3.6
 */
void
gtk_menu_button_set_menu_model (GtkMenuButton *menu_button,
Matthias Clasen's avatar
Matthias Clasen committed
858
                                GMenuModel    *menu_model)
859 860 861 862 863 864 865
{
  GtkMenuButtonPrivate *priv;

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
  g_return_if_fail (G_IS_MENU_MODEL (menu_model) || menu_model == NULL);

  priv = menu_button->priv;
866

867
  g_object_freeze_notify (G_OBJECT (menu_button));
868

869 870
  if (menu_model)
    g_object_ref (menu_model);
871

872
  if (menu_model)
873
    {
874 875 876 877 878 879 880 881 882 883
      if (priv->use_popover)
        {
          GtkWidget *popover;

          popover = gtk_popover_new_from_model (GTK_WIDGET (menu_button), menu_model);
          gtk_menu_button_set_popover (menu_button, popover);
        }
      else
        {
          GtkWidget *menu;
884

885 886 887 888
          menu = gtk_menu_new_from_model (menu_model);
          gtk_widget_show_all (menu);
          gtk_menu_button_set_popup (menu_button, menu);
        }
889
    }
890 891
  else
    {
892 893
      gtk_menu_button_set_popup (menu_button, NULL);
      gtk_menu_button_set_popover (menu_button, NULL);
894
    }
895 896

  priv->model = menu_model;
897
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_MENU_MODEL]);
898 899

  g_object_thaw_notify (G_OBJECT (menu_button));
900 901 902 903 904 905
}

/**
 * gtk_menu_button_get_menu_model:
 * @menu_button: a #GtkMenuButton
 *
906
 * Returns the #GMenuModel used to generate the popup.
907
 *
908
 * Returns: (transfer none): a #GMenuModel or %NULL
909 910 911 912 913 914 915 916 917 918 919
 *
 * Since: 3.6
 */
GMenuModel *
gtk_menu_button_get_menu_model (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

  return menu_button->priv->model;
}

920 921 922 923 924 925 926 927 928 929 930 931 932
static void
set_align_widget_pointer (GtkMenuButton *menu_button,
                          GtkWidget     *align_widget)
{
  GtkMenuButtonPrivate *priv;

  priv = menu_button->priv;

  if (priv->align_widget)
    g_object_remove_weak_pointer (G_OBJECT (priv->align_widget), (gpointer *) &priv->align_widget);

  priv->align_widget = align_widget;

Matthias Clasen's avatar
Matthias Clasen committed
933
  if (priv->align_widget)
934 935 936
    g_object_add_weak_pointer (G_OBJECT (priv->align_widget), (gpointer *) &priv->align_widget);
}

937 938 939 940 941
/**
 * gtk_menu_button_set_align_widget:
 * @menu_button: a #GtkMenuButton
 * @align_widget: (allow-none): a #GtkWidget
 *
942 943
 * Sets the #GtkWidget to use to line the menu with when popped up.
 * Note that the @align_widget must contain the #GtkMenuButton itself.
944
 *
945
 * Setting it to %NULL means that the menu will be aligned with the
946 947
 * button itself.
 *
948 949 950
 * Note that this property is only used with menus currently,
 * and not for popovers.
 *
951 952 953 954
 * Since: 3.6
 */
void
gtk_menu_button_set_align_widget (GtkMenuButton *menu_button,
Matthias Clasen's avatar
Matthias Clasen committed
955
                                  GtkWidget     *align_widget)
956 957 958 959 960 961 962 963 964 965
{
  GtkMenuButtonPrivate *priv;

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
  g_return_if_fail (align_widget == NULL || gtk_widget_is_ancestor (GTK_WIDGET (menu_button), align_widget));

  priv = menu_button->priv;
  if (priv->align_widget == align_widget)
    return;

966
  set_align_widget_pointer (menu_button, align_widget);
967

968
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_ALIGN_WIDGET]);
969 970 971 972 973 974 975 976
}

/**
 * gtk_menu_button_get_align_widget:
 * @menu_button: a #GtkMenuButton
 *
 * Returns the parent #GtkWidget to use to line up with menu.
 *
977
 * Returns: (transfer none): a #GtkWidget value or %NULL
978 979 980 981 982 983 984 985 986 987 988
 *
 * Since: 3.6
 */
GtkWidget *
gtk_menu_button_get_align_widget (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

  return menu_button->priv->align_widget;
}

989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014
static void
update_popover_direction (GtkMenuButton *menu_button)
{
  GtkMenuButtonPrivate *priv = menu_button->priv;

  if (!priv->popover)
    return;

  switch (priv->arrow_type)
    {
    case GTK_ARROW_UP:
      gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_TOP);
      break;
    case GTK_ARROW_DOWN:
    case GTK_ARROW_NONE:
      gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_BOTTOM);
      break;
    case GTK_ARROW_LEFT:
      gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_LEFT);
      break;
    case GTK_ARROW_RIGHT:
      gtk_popover_set_position (GTK_POPOVER (priv->popover), GTK_POS_RIGHT);
      break;
    }
}

1015 1016 1017 1018 1019
/**
 * gtk_menu_button_set_direction:
 * @menu_button: a #GtkMenuButton
 * @direction: a #GtkArrowType
 *
1020
 * Sets the direction in which the popup will be popped up, as
1021
 * well as changing the arrow’s direction. The child will not
1022 1023
 * be changed to an arrow if it was customized.
 *
1024 1025
 * If the does not fit in the available space in the given direction,
 * GTK+ will its best to keep it inside the screen and fully visible.
1026
 *
1027
 * If you pass %GTK_ARROW_NONE for a @direction, the popup will behave
1028
 * as if you passed %GTK_ARROW_DOWN (although you won’t see any arrows).
1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044
 *
 * Since: 3.6
 */
void
gtk_menu_button_set_direction (GtkMenuButton *menu_button,
                               GtkArrowType   direction)
{
  GtkMenuButtonPrivate *priv = menu_button->priv;
  GtkWidget *child;

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

  if (priv->arrow_type == direction)
    return;

  priv->arrow_type = direction;
1045
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_DIRECTION]);
1046

1047
  /* Is it custom content? We don't change that */
1048
  child = gtk_bin_get_child (GTK_BIN (menu_button));
1049 1050 1051
  if (priv->arrow_widget != child)
    return;

1052
  set_arrow_type (GTK_IMAGE (child), priv->arrow_type);
1053
  update_popover_direction (menu_button);
1054 1055 1056 1057 1058 1059
}

/**
 * gtk_menu_button_get_direction:
 * @menu_button: a #GtkMenuButton
 *
1060
 * Returns the direction the popup will be pointing at when popped up.
1061
 *
1062
 * Returns: a #GtkArrowType value
1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074
 *
 * Since: 3.6
 */
GtkArrowType
gtk_menu_button_get_direction (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), GTK_ARROW_DOWN);

  return menu_button->priv->arrow_type;
}

static void
1075
gtk_menu_button_dispose (GObject *object)
1076 1077 1078
{
  GtkMenuButtonPrivate *priv = GTK_MENU_BUTTON (object)->priv;

1079
  if (priv->menu)
1080
    {
1081
      g_signal_handlers_disconnect_by_func (priv->menu,
1082 1083
                                            menu_deactivate_cb,
                                            object);
1084 1085 1086 1087 1088 1089 1090 1091
      gtk_menu_detach (GTK_MENU (priv->menu));
      priv->menu = NULL;
    }

  if (priv->popover)
    {
      gtk_widget_destroy (priv->popover);
      priv->popover = NULL;
1092 1093
    }

1094 1095
  set_align_widget_pointer (GTK_MENU_BUTTON (object), NULL);

1096 1097
  g_clear_object (&priv->model);

1098
  G_OBJECT_CLASS (gtk_menu_button_parent_class)->dispose (object);
1099
}
1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128

/**
 * gtk_menu_button_set_use_popover:
 * @menu_button: a #GtkMenuButton
 * @use_popover: %TRUE to construct a popover from the menu model
 *
 * Sets whether to construct a #GtkPopover instead of #GtkMenu
 * when gtk_menu_button_set_menu_model() is called. Note that
 * this property is only consulted when a new menu model is set.
 *
 * Since: 3.12
 */
void
gtk_menu_button_set_use_popover (GtkMenuButton *menu_button,
                                 gboolean       use_popover)
{
  GtkMenuButtonPrivate *priv;

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

  priv = menu_button->priv;

  use_popover = use_popover != FALSE;

  if (priv->use_popover == use_popover)
    return;

  priv->use_popover = use_popover;

1129 1130 1131 1132 1133
  g_object_freeze_notify (G_OBJECT (menu_button));

  if (priv->model)
    gtk_menu_button_set_menu_model (menu_button, priv->model);

1134
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_USE_POPOVER]);
1135 1136

  g_object_thaw_notify (G_OBJECT (menu_button));
1137 1138 1139 1140 1141 1142 1143 1144 1145
}

/**
 * gtk_menu_button_get_use_popover:
 * @menu_button: a #GtkMenuButton
 *
 * Returns whether a #GtkPopover or a #GtkMenu will be constructed
 * from the menu model.
 *
1146
 * Returns: %TRUE if using a #GtkPopover
1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177
 *
 * Since: 3.12
 */
gboolean
gtk_menu_button_get_use_popover (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), FALSE);

  return menu_button->priv->use_popover;
}

/**
 * gtk_menu_button_set_popover:
 * @menu_button: a #GtkMenuButton
 * @popover: (allow-none): a #GtkPopover
 *
 * Sets the #GtkPopover that will be popped up when the button is
 * clicked, or %NULL to disable the button. If #GtkMenuButton:menu-model
 * or #GtkMenuButton:popup are set, they will be set to %NULL.
 *
 * Since: 3.12
 */
void
gtk_menu_button_set_popover (GtkMenuButton *menu_button,
                             GtkWidget     *popover)
{
  GtkMenuButtonPrivate *priv = menu_button->priv;

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
  g_return_if_fail (GTK_IS_POPOVER (popover) || popover == NULL);

1178 1179
  g_object_freeze_notify (G_OBJECT (menu_button));

1180 1181 1182 1183 1184 1185 1186 1187 1188 1189
  g_clear_object (&priv->model);

  if (priv->popover)
    {
      if (gtk_widget_get_visible (priv->popover))
        gtk_widget_hide (priv->popover);

      g_signal_handlers_disconnect_by_func (priv->popover,
                                            menu_deactivate_cb,
                                            menu_button);
1190 1191

      gtk_popover_set_relative_to (GTK_POPOVER (priv->popover), NULL);
1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207
    }

  priv->popover = popover;

  if (popover)
    {
      gtk_popover_set_relative_to (GTK_POPOVER (priv->popover), GTK_WIDGET (menu_button));
      g_signal_connect_swapped (priv->popover, "closed",
                                G_CALLBACK (menu_deactivate_cb), menu_button);
      update_popover_direction (menu_button);
      gtk_style_context_remove_class (gtk_widget_get_style_context (GTK_WIDGET (menu_button)), "menu-button");
    }

  if (popover && priv->menu)
    gtk_menu_button_set_popup (menu_button, NULL);

1208
  update_sensitivity (menu_button);
1209

1210 1211
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_POPOVER]);
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_MENU_MODEL]);
1212
  g_object_thaw_notify (G_OBJECT (menu_button));
1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233
}

/**
 * gtk_menu_button_get_popover:
 * @menu_button: a #GtkMenuButton
 *
 * Returns the #GtkPopover that pops out of the button.
 * If the button is not using a #GtkPopover, this function
 * returns %NULL.
 *
 * Returns: (transfer none): a #GtkPopover or %NULL
 *
 * Since: 3.12
 */
GtkPopover *
gtk_menu_button_get_popover (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

  return GTK_POPOVER (menu_button->priv->popover);
}