gtkmenubutton.c 33 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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/>.
 */

/**
Matthias Clasen's avatar
Matthias Clasen committed
22
23
 * GtkMenuButton:
 *
24
 * The `GtkMenuButton` widget is used to display a popup when clicked.
Matthias Clasen's avatar
Matthias Clasen committed
25
26
27
28
29
30
31
32
33
34
 *
 * ![An example GtkMenuButton](menu-button.png)
 *
 * This popup can be provided either as a `GtkPopover` or as an abstract
 * `GMenuModel`.
 *
 * The `GtkMenuButton` widget can show either an icon (set with the
 * [property@Gtk.MenuButton:icon-name] property) or a label (set with the
 * [property@Gtk.MenuButton:label] property). If neither is explicitly set,
 * a [class@Gtk.Image] is automatically created, using an arrow image oriented
35
36
 * according to [property@Gtk.MenuButton:direction] or the generic
 * “open-menu-symbolic” icon if the direction is not set.
37
 *
Matthias Clasen's avatar
Matthias Clasen committed
38
 * The positioning of the popup is determined by the
39
 * [property@Gtk.MenuButton:direction] property of the menu button.
40
 *
Matthias Clasen's avatar
Matthias Clasen committed
41
42
43
44
 * For menus, the [property@Gtk.Widget:halign] and [property@Gtk.Widget: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
45
46
47
48
 * (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”.
49
 *
50
51
52
53
54
55
 * |           | start                | center                | end                |
 * | -         | ---                  | ---                   | ---                |
 * | **down**  | ![](down-start.png)  | ![](down-center.png)  | ![](down-end.png)  |
 * | **up**    | ![](up-start.png)    | ![](up-center.png)    | ![](up-end.png)    |
 * | **left**  | ![](left-start.png)  | ![](left-center.png)  | ![](left-end.png)  |
 * | **right** | ![](right-start.png) | ![](right-center.png) | ![](right-end.png) |
56
 *
57
58
 * # CSS nodes
 *
59
 * ```
60
61
 * menubutton
 * ╰── button.toggle
62
63
 *     ╰── <content>
 *          ╰── [arrow]
64
 * ```
65
 *
Matthias Clasen's avatar
Matthias Clasen committed
66
67
 * `GtkMenuButton` has a single CSS node with name `menubutton`
 * which contains a `button` node with a `.toggle` style class.
68
 *
Matthias Clasen's avatar
Matthias Clasen committed
69
70
71
 * Inside the toggle button content, there is an `arrow` node for
 * the indicator, which will carry one of the `.none`, `.up`, `.down`,
 * `.left` or `.right` style classes to indicate the direction that
72
 * the menu will appear in. The CSS is expected to provide a suitable
Matthias Clasen's avatar
Matthias Clasen committed
73
 * image for each of these cases using the `-gtk-icon-source` property.
74
 *
Matthias Clasen's avatar
Matthias Clasen committed
75
 * Optionally, the `menubutton` node can carry the `.circular` style class
Matthias Clasen's avatar
Matthias Clasen committed
76
77
 * to request a round appearance.
 *
78
79
 * # Accessibility
 *
Matthias Clasen's avatar
Matthias Clasen committed
80
 * `GtkMenuButton` uses the #GTK_ACCESSIBLE_ROLE_BUTTON role.
81
82
83
84
 */

#include "config.h"

Timm Bäder's avatar
Timm Bäder committed
85
#include "gtkactionable.h"
86
#include "gtkbuiltiniconprivate.h"
87
#include "gtkintl.h"
88
#include "gtkmain.h"
89
90
#include "gtkmenubutton.h"
#include "gtkmenubuttonprivate.h"
91
#include "gtkpopover.h"
92
#include "gtkpopovermenu.h"
93
#include "gtkprivate.h"
94
#include "gtktypebuiltins.h"
95
96
#include "gtklabel.h"
#include "gtkbox.h"
97
98
99
#include "gtkwidgetprivate.h"
#include "gtkbuttonprivate.h"
#include "gtknative.h"
100

Matthias Clasen's avatar
Matthias Clasen committed
101
102
103
104
105
106
107
typedef struct _GtkMenuButtonClass   GtkMenuButtonClass;
typedef struct _GtkMenuButtonPrivate GtkMenuButtonPrivate;

struct _GtkMenuButton
{
  GtkWidget parent_instance;

108
  GtkWidget *button;
109
  GtkWidget *popover; /* Only one at a time can be set */
110
111
  GMenuModel *model;

112
113
114
  GtkMenuButtonCreatePopupFunc create_popup_func;
  gpointer create_popup_user_data;
  GDestroyNotify create_popup_destroy_notify;
115

116
  GtkWidget *label_widget;
117
  GtkWidget *arrow_widget;
118
  GtkArrowType arrow_type;
119
120
};

121
122
123
124
125
struct _GtkMenuButtonClass
{
  GtkWidgetClass parent_class;
};

