gtkassistant.c 67.1 KB
Newer Older
Matthias Clasen's avatar
Matthias Clasen committed
1
/*
Cody Russell's avatar
Cody Russell committed
2
 * GTK - The GIMP Toolkit
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 * Copyright (C) 1999  Red Hat, Inc.
 * Copyright (C) 2002  Anders Carlsson <andersca@gnu.org>
 * Copyright (C) 2003  Matthias Clasen <mclasen@redhat.com>
 * Copyright (C) 2005  Carlos Garnacho Parro <carlosg@gnome.org>
 *
 * All rights reserved.
 *
 * 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
Javier Jardón's avatar
Javier Jardón committed
21
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
22
23
 */

24
/**
Matthias Clasen's avatar
Matthias Clasen committed
25
26
 * GtkAssistant:
 *
Matthias Clasen's avatar
Matthias Clasen committed
27
 * `GtkAssistant` is used to represent a complex as a series of steps.
Matthias Clasen's avatar
Matthias Clasen committed
28
29
30
31
32
33
34
35
36
37
38
39
40
41
 *
 * ![An example GtkAssistant](assistant.png)
 *
 * Each step consists of one or more pages. `GtkAssistant` guides the user
 * through the pages, and controls the page flow to collect the data needed
 * for the operation.
 *
 * `GtkAssistant` handles which buttons to show and to make sensitive based
 * on page sequence knowledge and the [enum@Gtk.AssistantPageType] of each
 * page in addition to state information like the *completed* and *committed*
 * page statuses.
 *
 * If you have a case that doesn’t quite fit in `GtkAssistant`s way of
 * handling buttons, you can use the %GTK_ASSISTANT_PAGE_CUSTOM page
Matthias Clasen's avatar
Matthias Clasen committed
42
 * type and handle buttons yourself.
43
 *
Matthias Clasen's avatar
Matthias Clasen committed
44
 * `GtkAssistant` maintains a `GtkAssistantPage` object for each added
45
 * child, which holds additional per-child properties. You
Matthias Clasen's avatar
Matthias Clasen committed
46
47
 * obtain the `GtkAssistantPage` for a child with [method@Gtk.Assistant.get_page].
 *
48
 * # GtkAssistant as GtkBuildable
49
 *
Matthias Clasen's avatar
Matthias Clasen committed
50
 * The `GtkAssistant` implementation of the `GtkBuildable` interface
Matthias Clasen's avatar
Matthias Clasen committed
51
 * exposes the @action_area as internal children with the name
William Jon McCann's avatar
William Jon McCann committed
52
 * “action_area”.
Matthias Clasen's avatar
Matthias Clasen committed
53
 *
Matthias Clasen's avatar
Matthias Clasen committed
54
55
56
 * To add pages to an assistant in `GtkBuilder`, simply add it as a
 * child to the `GtkAssistant` object. If you need to set per-object
 * properties, create a `GtkAssistantPage` object explicitly, and
57
 * set the child widget as a property on it.
58
59
60
 *
 * # CSS nodes
 *
Matthias Clasen's avatar
Matthias Clasen committed
61
 * `GtkAssistant` has a single CSS node with the name window and style
62
 * class .assistant.
63
64
 */

Matthias Clasen's avatar
Matthias Clasen committed
65
66
67
68
69
70
/**
 * GtkAssistantPage:
 *
 * `GtkAssistantPage` is an auxiliary object used by `GtkAssistant.
 */

71
#include "config.h"
72
73
74

#include "gtkassistant.h"

75
#include "gtkbox.h"
76
77
#include "gtkbuildable.h"
#include "gtkbutton.h"
78
#include "gtkframe.h"
79
80
#include "gtkheaderbar.h"
#include "gtkintl.h"
81
82
#include "gtkimage.h"
#include "gtklabel.h"
83
84
#include "gtklistlistmodelprivate.h"
#include "gtkmaplistmodel.h"
85
#include "gtkprivate.h"
86
#include "gtksettings.h"
87
#include "gtksizegroup.h"
88
#include "gtksizerequest.h"
89
#include "gtkstack.h"
90
#include "gtktypebuiltins.h"
91

Matthias Clasen's avatar
Matthias Clasen committed
92
93
typedef struct _GtkAssistantPageClass   GtkAssistantPageClass;

94
95
struct _GtkAssistantPage
{
96
  GObject instance;
97
  GtkAssistantPageType type;
98
  guint      complete     : 1;
99
  guint      complete_set : 1;
100

Benjamin Otte's avatar
Benjamin Otte committed
101
  char *title;
102
103
104
105

  GtkWidget *page;
  GtkWidget *regular_title;
  GtkWidget *current_title;
106
107
};

108
109
110
111
112
struct _GtkAssistantPageClass
{
  GObjectClass parent_class;
};

Matthias Clasen's avatar
Matthias Clasen committed
113
114
115
116
117
118
typedef struct _GtkAssistantClass   GtkAssistantClass;

struct _GtkAssistant
{
  GtkWindow  parent;

119
120
121
122
123
124
125
  GtkWidget *cancel;
  GtkWidget *forward;
  GtkWidget *back;
  GtkWidget *apply;
  GtkWidget *close;
  GtkWidget *last;

126
127
  GtkWidget *sidebar;
  GtkWidget *content;
128
  GtkWidget *action_area;
129
  GtkWidget *headerbar;
Benjamin Otte's avatar
Benjamin Otte committed
130
  int use_header_bar;
131
  gboolean constructed;
132
133
134

