r5024
[geany-mirror.git] / src / notebook.c
bloba1d10bdb799b59bdeb26e4d236762a8c631d1e18
1 /*
2 * notebook.c - this file is part of Geany, a fast and lightweight IDE
4 * Copyright 2006-2010 Enrico Tröger <enrico(dot)troeger(at)uvena(dot)de>
5 * Copyright 2006-2010 Nick Treleaven <nick(dot)treleaven(at)btinternet(dot)com>
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21 * $Id$
25 * Notebook tab Drag 'n' Drop reordering and tab management.
28 #include "geany.h"
29 #include "notebook.h"
30 #include "document.h"
31 #include "editor.h"
32 #include "documentprivate.h"
33 #include "ui_utils.h"
34 #include "sidebar.h"
35 #include "support.h"
36 #include "callbacks.h"
37 #include "utils.h"
38 #include "keybindings.h"
40 #define GEANY_DND_NOTEBOOK_TAB_TYPE "geany_dnd_notebook_tab"
42 static const GtkTargetEntry drag_targets[] =
44 {GEANY_DND_NOTEBOOK_TAB_TYPE, GTK_TARGET_SAME_APP | GTK_TARGET_SAME_WIDGET, 0}
47 static GtkTargetEntry files_drop_targets[] = {
48 { "STRING", 0, 0 },
49 { "UTF8_STRING", 0, 0 },
50 { "text/plain", 0, 0 },
51 { "text/uri-list", 0, 0 }
55 static gboolean
56 notebook_drag_motion_cb(GtkWidget *widget, GdkDragContext *dc,
57 gint x, gint y, guint event_time, gpointer user_data);
59 static void
60 notebook_page_reordered_cb(GtkNotebook *notebook, GtkWidget *child, guint page_num,
61 gpointer user_data);
63 static void
64 on_window_drag_data_received(GtkWidget *widget, GdkDragContext *drag_context,
65 gint x, gint y, GtkSelectionData *data, guint info,
66 guint event_time, gpointer user_data);
68 static gint
69 notebook_find_tab_num_at_pos(GtkNotebook *notebook, gint x, gint y);
71 static void
72 notebook_tab_close_clicked_cb(GtkButton *button, gpointer user_data);
74 static void setup_tab_dnd(void);
77 static gboolean focus_sci(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
79 GeanyDocument *doc = document_get_current();
81 if (doc != NULL && event->button == 1)
82 gtk_widget_grab_focus(GTK_WIDGET(doc->editor->sci));
84 return FALSE;
88 static gboolean gtk_notebook_show_arrows(GtkNotebook *notebook)
90 return notebook->scrollable;
91 #if 0
92 /* To get this working we would need to define at least the first two fields of
93 * GtkNotebookPage since it is a private field. The better way would be to
94 * subclass GtkNotebook.
95 struct _FakeGtkNotebookPage
97 GtkWidget *child;
98 GtkWidget *tab_label;
101 gboolean show_arrow = FALSE;
102 GList *children;
104 if (! notebook->scrollable)
105 return FALSE;
107 children = notebook->children;
108 while (children)
110 struct _FakeGtkNotebookPage *page = children->data;
112 if (page->tab_label && ! gtk_widget_get_child_visible(page->tab_label))
113 show_arrow = TRUE;
115 children = children->next;
117 return show_arrow;
118 #endif
122 static gboolean is_position_on_tab_bar(GtkNotebook *notebook, GdkEventButton *event)
124 GtkWidget *page;
125 GtkWidget *tab;
126 GtkWidget *nb;
127 GtkPositionType tab_pos;
128 gint scroll_arrow_hlength, scroll_arrow_vlength;
129 gdouble x, y;
131 page = gtk_notebook_get_nth_page(notebook, 0);
132 g_return_val_if_fail(page != NULL, FALSE);
134 tab = gtk_notebook_get_tab_label(notebook, page);
135 g_return_val_if_fail(tab != NULL, FALSE);
137 tab_pos = gtk_notebook_get_tab_pos(notebook);
138 nb = GTK_WIDGET(notebook);
140 #if GTK_CHECK_VERSION(2, 10, 0)
141 gtk_widget_style_get(GTK_WIDGET(notebook), "scroll-arrow-hlength", &scroll_arrow_hlength,
142 "scroll-arrow-vlength", &scroll_arrow_vlength, NULL);
143 #else
144 scroll_arrow_hlength = scroll_arrow_vlength = 16;
145 #endif
147 if (! gdk_event_get_coords((GdkEvent*) event, &x, &y))
149 x = event->x;
150 y = event->y;
153 switch (tab_pos)
155 case GTK_POS_TOP:
156 case GTK_POS_BOTTOM:
158 if (event->y >= 0 && event->y <= tab->allocation.height)
160 if (! gtk_notebook_show_arrows(notebook) || (
161 x > scroll_arrow_hlength &&
162 x < nb->allocation.width - scroll_arrow_hlength))
163 return TRUE;
165 break;
167 case GTK_POS_LEFT:
168 case GTK_POS_RIGHT:
170 if (event->x >= 0 && event->x <= tab->allocation.width)
172 if (! gtk_notebook_show_arrows(notebook) || (
173 y > scroll_arrow_vlength &&
174 y < nb->allocation.height - scroll_arrow_vlength))
175 return TRUE;
180 return FALSE;
184 static void tab_bar_menu_activate_cb(GtkMenuItem *menuitem, gpointer data)
186 GeanyDocument *doc = data;
188 if (! DOC_VALID(doc))
189 return;
191 gtk_notebook_set_current_page(GTK_NOTEBOOK(main_widgets.notebook),
192 document_get_notebook_page(doc));
196 static void show_tab_bar_popup_menu(GdkEventButton *event)
198 GtkWidget *menu_item;
199 static GtkWidget *menu = NULL;
201 if (menu == NULL)
202 menu = gtk_menu_new();
204 /* clear the old menu items */
205 gtk_container_foreach(GTK_CONTAINER(menu), (GtkCallback) gtk_widget_destroy, NULL);
207 ui_menu_add_document_items(GTK_MENU(menu), document_get_current(),
208 G_CALLBACK(tab_bar_menu_activate_cb));
210 menu_item = gtk_separator_menu_item_new();
211 gtk_widget_show(menu_item);
212 gtk_container_add(GTK_CONTAINER(menu), menu_item);
214 menu_item = ui_image_menu_item_new(GTK_STOCK_CLOSE, _("Close Ot_her Documents"));
215 gtk_widget_show(menu_item);
216 gtk_container_add(GTK_CONTAINER(menu), menu_item);
217 g_signal_connect(menu_item, "activate", G_CALLBACK(on_close_other_documents1_activate), NULL);
219 menu_item = ui_image_menu_item_new(GTK_STOCK_CLOSE, _("C_lose All"));
220 gtk_widget_show(menu_item);
221 gtk_container_add(GTK_CONTAINER(menu), menu_item);
222 g_signal_connect(menu_item, "activate", G_CALLBACK(on_close_all1_activate), NULL);
224 gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
228 static gboolean notebook_tab_bar_click_cb(GtkWidget *widget, GdkEventButton *event,
229 gpointer user_data)
231 if (event->type == GDK_2BUTTON_PRESS)
233 /* accessing ::event_window is a little hacky but we need to make sure the click
234 * was in the tab bar and not inside the child */
235 if (event->window != GTK_NOTEBOOK(main_widgets.notebook)->event_window)
236 return FALSE;
238 if (is_position_on_tab_bar(GTK_NOTEBOOK(widget), event))
240 document_new_file(NULL, NULL, NULL);
241 return TRUE;
244 else if (event->button == 3)
246 show_tab_bar_popup_menu(event);
248 return FALSE;
252 void notebook_init()
254 /* Individual style for the tab close buttons */
255 gtk_rc_parse_string(
256 "style \"geany-close-tab-button-style\" {\n"
257 " GtkWidget::focus-padding = 0\n"
258 " GtkWidget::focus-line-width = 0\n"
259 " xthickness = 0\n"
260 " ythickness = 0\n"
261 "}\n"
262 "widget \"*.geany-close-tab-button\" style \"geany-close-tab-button-style\""
265 g_signal_connect_after(main_widgets.notebook, "button-press-event",
266 G_CALLBACK(notebook_tab_bar_click_cb), NULL);
268 g_signal_connect(main_widgets.notebook, "drag-data-received",
269 G_CALLBACK(on_window_drag_data_received), NULL);
271 setup_tab_dnd();
275 static void setup_tab_dnd()
277 GtkWidget *notebook = main_widgets.notebook;
279 /* Due to a segfault with manual tab DnD setup on GTK 2.10, we must
280 * use the built in gtk_notebook_set_tab_reorderable from GTK 2.10.
281 * This means a binary compiled against < 2.10 but run on >= 2.10
282 * will not have tab DnD support, but this is necessary until
283 * there is a fix for the older tab DnD code or GTK 2.10. */
284 if (gtk_check_version(2, 10, 0) == NULL) /* null means version ok */
286 #if GTK_CHECK_VERSION(2, 10, 0)
287 g_signal_connect(notebook, "page-reordered", G_CALLBACK(notebook_page_reordered_cb), NULL);
288 #endif
289 return;
292 /* Set up drag movement callback */
293 g_signal_connect(notebook, "drag-motion", G_CALLBACK(notebook_drag_motion_cb), NULL);
295 /* set up drag motion for moving notebook pages */
296 gtk_drag_dest_set(notebook, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
297 drag_targets, G_N_ELEMENTS(drag_targets), GDK_ACTION_MOVE);
298 /* set drag source, but for GTK+2.6 it's changed in motion-notify-event handler */
299 gtk_drag_source_set(notebook, GDK_BUTTON1_MASK,
300 drag_targets, G_N_ELEMENTS(drag_targets), GDK_ACTION_MOVE);
304 static void
305 notebook_page_reordered_cb(GtkNotebook *notebook, GtkWidget *child, guint page_num,
306 gpointer user_data)
308 /* Not necessary to update open files treeview if it's sorted.
309 * Note: if enabled, it's best to move the item instead of recreating all items. */
310 /*sidebar_openfiles_update_all();*/
314 static gboolean
315 notebook_drag_motion_cb(GtkWidget *widget, GdkDragContext *dc,
316 gint x, gint y, guint event_time, gpointer user_data)
318 static gint oldx, oldy; /* for determining direction of mouse drag */
319 GtkNotebook *notebook = GTK_NOTEBOOK(widget);
320 gint ndest = notebook_find_tab_num_at_pos(notebook, x, y);
321 gint ncurr = gtk_notebook_get_current_page(notebook);
323 if (ndest >= 0 && ndest != ncurr)
325 gboolean ok = FALSE;
326 /* prevent oscillation between non-homogeneous sized tabs */
327 switch (gtk_notebook_get_tab_pos(notebook))
329 case GTK_POS_LEFT:
330 case GTK_POS_RIGHT:
331 ok = ((ndest > ncurr) && (y > oldy)) || ((ndest < ncurr) && (y < oldy));
332 break;
334 case GTK_POS_TOP:
335 case GTK_POS_BOTTOM:
336 ok = ((ndest > ncurr) && (x > oldx)) || ((ndest < ncurr) && (x < oldx));
337 break;
340 if (ok)
342 gtk_notebook_reorder_child(notebook,
343 gtk_notebook_get_nth_page(notebook, ncurr), ndest);
344 notebook_page_reordered_cb(NULL, NULL, ndest, NULL);
348 oldx = x; oldy = y;
349 return FALSE;
353 /* Adapted from Epiphany absolute version in ephy-notebook.c, thanks.
354 * x,y are co-ordinates local to the notebook (not including border padding)
355 * notebook tab label widgets must not be NULL.
356 * N.B. This only checks the dimension that the tabs are in,
357 * e.g. for GTK_POS_TOP it does not check the y coordinate. */
358 static gint
359 notebook_find_tab_num_at_pos(GtkNotebook *notebook, gint x, gint y)
361 GtkPositionType tab_pos;
362 int page_num = 0;
363 GtkWidget *page;
365 /* deal with less than 2 pages */
366 switch (gtk_notebook_get_n_pages(notebook))
367 {case 0: return -1; case 1: return 0;}
369 tab_pos = gtk_notebook_get_tab_pos(notebook); /* which edge */
371 while ((page = gtk_notebook_get_nth_page(notebook, page_num)))
373 gint max_x, max_y;
374 GtkWidget *tab = gtk_notebook_get_tab_label(notebook, page);
376 g_return_val_if_fail(tab != NULL, -1);
378 if (!GTK_WIDGET_MAPPED(GTK_WIDGET(tab)))
379 { /* skip hidden tabs, e.g. tabs scrolled out of view */
380 page_num++;
381 continue;
384 /* subtract notebook pos to remove possible border padding */
385 max_x = tab->allocation.x + tab->allocation.width - GTK_WIDGET(notebook)->allocation.x;
386 max_y = tab->allocation.y + tab->allocation.height - GTK_WIDGET(notebook)->allocation.y;
388 if (((tab_pos == GTK_POS_TOP) || (tab_pos == GTK_POS_BOTTOM)) && (x <= max_x))
389 return page_num;
390 else if (((tab_pos == GTK_POS_LEFT) || (tab_pos == GTK_POS_RIGHT)) && (y <= max_y))
391 return page_num;
393 page_num++;
395 return -1;
399 /* call this after the number of tabs in main_widgets.notebook changes. */
400 static void tab_count_changed(void)
402 switch (gtk_notebook_get_n_pages(GTK_NOTEBOOK(main_widgets.notebook)))
404 case 0:
405 /* Enables DnD for dropping files into the empty notebook widget */
406 gtk_drag_dest_set(main_widgets.notebook, GTK_DEST_DEFAULT_ALL,
407 files_drop_targets, G_N_ELEMENTS(files_drop_targets),
408 GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK);
409 break;
411 case 1:
412 /* Disables DnD for dropping files into the notebook widget and enables the DnD for moving file
413 * tabs. Files can still be dropped into the notebook widget because it will be handled by the
414 * active Scintilla Widget (only dropping to the tab bar is not possible but it should be ok) */
415 gtk_drag_dest_set(main_widgets.notebook, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
416 drag_targets, G_N_ELEMENTS(drag_targets), GDK_ACTION_MOVE);
417 break;
422 static gboolean notebook_tab_click(GtkWidget *widget, GdkEventButton *event, gpointer data)
424 guint state;
426 /* toggle additional widgets on double click */
427 if (event->type == GDK_2BUTTON_PRESS)
429 if (interface_prefs.notebook_double_click_hides_widgets)
430 on_menu_toggle_all_additional_widgets1_activate(NULL, NULL);
432 return TRUE; /* stop other handlers like notebook_tab_bar_click_cb() */
434 /* close tab on middle click */
435 if (event->button == 2)
437 document_remove_page(gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook),
438 GTK_WIDGET(data)));
439 return TRUE; /* stop other handlers like notebook_tab_bar_click_cb() */
441 /* switch last used tab on ctrl-click */
442 state = event->state & gtk_accelerator_get_default_mod_mask();
443 if (event->button == 1 && state == GDK_CONTROL_MASK)
445 keybindings_send_command(GEANY_KEY_GROUP_NOTEBOOK,
446 GEANY_KEYS_NOTEBOOK_SWITCHTABLASTUSED);
447 return TRUE;
449 return FALSE;
453 static void notebook_tab_close_button_style_set(GtkWidget *btn, GtkRcStyle *prev_style,
454 gpointer data)
456 gint w, h;
458 gtk_icon_size_lookup_for_settings(gtk_widget_get_settings(btn), GTK_ICON_SIZE_MENU, &w, &h);
459 gtk_widget_set_size_request(btn, w + 2, h + 2);
463 /* Returns page number of notebook page, or -1 on error */
464 gint notebook_new_tab(GeanyDocument *this)
466 GtkWidget *hbox, *ebox;
467 gint tabnum;
468 GtkWidget *page;
469 gint cur_page;
471 g_return_val_if_fail(this != NULL, -1);
473 page = GTK_WIDGET(this->editor->sci);
475 this->priv->tab_label = gtk_label_new(NULL);
477 /* get button press events for the tab label and the space between it and
478 * the close button, if any */
479 ebox = gtk_event_box_new();
480 GTK_WIDGET_SET_FLAGS(ebox, GTK_NO_WINDOW);
481 g_signal_connect(ebox, "button-press-event", G_CALLBACK(notebook_tab_click), page);
482 /* focus the current document after clicking on a tab */
483 g_signal_connect_after(ebox, "button-release-event",
484 G_CALLBACK(focus_sci), NULL);
486 hbox = gtk_hbox_new(FALSE, 2);
487 gtk_box_pack_start(GTK_BOX(hbox), this->priv->tab_label, FALSE, FALSE, 0);
488 gtk_container_add(GTK_CONTAINER(ebox), hbox);
490 if (file_prefs.show_tab_cross)
492 GtkWidget *image, *btn, *align;
494 btn = gtk_button_new();
495 gtk_button_set_relief(GTK_BUTTON(btn), GTK_RELIEF_NONE);
496 gtk_button_set_focus_on_click(GTK_BUTTON(btn), FALSE);
497 gtk_widget_set_name(btn, "geany-close-tab-button");
499 image = gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU);
500 gtk_container_add(GTK_CONTAINER(btn), image);
502 align = gtk_alignment_new(1.0, 0.5, 0.0, 0.0);
503 gtk_container_add(GTK_CONTAINER(align), btn);
504 gtk_box_pack_start(GTK_BOX(hbox), align, TRUE, TRUE, 0);
506 g_signal_connect(btn, "clicked", G_CALLBACK(notebook_tab_close_clicked_cb), page);
507 /* button overrides event box, so make middle click on button also close tab */
508 g_signal_connect(btn, "button-press-event", G_CALLBACK(notebook_tab_click), page);
509 /* handle style modification to keep button small as possible even when theme change */
510 g_signal_connect(btn, "style-set", G_CALLBACK(notebook_tab_close_button_style_set), NULL);
513 gtk_widget_show_all(ebox);
515 document_update_tab_label(this);
517 if (file_prefs.tab_order_beside)
518 cur_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.notebook));
519 else
520 cur_page = file_prefs.tab_order_ltr ? -2 /* hack: -2 + 1 = -1, last page */ : 0;
521 if (file_prefs.tab_order_ltr)
522 tabnum = gtk_notebook_insert_page_menu(GTK_NOTEBOOK(main_widgets.notebook), page,
523 ebox, NULL, cur_page + 1);
524 else
525 tabnum = gtk_notebook_insert_page_menu(GTK_NOTEBOOK(main_widgets.notebook), page,
526 ebox, NULL, cur_page);
528 tab_count_changed();
530 /* This is where tab DnD is enabled for GTK 2.10 and higher */
531 #if GTK_CHECK_VERSION(2, 10, 0)
532 if (gtk_check_version(2, 10, 0) == NULL) /* null means version ok */
534 gtk_notebook_set_tab_reorderable(GTK_NOTEBOOK(main_widgets.notebook), page, TRUE);
536 #endif
537 return tabnum;
541 static void
542 notebook_tab_close_clicked_cb(GtkButton *button, gpointer user_data)
544 gint cur_page = gtk_notebook_page_num(GTK_NOTEBOOK(main_widgets.notebook),
545 GTK_WIDGET(user_data));
547 document_remove_page(cur_page);
551 /* Always use this instead of gtk_notebook_remove_page(). */
552 void notebook_remove_page(gint page_num)
554 gint curpage = gtk_notebook_get_current_page(GTK_NOTEBOOK(main_widgets.notebook));
556 /* Focus the next page, not the previous */
557 if (curpage == page_num && file_prefs.tab_order_ltr)
559 gtk_notebook_set_current_page(GTK_NOTEBOOK(main_widgets.notebook), curpage + 1);
562 /* now remove the page (so we don't temporarily switch to the previous page) */
563 gtk_notebook_remove_page(GTK_NOTEBOOK(main_widgets.notebook), page_num);
565 tab_count_changed();
569 static void
570 on_window_drag_data_received(GtkWidget *widget, GdkDragContext *drag_context,
571 gint x, gint y, GtkSelectionData *data, guint target_type,
572 guint event_time, gpointer user_data)
574 gboolean success = FALSE;
576 if (data->length > 0 && data->format == 8)
578 if (drag_context->action == GDK_ACTION_ASK)
580 drag_context->action = GDK_ACTION_COPY;
583 document_open_file_list((const gchar *)data->data, data->length);
585 success = TRUE;
587 gtk_drag_finish(drag_context, success, FALSE, event_time);