terminal-prefs.c 34.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
/*
 * Copyright © 2001, 2002 Havoc Pennington, Red Hat Inc.
 * Copyright © 2008, 2011, 2012, 2013 Christian Persch
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

23 24 25
#include <uuid.h>
#include <dconf.h>

26 27
#include <glib.h>
#include <glib/gi18n.h>
28 29
#include <gtk/gtk.h>

30
#include "profile-editor.h"
31 32 33 34 35 36
#include "terminal-prefs.h"
#include "terminal-accels.h"
#include "terminal-app.h"
#include "terminal-debug.h"
#include "terminal-schemas.h"
#include "terminal-util.h"
37
#include "terminal-profiles-list.h"
38
#include "terminal-libgsystem.h"
39

40
PrefData *the_pref_data = NULL;  /* global */
41

42
/* Bottom */
43 44

static void
45 46
prefs_dialog_help_button_clicked_cb (GtkWidget *button,
                                     PrefData *data)
47
{
48
  terminal_util_show_help ("pref");
49
}
50

51 52 53 54
static void
prefs_dialog_close_button_clicked_cb (GtkWidget *button,
                                      PrefData *data)
{
55 56 57
  gtk_widget_destroy (data->dialog);
}

58
/* Sidebar */
59

60 61 62
static inline GSimpleAction *
lookup_action (GtkWindow *window,
               const char *name)
63
{
64 65 66 67
  GAction *action;

  action = g_action_map_lookup_action (G_ACTION_MAP (window), name);
  g_return_val_if_fail (action != NULL, NULL);
68

69 70 71 72
  return G_SIMPLE_ACTION (action);
}

/* Update the sidebar (visibility of icons, sensitivity of menu entries) to reflect the default and the selected profiles. */
73
static void
74
listbox_update (GtkListBox *box)
75
{
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
  int i;
  GtkListBoxRow *row;
  GSettings *profile;
  gs_unref_object GSettings *default_profile;
  GtkStack *stack;
  GtkMenuButton *button;

  default_profile = terminal_settings_list_ref_default_child (the_pref_data->profiles_list);

  /* GTK+ doesn't seem to like if a popover is assigned to multiple buttons at once
   * (not even temporarily), so make sure to remove it from the previous button first. */
  for (i = 0; (row = gtk_list_box_get_row_at_index (box, i)) != NULL; i++) {
    button = g_object_get_data (G_OBJECT (row), "popover-button");
    gtk_menu_button_set_popover (button, NULL);
  }

  for (i = 0; (row = gtk_list_box_get_row_at_index (box, i)) != NULL; i++) {
    profile = g_object_get_data (G_OBJECT (row), "gsettings");
94

95 96
    gboolean is_selected_profile = (profile != NULL && profile == the_pref_data->selected_profile);
    gboolean is_default_profile = (profile != NULL && profile == default_profile);
97

98 99
    stack = g_object_get_data (G_OBJECT (row), "home-stack");
    gtk_stack_set_visible_child_name (stack, is_default_profile ? "home" : "placeholder");
100

101 102 103 104 105 106 107 108 109 110 111 112
    stack = g_object_get_data (G_OBJECT (row), "popover-stack");
    gtk_stack_set_visible_child_name (stack, is_selected_profile ? "button" : "placeholder");
    if (is_selected_profile) {
      g_simple_action_set_enabled (lookup_action (GTK_WINDOW (the_pref_data->dialog), "delete"), !is_default_profile);
      g_simple_action_set_enabled (lookup_action (GTK_WINDOW (the_pref_data->dialog), "set-as-default"), !is_default_profile);

      GtkPopover *popover_menu = GTK_POPOVER (gtk_builder_get_object (the_pref_data->builder, "popover-menu"));
      button = g_object_get_data (G_OBJECT (row), "popover-button");
      gtk_menu_button_set_popover (button, GTK_WIDGET (popover_menu));
      gtk_popover_set_relative_to (popover_menu, GTK_WIDGET (button));
    }
  }
113 114
}

115 116
static void
update_window_title (void)
117
{
118 119 120
  GtkListBoxRow *row = the_pref_data->selected_list_box_row;
  if (row == NULL)
    return;
121

122 123 124 125 126
  GSettings *profile = g_object_get_data (G_OBJECT (row), "gsettings");
  GtkLabel *label = g_object_get_data (G_OBJECT (row), "label");
  const char *text = gtk_label_get_text (label);
  gs_free char *subtitle;
  gs_free char *title;
127

128 129 130 131 132 133 134 135
  if (profile == NULL) {
    subtitle = g_strdup (text);
  } else {
    subtitle = g_strdup_printf (_("Profile “%s”"), text);
  }

  title = g_strdup_printf (_("Preferences – %s"), subtitle);
  gtk_window_set_title (GTK_WINDOW (the_pref_data->dialog), title);
136 137
}

138 139 140 141 142
/* A new entry is selected in the sidebar */
static void
listbox_row_selected_cb (GtkListBox *box,
                         GtkListBoxRow *row,
                         GtkStack *stack)