  GList     *pages;
  GSList    *visited_pages;
135
  GtkAssistantPage *current_page;
136

137
138
  GtkSizeGroup *button_size_group;
  GtkSizeGroup *title_size_group;
139
140
141
142

  GtkAssistantPageFunc forward_function;
  gpointer forward_function_data;
  GDestroyNotify forward_data_destroy;
143

144
145
  GListModel *model;

Benjamin Otte's avatar
Benjamin Otte committed
146
  int extra_buttons;
147

148
  guint committed : 1;
149
150
};

151
152
153
154
155
156
157
158
159
160
struct _GtkAssistantClass
{
  GtkWindowClass parent_class;

  void (* prepare) (GtkAssistant *assistant, GtkWidget *page);
  void (* apply)   (GtkAssistant *assistant);
  void (* close)   (GtkAssistant *assistant);
  void (* cancel)  (GtkAssistant *assistant);
};

161
static void     gtk_assistant_dispose            (GObject           *object);
162
163
static void     gtk_assistant_map                (GtkWidget         *widget);
static void     gtk_assistant_unmap              (GtkWidget         *widget);
164
165
static gboolean gtk_assistant_close_request      (GtkWindow         *window);

166
static void     gtk_assistant_page_set_property  (GObject           *object,
Matthias Clasen's avatar
Matthias Clasen committed
167
168
169
                                                  guint              property_id,
                                                  const GValue      *value,
                                                  GParamSpec        *pspec);
170
static void     gtk_assistant_page_get_property  (GObject           *object,
Matthias Clasen's avatar
Matthias Clasen committed
171
172
173
                                                  guint              property_id,
                                                  GValue            *value,
                                                  GParamSpec        *pspec);
174

175
176
177
178
179
180
181
182
static void     gtk_assistant_buildable_interface_init   (GtkBuildableIface  *iface);
static void     gtk_assistant_buildable_add_child        (GtkBuildable       *buildable,
                                                          GtkBuilder         *builder,
                                                          GObject            *child,
                                                          const char         *type);
static gboolean gtk_assistant_buildable_custom_tag_start (GtkBuildable       *buildable,
                                                          GtkBuilder         *builder,
                                                          GObject            *child,
Benjamin Otte's avatar
Benjamin Otte committed
183
                                                          const char         *tagname,
184
185
186
187
188
                                                          GtkBuildableParser *parser,
                                                          gpointer           *data);
static void     gtk_assistant_buildable_custom_finished  (GtkBuildable       *buildable,
                                                          GtkBuilder         *builder,
                                                          GObject            *child,
Benjamin Otte's avatar
Benjamin Otte committed
189
                                                          const char         *tagname,
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
                                                          gpointer            user_data);

static GList*   find_page                                (GtkAssistant       *assistant,
                                                          GtkWidget          *page);
static void     on_assistant_close                       (GtkWidget          *widget,
                                                          GtkAssistant       *assistant);
static void     on_assistant_apply                       (GtkWidget          *widget,
                                                          GtkAssistant       *assistant);
static void     on_assistant_forward                     (GtkWidget          *widget,
                                                          GtkAssistant       *assistant);
static void     on_assistant_back                        (GtkWidget          *widget,
                                                          GtkAssistant       *assistant);
static void     on_assistant_cancel                      (GtkWidget          *widget,
                                                          GtkAssistant       *assistant);
static void     on_assistant_last                        (GtkWidget          *widget,
                                                          GtkAssistant       *assistant);

static int        gtk_assistant_add_page                 (GtkAssistant     *assistant,
                                                          GtkAssistantPage *page_info,
Benjamin Otte's avatar
Benjamin Otte committed
209
                                                          int               position);
210

211
212
213
enum
{
  CHILD_PROP_0,
214
  CHILD_PROP_CHILD,
215
216
  CHILD_PROP_PAGE_TYPE,
  CHILD_PROP_PAGE_TITLE,
217
218
  CHILD_PROP_PAGE_COMPLETE,
  CHILD_PROP_HAS_PADDING
219
220
};

221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
G_DEFINE_TYPE (GtkAssistantPage, gtk_assistant_page, G_TYPE_OBJECT)

static void
gtk_assistant_page_init (GtkAssistantPage *page)
{
  page->type = GTK_ASSISTANT_PAGE_CONTENT;
}

static void
gtk_assistant_page_finalize (GObject *object)
{
  GtkAssistantPage *page = GTK_ASSISTANT_PAGE (object);

  g_clear_object (&page->page);
  g_free (page->title);

  G_OBJECT_CLASS (gtk_assistant_page_parent_class)->finalize (object);
}

