/*
 * gedit-notebook.c
 * This file is part of gedit
 *
 * Copyright (C) 2005 - Paolo Maggi
 * Copyright (C) 2015 - Sébastien Wilmet
 *
 * 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 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/>.
 */

/* This file is a modified version of the epiphany file ephy-notebook.c
 * Here the relevant copyright:
 *
 *  Copyright (C) 2002 Christophe Fergeau
 *  Copyright (C) 2003 Marco Pesenti Gritti
 *  Copyright (C) 2003, 2004 Christian Persch
 */

#include "gedit-notebook.h"
#include "gedit-tab-label.h"

#define GEDIT_NOTEBOOK_GROUP_NAME "GeditNotebookGroup"

/* The DND targets defined in GeditView start at 100.
 * Those defined in GtkSourceView start at 200.
 */
#define TARGET_TAB 150

struct _GeditNotebookPrivate
{
	/* History of focused pages. The first element contains the most recent
	 * one.
	 */
	GList *focused_pages;

	guint ignore_focused_page_update : 1;
};

G_DEFINE_TYPE_WITH_PRIVATE (GeditNotebook, gedit_notebook, GTK_TYPE_NOTEBOOK)

enum
{
	TAB_CLOSE_REQUEST,
	SHOW_POPUP_MENU,
	CHANGE_TO_PAGE,
	LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

static void
gedit_notebook_finalize (GObject *object)
{
	GeditNotebook *notebook = GEDIT_NOTEBOOK (object);

	g_list_free (notebook->priv->focused_pages);

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

static void
gedit_notebook_grab_focus (GtkWidget *widget)
{
	GtkNotebook *notebook = GTK_NOTEBOOK (widget);
	gint current_page;
	GtkWidget *tab;

	current_page = gtk_notebook_get_current_page (notebook);
	tab = gtk_notebook_get_nth_page (notebook, current_page);

	if (tab != NULL)
	{
		gtk_widget_grab_focus (tab);
	}
}

static gint
find_tab_num_at_pos (GtkNotebook *notebook,
                     gint         screen_x,
                     gint         screen_y)
{
	GtkPositionType tab_pos;
	GtkAllocation tab_allocation;
	gint page_num;

	tab_pos = gtk_notebook_get_tab_pos (notebook);

	for (page_num = 0; ; page_num++)
	{
		GtkWidget *page;
		GtkWidget *tab_label;
		gint max_x, max_y, x_root, y_root;

		page = gtk_notebook_get_nth_page (notebook, page_num);

		if (page == NULL)
		{
			break;
		}

		tab_label = gtk_notebook_get_tab_label (notebook, page);
		g_return_val_if_fail (tab_label != NULL, -1);

		if (!gtk_widget_get_mapped (tab_label))
		{
			continue;
		}

		gdk_window_get_origin (gtk_widget_get_window (tab_label), &x_root, &y_root);

		gtk_widget_get_allocation (tab_label, &tab_allocation);
		max_x = x_root + tab_allocation.x + tab_allocation.width;
		max_y = y_root + tab_allocation.y + tab_allocation.height;

		if ((tab_pos == GTK_POS_TOP || tab_pos == GTK_POS_BOTTOM) &&
		    screen_x <= max_x)
		{
			return page_num;
		}

		if ((tab_pos == GTK_POS_LEFT || tab_pos == GTK_POS_RIGHT) &&
		    screen_y <= max_y)
		{
			return page_num;
		}
	}

	return -1;
}

static gboolean
gedit_notebook_button_press_event (GtkWidget      *widget,
				   GdkEventButton *event)
{
	GtkNotebook *notebook = GTK_NOTEBOOK (widget);

	if (event->type == GDK_BUTTON_PRESS &&
	    (event->state & gtk_accelerator_get_default_mod_mask ()) == 0)
	{
		gint tab_clicked;

		tab_clicked = find_tab_num_at_pos (notebook, event->x_root, event->y_root);
		if (tab_clicked >= 0)
		{
			GtkWidget *tab;

			tab = gtk_notebook_get_nth_page (notebook, tab_clicked);
			switch (event->button)
			{
				case GDK_BUTTON_SECONDARY:
					g_signal_emit (G_OBJECT (widget), signals[SHOW_POPUP_MENU], 0, event, tab);
					return GDK_EVENT_STOP;

				case GDK_BUTTON_MIDDLE:
					g_signal_emit (G_OBJECT (notebook), signals[TAB_CLOSE_REQUEST], 0, tab);
					return GDK_EVENT_STOP;

				default:
					break;
			}
		}
	}

	return GTK_WIDGET_CLASS (gedit_notebook_parent_class)->button_press_event (widget, event);
}

/*
 * We need to override this because when we don't show the tabs, like in
 * fullscreen we need to have wrap around too
 */
static gboolean
gedit_notebook_change_current_page (GtkNotebook *notebook,
				    gint         offset)
{
	gint current;

	current = gtk_notebook_get_current_page (notebook);

	if (current != -1)
	{
		gint target;
		gboolean wrap_around;

		target = current + offset;

		g_object_get (gtk_widget_get_settings (GTK_WIDGET (notebook)),
			      "gtk-keynav-wrap-around", &wrap_around,
			      NULL);

		if (wrap_around)
		{
			if (target < 0)
			{
				target = gtk_notebook_get_n_pages (notebook) - 1;
			}
			else if (target >= gtk_notebook_get_n_pages (notebook))
			{
				target = 0;
			}
		}

		gtk_notebook_set_current_page (notebook, target);
	}
	else
	{
		gtk_widget_error_bell (GTK_WIDGET (notebook));
	}

	return TRUE;
}

static void
gedit_notebook_switch_page (GtkNotebook *notebook,
                            GtkWidget   *page,
                            guint        page_num)
{
	GeditNotebookPrivate *priv = GEDIT_NOTEBOOK (notebook)->priv;

	GTK_NOTEBOOK_CLASS (gedit_notebook_parent_class)->switch_page (notebook, page, page_num);

	if (!priv->ignore_focused_page_update)
	{
		gint page_num;

		/* Get again page_num and page, the signal handler may have
		 * changed them.
		 */
		page_num = gtk_notebook_get_current_page (notebook);
		if (page_num != -1)
		{
			GtkWidget *page = gtk_notebook_get_nth_page (notebook, page_num);
			g_assert (page != NULL);

			/* Remove the old page, we dont want to grow unnecessarily
			 * the list.
			 */
			priv->focused_pages = g_list_remove (priv->focused_pages, page);

			priv->focused_pages = g_list_prepend (priv->focused_pages, page);
		}
	}

	/* give focus to the tab */
	gtk_widget_grab_focus (page);
}

static void
close_button_clicked_cb (GeditTabLabel *tab_label,
			 GeditNotebook *notebook)
{
	GeditTab *tab;

	tab = gedit_tab_label_get_tab (tab_label);
	g_signal_emit (notebook, signals[TAB_CLOSE_REQUEST], 0, tab);
}

static void
switch_to_last_focused_page (GeditNotebook *notebook,
			     GeditTab      *tab)
{
	if (notebook->priv->focused_pages != NULL)
	{
		GList *node;
		GtkWidget *page;
		gint page_num;

		node = notebook->priv->focused_pages;
		page = GTK_WIDGET (node->data);

		page_num = gtk_notebook_page_num (GTK_NOTEBOOK (notebook), page);
		g_return_if_fail (page_num != -1);

		gtk_notebook_set_current_page (GTK_NOTEBOOK (notebook), page_num);
	}
}

static void
drag_data_received_cb (GtkWidget        *widget,
		       GdkDragContext   *context,
		       gint              x,
		       gint              y,
		       GtkSelectionData *selection_data,
		       guint             info,
		       guint             timestamp)
{
	GtkWidget *notebook;
	GtkWidget *new_notebook;
	GtkWidget *page;

	if (info != TARGET_TAB)
	{
		return;
	}

	notebook = gtk_drag_get_source_widget (context);

	if (!GTK_IS_WIDGET (notebook))
	{
		return;
	}

	page = *(GtkWidget **) gtk_selection_data_get_data (selection_data);
	g_return_if_fail (page != NULL);

	/* We need to iterate and get the notebook of the target view
	 * because we can have several notebooks per window.
	 */
	new_notebook = gtk_widget_get_ancestor (widget, GEDIT_TYPE_NOTEBOOK);
	g_return_if_fail (new_notebook != NULL);

	if (notebook != new_notebook)
	{
		gedit_notebook_move_tab (GEDIT_NOTEBOOK (notebook),
					 GEDIT_NOTEBOOK (new_notebook),
					 GEDIT_TAB (page),
					 0);
	}

	gtk_drag_finish (context, TRUE, TRUE, timestamp);
}

static void
gedit_notebook_page_removed (GtkNotebook *notebook,
                             GtkWidget   *page,
                             guint        page_num)
{
	GeditNotebookPrivate *priv = GEDIT_NOTEBOOK (notebook)->priv;
	gboolean current_page;

	/* The page removed was the current page. */
	current_page = (priv->focused_pages != NULL &&
			priv->focused_pages->data == page);

	priv->focused_pages = g_list_remove (priv->focused_pages, page);

	if (current_page)
	{
		switch_to_last_focused_page (GEDIT_NOTEBOOK (notebook),
					     GEDIT_TAB (page));
	}
}

static void
gedit_notebook_page_added (GtkNotebook *notebook,
                           GtkWidget   *page,
                           guint        page_num)
{
	GtkWidget *tab_label;
	GeditView *view;

	g_return_if_fail (GEDIT_IS_TAB (page));

	tab_label = gtk_notebook_get_tab_label (notebook, page);
	g_return_if_fail (GEDIT_IS_TAB_LABEL (tab_label));

	/* For a DND from one notebook to another, the same tab_label can be
	 * used, so we need to connect the signal here.
	 * More precisely, the same tab_label is used when the drop zone is in
	 * the tab labels (not the GeditView), that is, when the DND is handled
	 * by the GtkNotebook implementation.
	 */
	g_signal_connect (tab_label,
	                  "close-clicked",
	                  G_CALLBACK (close_button_clicked_cb),
	                  notebook);

	view = gedit_tab_get_view (GEDIT_TAB (page));
	g_signal_connect (view,
			  "drag-data-received",
			  G_CALLBACK (drag_data_received_cb),
			  NULL);
}

static void
gedit_notebook_remove (GtkContainer *container,
                       GtkWidget    *widget)
{
	GtkNotebook *notebook = GTK_NOTEBOOK (container);
	GeditNotebookPrivate *priv = GEDIT_NOTEBOOK (container)->priv;
	GtkWidget *tab_label;
	GeditView *view;

	g_return_if_fail (GEDIT_IS_TAB (widget));

	tab_label = gtk_notebook_get_tab_label (notebook, widget);
	g_return_if_fail (GEDIT_IS_TAB_LABEL (tab_label));

	/* For a DND from one notebook to another, the same tab_label can be
	 * used, so we need to disconnect the signal.
	 */
	g_signal_handlers_disconnect_by_func (tab_label,
					      G_CALLBACK (close_button_clicked_cb),
					      notebook);

	view = gedit_tab_get_view (GEDIT_TAB (widget));
	g_signal_handlers_disconnect_by_func (view, drag_data_received_cb, NULL);

	/* This is where GtkNotebook will remove the page. By doing so, it
	 * will also switch to a new page, messing up our focus list. So we
	 * set a flag here to ignore the switch temporarily.
	 */
	priv->ignore_focused_page_update = TRUE;

	if (GTK_CONTAINER_CLASS (gedit_notebook_parent_class)->remove != NULL)
	{
		GTK_CONTAINER_CLASS (gedit_notebook_parent_class)->remove (container,
									   widget);
	}

	priv->ignore_focused_page_update = FALSE;
}

static gboolean
gedit_notebook_change_to_page (GeditNotebook *notebook,
                               gint           page_num)
{
	gint n_pages;

	n_pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (notebook));

	if (page_num >= n_pages)
	{
		return FALSE;
	}

	gtk_notebook_set_current_page (GTK_NOTEBOOK (notebook),
	                               page_num);

	return TRUE;
}

static void
gedit_notebook_class_init (GeditNotebookClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
	GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
	GtkNotebookClass *notebook_class = GTK_NOTEBOOK_CLASS (klass);
	GtkBindingSet *binding_set;
	gint i;

	object_class->finalize = gedit_notebook_finalize;

	widget_class->grab_focus = gedit_notebook_grab_focus;
	widget_class->button_press_event = gedit_notebook_button_press_event;

	container_class->remove = gedit_notebook_remove;

	notebook_class->change_current_page = gedit_notebook_change_current_page;
	notebook_class->switch_page = gedit_notebook_switch_page;
	notebook_class->page_removed = gedit_notebook_page_removed;
	notebook_class->page_added = gedit_notebook_page_added;

	klass->change_to_page = gedit_notebook_change_to_page;

	signals[TAB_CLOSE_REQUEST] =
		g_signal_new ("tab-close-request",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (GeditNotebookClass, tab_close_request),
			      NULL, NULL, NULL,
			      G_TYPE_NONE,
			      1,
			      GEDIT_TYPE_TAB);

	signals[SHOW_POPUP_MENU] =
		g_signal_new ("show-popup-menu",
			      G_OBJECT_CLASS_TYPE (object_class),
			      G_SIGNAL_RUN_FIRST,
			      G_STRUCT_OFFSET (GeditNotebookClass, show_popup_menu),
			      NULL, NULL, NULL,
			      G_TYPE_NONE,
			      2,
			      GDK_TYPE_EVENT | G_SIGNAL_TYPE_STATIC_SCOPE,
			      GEDIT_TYPE_TAB);

	signals[CHANGE_TO_PAGE] =
		g_signal_new ("change-to-page",
		              G_TYPE_FROM_CLASS (object_class),
		              G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
		              G_STRUCT_OFFSET (GeditNotebookClass, change_to_page),
		              NULL, NULL, NULL,
		              G_TYPE_BOOLEAN, 1,
		              G_TYPE_INT);

	binding_set = gtk_binding_set_by_class (klass);
	for (i = 1; i < 10; i++)
	{
		gtk_binding_entry_add_signal (binding_set,
		                              GDK_KEY_0 + i, GDK_MOD1_MASK,
		                              "change-to-page", 1,
		                              G_TYPE_INT, i - 1);
	}
}

/**
 * gedit_notebook_new:
 *
 * Creates a new #GeditNotebook object.
 *
 * Returns: a new #GeditNotebook
 */
GtkWidget *
gedit_notebook_new (void)
{
	return GTK_WIDGET (g_object_new (GEDIT_TYPE_NOTEBOOK, NULL));
}

static void
gedit_notebook_init (GeditNotebook *notebook)
{
	notebook->priv = gedit_notebook_get_instance_private (notebook);

	gtk_notebook_set_scrollable (GTK_NOTEBOOK (notebook), TRUE);
	gtk_notebook_set_show_border (GTK_NOTEBOOK (notebook), FALSE);
	gtk_notebook_set_show_tabs (GTK_NOTEBOOK (notebook), TRUE);
	gtk_notebook_set_group_name (GTK_NOTEBOOK (notebook),
	                             GEDIT_NOTEBOOK_GROUP_NAME);
	gtk_container_set_border_width (GTK_CONTAINER (notebook), 0);
}

/**
 * gedit_notebook_add_tab:
 * @notebook: a #GeditNotebook
 * @tab: a #GeditTab
 * @position: the position where the @tab should be added
 * @jump_to: %TRUE to set the @tab as active
 *
 * Adds the specified @tab to the @notebook.
 */
void
gedit_notebook_add_tab (GeditNotebook *notebook,
		        GeditTab      *tab,
		        gint           position,
		        gboolean       jump_to)
{
	GtkWidget *tab_label;
	GeditView *view;
	GtkTargetList *target_list;

	g_return_if_fail (GEDIT_IS_NOTEBOOK (notebook));
	g_return_if_fail (GEDIT_IS_TAB (tab));

	tab_label = gedit_tab_label_new (tab);

	gtk_notebook_insert_page (GTK_NOTEBOOK (notebook),
				  GTK_WIDGET (tab),
				  tab_label,
				  position);

	gtk_notebook_set_tab_reorderable (GTK_NOTEBOOK (notebook),
	                                  GTK_WIDGET (tab),
	                                  TRUE);

	gtk_notebook_set_tab_detachable (GTK_NOTEBOOK (notebook),
	                                 GTK_WIDGET (tab),
	                                 TRUE);

	gtk_container_child_set (GTK_CONTAINER (notebook),
				 GTK_WIDGET (tab),
				 "tab-expand", TRUE,
				 NULL);

	/* Drag and drop support: move a tab to another notebook, with the drop
	 * zone in the GeditView. The drop zone in the tab labels is already
	 * implemented by GtkNotebook.
	 */
	view = gedit_tab_get_view (tab);
	target_list = gtk_drag_dest_get_target_list (GTK_WIDGET (view));

	if (target_list != NULL)
	{
		gtk_target_list_add (target_list,
		                     gdk_atom_intern_static_string ("GTK_NOTEBOOK_TAB"),
		                     GTK_TARGET_SAME_APP,
		                     TARGET_TAB);
	}

	/* The signal handler may have reordered the tabs */
	position = gtk_notebook_page_num (GTK_NOTEBOOK (notebook),
					  GTK_WIDGET (tab));

	if (jump_to)
	{
		gtk_notebook_set_current_page (GTK_NOTEBOOK (notebook), position);
		gtk_widget_grab_focus (GTK_WIDGET (tab));
	}
}

/**
 * gedit_notebook_move_tab:
 * @src: a #GeditNotebook
 * @dest: a #GeditNotebook
 * @tab: a #GeditTab
 * @dest_position: the position for @tab
 *
 * Moves @tab from @src to @dest.
 * If @dest_position is greater than or equal to the number of tabs
 * of the destination nootebook or negative, tab will be moved to the
 * end of the tabs.
 */
void
gedit_notebook_move_tab (GeditNotebook *src,
                         GeditNotebook *dest,
                         GeditTab      *tab,
                         gint           dest_position)
{
	g_return_if_fail (GEDIT_IS_NOTEBOOK (src));
	g_return_if_fail (GEDIT_IS_NOTEBOOK (dest));
	g_return_if_fail (src != dest);
	g_return_if_fail (GEDIT_IS_TAB (tab));

	/* Make sure the tab isn't destroyed while we move it. */
	g_object_ref (tab);

	/* Make sure the @src notebook isn't destroyed during the tab
	 * detachment, to prevent a crash in gtk_notebook_detach_tab(). In fact,
	 * if @tab is the last tab of @src, and if @src is not the last notebook
	 * of the GeditMultiNotebook, then @src will be destroyed when
	 * gtk_container_remove() is called by gtk_notebook_detach_tab().
	 */
	g_object_ref (src);
	gtk_notebook_detach_tab (GTK_NOTEBOOK (src), GTK_WIDGET (tab));
	g_object_unref (src);

	gedit_notebook_add_tab (dest, tab, dest_position, TRUE);

	g_object_unref (tab);
}

/**
 * gedit_notebook_remove_all_tabs:
 * @notebook: a #GeditNotebook
 *
 * Removes all #GeditTab from @notebook.
 */
void
gedit_notebook_remove_all_tabs (GeditNotebook *notebook)
{
	GList *tabs;
	GList *t;

	g_return_if_fail (GEDIT_IS_NOTEBOOK (notebook));

	g_list_free (notebook->priv->focused_pages);
	notebook->priv->focused_pages = NULL;

	/* Remove tabs in reverse order since it is faster
	 * due to how GtkNotebook works.
	 */
	tabs = gtk_container_get_children (GTK_CONTAINER (notebook));
	for (t = g_list_last (tabs); t != NULL; t = t->prev)
	{
		GtkWidget *tab = t->data;
		gtk_container_remove (GTK_CONTAINER (notebook), tab);
	}

	g_list_free (tabs);
}

/* ex:set ts=8 noet: */