143
{
144
  profile_prefs_unload ();
145

146 147 148 149 150 151 152 153 154 155
  /* row can be NULL intermittently during a profile meta operations */
  g_free (the_pref_data->selected_profile_uuid);
  if (row != NULL) {
    the_pref_data->selected_profile = g_object_get_data (G_OBJECT (row), "gsettings");
    the_pref_data->selected_profile_uuid = g_strdup (g_object_get_data (G_OBJECT (row), "uuid"));
  } else {
    the_pref_data->selected_profile = NULL;
    the_pref_data->selected_profile_uuid = NULL;
  }
  the_pref_data->selected_list_box_row = row;
156

157
  listbox_update (box);
158

159 160 161
  if (row != NULL) {
    if (the_pref_data->selected_profile != NULL) {
      profile_prefs_load (the_pref_data->selected_profile_uuid, the_pref_data->selected_profile);
162 163
    }

164 165 166
    char *stack_child_name = g_object_get_data (G_OBJECT (row), "stack_child_name");
    gtk_stack_set_visible_child_name (stack, stack_child_name);
  }
167

168
  update_window_title ();
169 170
}

171 172 173 174 175
/* A profile's name changed, perhaps externally */
static void
profile_name_changed_cb (GtkLabel      *label,
                         GParamSpec    *pspec,
                         GtkListBoxRow *row)
176
{
177
  gtk_list_box_row_changed (row);  /* trigger re-sorting */
178

179 180 181
  if (row == the_pref_data->selected_list_box_row)
    update_window_title ();
}
182

183 184 185 186 187 188 189 190 191 192 193 194 195
/* Select a profile in the sidebar by UUID */
static gboolean
listbox_select_profile (const char *uuid)
{
  GtkListBoxRow *row;
  for (int i = 0; (row = gtk_list_box_get_row_at_index (the_pref_data->listbox, i)) != NULL; i++) {
    const char *rowuuid = g_object_get_data (G_OBJECT (row), "uuid");
    if (g_strcmp0 (rowuuid, uuid) == 0) {
      g_signal_emit_by_name (row, "activate");
      return TRUE;
    }
  }
  return FALSE;
196 197
}

198
/* Create a new profile now, select it, update the UI. */
199
static void
200
profile_new_now (const char *name)
201
{
202
  gs_free char *uuid = terminal_app_new_profile (terminal_app_get (), NULL, name);
203

204 205
  listbox_select_profile (uuid);
}
206

207 208 209 210 211 212
/* Clone the selected profile now, select it, update the UI. */
static void
profile_clone_now (const char *name)
{
  if (the_pref_data->selected_profile == NULL)
    return;
213

214 215 216
  gs_free char *uuid = terminal_app_new_profile (terminal_app_get (), the_pref_data->selected_profile, name);

  listbox_select_profile (uuid);
217 218
}

219 220 221
/* Rename the selected profile now, update the UI. */
static void
profile_rename_now (const char *name)
222
{
223 224
  if (the_pref_data->selected_profile == NULL)
    return;
225

226 227 228
  /* This will automatically trigger a call to profile_name_changed_cb(). */
  g_settings_set_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY, name);
}
229

230 231 232 233 234 235
/* Delete the selected profile now, update the UI. */
static void
profile_delete_now (const char *dummy)
{
  if (the_pref_data->selected_profile == NULL)
    return;
236

237 238 239 240 241 242 243 244 245
  /* Prepare to select the next one, or if there's no such then the previous one. */
  int index = gtk_list_box_row_get_index (the_pref_data->selected_list_box_row);
  GtkListBoxRow *new_selected_row = gtk_list_box_get_row_at_index (the_pref_data->listbox, index + 1);
  if (new_selected_row == NULL)
    new_selected_row = gtk_list_box_get_row_at_index (the_pref_data->listbox, index - 1);
  GSettings *new_selected_profile = g_object_get_data (G_OBJECT (new_selected_row), "gsettings");
  gs_free char *uuid = NULL;
  if (new_selected_profile != NULL)
    uuid = terminal_settings_list_dup_uuid_from_child (the_pref_data->profiles_list, new_selected_profile);
246

247
  terminal_app_remove_profile (terminal_app_get (), the_pref_data->selected_profile);
248

249
  listbox_select_profile (uuid);
250 251
}

252
/* "Set as default" selected. Do it now without asking for confirmation. */
253
static void
254 255 256
profile_set_as_default_cb (GSimpleAction *simple,
                           GVariant      *parameter,
                           gpointer       user_data)
257
{
258
  if (the_pref_data->selected_profile_uuid == NULL)
259 260
    return;

261 262
  /* This will automatically trigger a call to listbox_update() via "default-changed". */
  terminal_settings_list_set_default_child (the_pref_data->profiles_list, the_pref_data->selected_profile_uuid);
263 264 265
}