static void
gtk_assistant_page_class_init (GtkAssistantPageClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  object_class->finalize = gtk_assistant_page_finalize;
  object_class->get_property = gtk_assistant_page_get_property;
  object_class->set_property = gtk_assistant_page_set_property;

  /**
   * GtkAssistantPage:page-type:
   *
   * The type of the assistant page.
   */
  g_object_class_install_property (object_class,
                                   CHILD_PROP_PAGE_TYPE,
                                   g_param_spec_enum ("page-type",
                                                      P_("Page type"),
                                                      P_("The type of the assistant page"),
                                                      GTK_TYPE_ASSISTANT_PAGE_TYPE,
                                                      GTK_ASSISTANT_PAGE_CONTENT,
261
                                                      GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY));
262
263
264
265
266
267
268
269
270
271
272
273

  /**
   * GtkAssistantPage:title:
   *
   * The title of the page.
   */
  g_object_class_install_property (object_class,
                                   CHILD_PROP_PAGE_TITLE,
                                   g_param_spec_string ("title",
                                                        P_("Page title"),
                                                        P_("The title of the assistant page"),
                                                        NULL,
274
                                                        GTK_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY));
275
276
277
278

  /**
   * GtkAssistantPage:complete:
   *
Matthias Clasen's avatar
Matthias Clasen committed
279
280
281
282
   * Whether all required fields are filled in.
   *
   * GTK uses this information to control the sensitivity
   * of the navigation buttons.
283
284
285
286
287
288
289
   */
  g_object_class_install_property (object_class,
                                   CHILD_PROP_PAGE_COMPLETE,
                                   g_param_spec_boolean ("complete",
                                                         P_("Page complete"),
                                                         P_("Whether all required fields on the page have been filled out"),
                                                         FALSE,
290
                                                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY));
Matthias Clasen's avatar
Matthias Clasen committed
291
292
293
294
295
296

  /**
   * GtkAssistantPage:child:
   *
   * The child widget.
   */
297
298
299
300
301
302
303
304
305
  g_object_class_install_property (object_class,
                                   CHILD_PROP_CHILD,
                                   g_param_spec_object ("child",
                                                        P_("Child widget"),
                                                        P_("The content the assistant page"),
                                                        GTK_TYPE_WIDGET,
                                                        GTK_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}

306
307
308
309
310
311
enum
{
  CANCEL,
  PREPARE,
  APPLY,
  CLOSE,
312
  ESCAPE,
313
314
315
  LAST_SIGNAL
};

316
317
enum {
  PROP_0,
318
319
  PROP_USE_HEADER_BAR,
  PROP_PAGES
320
321
};

322
323
static guint signals [LAST_SIGNAL] = { 0 };

324
325
326
G_DEFINE_TYPE_WITH_CODE (GtkAssistant, gtk_assistant, GTK_TYPE_WINDOW,
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
                                                gtk_assistant_buildable_interface_init))
327

328
329
static void
set_use_header_bar (GtkAssistant *assistant,
Benjamin Otte's avatar
Benjamin Otte committed
330
                    int           use_header_bar)
331
332
333
334
{
  if (use_header_bar == -1)
    return;

335
  assistant->use_header_bar = use_header_bar;
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
}

static void
gtk_assistant_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  GtkAssistant *assistant = GTK_ASSISTANT (object);

  switch (prop_id)
    {
    case PROP_USE_HEADER_BAR:
      set_use_header_bar (assistant, g_value_get_int (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
gtk_assistant_get_property (GObject      *object,
                            guint         prop_id,
                            GValue       *value,
                            GParamSpec   *pspec)
{
  GtkAssistant *assistant = GTK_ASSISTANT (object);

  switch (prop_id)
    {
    case PROP_USE_HEADER_BAR:
369
      g_value_set_int (value, assistant->use_header_bar);
370
371
      break;

372
373
374
375
    case PROP_PAGES:
      g_value_set_object (value, gtk_assistant_get_pages (assistant));
      break;

376
377
378
379
380
381
382
383
384
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
apply_use_header_bar (GtkAssistant *assistant)
{
385
386
387
  gtk_widget_set_visible (assistant->action_area, !assistant->use_header_bar);
  gtk_widget_set_visible (assistant->headerbar, assistant->use_header_bar);
  if (!assistant->use_header_bar)
388
389
390
391
392
393
394
395
396
    gtk_window_set_titlebar (GTK_WINDOW (assistant), NULL);
}

static void
add_to_header_bar (GtkAssistant *assistant,
                   GtkWidget    *child)
{
  gtk_widget_set_valign (child, GTK_ALIGN_CENTER);

397
398
  if (child == assistant->back || child == assistant->cancel)
    gtk_header_bar_pack_start (GTK_HEADER_BAR (assistant->headerbar), child);
399
  else
400
    gtk_header_bar_pack_end (GTK_HEADER_BAR (assistant->headerbar), child);
401
402
403
404
405
}

static void
add_action_widgets (GtkAssistant *assistant)
{
406
407
  GList *children, *l;
  GtkWidget *child;
408

409
  if (assistant->use_header_bar)
410
    {
411
412
413
414
415
      children = NULL;
      for (child = gtk_widget_get_last_child (assistant->action_area);
           child != NULL;
           child = gtk_widget_get_prev_sibling (child))
        children = g_list_prepend (children, child);
416
417
418
419
      for (l = children; l != NULL; l = l->next)
        {
          gboolean has_default;

420
          child = l->data;
421
422
423
          has_default = gtk_widget_has_default (child);

          g_object_ref (child);
424
          gtk_box_remove (GTK_BOX (assistant->action_area), child);
425
426
427
428
429
          add_to_header_bar (assistant, child);
          g_object_unref (child);

          if (has_default)
            {
430
              gtk_window_set_default_widget (GTK_WINDOW (assistant), child);
Matthias Clasen's avatar
Matthias Clasen committed
431
              gtk_widget_add_css_class (child, "suggested-action");
432
433
434
435
436
437
            }
        }
      g_list_free (children);
    }
}

438
439
static void
gtk_assistant_constructed (GObject *object)
440
{
441
  GtkAssistant *assistant = GTK_ASSISTANT (object);
442

443
  G_OBJECT_CLASS (gtk_assistant_parent_class)->constructed (object);
444

445
446
447
  assistant->constructed = TRUE;
  if (assistant->use_header_bar == -1)
    assistant->use_header_bar = FALSE;
448
449
450
451

  add_action_widgets (assistant);
  apply_use_header_bar (assistant);
}
452

453
454
455
456
static void
escape_cb (GtkAssistant *assistant)
{
  /* Do not allow cancelling in the middle of a progress page */
457
458
459
  if (assistant->current_page &&
      (assistant->current_page->type != GTK_ASSISTANT_PAGE_PROGRESS ||
       assistant->current_page->complete))
460
461
462
463
464
465
    g_signal_emit (assistant, signals [CANCEL], 0, NULL);

  /* don't run any user handlers - this is not a public signal */
  g_signal_stop_emission (assistant, signals[ESCAPE], 0);
}

466
467
468
469
470
static void
gtk_assistant_finalize (GObject *object)
{
  GtkAssistant *assistant = GTK_ASSISTANT (object);

471
472
  if (assistant->model)
    g_object_remove_weak_pointer (G_OBJECT (assistant->model), (gpointer *)&assistant->model);
473
474
475
476

  G_OBJECT_CLASS (gtk_assistant_parent_class)->finalize (object);
}

477
478
479
480
481
static void
gtk_assistant_class_init (GtkAssistantClass *class)
{
  GObjectClass *gobject_class;
  GtkWidgetClass *widget_class;
482
  GtkWindowClass *window_class;
483
484
485

  gobject_class   = (GObjectClass *) class;
  widget_class    = (GtkWidgetClass *) class;
486
  window_class    = (GtkWindowClass *) class;
487

488
  gobject_class->dispose = gtk_assistant_dispose;
489
  gobject_class->finalize = gtk_assistant_finalize;
490
  gobject_class->constructed  = gtk_assistant_constructed;
491
492
493
  gobject_class->set_property = gtk_assistant_set_property;
  gobject_class->get_property = gtk_assistant_get_property;

494
495
  widget_class->map = gtk_assistant_map;
  widget_class->unmap = gtk_assistant_unmap;
496

497
498
  window_class->close_request = gtk_assistant_close_request;

499
500
  /**
   * GtkAssistant::cancel:
Matthias Clasen's avatar
Matthias Clasen committed
501
   * @assistant: the `GtkAssistant`
502
   *
Matthias Clasen's avatar
Matthias Clasen committed
503
   * Emitted when then the cancel button is clicked.
504
505
506
   */
  signals[CANCEL] =
    g_signal_new (I_("cancel"),
Matthias Clasen's avatar
Matthias Clasen committed
507
508
509
510
                  G_TYPE_FROM_CLASS (gobject_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GtkAssistantClass, cancel),
                  NULL, NULL,
511
                  NULL,
Matthias Clasen's avatar
Matthias Clasen committed
512
513
                  G_TYPE_NONE, 0);

514
515
  /**
   * GtkAssistant::prepare:
Matthias Clasen's avatar
Matthias Clasen committed
516
   * @assistant: the `GtkAssistant`
517
518
   * @page: the current page
   *
Matthias Clasen's avatar
Matthias Clasen committed
519
520
   * Emitted when a new page is set as the assistant's current page,
   * before making the new page visible.
521
522
523
   *
   * A handler for this signal can do any preparations which are
   * necessary before showing @page.
524
525
526
   */
  signals[PREPARE] =
    g_signal_new (I_("prepare"),
Matthias Clasen's avatar
Matthias Clasen committed
527
528
529
530
                  G_TYPE_FROM_CLASS (gobject_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GtkAssistantClass, prepare),
                  NULL, NULL,
531
                  NULL,
Matthias Clasen's avatar
Matthias Clasen committed
532
                  G_TYPE_NONE, 1, GTK_TYPE_WIDGET);
533
534
535

  /**
   * GtkAssistant::apply:
Matthias Clasen's avatar
Matthias Clasen committed
536
   * @assistant: the `GtkAssistant`
537
   *
Matthias Clasen's avatar
Matthias Clasen committed
538
   * Emitted when the apply button is clicked.
539
   *
Matthias Clasen's avatar
Matthias Clasen committed
540
   * The default behavior of the `GtkAssistant` is to switch to the page
541
   * after the current page, unless the current page is the last one.
542
   *
543
544
545
546
   * A handler for the ::apply signal should carry out the actions for
   * which the wizard has collected data. If the action takes a long time
   * to complete, you might consider putting a page of type
   * %GTK_ASSISTANT_PAGE_PROGRESS after the confirmation page and handle
Matthias Clasen's avatar
Matthias Clasen committed
547
548
   * this operation within the [signal@Gtk.Assistant::prepare] signal of
   * the progress page.
549
550
551
   */
  signals[APPLY] =
    g_signal_new (I_("apply"),
Matthias Clasen's avatar
Matthias Clasen committed
552
553
554
555
                  G_TYPE_FROM_CLASS (gobject_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GtkAssistantClass, apply),
                  NULL, NULL,
556
                  NULL,
Matthias Clasen's avatar
Matthias Clasen committed
557
                  G_TYPE_NONE, 0);
558
559
560

  /**
   * GtkAssistant::close:
Matthias Clasen's avatar
Matthias Clasen committed
561
   * @assistant: the `GtkAssistant`
562
   *
Matthias Clasen's avatar
Matthias Clasen committed
563
564
565
   * Emitted either when the close button of a summary page is clicked,
   * or when the apply button in the last page in the flow (of type
   * %GTK_ASSISTANT_PAGE_CONFIRM) is clicked.
566
567
568
   */
  signals[CLOSE] =
    g_signal_new (I_("close"),
Matthias Clasen's avatar
Matthias Clasen committed
569
570
571
572
                  G_TYPE_FROM_CLASS (gobject_class),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GtkAssistantClass, close),
                  NULL, NULL,
573
                  NULL,
Matthias Clasen's avatar
Matthias Clasen committed
574
                  G_TYPE_NONE, 0);
575

576
577
  /**
   * GtkAssistant::escape
Matthias Clasen's avatar
Matthias Clasen committed
578
   * @assistant: the `GtkAssistant`
579
580
581
   *
   * The action signal for the Escape binding.
   */
582
583
584
585
586
587
  signals[ESCAPE] =
    g_signal_new_class_handler (I_("escape"),
                                G_TYPE_FROM_CLASS (gobject_class),
                                G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
                                G_CALLBACK (escape_cb),
                                NULL, NULL,
588
                                NULL,
589
590
                                G_TYPE_NONE, 0);

591
592
593
594
  gtk_widget_class_add_binding_signal (widget_class,
                                       GDK_KEY_Escape, 0,
                                       "escape",
                                       NULL);
595

596
597
598
  /**
   * GtkAssistant:use-header-bar:
   *
Matthias Clasen's avatar
Matthias Clasen committed
599
   * %TRUE if the assistant uses a `GtkHeaderBar` for action buttons
600
601
602
603
604
605
606
607
608
609
610
611
612
   * instead of the action-area.
   *
   * For technical reasons, this property is declared as an integer
   * property, but you should only set it to %TRUE or %FALSE.
   */
  g_object_class_install_property (gobject_class,
                                   PROP_USE_HEADER_BAR,
                                   g_param_spec_int ("use-header-bar",
                                                     P_("Use Header Bar"),
                                                     P_("Use Header Bar for actions."),
                                                     -1, 1, -1,
                                                     GTK_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));

Matthias Clasen's avatar
Matthias Clasen committed
613
614
615
616
617
  /**
   * GtkAssistant:pages:
   *
   * `GListModel` containing the pages.
   */
618
619
620
621
  g_object_class_install_property (gobject_class,
                                   PROP_PAGES,
                                   g_param_spec_object ("pages",
                                                        P_("Pages"),
622
                                                        P_("The pages of the assistant."),
623
624
                                                        G_TYPE_LIST_MODEL,
                                                        GTK_PARAM_READABLE));
625

626
627
628
  /* Bind class to template
   */
  gtk_widget_class_set_template_from_resource (widget_class,
629
630
631
632
633
634
635
636
637
638
639
640
641
642
                                               "/org/gtk/libgtk/ui/gtkassistant.ui");

  gtk_widget_class_bind_template_child_internal (widget_class, GtkAssistant, action_area);
  gtk_widget_class_bind_template_child_internal (widget_class, GtkAssistant, headerbar);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, content);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, cancel);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, forward);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, back);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, apply);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, close);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, last);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, sidebar);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, button_size_group);
  gtk_widget_class_bind_template_child (widget_class, GtkAssistant, title_size_group);
