/* $Id$ Copyright (C) 2003-2010 tooar Portions copyright (C) 2004 Florian Zähringer Portions copyright (C) 1999 Michael Clark. This file is part of emelFM2. emelFM2 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, or (at your option) any later version. emelFM2 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 emelFM2; see the file GPL. If not, see http://www.gnu.org/licenses. */ /** @file src/e2_output.c @brief output pane creation and action functions includes actions related to output contents, but not to the output visibility */ /** \page output the output pane ToDo - descibe how this works \section tabs output pane tabs ToDo */ #include "emelfm2.h" #include #include #include #include "e2_output.h" #include "e2_dialog.h" //for pane-text activation #include "e2_task.h" #include "e2_filetype.h" #include "e2_context_menu.h" //for selection-save #include "e2_view_dialog.h" #ifdef E2_MOUSECUSTOM # include "e2_mousebinding.h" #endif #define E2_PANED_TOLERANCE 10 //#define VOL volatile #define VOL static void _e2_output_edit_cb (GtkMenuItem *menuitem, E2_OutputTabRuntime *rt); #ifdef E2_TABS_DETACH static void _e2_output_tabattach_cb (GtkMenuItem *menuitem, E2_OutputTabRuntime *rt); static void _e2_output_clear_cb (GtkMenuItem *menuitem, E2_OutputTabRuntime *rt); #endif static void _e2_output_scrolled_cb (GtkAdjustment *adjust, E2_OutputTabRuntime *rt); static gboolean _e2_output_button_press_cb (GtkWidget *textview, GdkEventButton *event, E2_OutputTabRuntime *rt); static gboolean _e2_output_popup_menu_cb (GtkWidget *widget, E2_OutputTabRuntime *rt); //static gboolean output_activated = FALSE; static GStaticRecMutex print_mutex; extern pthread_mutex_t task_mutex; #ifdef E2_TABS_DETACH #define TARGET_NOTEBOOK_TAB 3 static GtkTargetEntry target_table2[] = { { "GTK_NOTEBOOK_TAB", GTK_TARGET_SAME_APP, TARGET_NOTEBOOK_TAB }, }; static guint n_targets2 = sizeof(target_table2) / sizeof(GtkTargetEntry); #endif /*****************/ /***** utils *****/ /*****************/ /** @brief set flag according to whether @a iter in an output pane text buffer is presently visible @param rt output pane tab runtime data struct @param iter pointer to data for the position in @a text_view that is to be checked @return */ static void _e2_output_iter_check_visible (E2_OutputTabRuntime *rt, VOLATILE GtkTextIter *iter) { GdkRectangle visible_rect, iter_rect; gtk_text_view_get_visible_rect (rt->text, &visible_rect); gtk_text_view_get_iter_location (rt->text, iter, &iter_rect); gboolean off = //this makes the pane scroll 1 line too many, but is faster ((iter_rect.y + iter_rect.height) > (visible_rect.y + visible_rect.height) //this causes jiggling, due to trailing blank line (\n at line-ends) being //re-scrolled off-screeen-bottom // (iter_rect.y > (visible_rect.y + visible_rect.height) || iter_rect.y < visible_rect.y); //atomic change to minimise race with continuing output g_atomic_int_set (&rt->onscreen, (off) ? 0:1); } /** @brief move the 'page' displayed in the output pane, relative to its text content @param down TRUE to move down, FALSE to move up @param arg string containing a number, the no. of 'moves' @param page TRUE move @a arg 'pages', FALSE move @a arg 'steps' @return */ static void _e2_output_scroll_helper (gboolean down, gchar *arg, gboolean page) { printd (DEBUG, "scroll_helper (down:%d,arg:%s,page:%d)", down, arg, page); E2_OutputTabRuntime *rt = &app.tab; g_return_if_fail (rt->scroll != NULL); gchar *end = NULL; gdouble times = 1.0; if (arg != NULL) times = g_ascii_strtod (arg, &end); if (end == arg) times = 1.0; GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (rt->scroll)); gdouble value = gtk_adjustment_get_value (vadj); gdouble inc = page ? #ifdef USE_GTK2_14 gtk_adjustment_get_page_increment (vadj): gtk_adjustment_get_step_increment (vadj); #else vadj->page_increment : vadj->step_increment; #endif if (down) { value += (inc * times); gdouble rest = #ifdef USE_GTK2_14 gtk_adjustment_get_upper (vadj) - gtk_adjustment_get_page_size (vadj); #else vadj->upper - vadj->page_size; #endif if (value > rest) value = rest; } else value -= (inc * times); gtk_adjustment_set_value (vadj, value); } /** @brief set a mark at the position of the bottom-left of the output pane This supports e2_output_scroll_to_bottom() @param rt pointer to tab data @return */ void e2_output_mark_end (E2_OutputTabRuntime *rt) { g_return_if_fail (rt->scroll != NULL); GdkRectangle visible_rect; gtk_text_view_get_visible_rect (rt->text, &visible_rect); GtkTextIter iter; gtk_text_view_get_iter_at_location (rt->text, &iter, visible_rect.x, visible_rect.y + visible_rect.height - 1); GtkTextMark *mark = gtk_text_buffer_get_mark (rt->buffer, "bottom-scroll"); if (mark != NULL) gtk_text_buffer_move_mark (rt->buffer, mark, &iter); else gtk_text_buffer_create_mark (rt->buffer, "bottom-scroll", &iter, TRUE); } /** @brief idle callback to scroll the 'page' displayed in an output-pane tab to the end of content @param rt pointer to data for the relevant output tab @return FALSE to remove the source */ static gboolean _e2_output_do_scroll (E2_OutputTabRuntime *rt) { if (rt->scroll != NULL) { GtkTextMark *mark = gtk_text_buffer_get_mark (rt->buffer, "bottom-scroll"); if (G_LIKELY (mark != NULL)) { gdk_threads_enter (); gtk_text_view_scroll_to_mark (rt->text, mark, 0.0, TRUE, 0.0, 1.0); gdk_threads_leave (); } } return FALSE; } /** @brief move the 'page' displayed in the output pane, to show a point at the bottom @param rt pointer to tab data @return */ void e2_output_scroll_to_end (E2_OutputTabRuntime *rt) { //this won't work if a higher priority is applied g_idle_add ((GSourceFunc) _e2_output_do_scroll, rt); } /** @brief move the displayed text window to show text printed by a child process @param item UNUSED the selected item from a children-menu @param rt pointer to data for the command related to @a item, or NULL for "no children" item @return */ static void _e2_output_scroll_to_child (GtkMenuItem *item, E2_TaskRuntime *rt) { if (rt != NULL) { //we have a child pthread_mutex_lock (&task_mutex); GList *member = g_list_find (app.taskhistory, rt); pthread_mutex_unlock (&task_mutex); if (member != NULL) //command data still exists { //the buffer may be cleared while the menu is active, //so check again for matching content GtkTextMark *origin_mark = gtk_text_buffer_get_mark (app.tab.buffer, rt->pidstr); if (origin_mark != NULL) gtk_text_view_scroll_to_mark (app.tab.text, origin_mark, 0.0, TRUE, 0.0, 1.0); else { gchar *msg = g_strdup_printf (_("Cannot find any output from process %s"), rt->pidstr); e2_output_print_error (msg, TRUE); } } } } /** @brief set popup menu position This function is supplied when calling gtk_menu_popup(), to position the displayed menu. set @a push_in to TRUE for menu completely inside the screen, FALSE for menu clamped to screen size @param menu the GtkMenu to be positioned @param x place to store gint representing the menu left @param y place to store gint representing the menu top @param push_in place to store pushin flag @param textview output pane widget in focus when the menu key was pressed @return */ void e2_output_set_menu_position (GtkWidget *menu, gint *x, gint *y, gboolean *push_in, GtkWidget *textview) { gint left, top; gtk_window_get_position (GTK_WINDOW (app.main_window), &left, &top); GtkAllocation alloc; #ifdef USE_GTK2_18 gtk_widget_get_allocation (textview, &alloc); #else alloc = textview->allocation; #endif *x = left + alloc.x + alloc.width/2; *y = top + alloc.y + alloc.height/2; *push_in = FALSE; } /** @brief update gtk's flag which sets output pane text wrapping This is a hook fn @param pvalue pointerised form of the GtkWrapMode-enum value to be set @param rt pointer to data struct for an output pane tab @return TRUE always, so func is not delisted after run */ static gboolean _e2_output_set_op_wrap_hook (gpointer pvalue, E2_OutputTabRuntime *rt) { // printd (DEBUG, "_e2_output_set_op_wrap (pvalue:_,rt:_)"); gint value = GPOINTER_TO_INT (pvalue); gint cur = gtk_text_view_get_wrap_mode (rt->text); if (cur != value) gtk_text_view_set_wrap_mode (rt->text, value); return TRUE; } /** @brief execute action corresponding to item selected from filetype tasks menu This is the callback for handling a selection of a filetype action from the context menu @param widget the activated menu item widget @return */ static void _e2_output_choose_filetype_action_cb (GtkMenuItem *menu_item) { gpointer *command = g_object_get_data (G_OBJECT (menu_item), "e2-file-operation"); e2_command_run ((gchar *) command, E2_COMMAND_RANGE_DEFAULT, menu_item #ifdef E2_COMMANDQ , FALSE #endif ); } /** @brief populate @a menu with items for the actions for a filetype Each member of @a actions is like "command" or "label@command" @param text the path of the item to open, utf8 string @param menu the menu widget to which the action menu-items are to be added @param actions NULL-terminated array of utf8 strings, each a command for a filetype @return */ static void _e2_output_menu_create_filetype_actions_menu (gchar *text, GtkWidget *menu, const gchar **actions) { gchar *sep, *fullcmd; GtkWidget *menu_item; while (*actions != NULL) { if ((sep = strchr (*actions, '@')) != NULL) //if always ascii @, don't need g_utf8_strchr() { *sep = '\0'; menu_item = e2_menu_add (menu, (gchar *)*actions, NULL, NULL, _e2_output_choose_filetype_action_cb, NULL); fullcmd = e2_utils_replace_name_macros (sep + sizeof (gchar), text); if (fullcmd == sep + sizeof (gchar)) //no replaced macro in action-string { //tag E2_BADQUOTES gchar *qp = e2_utils_quote_string (text); fullcmd = g_strconcat (sep + sizeof (gchar), " ", qp, NULL); g_free (qp); } *sep = '@'; //revert to original form (this is the 'source' data) } else { menu_item = e2_menu_add (menu, (gchar *)*actions, NULL, NULL, _e2_output_choose_filetype_action_cb, NULL); fullcmd = e2_utils_replace_name_macros ((gchar *)*actions, text); if (fullcmd == (gchar *)*actions) //no replaced macro in *actions { //tag E2_BADQUOTES gchar *qp = e2_utils_quote_string (text); fullcmd = g_strconcat (*actions, " ", qp, NULL); g_free (qp); } } g_object_set_data_full (G_OBJECT (menu_item), "e2-file-operation", fullcmd, g_free); actions++; } } /** @brief create a filetypes sub-menu for @a menu @param menu menu widget to populate @param item UTF-8 string describing the item to process, may have absolute or relative or no path @return */ static void _e2_output_populate_filetype_menu (GtkWidget *menu, gchar *item) { struct stat statbuf; E2_ERR_DECLARE gchar *usepath = e2_utils_translate_relative_path (curr_view->dir, item); *(usepath + strlen(usepath) - sizeof(gchar)) = '\0'; //stat() hates trailing separator gchar *local = F_FILENAME_TO_LOCALE (usepath); #ifdef E2_VFS # ifdef E2_VFSTMP get relevant spacedata, or allow only local (NULL) # endif VPATH sdata = {local, NULL}; if (e2_fs_stat (&sdata, &statbuf E2_ERR_PTR())) #else if (e2_fs_stat (local, &statbuf E2_ERR_PTR())) #endif { #ifdef E2_VFSTMP //FIXME handle error #endif E2_ERR_CLEAR g_free (usepath); F_FREE (local, usepath); return; } gboolean exec; const gchar **actions = NULL; gchar *ext, *ext2 = NULL; gchar *base = g_path_get_basename (item); #ifdef E2_VFS if (e2_fs_is_dir3 (&sdata E2_ERR_NONE())) #else if (e2_fs_is_dir3 (local E2_ERR_NONE())) #endif { exec = FALSE; //no special treatment of dirs ext = ext2 = g_strconcat (".", _(""), NULL); } else { exec = e2_fs_is_exec2 ( #ifdef E2_VFS &sdata E2_ERR_NONE()); #else local E2_ERR_NONE()); #endif //extension is the part of the name after the leftmost '.' ext = (*base == '.') ? base + sizeof (gchar) : base; //FIXME generalise hidden-item test ext = strchr (ext, '.'); //'.' is ascii if ((ext == NULL || *(ext + sizeof (gchar)) == '\0') //no extension && e2_fs_is_text ( #ifdef E2_VFS &sdata E2_ERR_NONE())) #else local E2_ERR_NONE())) #endif //fake text extension //too bad if this is not a recognised text extension in filetypes data ext = ".txt"; } if (ext != NULL) { //check all possible extensions for a matching filetype do { //skip leading dot "." ext += sizeof (gchar); //ascii '.'. always single char actions = e2_filetype_get_actions (ext); if (actions != NULL) { _e2_output_menu_create_filetype_actions_menu (usepath, menu, actions); break; } } while ((ext = strchr (ext, '.')) != NULL); //always ascii '.', don't need g_utf8_strchr() } if (exec) { //add exec-filetype items unless item has been found in that type already const gchar **acts2 = e2_filetype_get_actions (_("")); if (actions != NULL //was a matching extension && actions != acts2 && acts2 != NULL) //CHECKME this test _e2_output_menu_create_filetype_actions_menu (usepath, menu, acts2); else if (acts2 != NULL) _e2_output_menu_create_filetype_actions_menu (usepath, menu, acts2); //if appropriate, get desktop entry actions and add them too gchar *path, *localpath; gchar *dfile = e2_utils_strcat (base, ".desktop"); gchar *localname = F_FILENAME_TO_LOCALE (dfile); const gchar* const *sysdir; for (sysdir = g_get_system_data_dirs (); *sysdir != NULL; sysdir++) { path = g_build_filename (*sysdir, "applications", localname, NULL); localpath = F_FILENAME_TO_LOCALE (path); //probably redundant #ifdef E2_VFS sdata.path = localpath; if (!e2_fs_access (&sdata, R_OK E2_ERR_NONE())) #else if (!e2_fs_access (localpath, R_OK E2_ERR_NONE())) #endif { e2_context_menu_create_desktop_actions_menu (menu, localpath); g_free (path); F_FREE (localpath, path); break; } g_free (path); F_FREE (localpath, path); } g_free (dfile); F_FREE (localname, dfile); } g_free (usepath); F_FREE (local, usepath); g_free (base); if (ext2 != NULL) g_free (ext2); } /** @brief check for or open a taskable item, asking for choice if @a run is TRUE Expects BGL on/closed @param item UTF-8 string describing the item to open, may have absolute or relative or no path @param run TRUE to execute a matched command, FALSE to just return the match status @return TRUE if the item was processed (@a run = FALSE) or found (@a run = TRUE) */ static gboolean _e2_output_open_text (gchar *item, gboolean run) { gboolean retval; struct stat statbuf; gchar *usepath, *local; usepath = e2_utils_translate_relative_path (curr_view->dir, item); *(usepath + strlen(usepath) - sizeof(gchar)) = '\0'; //stat() hates trailing separator local = F_FILENAME_TO_LOCALE (usepath); //make sure the text means something #ifdef E2_VFS # ifdef E2_VFSTMP assume text from local command run on local items # endif VPATH sdata = { local, NULL }; if (e2_fs_stat (&sdata, &statbuf E2_ERR_NONE())) #else if (e2_fs_stat (local, &statbuf E2_ERR_NONE())) #endif { retval = FALSE; if (run) { gchar *msg = g_strdup_printf (_("Cannot get information about %s"), item); e2_output_print_error (msg, TRUE); } } else if (run) { #ifdef E2_VFS retval = e2_task_backend_open (&sdata, TRUE); #else retval = e2_task_backend_open (local, TRUE); #endif } else { //just check for a known filetype #ifdef E2_VFS if (e2_fs_is_dir3 (&sdata E2_ERR_NONE()) //we can always handle dirs || !e2_fs_access (&sdata, X_OK E2_ERR_NONE())) //and executable items #else if (e2_fs_is_dir3 (local E2_ERR_NONE()) //we can always handle dirs || !e2_fs_access (local, X_OK E2_ERR_NONE())) //and executable items #endif retval = TRUE; else { gchar *base = g_path_get_basename (item); gchar *ext = strchr (base, '.'); //assumes '.' is ascii if (ext == NULL //no extension || ext == base) //hidden file { #ifdef E2_VFS retval = e2_fs_is_text (&sdata E2_ERR_NONE()); #else retval = e2_fs_is_text (local E2_ERR_NONE()); #endif } else { gchar *action; retval = FALSE; do { ext += sizeof (gchar); //skip the . prefix action = e2_filetype_get_default_action (ext); if (action != NULL) { retval = TRUE; g_free (action); break; } } while ((ext = strchr (ext, '.')) != NULL); //if always ascii '.', don't need g_utf8_strchr() } g_free (base); } } g_free (usepath); F_FREE (local, usepath); return retval; } /** @brief get and optionally select text (if any) surrounding event-position If there is a selection at cursor position, that selection is returned. Otherwise surrounding whitespace or newline chars are scanned for. @param x event x coordinate @param y event y coordinate @param rt pointer to data struct for output pane tab @param select TRUE to select text if any @return newly allocated string containing item, or NULL if no suitable text found */ static gchar *_e2_output_get_item_at_pointer (gint x, gint y, E2_OutputTabRuntime *rt, gboolean select) { GtkTextIter iter, start, end; gint buffer_x, buffer_y; gboolean quoted; gunichar c, d; gtk_text_buffer_get_bounds (rt->buffer, &start, &end); if (gtk_text_iter_equal (&start, &end)) return NULL; gtk_text_view_window_to_buffer_coords (rt->text, GTK_TEXT_WINDOW_TEXT, x, y, &buffer_x, &buffer_y); gtk_text_view_get_iter_at_location (rt->text, &iter, buffer_x, buffer_y); if (!gtk_text_buffer_get_selection_bounds (rt->buffer, &start, &end) || !gtk_text_iter_in_range (&iter, &start, &end)) { c = gtk_text_iter_get_char (&iter); if (g_unichar_isspace (c)) return NULL; //word separators include valid path chars, so need char iteration while (gtk_text_iter_backward_char (&iter)) { c = gtk_text_iter_get_char (&iter); if (g_unichar_isspace (c)) { gtk_text_iter_forward_char (&iter); break; } } c = gtk_text_iter_get_char (&iter); quoted = (c == (gunichar) '"' || c == (gunichar) '\''); if (quoted) { if (!gtk_text_iter_forward_char (&iter)) return NULL; } start = iter; while (gtk_text_iter_forward_word_end (&iter)) { nextchar: d = gtk_text_iter_get_char (&iter); if (quoted && d == c) break; else if (g_unichar_isspace (d)) break; else if (ispunct ((guchar)d)) { if (!gtk_text_iter_forward_char (&iter)) break; goto nextchar; } } end = iter; if (select) gtk_text_buffer_select_range (rt->buffer, &start, &end); } return (gtk_text_iter_get_text (&start, &end)); } /** @brief construct and pop up destroyable context-menu for output-pane @param textview the textview widget where the click happened @param event_button which mouse button was clicked (0 for a menu key) @param event_time time that the event happened (0 for a menu key) @param rt runtime struct to work on @return */ static void _e2_output_show_context_menu (GtkWidget *textview, guint event_button, gint event_time, E2_OutputTabRuntime *rt) { gchar *item_name; GtkWidget *item; GtkWidget *menu = gtk_menu_new (); #ifdef E2_TABS_DETACH if (!rt->detached) { #endif item_name = g_strconcat (_A(10),".",_A(31),NULL); e2_menu_add_action (menu, _("_Hide"), "output_hide"E2ICONTB, _("Do not show the output pane"), item_name, "1"); //no arg-string translation g_free (item_name); item_name = g_strconcat (_A(10),".",_A(31),NULL); e2_menu_add_action (menu, _("_Toggle full"), (app.window.output_paned_ratio > 0.01) ? GTK_STOCK_ZOOM_FIT : GTK_STOCK_ZOOM_OUT, _("Toggle output pane size to/from the full window size"), item_name, "0,*"); //no string translation g_free (item_name); item_name = g_strconcat (_A(10),".",_A(30),NULL); e2_menu_add_action (menu, _("_New tab"), GTK_STOCK_ADD, _("Add another tab for the output pane"), item_name, NULL); g_free (item_name); item_name = g_strconcat (_A(10),".",_A(42),NULL); item = e2_menu_add_action (menu, _("_Remove tab"), GTK_STOCK_REMOVE, _("Close this this tab"), item_name, NULL); g_free (item_name); if (item != NULL && app.tabcount == 1) gtk_widget_set_sensitive (item, FALSE); #ifdef E2_TABS_DETACH } else //rt->detached { e2_menu_add (menu, _("_Attach"), NULL, _("Move this tab back to output pane"), _e2_output_tabattach_cb, rt); } #endif e2_menu_add_separator (menu); #ifdef E2_TABS_DETACH e2_menu_add (menu, _("_Clear"), GTK_STOCK_CLEAR, _("Clear this tab"), _e2_output_clear_cb, rt); #else item_name = g_strconcat (_A(10),".",_A(33),NULL); e2_menu_add_action (menu, _("_Clear"), GTK_STOCK_CLEAR, _("Clear this tab"), item_name, NULL); g_free (item_name); #endif /* e2_menu_add (menu, _("Save as.."), "save_selection"E2ICOND, //no suitable mnemonic _("Save the selected text"), _e2_output_savesel_cb, rt); if (!gtk_text_buffer_get_selection_bounds (rt->buffer, NULL, NULL)) gtk_widget_set_sensitive (item, FALSE); */ e2_menu_add (menu, _("_Edit"), GTK_STOCK_EDIT, _("Edit the tab contents"), _e2_output_edit_cb, rt); item = e2_menu_add (menu, _("_Open"), GTK_STOCK_EXECUTE, NULL, NULL, NULL); gint x, y; gdk_window_get_pointer ( #ifdef USE_GTK2_14 gtk_widget_get_window (textview), #else textview->window, #endif &x, &y, NULL); item_name = _e2_output_get_item_at_pointer (x, y, rt, FALSE); if (item_name == NULL || !_e2_output_open_text (item_name, FALSE)) gtk_widget_set_sensitive (item, FALSE); else { GtkWidget *submenu = gtk_menu_new (); _e2_output_populate_filetype_menu (submenu, item_name); gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); gtk_widget_show (submenu); } g_free (item_name); //?? /* item_name = g_strconcat (_A(10),".",_A(54),NULL); e2_menu_add_action (menu, _("C_ommand help"), GTK_STOCK_HELP, _("Show information about using the command line"), item_name, NULL); g_free (item_name); */ item = e2_menu_add (menu, _("Co_mmand output"), "ps"E2ICONTB, _("Show output from a completed command"), NULL, NULL); GtkWidget *submenu = e2_menu_create_child_menu (E2_CHILD_OUTPUT, _e2_output_scroll_to_child); if (submenu == NULL) gtk_widget_set_sensitive (item, FALSE); else gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu); #ifdef E2_TABS_DETACH if (!rt->detached) { #endif e2_menu_add_separator (menu); submenu = e2_menu_add_submenu (menu, _("_Settings"), GTK_STOCK_PREFERENCES); e2_menu_create_options_menu (GTK_WIDGET (rt->text), submenu, rt->opt_wrap, NULL, NULL, app.output.opt_show_on_new, NULL, NULL, app.output.opt_show_on_focus_in, NULL, NULL, app.output.opt_hide_on_focus_out, NULL, NULL, app.output.opt_jump, NULL, NULL, app.output.opt_jump_follow, NULL, NULL, app.output.opt_jump_end, NULL, NULL, NULL); item_name = g_strconcat (_A(3),".",_A(32),NULL); e2_menu_add_action (submenu, _("_Other"), NULL, _("Open the configuration dialog at the output options page"), item_name, _C(28)); //_("output") g_free (item_name); #ifdef E2_TABS_DETACH } #endif g_signal_connect (G_OBJECT (menu), "selection-done", G_CALLBACK (e2_menu_selection_done_cb), NULL); if (event_button == 0) gtk_menu_popup (GTK_MENU (menu), NULL, NULL, (GtkMenuPositionFunc) e2_output_set_menu_position, textview, 0, event_time); else //this was a button-3 click gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, event_button, event_time); } /** @brief create a new textview and buffer and some basic tags @param rt runtime data struct for the tab (NOT app.tab) @return scrolled window containing a textview */ static GtkWidget *_e2_output_create_view (E2_OutputTabRuntime *rt) { //init some vars //at the outset, assume that the current content is on-screen rt->onscreen = 1; //visible flag is set elsewhere, depending on cache data //create scrolled window rt->scroll = e2_widget_get_sw (GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_OUT); //create text view rt->text = GTK_TEXT_VIEW (gtk_text_view_new ()); gtk_container_add (GTK_CONTAINER (rt->scroll), GTK_WIDGET (rt->text)); gtk_text_view_set_editable (rt->text, FALSE); gtk_text_view_set_cursor_visible (rt->text, FALSE); // allow focus so popup menu signal can happen, & can select text in the pane // GTK_WIDGET_UNSET_FLAGS (rt->text, GTK_CAN_FOCUS); gtk_text_view_set_wrap_mode (rt->text, e2_option_int_get ("output-wrap-mode")); gtk_text_view_set_left_margin (rt->text, e2_option_int_get ("output-left-margin")); gtk_text_view_set_right_margin (rt->text, e2_option_int_get ("output-right-margin")); const gchar *fntname = e2_utils_get_output_font (); PangoFontDescription *font_desc = pango_font_description_from_string (fntname); gtk_widget_modify_font (GTK_WIDGET (rt->text), font_desc); pango_font_description_free (font_desc); //signal used for "links" in the output pane // g_signal_connect (G_OBJECT (rt->text), "motion-notify-event", // G_CALLBACK (test_cb), NULL); gtk_widget_set_events (GTK_WIDGET (rt->text), gtk_widget_get_events (GTK_WIDGET (rt->text)) | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK); g_signal_connect (G_OBJECT (rt->text), "popup_menu", G_CALLBACK (_e2_output_popup_menu_cb), rt); //this cb does generic stuff whether or not E2_MOUSECUSTOM applies g_signal_connect (G_OBJECT (rt->text), "button-press-event", G_CALLBACK (_e2_output_button_press_cb), rt); // g_signal_connect_after (G_OBJECT (rt->text), "button-release-event", // G_CALLBACK (_e2_output_button_release_cb), rt); if this is used, log the presses //in case it's a new tab, don't rely on ...register_all() #ifdef E2_MOUSECUSTOM e2_output_register_pointerbindings (GTK_WIDGET (rt->text)); #endif //arrange for key-translations from locale g_signal_connect (G_OBJECT (rt->text), "key-press-event", G_CALLBACK (e2_utils_key_translate_cb), NULL); g_signal_connect (G_OBJECT (rt->text), "key-release-event", G_CALLBACK (e2_utils_key_translate_cb), NULL); e2_output_register_keybindings (GTK_WIDGET (rt->text)); //arrange to retain any manual scroll away from latest output //YUK no "activate" available, nor so for any ancestor // g_signal_connect_after (G_OBJECT (rt->text->vadjustment), "value-changed", GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (rt->scroll)); g_signal_connect_after (G_OBJECT (vadj), "value-changed", G_CALLBACK (_e2_output_scrolled_cb), rt); gtk_widget_show (GTK_WIDGET (rt->text)); rt->buffer = gtk_text_view_get_buffer (rt->text); gtk_text_buffer_create_tag (rt->buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL); gtk_text_buffer_create_tag (rt->buffer, "italic", "style", PANGO_STYLE_ITALIC, NULL); gtk_text_buffer_create_tag (rt->buffer, "uline", "underline", PANGO_UNDERLINE_SINGLE, NULL); gtk_text_buffer_create_tag (rt->buffer, "small", "weight", PANGO_WEIGHT_NORMAL, "size", (gint) (PANGO_SCALE_SMALL * app.output.font_size), NULL); gtk_text_buffer_create_tag (rt->buffer, "blue", "foreground", "blue", NULL); gtk_text_buffer_create_tag (rt->buffer, "green", "foreground", e2_option_str_get ("color-positive"), NULL); gtk_text_buffer_create_tag (rt->buffer, "red", "foreground", e2_option_str_get ("color-negative"), NULL); gtk_text_buffer_create_tag (rt->buffer, "grey", "foreground", e2_option_str_get ("color-unimportant"), NULL); /* these not needed unless output links are parsed gtk_text_buffer_create_tag (rt->tab.buffer, "link", "foreground", "blue", "underline", PANGO_UNDERLINE_NONE, NULL); gtk_text_buffer_create_tag (rt->tab.buffer, "link-active", "foreground", "blue", "underline", PANGO_UNDERLINE_SINGLE, NULL); */ GtkTextIter start; gtk_text_buffer_get_start_iter (rt->buffer, &start); rt->mark = gtk_text_buffer_create_mark (rt->buffer, NULL, &start, FALSE); //scroll-helper at start //attach options rt->opt_wrap = e2_option_attach_value_changed ("output-wrap-mode", GTK_WIDGET (rt->text), (HookFunc)_e2_output_set_op_wrap_hook, rt); gtk_widget_show (rt->scroll); return rt->scroll; } #ifdef E2_TABS_DETACH /** @brief clear text buffer of notebook tab associated with @a rt This assumes BGL is closed @param rt tab runtime data struct @return */ static void _e2_output_clear_buffer (E2_OutputTabRuntime *rt) { //order of things here is to minimise risk when clearing during an ongoing print operation GtkTextTagTable *table = gtk_text_buffer_get_tag_table (rt->buffer); GtkTextBuffer *buffer = gtk_text_buffer_new (table); GtkTextIter start; gtk_text_buffer_get_start_iter (buffer, &start); GtkTextMark *mark = gtk_text_buffer_create_mark (buffer, NULL, &start, FALSE); //scroll-helper at start WAIT_FOR_EVENTS gtk_text_view_set_buffer (rt->text, buffer); rt->buffer = buffer; rt->mark = mark; g_atomic_int_set (&rt->onscreen, 1); g_object_unref (G_OBJECT (buffer)); } /** @brief set or clear detached-related data for tab whose widget is @a child @param child the notebook tab widget @param dest_notebook the notebook into which a moved tab will go, NULL = app.outbook @return */ static void _e2_output_tab_set_detached_state (GtkWidget *child, GtkWidget *dest_notebook) { gboolean attaching; GtkWidget *window; attaching = (dest_notebook == NULL || dest_notebook == app.outbook); if (attaching) //moving to main notebook window = NULL; else //moving from main notebook or some other drop window { window = gtk_widget_get_toplevel (dest_notebook); #ifdef USE_GTK2_18 if (!gtk_widget_is_toplevel (window)) #else if (!GTK_WIDGET_TOPLEVEL (window)) #endif { //FIXME printd (DEBUG, "can't find top window for moved tab"); return; } } if (child == app.tab.scroll) { app.tab.detached = !attaching; app.tab.dropwindow = window; } GList *member; for (member = app.tabslist; member != NULL; member = member->next) { if (((E2_OutputTabRuntime *)member->data)->scroll == child) { ((E2_OutputTabRuntime *)member->data)->detached = !attaching; printd (DEBUG, "detached-flag of tab %d set %s", ((E2_OutputTabRuntime *)member->data)->labelnum, (attaching) ? "FALSE":"TRUE"); ((E2_OutputTabRuntime *)member->data)->dropwindow = window; break; } } } /** @brief move tab whose widget is @a child from @a newbook to @a oldbook @param child tab widget @param frombook notebook from which the tab will be removed @param tobook notebook to which the tab will be appended @return */ static void _e2_output_tab_move (GtkWidget *child, GtkNotebook *frombook, GtkNotebook *tobook) { GtkWidget *tab_label, *menu_label; gboolean tab_expand, tab_fill; //, reorderable, detachable; guint tab_pack; //this is essentially the same process that gtk uses g_object_ref (G_OBJECT (child)); tab_label = gtk_notebook_get_tab_label (frombook, child); if (tab_label) g_object_ref (G_OBJECT (tab_label)); menu_label = gtk_notebook_get_menu_label (frombook, child); if (menu_label) g_object_ref (G_OBJECT (menu_label)); gtk_container_child_get (GTK_CONTAINER (frombook), child, "tab-expand", &tab_expand, "tab-fill", &tab_fill, "tab-pack", &tab_pack, // "reorderable", &reorderable, // "detachable", &detachable, NULL); gtk_container_remove (GTK_CONTAINER (frombook), child); gtk_notebook_append_page_menu (tobook, child, tab_label, menu_label); gtk_container_child_set (GTK_CONTAINER (tobook), child, "tab-pack", tab_pack, "tab-expand", tab_expand, "tab-fill", tab_fill, "reorderable", TRUE, //reorderable, "detachable", TRUE, //detachable, NULL); g_object_unref (G_OBJECT (child)); if (tab_label) g_object_unref (G_OBJECT (tab_label)); if (menu_label) g_object_unref (G_OBJECT (menu_label)); if (child == app.tab.scroll) { //this is the default tab gtk_notebook_set_current_page (tobook, -1); } //update detached-data for the tab _e2_output_tab_set_detached_state (child, GTK_WIDGET (tobook)); } #endif //def E2_TABS_DETACH /*********************/ /***** callbacks *****/ /*********************/ /* * @brief save selected output-pane text @param menuitem UNUSED the selected widget, or NULL @param rt runtime struct to work on @return */ /*static void _e2_output_savesel_cb (GtkWidget *menuitem, E2_OutputTabRuntime *rt) { e2_edit_dialog_save_selected (rt->buffer, #ifdef E2_VFS NULL, //local namespace assumed #endif app.main_window); } */ /** @brief edit output-pane-tab text @param menuitem UNUSED the activated widget, or NULL @param rt data struct for the tab @return */ static void _e2_output_edit_cb (GtkMenuItem *menuitem, E2_OutputTabRuntime *rt) { //editing an empty buffer will cause a freeze, so we fake some content... GtkTextIter start, end; gtk_text_buffer_get_bounds (rt->buffer, &start, &end); if (gtk_text_iter_equal (&start, &end)) e2_output_print (rt, " ", NULL, FALSE, NULL); e2_edit_dialog_create (NULL, rt->buffer); } /** @brief handle an output-pane tab change This is callback for the notebook's "switch-page" signal Essentially, tab-specific data are swapped between stack and heap space, and pointers for affected running commands are adjusted accordingly @param notebook the notebook widget @param page UNDOCUMENTED notebook page which is now focused @param page_num the 0-based index of the new page @param data UNUSED pointer to data specified when callback was connected @return */ static void _e2_output_tabchange_cb (GtkNotebook *notebook, GtkNotebookPage *page, guint page_num, gpointer data) { E2_OutputTabRuntime *newtab; #ifdef E2_TABS_DETACH GtkWidget *child = gtk_notebook_get_nth_page (notebook, page_num); GList *member; for (member = app.tabslist; member != NULL; member = member->next) { newtab = (E2_OutputTabRuntime *)member->data; if (newtab->scroll == child) { printd (DEBUG, "output tab change cb, new current-tab ID is %d", newtab->labelnum); break; } } if (member == NULL) return; //should never happen #else printd (DEBUG, "output tab change cb, new current-tab index %d", page_num); // printd (DEBUG, "curr_tab is %x", curr_tab); newtab = g_list_nth_data (app.tabslist, page_num); #endif //swap with mimimum race-risk ... *curr_tab = app.tab; //backup current tab's data from stack to heap app.tab = *newtab; //get the replacement stuff into stackspace curr_tab = newtab; // printd (DEBUG, "curr_tab NOW is %x", curr_tab); //adjust all relevant child foreground-tab pointers to/from the stacked tab data e2_command_retab_children (&app.tab, curr_tab); } /* * @brief process an 'intercepted' double-click on the output pane This handles 'activated' text in the output pane. @param rt runtime struct for the pane @return TRUE if the press was handled */ /*static gboolean _e2_output_activated_cb (E2_OutputTabRuntime *rt) { printd (DEBUG, "output activated cb"); GtkTextIter start, end; if (gtk_text_buffer_get_selection_bounds (rt->buffer, &start, &end)) { gchar *seltext = gtk_text_buffer_get_text (rt->buffer, &start, &end, FALSE); printd (DEBUG, "output text is %s", seltext); _e2_output_open_text (seltext, TRUE); g_free (seltext); //BAD - this locks text DnD on // output_activated = TRUE; //prevent normal button-release handling return TRUE; //block internal click handling } printd (DEBUG, "nothing selected in output"); // output_activated = FALSE; //allow normal button-release handling return FALSE; } */ # ifdef E2_PTRGESTURES /* * @brief process mouse button-release in the output pane This is to block default handling of modified events, so that gestures etc are not interfered with @param textview the widget where the click happened @param event gdk event data struct @param user_data UNUSED pointer specified when cb was connected @return TRUE if modifier key(s) were pressed */ /*BUGGY static gboolean _e2_output_button_filter_cb (GtkWidget *textview, GdkEventButton *event, gpointer user_data) { GdkModifierType mask = e2_utils_get_modifiers (); return ((mask & E2_MODIFIER_MASK) != 0); } */ #endif /** @brief process mouse button-press in the output pane 1/L = focus, 2/M = hide, 3/R = context menu L = open This also detects left-button double-clicks (for which there is no API) and performs an open if relevant @param textview the widget where the click happened @param event gdk event data struct @param rt runtime struct to work on @return TRUE if the signal has been handled here */ static gboolean _e2_output_button_press_cb (GtkWidget *textview, GdkEventButton *event, E2_OutputTabRuntime *rt) { /* GtkTextWindowType wtype = gtk_text_view_get_window_type ( GTK_TEXT_VIEW (textview), event->window); if (wtype != GTK_TEXT_WINDOW_TEXT) return FALSE; */ printd (DEBUG, "output button press cb"); #ifdef E2_TABS_DETACH //just changing notebook page is not sufficient to set current tab when //there's > 1 notebook if (event->type == GDK_BUTTON_PRESS) { if (rt != curr_tab) { //swap with mimimum race-risk ... *curr_tab = app.tab; //backup current tab's data from stack to heap app.tab = *rt; //get the replacement stuff into stackspace curr_tab = rt; //adjust all relevant child foreground-tab pointers to/from the stacked tab data e2_command_retab_children (&app.tab, curr_tab); printd (DEBUG, "output tab change, new current-tab ID is %d", curr_tab->labelnum); } } #endif E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt; /*for double-clicks, the callback sequence is: 1, then another < click interval, then 1 more with 0 click interval for triple-clicks, the callback sequence is: 1, then 2 * last 2 of double click sequence = 5 total The 0-click interval clicks are of type GDK_2BUTTON_PRESS or GDK_3BUTTON_PRESS as appropriate */ if (event->button == 1 && event->type == GDK_BUTTON_PRESS //no multi-click #ifdef E2_MOUSECUSTOM && (event->state & E2_MODIFIER_MASK) == 0 #endif ) { static gboolean valid_selection = FALSE; static GtkTextIter start, end; extern guint click_interval; static guint32 last_event_time = 0; guint32 interval; #ifndef E2_MOUSECUSTOM if (event->state & GDK_CONTROL_MASK) { gchar *text = _e2_output_get_item_at_pointer (event->x, event->y, rrt, TRUE); if (text != NULL) { _e2_output_open_text (text, TRUE); g_free (text); return TRUE; //CHECKME FALSE if open_text failed ? } } #endif interval = event->time - last_event_time; last_event_time = event->time; // printd (DEBUG, "output button press interval %d", interval); //we don't want to repeat the action, if there was a double/triple click if (interval >= click_interval) { // output_activated = FALSE; //reinstate normal release handling //because a selection will be cleared by thge normal release //callback, note what is selected in case it's a double-click valid_selection = gtk_text_buffer_get_selection_bounds (rrt->buffer, &start, &end); /* //CHECKME is this sensible ?? it does not focus the commandline gchar *action_name = g_strconcat (_A(1),".",_A(47),NULL); //_("command.focus") e2_action_run_simple (action_name, textview); g_free (action_name); */ //focus the output tab if it's not going to be hidden if (!e2_option_bool_get_direct (app.output.opt_hide_on_focus_out) #ifdef E2_TABS_DETACH || rrt->detached #endif ) gtk_widget_grab_focus (textview); } else { //reselect what was de-selected by the intervening release callback, //ready for the next press callback (type GDK_2BUTTON_PRESS etc) if (valid_selection) { gtk_text_buffer_select_range (rrt->buffer, &start, &end); valid_selection = FALSE; } } } #ifndef E2_MOUSECUSTOM else if (event->button == 1 && // ( event->type == GDK_2BUTTON_PRESS //|| event->type == GDK_3BUTTON_PRESS) ) { printd (DEBUG, "output button press event type %d", event->type); gchar *text = _e2_output_get_item_at_pointer (event->x, event->y, rrt, TRUE); if (text != NULL) { gboolean retval = _e2_output_open_text (text, TRUE); g_free (text); return retval; // return (_e2_output_activated_cb (rrt)); } //FIXME stop the selected text from being de-selected in the //button-release callback - but we can't just block that !! } else if (event->button == 2 # ifdef E2_TABS_DETACH && !rrt->detached # endif && event->state & GDK_CONTROL_MASK ) //differentiate from default gtk btn2 behaviour { e2_window_output_hide (NULL, NULL, NULL); return TRUE; } #endif //ndef E2_MOUSECUSTOM else if (event->button == 3) { #ifdef E2_MOUSECUSTOM if ((event->state & E2_MODIFIER_MASK) == 0) #endif _e2_output_show_context_menu (textview, 3, event->time, rrt); return TRUE; } return FALSE; } /** @brief process mouse button-release in the output pane This is essentially to re-select text which is de-selected by the standard release callback @param textview the widget where the release happened @param event gdk event data struct @param rt runtime struct to work on @return TRUE if the signal has been handled */ /*static gboolean _e2_output_button_release_cb (GtkWidget *textview, GdkEventButton *event, E2_OutputTabRuntime *rt) { printd (DEBUG, "output button release cb"); return FALSE; E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt; gtk_text_buffer_select_range (rrt->buffer, &start, &end); return TRUE; } */ /** @brief process menu button press in the output pane @param widget the textview widget where the press happened @param rt output runtime struct to work on @return TRUE if the signal has been handled */ static gboolean _e2_output_popup_menu_cb (GtkWidget *widget, E2_OutputTabRuntime *rt) { E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt; gint event_time = gtk_get_current_event_time (); _e2_output_show_context_menu (widget, 0, event_time, rrt); return TRUE; } /** @brief Determine whether the buffer end-mark is visible in its output pane-tab, after manual change to the vertical adjustment This is a callback for manual changes to the output textview vertical adjustment @param adjust UNUSED the vertical adjustment for the textview @param rt data struct for the output pane @return */ static void _e2_output_scrolled_cb (GtkAdjustment *adjust, E2_OutputTabRuntime *rt) { //use active-tab data when relevant E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt; //FIXME make this faster - local copy of the option value ? GtkTextIter iter; if (e2_option_bool_get_direct (app.output.opt_jump_end)) //scrolling only when new output belongs to the last context in the textbuffer gtk_text_buffer_get_end_iter (rrt->buffer, &iter); else gtk_text_buffer_get_iter_at_mark (rrt->buffer, &iter, rrt->mark); #ifdef DEBUG_MESSAGES gint old = g_atomic_int_get (&rrt->onscreen); #endif _e2_output_iter_check_visible (rrt, &iter); #ifdef DEBUG_MESSAGES if (g_atomic_int_get (&rrt->onscreen) == 0) { if (old == 1) printd (NOTICE, "not following anymore"); } else // == 1 if (old == 0) printd (NOTICE, "following again"); #endif } /* static gboolean test_cb (GtkWidget *widget, GdkEventMotion *event) { int x, y; GdkModifierType state; if (event->is_hint) { gdk_window_get_pointer(event->window, &x, &y, &state); printf("x = %d y = %d\n", x, y); } else { printf("x: %lf y: %lf\n", event->x, event->y); } return FALSE; } */ #ifdef E2_TABS_DETACH static void _e2_output_tabattach_cb (GtkMenuItem *menuitem, E2_OutputTabRuntime *rt) { GtkNotebook *newbook = GTK_NOTEBOOK (gtk_bin_get_child (GTK_BIN (rt->dropwindow))); _e2_output_tab_move (rt->scroll, newbook, GTK_NOTEBOOK (app.outbook)); } static void _e2_output_clear_cb (GtkMenuItem *menuitem, E2_OutputTabRuntime *rt) { return _e2_output_clear_buffer (rt); } /** @brief process a tab being dragged This is called for second and later drops onto a tabdrop window and for all drops back onto the output-pane main notebook Page-widget properties are not changed when the page is dragged @param dest_notebook the notebook to which a tab is being dragged @param context drag context data @param x X coordinate where the drop happens @param y Y coordinate where the drop happens @param sel_data the received data @param info the info registered for the target in target_table2 @param time timestamp at which the data was received @param user_data UNUSED data specified when the callback was connected @return */ static void _e2_output_tabdrag_data_received_cb ( GtkWidget *dest_notebook, GdkDragContext *context, gint x, gint y, GtkSelectionData *sel_data, guint info, guint time, gpointer user_data) { printd (DEBUG, "_e2_output_tab_drag_data_received_cb"); gboolean success; #ifdef USE_GTK2_99 ->length, ->data, ->target #endif if (sel_data->length > 0) //&& sel_data->format == 8) { GtkWidget *source_notebook = gtk_drag_get_source_widget (context); GtkWidget *child = *(GtkWidget **)sel_data->data; if (source_notebook == dest_notebook) { // printd (DEBUG, "trying to drag to same place"); success = FALSE; } else if (source_notebook == app.outbook && gtk_notebook_get_n_pages (GTK_NOTEBOOK (source_notebook)) == 1) { //prevent dragging the only output-pane tab // printd (DEBUG, "trying to drag last output tab"); success = FALSE; } else { success = TRUE; _e2_output_tab_set_detached_state (child, dest_notebook); //prevent changes of default tab, by block here //(WAS unblock in tab-removed cb, but that spits error - seems //that gtk unblocks this all by itself ! g_signal_handlers_block_by_func (G_OBJECT (source_notebook), _e2_output_tabchange_cb, NULL); } } else success = FALSE; if (!success) sel_data->target = GDK_NONE; gtk_drag_finish (context, success, FALSE, time); } /** @brief cleanup when a tab-drop window becomes empty This is a callback for the "page-removed" signal on @a notebook @param notebook the affected notebook @param child UNUSED the widget for the removed page @param page_num UNUSED the child page number @param window the parent window for @a notebook @return */ static void _e2_output_tabgone_cb (GtkNotebook *notebook, GtkWidget *child, guint page_num, GtkWidget *window) { printd (DEBUG, "_e2_output_tabgone_cb"); if (notebook != GTK_NOTEBOOK (app.outbook) && gtk_notebook_get_n_pages (notebook) == 0) gtk_widget_destroy (window); /* this generates warning - seems as if gtk already unblocked the thing else //revert the block added when the drag was in progress g_signal_handlers_unblock_by_func (G_OBJECT (notebook), _e2_output_tabchange_cb, NULL); */ } /** @brief cleanup when closing a tab-drop window This moves the tab(s) in the closing window back to the output-pane notebook @param window the window being closed @param event UNUSED event data struct @param data UNUSED data specified when callback was connected @return FALSE to allow event to propogate */ static gboolean _e2_output_tabrestore_cb (GtkWidget *window, GdkEvent *event, gpointer data) { printd (DEBUG, "_e2_output_tabrestore_cb ()"); gint j; GtkNotebook *newbook = GTK_NOTEBOOK (gtk_bin_get_child (GTK_BIN (window))); if (newbook != NULL && (j = gtk_notebook_get_n_pages (newbook)) > 0) { //prevent sequential tab-changes during the cleanout process g_signal_handlers_disconnect_by_func (G_OBJECT (newbook), _e2_output_tabchange_cb, NULL); //no need to go back and check for empty book g_signal_handlers_disconnect_by_func (G_OBJECT (newbook), _e2_output_tabgone_cb, window); GtkWidget *defchild = app.tab.scroll; //remember which page is default now gint i; for (i = 0; i < j; i++) { GtkWidget *child = gtk_notebook_get_nth_page (newbook, 0); _e2_output_tab_move (child, newbook, GTK_NOTEBOOK (app.outbook)); } //reset the default page in the destination book gint indx = gtk_notebook_page_num (GTK_NOTEBOOK (app.outbook), defchild); if (indx != -1) { g_signal_handlers_block_by_func (G_OBJECT (app.outbook), _e2_output_tabchange_cb, NULL); gtk_notebook_set_current_page (GTK_NOTEBOOK (app.outbook), indx); g_signal_handlers_unblock_by_func (G_OBJECT (app.outbook), _e2_output_tabchange_cb, NULL); } } return FALSE; } /** @brief when a mouse-button is pressed on tab "label", update current-page data if needed This is a callback for the "grab-focus" signal on @a notebook It's called after a tab-change, if any @param notebook the affected notebook @param user_data UNUSED data specified when the callback was connected @return */ static void _e2_output_grab_focus_cb (GtkWidget *notebook, gpointer user_data) { printd (DEBUG, "_e2_output_grab_focus_cb"); gint curindx = gtk_notebook_get_current_page (GTK_NOTEBOOK (notebook)); GtkWidget *child = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook), curindx); GList *member; for (member = app.tabslist; member != NULL; member = member->next) { E2_OutputTabRuntime *tab; tab = (E2_OutputTabRuntime *)member->data; if (tab->scroll == child) { if (tab != curr_tab) { //swap with mimimum race-risk ... *curr_tab = app.tab; //backup current tab's data from stack to heap app.tab = *tab; //get the replacement stuff into stackspace curr_tab = tab; //adjust all relevant child foreground-tab pointers to/from the stacked tab data e2_command_retab_children (&app.tab, curr_tab); printd (DEBUG, "output tab change, new current-tab ID is %d", tab->labelnum); } break; } } } //for "focus-in-event" NO USE /*static gboolean _e2_output_focus_in_cb (GtkWidget *widget, GdkEventFocus *event, gpointer user_data) { printd (DEBUG, "_e2_output_focus_in_cb"); return FALSE; } void _e2_output_page_add_cb (GtkNotebook *notebook, GtkWidget *child, guint page_num, gpointer user_data) { printd (DEBUG, "_e2_output_page_add_cb"); } */ /** @brief when a detached notebook tab is dropped in an empty area, create a window containing a notebook to receive the tab Page-widget properties are not changed when dragged @param source the source GtkNotebook of the drag operation @param sw the child GtkWidget to be dropped @param x the X coordinate where the drop happens @param y the Y coordinate where the drop happens @param data data specified when the arrangement was set up @return the created GtkNotebook where the tab will be attached, or NULL to cancel the drag */ static GtkNotebook *_e2_output_tab_drop_new (GtkNotebook *source, GtkWidget *sw, gint x, gint y, gpointer data) { printd (DEBUG, "_e2_output_tab_drop_new ()"); printd (DEBUG, "name of current tab is %d", curr_tab->labelnum); if (gtk_notebook_get_n_pages (source) == 1) return NULL; //prevent dragging of the only tab GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL); e2_window_set_title (window, _("output tabs")); // gtk_window_set_role (GTK_WINDOW (window), "tabdrop"); // gtk_window_set_wmclass (GTK_WINDOW (window), "main", BINNAME); #ifdef E2_COMPOSIT e2_window_set_opacity (window, -1); #endif gtk_window_set_position (GTK_WINDOW (window), GTK_WIN_POS_MOUSE); //restore dropped tab(s) when new window is closed g_signal_connect (G_OBJECT (window), "delete-event", G_CALLBACK (_e2_output_tabrestore_cb), NULL); GtkWidget *notebook = gtk_notebook_new (); printd (DEBUG, "new notebook widget = %x", notebook); gtk_container_add (GTK_CONTAINER (window), notebook); //CRASHER gtk_drag_source_set (notebook, GDK_BUTTON1_MASK, target_table2, n_targets2, // GDK_ACTION_PRIVATE); //setup to process further tabs being dragged here gtk_drag_dest_set (notebook, GTK_DEST_DEFAULT_DROP, target_table2, n_targets2, GDK_ACTION_MOVE); g_signal_connect (G_OBJECT (notebook), "drag-data-received", G_CALLBACK (_e2_output_tabdrag_data_received_cb), NULL); //CHECKME user_data //close window when all tabs dragged out or re-attached by menu-selection g_signal_connect (G_OBJECT (notebook), "page-removed", G_CALLBACK (_e2_output_tabgone_cb), window); //capture clicks on tab labels g_signal_connect (G_OBJECT (notebook), "grab-focus", G_CALLBACK (_e2_output_grab_focus_cb), NULL); // g_signal_connect (G_OBJECT (notebook), "focus-in-event", // G_CALLBACK (_e2_output_focus_in_cb), NULL); // g_signal_connect (G_OBJECT (notebook), "page-added", // G_CALLBACK (_e2_output_page_add_cb), NULL); g_signal_connect (G_OBJECT (notebook), "switch-page", G_CALLBACK (_e2_output_tabchange_cb), NULL); //no data #ifdef USE_GTK2_12DND //allow further dragging from this window //(this seems to be un-necessary here, though that may be a gtk deficiency) g_signal_connect (G_OBJECT (notebook), "create-window", G_CALLBACK (_e2_output_tab_drop_new), NULL); //CHECKME user_data #endif e2_keybinding_enrol (window, _C(17), (void(*)(E2_OptionSet*))NULL); #ifdef E2_MOUSECUSTOM e2_mousebinding_enrol (window, _C(17), (void(*)(E2_OptionSet*))NULL); # ifdef E2_PTRGESTURES e2_mousegesture_enrol (window, _C(17), (void(*)(E2_OptionSet*))NULL); # endif #endif GtkNotebook *book = GTK_NOTEBOOK (notebook); gtk_notebook_popup_enable (book); gtk_notebook_set_scrollable (book, TRUE); gtk_notebook_set_show_border (book, FALSE); gtk_notebook_set_show_tabs (book, TRUE); GtkCornerType where = (GtkCornerType) e2_option_sel_get ("scrollbar-position"); #ifdef ADJACENT_TABS if (where == GTK_CORNER_TOP_LEFT || where == GTK_CORNER_BOTTOM_LEFT) gtk_notebook_set_tab_pos (book, GTK_POS_RIGHT); else gtk_notebook_set_tab_pos (book, GTK_POS_LEFT); #else if (where == GTK_CORNER_TOP_LEFT || where == GTK_CORNER_BOTTOM_LEFT) gtk_notebook_set_tab_pos (book, GTK_POS_LEFT); else gtk_notebook_set_tab_pos (book, GTK_POS_RIGHT); #endif // these 2 needed ? // gtk_notebook_set_tab_reorderable (book, sw, TRUE); // gtk_notebook_set_tab_detachable (book, sw, TRUE); #ifdef USE_GTK2_12DND gpointer group = gtk_notebook_get_group (source); gtk_notebook_set_group (book, group); #else gint group_id = gtk_notebook_get_group_id (source); gtk_notebook_set_group_id (book, group_id); #endif #ifdef USE_GTK2_18 GtkAllocation alloc; GtkAllocation alloc2; gtk_widget_get_allocation (app.main_window, &alloc); gtk_widget_get_allocation (app.tab.scroll, &alloc2); #endif gtk_window_set_default_size (GTK_WINDOW (window), #ifdef USE_GTK2_18 alloc.width, alloc2.height); #else app.main_window->allocation.width, app.tab.scroll->allocation.height); #endif gtk_widget_show_all (window); //dragging has made the dragged tab "current", so we can set data for that app.tab.dropwindow = window; //set flag for tab context-menu so it shows only items relevant to separate window app.tab.detached = TRUE; printd (DEBUG, "detached-flag of tab %d set TRUE", app.tab.labelnum); //gtk will change the active tab in the source notebook and then the active //page in the dest notebook - any way to prevent the former of these ? return book; } #endif //def E2_TABS_DETACH /*******************/ /***** actions *****/ /*******************/ /** @brief print to output pane, with default settings Any escaped newlines are converted to real ones. @param from the button, menu item etc which was activated @param art action runtime data @return TRUE */ static gboolean _e2_output_print_action (gpointer from, E2_ActionRuntime *art) { gchar *real_message = e2_utils_str_replace ((gchar *)art->data , "\\n", "\n"); e2_output_print (&app.tab, real_message, NULL, FALSE, NULL); g_free (real_message); return TRUE; } /** @brief print help message to output pane This expects as data a string indicating which message is to be displayed @param from the button, menu item etc which was activated @param art action runtime data @return TRUE if type of help was recognised */ static gboolean _e2_output_help_action (gpointer from, E2_ActionRuntime *art) { gchar *arg = (gchar *)art->data; if ((arg == NULL) || (*arg == '\0') || g_str_has_prefix (arg, _("commands"))) { e2_command_output_help (); return TRUE; } else if (g_str_has_prefix (arg, _("keys"))) //this must be translated same as in default aliases { e2_keybinding_output_help (arg); return TRUE; } #ifdef E2_MOUSECUSTOM else if (g_str_has_prefix (arg, _("buttons"))) //this must be translated same as in default aliases { e2_mousebinding_output_help (arg); return TRUE; } #endif return FALSE; } /** @brief move the displayed text window by @a arg 'steps' This expects as data a string containing the no. of 'steps' to move @param from the button, menu item etc which was activated @param art action runtime data @return TRUE */ static gboolean _e2_output_scroll_step (gpointer from, E2_ActionRuntime *art) { //action data = TRUE to move down, FALSE to move up gboolean down = GPOINTER_TO_INT (art->action->data); _e2_output_scroll_helper (down, (gchar *)art->data, FALSE); return TRUE; } /** @brief move the displayed text window by @a arg 'pages' This expects as data a string containing the no. of 'pages' to move @param from the button, menu item etc which was activated @param art action runtime data @return TRUE */ static gboolean _e2_output_scroll_page (gpointer from, E2_ActionRuntime *art) { //action data = TRUE to move down, FALSE to move up gboolean down = GPOINTER_TO_INT (art->action->data); _e2_output_scroll_helper (down, (gchar *)art->data, TRUE); return TRUE; } /** @brief move the displayed text window to start or end of its content @param from the button, menu item etc which was activated @param art action runtime data @return TRUE */ static gboolean _e2_output_scroll_all (gpointer from, E2_ActionRuntime *art) { E2_OutputTabRuntime *rt = &app.tab; g_return_val_if_fail (rt->scroll != NULL, FALSE); GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (rt->scroll)); //action data = TRUE to move down, FALSE to move up gboolean down = GPOINTER_TO_INT (art->action->data); if (down) gtk_adjustment_set_value (vadj, #ifdef USE_GTK2_14 (gtk_adjustment_get_upper (vadj) - gtk_adjustment_get_page_size (vadj)) #else vadj->upper - vadj->page_size #endif ); else gtk_adjustment_set_value (vadj, 0.0); return TRUE; } /** @brief clear all content of an output-pane tab @param from the button, menu item etc which was activated @param art action runtime data @return TRUE */ static gboolean _e2_output_clear_action (gpointer from, E2_ActionRuntime *art) { #ifdef E2_TABS_DETACH _e2_output_clear_buffer (&app.tab); #else // gdk_threads_enter (); //order of things here is to handle clearing when the text buffer is being added to GtkTextTagTable *table = gtk_text_buffer_get_tag_table (app.tab.buffer); GtkTextBuffer *buffer = gtk_text_buffer_new (table); GtkTextIter start; gtk_text_buffer_get_start_iter (buffer, &start); GtkTextMark *mark = gtk_text_buffer_create_mark (buffer, NULL, &start, FALSE); WAIT_FOR_EVENTS gtk_text_view_set_buffer (app.tab.text, buffer); // gdk_threads_leave (); app.tab.buffer = buffer; app.tab.mark = mark; g_atomic_int_set (&app.tab.onscreen, 1); g_object_unref (G_OBJECT (buffer)); #endif *(curr_tab) = app.tab; //listed tab data needs the updated values too return TRUE; } /** @brief create another output pane tab, and go there @param from the button, menu item etc which was activated @param art action runtime data @return TRUE */ static gboolean _e2_output_tab_add (gpointer from, E2_ActionRuntime *art) { E2_OutputTabRuntime *tab = ALLOCATE0 (E2_OutputTabRuntime); CHECKALLOCATEDWARN (tab, return FALSE;) GtkWidget *sw = _e2_output_create_view (tab); GtkWidget *wid; #ifdef USE_GTK2_10 //there may be gaps in the tab labels //and for gtk >= 2.10, tabs can be in any order or in another notebook, //so check all tabs to find the last one and bump its label gint lablid, new = 1; GList *member; for (member = app.tabslist; member != NULL; member = member->next) { lablid = ((E2_OutputTabRuntime *)member->data)->labelnum; if (lablid >= new) new = lablid + 1; } tab->labelnum = new; //remember, for later searching #else wid = gtk_notebook_get_nth_page (GTK_NOTEBOOK (app.outbook), app.tabcount-1); const gchar *labtxt = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (app.outbook), wid); gint new = atoi (labtxt) + 1; #endif gchar *txt = g_strdup_printf ("%d", new); //no translation wid = gtk_label_new (txt); g_free (txt); gtk_notebook_append_page (GTK_NOTEBOOK (app.outbook), sw, wid); #ifdef USE_GTK2_10 gtk_notebook_set_tab_reorderable (GTK_NOTEBOOK (app.outbook), sw, TRUE); #ifdef E2_TABS_DETACH gtk_notebook_set_tab_detachable (GTK_NOTEBOOK (app.outbook), sw, TRUE); #endif #endif app.tabslist = g_list_append (app.tabslist, tab); app.tabcount++; //log, for cache if (app.tabcount == 2) //doesn't matter where the tabs are gtk_notebook_set_show_tabs (GTK_NOTEBOOK (app.outbook), TRUE); //focus the new tab #ifdef E2_TABS_DETACH new = gtk_notebook_get_n_pages (GTK_NOTEBOOK (app.outbook)); gtk_notebook_set_current_page (GTK_NOTEBOOK (app.outbook), new-1); #else gtk_notebook_set_current_page (GTK_NOTEBOOK (app.outbook), app.tabcount - 1); //uses new last-tab index #endif return TRUE; } /** @brief remove currently focused output pane tab This may be called from a keybinding or context menu item Essentially, tab-specific data are swpped between stack and heap space, and pointers for affected running commands are adjusted accordingly @param from the button, menu item etc which was activated @param art action runtime data @return TRUE if the removal was done */ static gboolean _e2_output_tab_remove (gpointer from, E2_ActionRuntime *art) { #ifdef E2_TABS_DETACH //ignore removal of only tab in this notebook gint homecount = gtk_notebook_get_n_pages (GTK_NOTEBOOK (app.outbook)); if (homecount == 1) return FALSE; //cleanup the notebook //FIXME only if tab in this notebook (context menu cleanup to prevent this ?) gint index = gtk_notebook_get_current_page (GTK_NOTEBOOK (app.outbook)); GtkWidget *sw = gtk_notebook_get_nth_page (GTK_NOTEBOOK (app.outbook), index); GList *member; for (member = app.tabslist; member != NULL; member = member->next) { if (((E2_OutputTabRuntime *)member->data)->scroll == sw) break; } if (member == NULL) return FALSE; //should never happen gtk_notebook_remove_page (GTK_NOTEBOOK (app.outbook), index); if (--app.tabcount == 1) gtk_notebook_set_show_tabs (GTK_NOTEBOOK (app.outbook), FALSE); index = gtk_notebook_get_current_page (GTK_NOTEBOOK (app.outbook)); sw = gtk_notebook_get_nth_page (GTK_NOTEBOOK (app.outbook), index); GList *newmember; for (newmember = app.tabslist; newmember != NULL; newmember = newmember->next) { if (((E2_OutputTabRuntime *)newmember->data)->scroll == sw) break; } if (newmember == NULL) return FALSE; //should never happen /*//CHECKME does this conform to gtk behaviour ? GList *member2; if (member == app.tabslist) member2 = member->next; else member2 = member->prev; if (member2 != newmember) printd (DEBUG, "replacement tab not correct !!"); */ #else //ignore removal of only tab if (app.tabcount == 1) return FALSE; //cleanup the notebook gint index = gtk_notebook_get_current_page (GTK_NOTEBOOK (app.outbook)); gtk_notebook_remove_page (GTK_NOTEBOOK (app.outbook), index); if (--app.tabcount == 1) gtk_notebook_set_show_tabs (GTK_NOTEBOOK (app.outbook), FALSE); //CHECKME does this conform to gtk behaviour ? GList *member = g_list_nth (app.tabslist, index); GList *newmember; if (index == 0) newmember = member->next; else newmember = member->prev; #endif E2_OutputTabRuntime *newtab = newmember->data; // printd (DEBUG, "curr_tab is %x", curr_tab); // printd (DEBUG, "replacement tab at %x", newtab); //quickly install replacement data, ready for use by any printing children app.tab = *newtab; //anthing running in the focused tab stays there, //so no need to update foreground pointers //but do update background-tab ptrs of children using the tab e2_command_retab2_children (member->data, newtab); // != curr_tab? //now its ok to update the list pointer curr_tab = newtab; //clean the old tab's data FIXME any leak ? text buffer ? if (((E2_OutputTabRuntime *)member->data)->origin_lastime!= NULL) g_free (((E2_OutputTabRuntime *)member->data)->origin_lastime); // GHashTable *hash = ((E2_OutputTabRuntime *)member->data)->origins; // if (hash != NULL) // g_hash_table_destroy (hash); // printd (DEBUG, "removing tab at %x", member->data); gchar *category = g_strconcat (_C(17),".",_C(28),NULL); //_("general.output e2_keybinding_disrol (GTK_WIDGET (((E2_OutputTabRuntime*)member->data)->text), category); #ifdef E2_MOUSECUSTOM e2_mousebinding_disrol (GTK_WIDGET (((E2_OutputTabRuntime*)member->data)->text), category); # ifdef E2_PTRGESTURES e2_mousegesture_disrol (GTK_WIDGET (((E2_OutputTabRuntime*)member->data)->text), category); # endif #endif g_free (category); DEALLOCATE (E2_OutputTabRuntime, member->data); app.tabslist = g_list_delete_link (app.tabslist, member); //counter adjusted above return TRUE; } /** @brief focus output pane @param from UNUSED the button, menu item etc which was activated @param art action runtime data @return TRUE if the tab was found and made current */ static gboolean _e2_output_focus_action (gpointer from, E2_ActionRuntime *art) { gtk_widget_grab_focus (GTK_WIDGET (curr_tab->text)); //action data, if any, is a string with tab number if (art->data == NULL || *(gchar *)art->data == '\0') return TRUE; else { gchar *end; gulong num = strtoul ((gchar *)art->data, &end, 10); if (*end != '\0') return FALSE; if (num == 0 || num > g_list_length (app.tabslist)) return FALSE; #ifdef USE_GTK2_10 //there may be gaps in the tab labels //and for gtk >= 2.10, tabs can be in any order or in any notebook, //so check all tabs to find the matching label GList *member; for (member = app.tabslist; member != NULL; member = member->next) { E2_OutputTabRuntime *tab = (E2_OutputTabRuntime *)member->data; if (tab->labelnum == num) { GtkWidget *book; # ifdef E2_TABS_DETACH if (tab->detached) //tab has been dropped to a separate window { book = gtk_widget_get_ancestor (tab->scroll, GTK_TYPE_NOTEBOOK); gtk_widget_grab_focus (tab->dropwindow); } else # endif book = app.outbook; gint indx = gtk_notebook_page_num (GTK_NOTEBOOK (book), tab->scroll); gtk_notebook_set_current_page (GTK_NOTEBOOK (book), indx); if (tab != curr_tab) { //swap with mimimum race-risk ... *curr_tab = app.tab; //backup current tab's data from stack to heap app.tab = *tab; //get the replacement stuff into stackspace curr_tab = tab; //adjust all relevant child foreground-tab pointers to/from the stacked tab data e2_command_retab_children (&app.tab, curr_tab); printd (DEBUG, "output tab change, new current-tab ID is %d", curr_tab->labelnum); } break; } } return (member != NULL); #else gtk_notebook_set_current_page (GTK_NOTEBOOK (app.outbook), num -1); E2_OutputTabRuntime *tab = g_list_nth_data (app.tabslist, num - 1); if (tab != curr_tab) { //swap with mimimum race-risk ... *curr_tab = app.tab; //backup current tab's data from stack to heap app.tab = *tab; //get the replacement stuff into stackspace curr_tab = tab; //adjust all relevant child foreground-tab pointers to/from the stacked tab data e2_command_retab_children (&app.tab, curr_tab); printd (DEBUG, "output tab change, new current-tab ID is %d", num - 1); } return TRUE; #endif } } /** @brief activate filetype item if any at the pointer in output pane This is primarily for a pointer button action @param from the button, menu item etc which was activated @param art UNUSED action runtime data @return TRUE if there was a recognised filetype string to open */ static gboolean _e2_output_open_filetype (gpointer from, E2_ActionRuntime *art) { gint x, y; gdk_window_get_pointer ( #ifdef USE_GTK2_14 gtk_widget_get_window (GTK_WIDGET (curr_tab->text)), #else GTK_WIDGET (curr_tab->text)->window, #endif &x, &y, NULL); gchar *text = _e2_output_get_item_at_pointer (x, y, curr_tab, TRUE); if (text != NULL) { gboolean retval = _e2_output_open_text (text, TRUE); g_free (text); return retval; // return (_e2_output_activated_cb (curr_tab)); } return FALSE; } /******************/ /***** public *****/ /******************/ /** @brief [re]register all key-binding callbacks for an output-tab textview There are also generic keypress-event bindings with 'higher priority' than these @param textview the widget being processed @return */ void e2_output_register_keybindings (GtkWidget *textview) { //other "key-press-event" connection(s) have non-NULL data //also, if needed, could use bindings cb func guint id = g_signal_lookup ("key-press-event", GTK_TYPE_TEXT_VIEW); g_signal_handlers_disconnect_matched (G_OBJECT (textview), G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_DATA , id, 0, NULL, NULL, NULL); gchar *category = g_strconcat(_C(17),".",_C(23),".",_C(28),NULL); //_("general.main.output e2_keybinding_enrol (textview, category, (void(*)(E2_OptionSet*))NULL); //connection may be sync or async, see E2_IDLE_KEYSYNC g_free (category); } #ifdef E2_MOUSECUSTOM /** @brief [re]register all button-event callbacks for an output-tab textview @param textview the widget being processed @return */ void e2_output_register_pointerbindings (GtkWidget *textview) { //other "button-press-event" connection(s) have non-NULL data //also if needed, could use bindings cb func guint id = g_signal_lookup ("button-press-event", GTK_TYPE_TEXT_VIEW); g_signal_handlers_disconnect_matched (G_OBJECT (textview), G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_DATA, id, 0, NULL, NULL, NULL); // id = g_signal_lookup ("button-release-event", GTK_TYPE_TEXT_VIEW); // g_signal_handlers_disconnect_matched (G_OBJECT (textview), // G_SIGNAL_MATCH_ID, id, 0, NULL, NULL, NULL); gchar *category = g_strconcat(_C(17),".",_C(23),".",_C(28),NULL); //_("general.main.output e2_mousebinding_enrol (textview, category, (void(*)(E2_OptionSet*))NULL); #ifdef E2_PTRGESTURES e2_mousegesture_enrol (textview, category, (void(*)(E2_OptionSet*))NULL); #endif g_free (category); } #endif /** @brief show end-of-output message in tab associated with @a tab @param tab pointer to tab runtime @param beep TRUE to sound when the message is printed @return */ void e2_output_print_end (E2_OutputTabRuntime *tab, gboolean beep) { e2_output_print (tab, _("-- end-of-output --"), NULL, TRUE, "small", "grey", NULL); if (beep) e2_utils_beep (); } /** @brief show error message @a msg in current output pane tab, with beep @param msg message @param freemsg TRUE to free @a msg after it has been shown @return */ //FIXME use the "correct" tab instead of always the current one void e2_output_print_error (gchar *msg, gboolean freemsg) { e2_output_print (&app.tab, msg, NULL, TRUE, E2_ERRORTAGS, NULL); if (freemsg) g_free (msg); e2_utils_beep (); } /** @brief show system error message in current output pane tab, with beep @return */ //FIXME use the "correct" tab instead of always the current one void e2_output_print_strerrno (void) { e2_output_print (&app.tab, (gchar *) g_strerror (errno), NULL, TRUE, E2_ERRORTAGS, NULL); e2_utils_beep (); } /** @brief show @a msg in output pane @a msg is converted to UTF-8 if it's not that encoding already Provides special handling of any '\\b', '\\r' char(s) in @a msg for an error message, style parameters are ignored Expects BGL to be on/closed @param tab pointer to data structure for the tab to get the message @param msg actual message @param origin context to which the message belongs (eg a pid string) or NULL for default @param newline TRUE if @a msg is to be printed with pre- and post- newline @param first_tag one or more tags to apply, terminated by NULL @return */ void e2_output_print (E2_OutputTabRuntime *tab, gchar *msg, gchar *origin, gboolean newline, const gchar *first_tag, ...) { //early exit if (msg == NULL || *msg == '\0') return; // printd (DEBUG, "e2_ouput_print (msg:,origin:%s,newline:%d,first_tag:)", // origin, newline); //leading backspaces delete characters from the last message of this context, if any gint back_prior = 0; while (msg[0] == '\b') { back_prior++; msg++; } //show the output pane if the user wishes that if (!app.output.visible && e2_option_bool_get_direct (app.output.opt_show_on_new)) { e2_window_output_show (NULL, NULL); } VOL gchar *utf; if (g_utf8_validate (msg, -1, NULL)) utf = msg; else { //convert to utf before inserting GError *error = NULL; utf = g_locale_to_utf8 (msg, -1, NULL, NULL, &error); if (error != NULL) { printd (WARN, "locale string to UTF8 conversion failed: %s", error->message); g_error_free (error); utf = e2_utf8_from_locale_fallback (msg); } } //parse the utf string to handle special characters VOL gboolean line_return = FALSE; VOL gchar *s1, *s2; gchar *strend; strend = (gchar*)(utf + strlen ((gchar*)utf)); for (s1 = utf; s1 < strend; s1++) { register gchar c = *s1; // printd (DEBUG, "%c - %d", c, (gint) c; /* no point in handling just this escape code ... if (c == 27) //escape { //clear sequence: ESC[2J if ((s1+3) < end && *(s1+1) == '[' && *(s1+2) == '2' && *(s1+3) == 'J') { _e2_output_clear (); return; } } else */ if (c == '\r' && *(s1 + sizeof(gchar)) != '\n') //carriage return { if (s1 + sizeof(gchar) == strend) { //return is at end of msg *s1 = '\0'; //doesn't matter if it's also the start of msg line_return = TRUE; //wait until next msg before doing anything } else { s2 = s1 - sizeof (gchar); while (s2 >= utf) { if (*s2 == '\n') { gint slide = (strend - s1); //include trailing 0 memmove ((gchar *)(s2 + 1), (gchar *)(s1 + 1), slide); strend -= (s1-s2); s1 = s2; //allow for loop-end increment break; } s2--; } if (s2 < utf) { //FIXME properly handle sole-\r in msg *s1 = '\n'; line_return = TRUE; } } } else if (c == '\b') //backspace { do { //find a character back in the string that's not already a backspace s2 = s1 - sizeof (gchar); while (s2 >= utf) { c = *s2; if (c >= 0x80) //part of UTF-8 multi-byte char { //overwrite it with backspaces do { *s2 = '\b'; } while (--s2 >= utf && *s2 > 0x80); break; } else if (c != '\b') { //overwrite it with backspace *s2 = '\b'; break; } else { //scan more s2--; } } //if there is no prior non-backspace character, //setup to delete one from the previous message in this context if (s2 < utf) back_prior++; s1++; } while (s1 < strend && *s1 == '\b'); //adjacent backspace in msg if (s2 < utf) s2 = utf; //s1 now points after last \b char, s2 points to first one //get rid of them gint slide = (strend + sizeof (gchar) - s1); //include trailing 0 memmove ((gchar *)s2, (gchar *)s1, slide); strend -= (s1-s2); s1 = s2 - sizeof (gchar); //allow for loop-end increment } } VOL gboolean is_default_origin; //flag whether this origin is new VOL gboolean is_new; //get the text buffer for the output pane VOL GtkTextBuffer *buffer = tab->buffer; VOL GtkTextIter start, end; //ensure that there's an origin set if (origin == NULL) { origin = "default"; //no translate is_default_origin = TRUE; } else is_default_origin = !strcmp (origin, "default"); VOL gchar *mark_del_name = g_strconcat (origin, "-del", NULL); //no translate VOL GtkTextMark *mark_del = gtk_text_buffer_get_mark ((GtkTextBuffer*)buffer, (gchar*)mark_del_name); //try to get insert mark //it exists if there has been output in this origin before VOL GtkTextMark *origin_mark = gtk_text_buffer_get_mark ((GtkTextBuffer*)buffer, origin); //if not, create it at the end if (origin_mark == NULL) { is_new = TRUE; gtk_text_buffer_get_end_iter ((GtkTextBuffer*)buffer, (GtkTextIter*)&end); origin_mark = gtk_text_buffer_create_mark ((GtkTextBuffer*)buffer, origin, (GtkTextIter*)&end, TRUE); } else { is_new = FALSE; //the default context always outputs to the end of the textview //(ie the output is not "glued together" if (is_default_origin) { gtk_text_buffer_get_end_iter ((GtkTextBuffer*)buffer, (GtkTextIter*)&end); gtk_text_buffer_move_mark ((GtkTextBuffer*)buffer, (GtkTextMark*)origin_mark, (GtkTextIter*)&end); } else gtk_text_buffer_get_iter_at_mark ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, (GtkTextMark*)origin_mark); //do we have to overwrite a previous message's characters in front of us? //(if a previous message had a carriage return '\r' in it) if (mark_del != NULL) { VOL GtkTextIter iter_del; gtk_text_buffer_get_iter_at_mark ((GtkTextBuffer*)buffer, (GtkTextIter*)&iter_del, (GtkTextMark*)mark_del); gtk_text_buffer_delete ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, (GtkTextIter*)&iter_del); //delete the mark, no further use this time gtk_text_buffer_delete_mark ((GtkTextBuffer*)buffer, (GtkTextMark*)mark_del); } } //move the scroll-helper mark gtk_text_buffer_move_mark ((GtkTextBuffer*)buffer, tab->mark, (GtkTextIter*)&end); gtk_text_buffer_place_cursor ((GtkTextBuffer*)buffer, (GtkTextIter*)&end); if (back_prior > 0) { //want to backspace into previuos message for this context VOL gint _offset = gtk_text_iter_get_offset ((GtkTextIter*)&end); GtkTextIter backi; //FIXME limit backshift to start of current context //CHECKME ignore \n for the purposes of going backwards gtk_text_buffer_get_iter_at_offset ((GtkTextBuffer*)buffer, &backi, _offset - back_prior); gtk_text_buffer_delete ((GtkTextBuffer*)buffer, &backi, (GtkTextIter*)&end); // gtk_text_buffer_get_iter_at_offset ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, _offset - back_prior); end = backi; } VOL gint lfcount = 0; //if necessary, insert a newline character before the message because //it has a different context from the last one and the last one hasn't //printed one yet; // g_static_rec_mutex_lock (&print_mutex); if (tab->origin_lastime != NULL && strcmp (tab->origin_lastime, origin) && !gtk_text_iter_starts_line ((GtkTextIter*)&end) && (is_new || is_default_origin)) lfcount = 1; // g_static_rec_mutex_unlock (&print_mutex); if (newline && (!gtk_text_iter_starts_line ((GtkTextIter*)&end))) { printd (DEBUG, "inserted additional newline because the message requested it"); lfcount++; } VOL GtkTextMark *mark_end; if (lfcount > 0) { gtk_text_buffer_insert ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, "\n", lfcount); mark_end = gtk_text_buffer_get_insert ((GtkTextBuffer*)buffer); gtk_text_buffer_get_iter_at_mark ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, (GtkTextMark*)mark_end); } //AT LAST, WE PUT IT IN ! gtk_text_buffer_insert ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, (gchar*)utf, -1); //get start and end of text just inserted gtk_text_buffer_get_iter_at_mark ((GtkTextBuffer*)buffer, (GtkTextIter*)&start, (GtkTextMark*)origin_mark); mark_end = gtk_text_buffer_get_insert ((GtkTextBuffer*)buffer); gtk_text_buffer_get_iter_at_mark ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, (GtkTextMark*)mark_end); if (first_tag != NULL) { //apply style-tags (if any) for added text va_list args; va_start (args, first_tag); VOL const gchar *tag_name = first_tag; while (tag_name != NULL) { #ifdef USE_GTK2_99 ->tag_table #endif VOL GtkTextTag *tag = gtk_text_tag_table_lookup (((GtkTextBuffer*)buffer)->tag_table, (gchar*)tag_name); /* tags are all pre-configured if (tag == NULL) printd (WARN, "%s: no tag with name '%s'!", G_STRLOC, tag_name); else */ gtk_text_buffer_apply_tag ((GtkTextBuffer*)buffer, (GtkTextTag*)tag, (GtkTextIter*)&start, (GtkTextIter*)&end); tag_name = va_arg (args, const gchar*); } va_end (args); } /* when there is a context after the current one, the font weight of the later one (eg bold) is sometimes (eg current has error then normal) used instead of the proper font (normal) EVEN IF the inserted text is explicitly set to normal style probably a gtk bug as the font color is correct the following is a workaround */ else //if (!gtk_text_iter_is_end (&end)) { //gtk_text_buffer_remove_all_tags (buffer, &start, &end); VOL GSList *tags = gtk_text_iter_get_tags ((GtkTextIter*)&start); if (tags != NULL) { gtk_text_buffer_remove_all_tags ((GtkTextBuffer*)buffer, (GtkTextIter*)&start, (GtkTextIter*)&end); g_slist_free ((GSList*)tags); } } //if there was a carriage return, save marks for processing during the next message if (line_return) { //find the start of current line VOL GtkTextIter backsearch; if (!gtk_text_iter_backward_search ((GtkTextIter*)&end, "\n", GTK_TEXT_SEARCH_TEXT_ONLY, NULL, (GtkTextIter*)&backsearch, NULL)) gtk_text_buffer_get_start_iter ((GtkTextBuffer*)buffer, (GtkTextIter*)&backsearch); gtk_text_buffer_place_cursor ((GtkTextBuffer*)buffer, (GtkTextIter*)&backsearch); //move the origin-mark there gtk_text_buffer_move_mark ((GtkTextBuffer*)buffer, (GtkTextMark*)origin_mark, (GtkTextIter*)&backsearch); gtk_text_buffer_create_mark ((GtkTextBuffer*)buffer, (gchar*)mark_del_name, (GtkTextIter*)&end, TRUE); } else if (newline) //CHECKME handle newline and line_return together ? { //trailing \n gtk_text_buffer_insert ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, "\n", 1); mark_end = gtk_text_buffer_get_insert ((GtkTextBuffer*)buffer); gtk_text_buffer_get_iter_at_mark ((GtkTextBuffer*)buffer, (GtkTextIter*)&end, (GtkTextMark*)mark_end); } //if there was no carriage return, set mark for next message in this context if (!line_return) gtk_text_buffer_move_mark ((GtkTextBuffer*)buffer, (GtkTextMark*)origin_mark, (GtkTextIter*)&end); //scroll down if necessary if (//scrolling to new output is in effect e2_option_bool_get_direct (app.output.opt_jump) && (//manual scrolling has not moved the insert mark off-screen g_atomic_int_get (&tab->onscreen) == 1 //OR scroll regardless of whether manually scrolled away || !e2_option_bool_get_direct (app.output.opt_jump_follow)) && ( //added text is at the end of the buffer gtk_text_iter_is_end ((GtkTextIter*)&end) //OR don't care whether added text is at the end of the buffer || !e2_option_bool_get_direct (app.output.opt_jump_end))) { _e2_output_iter_check_visible (tab, (GtkTextIter*)&end); if (g_atomic_int_compare_and_exchange (&tab->onscreen, 0, 1)) //the end of the added text is not visible now gtk_text_view_scroll_to_mark (tab->text, (GtkTextMark*)mark_end, 0.0, TRUE, 0.1, 1.0); } if (utf != msg) g_free ((gchar *)utf); g_free ((gchar *)mark_del_name); //protect threaded clearing of origin_buffer g_static_rec_mutex_lock (&print_mutex); if (tab->origin_lastime == NULL || strcmp (tab->origin_lastime, origin)) { //save the origin of the last message if (tab->origin_lastime != NULL) g_free (tab->origin_lastime); tab->origin_lastime = g_strdup (origin); } g_static_rec_mutex_unlock (&print_mutex); } #undef VOL /** @brief update several output pane textview settings to conform to current config parameters Used only during window re-creation @return */ void e2_output_update_style (void) { const gchar *fntname = e2_utils_get_output_font (); PangoFontDescription *font_desc = pango_font_description_from_string (fntname); app.output.font_size = pango_font_description_get_size (font_desc); //(pixels or points) * PANGO_SCALE GList *member; for (member = app.tabslist; member != NULL; member = member->next) { GtkTextView *tvw = ((E2_OutputTabRuntime *)member->data)->text; gtk_text_view_set_wrap_mode (tvw, e2_option_int_get ("output-wrap-mode")); gtk_text_view_set_left_margin (tvw, e2_option_int_get ("output-left-margin")); gtk_text_view_set_right_margin (tvw, e2_option_int_get ("output-right-margin")); gtk_widget_modify_font (GTK_WIDGET (tvw), font_desc); GtkTextBuffer *buf = ((E2_OutputTabRuntime *)member->data)->buffer; GtkTextTagTable *table = gtk_text_buffer_get_tag_table (buf); GtkTextTag *tag = gtk_text_tag_table_lookup (table, "green"); g_object_set (G_OBJECT (tag), "foreground", e2_option_str_get ("color-positive"), NULL); tag = gtk_text_tag_table_lookup (table, "red"); g_object_set (G_OBJECT (tag), "foreground", e2_option_str_get ("color-negative"), NULL); tag = gtk_text_tag_table_lookup (table, "grey"); g_object_set (G_OBJECT (tag), "foreground", e2_option_str_get ("color-unimportant"), NULL); tag = gtk_text_tag_table_lookup (table, "small"); g_object_set (G_OBJECT (tag), "size", (gint) (PANGO_SCALE_SMALL * app.output.font_size), NULL); } pango_font_description_free (font_desc); } /** @brief create output pane Create initial output pane - with initial no. of panes as per cache @return notebook, one or more pages, each containing an output pane */ GtkWidget *e2_output_initialise (void) { //create notebook for tabs, no callback until all pages created app.outbook = gtk_notebook_new (); GtkNotebook *book = GTK_NOTEBOOK (app.outbook); gtk_notebook_set_show_tabs (book, (app.tabcount > 1)); gtk_notebook_popup_enable (book); GtkCornerType where = (GtkCornerType) e2_option_sel_get ("scrollbar-position"); #ifdef ADJACENT_TABS if (where == GTK_CORNER_TOP_LEFT || where == GTK_CORNER_BOTTOM_LEFT) gtk_notebook_set_tab_pos (book, GTK_POS_RIGHT); else gtk_notebook_set_tab_pos (book, GTK_POS_LEFT); #else if (where == GTK_CORNER_TOP_LEFT || where == GTK_CORNER_BOTTOM_LEFT) gtk_notebook_set_tab_pos (book, GTK_POS_LEFT); else gtk_notebook_set_tab_pos (book, GTK_POS_RIGHT); #endif gtk_notebook_set_scrollable (book, TRUE); gtk_notebook_set_show_border (book, FALSE); const gchar *fntname = e2_utils_get_output_font (); PangoFontDescription *font_desc = pango_font_description_from_string (fntname); app.output.font_size = pango_font_description_get_size (font_desc);//(pixels or points) * PANGO_SCALE pango_font_description_free (font_desc); if (app.output.font_size == 0) //in case of invalid font name string { //set 10-point default app.output.font_size = (pango_font_description_get_size_is_absolute (font_desc)) ? 10 * PANGO_SCALE * 96 / 72 : //assume screen is 96 DPI 10 * PANGO_SCALE; } printd (DEBUG, "stacked tab is at %x", &app.tab); //we're always going to have at least 1 tab, which gets the initial focus gint i; E2_OutputTabRuntime *tab = NULL; //assignment for complier-warning prevention only //iterate backward so that we end with the first (default) tab for (i = app.tabcount ; i > 0 ; i--) { tab = ALLOCATE0 (E2_OutputTabRuntime); //FIXME only deallocated by the user, not at session end CHECKALLOCATEDFATAL (tab); // printd (DEBUG, "created tab data at %x", tab); app.tabslist = g_list_prepend (app.tabslist, tab); GtkWidget *sw = _e2_output_create_view (tab); gchar *labltxt = g_strdup_printf ("%d", i); //tab labels are numbers GtkWidget *label = gtk_label_new (labltxt); gtk_notebook_prepend_page (book, sw, label); g_free (labltxt); #ifdef USE_GTK2_10 tab->labelnum = i; //save tab id for matching gtk_notebook_set_tab_reorderable (book, sw, TRUE); # ifdef E2_TABS_DETACH gtk_notebook_set_tab_detachable (book, sw, TRUE); # ifdef USE_GTK2_12DND gtk_notebook_set_group (book, app.outbook); //instance-specific pointer # else gtk_notebook_set_group_id (book, getpid()); # endif # endif #endif } //last-added one becomes current //its current-page is set when the window is shown app.tab = *tab; curr_tab = tab; #ifdef E2_TABS_DETACH //enable tab dragging to new windows # ifdef USE_GTK2_12DND g_signal_connect (G_OBJECT (app.outbook), "create-window", G_CALLBACK (_e2_output_tab_drop_new), NULL); //CHECKME user_data # else //this is the gtk 2.10 approach gtk_notebook_set_window_creation_hook ((GtkNotebookWindowCreationFunc) _e2_output_tab_drop_new, NULL, //CHECKME gpointer data NULL); //(GDestroyNotify) destroy # endif //enable processing of tabs being dragged back from a new window gtk_drag_dest_set (app.outbook, GTK_DEST_DEFAULT_DROP, target_table2, n_targets2, GDK_ACTION_MOVE); g_signal_connect (G_OBJECT (app.outbook), "drag-data-received", G_CALLBACK (_e2_output_tabdrag_data_received_cb), NULL); //CHECKME user_data //capture clicks on tab labels g_signal_connect (G_OBJECT (app.outbook), "grab-focus", G_CALLBACK (_e2_output_grab_focus_cb), NULL); // g_signal_connect (G_OBJECT (app.outbook), "focus-in-event", // G_CALLBACK (_e2_output_focus_in_cb), NULL); //for unblocking g_signal_connect (G_OBJECT (app.outbook), "page-removed", G_CALLBACK (_e2_output_tabgone_cb), app.main_window); #endif g_signal_connect (G_OBJECT (app.outbook), "switch-page", G_CALLBACK (_e2_output_tabchange_cb), NULL); //no data //setup data common to all tabs //repetitive, but option data structs may move after options hash recreation, //and possibly without re-running the options init function app.output.opt_show_on_new = e2_option_get ("show-output-window-on-output"); // app.output.opt_show_on_focus_in = e2_option_get ("command-line-show-output-on-focus-in"); UNUSED DIRECTLY app.output.opt_hide_on_focus_out = e2_option_get ("command-line-hide-output-on-focus-out"); app.output.opt_jump = e2_option_get ("output-jump-new"); app.output.opt_jump_follow = e2_option_get ("output-jump-new-following"); app.output.opt_jump_end = e2_option_get ("output-jump-new-end"); //setup mutex to protect threaded access to print-function static variables g_static_rec_mutex_init (&print_mutex); return app.outbook; } /** @brief register actions related to output pane @return */ void e2_output_actions_register (void) { E2_Action actions[] = { {g_strconcat(_A(10),".",_A(29),NULL),_e2_output_open_filetype,FALSE,E2_ACTION_TYPE_ITEM,0,NULL,NULL}, {g_strconcat(_A(10),".",_A(33),NULL),_e2_output_clear_action, FALSE,E2_ACTION_TYPE_ITEM,0,NULL,NULL}, {g_strconcat(_A(10),".",_A(71),NULL),_e2_output_print_action, TRUE, E2_ACTION_TYPE_ITEM,0,NULL,NULL}, {g_strconcat(_A(10),".",_A(54),NULL),_e2_output_help_action, TRUE, E2_ACTION_TYPE_ITEM,0,NULL,NULL}, {g_strconcat(_A(10),".",_A(77),NULL),_e2_output_scroll_step, TRUE, E2_ACTION_TYPE_ITEM,0,GINT_TO_POINTER(TRUE),NULL}, {g_strconcat(_A(10),".",_A(78),NULL),_e2_output_scroll_step, TRUE, E2_ACTION_TYPE_ITEM,0,GINT_TO_POINTER(FALSE),NULL}, {g_strconcat(_A(10),".",_A(68),NULL),_e2_output_scroll_page, TRUE, E2_ACTION_TYPE_ITEM,0,GINT_TO_POINTER(TRUE),NULL}, {g_strconcat(_A(10),".",_A(69),NULL),_e2_output_scroll_page, TRUE, E2_ACTION_TYPE_ITEM,0,GINT_TO_POINTER(FALSE),NULL}, {g_strconcat(_A(10),".",_A(52),NULL),_e2_output_scroll_all, TRUE, E2_ACTION_TYPE_ITEM,0,GINT_TO_POINTER(TRUE),NULL}, {g_strconcat(_A(10),".",_A(53),NULL),_e2_output_scroll_all, TRUE, E2_ACTION_TYPE_ITEM,0,GINT_TO_POINTER(FALSE),NULL}, {g_strconcat(_A(10),".",_A(30),NULL),_e2_output_tab_add, FALSE,E2_ACTION_TYPE_ITEM,0,NULL,NULL}, {g_strconcat(_A(10),".",_A(42),NULL),_e2_output_tab_remove, FALSE,E2_ACTION_TYPE_ITEM,0,NULL,NULL}, //FIXME data {g_strconcat(_A(10),".",_A(47),NULL),_e2_output_focus_action, TRUE, E2_ACTION_TYPE_ITEM,0,NULL,NULL}, //optional arg is string tab number }; guint i, count = sizeof (actions)/sizeof (E2_Action); for (i = 0; i < count; i++) e2_action_register (&actions[i]); } /** @brief register options related to output pane @return */ void e2_output_options_register (void) { gchar *group_name = g_strconcat(_C(6),".",_C(28),":",_C(26),NULL); //_("commands.output:miscellaneous" e2_option_bool_register ("show-output-window-on-output", group_name, _("show output pane when a new message appears"), _("This will ensure you don't miss any messages"), NULL, FALSE, E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP); e2_option_bool_register ("command-line-show-output-on-focus-in", group_name, _("show output pane if the command line is focused"), _("This causes the output pane to be opened when you are about enter a command"), NULL, FALSE, E2_OPTION_FLAG_ADVANCED); //? | E2_OPTION_FLAG_BUILDBARS); e2_option_bool_register ("command-line-hide-output-on-focus-out", group_name, _("hide output pane if the command line is unfocused"), _("This causes the output pane to be closed when you move focus away from the command line"), NULL, FALSE, E2_OPTION_FLAG_ADVANCED); //? | E2_OPTION_FLAG_BUILDBARS); e2_option_bool_register ("fileop-show", group_name, _("show commands"), _("This echoes the commands that are run and their exit value"), NULL, TRUE, E2_OPTION_FLAG_ADVANCED); e2_option_bool_register ("output-jump-new", group_name, _("scroll to new output"), _("This will automatically scroll the output pane content, to show new message(s)"), NULL, TRUE, E2_OPTION_FLAG_ADVANCED); e2_option_bool_register ("output-jump-new-following", group_name, _("only scroll when really following"), _("This stops automatic pane scrolling to new output if you've manually scrolled away"), "output-jump-new", TRUE, E2_OPTION_FLAG_ADVANCED); e2_option_bool_register ("output-jump-new-end", group_name, _("only scroll when new output is at the end"), _("This stops pane scrolling if a different process has displayed text after the current insert position"), "output-jump-new", FALSE, E2_OPTION_FLAG_ADVANCED); const gchar *opt_wrap_mode[] = {_("none"), _("everywhere"), _("words"), NULL}; group_name = g_strconcat(_C(6),".",_C(28),":",_C(39),NULL); //_("commands.output:style" e2_option_sel_register ("output-wrap-mode", group_name, _("line wrap mode"), _("If mode is 'none', a horizontal scrollbar will be available. Mode 'words' will only break the line between words"), NULL, 2, opt_wrap_mode, E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP | E2_OPTION_FLAG_BUILDALL); e2_option_int_register ("output-left-margin", group_name, _("left margin (pixels)"), _("This is the left margin between the output-pane edge and the text in it"), NULL, 6, 0, 1000, E2_OPTION_FLAG_ADVANCED); e2_option_int_register ("output-right-margin", group_name, _("right margin (pixels)"), _("This is the right margin between the output-pane edge and the text in it"), NULL, 2, 0, 1000, E2_OPTION_FLAG_ADVANCED); e2_option_bool_register ("custom-output-font", group_name, _("use custom font"), _("If activated, the font specified below will be used, instead of the theme default"), NULL, FALSE, E2_OPTION_FLAG_BASIC | E2_OPTION_FLAG_BUILDOUT); e2_option_font_register ("output-font", group_name, _("custom output font"), _("This is the font used for text in the output pane"), "custom-output-font", "Sans 10", //_I( font name E2_OPTION_FLAG_BASIC | E2_OPTION_FLAG_BUILDOUT); // group_name = g_strconcat(_C(33),".",_C(2),":",_C(28),NULL); //_("panes.other colors:output" group_name = g_strconcat(_C(6),".",_C(28),":",_C(2),NULL); //_("commands.output:colors" //CHECKME which of these output colors really do need the whole window to be reconstructed ? e2_option_color_register ("color-positive", group_name, _("positive color"), _("This color is used for messages about successful operations or other 'positive events'"), NULL, "dark green", E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP | E2_OPTION_FLAG_BUILDOUT); e2_option_color_register ("color-negative", group_name, _("negative color"), _("This color is used for messages about unsuccessful operations or other 'negative events'"), NULL, "red", E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_BUILDOUT); e2_option_color_register ("color-unimportant", group_name, _("unimportant color"), _("This color is used for messages of minor importance or other miscellaneous events"), NULL, "light grey", E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_BUILDOUT); }