/***********************************************************************
 Freeciv - Copyright (C) 1996 - A Kjeldberg, L Gregersen, P Unold
   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 2, 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.
***********************************************************************/

#ifdef HAVE_CONFIG_H
#include <fc_config.h>
#endif

#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

/* utility */
#include "fcintl.h"
#include "log.h"
#include "mem.h"
#include "support.h"

/* client */
#include "options.h"

/* client/gui-gtk-4.0 */
#include "colors.h"
#include "gui_main.h"

#include "gui_stuff.h"


static GList *dialog_list;

static GtkSizeGroup *gui_action;

static GtkCssProvider *dlg_tab_provider = NULL;


/**********************************************************************//**
  Draw widget now
**************************************************************************/
void gtk_expose_now(GtkWidget *w)
{
  gtk_widget_queue_draw(w);
}

/**********************************************************************//**
  Create new icon button with label.

  Current implementation sets either icon or label, preferring label,
  never both. Caller should not rely on that, though.
**************************************************************************/
GtkWidget *icon_label_button_new(const gchar *icon_name,
                                 const gchar *label_text)
{
  GtkWidget *button;

  fc_assert(icon_name != NULL || label_text != NULL);

  if (label_text != NULL) {
    button = gtk_button_new_with_mnemonic(label_text);
  } else if (icon_name != NULL) {
    button = gtk_button_new_from_icon_name(icon_name);
  } else {
    button = NULL;
  }

  return button;
}

/**********************************************************************//**
  Changes the label (with mnemonic) on an existing stockbutton.  See
  gtk_stockbutton_new.
**************************************************************************/
void gtk_stockbutton_set_label(GtkWidget *button, const gchar *label_text)
{
  gtk_button_set_label(GTK_BUTTON(button), label_text);
}

/**********************************************************************//**
  Returns gettext-converted list of n strings.  The individual strings
  in the list are as returned by gettext().  In case of no NLS, the strings
  will be the original strings, so caller should ensure that the originals
  persist for as long as required.  (For no NLS, still allocate the
  list, for consistency.)

  (This is not directly gui/gtk related, but it fits in here
  because so far it is used for doing i18n for gtk titles...)
**************************************************************************/
void intl_slist(int n, const char **s, bool *done)
{
  int i;

  if (!*done) {
    for (i = 0; i < n; i++) {
      s[i] = Q_(s[i]);
    }

    *done = TRUE;
  }
}

/**********************************************************************//**
  Set itree to the beginning
**************************************************************************/
void itree_begin(GtkTreeModel *model, ITree *it)
{
  it->model = model;
  it->end = !gtk_tree_model_get_iter_first(it->model, &it->it);
}

/**********************************************************************//**
  Return whether itree end has been reached
**************************************************************************/
gboolean itree_end(ITree *it)
{
  return it->end;
}

/**********************************************************************//**
  Make itree to go forward one step
**************************************************************************/
void itree_next(ITree *it)
{
  it->end = !gtk_tree_model_iter_next(it->model, &it->it);
}

/**********************************************************************//**
  Store values to itree
**************************************************************************/
void itree_set(ITree *it, ...)
{
  va_list ap;

  va_start(ap, it);
  gtk_tree_store_set_valist(GTK_TREE_STORE(it->model), &it->it, ap);
  va_end(ap);
}

/**********************************************************************//**
  Get values from itree
**************************************************************************/
void itree_get(ITree *it, ...)
{
  va_list ap;

  va_start(ap, it);
  gtk_tree_model_get_valist(it->model, &it->it, ap);
  va_end(ap);
}

/**********************************************************************//**
  Append one item to the end of tree store
**************************************************************************/
void tstore_append(GtkTreeStore *store, ITree *it, ITree *parent)
{
  it->model = GTK_TREE_MODEL(store);
  if (parent)
    gtk_tree_store_append(GTK_TREE_STORE(it->model), &it->it, &parent->it);
  else
    gtk_tree_store_append(GTK_TREE_STORE(it->model), &it->it, NULL);
  it->end = FALSE;
}

/**********************************************************************//**
  Return whether current itree item is selected
**************************************************************************/
gboolean itree_is_selected(GtkTreeSelection *selection, ITree *it)
{
  return gtk_tree_selection_iter_is_selected(selection, &it->it);
}

/**********************************************************************//**
  Add current itree item to selection
**************************************************************************/
void itree_select(GtkTreeSelection *selection, ITree *it)
{
  gtk_tree_selection_select_iter(selection, &it->it);
}