643
644
645
646
647
648
649

  gtk_widget_class_bind_template_callback (widget_class, on_assistant_close);
  gtk_widget_class_bind_template_callback (widget_class, on_assistant_apply);
  gtk_widget_class_bind_template_callback (widget_class, on_assistant_forward);
  gtk_widget_class_bind_template_callback (widget_class, on_assistant_back);
  gtk_widget_class_bind_template_callback (widget_class, on_assistant_cancel);
  gtk_widget_class_bind_template_callback (widget_class, on_assistant_last);
650
651
}

Benjamin Otte's avatar
Benjamin Otte committed
652
653
static int
default_forward_function (int current_page, gpointer data)
654
{
655
  GtkAssistant *assistant = GTK_ASSISTANT (data);
656
657
658
  GtkAssistantPage *page_info;
  GList *page_node;

659
  page_node = g_list_nth (assistant->pages, ++current_page);
660
661
662
663
664
665

  if (!page_node)
    return -1;

  page_info = (GtkAssistantPage *) page_node->data;

666
  while (page_node && !gtk_widget_get_visible (page_info->page))
667
668
669
    {
      page_node = page_node->next;
      current_page++;
670
671

      if (page_node)
Matthias Clasen's avatar
Matthias Clasen committed
672
        page_info = (GtkAssistantPage *) page_node->data;
673
674
675
676
677
    }

  return current_page;
}