126
127
128
enum
{
  PROP_0,
129
  PROP_MENU_MODEL,
130
  PROP_DIRECTION,
131
  PROP_POPOVER,
132
133
  PROP_ICON_NAME,
  PROP_LABEL,
134
  PROP_USE_UNDERLINE,
Matthias Clasen's avatar
Matthias Clasen committed
135
  PROP_HAS_FRAME,
136
  LAST_PROP
137
138
};

139
140
static GParamSpec *menu_button_props[LAST_PROP];

141
G_DEFINE_TYPE (GtkMenuButton, gtk_menu_button, GTK_TYPE_WIDGET)
142

143
static void gtk_menu_button_dispose (GObject *object);
144
145
146

static void
gtk_menu_button_set_property (GObject      *object,
Matthias Clasen's avatar
Matthias Clasen committed
147
148
149
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
150
151
152
153
154
{
  GtkMenuButton *self = GTK_MENU_BUTTON (object);

  switch (property_id)
    {
155
      case PROP_MENU_MODEL:
156
157
158
159
160
        gtk_menu_button_set_menu_model (self, g_value_get_object (value));
        break;
      case PROP_DIRECTION:
        gtk_menu_button_set_direction (self, g_value_get_enum (value));
        break;
161
162
163
      case PROP_POPOVER:
        gtk_menu_button_set_popover (self, g_value_get_object (value));
        break;
164
165
166
167
168
169
      case PROP_ICON_NAME:
        gtk_menu_button_set_icon_name (self, g_value_get_string (value));
        break;
      case PROP_LABEL:
        gtk_menu_button_set_label (self, g_value_get_string (value));
        break;
170
171
172
      case PROP_USE_UNDERLINE:
        gtk_menu_button_set_use_underline (self, g_value_get_boolean (value));
        break;
Matthias Clasen's avatar
Matthias Clasen committed
173
174
      case PROP_HAS_FRAME:
        gtk_menu_button_set_has_frame (self, g_value_get_boolean (value));
175
        break;
176
177
178
179
180
181
182
      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
183
184
185
                              guint       property_id,
                              GValue     *value,
                              GParamSpec *pspec)
186
{
187
  GtkMenuButton *self = GTK_MENU_BUTTON (object);
188
189
190

  switch (property_id)
    {
191
      case PROP_MENU_MODEL:
192
        g_value_set_object (value, self->model);
193
194
        break;
      case PROP_DIRECTION:
195
        g_value_set_enum (value, self->arrow_type);
196
        break;
197
      case PROP_POPOVER:
198
        g_value_set_object (value, self->popover);
199
        break;
200
201
202
203
204
205
      case PROP_ICON_NAME:
        g_value_set_string (value, gtk_menu_button_get_icon_name (GTK_MENU_BUTTON (object)));
        break;
      case PROP_LABEL:
        g_value_set_string (value, gtk_menu_button_get_label (GTK_MENU_BUTTON (object)));
        break;
206
207
208
      case PROP_USE_UNDERLINE:
        g_value_set_boolean (value, gtk_menu_button_get_use_underline (GTK_MENU_BUTTON (object)));
        break;
Matthias Clasen's avatar
Matthias Clasen committed
209
210
      case PROP_HAS_FRAME:
        g_value_set_boolean (value, gtk_menu_button_get_has_frame (GTK_MENU_BUTTON (object)));
211
        break;
212
213
214
215
216
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
    }
}

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
static void
gtk_menu_button_notify (GObject    *object,
                        GParamSpec *pspec)
{
  if (strcmp (pspec->name, "focus-on-click") == 0)
    {
      GtkMenuButton *self = GTK_MENU_BUTTON (object);

      gtk_widget_set_focus_on_click (self->button,
                                     gtk_widget_get_focus_on_click (GTK_WIDGET (self)));
    }

  if (G_OBJECT_CLASS (gtk_menu_button_parent_class)->notify)
    G_OBJECT_CLASS (gtk_menu_button_parent_class)->notify (object, pspec);
}

233
234
235
236
static void
gtk_menu_button_state_flags_changed (GtkWidget    *widget,
                                     GtkStateFlags previous_state_flags)
{
237
  GtkMenuButton *self = GTK_MENU_BUTTON (widget);
238

239
240
  if (!gtk_widget_is_sensitive (widget))
    {
241
242
      if (self->popover)
        gtk_widget_hide (self->popover);
243
    }
244
245
246
}

static void
247
gtk_menu_button_toggled (GtkMenuButton *self)
248
{
249
  const gboolean active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->button));
250
251

  /* Might set a new menu/popover */
252
  if (active && self->create_popup_func)
253
    {
254
      self->create_popup_func (self, self->create_popup_user_data);
255
    }
256

257
  if (self->popover)
258
259
    {
      if (active)
260
261
262
263
264
265
        {
          gtk_popover_popup (GTK_POPOVER (self->popover));
          gtk_accessible_update_state (GTK_ACCESSIBLE (self),
                                       GTK_ACCESSIBLE_STATE_EXPANDED, TRUE,
                                       -1);
        }
266
      else
267
268
269
270
271
        {
          gtk_popover_popdown (GTK_POPOVER (self->popover));
          gtk_accessible_reset_state (GTK_ACCESSIBLE (self),
                                      GTK_ACCESSIBLE_STATE_EXPANDED);
        }
272
273
274
    }
}