266 267 268 269 270
static void
popover_dialog_cancel_clicked_cb (GtkButton *button,
                                  gpointer user_data)
{
  GtkPopover *popover_dialog = GTK_POPOVER (gtk_builder_get_object (the_pref_data->builder, "popover-dialog"));
271

272 273 274 275 276
#if GTK_CHECK_VERSION (3, 22, 0)
  gtk_popover_popdown (popover_dialog);
#else
  gtk_widget_hide (GTK_WIDGET (popover_dialog));
#endif
277 278 279
}

static void
280 281
popover_dialog_ok_clicked_cb (GtkButton *button,
                              void (*fn) (const char *))
282
{
283 284
  GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry"));
  const char *name = gtk_entry_get_text (entry);
285

286 287
  /* Perform what we came for */
  (*fn) (name);
288

289 290
  /* Hide/popdown the popover */
  popover_dialog_cancel_clicked_cb (button, NULL);
291 292 293
}

static void
294 295
popover_dialog_closed_cb (GtkPopover *popover,
                          gpointer   user_data)
296 297
{

298 299 300 301 302
  GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry"));
  gtk_entry_set_text (entry, "");

  GtkButton *ok = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-ok"));
  GtkButton *cancel = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-cancel"));
303

304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
  g_signal_handlers_disconnect_matched (ok, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
                                        G_CALLBACK (popover_dialog_ok_clicked_cb), NULL);
  g_signal_handlers_disconnect_matched (cancel, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
                                        G_CALLBACK (popover_dialog_cancel_clicked_cb), NULL);
  g_signal_handlers_disconnect_matched (popover, G_SIGNAL_MATCH_FUNC, 0, 0, NULL,
                                        G_CALLBACK (popover_dialog_closed_cb), NULL);
}


/* Updates the OK button's sensitivity (insensitive if entry field is empty or whitespace only).
 * The entry's initial value and OK's initial sensitivity have to match in the .ui file. */
static void
popover_dialog_notify_text_cb (GtkEntry   *entry,
                               GParamSpec *pspec,
                               GtkWidget  *ok)
{
  gs_free char *text = g_strchomp (g_strdup (gtk_entry_get_text (entry)));
  gtk_widget_set_sensitive (ok, text[0] != '\0');
322 323
}

324 325 326 327 328 329 330 331 332

/* Common dialog for entering new profile name, or confirming deletion */
static void
profile_popup_dialog (GtkWidget *relative_to,
                      const char *header,
                      const char *body,
                      const char *entry_text,
                      const char *ok_text,
                      void (*fn) (const char *))
333
{
334 335 336 337 338 339 340 341 342 343 344 345 346 347
  GtkLabel *label1 = GTK_LABEL (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-label1"));
  gtk_label_set_text (label1, header);

  GtkLabel *label2 = GTK_LABEL (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-label2"));
  gtk_label_set_text (label2, body);

  GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry"));
  if (entry_text != NULL) {
    gtk_entry_set_text (entry, entry_text);
    gtk_widget_show (GTK_WIDGET (entry));
  } else {
    gtk_entry_set_text (entry, ".");  /* to make the OK button sensitive */
    gtk_widget_hide (GTK_WIDGET (entry));
  }
348

349 350 351 352
  GtkButton *ok = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-ok"));
  gtk_button_set_label (ok, ok_text);
  GtkButton *cancel = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-cancel"));
  GtkPopover *popover_dialog = GTK_POPOVER (gtk_builder_get_object (the_pref_data->builder, "popover-dialog"));
353

354 355 356
  g_signal_connect (ok, "clicked", G_CALLBACK (popover_dialog_ok_clicked_cb), fn);
  g_signal_connect (cancel, "clicked", G_CALLBACK (popover_dialog_cancel_clicked_cb), NULL);
  g_signal_connect (popover_dialog, "closed", G_CALLBACK (popover_dialog_closed_cb), NULL);
357

358 359 360
  gtk_popover_set_relative_to (popover_dialog, relative_to);
  gtk_popover_set_position (popover_dialog, GTK_POS_BOTTOM);
  gtk_popover_set_default_widget (popover_dialog, GTK_WIDGET (ok));
361

362 363 364 365 366
#if GTK_CHECK_VERSION (3, 22, 0)
  gtk_popover_popup (popover_dialog);
#else
  gtk_widget_show (GTK_WIDGET (popover_dialog));
#endif
367

368
  gtk_widget_grab_focus (entry_text != NULL ? GTK_WIDGET (entry) : GTK_WIDGET (cancel));
369 370
}

371
/* "New" selected, ask for profile name */
372
static void
373 374
profile_new_cb (GtkButton *button,
                gpointer   user_data)
375
{
376 377 378 379 380 381 382
  profile_popup_dialog (GTK_WIDGET (the_pref_data->new_profile_button),
                        _("New Profile"),
                        _("Enter name for new profile with default settings:"),
                        "",
                        _("Create"),
                        profile_new_now);
}
383

384 385 386 387 388 389 390
/* "Clone" selected, ask for profile name */
static void
profile_clone_cb (GSimpleAction *simple,
                  GVariant      *parameter,
                  gpointer       user_data)
{
  gs_free char *name = g_settings_get_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY);
391

392 393
  gs_free char *label = g_strdup_printf (_("Enter name for new profile based on “%s”:"), name);
  gs_free char *clone_name = g_strdup_printf (_("%s (Copy)"), name);
394

395 396 397 398 399 400
  profile_popup_dialog (GTK_WIDGET (the_pref_data->selected_list_box_row),
                        _("Clone Profile"),
                        label,
                        clone_name,
                        _("Clone"),
                        profile_clone_now);
401 402
}

403
/* "Rename" selected, ask for new name */
404
static void
405 406 407
profile_rename_cb (GSimpleAction *simple,
                        GVariant      *parameter,
                        gpointer       user_data)
408
{
409 410 411 412
  if (the_pref_data->selected_profile == NULL)
    return;

  gs_free char *name = g_settings_get_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY);
413

414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
  gs_free char *label = g_strdup_printf (_("Enter new name for profile “%s”:"), name);

  profile_popup_dialog (GTK_WIDGET (the_pref_data->selected_list_box_row),
                        _("Rename Profile"),
                        label,
                        name,
                        _("Rename"),
                        profile_rename_now);
}