/**********************************************************************//**
  Remove current itree item from selection
**************************************************************************/
void itree_unselect(GtkTreeSelection *selection, ITree *it)
{
  gtk_tree_selection_unselect_iter(selection, &it->it);
}

/**********************************************************************//**
  Return the selected row in a GtkTreeSelection.
  If no row is selected return -1.
**************************************************************************/
gint gtk_tree_selection_get_row(GtkTreeSelection *selection)
{
  GtkTreeModel *model;
  GtkTreeIter it;
  gint row = -1;

  if (gtk_tree_selection_get_selected(selection, &model, &it)) {
    GtkTreePath *path;
    gint *idx;

    path = gtk_tree_model_get_path(model, &it);
    idx = gtk_tree_path_get_indices(path);
    row = idx[0];
    gtk_tree_path_free(path);
  }
  return row;
}

/**********************************************************************//**
  Give focus to view
**************************************************************************/
void gtk_tree_view_focus(GtkTreeView *view)
{
  GtkTreeModel *model;
  GtkTreePath *path;
  GtkTreeIter iter;

  if ((model = gtk_tree_view_get_model(view))
      && gtk_tree_model_get_iter_first(model, &iter)
      && (path = gtk_tree_model_get_path(model, &iter))) {
    gtk_tree_view_set_cursor(view, path, NULL, FALSE);
    gtk_tree_path_free(path);
    gtk_widget_grab_focus(GTK_WIDGET(view));
  }
}

/**********************************************************************//**
  Create an auxiliary menubar (i.e., not the main menubar at the top of
  the window).
**************************************************************************/
GtkWidget *aux_menu_new(void)
{
  GtkWidget *menu_button = gtk_menu_button_new();

  return menu_button;
}

/**********************************************************************//**
  Generic close callback for all dialogs
**************************************************************************/
static void close_callback(GtkDialog *dialog, gpointer data)
{
  gtk_window_destroy(GTK_WINDOW(dialog));
}

/**********************************************************************//**
  This function handles new windows which are subwindows to the
  toplevel window. It must be called on every dialog in the game,
  so fullscreen windows are handled properly by the window manager.
**************************************************************************/
void setup_dialog(GtkWidget *shell, GtkWidget *parent)
{
  if (GUI_GTK_OPTION(dialogs_on_top) || GUI_GTK_OPTION(fullscreen)) {
    gtk_window_set_transient_for(GTK_WINDOW(shell),
                                 GTK_WINDOW(parent));
  }

  /* Close dialog window on Escape keypress. */
  if (GTK_IS_DIALOG(shell)) {
    GtkEventController *controller;

    controller = GTK_EVENT_CONTROLLER(gtk_event_controller_focus_new());
    g_signal_connect(controller, "enter",
                     G_CALLBACK(fc_gained_focus), NULL);
    g_signal_connect(controller, "leave",
                     G_CALLBACK(fc_lost_focus), NULL);
    gtk_widget_add_controller(shell, controller);

    g_signal_connect_after(shell, "close", G_CALLBACK(close_callback), shell);
  }
}

/**********************************************************************//**
  Emit a dialog response.
**************************************************************************/
static void gui_dialog_response(struct gui_dialog *dlg, int response)
{
  if (dlg->response_callback) {
    (*dlg->response_callback)(dlg, response, dlg->user_data);
  }
}

/**********************************************************************//**
  Default dialog response handler. Destroys the dialog.
**************************************************************************/
static void gui_dialog_destroyed(struct gui_dialog *dlg, int response,
                                 gpointer data)
{
  gui_dialog_destroy(dlg);
}

/**********************************************************************//**
  Cleanups the leftovers after a dialog is destroyed.
**************************************************************************/
static void gui_dialog_destroy_handler(GtkWidget *w, struct gui_dialog *dlg)
{
  if (dlg->type == GUI_DIALOG_TAB) {
    GtkWidget *notebook = dlg->v.tab.notebook;
    gulong handler_id = dlg->v.tab.handler_id;

    g_signal_handler_disconnect(notebook, handler_id);
  }

  g_object_unref(dlg->gui_button);

  if (*(dlg->source)) {
    *(dlg->source) = NULL;
  }

  dialog_list = g_list_remove(dialog_list, dlg);

  /* Raise the return dialog set by gui_dialog_set_return_dialog() */
  if (dlg->return_dialog_id != -1) {
    GList *it;

    for (it = dialog_list; it; it = g_list_next(it)) {
      struct gui_dialog *adialog = (struct gui_dialog *)it->data;

      if (adialog->id == dlg->return_dialog_id) {
        gui_dialog_raise(adialog);
        break;
      }
    }
  }

  if (dlg->title) {
    free(dlg->title);
  }

  free(dlg);
}