678
679
static gboolean
last_button_visible (GtkAssistant *assistant, GtkAssistantPage *page)
680
{
681
  GtkAssistantPage *page_info;
Benjamin Otte's avatar
Benjamin Otte committed
682
  int count, page_num, n_pages;
683

684
685
686
687
688
689
  if (page == NULL)
    return FALSE;

  if (page->type != GTK_ASSISTANT_PAGE_CONTENT)
    return FALSE;

690
  count = 0;
691
692
  page_num = g_list_index (assistant->pages, page);
  n_pages  = g_list_length (assistant->pages);
693
  page_info = page;
694

695
  while (page_num >= 0 && page_num < n_pages &&
Matthias Clasen's avatar
Matthias Clasen committed
696
697
698
         page_info->type == GTK_ASSISTANT_PAGE_CONTENT &&
         (count == 0 || page_info->complete) &&
         count < n_pages)
699
    {
700
701
      page_num = (assistant->forward_function) (page_num, assistant->forward_function_data);
      page_info = g_list_nth_data (assistant->pages, page_num);
702

703
704
705
      count++;
    }

706
  /* Make the last button visible if we can skip multiple
707
   * pages and end on a confirmation or summary page
708
   */
709
  if (count > 1 && page_info &&
710
711
      (page_info->type == GTK_ASSISTANT_PAGE_CONFIRM ||
       page_info->type == GTK_ASSISTANT_PAGE_SUMMARY))
712
    return TRUE;
713
  else
714
    return FALSE;
715
716
}