/* "Delete" selected, ask for confirmation */
static void
profile_delete_cb (GSimpleAction *simple,
                   GVariant      *parameter,
                   gpointer       user_data)
{
  if (the_pref_data->selected_profile == NULL)
431 432
    return;

433
  gs_free char *name = g_settings_get_string (the_pref_data->selected_profile, TERMINAL_PROFILE_VISIBLE_NAME_KEY);
434

435
  gs_free char *label = g_strdup_printf (_("Really delete profile “%s”?"), name);
436

437 438 439 440 441 442 443
  profile_popup_dialog (GTK_WIDGET (the_pref_data->selected_list_box_row),
                        _("Delete Profile"),
                        label,
                        NULL,
                        _("Delete"),
                        profile_delete_now);
}
444

445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
#if !GTK_CHECK_VERSION (3, 22, 27)
/* Avoid crash on PageUp and PageDown: bugs 791549 & 770703 */
static gboolean
listbox_key_press_event_cb (GtkListBox  *box,
                            GdkEventKey *event,
                            gpointer     user_data)
{
  switch (event->keyval) {
  case GDK_KEY_Page_Up:
  case GDK_KEY_Page_Down:
  case GDK_KEY_KP_Page_Up:
  case GDK_KEY_KP_Page_Down:
    return TRUE;
  default:
    return FALSE;
  }
}
#endif
463

464 465 466 467 468
/* Create a (non-header) row of the sidebar, either a global or a profile entry. */
static GtkListBoxRow *
listbox_create_row (const char *name,
                    const char *stack_child_name,
                    const char *uuid,
469
                    GSettings  *gsettings /* adopted */,
470 471 472 473 474 475
                    gpointer    sort_order)
{
  GtkListBoxRow *row = GTK_LIST_BOX_ROW (gtk_list_box_row_new ());

  g_object_set_data_full (G_OBJECT (row), "stack_child_name", g_strdup (stack_child_name), g_free);
  g_object_set_data_full (G_OBJECT (row), "uuid", g_strdup (uuid), g_free);
476 477
  if (gsettings != NULL)
    g_object_set_data_full (G_OBJECT (row), "gsettings", gsettings, (GDestroyNotify)g_object_unref);
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
  g_object_set_data (G_OBJECT (row), "sort_order", sort_order);

  GtkBox *hbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0));
  gtk_widget_set_margin_start (GTK_WIDGET (hbox), 6);
  gtk_widget_set_margin_end (GTK_WIDGET (hbox), 6);
  gtk_widget_set_margin_top (GTK_WIDGET (hbox), 6);
  gtk_widget_set_margin_bottom (GTK_WIDGET (hbox), 6);

  GtkLabel *label = GTK_LABEL (gtk_label_new (name));
  if (gsettings != NULL) {
    g_signal_connect (label, "notify::label", G_CALLBACK (profile_name_changed_cb), row);
    g_settings_bind (gsettings,
                     TERMINAL_PROFILE_VISIBLE_NAME_KEY,
                     label,
                     "label",
                     G_SETTINGS_BIND_GET);
  }
  gtk_label_set_xalign (label, 0);
  gtk_box_pack_start (hbox, GTK_WIDGET (label), TRUE, TRUE, 0);
  g_object_set_data (G_OBJECT (row), "label", label);

  /* Always add the "default" symbol and the "menu" button, even on rows of global prefs.
   * Use GtkStack to possible achieve visibility:hidden on it.
   * This is so that all listbox rows have the same dimensions, and the width doesn't change
   * as you switch the default profile. */

  GtkStack *popover_stack = GTK_STACK (gtk_stack_new ());
  gtk_widget_set_margin_start (GTK_WIDGET (popover_stack), 6);
  GtkMenuButton *popover_button = GTK_MENU_BUTTON (gtk_menu_button_new ());
  gtk_button_set_relief (GTK_BUTTON (popover_button), GTK_RELIEF_NONE);
  gtk_stack_add_named (popover_stack, GTK_WIDGET (popover_button), "button");
  GtkLabel *popover_label = GTK_LABEL (gtk_label_new (""));
  gtk_stack_add_named (popover_stack, GTK_WIDGET (popover_label), "placeholder");
  g_object_set_data (G_OBJECT (row), "popover-stack", popover_stack);
  g_object_set_data (G_OBJECT (row), "popover-button", popover_button);

  gtk_box_pack_end (hbox, GTK_WIDGET (popover_stack), FALSE, FALSE, 0);

  GtkStack *home_stack = GTK_STACK (gtk_stack_new ());
  gtk_widget_set_margin_start (GTK_WIDGET (home_stack), 12);
  GtkImage *home_image = GTK_IMAGE (gtk_image_new_from_icon_name ("emblem-default-symbolic", GTK_ICON_SIZE_BUTTON));
  gtk_widget_set_tooltip_text (GTK_WIDGET (home_image), _("This is the default profile"));
  gtk_stack_add_named (home_stack, GTK_WIDGET (home_image), "home");
  GtkLabel *home_label = GTK_LABEL (gtk_label_new (""));
  gtk_stack_add_named (home_stack, GTK_WIDGET (home_label), "placeholder");
  g_object_set_data (G_OBJECT (row), "home-stack", home_stack);

  gtk_box_pack_end (hbox, GTK_WIDGET (home_stack), FALSE, FALSE, 0);

  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (hbox));

  gtk_widget_show_all (GTK_WIDGET (row));

  gtk_stack_set_visible_child_name (popover_stack, "placeholder");
  gtk_stack_set_visible_child_name (home_stack, "placeholder");

  return row;
}
536