/**********************************************************************//**
  Emit a delete event response on dialog deletion in case the end-user
  needs to know when a deletion took place.
  Popup dialog version
**************************************************************************/
static gint gui_dialog_delete_handler(GtkWidget *widget, gpointer data)
{
  struct gui_dialog *dlg = data;

  /* emit response signal. */
  gui_dialog_response(dlg, GTK_RESPONSE_DELETE_EVENT);

  /* do the destroy by default. */
  return FALSE;
}

/**********************************************************************//**
  Emit a delete event response on dialog deletion in case the end-user
  needs to know when a deletion took place.
  TAB version
**************************************************************************/
static gint gui_dialog_delete_tab_handler(struct gui_dialog* dlg)
{
  GtkWidget* notebook;
  int n;

  notebook = dlg->v.tab.notebook;
  n = gtk_notebook_get_current_page(GTK_NOTEBOOK(notebook));
  if (gtk_notebook_get_nth_page(GTK_NOTEBOOK(notebook), n)
      != dlg->v.tab.child) {
    gui_dialog_set_return_dialog(dlg, NULL);
  }

  /* emit response signal. */
  gui_dialog_response(dlg, GTK_RESPONSE_DELETE_EVENT);

  /* do the destroy by default. */
  return FALSE;
}


/**********************************************************************//**
  Allow the user to close a dialog using Escape or CTRL+W.
**************************************************************************/
static gboolean gui_dialog_key_press_handler(GtkEventControllerKey *controller,
                                             guint keyval, guint keycode,
                                             GdkModifierType state,
                                             gpointer data)
{
  struct gui_dialog *dlg = (struct gui_dialog *)data;

  if (keyval == GDK_KEY_Escape
      || ((state & GDK_CONTROL_MASK) && keyval == GDK_KEY_w)) {
    /* Emit response signal. */
    gui_dialog_response(dlg, GTK_RESPONSE_DELETE_EVENT);
  }

  /* Propagate event further. */
  return FALSE;
}

/**********************************************************************//**
  Resets tab colour on tab activation.
**************************************************************************/
static void gui_dialog_switch_page_handler(GtkNotebook *notebook,
                                           GtkWidget *page,
                                           guint num,
                                           struct gui_dialog *dlg)
{
  gint n;

  n = gtk_notebook_page_num(GTK_NOTEBOOK(dlg->v.tab.notebook), dlg->grid);

  if (n == num) {
    gtk_widget_remove_css_class(dlg->v.tab.label, "alert");
    gtk_widget_remove_css_class(dlg->v.tab.label, "notice");
  }
}

/**********************************************************************//**
  Changes a tab into a window.
**************************************************************************/
static void gui_dialog_detach(struct gui_dialog *dlg)
{
  gint n;
  GtkWidget *window, *notebook;
  gulong handler_id;

  if (dlg->type != GUI_DIALOG_TAB) {
    return;
  }
  dlg->type = GUI_DIALOG_WINDOW;

  /* Create a new reference to the main widget, so it won't be
   * destroyed in gtk_notebook_remove_page() */
  g_object_ref(dlg->grid);

  /* Remove widget from the notebook */
  notebook = dlg->v.tab.notebook;
  handler_id = dlg->v.tab.handler_id;
  g_signal_handler_disconnect(notebook, handler_id);

  n = gtk_notebook_page_num(GTK_NOTEBOOK(dlg->v.tab.notebook), dlg->grid);
  gtk_notebook_remove_page(GTK_NOTEBOOK(dlg->v.tab.notebook), n);


  /* Create window and put the widget inside */
  window = gtk_window_new();
  gtk_window_set_title(GTK_WINDOW(window), dlg->title);
  setup_dialog(window, toplevel);

  gtk_window_set_child(GTK_WINDOW(window), dlg->grid);
  dlg->v.window = window;
  g_signal_connect(window, "close-request",
                   G_CALLBACK(gui_dialog_delete_handler), dlg);

  gtk_window_set_default_size(GTK_WINDOW(dlg->v.window),
                              dlg->default_width,
                              dlg->default_height);
  gtk_widget_set_visible(window, TRUE);
}

