geany/src/notebook.c
Nick Treleaven 37e0d1f3f1 Move GeanyDocument GUI elements and saved_encoding to private
Document struct which inherits from GeanyDocument. This is to hide
implementation fields from the plugin API, so that we can change
them if necessary.
Add DOCUMENT() macro to convert a GeanyDocument* to a Document*.
Also move UNDO_*, FileEncoding to documentprivate.h.
Move undo_action struct to document.c.


git-svn-id: https://geany.svn.sourceforge.net/svnroot/geany/trunk@2631 ea778897-0a13-0410-b9d1-a72fbfd435f5
2008-06-02 15:31:59 +00:00

451 lines
14 KiB
C

/*
* notebook.c - this file is part of Geany, a fast and lightweight IDE
*
* Copyright 2006-2008 Enrico Tröger <enrico(dot)troeger(at)uvena(dot)de>
* Copyright 2006-2008 Nick Treleaven <nick(dot)treleaven(at)btinternet(dot)com>
*
* 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, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
/*
* Notebook tab Drag 'n' Drop reordering and tab management.
*/
#include "geany.h"
#include "notebook.h"
#include "document.h"
#include "documentprivate.h"
#include "ui_utils.h"
#include "treeviews.h"
#include "support.h"
#include "callbacks.h"
#define GEANY_DND_NOTEBOOK_TAB_TYPE "geany_dnd_notebook_tab"
static const GtkTargetEntry drag_targets[] =
{
{GEANY_DND_NOTEBOOK_TAB_TYPE, GTK_TARGET_SAME_APP | GTK_TARGET_SAME_WIDGET, 0}
};
static GtkTargetEntry files_drop_targets[] = {
{ "STRING", 0, 0 },
{ "UTF8_STRING", 0, 0 },
{ "text/plain", 0, 0 },
{ "text/uri-list", 0, 0 }
};
static gboolean
notebook_drag_motion_cb(GtkWidget *widget, GdkDragContext *dc,
gint x, gint y, guint event_time, gpointer user_data);
static void
notebook_page_reordered_cb(GtkNotebook *notebook, GtkWidget *child, guint page_num,
gpointer user_data);
#if ! GTK_CHECK_VERSION(2, 8, 0)
static gboolean
notebook_motion_notify_event_cb(GtkWidget *widget, GdkEventMotion *event,
gpointer user_data);
#endif
static void
on_window_drag_data_received(GtkWidget *widget, GdkDragContext *drag_context,
gint x, gint y, GtkSelectionData *data, guint info,
guint event_time, gpointer user_data);
static gint
notebook_find_tab_num_at_pos(GtkNotebook *notebook, gint x, gint y);
static void
notebook_tab_close_clicked_cb(GtkButton *button, gpointer user_data);
static void setup_tab_dnd(void);
static void focus_sci(GtkWidget *widget, gpointer user_data)
{
gint idx = document_get_cur_idx();
if (! DOC_IDX_VALID(idx)) return;
gtk_widget_grab_focus(GTK_WIDGET(documents[idx]->sci));
}
void notebook_init()
{
/* focus the current document after clicking on a tab */
g_signal_connect_after(G_OBJECT(main_widgets.notebook), "button-release-event",
G_CALLBACK(focus_sci), NULL);
g_signal_connect(G_OBJECT(main_widgets.notebook), "drag-data-received",
G_CALLBACK(on_window_drag_data_received), NULL);
setup_tab_dnd();
}
static void setup_tab_dnd()
{
GtkWidget *notebook = main_widgets.notebook;
/* Due to a segfault with manual tab DnD setup on GTK 2.10, we must
* use the built in gtk_notebook_set_tab_reorderable from GTK 2.10.
* This means a binary compiled against < 2.10 but run on >= 2.10
* will not have tab DnD support, but this is necessary until
* there is a fix for the older tab DnD code or GTK 2.10. */
if (gtk_check_version(2, 10, 0) == NULL) /* null means version ok */
{
#if GTK_CHECK_VERSION(2, 10, 0)
g_signal_connect(G_OBJECT(notebook), "page-reordered",
G_CALLBACK(notebook_page_reordered_cb), NULL);
#endif
return;
}
/* Set up drag movement callback */
g_signal_connect(G_OBJECT(notebook), "drag-motion",
G_CALLBACK(notebook_drag_motion_cb), NULL);
/* There is a bug on GTK 2.6 with drag reordering of notebook tabs.
* Clicking (not dragging) on a notebook tab, then making a selection in the
* Scintilla widget will cause a strange selection bug.
* It seems there is a conflict; the drag cursor is shown,
* and the selection is blocked; however, when releasing the
* mouse button, the selection continues.
* Bug is present with gtk+2.6.8, not gtk+2.8.x - ntrel */
#if ! GTK_CHECK_VERSION(2, 8, 0)
/* handle higher gtk+ runtime than build environment */
if (gtk_check_version(2, 8, 0) != NULL) /* null means version ok */
{
/* workaround GTK+2.6 drag start bug when over sci widget: */
gtk_widget_add_events(notebook, GDK_POINTER_MOTION_MASK);
g_signal_connect(G_OBJECT(notebook), "motion-notify-event",
G_CALLBACK(notebook_motion_notify_event_cb), NULL);
}
#endif
/* set up drag motion for moving notebook pages */
gtk_drag_dest_set(notebook, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
drag_targets, G_N_ELEMENTS(drag_targets), GDK_ACTION_MOVE);
/* set drag source, but for GTK+2.6 it's changed in motion-notify-event handler */
gtk_drag_source_set(notebook, GDK_BUTTON1_MASK,
drag_targets, G_N_ELEMENTS(drag_targets), GDK_ACTION_MOVE);
}
#if ! GTK_CHECK_VERSION(2, 8, 0)
/* This is used to disable tab DnD when the cursor is over the
* Scintilla widget, and re-enable tab DnD when over the notebook tabs
*/
static gboolean
notebook_motion_notify_event_cb(GtkWidget *widget, GdkEventMotion *event,
gpointer user_data)
{
static gboolean drag_enabled = TRUE; /* stores current state */
GtkWidget *page = gtk_notebook_get_nth_page(GTK_NOTEBOOK(main_widgets.notebook),
gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.notebook)));
if (page == NULL || event->x < 0 || event->y < 0) return FALSE;
if (event->window == page->window) /* cursor over sci widget */
{
if (drag_enabled) gtk_drag_source_unset(widget); /* disable */
drag_enabled = FALSE;
}
else /* assume cursor over notebook tab */
{
if (! drag_enabled)
gtk_drag_source_set(widget, GDK_BUTTON1_MASK,
drag_targets, G_N_ELEMENTS(drag_targets), GDK_ACTION_MOVE);
drag_enabled = TRUE;
}
return FALSE; /* propagate event */
}
#endif
static void
notebook_page_reordered_cb(GtkNotebook *notebook, GtkWidget *child, guint page_num,
gpointer user_data)
{
/* Not necessary to update open files treeview if it's sorted.
* Note: if enabled, it's best to move the item instead of recreating all items. */
/*treeviews_openfiles_update_all();*/
}
static gboolean
notebook_drag_motion_cb(GtkWidget *widget, GdkDragContext *dc,
gint x, gint y, guint event_time, gpointer user_data)
{
static gint oldx, oldy; /* for determining direction of mouse drag */
GtkNotebook *notebook = GTK_NOTEBOOK(widget);
gint ndest = notebook_find_tab_num_at_pos(notebook, x, y);
gint ncurr = gtk_notebook_get_current_page(notebook);
if (ndest >= 0 && ndest != ncurr)
{
gboolean ok = FALSE;
/* prevent oscillation between non-homogeneous sized tabs */
switch(gtk_notebook_get_tab_pos(notebook))
{
case GTK_POS_LEFT:
case GTK_POS_RIGHT:
ok = ((ndest > ncurr) && (y > oldy)) || ((ndest < ncurr) && (y < oldy));
break;
case GTK_POS_TOP:
case GTK_POS_BOTTOM:
ok = ((ndest > ncurr) && (x > oldx)) || ((ndest < ncurr) && (x < oldx));
break;
}
if (ok)
{
gtk_notebook_reorder_child(notebook,
gtk_notebook_get_nth_page(notebook, ncurr), ndest);
notebook_page_reordered_cb(NULL, NULL, ndest, NULL);
}
}
oldx = x; oldy = y;
return FALSE;
}
/* Adapted from Epiphany absolute version in ephy-notebook.c, thanks.
* x,y are co-ordinates local to the notebook (not including border padding)
* notebook tab label widgets must not be NULL.
* N.B. This only checks the dimension that the tabs are in,
* e.g. for GTK_POS_TOP it does not check the y coordinate. */
static gint
notebook_find_tab_num_at_pos(GtkNotebook *notebook, gint x, gint y)
{
GtkPositionType tab_pos;
int page_num = 0;
GtkWidget *page;
/* deal with less than 2 pages */
switch(gtk_notebook_get_n_pages(notebook))
{case 0: return -1; case 1: return 0;}
tab_pos = gtk_notebook_get_tab_pos(notebook); /* which edge */
while ((page = gtk_notebook_get_nth_page(notebook, page_num)))
{
gint max_x, max_y;
GtkWidget *tab = gtk_notebook_get_tab_label(notebook, page);
g_return_val_if_fail(tab != NULL, -1);
if (!GTK_WIDGET_MAPPED(GTK_WIDGET(tab)))
{ /* skip hidden tabs, e.g. tabs scrolled out of view */
page_num++;
continue;
}
/* subtract notebook pos to remove possible border padding */
max_x = tab->allocation.x + tab->allocation.width -
GTK_WIDGET(notebook)->allocation.x;
max_y = tab->allocation.y + tab->allocation.height -
GTK_WIDGET(notebook)->allocation.y;
if (((tab_pos == GTK_POS_TOP)
|| (tab_pos == GTK_POS_BOTTOM))
&&(x<=max_x)) return page_num;
else if (((tab_pos == GTK_POS_LEFT)
|| (tab_pos == GTK_POS_RIGHT))
&& (y<=max_y)) return page_num;
page_num++;
}
return -1;
}
/* call this after the number of tabs in main_widgets.notebook changes. */
static void tab_count_changed(void)
{
switch (gtk_notebook_get_n_pages(GTK_NOTEBOOK(main_widgets.notebook)))
{
case 0:
/* Enables DnD for dropping files into the empty notebook widget */
gtk_drag_dest_set(main_widgets.notebook, GTK_DEST_DEFAULT_ALL,
files_drop_targets, G_N_ELEMENTS(files_drop_targets),
GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK);
break;
case 1:
/* Disables DnD for dropping files into the notebook widget and enables the DnD for moving file
* tabs. Files can still be dropped into the notebook widget because it will be handled by the
* active Scintilla Widget (only dropping to the tab bar is not possible but it should be ok) */
gtk_drag_dest_set(main_widgets.notebook, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
drag_targets, G_N_ELEMENTS(drag_targets), GDK_ACTION_MOVE);
break;
}
}
gboolean notebook_tab_label_cb(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
{
/* toggle additional widgets on double click */
if (event->type == GDK_2BUTTON_PRESS)
on_menu_toggle_all_additional_widgets1_activate(NULL, NULL);
/* close tab on middle click */
if (event->button == 2)
document_remove(gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook), GTK_WIDGET(user_data)));
return FALSE;
}
/* Returns page number of notebook page, or -1 on error */
gint notebook_new_tab(gint doc_idx)
{
GtkWidget *hbox, *ebox;
gint tabnum;
gchar *title;
GeanyDocument *this = documents[doc_idx];
Document *fdoc = DOCUMENT(this);
GtkWidget *page;
g_return_val_if_fail(doc_idx >= 0 && this != NULL, -1);
page = GTK_WIDGET(this->sci);
title = g_path_get_basename(DOC_FILENAME(doc_idx));
fdoc->tab_label = gtk_label_new(title);
ebox = gtk_event_box_new();
GTK_WIDGET_SET_FLAGS(ebox, GTK_NO_WINDOW);
g_signal_connect(G_OBJECT(ebox), "button-press-event",
G_CALLBACK(notebook_tab_label_cb), page);
hbox = gtk_hbox_new(FALSE, 2);
gtk_container_add(GTK_CONTAINER(ebox), fdoc->tab_label);
gtk_box_pack_start(GTK_BOX(hbox), ebox, FALSE, FALSE, 0);
if (file_prefs.show_tab_cross)
{
GtkWidget *image, *btn, *align;
GtkRcStyle *rcstyle;
GtkRequisition size;
btn = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(btn), GTK_RELIEF_NONE);
/* don't allow focus on the close button */
gtk_button_set_focus_on_click(GTK_BUTTON(btn), FALSE);
/* make it as small as possible */
rcstyle = gtk_rc_style_new();
rcstyle->xthickness = rcstyle->ythickness = 0;
gtk_widget_modify_style(btn, rcstyle);
gtk_rc_style_unref(rcstyle);
image = gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU);
gtk_widget_size_request(image, &size);
gtk_widget_set_size_request(btn, size.width, size.height);
gtk_container_add(GTK_CONTAINER(btn), image);
align = gtk_alignment_new(1.0, 0.0, 0.0, 0.0);
gtk_container_add(GTK_CONTAINER(align), btn);
gtk_box_pack_start(GTK_BOX(hbox), align, TRUE, TRUE, 0);
g_signal_connect(G_OBJECT(btn), "clicked",
G_CALLBACK(notebook_tab_close_clicked_cb), page);
}
gtk_widget_show_all(hbox);
fdoc->tabmenu_label = gtk_label_new(title);
gtk_misc_set_alignment(GTK_MISC(fdoc->tabmenu_label), 0.0, 0);
if (file_prefs.tab_order_ltr)
tabnum = gtk_notebook_append_page_menu(GTK_NOTEBOOK(main_widgets.notebook), page,
hbox, fdoc->tabmenu_label);
else
tabnum = gtk_notebook_insert_page_menu(GTK_NOTEBOOK(main_widgets.notebook), page,
hbox, fdoc->tabmenu_label, 0);
tab_count_changed();
/* This is where tab DnD is enabled for GTK 2.10 and higher */
#if GTK_CHECK_VERSION(2, 10, 0)
if (gtk_check_version(2, 10, 0) == NULL) /* null means version ok */
{
gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(main_widgets.notebook), page, TRUE);
}
#endif
g_free(title);
return tabnum;
}
static void
notebook_tab_close_clicked_cb(GtkButton *button, gpointer user_data)
{
gint cur_page = gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook),
GTK_WIDGET(user_data));
document_remove(cur_page);
}
/* Always use this instead of gtk_notebook_remove_page(). */
void notebook_remove_page(gint page_num)
{
gint curpage = gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.notebook));
/* Focus the next page, not the previous */
if (curpage == page_num && file_prefs.tab_order_ltr)
{
gtk_notebook_set_current_page(GTK_NOTEBOOK(main_widgets.notebook), curpage + 1);
}
/* now remove the page (so we don't temporarily switch to the previous page) */
gtk_notebook_remove_page(GTK_NOTEBOOK(main_widgets.notebook), page_num);
tab_count_changed();
}
static void
on_window_drag_data_received(GtkWidget *widget, GdkDragContext *drag_context,
gint x, gint y, GtkSelectionData *data, guint target_type,
guint event_time, gpointer user_data)
{
gboolean success = FALSE;
if (data->length > 0 && data->format == 8)
{
if (drag_context->action == GDK_ACTION_ASK)
{
drag_context->action = GDK_ACTION_COPY;
}
document_open_file_list((const gchar *)data->data, data->length);
success = TRUE;
}
gtk_drag_finish(drag_context, success, FALSE, event_time);
}