gtkstacksidebar.c 15.3 KB
Newer Older
Ikey Doherty's avatar
Ikey Doherty committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/*
 * Copyright (c) 2014 Intel Corporation
 *
 * This program 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 program 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 program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Author:
 *      Ikey Doherty <michael.i.doherty@intel.com>
 */

#include "config.h"

24
#include "gtkstacksidebar.h"
25 26

#include "gtklabel.h"
Ikey Doherty's avatar
Ikey Doherty committed
27
#include "gtklistbox.h"
28
#include "gtkscrolledwindow.h"
Ikey Doherty's avatar
Ikey Doherty committed
29 30 31 32 33 34
#include "gtkseparator.h"
#include "gtkstylecontext.h"
#include "gtkprivate.h"
#include "gtkintl.h"

/**
35 36
 * SECTION:gtkstacksidebar
 * @Title: GtkStackSidebar
37
 * @Short_description: An automatic sidebar widget
Ikey Doherty's avatar
Ikey Doherty committed
38
 *
39 40
 * A GtkStackSidebar enables you to quickly and easily provide a
 * consistent "sidebar" object for your user interface.
Ikey Doherty's avatar
Ikey Doherty committed
41
 *
42 43 44 45
 * In order to use a GtkStackSidebar, you simply use a GtkStack to
 * organize your UI flow, and add the sidebar to your sidebar area. You
 * can use gtk_stack_sidebar_set_stack() to connect the #GtkStackSidebar
 * to the #GtkStack.
Ikey Doherty's avatar
Ikey Doherty committed
46
 *
47 48
 * # CSS nodes
 *
49 50 51
 * GtkStackSidebar has a single CSS node with name stacksidebar and
 * style class .sidebar.
 *
52
 * When circumstances require it, GtkStackSidebar adds the
53 54
 * .needs-attention style class to the widgets representing the stack
 * pages.
55
 *
Ikey Doherty's avatar
Ikey Doherty committed
56 57
 * Since: 3.16
 */
58

59
struct _GtkStackSidebarPrivate
Ikey Doherty's avatar
Ikey Doherty committed
60
{
61
  GtkListBox *list;
Ikey Doherty's avatar
Ikey Doherty committed
62 63 64 65 66
  GtkStack *stack;
  GHashTable *rows;
  gboolean in_child_changed;
};

67
G_DEFINE_TYPE_WITH_PRIVATE (GtkStackSidebar, gtk_stack_sidebar, GTK_TYPE_BIN)
Ikey Doherty's avatar
Ikey Doherty committed
68 69 70 71 72 73 74 75 76 77

enum
{
  PROP_0,
  PROP_STACK,
  N_PROPERTIES
};
static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };

static void
78 79 80 81
gtk_stack_sidebar_set_property (GObject    *object,
                                guint       prop_id,
                                const       GValue *value,
                                GParamSpec *pspec)