/**********************************************************************//**
  Someone has clicked on a label in a notebook
**************************************************************************/
static gboolean click_on_tab_callback(GtkGestureClick *gesture,
                                      int n_press,
                                      double x, double y, gpointer data)
{
  if (n_press == 2) {
    gui_dialog_detach((struct gui_dialog *)data);
  }

  return TRUE;
}

/**********************************************************************//**
  Creates a new dialog. It will be a tab or a window depending on the
  current user setting of 'enable_tabs' gtk-gui option.
  Sets pdlg to point to the dialog once it is create, Zeroes pdlg on
  dialog destruction.
  user_data will be passed through response function
  check_top indicates if the layout decision should depend on the parent.
**************************************************************************/
void gui_dialog_new(struct gui_dialog **pdlg, GtkNotebook *notebook,
                    gpointer user_data, bool check_top)
{
  struct gui_dialog *dlg;
  GtkWidget *action_area;
  static int dialog_id_counter;
  GtkEventController *controller;

  dlg = fc_malloc(sizeof(*dlg));
  dialog_list = g_list_prepend(dialog_list, dlg);

  dlg->source = pdlg;
  *pdlg = dlg;
  dlg->user_data = user_data;
  dlg->title = NULL;

  dlg->default_width = 200;
  dlg->default_height = 300;

  if (GUI_GTK_OPTION(enable_tabs)) {
    dlg->type = GUI_DIALOG_TAB;
  } else {
    dlg->type = GUI_DIALOG_WINDOW;
  }

  if (!gui_action) {
    gui_action = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL);
  }
  dlg->gui_button = gtk_size_group_new(GTK_SIZE_GROUP_BOTH);

  dlg->grid = gtk_grid_new();
  dlg->content_counter = 0;
  if (GUI_GTK_OPTION(enable_tabs)
      && (check_top && notebook != GTK_NOTEBOOK(top_notebook))
      && !GUI_GTK_OPTION(small_display_layout)) {
    /* We expect this to be short (as opposed to tall); maximise usable
     * height by putting buttons down the right hand side */
    action_area = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
    dlg->vertical_content = FALSE;
  } else {
    /* We expect this to be reasonably tall; maximise usable width by
     * putting buttons along the bottom */
    action_area = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
    gtk_orientable_set_orientation(GTK_ORIENTABLE(dlg->grid),
                                   GTK_ORIENTATION_VERTICAL);
    dlg->vertical_content = TRUE;
  }

  gtk_widget_set_visible(dlg->grid, TRUE);
  gui_dialog_add_content_widget(dlg, action_area);
  gtk_widget_set_visible(action_area, TRUE);

  gtk_widget_set_margin_start(dlg->grid, 2);
  gtk_widget_set_margin_end(dlg->grid, 2);
  gtk_widget_set_margin_top(dlg->grid, 2);
  gtk_widget_set_margin_bottom(dlg->grid, 2);

  gtk_widget_set_margin_start(action_area, 2);
  gtk_widget_set_margin_end(action_area, 2);
  gtk_widget_set_margin_top(action_area, 2);
  gtk_widget_set_margin_bottom(action_area, 2);

  switch (dlg->type) {
  case GUI_DIALOG_WINDOW:
    {
      GtkWidget *window;

      window = gtk_window_new();
      gtk_widget_set_name(window, "Freeciv");
      setup_dialog(window, toplevel);

      gtk_window_set_child(GTK_WINDOW(window), dlg->grid);
      dlg->v.window = window;
      g_signal_connect(window, "close-request",
        G_CALLBACK(gui_dialog_delete_handler), dlg);

    }
    break;
  case GUI_DIALOG_TAB:
    {
      GtkWidget *hbox, *label, *button;
      gchar *buf;

      hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);

      label = gtk_label_new(NULL);
      gtk_widget_set_halign(label, GTK_ALIGN_START);
      gtk_widget_set_valign(label, GTK_ALIGN_CENTER);
      gtk_widget_set_margin_start(label, 4);
      gtk_widget_set_margin_end(label, 4);
      gtk_widget_set_margin_top(label, 0);
      gtk_widget_set_margin_bottom(label, 0);
      gtk_box_append(GTK_BOX(hbox), label);

      button = gtk_button_new();
      gtk_button_set_has_frame(GTK_BUTTON(button), FALSE);
      g_signal_connect_swapped(button, "clicked",
                               G_CALLBACK(gui_dialog_delete_tab_handler), dlg);

      buf = g_strdup_printf(_("Close Tab:\n%s"), _("Ctrl+W"));
      gtk_widget_set_tooltip_text(button, buf);
      g_free(buf);

      gtk_button_set_icon_name(GTK_BUTTON(button), "window-close");

      gtk_box_append(GTK_BOX(hbox), button);

      gtk_widget_set_visible(hbox, TRUE);

      gtk_notebook_append_page(GTK_NOTEBOOK(notebook), dlg->grid, hbox);
      dlg->v.tab.handler_id
        = g_signal_connect(notebook, "switch-page",
                           G_CALLBACK(gui_dialog_switch_page_handler), dlg);
      dlg->v.tab.child = dlg->grid;

      dlg->v.tab.label = label;
      dlg->v.tab.notebook = GTK_WIDGET(notebook);

      controller = GTK_EVENT_CONTROLLER(gtk_gesture_click_new());
      g_signal_connect(controller, "pressed",
                       G_CALLBACK(click_on_tab_callback), dlg);
      gtk_widget_add_controller(hbox, controller);
    }
    break;
  }

  dlg->actions = action_area;

  dlg->response_callback = gui_dialog_destroyed;

  dlg->id = dialog_id_counter;
  dialog_id_counter++;
  dlg->return_dialog_id = -1;

  g_signal_connect(dlg->grid, "destroy",
                   G_CALLBACK(gui_dialog_destroy_handler), dlg);
  controller = gtk_event_controller_key_new();
  g_signal_connect(controller, "key-pressed",
                   G_CALLBACK(gui_dialog_key_press_handler), dlg);
  gtk_widget_add_controller(dlg->grid, controller);

  g_object_set_data(G_OBJECT(dlg->grid), "gui-dialog-data", dlg);
}