537 538 539 540 541 542 543 544 545 546 547 548 549 550 551
/* Add all the non-profile rows to the sidebar */
static void
listbox_add_all_globals (PrefData *data)
{
  GtkListBoxRow *row;

  row = listbox_create_row (_("General"),
                            "general-prefs",
                            NULL, NULL, (gpointer) 0);
  gtk_list_box_insert (data->listbox, GTK_WIDGET (row), -1);

  row = listbox_create_row (_("Shortcuts"),
                            "shortcut-prefs",
                            NULL, NULL, (gpointer) 1);
  gtk_list_box_insert (data->listbox, GTK_WIDGET (row), -1);
552 553
}

554
/* Remove all the profile rows from the sidebar */
555
static void
556
listbox_remove_all_profiles (PrefData *data)
557
{
558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
  int i = 0;

  data->selected_profile = NULL;
  g_free (data->selected_profile_uuid);
  data->selected_profile_uuid = NULL;
  profile_prefs_unload ();

  GtkListBoxRow *row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (the_pref_data->listbox), 0);
  g_signal_emit_by_name (row, "activate");

  while ((row = gtk_list_box_get_row_at_index (data->listbox, i)) != NULL) {
    if (g_object_get_data (G_OBJECT (row), "gsettings") != NULL) {
      gtk_widget_destroy (GTK_WIDGET (row));
    } else {
      i++;
    }
  }
575 576
}

577
/* Add all the profiles to the sidebar */
578
static void
579
listbox_add_all_profiles (PrefData *data)
580
{
581 582
  GList *list, *l;
  GtkListBoxRow *row;
583

584 585 586 587 588 589 590 591 592
  list = terminal_settings_list_ref_children (data->profiles_list);

  for (l = list; l != NULL; l = l->next) {
    GSettings *profile = (GSettings *) l->data;
    gs_free gchar *uuid = terminal_settings_list_dup_uuid_from_child (data->profiles_list, profile);

    row = listbox_create_row (NULL,
                              "profile-prefs",
                              uuid,
593
                              profile /* adopts */,
594 595 596
                              (gpointer) 42);
    gtk_list_box_insert (data->listbox, GTK_WIDGET (row), -1);
  }
597

598 599
  g_list_free(list); /* the items themselves were adopted into the model above */

600
  listbox_update (data->listbox);  /* FIXME: This is not needed but I don't know why :-) */
601 602
}

603 604 605 606 607 608 609 610
/* Re-add all the profiles to the sidebar.
 * This is called when a profile is added or removed, and also when the list of profiles is
 * modified externally.
 * Try to keep the selected profile, whenever possible.
 * When the list is modified externally, the terminal_settings_list_*() methods seem to preserve
 * the GSettings object for every profile that remains in the list. There's no guarantee however
 * that a newly created GSettings can't receive the same address that a ceased one used to have.
 * So don't rely on GSettings* to keep track of the selected profile, use the UUID instead. */
611
static void
612
listbox_readd_profiles (PrefData *data)
613
{
614
  gs_free char *uuid = g_strdup (data->selected_profile_uuid);
615

616 617
  listbox_remove_all_profiles (data);
  listbox_add_all_profiles (data);
618

619 620
  if (uuid != NULL)
    listbox_select_profile (uuid);
621 622
}