717
static void
718
update_actions_size (GtkAssistant *assistant)
719
{
720
721
  GList *l;
  GtkAssistantPage *page;
Benjamin Otte's avatar
Benjamin Otte committed
722
  int buttons, page_buttons;
723

724
  if (!assistant->current_page)
725
    return;
726

727
728
729
730
731
  /* Some heuristics to find out how many buttons we should
   * reserve space for. It is possible to trick this code
   * with page forward functions and invisible pages, etc.
   */
  buttons = 0;
732
  for (l = assistant->pages; l; l = l->next)
733
734
    {
      page = l->data;
735

736
737
738
739
      if (!gtk_widget_get_visible (page->page))
        continue;

      page_buttons = 2; /* cancel, forward/apply/close */
740
      if (l != assistant->pages)
741
742
743
744
745
746
747
        page_buttons += 1; /* back */
      if (last_button_visible (assistant, page))
        page_buttons += 1; /* last */

      buttons = MAX (buttons, page_buttons);
    }

748
  buttons += assistant->extra_buttons;
749

750
751
  gtk_widget_set_size_request (assistant->action_area,
                               buttons * gtk_widget_get_allocated_width (assistant->cancel) + (buttons - 1) * 6,
752
                               -1);
753
754
}

755
static void
756
compute_last_button_state (GtkAssistant *assistant)
757
{
758
759
760
  gtk_widget_set_sensitive (assistant->last, assistant->current_page->complete);
  if (last_button_visible (assistant, assistant->current_page))
    gtk_widget_show (assistant->last);
761
  else
762
    gtk_widget_hide (assistant->last);
763
764
765
}

static void
766
compute_progress_state (GtkAssistant *assistant)
767
{
Benjamin Otte's avatar
Benjamin Otte committed
768
  int page_num, n_pages;
769
770
771

  n_pages = gtk_assistant_get_n_pages (assistant);
  page_num = gtk_assistant_get_current_page (assistant);
772

773
  page_num = (assistant->forward_function) (page_num, assistant->forward_function_data);
774

775
  if (page_num >= 0 && page_num < n_pages)
776
    gtk_widget_show (assistant->forward);
777
  else
778
    gtk_widget_hide (assistant->forward);
779
780
781
}

static void
782
update_buttons_state (GtkAssistant *assistant)
783
{
784
  if (!assistant->current_page)
785
    return;
Matthias Clasen's avatar
Matthias Clasen committed
786

787
  switch (assistant->current_page->type)
788
789
    {
    case GTK_ASSISTANT_PAGE_INTRO:
790
791
792
793
794
795
796
      gtk_widget_set_sensitive (assistant->cancel, TRUE);
      gtk_widget_set_sensitive (assistant->forward, assistant->current_page->complete);
      gtk_window_set_default_widget (GTK_WINDOW (assistant), assistant->forward);
      gtk_widget_show (assistant->forward);
      gtk_widget_hide (assistant->back);
      gtk_widget_hide (assistant->apply);
      gtk_widget_hide (assistant->close);
797
798
799
      compute_last_button_state (assistant);
      break;
    case GTK_ASSISTANT_PAGE_CONFIRM:
800
801
802
803
804
805
806
807
808
      gtk_widget_set_sensitive (assistant->cancel, TRUE);
      gtk_widget_set_sensitive (assistant->back, TRUE);
      gtk_widget_set_sensitive (assistant->apply, assistant->current_page->complete);
      gtk_window_set_default_widget (GTK_WINDOW (assistant), assistant->apply);
      gtk_widget_show (assistant->back);
      gtk_widget_show (assistant->apply);
      gtk_widget_hide (assistant->forward);
      gtk_widget_hide (assistant->close);
      gtk_widget_hide (assistant->last);
809
810
      break;
    case GTK_ASSISTANT_PAGE_CONTENT:
811
812
813
814
815
816
817
818
      gtk_widget_set_sensitive (assistant->cancel, TRUE);
      gtk_widget_set_sensitive (assistant->back, TRUE);
      gtk_widget_set_sensitive (assistant->forward, assistant->current_page->complete);
      gtk_window_set_default_widget (GTK_WINDOW (assistant), assistant->forward);
      gtk_widget_show (assistant->back);
      gtk_widget_show (assistant->forward);
      gtk_widget_hide (assistant->apply);
      gtk_widget_hide (assistant->close);
819
820
821
      compute_last_button_state (assistant);
      break;
    case GTK_ASSISTANT_PAGE_SUMMARY:
822
823
824
825
826
827
828
      gtk_widget_set_sensitive (assistant->close, assistant->current_page->complete);
      gtk_window_set_default_widget (GTK_WINDOW (assistant), assistant->close);
      gtk_widget_show (assistant->close);
      gtk_widget_hide (assistant->back);
      gtk_widget_hide (assistant->forward);
      gtk_widget_hide (assistant->apply);
      gtk_widget_hide (assistant->last);
829
830
      break;
    case GTK_ASSISTANT_PAGE_PROGRESS:
831
832
833
834
835
836
837
838
      gtk_widget_set_sensitive (assistant->cancel, assistant->current_page->complete);
      gtk_widget_set_sensitive (assistant->back, assistant->current_page->complete);
      gtk_widget_set_sensitive (assistant->forward, assistant->current_page->complete);
      gtk_window_set_default_widget (GTK_WINDOW (assistant), assistant->forward);
      gtk_widget_show (assistant->back);
      gtk_widget_hide (assistant->apply);
      gtk_widget_hide (assistant->close);
      gtk_widget_hide (assistant->last);
839
      compute_progress_state (assistant);
840
      break;
841
    case GTK_ASSISTANT_PAGE_CUSTOM:
842
843
844
845
846
847
      gtk_widget_hide (assistant->cancel);
      gtk_widget_hide (assistant->back);
      gtk_widget_hide (assistant->forward);
      gtk_widget_hide (assistant->apply);
      gtk_widget_hide (assistant->last);
      gtk_widget_hide (assistant->close);
848
      break;
849
850
851
852
    default:
      g_assert_not_reached ();
    }

853
854
855
856
857
  if (assistant->committed)
    gtk_widget_hide (assistant->cancel);
  else if (assistant->current_page->type == GTK_ASSISTANT_PAGE_SUMMARY ||
           assistant->current_page->type == GTK_ASSISTANT_PAGE_CUSTOM)
    gtk_widget_hide (assistant->cancel);
858
  else
859
    gtk_widget_show (assistant->cancel);
860

861
  /* this is quite general, we don't want to
Matthias Clasen's avatar
Matthias Clasen committed
862
863
   * go back if it's the first page
   */
864
865
  if (!assistant->visited_pages)
    gtk_widget_hide (assistant->back);
866
867
}