/**********************************************************************//**
  Called when a dialog button is activated.
**************************************************************************/
static void action_widget_activated(GtkWidget *button, GtkWidget *vbox)
{
  struct gui_dialog *dlg =
    g_object_get_data(G_OBJECT(vbox), "gui-dialog-data");
  gpointer arg2 =
    g_object_get_data(G_OBJECT(button), "gui-dialog-response-data");

  gui_dialog_response(dlg, GPOINTER_TO_INT(arg2));
}

/**********************************************************************//**
  Places a button into a dialog, taking care of setting up signals, etc.
**************************************************************************/
static void gui_dialog_pack_button(struct gui_dialog *dlg, GtkWidget *button,
                                   int response)
{
  gint signal_id;

  fc_assert_ret(GTK_IS_BUTTON(button));

  g_object_set_data(G_OBJECT(button), "gui-dialog-response-data",
      GINT_TO_POINTER(response));

  if ((signal_id = g_signal_lookup("clicked", GTK_TYPE_BUTTON))) {
    GClosure *closure;

    closure = g_cclosure_new_object(G_CALLBACK(action_widget_activated),
                                    G_OBJECT(dlg->grid));
    g_signal_connect_closure_by_id(button, signal_id, 0, closure, FALSE);
  }

  gui_dialog_add_action_widget(dlg, button);
  gtk_size_group_add_widget(dlg->gui_button, button);
}

/**********************************************************************//**
  Adds a button to a dialog.
**************************************************************************/
GtkWidget *gui_dialog_add_button(struct gui_dialog *dlg,
                                 const char *icon_name,
                                 const char *text, int response)
{
  GtkWidget *button;

  button = icon_label_button_new(icon_name, text);
  gui_dialog_pack_button(dlg, button, response);

  return button;
}

/**********************************************************************//**
  Adds a widget to a dialog.
**************************************************************************/
GtkWidget *gui_dialog_add_action_widget(struct gui_dialog *dlg,
                                        GtkWidget *widget)
{
  gtk_box_append(GTK_BOX(dlg->actions), widget);

  gtk_size_group_add_widget(gui_action, widget);

  return widget;
}

/**********************************************************************//**
  Change the sensitivity of a dialog button.
**************************************************************************/
void gui_dialog_set_response_sensitive(struct gui_dialog *dlg,
                                       int response, bool setting)
{
  GtkWidget *iter;

  for (iter = gtk_widget_get_first_child(dlg->actions);
       iter != NULL;
       iter = gtk_widget_get_next_sibling(iter)) {
    if (GTK_IS_BUTTON(iter)) {
      gpointer data = g_object_get_data(G_OBJECT(iter),
                                        "gui-dialog-response-data");

      if (response == GPOINTER_TO_INT(data)) {
        gtk_widget_set_sensitive(iter, setting);
      }
    }
  }
}