623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
/* Create a header row ("Global" or "Profiles +") */
static GtkWidget *
listboxrow_create_header (const char *text,
                          gboolean visible_button)
{
  GtkBox *hbox = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0));
  gtk_widget_set_margin_start (GTK_WIDGET (hbox), 6);
  gtk_widget_set_margin_end (GTK_WIDGET (hbox), 6);
  gtk_widget_set_margin_top (GTK_WIDGET (hbox), 6);
  gtk_widget_set_margin_bottom (GTK_WIDGET (hbox), 6);

  GtkLabel *label = GTK_LABEL (gtk_label_new (NULL));
  gs_free char *markup = g_markup_printf_escaped ("<b>%s</b>", text);
  gtk_label_set_markup (label, markup);
  gtk_label_set_xalign (label, 0);
  gtk_box_pack_start (hbox, GTK_WIDGET (label), TRUE, TRUE, 0);

  /* Always add a "new profile" button. Use GtkStack to possible achieve visibility:hidden on it.
   * This is so that both header rows have the same dimensions. */

  GtkStack *stack = GTK_STACK (gtk_stack_new ());
  GtkButton *button = GTK_BUTTON (gtk_button_new_from_icon_name ("list-add-symbolic", GTK_ICON_SIZE_BUTTON));
  gtk_button_set_relief (button, GTK_RELIEF_NONE);
  gtk_stack_add_named (stack, GTK_WIDGET (button), "button");
  GtkLabel *labelx = GTK_LABEL (gtk_label_new (""));
  gtk_stack_add_named (stack, GTK_WIDGET (labelx), "placeholder");

  gtk_box_pack_end (hbox, GTK_WIDGET (stack), FALSE, FALSE, 0);

  gtk_widget_show_all (GTK_WIDGET (hbox));

  if (visible_button) {
    gtk_stack_set_visible_child_name (stack, "button");
    g_signal_connect (button, "clicked", G_CALLBACK (profile_new_cb), NULL);
    the_pref_data->new_profile_button = GTK_WIDGET (button);
  } else {
    gtk_stack_set_visible_child_name (stack, "placeholder");
  }

  return GTK_WIDGET (hbox);
}

/* Manage the creation or removal of the header row ("Global" or "Profiles +") */
666
static void
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
listboxrow_update_header (GtkListBoxRow *row,
                          GtkListBoxRow *before,
                          gpointer       user_data)
{
  if (before == NULL) {
    if (gtk_list_box_row_get_header (row) == NULL) {
      gtk_list_box_row_set_header (row, listboxrow_create_header (_("Global"), FALSE));
    }
    return;
  }

  GSettings *profile = g_object_get_data (G_OBJECT (row), "gsettings");
  if (profile != NULL) {
    GSettings *profile_before = g_object_get_data (G_OBJECT (before), "gsettings");
    if (profile_before != NULL) {
      gtk_list_box_row_set_header (row, NULL);
    } else {
      if (gtk_list_box_row_get_header (row) == NULL) {
        gtk_list_box_row_set_header (row, listboxrow_create_header (_("Profiles"), TRUE));
      }
    }
  }
}

/* Sort callback for rows of the sidebar (global and profile ones).
 * Global ones are kept at the top in fixed order. This is implemented via sort_order
 * which is an integer disguised as a pointer for ease of implementation.
 * Profile ones are sorted lexicographically. */
static gint
listboxrow_compare_cb (GtkListBoxRow *row1,
                       GtkListBoxRow *row2,
                       gpointer       user_data)
699
{
700 701 702 703 704 705 706 707 708 709
  gpointer sort_order_1 = g_object_get_data (G_OBJECT (row1), "sort_order");
  gpointer sort_order_2 = g_object_get_data (G_OBJECT (row2), "sort_order");

  if (sort_order_1 != sort_order_2)
    return sort_order_1 < sort_order_2 ? -1 : 1;

  GtkLabel *label1 = g_object_get_data (G_OBJECT (row1), "label");
  const char *text1 = gtk_label_get_text (label1);
  GtkLabel *label2 = g_object_get_data (G_OBJECT (row2), "label");
  const char *text2 = gtk_label_get_text (label2);
710

711
  return g_utf8_collate (text1, text2);
712 713 714 715
}

/* Keybindings tab */

716 717 718 719 720 721 722 723
/* Make sure the treeview is repainted with the correct text color, see bug 792139. */
static void
shortcuts_button_toggled_cb (GtkWidget *widget,
                             GtkTreeView *tree_view)
{
  gtk_widget_queue_draw (GTK_WIDGET (tree_view));
}

724 725 726 727 728 729
/* misc */