Ikey Doherty's avatar
Ikey Doherty committed
82 83 84 85
{
  switch (prop_id)
    {
    case PROP_STACK:
86
      gtk_stack_sidebar_set_stack (GTK_STACK_SIDEBAR (object), g_value_get_object (value));
Ikey Doherty's avatar
Ikey Doherty committed
87 88 89 90 91 92 93 94 95
      break;

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

static void
96 97 98 99
gtk_stack_sidebar_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
Ikey Doherty's avatar
Ikey Doherty committed
100
{
101
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (GTK_STACK_SIDEBAR (object));
Ikey Doherty's avatar
Ikey Doherty committed
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133

  switch (prop_id)
    {
    case PROP_STACK:
      g_value_set_object (value, priv->stack);
      break;

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

static void
update_header (GtkListBoxRow *row,
               GtkListBoxRow *before,
               gpointer       userdata)
{
  GtkWidget *ret = NULL;

  if (before && !gtk_list_box_row_get_header (row))
    {
      ret = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
      gtk_list_box_row_set_header (row, ret);
    }
}

static gint
sort_list (GtkListBoxRow *row1,
           GtkListBoxRow *row2,
           gpointer       userdata)
{
134 135
  GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (userdata);
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
  GtkWidget *item;
  GtkWidget *widget;
  gint left = 0; gint right = 0;


  if (row1)
    {
      item = gtk_bin_get_child (GTK_BIN (row1));
      widget = g_object_get_data (G_OBJECT (item), "stack-child");
      gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
                               "position", &left,
                               NULL);
    }

  if (row2)
    {
      item = gtk_bin_get_child (GTK_BIN (row2));
      widget = g_object_get_data (G_OBJECT (item), "stack-child");
      gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
                               "position", &right,
                               NULL);
    }

  if (left < right)
    return  -1;

  if (left == right)
    return 0;

  return 1;
}

static void
169 170 171
gtk_stack_sidebar_row_selected (GtkListBox    *box,
                                GtkListBoxRow *row,
                                gpointer       userdata)
Ikey Doherty's avatar
Ikey Doherty committed
172
{
173 174
  GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (userdata);
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
  GtkWidget *item;
  GtkWidget *widget;

  if (priv->in_child_changed)
    return;

  if (!row)
    return;

  item = gtk_bin_get_child (GTK_BIN (row));
  widget = g_object_get_data (G_OBJECT (item), "stack-child");
  gtk_stack_set_visible_child (priv->stack, widget);
}

static void
190
gtk_stack_sidebar_init (GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
191 192
{
  GtkStyleContext *style;
193
  GtkStackSidebarPrivate *priv;
194
  GtkWidget *sw;
Ikey Doherty's avatar
Ikey Doherty committed
195

196
  priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
197

198 199 200 201 202 203
  sw = gtk_scrolled_window_new (NULL, NULL);
  gtk_widget_show (sw);
  gtk_widget_set_no_show_all (sw, TRUE);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw),
                                  GTK_POLICY_NEVER,
                                  GTK_POLICY_AUTOMATIC);
204

205
  gtk_container_add (GTK_CONTAINER (sidebar), sw);
206 207

  priv->list = GTK_LIST_BOX (gtk_list_box_new ());
208
  gtk_widget_show (GTK_WIDGET (priv->list));
209 210

  gtk_container_add (GTK_CONTAINER (sw), GTK_WIDGET (priv->list));
211 212 213 214 215

  gtk_list_box_set_header_func (priv->list, update_header, sidebar, NULL);
  gtk_list_box_set_sort_func (priv->list, sort_list, sidebar, NULL);

  g_signal_connect (priv->list, "row-selected",
216
                    G_CALLBACK (gtk_stack_sidebar_row_selected), sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
217 218 219 220 221 222 223 224

  style = gtk_widget_get_style_context (GTK_WIDGET (sidebar));
  gtk_style_context_add_class (style, "sidebar");

  priv->rows = g_hash_table_new (NULL, NULL);
}

static void
225 226 227
update_row (GtkStackSidebar *sidebar,
            GtkWidget       *widget,
            GtkWidget       *row)
Ikey Doherty's avatar
Ikey Doherty committed
228
{
229
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
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
  GtkWidget *item;
  gchar *title;
  gboolean needs_attention;
  GtkStyleContext *context;

  gtk_container_child_get (GTK_CONTAINER (priv->stack), widget,
                           "title", &title,
                           "needs-attention", &needs_attention,
                           NULL);

  item = gtk_bin_get_child (GTK_BIN (row));
  gtk_label_set_text (GTK_LABEL (item), title);

  gtk_widget_set_visible (row, gtk_widget_get_visible (widget) && title != NULL);

  context = gtk_widget_get_style_context (row);
  if (needs_attention)
     gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
  else
    gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);

  g_free (title);
}

static void
255 256 257
on_position_updated (GtkWidget       *widget,
                     GParamSpec      *pspec,
                     GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
258
{
259
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
260 261

  gtk_list_box_invalidate_sort (priv->list);
Ikey Doherty's avatar
Ikey Doherty committed
262 263 264
}

static void
265 266 267
on_child_updated (GtkWidget       *widget,
                  GParamSpec      *pspec,
                  GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
268
{
269
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
270 271 272 273 274 275 276
  GtkWidget *row;

  row = g_hash_table_lookup (priv->rows, widget);
  update_row (sidebar, widget, row);
}

static void
277 278
add_child (GtkWidget       *widget,
           GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
279
{
280
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  GtkWidget *item;
  GtkWidget *row;

  /* Check we don't actually already know about this widget */
  if (g_hash_table_lookup (priv->rows, widget))
    return;

  /* Make a pretty item when we add kids */
  item = gtk_label_new ("");
  gtk_widget_set_halign (item, GTK_ALIGN_START);
  gtk_widget_set_valign (item, GTK_ALIGN_CENTER);
  row = gtk_list_box_row_new ();
  gtk_container_add (GTK_CONTAINER (row), item);
  gtk_widget_show (item);

  update_row (sidebar, widget, row);

  /* Hook up for events */
  g_signal_connect (widget, "child-notify::title",
                    G_CALLBACK (on_child_updated), sidebar);
  g_signal_connect (widget, "child-notify::needs-attention",
                    G_CALLBACK (on_child_updated), sidebar);
  g_signal_connect (widget, "notify::visible",
                    G_CALLBACK (on_child_updated), sidebar);
  g_signal_connect (widget, "child-notify::position",
                    G_CALLBACK (on_position_updated), sidebar);

  g_object_set_data (G_OBJECT (item), "stack-child", widget);
  g_hash_table_insert (priv->rows, widget, row);
310
  gtk_container_add (GTK_CONTAINER (priv->list), row);
Ikey Doherty's avatar
Ikey Doherty committed
311 312 313
}

static void
314 315
remove_child (GtkWidget       *widget,
              GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
316
{
317
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
318 319 320 321 322 323 324 325 326
  GtkWidget *row;

  row = g_hash_table_lookup (priv->rows, widget);
  if (!row)
    return;

  g_signal_handlers_disconnect_by_func (widget, on_child_updated, sidebar);
  g_signal_handlers_disconnect_by_func (widget, on_position_updated, sidebar);

327
  gtk_container_remove (GTK_CONTAINER (priv->list), row);
Ikey Doherty's avatar
Ikey Doherty committed
328 329 330 331
  g_hash_table_remove (priv->rows, widget);
}

static void
332
populate_sidebar (GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
333
{
334
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
335
  GtkWidget *widget, *row;
Ikey Doherty's avatar
Ikey Doherty committed
336 337

  gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)add_child, sidebar);
338 339 340 341 342 343 344

  widget = gtk_stack_get_visible_child (priv->stack);
  if (widget)
    {
      row = g_hash_table_lookup (priv->rows, widget);
      gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row));
    }
Ikey Doherty's avatar
Ikey Doherty committed
345 346 347
}

static void
348
clear_sidebar (GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
349
{
350
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
351 352 353 354 355

  gtk_container_foreach (GTK_CONTAINER (priv->stack), (GtkCallback)remove_child, sidebar);
}

static void
356 357 358
on_child_changed (GtkWidget       *widget,
                  GParamSpec      *pspec,
                  GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
359
{
360
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
361 362 363 364 365 366 367 368
  GtkWidget *child;
  GtkWidget *row;

  child = gtk_stack_get_visible_child (GTK_STACK (widget));
  row = g_hash_table_lookup (priv->rows, child);
  if (row != NULL)
    {
      priv->in_child_changed = TRUE;
369
      gtk_list_box_select_row (priv->list, GTK_LIST_BOX_ROW (row));
Ikey Doherty's avatar
Ikey Doherty committed
370 371 372 373 374
      priv->in_child_changed = FALSE;
    }
}

static void
375 376 377
on_stack_child_added (GtkContainer    *container,
                      GtkWidget       *widget,
                      GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
378 379 380 381 382
{
  add_child (widget, sidebar);
}

static void
383 384 385
on_stack_child_removed (GtkContainer    *container,
                        GtkWidget       *widget,
                        GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
386 387 388 389 390
{
  remove_child (widget, sidebar);
}

static void
391
disconnect_stack_signals (GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
392
{
393
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
394 395 396 397 398 399 400 401

  g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_added, sidebar);
  g_signal_handlers_disconnect_by_func (priv->stack, on_stack_child_removed, sidebar);
  g_signal_handlers_disconnect_by_func (priv->stack, on_child_changed, sidebar);
  g_signal_handlers_disconnect_by_func (priv->stack, disconnect_stack_signals, sidebar);
}

static void
402
connect_stack_signals (GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
403
{
404
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
405 406 407 408 409 410 411 412 413 414 415 416

  g_signal_connect_after (priv->stack, "add",
                          G_CALLBACK (on_stack_child_added), sidebar);
  g_signal_connect_after (priv->stack, "remove",
                          G_CALLBACK (on_stack_child_removed), sidebar);
  g_signal_connect (priv->stack, "notify::visible-child",
                    G_CALLBACK (on_child_changed), sidebar);
  g_signal_connect_swapped (priv->stack, "destroy",
                            G_CALLBACK (disconnect_stack_signals), sidebar);
}

static void
417
gtk_stack_sidebar_dispose (GObject *object)
Ikey Doherty's avatar
Ikey Doherty committed
418
{
419
  GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (object);
Ikey Doherty's avatar
Ikey Doherty committed
420

421
  gtk_stack_sidebar_set_stack (sidebar, NULL);
Ikey Doherty's avatar
Ikey Doherty committed
422

423
  G_OBJECT_CLASS (gtk_stack_sidebar_parent_class)->dispose (object);
Ikey Doherty's avatar
Ikey Doherty committed
424 425 426
}

static void
427
gtk_stack_sidebar_finalize (GObject *object)
Ikey Doherty's avatar
Ikey Doherty committed
428
{
429 430
  GtkStackSidebar *sidebar = GTK_STACK_SIDEBAR (object);
  GtkStackSidebarPrivate *priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
431 432 433

  g_hash_table_destroy (priv->rows);

434
  G_OBJECT_CLASS (gtk_stack_sidebar_parent_class)->finalize (object);
Ikey Doherty's avatar
Ikey Doherty committed
435 436 437
}

static void
438
gtk_stack_sidebar_class_init (GtkStackSidebarClass *klass)
Ikey Doherty's avatar
Ikey Doherty committed
439 440
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
441
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
Ikey Doherty's avatar
Ikey Doherty committed
442

443 444 445 446
  object_class->dispose = gtk_stack_sidebar_dispose;
  object_class->finalize = gtk_stack_sidebar_finalize;
  object_class->set_property = gtk_stack_sidebar_set_property;
  object_class->get_property = gtk_stack_sidebar_get_property;
Ikey Doherty's avatar
Ikey Doherty committed
447 448

  obj_properties[PROP_STACK] =
449
      g_param_spec_object (I_("stack"), P_("Stack"),
450
                           P_("Associated stack for this GtkStackSidebar"),
451 452
                           GTK_TYPE_STACK,
                           G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);
Ikey Doherty's avatar
Ikey Doherty committed
453 454

  g_object_class_install_properties (object_class, N_PROPERTIES, obj_properties);
455 456

  gtk_widget_class_set_css_name (widget_class, "stacksidebar");
Ikey Doherty's avatar
Ikey Doherty committed
457 458 459
}

/**
460
 * gtk_stack_sidebar_new:
Ikey Doherty's avatar
Ikey Doherty committed
461 462 463
 *
 * Creates a new sidebar.
 *
464
 * Returns: the new #GtkStackSidebar
Ikey Doherty's avatar
Ikey Doherty committed
465 466 467 468
 *
 * Since: 3.16
 */
GtkWidget *
469
gtk_stack_sidebar_new (void)
Ikey Doherty's avatar
Ikey Doherty committed
470
{
471
  return GTK_WIDGET (g_object_new (GTK_TYPE_STACK_SIDEBAR, NULL));
Ikey Doherty's avatar
Ikey Doherty committed
472 473 474
}

/**
475 476
 * gtk_stack_sidebar_set_stack:
 * @sidebar: a #GtkStackSidebar
Ikey Doherty's avatar
Ikey Doherty committed
477 478
 * @stack: a #GtkStack
 *
479
 * Set the #GtkStack associated with this #GtkStackSidebar.
Ikey Doherty's avatar
Ikey Doherty committed
480 481 482 483 484 485 486
 *
 * The sidebar widget will automatically update according to the order
 * (packing) and items within the given #GtkStack.
 *
 * Since: 3.16
 */
void
487 488
gtk_stack_sidebar_set_stack (GtkStackSidebar *sidebar,
                             GtkStack        *stack)
Ikey Doherty's avatar
Ikey Doherty committed
489
{
490
  GtkStackSidebarPrivate *priv;
Ikey Doherty's avatar
Ikey Doherty committed
491

492
  g_return_if_fail (GTK_IS_STACK_SIDEBAR (sidebar));
Ikey Doherty's avatar
Ikey Doherty committed
493 494
  g_return_if_fail (GTK_IS_STACK (stack) || stack == NULL);

495
  priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518

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

  if (priv->stack)
    {
      disconnect_stack_signals (sidebar);
      clear_sidebar (sidebar);
      g_clear_object (&priv->stack);
    }
  if (stack)
    {
      priv->stack = g_object_ref (stack);
      populate_sidebar (sidebar);
      connect_stack_signals (sidebar);
    }

  gtk_widget_queue_resize (GTK_WIDGET (sidebar));

  g_object_notify (G_OBJECT (sidebar), "stack");
}

/**
519 520
 * gtk_stack_sidebar_get_stack:
 * @sidebar: a #GtkStackSidebar
Ikey Doherty's avatar
Ikey Doherty committed
521
 *
522
 * Retrieves the stack.
523
 * See gtk_stack_sidebar_set_stack().
524
 *
525
 * Returns: (nullable) (transfer none): the associated #GtkStack or
526
 *     %NULL if none has been set explicitly
Ikey Doherty's avatar
Ikey Doherty committed
527 528 529 530
 *
 * Since: 3.16
 */
GtkStack *
531
gtk_stack_sidebar_get_stack (GtkStackSidebar *sidebar)
Ikey Doherty's avatar
Ikey Doherty committed
532
{
533
  GtkStackSidebarPrivate *priv;
Ikey Doherty's avatar
Ikey Doherty committed
534

535
  g_return_val_if_fail (GTK_IS_STACK_SIDEBAR (sidebar), NULL);
Ikey Doherty's avatar
Ikey Doherty committed
536

537
  priv = gtk_stack_sidebar_get_instance_private (sidebar);
Ikey Doherty's avatar
Ikey Doherty committed
538 539 540

  return GTK_STACK (priv->stack);
}