/**********************************************************************//**
  Get the dialog's toplevel window.
**************************************************************************/
GtkWidget *gui_dialog_get_toplevel(struct gui_dialog *dlg)
{
  return gtk_widget_get_ancestor(dlg->grid, GTK_TYPE_WINDOW);
}

/**********************************************************************//**
  Show the dialog contents, but not the dialog per se.
**************************************************************************/
void gui_dialog_show_all(struct gui_dialog *dlg)
{
  gtk_widget_set_visible(dlg->grid, TRUE);

  if (dlg->type == GUI_DIALOG_TAB) {
    GtkWidget *iter;
    gint num_visible = 0;

    for (iter = gtk_widget_get_first_child(dlg->actions);
         iter != NULL;
         iter = gtk_widget_get_next_sibling(iter)) {
      if (!GTK_IS_BUTTON(iter)) {
        num_visible++;
      } else {
        gpointer data = g_object_get_data(G_OBJECT(iter),
                                          "gui-dialog-response-data");
        int response = GPOINTER_TO_INT(data);

        if (response != GTK_RESPONSE_CLOSE
            && response != GTK_RESPONSE_CANCEL) {
          num_visible++;
        } else {
          gtk_widget_set_visible(iter, FALSE);
        }
      }
    }

    if (num_visible == 0) {
      gtk_widget_set_visible(dlg->actions, FALSE);
    }
  }
}

/**********************************************************************//**
  Notify the user the dialog has changed.
**************************************************************************/
void gui_dialog_present(struct gui_dialog *dlg)
{
  fc_assert_ret(NULL != dlg);

  switch (dlg->type) {
  case GUI_DIALOG_WINDOW:
    gtk_widget_set_visible(dlg->v.window, TRUE);
    break;
  case GUI_DIALOG_TAB:
    {
      GtkNotebook *notebook = GTK_NOTEBOOK(dlg->v.tab.notebook);
      gint current, n;

      current = gtk_notebook_get_current_page(notebook);
      n = gtk_notebook_page_num(notebook, dlg->grid);

      if (current != n) {
        gtk_widget_add_css_class(dlg->v.tab.label, "notice");
      }
    }
    break;
  }
}

/**********************************************************************//**
  Raise dialog to top.
**************************************************************************/
void gui_dialog_raise(struct gui_dialog *dlg)
{
  fc_assert_ret(NULL != dlg);

  switch (dlg->type) {
  case GUI_DIALOG_WINDOW:
    gtk_window_present(GTK_WINDOW(dlg->v.window));
    break;
  case GUI_DIALOG_TAB:
    {
      GtkNotebook *notebook = GTK_NOTEBOOK(dlg->v.tab.notebook);
      gint n;

      n = gtk_notebook_page_num(notebook, dlg->grid);
      gtk_notebook_set_current_page(notebook, n);
    }
    break;
  }
}

/**********************************************************************//**
  Alert the user to an important event.
**************************************************************************/
void gui_dialog_alert(struct gui_dialog *dlg)
{
  fc_assert_ret(NULL != dlg);

  switch (dlg->type) {
  case GUI_DIALOG_WINDOW:
    break;
  case GUI_DIALOG_TAB:
    {
      GtkNotebook *notebook = GTK_NOTEBOOK(dlg->v.tab.notebook);
      gint current, n;

      current = gtk_notebook_get_current_page(notebook);
      n = gtk_notebook_page_num(notebook, dlg->grid);

      if (current != n) {
        /* Have only alert - remove notice if it exist. */
        gtk_widget_remove_css_class(dlg->v.tab.label, "notice");
        gtk_widget_add_css_class(dlg->v.tab.label, "alert");
      }
    }
    break;
  }
}

/**********************************************************************//**
  Sets the dialog's default size (applies to toplevel windows only).
**************************************************************************/
void gui_dialog_set_default_size(struct gui_dialog *dlg, int width, int height)
{
  dlg->default_width = width;
  dlg->default_height = height;
  switch (dlg->type) {
  case GUI_DIALOG_WINDOW:
    gtk_window_set_default_size(GTK_WINDOW(dlg->v.window), width, height);
    break;
  case GUI_DIALOG_TAB:
    break;
  }
}

/**********************************************************************//**
  Changes a dialog's title.
**************************************************************************/
void gui_dialog_set_title(struct gui_dialog *dlg, const char *title)
{
  if (dlg->title) {
    free(dlg->title);
  }
  dlg->title = fc_strdup(title);
  switch (dlg->type) {
  case GUI_DIALOG_WINDOW:
    gtk_window_set_title(GTK_WINDOW(dlg->v.window), title);
    break;
  case GUI_DIALOG_TAB:
    gtk_label_set_text_with_mnemonic(GTK_LABEL(dlg->v.tab.label), title);
    break;
  }
}