868
869
static gboolean
update_page_title_state (GtkAssistant *assistant, GList *list)
870
{
871
872
873
  GtkAssistantPage *page, *other;
  gboolean visible;
  GList *l;
874

875
  page = list->data;
876

877
878
879
880
  if (page->title == NULL || page->title[0] == 0)
    visible = FALSE;
  else
    visible = gtk_widget_get_visible (page->page);
881

882
  if (page == assistant->current_page)
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
    {
      gtk_widget_set_visible (page->regular_title, FALSE);
      gtk_widget_set_visible (page->current_title, visible);
    }
  else
    {
      /* If multiple consecutive pages have the same title,
       * we only show it once, since it would otherwise look
       * silly. We have to be a little careful, since we
       * _always_ show the title of the current page.
       */
      if (list->prev)
        {
          other = list->prev->data;
          if (g_strcmp0 (page->title, other->title) == 0)
            visible = FALSE;
        }
      for (l = list->next; l; l = l->next)
        {
          other = l->data;
          if (g_strcmp0 (page->title, other->title) != 0)
            break;
905

906
          if (other == assistant->current_page)
907
908
909
910
911
            {
              visible = FALSE;
              break;
            }
        }
912

913
914
      gtk_widget_set_visible (page->regular_title, visible);
      gtk_widget_set_visible (page->current_title, FALSE);
915
    }
Matthias Clasen's avatar
Matthias Clasen committed
916

917
918
919
920
921
922
923
924
925
926
  return visible;
}

static void
update_title_state (GtkAssistant *assistant)
{
  GList *l;
  gboolean show_titles;

  show_titles = FALSE;
927
  for (l = assistant->pages; l != NULL; l = l->next)
928
    {
929
930
      if (update_page_title_state (assistant, l))
        show_titles = TRUE;
931
932
    }

933
  gtk_widget_set_visible (assistant->sidebar, show_titles);
934
935
936
937
}

static void
set_current_page (GtkAssistant *assistant,
Benjamin Otte's avatar
Benjamin Otte committed
938
                  int           page_num)
939
{
940
  assistant->current_page = (GtkAssistantPage *)g_list_nth_data (assistant->pages, page_num);
941

942
  g_signal_emit (assistant, signals [PREPARE], 0, assistant->current_page->page);
943
944
  /* do not continue if the prepare signal handler has already changed the
   * current page */
945
  if (assistant->current_page != (GtkAssistantPage *)g_list_nth_data (assistant->pages, page_num))
946
    return;
947
948
949

  update_title_state (assistant);

950
  gtk_window_set_title (GTK_WINDOW (assistant), assistant->current_page->title);
951

952
  gtk_stack_set_visible_child (GTK_STACK (assistant->content), assistant->current_page->page);
953
954
955
956
957

  /* update buttons state, flow may have changed */
  if (gtk_widget_get_mapped (GTK_WIDGET (assistant)))
    update_buttons_state (assistant);

958
  if (!gtk_widget_child_focus (assistant->current_page->page, GTK_DIR_TAB_FORWARD))
959
960
    {
      GtkWidget *button[6];
Benjamin Otte's avatar
Benjamin Otte committed
961
      int i;
962
963

      /* find the best button to focus */
964
965
966
967
968
969
      button[0] = assistant->apply;
      button[1] = assistant->close;
      button[2] = assistant->forward;
      button[3] = assistant->back;
      button[4] = assistant->cancel;
      button[5] = assistant->last;
970
971
      for (i = 0; i < 6; i++)
        {
Matthias Clasen's avatar
Matthias Clasen committed
972
973
          if (gtk_widget_get_visible (button[i]) &&
              gtk_widget_get_sensitive (button[i]))
974
975
976
977
978
979
            {
              gtk_widget_grab_focus (button[i]);
              break;
            }
        }
    }
980
981
}