Matthias Clasen's avatar
Matthias Clasen committed
275
static void
276
277
278
279
280
281
282
gtk_menu_button_measure (GtkWidget      *widget,
                         GtkOrientation  orientation,
                         int             for_size,
                         int            *minimum,
                         int            *natural,
                         int            *minimum_baseline,
                         int            *natural_baseline)
Matthias Clasen's avatar
Matthias Clasen committed
283
{
284
  GtkMenuButton *self = GTK_MENU_BUTTON (widget);
Matthias Clasen's avatar
Matthias Clasen committed
285

286
  gtk_widget_measure (self->button,
287
288
289
290
                      orientation,
                      for_size,
                      minimum, natural,
                      minimum_baseline, natural_baseline);
Matthias Clasen's avatar
Matthias Clasen committed
291

Matthias Clasen's avatar
Matthias Clasen committed
292
293
}

294
static void
295
296
297
298
gtk_menu_button_size_allocate (GtkWidget *widget,
                               int        width,
                               int        height,
                               int        baseline)
299
{
300
  GtkMenuButton *self= GTK_MENU_BUTTON (widget);
301

302
  gtk_widget_size_allocate (self->button,
303
304
                            &(GtkAllocation) { 0, 0, width, height },
                            baseline);
305
  if (self->popover)
306
    gtk_popover_present (GTK_POPOVER (self->popover));
307
308
309
310
311
312
}

static gboolean
gtk_menu_button_focus (GtkWidget        *widget,
                       GtkDirectionType  direction)
{
313
  GtkMenuButton *self = GTK_MENU_BUTTON (widget);
314

315
316
  if (self->popover && gtk_widget_get_visible (self->popover))
    return gtk_widget_child_focus (self->popover, direction);
317
  else
318
319
320
321
322
323
324
325
326
    return gtk_widget_child_focus (self->button, direction);
}

static gboolean
gtk_menu_button_grab_focus (GtkWidget *widget)
{
  GtkMenuButton *self = GTK_MENU_BUTTON (widget);

  return gtk_widget_grab_focus (self->button);
327
328
}

329
330
331
332
333
334
335
336
static void
gtk_menu_button_class_init (GtkMenuButtonClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  gobject_class->set_property = gtk_menu_button_set_property;
  gobject_class->get_property = gtk_menu_button_get_property;
337
  gobject_class->notify = gtk_menu_button_notify;
338
  gobject_class->dispose = gtk_menu_button_dispose;
339

340
341
  widget_class->measure = gtk_menu_button_measure;
  widget_class->size_allocate = gtk_menu_button_size_allocate;
342
343
  widget_class->state_flags_changed = gtk_menu_button_state_flags_changed;
  widget_class->focus = gtk_menu_button_focus;
344
  widget_class->grab_focus = gtk_menu_button_grab_focus;
345
346

  /**
347
   * GtkMenuButton:menu-model: (attributes org.gtk.Property.get=gtk_menu_button_get_menu_model org.gtk.Property.set=gtk_menu_button_set_menu_model)
348
   *
Matthias Clasen's avatar
Matthias Clasen committed
349
   * The `GMenuModel` from which the popup will be created.
350
   *
Matthias Clasen's avatar
Matthias Clasen committed
351
352
   * See [method@Gtk.MenuButton.set_menu_model] for the interaction
   * with the [property@Gtk.MenuButton:popover] property.
353
   */
354
355
356
357
358
359
360
  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);

361
  /**
362
   * GtkMenuButton:direction: (attributes org.gtk.Property.get=gtk_menu_button_get_direction org.gtk.Property.set=gtk_menu_button_set_direction)
363
   *
Matthias Clasen's avatar
Matthias Clasen committed
364
   * The `GtkArrowType` representing the direction in which the
365
   * menu or popover will be popped out.
366
   */
367
368
369
370
371
372
373
  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);
374

375
  /**
376
   * GtkMenuButton:popover: (attributes org.gtk.Property.get=gtk_menu_button_get_popover org.gtk.Property.set=gtk_menu_button_set_popover)
377
   *
Matthias Clasen's avatar
Matthias Clasen committed
378
   * The `GtkPopover` that will be popped up when the button is clicked.
379
   */
380
381
382
383
384
385
  menu_button_props[PROP_POPOVER] =
      g_param_spec_object ("popover",
                           P_("Popover"),
                           P_("The popover"),
                           GTK_TYPE_POPOVER,
                           G_PARAM_READWRITE);
386

Matthias Clasen's avatar
Matthias Clasen committed
387
  /**
388
   * GtkMenuButton:icon-name: (attributes org.gtk.Property.get=gtk_menu_button_get_icon_name org.gtk.Property.set=gtk_menu_button_set_icon_name)
Matthias Clasen's avatar
Matthias Clasen committed
389
390
391
   *
   * The name of the icon used to automatically populate the button.
   */