/**********************************************************************//**
  Destroy a dialog.
**************************************************************************/
void gui_dialog_destroy(struct gui_dialog *dlg)
{
  switch (dlg->type) {
  case GUI_DIALOG_WINDOW:
    gtk_window_destroy(GTK_WINDOW(dlg->v.window));
    break;
  case GUI_DIALOG_TAB:
    {
      gint n;

      n = gtk_notebook_page_num(GTK_NOTEBOOK(dlg->v.tab.notebook), dlg->grid);
      gtk_notebook_remove_page(GTK_NOTEBOOK(dlg->v.tab.notebook), n);
    }
    break;
  }
}

/**********************************************************************//**
  Destroy all dialogs.
**************************************************************************/
void gui_dialog_destroy_all(void)
{
  GList *it, *it_next;

  for (it = dialog_list; it; it = it_next) {
    it_next = g_list_next(it);

    gui_dialog_destroy((struct gui_dialog *)it->data);
  }
}

/**********************************************************************//**
  Set the response callback for a dialog.
**************************************************************************/
void gui_dialog_response_set_callback(struct gui_dialog *dlg,
    GUI_DIALOG_RESPONSE_FUN fun)
{
  dlg->response_callback = fun;
}

/**********************************************************************//**
  When the dlg dialog is destroyed the return_dialog will be raised
**************************************************************************/
void gui_dialog_set_return_dialog(struct gui_dialog *dlg,
                                  struct gui_dialog *return_dialog)
{
  if (return_dialog == NULL) {
    dlg->return_dialog_id = -1;
  } else {
    dlg->return_dialog_id = return_dialog->id;
  }
}