Benjamin Otte's avatar
Benjamin Otte committed
982
static int
983
984
985
compute_next_step (GtkAssistant *assistant)
{
  GtkAssistantPage *page_info;
Benjamin Otte's avatar
Benjamin Otte committed
986
  int current_page, n_pages, next_page;
987
988

  current_page = gtk_assistant_get_current_page (assistant);
989
  page_info = assistant->current_page;
990
991
  n_pages = gtk_assistant_get_n_pages (assistant);

992
993
  next_page = (assistant->forward_function) (current_page,
                                        assistant->forward_function_data);
994
995
996

  if (next_page >= 0 && next_page < n_pages)
    {
997
      assistant->visited_pages = g_slist_prepend (assistant->visited_pages, page_info);
998
      set_current_page (assistant, next_page);
999
1000
1001
1002
1003
1004
1005
1006

      return TRUE;
    }

  return FALSE;
}

static void
1007
1008
on_assistant_close (GtkWidget    *widget,
                    GtkAssistant *assistant)
1009
1010
1011
1012
1013
{
  g_signal_emit (assistant, signals [CLOSE], 0, NULL);
}

static void
1014
1015
on_assistant_apply (GtkWidget    *widget,
                    GtkAssistant *assistant)
1016
1017
1018
{
  gboolean success;

Matthias Clasen's avatar
Matthias Clasen committed
1019
  g_signal_emit (assistant, signals [APPLY], 0);
1020

1021
1022
  success = compute_next_step (assistant);

1023
1024
1025
1026
  /* if the assistant hasn't switched to another page, just emit
   * the CLOSE signal, it't the last page in the assistant flow
   */
  if (!success)
Matthias Clasen's avatar
Matthias Clasen committed
1027
    g_signal_emit (assistant, signals [CLOSE], 0);
1028
1029
1030
}

static void
1031
1032
on_assistant_forward (GtkWidget    *widget,
                      GtkAssistant *assistant)
1033
{
1034
  gtk_assistant_next_page (assistant);
1035
1036
1037
}

static void
1038
1039
on_assistant_back (GtkWidget    *widget,
                   GtkAssistant *assistant)
1040
{
1041
  gtk_assistant_previous_page (assistant);
1042
1043
1044
}

static void
1045
1046
on_assistant_cancel (GtkWidget    *widget,
                     GtkAssistant *assistant)
1047
1048
1049
1050
1051
{
  g_signal_emit (assistant, signals [CANCEL], 0, NULL);
}

static void
1052
1053
on_assistant_last (GtkWidget    *widget,
                   GtkAssistant *assistant)
1054
{
1055
1056
  while (assistant->current_page->type == GTK_ASSISTANT_PAGE_CONTENT &&
         assistant->current_page->complete)
1057
1058
1059
1060
1061
1062
1063
1064
    compute_next_step (assistant);
}

static gboolean
alternative_button_order (GtkAssistant *assistant)
{
  gboolean result;

1065
  g_object_get (gtk_widget_get_settings (GTK_WIDGET (assistant)),
Matthias Clasen's avatar
Matthias Clasen committed
1066
1067
                "gtk-alternative-button-order", &result,
                NULL);
1068
1069
1070
  return result;
}

1071
static void
1072
1073
1074
on_page_page_notify (GtkWidget  *widget,
                     GParamSpec *arg,
                     gpointer    data)
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
{
  GtkAssistant *assistant = GTK_ASSISTANT (data);

  if (gtk_widget_get_mapped (GTK_WIDGET (assistant)))
    {
      update_buttons_state (assistant);
      update_title_state (assistant);
    }
}

1085
1086
1087
1088
1089
1090
1091
1092
1093
static void
on_page_notify (GtkAssistantPage *page,
                GParamSpec *arg,
                gpointer    data)
{
  if (page->page)
    on_page_page_notify (page->page, arg, data);
}

1094
static void
1095
1096
assistant_remove_page (GtkAssistant *assistant,
                       GtkWidget    *page)
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
{
  GtkAssistantPage *page_info;
  GList *page_node;
  GList *element;

  element = find_page (assistant, page);
  if (!element)
    return;

  page_info = element->data;

  /* If this is the current page, we need to switch away. */
1109
  if (page_info == assistant->current_page)
1110
1111
1112
1113
1114
1115
    {
      if (!compute_next_step (assistant))
        {
          /* The best we can do at this point is probably to pick
           * the first visible page.
           */
1116
          page_node = assistant->pages;
1117
1118
1119
1120
1121
1122
1123
1124
1125

          while (page_node &&
                 !gtk_widget_get_visible (((GtkAssistantPage *) page_node->data)->page))
            page_node = page_node->next;

          if (page_node == element)
            page_node = page_node->next;

          if (page_node)
1126
            assistant->current_page = page_node->data;
1127
          else
1128
            assistant->current_page = NULL;
1129
1130
1131
        }
    }

1132
1133
  g_signal_handlers_disconnect_by_func (page_info->page, on_page_page_notify, assistant);
  g_signal_handlers_disconnect_by_func (page_info, on_page_notify, assistant);
1134

1135
1136
  gtk_size_group_remove_widget (assistant->title_size_group, page_info->regular_title);
  gtk_size_group_remove_widget (assistant->title_size_group, page_info->current_title);
1137

1138
1139
  gtk_box_remove (GTK_BOX (assistant->sidebar), page_info->regular_title);
  gtk_box_remove (GTK_BOX (assistant->sidebar), page_info->current_title);
1140

1141
1142
  assistant->pages = g_list_remove_link (assistant->pages, element);
  assistant->visited_pages = g_slist_remove_all (assistant->visited_pages, page_info);