392
393
394
395
396
397
398
  menu_button_props[PROP_ICON_NAME] =
      g_param_spec_string ("icon-name",
                           P_("Icon Name"),
                           P_("The name of the icon used to automatically populate the button"),
                           NULL,
                           GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);

Matthias Clasen's avatar
Matthias Clasen committed
399
  /**
400
   * GtkMenuButton:label: (attributes org.gtk.Property.get=gtk_menu_button_get_label org.gtk.Property.set=gtk_menu_button_set_label)
Matthias Clasen's avatar
Matthias Clasen committed
401
402
403
   *
   * The label for the button.
   */
404
405
406
407
408
409
410
  menu_button_props[PROP_LABEL] =
      g_param_spec_string ("label",
                           P_("Label"),
                           P_("The label for the button"),
                           NULL,
                           GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);

Matthias Clasen's avatar
Matthias Clasen committed
411
  /**
412
   * GtkMenuButton:use-underline: (attributes org.gtk.Property.get=gtk_menu_button_get_use_underline org.gtk.Property.set=gtk_menu_button_set_use_underline)
Matthias Clasen's avatar
Matthias Clasen committed
413
414
415
   *
   * If set an underscore in the text indicates a mnemonic.
   */
416
417
418
419
420
421
422
  menu_button_props[PROP_USE_UNDERLINE] =
      g_param_spec_boolean ("use-underline",
                            P_("Use underline"),
                            P_("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"),
                           FALSE,
                           GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);

Matthias Clasen's avatar
Matthias Clasen committed
423
  /**
424
   * GtkMenuButton:has-frame: (attributes org.gtk.Property.get=gtk_menu_button_get_has_frame org.gtk.Property.set=gtk_menu_button_set_has_frame)
Matthias Clasen's avatar
Matthias Clasen committed
425
426
427
   *
   * Whether the button has a frame.
   */
Matthias Clasen's avatar
Matthias Clasen committed
428
429
430
431
432
433
  menu_button_props[PROP_HAS_FRAME] =
    g_param_spec_boolean ("has-frame",
                          P_("Has frame"),
                          P_("Whether the button has a frame"),
                          TRUE,
                          GTK_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
434

435
  g_object_class_install_properties (gobject_class, LAST_PROP, menu_button_props);
Matthias Clasen's avatar
Matthias Clasen committed
436

437
  gtk_widget_class_set_css_name (widget_class, I_("menubutton"));
438
  gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_BUTTON);
439
440
}

441
static void
442
set_arrow_type (GtkWidget    *arrow,
443
444
                GtkArrowType  arrow_type,
                gboolean      visible)
445
{
446
447
448
449
450
  gtk_widget_remove_css_class (arrow, "none");
  gtk_widget_remove_css_class (arrow, "down");
  gtk_widget_remove_css_class (arrow, "up");
  gtk_widget_remove_css_class (arrow, "left");
  gtk_widget_remove_css_class (arrow, "right");
451
452
453
  switch (arrow_type)
    {
    case GTK_ARROW_NONE:
454
      gtk_widget_add_css_class (arrow, "none");
455
      break;
456
    case GTK_ARROW_DOWN:
457
      gtk_widget_add_css_class (arrow, "down");
458
459
      break;
    case GTK_ARROW_UP:
460
      gtk_widget_add_css_class (arrow, "up");
461
462
      break;
    case GTK_ARROW_LEFT:
463
      gtk_widget_add_css_class (arrow, "left");
464
465
      break;
    case GTK_ARROW_RIGHT:
466
      gtk_widget_add_css_class (arrow, "right");
467
      break;
468
469
    default:
      break;
470
    }
471
472

  if (visible)
473
    gtk_widget_show (arrow);
474
  else
475
    gtk_widget_hide (arrow);
476
477
}

478
static void
479
add_arrow (GtkMenuButton *self)
480
481
{
  GtkWidget *arrow;
482

483
484
  arrow = gtk_builtin_icon_new ("arrow");
  set_arrow_type (arrow, self->arrow_type, TRUE);
485
  gtk_button_set_child (GTK_BUTTON (self->button), arrow);
486
  self->arrow_widget = arrow;
487
488
489
}

static void
490
gtk_menu_button_init (GtkMenuButton *self)
491
{
492
  self->arrow_type = GTK_ARROW_DOWN;
493

494
495
496
497
  self->button = gtk_toggle_button_new ();
  gtk_widget_set_parent (self->button, GTK_WIDGET (self));
  g_signal_connect_swapped (self->button, "toggled", G_CALLBACK (gtk_menu_button_toggled), self);
  add_arrow (self);
498

499
  gtk_widget_set_sensitive (self->button, FALSE);
500

501
  gtk_widget_add_css_class (GTK_WIDGET (self), "popup");
502
503
504
505
506
}