/**********************************************************************//**
  Updates a gui font style.
**************************************************************************/
void gui_update_font(const char *font_name, const char *font_value)
{
  char *str;
  GtkCssProvider *provider;
  PangoFontDescription *desc;
  int size;
  const char *fam;
  const char *style;
  const char *weight;

  desc = pango_font_description_from_string(font_value);

  if (desc == NULL) {
    return;
  }

  fam = pango_font_description_get_family(desc);

  if (fam == NULL) {
    return;
  }

  if (pango_font_description_get_style(desc) == PANGO_STYLE_ITALIC) {
    style = "\n font-style: italic;";
  } else {
    style = "";
  }

  if (pango_font_description_get_weight(desc) >= 700) {
    weight = "\n font-weight: bold;";
  } else {
    weight = "";
  }

  size = pango_font_description_get_size(desc);

  if (size != 0) {
    if (pango_font_description_get_size_is_absolute(desc)) {
      str = g_strdup_printf("#Freeciv #%s { font-family: %s; font-size: %dpx;%s%s}",
                            font_name, fam, size / PANGO_SCALE, style, weight);
    } else {
      str = g_strdup_printf("#Freeciv #%s { font-family: %s; font-size: %dpt;%s%s}",
                            font_name, fam, size / PANGO_SCALE, style, weight);
    }
  } else {
    str = g_strdup_printf("#Freeciv #%s { font-family: %s;%s%s}",
                          font_name, fam, style, weight);
  }

  pango_font_description_free(desc);

  provider = gtk_css_provider_new();
  gtk_css_provider_load_from_data(GTK_CSS_PROVIDER(provider),
                                  str, -1);
  gtk_style_context_add_provider_for_display(
    gtk_widget_get_display(toplevel), GTK_STYLE_PROVIDER(provider),
    GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  g_free(str);
}

/**********************************************************************//**
  Update a font option which is not attached to a widget.
**************************************************************************/
void gui_update_font_full(const char *font_name, const char *font_value,
                          PangoFontDescription **font_desc)
{
  PangoFontDescription *f_desc;

  gui_update_font(font_name, font_value);

  f_desc = pango_font_description_from_string(font_value);
  pango_font_description_free(*font_desc);

  *font_desc = f_desc;
}

/**********************************************************************//**
  Temporarily disable signal invocation of the given callback for the given
  GObject. Re-enable the signal with enable_gobject_callback.
**************************************************************************/
void disable_gobject_callback(GObject *obj, GCallback cb)
{
  gulong hid;

  if (!obj || !cb) {
    return;
  }

  hid = g_signal_handler_find(obj, G_SIGNAL_MATCH_FUNC,
                              0, 0, NULL, cb, NULL);
  g_signal_handler_block(obj, hid);
}

/**********************************************************************//**
  Re-enable a signal callback blocked by disable_gobject_callback.
**************************************************************************/
void enable_gobject_callback(GObject *obj, GCallback cb)
{
  gulong hid;

  if (!obj || !cb) {
    return;
  }

  hid = g_signal_handler_find(obj, G_SIGNAL_MATCH_FUNC,
                              0, 0, NULL, cb, NULL);
  g_signal_handler_unblock(obj, hid);
}

/**********************************************************************//**
  Convenience function to add a column to a GtkTreeView. Returns the added
  column, or NULL if an error occurred.
**************************************************************************/
GtkTreeViewColumn *add_treeview_column(GtkWidget *view, const char *title,
                                       GType gtype, int model_index)
{
  GtkTreeViewColumn *col;
  GtkCellRenderer *rend;
  const char *attr;

  fc_assert_ret_val(view != NULL, NULL);
  fc_assert_ret_val(GTK_IS_TREE_VIEW(view), NULL);
  fc_assert_ret_val(title != NULL, NULL);

  if (gtype == G_TYPE_BOOLEAN) {
    rend = gtk_cell_renderer_toggle_new();
    attr = "active";
  } else if (gtype == GDK_TYPE_PIXBUF) {
    rend = gtk_cell_renderer_pixbuf_new();
    attr = "pixbuf";
  } else {
    rend = gtk_cell_renderer_text_new();
    attr = "text";
  }

  col = gtk_tree_view_column_new_with_attributes(title, rend, attr,
                                                 model_index, NULL);
  gtk_tree_view_append_column(GTK_TREE_VIEW(view), col);

  return col;
}

/**********************************************************************//**
  Prepare dialog tab style provider.
**************************************************************************/
void dlg_tab_provider_prepare(void)
{
  dlg_tab_provider = gtk_css_provider_new();

  gtk_css_provider_load_from_data(dlg_tab_provider,
                                  ".alert {\n"
                                  "color: rgba(255, 0, 0, 255);\n"
                                  "}\n"
                                  ".notice {\n"
                                  "color: rgba(0, 0, 255, 255);\n"
                                  "}\n",
                                  -1);

  gtk_style_context_add_provider_for_display(
                                     gtk_widget_get_display(toplevel),
                                     GTK_STYLE_PROVIDER(dlg_tab_provider),
                                     GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}

/**********************************************************************//**
  Add widget to the gui_dialog grid
**************************************************************************/
void gui_dialog_add_content_widget(struct gui_dialog *dlg, GtkWidget *wdg)
{
  if (dlg->vertical_content) {
    gtk_grid_attach(GTK_GRID(dlg->grid), wdg,
                    0, dlg->content_counter++, 1, 1);
  } else {
    gtk_grid_attach(GTK_GRID(dlg->grid), wdg,
                    dlg->content_counter++, 0, 1, 1);
  }
}

struct blocking_dialog_data {
  GMainLoop *loop;
  gint response;
};

/**********************************************************************//**
  Received a response to a blocking dialog
**************************************************************************/
static void blocking_dialog_response(GtkWidget *dlg, gint response, void *data)
{
  struct blocking_dialog_data *bd_data = (struct blocking_dialog_data *)data;

  bd_data->response = response;

  g_main_loop_quit(bd_data->loop);
}

/**********************************************************************//**
  Present a blocking dialog and wait for response
**************************************************************************/
gint blocking_dialog(GtkWidget *dlg)
{
  struct blocking_dialog_data data;
  GMainLoop *dlg_loop;

  gtk_widget_set_visible(dlg, TRUE);
  dlg_loop = g_main_loop_new(NULL, FALSE);
  data.loop = dlg_loop;
  g_signal_connect(dlg, "response", G_CALLBACK(blocking_dialog_response),
                   &data);

  g_main_loop_run(dlg_loop);

  return data.response;
}

/**********************************************************************//**
  Nullify GtkWidget pointer when widget is destroyed
**************************************************************************/
void widget_destroyed(GtkWidget *wdg, void *data)
{
  *(GtkWidget **)data = NULL;
}

/**********************************************************************//**
  Get child widget for a widget whose own type is not known
  (without further GTK_IS_...() checks) in the caller side.
**************************************************************************/
GtkWidget *widget_get_child(GtkWidget *wdg)
{
  return gtk_widget_get_first_child(wdg);
}