static void
prefs_dialog_destroy_cb (GtkWidget *widget,
                         PrefData *data)
{
730
  /* Don't run this handler again */
731
  g_signal_handlers_disconnect_by_func (widget, G_CALLBACK (prefs_dialog_destroy_cb), data);
732 733 734 735 736 737 738 739 740 741

  g_signal_handlers_disconnect_by_func (data->profiles_list,
                                        G_CALLBACK (listbox_readd_profiles), data);
  g_signal_handlers_disconnect_by_func (data->profiles_list,
                                        G_CALLBACK (listbox_update), data->listbox);

  profile_prefs_destroy ();

  g_object_unref (data->builder);
  g_free (data->selected_profile_uuid);
742
  g_free (data);
743 744 745
}

void
746
terminal_prefs_show_preferences (GSettings *profile, const char *widget_name)
747
{
748 749
  TerminalApp *app = terminal_app_get ();
  PrefData *data;
750 751
  GtkWidget *dialog, *tree_view;
  GtkWidget *show_menubar_button, *disable_mnemonics_button, *disable_menu_accel_button;
752
  GtkWidget *disable_shortcuts_button;
753 754
  GtkWidget *theme_variant_label, *theme_variant_combo;
  GtkWidget *new_terminal_mode_label, *new_terminal_mode_combo;
755
  GtkWidget *close_button, *help_button;
756
  GtkWidget *content_box, *general_frame, *keybindings_frame;
757 758
  GSettings *settings;

759 760 761 762 763 764 765 766
  const GActionEntry action_entries[] = {
    { "clone",          profile_clone_cb,          NULL, NULL, NULL },
    { "rename",         profile_rename_cb,         NULL, NULL, NULL },
    { "delete",         profile_delete_cb,         NULL, NULL, NULL },
    { "set-as-default", profile_set_as_default_cb, NULL, NULL, NULL },
  };

  if (the_pref_data != NULL)
767 768
    goto done;

769 770
  the_pref_data = g_new0 (PrefData, 1);
  data = the_pref_data;
771
  data->profiles_list = terminal_app_get_profiles_list (app);
772

773 774
  /* FIXME this method is only used from here. Inline it here instead. */
  data->builder = terminal_util_load_widgets_resource ("/org/gnome/terminal/ui/preferences.ui",
775
                                       "preferences-dialog",
776
                                       "preferences-dialog", &dialog,
777
                                       "dialogue-content-box", &content_box,
778 779
                                       "general-frame", &general_frame,
                                       "keybindings-frame", &keybindings_frame,
780 781
                                       "close-button", &close_button,
                                       "help-button", &help_button,
782
                                       "default-show-menubar-checkbutton", &show_menubar_button,
783 784
                                       "theme-variant-label", &theme_variant_label,
                                       "theme-variant-combobox", &theme_variant_combo,
785
                                       "new-terminal-mode-label", &new_terminal_mode_label,
786
                                       "new-terminal-mode-combobox", &new_terminal_mode_combo,
787
                                       "disable-mnemonics-checkbutton", &disable_mnemonics_button,
788
                                       "disable-shortcuts-checkbutton", &disable_shortcuts_button,
789 790
                                       "disable-menu-accel-checkbutton", &disable_menu_accel_button,
                                       "accelerators-treeview", &tree_view,
791 792
                                       "the-stack", &data->stack,
                                       "the-listbox", &data->listbox,
793 794
                                       NULL);

795 796
  data->dialog = dialog;

797 798
  gtk_window_set_application (GTK_WINDOW (data->dialog), GTK_APPLICATION (terminal_app_get ()));

799 800
  terminal_util_bind_mnemonic_label_sensitivity (dialog);

801
  settings = terminal_app_get_global_settings (app);
802

803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
  g_action_map_add_action_entries (G_ACTION_MAP (dialog),
                                   action_entries, G_N_ELEMENTS (action_entries),
                                   data);

  /* Sidebar */

  gtk_list_box_set_header_func (GTK_LIST_BOX (data->listbox),
                                listboxrow_update_header,
                                NULL,
                                NULL);
  g_signal_connect (data->listbox, "row-selected", G_CALLBACK (listbox_row_selected_cb), data->stack);
  gtk_list_box_set_sort_func (data->listbox, listboxrow_compare_cb, NULL, NULL);
#if !GTK_CHECK_VERSION (3, 22, 27)
  g_signal_connect (data->listbox, "key-press-event", G_CALLBACK (listbox_key_press_event_cb), NULL);
#endif

  listbox_add_all_globals (data);
  listbox_add_all_profiles (data);
  g_signal_connect_swapped (data->profiles_list, "children-changed",
                            G_CALLBACK (listbox_readd_profiles), data);
  g_signal_connect_swapped (data->profiles_list, "default-changed",
                            G_CALLBACK (listbox_update), data->listbox);

  GtkEntry *entry = GTK_ENTRY (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-entry"));
  GtkButton *ok = GTK_BUTTON (gtk_builder_get_object (the_pref_data->builder, "popover-dialog-ok"));
  g_signal_connect (entry, "notify::text", G_CALLBACK (popover_dialog_notify_text_cb), ok);

  /* General page */
831

832 833 834 835
  gboolean shell_shows_menubar;
  g_object_get (gtk_settings_get_default (),
                "gtk-shell-shows-menubar", &shell_shows_menubar,
                NULL);
836
  if (shell_shows_menubar || terminal_app_get_use_headerbar (app)) {
837 838 839 840 841 842 843 844
    gtk_widget_set_visible (show_menubar_button, FALSE);
  } else {
    g_settings_bind (settings,
                     TERMINAL_SETTING_DEFAULT_SHOW_MENUBAR_KEY,
                     show_menubar_button,
                     "active",
                     G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET);
  }
845

846 847 848 849 850 851 852 853 854 855 856
#if GTK_CHECK_VERSION (3, 19, 0)
  g_settings_bind (settings,
                   TERMINAL_SETTING_THEME_VARIANT_KEY,
                   theme_variant_combo,
                   "active-id",
                   G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET);
#else
  gtk_widget_set_visible (theme_variant_label, FALSE);
  gtk_widget_set_visible (theme_variant_combo, FALSE);
#endif /* GTK+ 3.19 */

857 858
  if (terminal_app_get_menu_unified (app) ||
      terminal_app_get_use_headerbar (app)) {
859 860 861 862 863 864 865 866 867
    g_settings_bind (settings,
                     TERMINAL_SETTING_NEW_TERMINAL_MODE_KEY,
                     new_terminal_mode_combo,
                     "active-id",
                     G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET);
  } else {
    gtk_widget_set_visible (new_terminal_mode_label, FALSE);
    gtk_widget_set_visible (new_terminal_mode_combo, FALSE);
  }
868

869 870 871 872 873 874 875 876 877
  if (shell_shows_menubar) {
    gtk_widget_set_visible (disable_mnemonics_button, FALSE);
  } else {
    g_settings_bind (settings,
                     TERMINAL_SETTING_ENABLE_MNEMONICS_KEY,
                     disable_mnemonics_button,
                     "active",
                     G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET);
  }
878
  g_settings_bind (settings,
879 880
                   TERMINAL_SETTING_ENABLE_MENU_BAR_ACCEL_KEY,
                   disable_menu_accel_button,
881 882
                   "active",
                   G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET);
883 884 885

  /* Shortcuts page */

886
  g_settings_bind (settings,
887 888
                   TERMINAL_SETTING_ENABLE_SHORTCUTS_KEY,
                   disable_shortcuts_button,
889 890 891
                   "active",
                   G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET);

892 893 894
  g_signal_connect (disable_shortcuts_button, "toggled",
                    G_CALLBACK (shortcuts_button_toggled_cb), tree_view);

895
  terminal_accels_fill_treeview (tree_view, disable_shortcuts_button);
896

897
  /* Profile page */
898

899
  profile_prefs_init ();
900

901
  /* Move action widgets to titlebar when headerbar is used */
902
  if (terminal_app_get_dialog_use_headerbar (app)) {
903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922
    GtkWidget *headerbar;
    GtkWidget *bbox;

    headerbar = g_object_new (GTK_TYPE_HEADER_BAR,
                              "show-close-button", TRUE,
                              NULL);
    bbox = gtk_widget_get_parent (help_button);

    gtk_container_remove (GTK_CONTAINER (bbox), g_object_ref (help_button));
    gtk_header_bar_pack_start (GTK_HEADER_BAR (headerbar), help_button);
    g_object_unref (help_button);

    gtk_style_context_add_class (gtk_widget_get_style_context (help_button),
                                 "text-button");

    gtk_widget_show (headerbar);
    gtk_widget_hide (bbox);

    gtk_window_set_titlebar (GTK_WINDOW (dialog), headerbar);

923
    /* Remove extra spacing around the content, and extra frames */
924
    g_object_set (G_OBJECT (content_box), "margin", 0, NULL);
925 926
    gtk_frame_set_shadow_type (GTK_FRAME (general_frame), GTK_SHADOW_NONE);
    gtk_frame_set_shadow_type (GTK_FRAME (keybindings_frame), GTK_SHADOW_NONE);
927 928
  }

929 930
  /* misc */

931 932
  g_signal_connect (close_button, "clicked", G_CALLBACK (prefs_dialog_close_button_clicked_cb), data);
  g_signal_connect (help_button, "clicked", G_CALLBACK (prefs_dialog_help_button_clicked_cb), data);
933
  g_signal_connect (dialog, "destroy", G_CALLBACK (prefs_dialog_destroy_cb), data);
934

935
  g_object_add_weak_pointer (G_OBJECT (dialog), (gpointer *) &the_pref_data);
936 937

done:
938 939 940 941 942 943 944 945 946
  if (profile != NULL) {
    gs_free char *uuid = terminal_settings_list_dup_uuid_from_child (the_pref_data->profiles_list, profile);
    listbox_select_profile (uuid);
  } else {
    GtkListBoxRow *row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (the_pref_data->listbox), 0);
    g_signal_emit_by_name (row, "activate");
  }

  terminal_util_dialog_focus_widget (the_pref_data->builder, widget_name);
947

948
  gtk_window_present (GTK_WINDOW (the_pref_data->dialog));
949
}