/**
 * gtk_menu_button_new:
 *
Matthias Clasen's avatar
Matthias Clasen committed
507
508
509
510
511
 * 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.
512
 *
Matthias Clasen's avatar
Matthias Clasen committed
513
 * Returns: The newly created `GtkMenuButton`
514
515
516
517
518
519
520
 */
GtkWidget *
gtk_menu_button_new (void)
{
  return g_object_new (GTK_TYPE_MENU_BUTTON, NULL);
}

521
static void
522
update_sensitivity (GtkMenuButton *self)
523
{
524
525
526
527
528
529
530
531
532
533
534
  gboolean has_popup;

  has_popup = self->popover != NULL || self->create_popup_func != NULL;

  gtk_widget_set_sensitive (self->button, has_popup);

  gtk_accessible_update_property (GTK_ACCESSIBLE (self),
                                  GTK_ACCESSIBLE_PROPERTY_HAS_POPUP, has_popup,
                                  -1);
  if (self->popover != NULL)
    gtk_accessible_update_relation (GTK_ACCESSIBLE (self),
535
                                    GTK_ACCESSIBLE_RELATION_CONTROLS, self->popover, NULL,
536
537
538
539
                                    -1);
  else
    gtk_accessible_reset_relation (GTK_ACCESSIBLE (self),
                                   GTK_ACCESSIBLE_RELATION_CONTROLS);
540
541
}

542
static gboolean
543
menu_deactivate_cb (GtkMenuButton *self)
544
{
545
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->button), FALSE);
546

547
  return TRUE;
548
549
}

550
/**
551
 * gtk_menu_button_set_menu_model: (attributes org.gtk.Method.set_property=menu-model)
Matthias Clasen's avatar
Matthias Clasen committed
552
553
 * @menu_button: a `GtkMenuButton`
 * @menu_model: (nullable): a `GMenuModel`, or %NULL to unset and disable the
554
 *   button
555
 *
Matthias Clasen's avatar
Matthias Clasen committed
556
 * Sets the `GMenuModel` from which the popup will be constructed.
557
 *
Matthias Clasen's avatar
Matthias Clasen committed
558
 * If @menu_model is %NULL, the button is disabled.
559
 *
Matthias Clasen's avatar
Matthias Clasen committed
560
561
562
563
 * A [class@Gtk.Popover] will be created from the menu model with
 * [ctor@Gtk.PopoverMenu.new_from_model]. Actions will be connected
 * as documented for this function.
 *
564
565
 * If [property@Gtk.MenuButton:popover] is already set, it will be
 * dissociated from the @menu_button, and the property is set to %NULL.
566
567
568
 */
void
gtk_menu_button_set_menu_model (GtkMenuButton *menu_button,
Matthias Clasen's avatar
Matthias Clasen committed
569
                                GMenuModel    *menu_model)
570
571
572
573
{
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
  g_return_if_fail (G_IS_MENU_MODEL (menu_model) || menu_model == NULL);

574
  g_object_freeze_notify (G_OBJECT (menu_button));
575

576
577
  if (menu_model)
    g_object_ref (menu_model);
578

579
  if (menu_model)
580
    {
581
      GtkWidget *popover;
582

Matthias Clasen's avatar
Matthias Clasen committed
583
      popover = gtk_popover_menu_new_from_model (menu_model);
584
      gtk_menu_button_set_popover (menu_button, popover);
585
    }
586
587
  else
    {
588
      gtk_menu_button_set_popover (menu_button, NULL);
589
    }
590

591
  menu_button->model = menu_model;
592
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_MENU_MODEL]);
593
594

  g_object_thaw_notify (G_OBJECT (menu_button));
595
596
597
}

/**
598
 * gtk_menu_button_get_menu_model: (attributes org.gtk.Method.get_property=menu-model)
Matthias Clasen's avatar
Matthias Clasen committed
599
 * @menu_button: a `GtkMenuButton`
600
 *
Matthias Clasen's avatar
Matthias Clasen committed
601
 * Returns the `GMenuModel` used to generate the popup.
602
 *
Matthias Clasen's avatar
Matthias Clasen committed
603
 * Returns: (nullable) (transfer none): a `GMenuModel` or %NULL
604
605
606
607
608
609
 */
GMenuModel *
gtk_menu_button_get_menu_model (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

610
  return menu_button->model;
611
612
}

613
static void
614
update_popover_direction (GtkMenuButton *self)
615
{
616
  if (!self->popover)
617
618
    return;

619
  switch (self->arrow_type)
620
621
    {
    case GTK_ARROW_UP:
622
      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_TOP);
623
624
625
      break;
    case GTK_ARROW_DOWN:
    case GTK_ARROW_NONE:
626
      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_BOTTOM);
627
628
      break;
    case GTK_ARROW_LEFT:
629
      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_LEFT);
630
631
      break;
    case GTK_ARROW_RIGHT:
632
      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_RIGHT);
633
      break;
634
635
    default:
      break;
636
637
638
    }
}

639
640
641
642
643
644
static void
popover_destroy_cb (GtkMenuButton *menu_button)
{
  gtk_menu_button_set_popover (menu_button, NULL);
}

645
/**
646
 * gtk_menu_button_set_direction: (attributes org.gtk.Method.set_property=direction)
Matthias Clasen's avatar
Matthias Clasen committed
647
648
649
650
 * @menu_button: a `GtkMenuButton`
 * @direction: a `GtkArrowType`
 *
 * Sets the direction in which the popup will be popped up.
651
 *
Matthias Clasen's avatar
Matthias Clasen committed
652
653
 * If the button is automatically populated with an arrow icon,
 * its direction will be changed to match.
654
 *
655
 * If the does not fit in the available space in the given direction,
Matthias Clasen's avatar
Matthias Clasen committed
656
 * GTK will its best to keep it inside the screen and fully visible.
657
 *
658
 * If you pass %GTK_ARROW_NONE for a @direction, the popup will behave
659
 * as if you passed %GTK_ARROW_DOWN (although you won’t see any arrows).
660
661
662
663
664
 */
void
gtk_menu_button_set_direction (GtkMenuButton *menu_button,
                               GtkArrowType   direction)
{
665
666
  gboolean is_image_button;

667
668
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

669
  if (menu_button->arrow_type == direction)
670
671
    return;

672
  menu_button->arrow_type = direction;
673
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_DIRECTION]);
674

675
  /* Is it custom content? We don't change that */
676
677
  is_image_button = menu_button->label_widget == NULL;
  if (is_image_button && (menu_button->arrow_widget != gtk_button_get_child (GTK_BUTTON (menu_button->button))))
678
679
    return;

680
  set_arrow_type (menu_button->arrow_widget,
681
682
                  menu_button->arrow_type,
                  is_image_button || (menu_button->arrow_type != GTK_ARROW_NONE));
683
  update_popover_direction (menu_button);
684
685
686
}

/**
687
 * gtk_menu_button_get_direction: (attributes org.gtk.Method.get_property=direction)
Matthias Clasen's avatar
Matthias Clasen committed
688
 * @menu_button: a `GtkMenuButton`
689
 *
690
 * Returns the direction the popup will be pointing at when popped up.
691
 *
Matthias Clasen's avatar
Matthias Clasen committed
692
 * Returns: a `GtkArrowType` value
693
694
695
696
697
698
 */
GtkArrowType
gtk_menu_button_get_direction (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), GTK_ARROW_DOWN);

699
  return menu_button->arrow_type;
700
701
702
}

static void
703
gtk_menu_button_dispose (GObject *object)
704
{
705
  GtkMenuButton *self = GTK_MENU_BUTTON (object);
706

707
  if (self->popover)
708
    {
709
      g_signal_handlers_disconnect_by_func (self->popover,
710
711
                                            menu_deactivate_cb,
                                            object);
712
      g_signal_handlers_disconnect_by_func (self->popover,
713
714
                                            popover_destroy_cb,
                                            object);
715
716
      gtk_widget_unparent (self->popover);
      self->popover = NULL;
717
718
    }

719
720
  g_clear_object (&self->model);
  g_clear_pointer (&self->button, gtk_widget_unparent);
721

722
723
  if (self->create_popup_destroy_notify)
    self->create_popup_destroy_notify (self->create_popup_user_data);
724

725
  G_OBJECT_CLASS (gtk_menu_button_parent_class)->dispose (object);
726
}
727
728

/**
729
 * gtk_menu_button_set_popover: (attributes org.gtk.Method.set_property=popover)
Matthias Clasen's avatar
Matthias Clasen committed
730
731
732
733
 * @menu_button: a `GtkMenuButton`
 * @popover: (nullable): a `GtkPopover`, or %NULL to unset and disable the button
 *
 * Sets the `GtkPopover` that will be popped up when the @menu_button is clicked.
734
 *
Matthias Clasen's avatar
Matthias Clasen committed
735
 * If @popover is %NULL, the button is disabled.
736
 *
Matthias Clasen's avatar
Matthias Clasen committed
737
738
 * If [property@Gtk.MenuButton:menu-model] is set, the menu model is dissociated
 * from the @menu_button, and the property is set to %NULL.
739
740
741
742
743
744
745
746
 */
void
gtk_menu_button_set_popover (GtkMenuButton *menu_button,
                             GtkWidget     *popover)
{
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
  g_return_if_fail (GTK_IS_POPOVER (popover) || popover == NULL);

747
748
  g_object_freeze_notify (G_OBJECT (menu_button));

749
  g_clear_object (&menu_button->model);
750

751
  if (menu_button->popover)
752
    {
753
754
      if (gtk_widget_get_visible (menu_button->popover))
        gtk_widget_hide (menu_button->popover);
755

756
      g_signal_handlers_disconnect_by_func (menu_button->popover,
757
758
                                            menu_deactivate_cb,
                                            menu_button);
759
      g_signal_handlers_disconnect_by_func (menu_button->popover,
760
761
                                            popover_destroy_cb,
                                            menu_button);
762

763
      gtk_widget_unparent (menu_button->popover);
764
765
    }

766
  menu_button->popover = popover;
767
768
769

  if (popover)
    {
770
771
      gtk_widget_set_parent (menu_button->popover, GTK_WIDGET (menu_button));
      g_signal_connect_swapped (menu_button->popover, "closed",
772
                                G_CALLBACK (menu_deactivate_cb), menu_button);
773
      g_signal_connect_swapped (menu_button->popover, "destroy",
774
                                G_CALLBACK (popover_destroy_cb), menu_button);
775
776
777
      update_popover_direction (menu_button);
    }

778
  update_sensitivity (menu_button);
779

780
781
  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]);
782
  g_object_thaw_notify (G_OBJECT (menu_button));
783
784
785
}

/**
786
 * gtk_menu_button_get_popover: (attributes org.gtk.Method.get_property=popover)
Matthias Clasen's avatar
Matthias Clasen committed
787
 * @menu_button: a `GtkMenuButton`
788
 *
Matthias Clasen's avatar
Matthias Clasen committed
789
790
791
 * Returns the `GtkPopover` that pops out of the button.
 *
 * If the button is not using a `GtkPopover`, this function
792
793
 * returns %NULL.
 *
Matthias Clasen's avatar
Matthias Clasen committed
794
 * Returns: (nullable) (transfer none): a `GtkPopover` or %NULL
795
796
797
798
799
800
 */
GtkPopover *
gtk_menu_button_get_popover (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

801
  return GTK_POPOVER (menu_button->popover);
802
}
803
804

/**
805
 * gtk_menu_button_set_icon_name: (attributes org.gtk.Method.set_property=icon-name)
Matthias Clasen's avatar
Matthias Clasen committed
806
 * @menu_button: a `GtkMenuButton`
807
808
809
810
811
812
813
814
815
816
 * @icon_name: the icon name
 *
 * Sets the name of an icon to show inside the menu button.
 */
void
gtk_menu_button_set_icon_name (GtkMenuButton *menu_button,
                               const char    *icon_name)
{
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

817
  gtk_button_set_icon_name (GTK_BUTTON (menu_button->button), icon_name);
818
819
820
821
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_ICON_NAME]);
}

/**
822
 * gtk_menu_button_get_icon_name: (attributes org.gtk.Method.get_property=icon-name)
Matthias Clasen's avatar
Matthias Clasen committed
823
 * @menu_button: a `GtkMenuButton`
824
825
826
827
828
829
830
831
832
833
 *
 * Gets the name of the icon shown in the button.
 *
 * Returns: the name of the icon shown in the button
 */
const char *
gtk_menu_button_get_icon_name (GtkMenuButton *menu_button)
{
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

834
  return gtk_button_get_icon_name (GTK_BUTTON (menu_button->button));
835
836
837
}

/**
838
 * gtk_menu_button_set_label: (attributes org.gtk.Method.set_property=label)
Matthias Clasen's avatar
Matthias Clasen committed
839
 * @menu_button: a `GtkMenuButton`
840
 * @label: the label
841
842
843
844
845
846
847
848
 *
 * Sets the label to show inside the menu button.
 */
void
gtk_menu_button_set_label (GtkMenuButton *menu_button,
                           const char    *label)
{
  GtkWidget *box;
849
  GtkWidget *label_widget;
850
  GtkWidget *arrow;
851
852
853
854

  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
855
856
857
858
  label_widget = gtk_label_new (label);
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));
  gtk_label_set_xalign (GTK_LABEL (label_widget), 0);
  gtk_label_set_use_underline (GTK_LABEL (label_widget),
859
                               gtk_button_get_use_underline (GTK_BUTTON (menu_button->button)));
860
  gtk_widget_set_hexpand (label_widget, TRUE);
861
  gtk_widget_set_halign (label_widget, GTK_ALIGN_CENTER);
862
  arrow = gtk_builtin_icon_new ("arrow");
863
  menu_button->arrow_widget = arrow;
864
  set_arrow_type (arrow, menu_button->arrow_type, menu_button->arrow_type != GTK_ARROW_NONE);
865
  gtk_box_append (GTK_BOX (box), label_widget);
866
  gtk_box_append (GTK_BOX (box), arrow);
867
  gtk_button_set_child (GTK_BUTTON (menu_button->button), box);
868
  menu_button->label_widget = label_widget;
869
870
871
872
873

  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_LABEL]);
}

/**
874
 * gtk_menu_button_get_label: (attributes org.gtk.Method.get_property=label)
Matthias Clasen's avatar
Matthias Clasen committed
875
 * @menu_button: a `GtkMenuButton`
876
877
878
879
880
881
882
883
884
885
886
887
 *
 * Gets the label shown in the button
 *
 * Returns: the label shown in the button
 */
const char *
gtk_menu_button_get_label (GtkMenuButton *menu_button)
{
  GtkWidget *child;

  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), NULL);

888
  child = gtk_button_get_child (GTK_BUTTON (menu_button->button));
889
890
891
892
893
894
895
896
  if (GTK_IS_BOX (child))
    {
      child = gtk_widget_get_first_child (child);
      return gtk_label_get_label (GTK_LABEL (child));
    }

  return NULL;
}
897
898

/**
899
 * gtk_menu_button_set_has_frame: (attributes org.gtk.Method.set_property=has-frame)
Matthias Clasen's avatar
Matthias Clasen committed
900
 * @menu_button: a `GtkMenuButton`
Matthias Clasen's avatar
Matthias Clasen committed
901
 * @has_frame: whether the button should have a visible frame
902
 *
Matthias Clasen's avatar
Matthias Clasen committed
903
 * Sets the style of the button.
904
905
 */
void
Matthias Clasen's avatar
Matthias Clasen committed
906
907
gtk_menu_button_set_has_frame (GtkMenuButton *menu_button,
                               gboolean       has_frame)
908
909
910
{
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

Matthias Clasen's avatar
Matthias Clasen committed
911
  if (gtk_button_get_has_frame (GTK_BUTTON (menu_button->button)) == has_frame)
912
913
    return;

Matthias Clasen's avatar
Matthias Clasen committed
914
  gtk_button_set_has_frame (GTK_BUTTON (menu_button->button), has_frame);
Matthias Clasen's avatar
Matthias Clasen committed
915
  g_object_notify_by_pspec (G_OBJECT (menu_button), menu_button_props[PROP_HAS_FRAME]);
916
917
918
}

/**
919
 * gtk_menu_button_get_has_frame: (attributes org.gtk.Method.get_property=has-frame)
Matthias Clasen's avatar
Matthias Clasen committed
920
 * @menu_button: a `GtkMenuButton`
921
 *
Matthias Clasen's avatar
Matthias Clasen committed
922
 * Returns whether the button has a frame.
923
 *
Matthias Clasen's avatar
Matthias Clasen committed
924
 * Returns: %TRUE if the button has a frame
925
 */
Matthias Clasen's avatar
Matthias Clasen committed
926
927
gboolean
gtk_menu_button_get_has_frame (GtkMenuButton *menu_button)
928
{
Matthias Clasen's avatar
Matthias Clasen committed
929
  g_return_val_if_fail (GTK_IS_MENU_BUTTON (menu_button), TRUE);
930

Matthias Clasen's avatar
Matthias Clasen committed
931
  return gtk_button_get_has_frame (GTK_BUTTON (menu_button->button));
932
933
}

934
935
/**
 * gtk_menu_button_popup:
Matthias Clasen's avatar
Matthias Clasen committed
936
 * @menu_button: a `GtkMenuButton`
937
938
939
940
941
942
943
944
 *
 * Pop up the menu.
 */
void
gtk_menu_button_popup (GtkMenuButton *menu_button)
{
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

945
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (menu_button->button), TRUE);
946
947
948
949
}

/**
 * gtk_menu_button_popdown:
Matthias Clasen's avatar
Matthias Clasen committed
950
 * @menu_button: a `GtkMenuButton`
951
952
953
954
955
956
957
958
 *
 * Dismiss the menu.
 */
void
gtk_menu_button_popdown (GtkMenuButton *menu_button)
{
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

959
  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (menu_button->button), FALSE);
960
}
961

962
963
/**
 * gtk_menu_button_set_create_popup_func:
Matthias Clasen's avatar
Matthias Clasen committed
964
965
 * @menu_button: a `GtkMenuButton`
 * @func: (nullable): function to call when a popup is about to
966
967
 *   be shown, but none has been provided via other means, or %NULL
 *   to reset to default behavior.
968
 * @user_data: (closure): user data to pass to @func.
969
970
971
 * @destroy_notify: (nullable): destroy notify for @user_data
 *
 * Sets @func to be called when a popup is about to be shown.
Matthias Clasen's avatar
Matthias Clasen committed
972
 *
973
974
 * @func should use one of
 *
Matthias Clasen's avatar
Matthias Clasen committed
975
976
 *  - [method@Gtk.MenuButton.set_popover]
 *  - [method@Gtk.MenuButton.set_menu_model]
977
 *
978
 * to set a popup for @menu_button.
979
980
 * If @func is non-%NULL, @menu_button will always be sensitive.
 *
Matthias Clasen's avatar
Matthias Clasen committed
981
982
 * Using this function will not reset the menu widget attached to
 * @menu_button. Instead, this can be done manually in @func.
983
984
985
986
987
988
989
990
991
 */
void
gtk_menu_button_set_create_popup_func (GtkMenuButton                *menu_button,
                                       GtkMenuButtonCreatePopupFunc  func,
                                       gpointer                      user_data,
                                       GDestroyNotify                destroy_notify)
{
  g_return_if_fail (GTK_IS_MENU_BUTTON (menu_button));

992
993
  if (menu_button->create_popup_destroy_notify)
    menu_button->create_popup_destroy_notify (menu_button->create_popup_user_data);