/* $Id$ Portions copyright (C) 2004-2010 tooar . 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_fileview.c @brief directory content view functions This file contains functions related to creation of treeviews to display directory content, plus related selections and sorting. */ /** \page treeviews filelist treeviews ToDo - description of how to work with the filelist treeviews \section cell_rend cell-renderers ToDo */ #include "emelfm2.h" //#include #include #include #include #include "e2_dnd.h" #include "e2_filelist.h" #include "e2_context_menu.h" #include "e2_option.h" //#include "e2_tree_dialog.h" #include "e2_dialog.h" #include "e2_task.h" #ifdef E2_MOUSECUSTOM # include "e2_mousebinding.h" #endif //allow in-place renaming of items TOO ANNOYING AND CLUMSY //(initiated by left-click on filename when line is not selected) //#define EDIT_INPLACE //do incremental filelist refreshes //#define E2_NEWREFRESH //tweak some BGL-related stuff //#define SYNC_DEBUG //#ifdef E2_NEWREFRESH typedef enum { REFRESH_ADD = 0, REFRESH_DELETE, //unused REFRESH_CHANGE, REFRESH_KEEP, } E2_RefreshMode; //one of these for each entry in a new list of FileInfo's created to check //against current FileInfo's in a view typedef struct _E2_RefreshInfo { guint newindx; //index of row in changes liststore guint oldindx; //index of row in current liststore (ex. when adding) E2_RefreshMode oldmode; //what to do with that row } E2_RefreshInfo; //#endif extern GtkTargetEntry target_table[]; extern guint n_targets; //this holds, in fixed column order (used in model & config, cache data), // the displayed column widths, a row for each pane gint col_width_store[2][MAX_COLUMNS]; //this holds, in fixed column order (used in model & config, cache data), // the displayed column order, a row for each pane gint stored_col_order[2][MAX_COLUMNS]; //this holds column data from the above arrays, for cache reading/writing //GList *cols_data; //this holds, in displayed column order // the fixed column order, a row for each pane gint displayed_col_order[2][MAX_COLUMNS]; // Colors //GdkColor LIST_COLOR; #ifdef E2_SELTXT_RECOLOR GdkColor selectedtext; #endif gboolean btn2_released; //flag used to decide if middle-btn DnD menu is wanted #ifndef E2_MOUSECUSTOM //local copy of flag static gboolean button2updir; #endif //number of last mouse button press, for distinguishing drags static gboolean pane1_all = FALSE, pane2_all=FALSE; //these are related to column dragging static gint header_button; static gboolean block; #ifdef E2_ALTLEFTMOUSE /*this provides a quick check whether the left btn has been pressed set and cleared in button-press and -release callbacks, respectively*/ gboolean left_pressed; /*flag for whether selection-by-dragging is underway set TRUE in drag-begin cb, cleared in drag-data-get cb*/ gboolean drag_sel_now; //max msec between clicks when checking for a doubleclick //extern guint click_interval; #endif static gpointer _e2_fileview_change_dir (E2_Listman *cddata); //functions for sorting of treeviews static gint _e2_fileview_name_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); static gint _e2_fileview_size_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); static gint _e2_fileview_perm_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); static gint _e2_fileview_user_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); static gint _e2_fileview_group_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); static gint _e2_fileview_mdate_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); static gint _e2_fileview_adate_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); static gint _e2_fileview_cdate_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction); E2_Column e2_all_columns[MAX_COLUMNS] = { {N_("Filename"), 120, _e2_fileview_name_sort}, {N_("Size"), 80, _e2_fileview_size_sort}, {N_("Permissions"), 80, _e2_fileview_perm_sort}, {N_("Owner"), 60, _e2_fileview_user_sort}, {N_("Group"), 60, _e2_fileview_group_sort}, {N_("Modified"), 120, _e2_fileview_mdate_sort}, {N_("Accessed"), 120, _e2_fileview_adate_sort}, {N_("Changed"), 120, _e2_fileview_cdate_sort} }; /** @brief item-name sort-order comparison function This compares 'sort-key' row data, which is UTF-8 compatible If the case-insensitive sorting option was in force when the data was loaded into the model, the keys are 'case-folded' i.e. this is not so dynamic as we might want Directories (and links to dirs) are placed before other things. NB utf8 collate keys for ascii text are case insensitive !! @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_name_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { gchar *keya, *keyb; FileInfo *infoa, *infob; gint result; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, NAMEKEY, &keya, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) result = (order) ? -1 : 1; else { gtk_tree_model_get (model, b, FINFO, &infob, NAMEKEY, &keyb, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) result = (order) ? 1 : -1; else if (da == db) //both are dirs or non-dirs result = strcmp (keya, keyb); else result = (da == order) ? -1 : 1; g_free (keyb); } g_free (keya); return result; } /** @brief item-extension sort-order comparison function This compares 'sort-key' row data, which is UTF-8 compatible Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ //FIXME = support case-insensitive sorting gint e2_fileview_ext_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { gchar *namea, *nameb, *keya, *keyb; FileInfo *infoa, *infob; gboolean da, db, order; gint result; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FILENAME, &namea, FINFO, &infoa, NAMEKEY, &keya, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) result = (order) ? -1 : 1; else { gtk_tree_model_get (model, b, FILENAME, &nameb, FINFO, &infob, NAMEKEY, &keyb, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infoa->filename, "..")) result = (order) ? 1 : -1; else if (da) { if (db) result = strcmp (keya, keyb); else result = (order) ? -1 : 1; } else if (db) result = (order) ? 1 : -1; else { gchar *ea, *eb; ea = namea; while (*ea == '.') //always ascii '.', don't need g_utf8_strrchr() ea++; eb = nameb; while (*eb == '.') //always ascii '.', don't need g_utf8_strrchr() eb++; if ((ea = strrchr (ea, '.')) == NULL) { // row a has NO extension.. check row b if ((strrchr (eb, '.')) != NULL) result = -1; else result = strcmp (keya, keyb); } else // row a HAS an extension.. check row b if ((eb = strrchr (eb, '.')) == NULL) result = 1; else // both have extensions { result = g_utf8_collate (ea, eb); //if they are the same, sort by name if (result == 0) result = strcmp (keya, keyb); } } g_free (nameb); g_free (keyb); } g_free (namea); g_free (keya); return result; } /** @brief item-size sort-order comparison function Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_size_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { FileInfo *infoa, *infob; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) return (order) ? -1 : 1; gtk_tree_model_get (model, b, FINFO, &infob, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) return (order) ? 1 : -1; if (da == db) //both are dirs or non-dirs { //long-handed approach avoids 64-bit overflows if (infoa->statbuf.st_size > infob->statbuf.st_size) return 1; else if (infoa->statbuf.st_size < infob->statbuf.st_size) return -1; else return strcmp (infoa->filename, infob->filename); } else return (da == order) ? -1 : 1; } /** @brief item-permission sort-order comparison function Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_perm_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { FileInfo *infoa, *infob; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) return (order) ? -1 : 1; gtk_tree_model_get (model, b, FINFO, &infob, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) return (order) ? 1 : -1; if (da == db) //both are dirs or non-dirs { //long-handed approach avoids 64-bit overflows if (infoa->statbuf.st_mode > infob->statbuf.st_mode) return 1; else if (infoa->statbuf.st_mode < infob->statbuf.st_mode) return -1; else return strcmp (infoa->filename, infob->filename); } else return (da == order) ? -1 : 1; } /** @brief item-owner sort-order comparison function Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_user_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { FileInfo *infoa, *infob; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) return (order) ? -1 : 1; gtk_tree_model_get (model, b, FINFO, &infob, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) return (order) ? 1 : -1; if (da == db) //both are dirs or non-dirs { //long-handed approach avoids 64-bit overflows if (infoa->statbuf.st_uid > infob->statbuf.st_uid) return 1; else if (infoa->statbuf.st_uid < infob->statbuf.st_uid) return -1; else return strcmp (infoa->filename, infob->filename); } else return (da == order) ? -1 : 1; } /** @brief item-group sort-order comparison function Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_group_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { FileInfo *infoa, *infob; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) return (order) ? -1 : 1; gtk_tree_model_get (model, b, FINFO, &infob, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) return (order) ? 1 : -1; if (da == db) //both are dirs or non-dirs { //long-handed approach avoids 64-bit overflows if (infoa->statbuf.st_gid > infob->statbuf.st_gid) return 1; else if (infoa->statbuf.st_gid < infob->statbuf.st_gid) return -1; else return strcmp (infoa->filename, infob->filename); } else return (da == order) ? -1 : 1; } /** @brief item-acess-date sort-order comparison function Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_adate_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { FileInfo *infoa, *infob; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) return (order) ? -1 : 1; gtk_tree_model_get (model, b, FINFO, &infob, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) return (order) ? 1 : -1; if (da == db) //both are dirs or non-dirs { //long-handed approach avoids 64-bit overflows if (infoa->statbuf.st_atime > infob->statbuf.st_atime) return 1; else if (infoa->statbuf.st_atime < infob->statbuf.st_atime) return -1; else return strcmp (infoa->filename, infob->filename); } else return (da == order) ? -1 : 1; } /** @brief item-modification-date sort-order comparison function Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_mdate_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { FileInfo *infoa, *infob; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) return (order) ? -1 : 1; gtk_tree_model_get (model, b, FINFO, &infob, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) return (order) ? 1 : -1; if (da == db) //both are dirs or non-dirs { //long-handed approach avoids 64-bit overflows if (infoa->statbuf.st_mtime > infob->statbuf.st_mtime) return 1; else if (infoa->statbuf.st_mtime < infob->statbuf.st_mtime) return -1; else return strcmp (infoa->filename, infob->filename); } else return (da == order) ? -1 : 1; } /** @brief item-status-change-date sort-order comparison function Directories are placed before other things. @param model the data model to be interrogated @param a pointer to model iter for the first row to be compared @param b pointer to model iter for the second row to be compared @param direction pointer to the views's sort direction @return integer <0, 0 or >0 according to whether a belongs before, = or after b */ static gint _e2_fileview_cdate_sort (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, GtkSortType *direction) { FileInfo *infoa, *infob; gboolean da, db, order; order = (*direction == GTK_SORT_ASCENDING); //TRUE for ascending, FALSE for descending gtk_tree_model_get (model, a, FINFO, &infoa, -1); da = ISDIR (infoa->statbuf.st_mode); if (da && strcmp (infoa->filename, "..") == 0) return (order) ? -1 : 1; gtk_tree_model_get (model, b, FINFO, &infob, -1); db = ISDIR (infob->statbuf.st_mode); if (db && strcmp (infob->filename, "..") == 0) return (order) ? 1 : -1; if (da == db) //both are dirs or non-dirs { //long-handed approach avoids 64-bit overflows if (infoa->statbuf.st_ctime > infob->statbuf.st_ctime) return 1; else if (infoa->statbuf.st_ctime < infob->statbuf.st_ctime) return -1; else return strcmp (infoa->filename, infob->filename); } else return (da == order) ? -1 : 1; } /** @brief sort treeview for @a view by column @a colnum This supports sorting by keypress. Expects BGL on/closed. @param colnum column-enumerator, FILENAME...CHANGED, or fake column EXTENSION @param view data structure for the pane @return */ gboolean e2_fileview_sort_column (gint colnum, ViewInfo *view) { if ((colnum < FILENAME || colnum > CHANGED) && colnum != EXTENSION) return FALSE; gint realcol = (colnum == EXTENSION) ? FILENAME : colnum; GList *cols = gtk_tree_view_get_columns (GTK_TREE_VIEW (view->treeview)); GtkTreeViewColumn *col = g_list_nth_data (cols, realcol); g_list_free (cols); gboolean visible; g_object_get (G_OBJECT (col), "visible", &visible, NULL); if (!visible) return FALSE; gint old_sortcol; GtkSortType old_order; // gboolean toggle = TRUE; GtkTreeSortable *sortable = GTK_TREE_SORTABLE (view->store); gtk_tree_sortable_get_sort_column_id (sortable, &old_sortcol, &old_order); if (colnum == FILENAME) { if (view->extsort) { //currently extension-sorted, do normal name-sorting now //counter the effects of the normal toggle // if (old_sortcol == colnum) // toggle = FALSE; gtk_tree_sortable_set_sort_column_id (sortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); gtk_tree_sortable_set_sort_func (sortable, FILENAME, e2_all_columns[FILENAME].sort_func, &view->sort_order, NULL); } } else if (colnum == EXTENSION) { //do extension-sorting now if (!view->extsort) { //not already that way //counter the effects of the normal toggle // if (old_sortcol == FILENAME) // toggle = FALSE; gtk_tree_sortable_set_sort_column_id (sortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); gtk_tree_sortable_set_sort_func (sortable, FILENAME, (GtkTreeIterCompareFunc) e2_fileview_ext_sort, &view->sort_order, NULL); } } if (old_sortcol == realcol) { //column is already sorted // if (toggle) view->sort_order = (old_order == GTK_SORT_ASCENDING) ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING; } else { //this is a new sort column //replace sort indicator gtk_widget_hide (view->sort_arrows[old_sortcol]); gtk_widget_show (view->sort_arrows[realcol]); //1st click makes it ascending view->sort_order = GTK_SORT_ASCENDING; // order = TRUE; } //set appropriate arrow type GtkArrowType arrow; if (colnum == EXTENSION) //FIXME do a more-intuitive arrow arrow = (view->sort_order == GTK_SORT_ASCENDING) ? GTK_ARROW_RIGHT : GTK_ARROW_LEFT; else arrow = (view->sort_order == GTK_SORT_ASCENDING) ? GTK_ARROW_DOWN : GTK_ARROW_UP; gtk_arrow_set (GTK_ARROW (view->sort_arrows[realcol]), arrow, //different shadow types don't do any good, really ... // (view->extsort) ? GTK_SHADOW_ETCHED_IN : GTK_SHADOW_OUT); GTK_SHADOW_NONE); //remember the column, for cacheing view->sort_column = realcol; //do the sort gtk_tree_sortable_set_sort_column_id (sortable, realcol, view->sort_order); return TRUE; } /*****************/ /*** callbacks ***/ /*****************/ /** @brief cursor change signal callback This records the row to which the cursor is moved @param treeview the widget where the cursor moved @param view data structure for the pane @return FALSE, so related signals are not blocked */ static gboolean _e2_fileview_cursor_change_cb (GtkTreeView *treeview, ViewInfo *view) { printd (DEBUG, "callback: cursor changed"); GtkTreePath *path; gtk_tree_view_get_cursor (treeview, &path, NULL); if (path != NULL) //path can be invalid when last row of a filelist is deleted { view->row = *gtk_tree_path_get_indices (path); gtk_tree_path_free (path); } printd (DEBUG, "focus row is %d", view->row); return FALSE; } /** @brief column-header button-press signal callback This func records which button was pressed, so that when a "release" callback occurs (which may be when a drag starts, not an actual release) we can check whether it is still pressed. This is a workaround for gtk's (2.4 at least) behaviour It relies on detecting events on the undocumented column->button widget @param header the column button header widget where the press occurred @param event pointer to event data struct @param view data structure for the view to which the file list belongs @return FALSE, so related signals are not blocked */ static gboolean _e2_fileview_column_header_buttonpress_cb (GtkWidget *header, GdkEventButton *event, ViewInfo *view) { printd (DEBUG, "callback: column header button press"); e2_utils_generic_press_cb (header, event, view); header_button = event->button; return FALSE; } /** @brief column-header button-release signal callback This checks whether a release event is real. In gtk 2.4 at least, such an event may be when a column-drag starts, not an actual release. @param UNUSED header the column button widget where the release occurred @param event pointed to event data struct @param view data structure for the view to which the file list belongs @return TRUE, if this is a 'bogus' release, else FALSE */ static gboolean _e2_fileview_column_header_buttonrel_cb (GtkWidget *header, GdkEventButton *event, ViewInfo *view) { printd (DEBUG, "callback: column header button release"); if (!e2_utils_check_release (event)) { printd (DEBUG, "IGNORED: missing button-press event"); return FALSE; //no corresponding press event } if (view != curr_view) e2_pane_activate_other (); //check for whether button is really still pressed //gtk 2.4 emits a bogus extra release signal when column-drag occurs gboolean pressed = FALSE; if (!block && event->send_event) { pressed = TRUE; block = TRUE; } else if (block) block = FALSE; //on the second pass, just unblock // else // pressed = FALSE; if (!pressed) header_button = -1; //signal to "clicked" callback not to do anything return pressed; //stop further action on this button if not released yet } /** @brief column-click callback This func updates the view sorting, by applying an ascending sort to a column that is not sorted, or by toggling the sort direction of a column that is already sorted. Sort indicators are updated. This is called when a column header is clicked, before any associated "column-change" signal. @param col the view column which was clicked @param rt data structure for the pane to which the file list belongs @return */ //FIXME -click is not always passed through to here static void _e2_fileview_column_header_clicked_cb (GtkTreeViewColumn *col, ViewInfo *view) { printd (DEBUG, "callback: column header click"); if (header_button != -1) return; //the button is still down, this is drag action //get which column was cliicked GList *cols = gtk_tree_view_get_columns (GTK_TREE_VIEW (view->treeview)); gint clicked_col = g_list_index (cols, (gpointer) col); g_list_free (cols); //get colum-order array for the pane gint *order_array = (view == &app.pane1.view) ? displayed_col_order[0] : displayed_col_order[1]; //get clicked-model-column number gint i = order_array[clicked_col]; /* if (i == 0) { //filename column DO WE WANT TO MAKE THIS DYNAMIC BUT SLOWER ?? //get a local copy of the case-sensitive flag E2_OptionSet *set = e2_option_get_simple ("namesort-case-sensitive"); case_sensitive = _e2_option_bool_get (set); } */ //get current-sorted-column details gint m; GtkSortType old_order; GtkTreeSortable *sortable = GTK_TREE_SORTABLE (view->store); gtk_tree_sortable_get_sort_column_id (sortable, &m, &old_order); gboolean toggle = TRUE; gboolean extsort = FALSE; if (i == FILENAME) { GdkModifierType mask = e2_utils_get_modifiers (); if (mask & GDK_CONTROL_MASK) { //do extension-sorting extsort = TRUE; if (!view->extsort) { //counter the effects of the normal toggle if (m == i) toggle = FALSE; view->extsort = TRUE; gtk_tree_sortable_set_sort_column_id (sortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); gtk_tree_sortable_set_sort_func (sortable, FILENAME, (GtkTreeIterCompareFunc) e2_fileview_ext_sort, &view->sort_order, NULL); } } else { //do normal name-sorting if (view->extsort) { //counter the effects of the normal toggle if (m == i) toggle = FALSE; view->extsort = FALSE; gtk_tree_sortable_set_sort_column_id (sortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); gtk_tree_sortable_set_sort_func (sortable, FILENAME, e2_all_columns[FILENAME].sort_func, &view->sort_order, NULL); } } } if (m == i) { //column is already sorted if (toggle) view->sort_order = (old_order == GTK_SORT_ASCENDING) ? GTK_SORT_DESCENDING : GTK_SORT_ASCENDING; } else { //this is a new sort column //replace sort indicator gtk_widget_hide (view->sort_arrows[m]); gtk_widget_show (view->sort_arrows[i]); //1st click makes it ascending view->sort_order = GTK_SORT_ASCENDING; // order = TRUE; } //set appropriate arrow type GtkArrowType arrow; if (extsort) //FIXME do a more-intuitive arrow arrow = (view->sort_order == GTK_SORT_ASCENDING) ? GTK_ARROW_RIGHT : GTK_ARROW_LEFT; else arrow = (view->sort_order == GTK_SORT_ASCENDING) ? GTK_ARROW_DOWN : GTK_ARROW_UP; gtk_arrow_set (GTK_ARROW (view->sort_arrows[i]), arrow, //different shadow types don't do any good, really ... // (view->extsort) ? GTK_SHADOW_ETCHED_IN : GTK_SHADOW_OUT); GTK_SHADOW_NONE); /* no use, gtk always returns gchar data to heap space //setup stack variable space for speedier name sorts if (i == FILENAME) { gchar namespace [2][E2_UTFNAME_MAX]; gtk_tree_sortable_set_sort_func (sortable, FILENAME, e2_all_columns[FILENAME].sort_func, (gpointer *) namespace, NULL); } */ //remember the column, for cacheing view->sort_column = i; //do the sort gtk_tree_sortable_set_sort_column_id (sortable, i, view->sort_order); gtk_widget_grab_focus (view->treeview); return; } /** @brief column-change callback This func refreshes the columns-order array for the pane, by iterating over pane columns glist, grabbing each title like "4", converting it to corresponding integer. It is called when a column is dropped to a different position and when the window is closed (also, multiple times when the window is re-created - CHECKME why) @param treeview the widget where the action occurred @param view data structure for the view @return */ static void _e2_fileview_col_change_cb (GtkTreeView *treeview, ViewInfo *view) { printd (DEBUG, "callback: column change"); gint *order_array = (view == &app.pane1.view) ? displayed_col_order[0] : displayed_col_order[1]; GList *cols = gtk_tree_view_get_columns (treeview); GList *tmp; const gchar *title; gint colnum = 0; for (tmp = cols; tmp != NULL; tmp = tmp->next) { title = gtk_tree_view_column_get_title (tmp->data); order_array[colnum++] = atoi (title); } g_list_free (cols); //update order of "slave" pane if appropriate gint *slave_array; ViewInfo *slave_view; if (e2_option_bool_get ("pane2-uses-other") && view == &app.pane1.view) { slave_array = displayed_col_order[1]; slave_view = &app.pane2.view; } else if (e2_option_bool_get ("pane1-uses-other") && view == &app.pane2.view) { slave_array = displayed_col_order[0]; slave_view = &app.pane1.view; } else return; printd (DEBUG, "slave-view column change"); g_signal_handlers_block_by_func (G_OBJECT (slave_view->treeview), _e2_fileview_col_change_cb, slave_view); cols = gtk_tree_view_get_columns (GTK_TREE_VIEW (slave_view->treeview)); for (colnum = 0; colnum < MAX_COLUMNS; colnum++) { gint current_col = order_array[colnum]; if (slave_array[colnum] != current_col) { gint j; GtkTreeViewColumn *prior_col, *moved_col; if (colnum == 0) prior_col = NULL; else prior_col = g_list_nth_data (cols, colnum-1); for (j = colnum+1; j < MAX_COLUMNS; j++) { if (slave_array[j] == current_col) { moved_col = g_list_nth_data (cols, j); gtk_tree_view_move_column_after ( GTK_TREE_VIEW (slave_view->treeview), moved_col, prior_col); //this approach seems too complex, but several simpler tries failed !! g_list_free (cols); cols = gtk_tree_view_get_columns (GTK_TREE_VIEW (slave_view->treeview)); j = colnum; for (tmp = g_list_nth (cols, j); tmp != NULL; tmp=tmp->next) { const gchar *title = gtk_tree_view_column_get_title (tmp->data); slave_array[j] = atoi(title); j++; } break; } } } } g_list_free (cols); g_signal_handlers_unblock_by_func (G_OBJECT (slave_view->treeview), _e2_fileview_col_change_cb, slave_view); } /** @brief row-activated callback Activation is triggered when is pressed or when a double-click happens This "executes" the clicked row item view->row will have been set by the button-release callback @param treeview the widget where the button was pressed @param path model path to the clicked row @param col UNUSED clicked view column @param view UNUSED rt data for the view to be worked on @return */ static void _e2_fileview_row_activated_cb ( GtkTreeView *treeview, GtkTreePath *tpath, GtkTreeViewColumn *col, ViewInfo *view) { printd (DEBUG, "callback: _e2_fileview_row_activated"); GtkTreeIter iter; GtkTreeModel *model = gtk_tree_view_get_model (treeview); if (gtk_tree_model_get_iter (model, &iter, tpath)) { gchar *localpath; FileInfo *info; gtk_tree_model_get (model, &iter, FINFO, &info, -1); //this may append "..", which will be parsed in the cd process localpath = e2_utils_dircat (view, info->filename, TRUE); #ifdef E2_VFS VPATH sdata = { localpath, view->spacedata }; e2_task_backend_open (&sdata, TRUE); #else e2_task_backend_open (localpath, TRUE); #endif g_free (localpath); } } /** @brief mouse button press callback This makes the clicked view active, if it wasn't already. A button-1 press when there is nothing else selected logs the clicked model row, if any, in case it's a drag start. (For anything other than a drag, the row is logged whem the cursor moves.) The gtk handler will select the row. Double-clicks are detected (based on cb sequence) here, to work around gtk's behaviour when its handler is aborted after a single click A button-2 press does an updir, if that option is in force, or else logs the clicked model row, if any. A button-3 press sets up the relevant context menu, and may select the line or unselect everything, depending on options in force @param treeview the widget where the button was pressed @param event gdk event data @param view rt data for the view to be worked on @return TRUE (stop other handlers) for btn 2 (updir) or btn 3 press, else FALSE (allow other handlers) */ static gboolean _e2_fileview_button_press_cb (GtkWidget *treeview, GdkEventButton *event, ViewInfo *view) { printd (DEBUG, "callback: mouse button %d press event type %d", event->button, event->type); //check that we're in the actual treeview (not its header) by making //sure that the window is correct GdkWindow *window = gtk_tree_view_get_bin_window (GTK_TREE_VIEW (treeview)); if (window != event->window) return FALSE; e2_utils_generic_press_cb (treeview, event, view); //log data for release-checks //make the clicked pane active, if it wasn't already if (view != curr_view) // && !panechange_started) e2_pane_activate_other (); GtkTreePath *path; GtkTreeSelection *sel; #ifdef E2_ALTLEFTMOUSE // gboolean oldleft = left_pressed; //set flag to assist processing any drag left_pressed = (event->button == 1 //#ifdef E2_MOUSECUSTOM //FIXME && (event->state & ?) == 0 //#endif // && event->type == GDK_BUTTON_PRESS ); if (left_pressed //we don't handle any 'modified' button-press (so that range-selection works as normal) //or a click within the specified double-click interval from the prior click && (event->state & E2_MODIFIER_MASK) == 0 ) { //event-time for detecting double-clicks /* because we block returns for selected items, double-clicks have to be detected and processed here, too instead of click-interval, test now relies on the normal cb sequence for doubles (pr, rl, pr, pr, rl) i.e. a press without an intervening release is a double (or triple!) static guint32 last_event_time = 0; guint32 interval = event->time - last_event_time; last_event_time = event->time; if (interval >= click_interval) //time-based check for double-click (the 3rd pr will pass this test too) { */ /*if we've passed all the tests, and a selected item is clicked, we block gtk's deselection of other items until the button is released. Then, we'll know how to treat it*/ if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview), event->x, event->y, &path, NULL, NULL, NULL)) { // sel = view->selection; if (gtk_tree_selection_path_is_selected (view->selection, path)) { //sometimes the focus doesn't get transferred properly by gtk, so ... gtk_widget_grab_focus (view->treeview); // if (oldleft) //this is a fake press, generated as part of a double-click if (event->type == GDK_2BUTTON_PRESS) { printd (DEBUG, "intercepted mouse double-click"); //process it as a double-click _e2_fileview_row_activated_cb (GTK_TREE_VIEW (treeview), path, NULL, view); } gtk_tree_path_free (path); //note that returning TRUE blocks some normal treeview behaviour //like refocussing a selected item and repeated double-clicks //and allowing in-place editing /*annoying, interferes with double-clicks #ifdef EDIT_INPLACE return (event->type != GDK_BUTTON_PRESS && gtk_tree_selection_count_selected_rows (view->selection) == 1); #else */ return TRUE; //wait until release, to see what to do with the selected row //#endif } gtk_tree_path_free (path); } // } } else if (left_pressed && event->type != GDK_BUTTON_PRESS) return TRUE; //kill left multi-clicks with mod key(s) else #endif /* if (event->button == 1 && (event->type == GDK_2BUTTON_PRESS || event->type == GDK_3BUTTON_PRESS)) ) { //double or triple left-click } else */ if (event->button == 2) return TRUE; //wait until release, to see whether to select the row else if (event->button == 3) { if ( #ifdef E2_MOUSECUSTOM (event->state & E2_MODIFIER_MASK) == 0 && #endif e2_option_bool_get ("windows-right-click") // && (view->tagged == NULL) ) { //if windows right click is enabled, we want to also select the item, //if not already done, and clear any other selected item(s) unless this //one is selected already sel = view->selection; if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview), event->x, event->y, &path, NULL, NULL, NULL)) { if (!gtk_tree_selection_path_is_selected (sel, path)) gtk_tree_selection_unselect_all (sel); gtk_tree_selection_select_path (sel, path); //do this now because menu created before cursor-move cb view->row = *gtk_tree_path_get_indices (path); gtk_tree_path_free (path); } else if (! e2_option_bool_get ("windows-right-click-extra")) gtk_tree_selection_unselect_all (sel); } //context menu gint type; guint modifiers = gtk_accelerator_get_default_mod_mask (); modifiers &= event->state; switch (modifiers) { case 0: type = 0; break; case GDK_SHIFT_MASK: type = 1; break; case GDK_CONTROL_MASK: type = 2; break; // is used for gestures case (GDK_SHIFT_MASK | GDK_CONTROL_MASK): type = 3; break; default: type = -1; break; } if (type >= 0) e2_context_menu_show (event->button, event->time, type); return TRUE; } return FALSE; } /** @brief mouse button release callback This checks if the click was a left click. If so, and it's not the end of a drag, or an 'un-select' click, the clicked row is logged. Detection of the end of a drag relies on an undocumented check whether the release happened in the same pane as was clicked. @param treeview UNUSED where the button was _pressed_ @param event @param view rt data for the view where the button was _pressed_ @return TRUE (stop other handlers) if updir initiated, else FALSE (allow other handlers) */ static gboolean _e2_fileview_button_release_cb (GtkWidget *treeview, GdkEventButton *event, ViewInfo *view) { //check that we're in the actual treeview (not its header) //(this test will also fail at the end of a drag to the other pane) GdkWindow *window = gtk_tree_view_get_bin_window (GTK_TREE_VIEW (treeview)); if (window != event->window) { printd (DEBUG, "callback: fileview button release after column-header-separator click or drag"); return FALSE; } else printd (DEBUG, "callback: fileview button release"); gint drag = e2_utils_check_drag (event); if (drag == 0) { printd (DEBUG, "IGNORED: missing button-press event"); return FALSE; //no corresponding press event } //for releases, there is no distinction between 1- 2- or 3- buttons #ifdef E2_ALTLEFTMOUSE if (event->button == 1 //#ifdef E2_MOUSECUSTOM //FIXME && (event->state & E2_MODIFIER_MASK) == 0 //#endif ) { left_pressed = FALSE; //we ignore releases when a modifier is pressed, //and normal-drag ends, // i.e. event->send_event seems to be FALSE (ie the event was not sent explicitly) //and drag-select ends if ((event->state & E2_MODIFIER_MASK) == 0 && drag == 1 //event->send_event == 0 && !drag_sel_now) { /*there was no drag-selection, or the drag did not cover anything but the clicked item we want an outcome same as from a normal button-press i.e. select just the clicked row and log it, if any */ GtkTreePath *path; if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (view->treeview), event->x, event->y, &path, NULL, NULL, NULL)) { //we're releasing over a treeview-item (which must have been selected) GtkTreeSelection *sel = view->selection; if (gtk_tree_selection_count_selected_rows (sel) > 1) { gtk_tree_selection_unselect_all (sel); gtk_tree_selection_select_path (sel, path); //record the row as the last-selected one // view->row = *gtk_tree_path_get_indices (path);NOW IN CURSOR MOVE CB } gtk_tree_path_free (path); } } } else #endif if (event->button == 2 #ifdef E2_MOUSECUSTOM && (event->state & E2_MODIFIER_MASK) == 0 #endif ) { // printd (DEBUG, "callback: mouse button-2 release"); //correctly distinguish drag releases, and normal clicks //event->send_event seems to be FALSE (ie the event was not sent explicitly) //for releases in the same pane as clicked?? if (drag == 1) //event->send_event == 0) { //not a drag release #ifndef E2_MOUSECUSTOM if (button2updir) { printd (DEBUG, "up"); E2_PaneRuntime *rt = (view == curr_view) ? curr_pane : other_pane; e2_pane_change_dir (rt, ".."); return TRUE; } #endif //otherwise, maybe log the row //don't bother with modifers-check in this context // guint modifiers = gtk_accelerator_get_default_mod_mask (); // if ((event->state & modifiers) != GDK_CONTROL_MASK) if ((event->state & GDK_CONTROL_MASK) == 0) { //not an "unselect" click GtkTreePath *path; if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (view->treeview), event->x, event->y, &path, NULL, NULL, NULL)) { // row was NOT selected when the mouse was clicked // GtkTreeSelection *sel = view->selection; gtk_tree_selection_select_path (view->selection, path); //record the row as the last-selected one // view->row = *gtk_tree_path_get_indices (path);NOW IN CURSOR MOVE CB gtk_tree_path_free (path); } } } else //set signal for DnD handler btn2_released = TRUE; } else btn2_released = FALSE; return FALSE; } /** @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 UNUSED 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 treeview in focus when the menu key was pressed @return */ void e2_fileview_set_menu_position (GtkMenu *menu, gint *x, gint *y, gboolean *push_in, GtkWidget *treeview) { gint left, top; gtk_window_get_position (GTK_WINDOW (app.main_window), &left, &top); GtkAllocation alloc; #ifdef USE_GTK2_18 gtk_widget_get_allocation (treeview, &alloc); #else alloc = treeview->allocation; #endif *x = left + alloc.x + alloc.width/2; *y = top + alloc.y +alloc.height/2 - 30; *push_in = FALSE; } /** @brief menu button callback (does not apply for 'modified' menu button) Sets up the relevant context menu @param treeview UNUSED the widget in focus when the action was initiated @param view UNUSED rt data for the view to be worked on @return TRUE always */ /*static gboolean _e2_fileview_popup_cb (GtkWidget *treeview, ViewInfo *view) { gint menu_type = 0; //default to 'normal' context menu //FIXME this check irrelevant, as cb never used for menu btn + Ctrl/Shift / * GdkModifierType state; if (gtk_get_current_event_state (&state)) { if ((state & GDK_SHIFT_MASK) && (state & GDK_CONTROL_MASK)) menu_type = 3; else if (state & GDK_SHIFT_MASK) menu_type = 1; else if (state & GDK_CONTROL_MASK) menu_type = 2; } * / gint event_time = gtk_get_current_event_time (); //button '0' signals this is not a mouse-button click e2_context_menu_show (0, event_time, menu_type); return TRUE; } */ /** @brief treeview key-press callback @param widget UNUSED the focused treeview widget when the key was pressed @param event pointer to event data struct @param view rt data for the view to be worked on @return TRUE when we handle a menu-key */ static gboolean _e2_fileview_key_press_cb (GtkWidget *widget, GdkEventKey *event, ViewInfo *view) { printd (DEBUG, "_e2_fileview_key_press_cb, key %u", event->keyval); if (event->keyval == GDK_Menu) { gint menu_type; switch (event->state & gtk_accelerator_get_default_mod_mask ()) { default: menu_type = 0; //'normal' context menu break; case GDK_SHIFT_MASK: menu_type = 1; break; case GDK_CONTROL_MASK: menu_type = 2; break; // is not a valid modifier for mouse right-click, so we also ignore //that for the menu-button case (GDK_SHIFT_MASK | GDK_CONTROL_MASK): menu_type = 3; break; } //button 0 signals this is not a mouse-button click e2_context_menu_show (0, event->time, menu_type); return TRUE; } e2_utils_translate_key_event (event); return FALSE; } #ifdef EDIT_INPLACE /** @brief disable filelist refreshing during a filename edit @param renderer the renderer for the cell @param editable the interface to @a renderer @param path_string string form of gtk tree path to the row to be amended @param view pointer to view data struct @return */ static void _e2_fileview_edit_start_cb (GtkCellRenderer *renderer, GtkCellEditable *editable, gchar *path_string, ViewInfo *view) { printd (DEBUG, "start cell edit cb"); e2_filelist_disable_one_refresh ((view == curr_view) ? PANEACTIVE : PANEINACTIVE); //disable keypressed that will interfere g_signal_handlers_block_by_func (G_OBJECT (view->treeview), _e2_fileview_key_press_cb, view); } /** @brief enable filelist refreshing after an aborted filename edit @param renderer the renderer for the cell @param view pointer to view data struct @return */ static void _e2_fileview_edit_cancel_cb (GtkCellRenderer *renderer, ViewInfo *view) { printd (DEBUG, "cancel cell edit cb"); g_signal_handlers_unblock_by_func (G_OBJECT (view->treeview), _e2_fileview_key_press_cb, view); e2_filelist_enable_one_refresh ((view == curr_view) ? PANEACTIVE : PANEINACTIVE); } /** @brief save edited text value in the underlying treestore @param renderer the renderer for the cell @param path_string string form of gtk tree path to the row to be amended @param new_text replacement text string for the cell @param view pointer to view data struct @return */ static void _e2_fileview_name_edited_cb (GtkCellRendererText *cell, gchar *path_string, gchar *new_text, ViewInfo *view) { if (new_text != NULL) //probably always TRUE { printd (DEBUG, "filename edited cb, new text is %s", new_text); GtkTreeIter iter; if (gtk_tree_model_get_iter_from_string (view->model, &iter, path_string)) { gchar *oldname; gtk_tree_model_get (view->model, &iter, FILENAME, &oldname, -1); if (!( !strcmp (oldname, new_text) || !strcmp (oldname, G_DIR_SEPARATOR_S) //some things can't be changed || !strcmp (oldname, "../"))) { gchar *old, *new, *utfold, *utfnew, *replacement; DialogButtons choice; gint len; gboolean isdir, success; len = strlen (oldname) - sizeof (gchar); isdir = (*(oldname + len) == G_DIR_SEPARATOR); //ascii check ok if (isdir && len > 0) *(oldname + len) = '\0'; //strip trailer utfold = e2_utils_dircat (view, oldname, FALSE); old = F_FILENAME_TO_LOCALE (utfold); utfnew = e2_utils_dircat (view, new_text, FALSE); new = F_FILENAME_TO_LOCALE (utfnew); len = strlen (new) - sizeof (gchar); if (*(new + len) == G_DIR_SEPARATOR && len > 0) //ascii check ok *(new + len) = '\0'; //strip trailer #ifdef E2_VFS VPATH ddata = { new, view->spacedata }; #endif if (e2_option_bool_get ("confirm-overwrite") && !e2_fs_access2 (new E2_ERR_NONE())) { gdk_threads_leave (); choice = e2_dialog_ow_check (NULL, #ifdef E2_VFS &ddata, #else new, #endif NONE); gdk_threads_enter (); } else choice = OK; if (choice == OK) { #ifdef E2_VFS VPATH sdata = { old, view->spacedata }; #endif gdk_threads_leave (); //CHECKME what if a task-Q is is progress ? success = e2_task_backend_rename #ifdef E2_VFS (&sdata , &ddata); #else (old, new); #endif gdk_threads_enter (); if (success) { if (isdir && !g_str_has_suffix (new_text, G_DIR_SEPARATOR_S)) replacement = g_strconcat (new_text, G_DIR_SEPARATOR_S, NULL); else if (!isdir && g_str_has_suffix (new_text, G_DIR_SEPARATOR_S)) { replacement = g_strdup (new_text); *(replacement + strlen (replacement) - sizeof (gchar)) = '\0'; } else replacement = (gchar *) new_text; GtkTreeIter child; gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER (view->model), &child, &iter); gtk_list_store_set (view->store, &child, FILENAME, replacement, -1); if (replacement != new_text) g_free (replacement); } //editing works only on unselected rows and anyway, //doesn't stay selected after the next refresh // gtk_tree_selection_select_iter (view->selection, &iter); // if (success) // e2_filelist_request_refresh (view->dir, TRUE); CHECKME ok without E2_FAM ? } g_free (utfold); F_FREE (old, utfold); g_free (utfnew); F_FREE (new, utfnew); } g_free (oldname); } } g_signal_handlers_unblock_by_func (G_OBJECT (view->treeview), _e2_fileview_key_press_cb, view); e2_filelist_enable_one_refresh ((view == curr_view) ? PANEACTIVE : PANEINACTIVE); } #endif //def EDIT_INPLACE /* * @brief timer callback function which checks whether ok to proceed with a cd @param busy flag to check @return FALSE when ready to proceed */ /*static gboolean _e2_fileview_check_completion (gboolean *busy) { if (*busy) return TRUE; gtk_main_quit (); FIXME local loop app.timers[REFRESHWAIT_T] = 0; return FALSE; } */ // some filter menu callbacks (others are in filter-dialog files) /** @brief "toggled" signal callback for filters menu item Downstream functions require BGL closed @param widget toggled menu-item @param view pointer to data for view to be [un]filtered @return */ void e2_fileview_filter_dirs_cb (GtkCheckMenuItem *widget, ViewInfo *view) { //toggle cached flag to match the menu check button // view->filter_directories = gtk_check_menu_item_get_active (widget); view->filter_directories = !view->filter_directories; e2_fileview_refilter_list (view); } /** @brief "activate" signal callback for remove-filters menu item Downstream functions require BGL closed @param widget activated menu item @param view pointer to data for view to be [un]filtered @return */ void e2_fileview_remove_filters_cb (GtkMenuItem *widget, ViewInfo *view) { // turn off local flags view->name_filter.active = FALSE; view->size_filter.active = FALSE; view->date_filter.active = FALSE; view->filter_directories = FALSE; //show the results e2_toolbar_toggle_filter_button (view); e2_fileview_refilter_list (view); } /** @brief initialise file filter flags to show everything @param view data structure for view being processed @return */ /*void e2_fileview_initialize_filters (ViewInfo *view) { view->filter_directories = FALSE; view->name_filter.active = FALSE; g_strlcpy (view->name_filter.pattern, "*", sizeof(view->name_filter.pattern)); view->name_filter.case_sensitive = TRUE; view->size_filter.active = FALSE; view->size_filter.size = 0; view->size_filter.op = GT; view->date_filter.active = FALSE; view->date_filter.time = time(NULL); view->date_filter.time_type = ATIME; view->date_filter.op = GT; } */ /** @brief clear filter patterns for view associated with @a view @param view rt data for the view, assigned when function was initiated @return */ static void _e2_fileview_clear_filter_patterns (ViewInfo *view) { E2_SelectPattern *patterninfo; GSList *member; if (view->name_filter.compiled_patterns == NULL) return; for (member = view->name_filter.compiled_patterns; member != NULL; member = member->next) { patterninfo = (E2_SelectPattern *)member->data; g_pattern_spec_free (patterninfo->pspec); DEMALLOCATE (E2_SelectPattern, patterninfo); } g_slist_free (view->name_filter.compiled_patterns); view->name_filter.compiled_patterns = NULL; } /** @brief compile filter patterns for view associated with @a view @param view rt data for the view, assigned when function was initiated @return TRUE if the process was completed successfully */ static gboolean _e2_fileview_compile_filter_patterns (ViewInfo *view) { if (view->name_filter.compiled_patterns != NULL) _e2_fileview_clear_filter_patterns (view); E2_SelectPattern *patterninfo; GSList *members = NULL; gchar *s, *p, *freeme; gchar save; p = view->name_filter.patternptr; //maybe several patterns, separated by commas while ((s = strchr (p, ',')) != NULL) //if always ascii ',', don't need g_utf8_strchr() { //check each pattern that is followed by a ',' while (p[0] == ' ') //no \t check p++; if (p == s) { //empty pattern p++; continue; } patterninfo = MALLOCATE (E2_SelectPattern); //too small for slice #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (patterninfo, g_slist_free (members);return FALSE;) #else if (patterninfo == NULL) { g_slist_free (members); //improper cleanup, too bad return FALSE; } #endif members = g_slist_append (members, patterninfo); if (p[0] == '!') { patterninfo->negated = !view->name_filter.invert_mask; p += sizeof (gchar); } else { patterninfo->negated = view->name_filter.invert_mask; if (p[0] == '\\' && p[1] == '!') p += sizeof (gchar); } save = *s; *s = '\0'; if (!view->name_filter.case_sensitive) { freeme = g_utf8_strdown (p, -1); patterninfo->pspec = g_pattern_spec_new (freeme); g_free (freeme); } else patterninfo->pspec = g_pattern_spec_new (p); *s = save; s += sizeof (gchar); //pass the ',' p = s; } //now the last (or only) pattern while (p[0] == ' ') p++; if (p[0] == '\0') { view->name_filter.compiled_patterns = members; return (members != NULL); } patterninfo = MALLOCATE (E2_SelectPattern); //too small for slice #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (patterninfo, g_slist_free (members);return FALSE;) #else if (patterninfo == NULL) { g_slist_free (members); //improper cleanup, too bad return FALSE; } #endif members = g_slist_append (members, patterninfo); if (p[0] == '!') { patterninfo->negated = !view->name_filter.invert_mask; p++; } else { patterninfo->negated = view->name_filter.invert_mask; if (p[0] == '\\' && p[1] == '!') p++; } if (!view->name_filter.case_sensitive) { freeme = g_utf8_strdown (p, -1); patterninfo->pspec = g_pattern_spec_new (freeme); g_free (freeme); } else patterninfo->pspec = g_pattern_spec_new (p); view->name_filter.compiled_patterns = members; return TRUE; } static gchar *name_pattern = NULL; static gboolean hide_updir = FALSE; //for speed, set in refilter-list func /** @brief treeview row visibility function @param model the child model of the GtkTreeModelFilter @param iter pointer to a GtkTreeIter for the row in @a model whose visibility is to be determined @param view rt data for the view, assigned when function was initiated @return TRUE if the row is visible */ static gboolean _e2_fileview_filter_check (GtkTreeModel *model, GtkTreeIter *iter, ViewInfo *view) { FileInfo *info; gtk_tree_model_get (model, iter, FINFO, &info, -1); gchar *name = info->filename; if (ITEM_ISHIDDEN (name) && ( //name[1] == '\0' || filtered out when dir read (!view->show_hidden && (name[1] != '.' || name[2] != '\0')) || (name[1] == '.' && name[2] == '\0' && (hide_updir || (view->dir[0] == G_DIR_SEPARATOR && view->dir[1] == '\0')) //never show updir in root directory ) ) ) #ifdef E2_VFSTMP //FIXME show updir in root of archive etc v-dirs #endif return FALSE; if (!view->filter_directories && e2_fs_is_dir (info, view)) return TRUE; if (view->name_filter.active) { //hidden updir's are already refused, above if (name[0] != '.' || name[1] != '.' || name[2] != '\0') { GSList *member; E2_SelectPattern *patterninfo; gboolean negated = FALSE, matched = FALSE, positive_check = FALSE, result = FALSE; gchar *freeme; gchar *utf = D_FILENAME_FROM_LOCALE (info->filename); //not DISPLAYNAME, always dup, to avoid refresh race for (member = view->name_filter.compiled_patterns; member != NULL; member= member->next) { patterninfo = (E2_SelectPattern *)member->data; negated = patterninfo->negated; if (!positive_check) positive_check = !negated; if (!view->name_filter.case_sensitive) { freeme = g_utf8_strdown (utf, -1); matched = g_pattern_match_string (patterninfo->pspec, freeme); g_free (freeme); } else matched = g_pattern_match_string (patterninfo->pspec, utf); if (matched && negated) { g_free (utf); return FALSE; } if (matched && !negated) result = TRUE; //but keep looking for any later exclude //if neither negated nor matched, we don't change result } g_free (utf); //extra check for unmatched final check if (!matched && negated && !positive_check) result = TRUE; if (!result) return FALSE; } } if (view->size_filter.active) { switch (view->size_filter.op) { case LT: if (info->statbuf.st_size >= view->size_filter.size) return FALSE; break; case EQ: if (info->statbuf.st_size != view->size_filter.size) return FALSE; break; default: // case GT: if (info->statbuf.st_size <= view->size_filter.size) return FALSE; break; } } if (view->date_filter.active) { switch (view->date_filter.time_type) { case ATIME: if (view->date_filter.op == GT) { if (difftime (view->date_filter.time, info->statbuf.st_atime) > 0) return FALSE; } else if (difftime (view->date_filter.time, info->statbuf.st_atime) <= 0) return FALSE; break; case CTIME: if (view->date_filter.op == GT) { if (difftime (view->date_filter.time, info->statbuf.st_ctime) > 0) return FALSE; } else if (difftime (view->date_filter.time, info->statbuf.st_ctime) <= 0) return FALSE; break; default: // case MTIME: if (view->date_filter.op == GT) { if (difftime (view->date_filter.time, info->statbuf.st_mtime) > 0) return FALSE; } else if (difftime (view->date_filter.time, info->statbuf.st_mtime) <= 0) return FALSE; break; } } return TRUE; } /** @brief treeview row visibility function for unfiltered view Assumes "." entry is filtered out when dir data is read @param model the child model of the GtkTreeModelFilter @param iter pointer to a GtkTreeIter for the row in @a model whose visibility is to be determined @param viewdir ptr to view->dir @return TRUE unless the row is ".." in the root directory */ static gboolean _e2_fileview_filter_none (GtkTreeModel *model, GtkTreeIter *iter, gchar *viewdir) { #ifdef E2_VFSTMP //FIXME show updir from root of archive etc v-dirs #endif //do fastest checks in order, to filter updir item from root dir if (viewdir[1] == '\0' && viewdir[0] == G_DIR_SEPARATOR) //ascii ok { FileInfo *info; gtk_tree_model_get (model, iter, FINFO, &info, -1); if (info->filename[2] == '\0' && info->filename[1] == '.' && ITEM_ISHIDDEN(info->filename)) return FALSE; } return TRUE; } /** @brief update filters for treeview This changes model, and attaches view to new model Assumes view attached to a model/store. Assumes BGL closed. When new store is created, view->filtered_before will be false @param view rt data for the view @return */ void e2_fileview_refilter_list (ViewInfo *view) { hide_updir = !e2_option_bool_get ("show-updir-entry"); //save, for speedier filtering gboolean filtered_now = ( view->name_filter.active || view->size_filter.active || view->date_filter.active || !view->show_hidden || hide_updir ); if (filtered_now) { //setup for filter func if (view->name_filter.active) { _e2_fileview_compile_filter_patterns (view); if (!view->name_filter.case_sensitive) name_pattern = g_utf8_strdown (view->name_filter.patternptr, -1); else name_pattern = g_strdup (view->name_filter.patternptr); } if (!view->filtered_before) { //gtk (2.6 at least) won't allow a changed visibility function, //so have to replace the filtermodel to make such change //this combination of changes has been confirmed to 0-ref old //filtermodel, and leave all other relevant refcounts = 1 view->model = gtk_tree_model_filter_new (GTK_TREE_MODEL (view->store), NULL); gtk_tree_view_set_model (GTK_TREE_VIEW (view->treeview), view->model); g_object_unref (G_OBJECT (view->model)); gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (view->model), (GtkTreeModelFilterVisibleFunc) _e2_fileview_filter_check, view, NULL); } gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (view->model)); if (view->name_filter.active) { _e2_fileview_clear_filter_patterns (view); g_free (name_pattern); } } else //if (view->filtered_before) { //de-filter or apply unfiltered visibility func view->model = gtk_tree_model_filter_new (GTK_TREE_MODEL (view->store), NULL); gtk_tree_view_set_model (GTK_TREE_VIEW (view->treeview), view->model); g_object_unref (G_OBJECT (view->model)); #ifdef E2_VFSTMP //FIXME for v-dir, display ".." in root of archive etc, needs view ptr #else gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (view->model), (GtkTreeModelFilterVisibleFunc) _e2_fileview_filter_none, view->dir, NULL); #endif gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (view->model)); } if (view->row > 0) { guint visible_rows = gtk_tree_model_iter_n_children (view->model, NULL); if (view->row > visible_rows - 1) view->row = visible_rows - 1; if (view->row < 0) view->row = 0; //if there was no selection, or the former selection is gone (after filter or deletion) //we want relative cursor-moves to stay in the same vicinity if (visible_rows > 0 && gtk_tree_selection_count_selected_rows (view->selection) == 0) { GtkTreePath *tpath = gtk_tree_path_new_from_indices (view->row, -1); //this "marks" the item, but mere selection is not enough for this to work gtk_tree_view_set_cursor (GTK_TREE_VIEW (view->treeview), tpath, NULL, FALSE); gtk_tree_selection_unselect_path (view->selection, tpath); gtk_tree_path_free (tpath); } } view->filtered_before = filtered_now; view->filtercount++; //signal to the world that something has changed } /** @brief decide whether type-ahead search matches a row @param model the treemodel being searched @param column UNUSED the search column (= FILENAME) @param key the aggregate string of keys sofar pressed @param iter an iter pointing the row of @a model that should be compared with @a key @param view pointer to data struct for the view @return FALSE when a match is found */ static gboolean _e2_fileview_match_filename (GtkTreeModel *model, gint column, const gchar *key, GtkTreeIter *iter, ViewInfo *view) { gchar *itemname; gtk_tree_model_get (model, iter, FILENAME, &itemname, -1); gboolean match = g_str_has_prefix (itemname, key); if (match) { //we have a match GtkTreePath *path = gtk_tree_model_get_path (model, iter); view->row = *gtk_tree_path_get_indices (path); if (view->row > 0) view->row++; //CHECKME why is this needed .. //these selection-flags probably ineffective e2_fileview_focus_row (view, view->row, FALSE, FALSE, FALSE, TRUE); gtk_tree_path_free (path); } g_free (itemname); return !match; } /* * @brief UNUSED find filename begining with specified char, in current view @param ch character at the start of the name @return the row number where the matching name was found, or -1 if not found */ /*static gint e2_fileview_find_filename_begining_with (gchar ch) { gint i; //number of rows in the model gint n = gtk_tree_model_iter_n_children (curr_view->model, NULL); GtkTreeIter iter; GtkTreeModel *model = curr_view->model; gchar *name; // search from after the current row to the end of the store for (i = curr_view->row+1; i < n; i++) { if (! gtk_tree_model_iter_nth_child (model, &iter, NULL, i)) continue; //the specified row doesn't exist any more gtk_tree_model_get (model, &iter, FILENAME, &name, -1); if (name[0] == ch) { g_free (name); return i; } else g_free (name); } // search from the first row to the current row for (i = 0; i < curr_view->row; i++) { if (! gtk_tree_model_iter_nth_child (model, &iter, NULL, i)) continue; //the specified row doesn't exist any more gtk_tree_model_get (model, &iter, FILENAME, &name, -1); if (name[0] == ch) { g_free (name); return i; } else g_free (name); } return -1; } */ // Public functions /** @brief display and optionally select a numbered row The treeview is scrolled, and the cursor is moved, whether or not the focused row is selected. Of course, the focused row may be selected by gtk, independently of @a select_row, and current selection will probably be cleared by gtk, independently of @a clear_selection. This assumes that BGL is closed @param view rt data for ther view to be worked on @param row treeview index of the row that is to be focused @param select_row TRUE if the focused row needs to be selected @param clear_selection TRUE if whole selection for the view is to be un-selected first @param center TRUE to place the focused row near the center of the view window @param grab_focus TRUE if the treeview is to be given the focus @return */ void e2_fileview_focus_row (ViewInfo *view, gint row, gboolean select_row, gboolean clear_selection, gboolean center, gboolean grab_focus) { if (view != NULL) { GtkTreeIter iter; GtkTreeModel *model = view->model; //gtk_tree_view_get_model (GTK_TREE_VIEW (view->treeview)); if (! gtk_tree_model_iter_nth_child (model, &iter, NULL, row)) return; //the specified row doesn't exist GtkTreeView *tvw = GTK_TREE_VIEW (view->treeview); GtkTreePath *path = gtk_tree_model_get_path (model, &iter); gtk_tree_view_scroll_to_cell (tvw, path, NULL, center, 0.382, 0.0); //remember existing selection, if any, because moving the cursor kills //the selection (if that hasn't been done by gtk already) GList *selitems = (clear_selection) ? NULL : gtk_tree_selection_get_selected_rows (view->selection, NULL); gtk_tree_view_set_cursor (tvw, path, NULL, FALSE); //selects just this row if (selitems != NULL) { GList *member; GtkTreePath *tp; for (member = selitems; member != NULL; member = member->next) { tp = (GtkTreePath *) member->data; gtk_tree_selection_select_path (view->selection, tp); gtk_tree_path_free (tp); } g_list_free (selitems); } if (!select_row) gtk_tree_selection_unselect_path (view->selection, path); if (grab_focus) gtk_widget_grab_focus (view->treeview); gtk_tree_path_free (path); } } /* * @brief UNUSED select 1st row, in specified view, where the filename matches a specified name model content update is suspended while this is performed @param view rt data for ther view to be worked on @param filename string with name of file to be used for selection @return */ /*void e2_fileview_select_row_by_filename (ViewInfo *view, gchar *filename) { gint i; //number of rows in the model gint n = gtk_tree_model_iter_n_children (curr_view->model, NULL); GtkTreeIter iter; GtkTreeModel *model = curr_view->model; gchar *thisname; e2_filelist_disable_refresh(); // search from after the current row to the end of the store for (i = 0; i < n; i++) { if (! gtk_tree_model_iter_nth_child (model, &iter, NULL, i)) continue; //the specified row doesn't exist any more gtk_tree_model_get (model, &iter, FILENAME, &thisname, -1); if (!strcmp (thisname, filename)) { e2_fileview_focus_row (view, i, TRUE, FALSE, TRUE, TRUE); g_free (thisname); break; } else g_free (thisname); } e2_filelist_enable_refresh(); } */ /** @brief get file info for 1st selected row This includes a special-case mechanism for getting "..", which may be opened or activated when updir entries are shown in filelists @param view data stuct for the view to be worked on @param updir TRUE to include "../" in the candidates @return FileInfo data for the 1st selected row, or if none, then NULL */ FileInfo *e2_fileview_get_selected_first_local (ViewInfo *view, gboolean updir) { FileInfo *info = NULL; // GtkTreeSelection *sel = view->selection; GtkTreeModel *model; GList *rowpath; GList *selpaths = gtk_tree_selection_get_selected_rows (view->selection, &model); for (rowpath = selpaths; rowpath != NULL; rowpath = rowpath->next) { GtkTreeIter iter; GtkTreePath *path = (GtkTreePath *) rowpath->data; if (gtk_tree_model_get_iter (model, &iter, path)) { gtk_tree_model_get (model, &iter, FINFO, &info, -1); if (updir || strcmp (info->filename, "..")) break; //ignore selection of "../" info = NULL; //in case nothing else is selected } } g_list_foreach (selpaths, (GFunc) gtk_tree_path_free, NULL); g_list_free (selpaths); return info; } /** @brief get list of selected rows' info items This is usable only when the data is used before any chance of filelist refresh, which would invalidate the listed data pointers @param view rt data for the view to be worked on @return list of selected rows' info items, excluding ".." */ GList *e2_fileview_get_selected_local (ViewInfo *view) { // g_signal_emit_by_name (G_OBJECT (view->treeview), "end-selection"); // if (view->tagged != NULL) // return g_list_copy (view->tagged); GList *selectedrow_data = NULL; FileInfo *info; GtkTreePath *path; GtkTreeIter iter; // GtkTreeSelection *sel = view->selection; GtkTreeModel *model; GList *rowpath; GList *selpaths = gtk_tree_selection_get_selected_rows (view->selection, &model); for (rowpath = selpaths; rowpath != NULL; rowpath = rowpath->next) { path = (GtkTreePath *) rowpath->data; if (gtk_tree_model_get_iter (model, &iter, path)) { gtk_tree_model_get (model, &iter, FINFO, &info, -1); if (strcmp (info->filename, "..")) //CHECKME ok, even with localised string? selectedrow_data = g_list_append (selectedrow_data, info); } gtk_tree_path_free (path); } g_list_free (selpaths); return selectedrow_data; } /** @brief toggle selection of all items in a pane This function selects or unselects all** items in a pane, depending on the current state of the corresponding flag, pane1_all or pane2_all ** except '..' entries, (if displayed) The pane will be activated, if it wasn't already. The function handles clicks of the select-all toggle buttons @param blah unused widget ptr (in case this is a callback) @param view fileview for pane that is to be (un)selected @return */ void e2_fileview_select_all (GtkWidget *blah, ViewInfo *view) { if (curr_view != view) e2_pane_activate_other (); GtkTreeSelection *sel = curr_view->selection; gboolean *allnow = (view == &app.pane1.view) ? &pane1_all : &pane2_all ; if (*allnow) { gtk_tree_selection_unselect_all (sel); *allnow = FALSE; } else //select { gtk_tree_selection_select_all (sel); *allnow = TRUE; } } /** @brief cleanup content of selected file info struct foreach function for e2_fileview_clean_selected() @param seldata ptr to the struct to be processed @param user_data user data passed to g_ptr_array_foreach() UNUSED @return */ void e2_fileview_selected1_clean (E2_SelectedItemInfo *seldata, gpointer user_data) { /* g_free (seldata->filename); #ifdef E2_INCLIST gtk_tree_row_reference_free (seldata->ref); #else gtk_tree_path_free (seldata->path); #endif */ DEALLOCATE (E2_SelectedItemInfo, seldata); } /** @brief cleanup pointer array of selected file info structs @param selected ptr to the array to be freed @return */ void e2_fileview_clean_selected (GPtrArray *selected) { printd (DEBUG, "In e2_fileview_clean_selected() selected: %x", selected); //#ifndef USE_GLIB2_22 g_ptr_array_foreach (selected, (GFunc) e2_fileview_selected1_clean, NULL); //#endif g_ptr_array_free (selected, TRUE); } /** @brief create pointer array of selected file info structs This fn creates a pointer array, with pointers to structs containing selected items' names and other info. If found, ".." is filtered out. If found, any trailing / is removed. Item name strings are localised Data is duplicated, so the selection is refresh-proof. Treerow refs support incremental store changes as the selection is processed The returned array needs to be freed with its data. @param view is data structure for the view to be interrogated @return pointer to GPtrArray structure, or NULL if nothing selected */ GPtrArray *e2_fileview_get_selected (ViewInfo *view) { // if (view == curr_view) // g_signal_emit_by_name (G_OBJECT(view->treeview), "end-selection"); GtkTreeModel *model; GList *selpaths = gtk_tree_selection_get_selected_rows (view->selection, &model); if (selpaths != NULL) { GList *rowpath; GtkTreeIter iter; FileInfo *info; E2_SelectedItemInfo *seldata; GPtrArray *array = g_ptr_array_sized_new (g_list_length(selpaths)); for (rowpath = selpaths; rowpath != NULL; rowpath = rowpath->next) { if (gtk_tree_model_get_iter (model, &iter, rowpath->data)) { gtk_tree_model_get (model, &iter, FINFO, &info, -1); if (strcmp (info->filename, "..")) { seldata = ALLOCATE (E2_SelectedItemInfo); CHECKALLOCATEDWARN (seldata, continue); memcpy (&seldata->filename, &info->filename, sizeof (seldata->filename)); /*#ifdef E2_INCLIST //get a reference in case the model changes during the operation on the selection seldata->ref = gtk_tree_row_reference_new (model, paths->data); #else //this is only relevant if store data not changed on the fly seldata->path = gtk_tree_path_copy (paths->data); #endif */ g_ptr_array_add (array, seldata); } gtk_tree_path_free ((GtkTreePath *)rowpath->data); } } g_list_free (selpaths); if (array->len > 0) //there was more than just a ".." selected return array; g_ptr_array_free (array, TRUE); } return NULL; } /** @brief cleanup one dir-history item, during app.dir_history destruction @param data ptr to the struct to be processed @return */ void e2_fileview_clean1_history (gpointer data) { E2_DirHistoryEntry *hist = (E2_DirHistoryEntry *)data; if (hist->selitems != NULL) g_hash_table_destroy (hist->selitems); DEALLOCATE (E2_DirHistoryEntry, hist); } /** @brief get itemname for a specified view and row @param view is data structure for the view to be interrogated @param row number of the row to be interrogated REAL or FILTERED ? @return pointer to duplicated string, or NULL if there's nothing found */ gchar *e2_fileview_get_row_name (ViewInfo *view, gint row) { GtkTreeIter iter; // GtkTreeModel *model = GTK_TREE_MODEL (view->store); maybe filter GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (view->treeview)); if (! gtk_tree_model_iter_nth_child (model, &iter, NULL, row)) return NULL; gchar *name; gtk_tree_model_get (model, &iter, FILENAME, &name, -1); return name; } /** @brief helper to set file-list font @param view pointer to data struct for the view to be changed @param fontstr name of font to apply to the filelist associated with @a view @return */ static void _e2_fileview_set_font (ViewInfo *view, gchar *fontstr) { GList* columns, *renderers, *base1, *base2; columns = base1 = gtk_tree_view_get_columns (GTK_TREE_VIEW (view->treeview)); for (; columns != NULL ; columns = columns->next) { GtkTreeViewColumn *column = columns->data; renderers = base2 = gtk_tree_view_column_get_cell_renderers (column); for (; renderers != NULL ; renderers = renderers->next) g_object_set (renderers->data, "font", fontstr, NULL); g_list_free (base2); } g_list_free (base1); } /** @brief set file-lists font This is needed to change font after a config dialog that does not rebuild the whole window @return */ void e2_fileview_set_font (void) { gchar *fontstr = (e2_option_bool_get ("custom-list-font")) ? e2_option_str_get ("list-font") : NULL; //NULL forces default _e2_fileview_set_font (curr_view, fontstr); _e2_fileview_set_font (other_view, fontstr); } /** @brief set background color of treeview line @param view data struct for the view to process @param iter pointer to iter for the row to be changed @param color pointer to struct with data for the new color @return */ void e2_fileview_set_row_background (ViewInfo *view, GtkTreeIter *iter, GdkColor *color) { //set the attribute column for this row gtk_list_store_set (GTK_LIST_STORE (view->store), iter, BACKCOLOR, color, -1); view->lit = TRUE; } /** @brief revert background color of treeview line @param view data struct for the view to process @param iter pointer to iter for the row to be changed @return */ void e2_fileview_clear_row_background (ViewInfo *view, GtkTreeIter *iter) { //restore the background-attribute column for this row #ifdef E2_ASSISTED GdkColor *bc; if (e2_option_bool_get ("color-background-set")) bc = e2_option_color_get ("color-background"); else bc = NULL; gtk_list_store_set (GTK_LIST_STORE (view->store), iter, BACKCOLOR, bc, -1); #else gtk_list_store_set (GTK_LIST_STORE (view->store), iter, BACKCOLOR, NULL, -1); #endif view->lit = FALSE; } /** @brief translate array into another form sorted in index order This is needed to convert the 'fixed' column order used in data models and config/cache data, to variable order in accord with the displayed view columns and vice versa @param src_array pointer to array of ints that is to be translated @param dest_array pointer to array of ints that stores the translated order @param size the number of elements to be translated @return */ void e2_fileview_translate_cols_array (gint *src_array, gint *dest_array, gint size) { gint i; for (i = 0; i < size; i++) dest_array[src_array[i]] = i; } /** @brief update columns data, that is not automatically updated, for caching Columns order and widths are udated and stored in a list List format is: pane1 {order, width, ...}, pane2 {order, width, ...} @return */ void e2_fileview_update_col_cachedata (void) { //get current column widths GtkTreeViewColumn *col, *last; G_CONST_RETURN gchar *title; gint i, lc; GList *columns = gtk_tree_view_get_columns (GTK_TREE_VIEW (app.pane1.view.treeview)); GList *base = columns; lc = 0; last = (GtkTreeViewColumn *)columns->data; for (; columns != NULL; columns=columns->next) { col = columns->data; title = gtk_tree_view_column_get_title (col); //title is "0" ... "7", for cols 0 ... 7 i = atoi (title); col_width_store [0][i] = gtk_tree_view_column_get_width (col); //remember this one if it's a candidate for last-displayed column if (col_width_store [0][i] > 0) { lc = i; last = col; } } //for last-displayed column, cache the user-specified width, not the //expanded-to-fill width col_width_store [0][lc] = gtk_tree_view_column_get_fixed_width (last); g_list_free (base); columns = gtk_tree_view_get_columns (GTK_TREE_VIEW (app.pane2.view.treeview)); base = columns; lc = 0; last = (GtkTreeViewColumn *)columns->data; for (; columns != NULL; columns=columns->next) { col = columns->data; title = gtk_tree_view_column_get_title (col); i = atoi (title); col_width_store [1][i] = gtk_tree_view_column_get_width (col); //remember this one if it's a candidate for last-displayed column if (col_width_store [1][i] > 0) { lc = i; last = col; } } col_width_store [1][lc] = gtk_tree_view_column_get_fixed_width (last); g_list_free (base); //get columns displayed order e2_fileview_translate_cols_array (displayed_col_order[0], stored_col_order[0], MAX_COLUMNS); e2_fileview_translate_cols_array (displayed_col_order[1], stored_col_order[1], MAX_COLUMNS); /*SUPERSEDED BY ARRAY CACHEING //setup for list-cache writing //start fresh list e2_list_free_with_data (&cols_data); gchar *buffer; gint j; for (j = 0; j < 2; j++) { for (i = 0; i < MAX_COLUMNS; i++) { //data need to be strings, for the cache writer buffer = g_strdup_printf ("%d", stored_col_order[j][i]); cols_data = g_list_append (cols_data, buffer); buffer = g_strdup_printf ("%d", col_width_store[j][i]); cols_data = g_list_append (cols_data, buffer); }} */ } /* * @brief UNUSED find which view column shows filenames Columns may be in any order Scans column titles, looking for "0" @param view data structure for the view to which the file list belongs @return integer column number, 0 to MAX_COLUMNS-1 */ /* gint e2_fileview_find_name_col (ViewInfo *view) { GList *cols = gtk_tree_view_get_columns (GTK_TREE_VIEW (view->treeview)); GList *base = cols; G_CONST_RETURN gchar *title; gint result = 0; for ( ; cols != NULL; cols = cols->next ) { title = gtk_tree_view_column_get_title (cols->data); if (!strcmp (title, "0")) break; result++; } g_list_free (base); // return (result < MAX_COLUMNS) ? result : -1; return result; } */ /** @brief exchange active and inactive panes This function is called from e2_pane_activate_other (), i.e. whenever we need to exchange curr_view and other_view. It toggles pointers and changes colours The latter is done by interrogating the selections in both file views, and so this fn should be blocked if either view is still being populated at the time this is called ?? CHECKME @return */ void e2_fileview_switch_views (void) { ViewInfo *temp; #ifdef E2_VFSTMP //FIXME what dir to open ? other_view->dir may be virtual, as indicated by //other_view->spacedata //add some func like e2_fs_chdir (view); if (other_view->spacedata != NULL) { if (e2_fs_chdir_local (other_view->spacedata->workplace E2_ERR_NONE())) return; } else #else if (!e2_fs_chdir (other_view->dir E2_ERR_NONE())) #endif return; temp = other_view; other_view = curr_view; curr_view = temp; e2_pane_flag_active (); //change the status-indicator for the panes } /* * @brief UNUSED swap view pointers Exchange curr_view and other_view, without doing anything else @return */ /*void e2_fileview_switch_views_simple (void) { ViewInfo *temp = other_view; other_view = curr_view; curr_view = temp; }*/ /** @brief get indices of top line and left column shown in a filelist treeview This gets scroll info for treeview rebuilds @param view data structure for the view to be changed @param leftcol pointer to store for the left column index @param toprow pointer to store for the top row index @return */ void e2_fileview_get_scroll_data (ViewInfo *view, gint *leftcol, gint *toprow) { GtkTreePath *startpath; GtkTreeViewColumn *startcol; // gint cell_x, cell_y; if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (view->treeview), 0, 0, &startpath, &startcol, NULL, NULL)) //&cell_x, &cell_y)) { *toprow = *gtk_tree_path_get_indices (startpath); printd (DEBUG, "path found, row is %d", *toprow); gtk_tree_path_free (startpath); } else //liststore is empty *toprow = 0; //gtk bug, startcol always is the one at index 0 (maybe fixed after 2.8.6) *leftcol = 0; } /** @brief scroll a filelist treeview to top line @a toprow and left column @a leftcol This is used in treeview rebuilds It does nothing if the treeview doesn't exist yet- i.e. sometimes when threaded Assumes BGL is closed @param view data structure for the view to be changed @param leftcol left column index @param toprow top row index @return TRUE if the treeview exists and scrolling was needed */ gboolean e2_fileview_scroll_to_position (ViewInfo *view, gint leftcol, gint toprow) { if (GTK_IS_TREE_VIEW (view->treeview)) { if (toprow > 0 || leftcol > 0) { GtkTreeView *tvw = GTK_TREE_VIEW (view->treeview); GtkTreeModel *model = gtk_tree_view_get_model (tvw); GtkTreeViewColumn* col = NULL; GtkTreePath* path = NULL; if (toprow > 0) { GtkTreeIter iter; if (gtk_tree_model_iter_nth_child (model, &iter, NULL, toprow)) path = gtk_tree_model_get_path (model, &iter); } else path = gtk_tree_path_new_first (); // WAIT_FOR_EVENTS gtk_tree_view_scroll_to_cell (tvw, path, col, TRUE, 0, 0); gtk_tree_path_free (path); return TRUE; } } return FALSE; } #ifdef E2_VFS /** @brief construct a guint hash for a vpath, for use in processing a hash table The hash can be passed to g_hash_table_new() as the @hash_func parameter, when using vpaths as keys @param v pointer to the vpath key @return a guint hash value corresponding to the key */ guint e2_fileview_vdir_hash (gconstpointer v) { // 31 bit hash function, same as g_str_hash VPATH *vp = (VPATH *)v; const char *p = VPCSTR (vp); guint h = *p; if (h) { for (p += 1; *p != '\0'; p++) h = (h << 5) - h + *p; } if (vp->spacedata != NULL) { guint i, count = sizeof (gpointer); for (i = 0, p = (const char *)vp->spacedata; i < count; i++, p++) h = (h << 5) - h + *p; } return h; } /** @brief check for equivalence of hash-table keys @a v1 and @a v2 @param v1 a VPATH key to compare with @a v2 @param v2 a VPATH key to compare with @a v1 @return TRUE if the keys match */ gboolean e2_fileview_vdir_equal (gconstpointer v1, gconstpointer v2) { VPATH *vp1 = (VPATH *)v1; VPATH *vp2 = (VPATH *)v2; if (vp1->spacedata != vp2->space_data) return FALSE; return (!strcmp(vp1->path), vp2->path)); } /** @brief clear a key from vpaths hash table @param k the key @return */ void e2_fieview_vdir_free (gpointer k) { VPATH *vp = (VPATH *)k; g_free ((gchar*)vp->path); DEALLOCATE (vpath, vp); } #endif /** @brief helper function to clean up redundant tree hash tables when there's nothing better to do @param data pointer to table to be destroyed @return FALSE so that this is not called again */ gboolean e2_fileview_treehash_free (GHashTable *data) { g_hash_table_destroy (data); return FALSE; } /** @brief get hash of selected-item names for pane in @a view Logged names are in utf-8 encoding, dirs have trailing / It is used as part of the view refresh or recreate processes (even with FAM, needed for refresh action, (un)hide, (un)filter, window re-create) @param view data structure for view being processed @return hash table of names, NULL if nothing selected */ //CHECKME filter & child GHashTable *e2_fileview_log_selected_names (ViewInfo *view) { GtkTreeModel *model; GList *selpaths = gtk_tree_selection_get_selected_rows (view->selection, &model); if (selpaths == NULL) return NULL; //build names-hash GHashTable *treehash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); GtkTreeIter iter; gchar *itemname; GList *rowpath; for (rowpath = selpaths; rowpath != NULL; rowpath = rowpath->next) { if (gtk_tree_model_get_iter (model, &iter, rowpath->data)) { gtk_tree_model_get (model, &iter, FILENAME, &itemname, -1); g_hash_table_insert (treehash, itemname, NULL); } gtk_tree_path_free (rowpath->data); } g_list_free (selpaths); return treehash; } /** @brief re-select items in list store for specified view @a view ->selected_names is freed here @param view data structure for view being processed @return */ void e2_fileview_reselect_names (ViewInfo *view) { // GtkTreeModel *model = GTK_TREE_MODEL (view->store); //may be filter model GtkTreeModel *model = gtk_tree_view_get_model (GTK_TREE_VIEW (view->treeview)); GtkTreeIter iter; gboolean notempty = gtk_tree_model_get_iter_first (model, &iter); GHashTable *oldselected = view->selected_names; if (oldselected != NULL && notempty) { guint more_hits = g_hash_table_size (oldselected); GtkTreeSelection *sel = view->selection; gchar *itemname; gpointer key; gpointer value; do { gtk_tree_model_get (model, &iter, FILENAME, &itemname, -1); if (g_hash_table_lookup_extended (oldselected, itemname, &key, &value)) { gtk_tree_selection_select_iter (sel, &iter); more_hits--; } g_free (itemname); } while (more_hits > 0 && gtk_tree_model_iter_next (model, &iter)); } if (oldselected != NULL) { //setup to get rid of hash when there's more time g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) e2_fileview_treehash_free, oldselected, NULL); view->selected_names = NULL; } } /** @brief [re]register all key-binding callbacks for a filelist treeview There are also generic keypress-event bindings with 'higher priority' than these @param treeview the widget being processed @param view pointer to view data corresponding to @a treeview @return */ void e2_fileview_register_keybindings (GtkWidget *treeview, ViewInfo *view) { //other "key-press-event" connection(s) have non-NULL data //also, if needed, bindings cb func is _e2_keybinding_key_press_cb() guint id = g_signal_lookup ("key-press-event", GTK_TYPE_TREE_VIEW); g_signal_handlers_disconnect_matched (G_OBJECT (treeview), G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_DATA, id, 0, NULL, NULL, NULL); gchar *category = g_strconcat(_C(17),".",_C(23),".",_C(33),NULL); //_("general.main.panes e2_keybinding_enrol (treeview, 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 a filelist treeview @param treeview the widget being processed @param view pointer to view data corresponding to @a treeview @return */ void e2_fileview_register_pointerbindings (GtkWidget *treeview, ViewInfo *view) { //disconnect all current handlers, so that the btn-binding callback can be //connected first g_signal_handlers_disconnect_matched (G_OBJECT (treeview), G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, _e2_fileview_button_press_cb, view); guint id = g_signal_lookup ("button-press-event", GTK_TYPE_TREE_VIEW); g_signal_handlers_disconnect_matched (G_OBJECT (treeview), G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_DATA, id, 0, NULL, NULL, NULL); g_signal_handlers_disconnect_matched (G_OBJECT (treeview), G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, NULL, _e2_fileview_button_release_cb, view); id = g_signal_lookup ("button-release-event", GTK_TYPE_TREE_VIEW); g_signal_handlers_disconnect_matched (G_OBJECT (treeview), G_SIGNAL_MATCH_ID | G_SIGNAL_MATCH_DATA, id, 0, NULL, NULL, NULL); gchar *category = g_strconcat(_C(17),".",_C(23),".",_C(33),NULL); //_("general.main.panes //pick up any custom button-bindings before the default press/release e2_mousebinding_enrol (treeview, category, (void(*)(E2_OptionSet*))NULL); # ifdef E2_PTRGESTURES e2_mousegesture_enrol (treeview, category, (void(*)(E2_OptionSet*))NULL); # endif g_free (category); //these button cbs do generic things whether or not E2_MOUSECUSTOM appies g_signal_connect (G_OBJECT (treeview), "button-press-event", G_CALLBACK (_e2_fileview_button_press_cb), view); g_signal_connect (G_OBJECT (treeview), "button-release-event", G_CALLBACK (_e2_fileview_button_release_cb), view); } #endif /** @brief cleanup helper function @param info data item to clean @param data UNUSED @return */ static void _e2_fileview_cleaninfo (FileInfo *info, gpointer data) { DEALLOCATE (FileInfo, info); } /** @brief convert @a entries from a list of localised heaped strings to a list of FileInfo's Some 'custom' flags (dirlinks etc) are set if appropriate. Upon failure, the supplied list is cleared and its ptr set to relevant error code @param parentpath absolute path of dir containing items in @a list, localised string @param list store for ptr to Glist of heaped item names @return TRUE if the list was converted successfully */ static gboolean _e2_fileview_make_all_infos (gchar *parentpath, GList **list) { gint len1, len2, trailer; gchar *item; FileInfo *info; GList *member; guint errval = E2DREAD_ENOENT; //assign to prevent compiler error #ifdef E2_VFS VPATH data; # ifdef E2_VFSTMP //get proper spacedata # endif data.spacedata = NULL; //FIXME # ifdef E2_VFSTMP //check if #'d vpath for parentpath exists, or else make one # endif #endif gboolean mingle = ! e2_option_bool_get ("sort-dirs-first"); len1 = strlen (parentpath); trailer = (*(parentpath + len1 - sizeof (gchar)) == G_DIR_SEPARATOR) ? 0 : sizeof (gchar); //ascii ok len2 = (len1 + trailer + NAME_MAX)/8*8 + 8; gchar localpath[len2]; g_strlcpy (localpath, parentpath, sizeof (localpath)); if (trailer) { *(parentpath + len1 - sizeof (gchar)) = G_DIR_SEPARATOR; len1 += sizeof (gchar); } len2 -= len1; //or NAME_MAX ? item = localpath + len1; //retval = TRUE; for (member = *list; member != NULL; member = member->next) { #ifdef USE_GLIB2_10 info = (FileInfo *) g_slice_alloc (sizeof (FileInfo)); // info = ALLOCATE (FileInfo); #else info = ALLOCATE (FileInfo); #endif #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (info, errval = E2DREAD_ENOMEM;goto errexit;) #else if (info == NULL) { errval = E2DREAD_ENOMEM; goto errexit; } #endif nextmember: g_strlcpy (item, (gchar *)member->data, len2); #ifdef E2_VFS data.path = localpath; if (e2_fs_lstat (&data, &info->statbuf E2_ERR_NONE())) #else if (e2_fs_lstat (localpath, &info->statbuf E2_ERR_NONE())) #endif { //FIXME use error code from prior lstat to do this parent-check better *item = '\0'; if (e2_fs_lstat ( #ifdef E2_VFS &data, &info->statbuf, NULL)) #else localpath, &info->statbuf)) #endif { errval = E2DREAD_NS; goto errexit; } else { //keep going if just this one item is N/A GList *tmp = member; member = member->next; *list = g_list_delete_link (*list, tmp); if (member != NULL) goto nextmember; DEALLOCATE (FileInfo, info); break; } } #ifdef S_IFLNK if (S_ISLNK(info->statbuf.st_mode) && !S_ISDIR (info->statbuf.st_mode)) { struct stat statbuf; if (!e2_fs_stat # ifdef E2_VFS (&data, &statbuf E2_ERR_NONE()) # else (localpath, &statbuf E2_ERR_NONE()) # endif && S_ISDIR (statbuf.st_mode)) { # ifndef E2_VFS printd (DEBUG, "%s is a link not flagged as a dir", localpath); # endif info->statbuf.st_mode |= E2_DIRLNK; if (mingle) info->statbuf.st_mode |= E2_MINGLEDIRS; //remember to NOT sort 1st } } #endif if (S_ISDIR (info->statbuf.st_mode)) { #ifdef S_IFLNK if (S_ISLNK (info->statbuf.st_mode)) { //probably this never happens # ifndef E2_VFS printd (DEBUG, "%s is a dir flagged as a link", localpath); # endif info->statbuf.st_mode |= E2_DIRLNK; //remember this item is link to a dir } #endif if (mingle) info->statbuf.st_mode |= E2_MINGLEDIRS; //remember to NOT sort 1st } //any truncation here (>NAME_MAX+1) means it will never be able to be stat'd again g_strlcpy (info->filename, (gchar *)member->data, sizeof (info->filename)); #ifdef E2_VFSTMP populate info->dir if needed #endif g_free (member->data); member->data = info; } return TRUE; errexit: //FIXME warning e2_list_free_with_data (&member); //get rid of remaining string data g_list_foreach (*list, (GFunc) _e2_fileview_cleaninfo, NULL); g_list_free (*list); *list = GINT_TO_POINTER (errval); return FALSE; } /** @brief update all items' info in the treeview associated with @a view After updating the underlying liststore contents, item(s) are (re) selected as appropriate, and the view is scrolled to the relevant position This is used for refreshes and cd's It can be a thread func - though not yet thread-safe Directory whose content we want is named in view->dir (utf-8) Current store content is cleared after successful preliminary things, but before new data is added, of course Active filter(s) are applied, but no sorting. If relevant, the text-color for selected items is updated here, rather than in the selection function (too many un-necessary repeats, there) This function is not necessarily thread-safe Assumes BGL is closed upon arrival here NOTE sets view->total_items, and view->row if needed @param view data structure for the view to which the file list belongs @return TRUE if all was completed */ gboolean e2_fileview_prepare_list (ViewInfo *view) { GtkTreeModel *mdl; GtkTreeSortable *sortable; GList *entries; //list of FileInfo's created when reading dir // printd (DEBUG, "e2_fileview_prepare_list"); #ifdef E2_REFRESH_DEBUG printd (DEBUG, "disable refresh, prepare list"); #endif e2_filelist_disable_refresh (); //limit FAM to prevent 'self-perpetuation' of refreshing e2_fs_FAM_less_monitor_dir (view->dir); e2_window_set_cursor (GDK_WATCH); #ifdef E2_VFS VPATH data; data.spacedata = view->spacedata; #endif // GObject *debug = G_OBJECT (view->model); //#ifdef E2_NEWREFRESH if (view->listcontrols.refreshtype == E2_CHANGE) //#else // if (view->listcontrols.refreshtype == E2_REFRESH || view->listcontrols.refreshtype == E2_CHANGE) //#endif { //========== EXPERIMENTAL STORE-COPY PROCESS #ifdef STORECOPY //CHECKME rationalise code; confirm that filtering; refreshing are ok if (!strcmp (curr_view->dir, other_view->dir) && curr_view->spacedata == other_view->spacedata) { ViewInfo *otherview = (view == curr_view) ? other_view:curr_view; //match only older refreshes // if (view->refreshtype == E2_CHANGE || otherview->dir_mtime < time (NULL)) // { printd (DEBUG, "copy other store"); GtkListStore *newstore = e2_filelist_copy_store (otherview->store); g_object_ref (G_OBJECT (view->store)); //this has same effect as unref existing filtermodel gtk_tree_view_set_model (GTK_TREE_VIEW (view->treeview), NULL); //arrange to cleanup the old store at an idle time gboolean newtimer = (app.used_stores == NULL); app.used_stores = g_slist_append (app.used_stores, view->store); if (newtimer) { printd (DEBUG, "setup to clear stores later"); g_idle_add (e2_filelist_clear_old_stores, NULL); } view->store = newstore; view->model = gtk_tree_model_filter_new (GTK_TREE_MODEL (newstore), NULL); g_object_unref (G_OBJECT (newstore)); //apply current filtering view->filtered_before = FALSE; //trigger re-attachment of filter func e2_fileview_refilter_list (view); //continue the current sorting arrangements sortable = GTK_TREE_SORTABLE (newstore); //allow non-sorted display using GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID // gtk_tree_sortable_set_default_sort_func (sortable, NULL, NULL, NULL); gint array_row = (view == app.pane1.view) ? 0 : 1; gint *order_array = displayed_col_order[array_row]; gint i, m; for (i = 0; i < MAX_COLUMNS; i++) { m = order_array[i]; //get the model/'external' index if (m == FILENAME && view->extsort) gtk_tree_sortable_set_sort_func (sortable, FILENAME, (GtkTreeIterCompareFunc) e2_fileview_ext_sort, &view->sort_order, NULL); else gtk_tree_sortable_set_sort_func (sortable, m, e2_all_columns[m].sort_func, &view->sort_order, NULL); } //sort the store using the current sort column & direction gtk_tree_sortable_set_sort_column_id (sortable, view->sort_column, view->sort_order); //make it fully ours gtk_tree_view_set_model (GTK_TREE_VIEW (view->treeview), view->model); // g_object_unref (G_OBJECT (view->model)); BAD //setup for status-line counters view->total_items = otherview->total_items; view->case_sensitive_names = otherview->case_sensitive_names; view->dir_mtime = otherview->dir_mtime; view->dir_ctime = otherview->dir_ctime; e2_window_set_cursor (GDK_LEFT_PTR); //reinstate standard FAM e2_fs_FAM_more_monitor_dir (view->dir); e2_filelist_enable_refresh (); return TRUE; // } // printd (DEBUG, "time-test failed"); } // else //panes have different dirs, or some error in copying // { printd (DEBUG, "use fresh store"); #endif //def STORECOPY //============ gchar *local = D_FILENAME_TO_LOCALE (view->dir); //always dup, to avoid dirchange race //construct relevant items data for store-filler //FIXME if we can, just get relevant items for a refresh //get a fresh set of FileInfo data from source // printd (DEBUG, "grab current directory data"); #ifdef E2_VFS data.path = local; gdk_threads_leave (); entries = (GList *)e2_fs_dir_foreach (&data, #else gdk_threads_leave (); entries = (GList *)e2_fs_dir_foreach (local, //get a list of item-names #endif E2_DIRWATCH_CHECK, //this is irrelevant for non-local dirs NULL, NULL, NULL E2_ERR_NONE()); // printd (DEBUG, "Read dir function returned list @ %x", entries); gdk_threads_enter (); if (E2DREAD_FAILED (entries) || !_e2_fileview_make_all_infos (local, &entries)) { g_free (local); //any cleanup of entries (possibly with mixed data types) done downstream goto cleanup2; } // printd (DEBUG, "current directory data loaded"); LISTS_LOCK gboolean abort = (view->listcontrols.cd_requested || view->listcontrols.newpath != NULL); //different dir wanted now LISTS_UNLOCK if (abort) { //stop now g_free (local); goto cleanup; } //even when FAM is working, some things (e.g. treedialog) must poll for //changes, so set times reflecting the completed poll struct stat sb; #ifdef E2_VFS data.path = local; if (e2_fs_stat (&data, &sb E2_ERR_NONE())) //through links #else if (e2_fs_stat (local, &sb E2_ERR_NONE())) //through links #endif { printd (WARN, "Unable to stat directory: %s", view->dir); g_free (local); goto cleanup; } g_free (local); //FIXME update these only after store is populated successfully view->case_sensitive_names = E2FSCASE_UNKNOWN; //after cd, can be replaced by cached value view->dir_mtime = sb.st_mtime; view->dir_ctime = sb.st_ctime; /*#ifndef E2_NEWREFRESH # ifdef E2_VFSTMP if (view->spacedata != NULL && view->spacedata->monitored) { //v-dir is monitored //FIXME } else if (view->spacedata != NULL) { # endif # ifdef E2_FAM_KERNEL if (view->refreshtype == E2_REFRESH) e2_fs_FAM_clean_reports (view->dir); / * prior reports about the dir are now gone. Henceforth any report about the dir or an item in it will trigger a new refresh, even if the change that caused the report is shown in this refresh. The alternative is to do this at the end, and lose any change after an item is processed in this refresh * / # endif //which fam # ifdef E2_VFSTMP } # endif #endif //ndef E2_NEWREFRESH */ // mdl = GTK_TREE_MODEL (view->store); sortable = GTK_TREE_SORTABLE (view->store); //remember the current sorting arrangements gtk_tree_sortable_get_sort_column_id (sortable, &view->sort_column, &view->sort_order); //turn off model sorting before any rows are added //CHECKME do we really benefit from turning off sorting when the //view is detached from the model? gtk_tree_sortable_set_sort_column_id (sortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, view->sort_order); //============ //#ifdef STORECOPY // } //end of block for experimental stuff //#endif //============ } else //doing a window recreation { //create new store with current items, in case content-format is changed entries = NULL; FileInfo *infoptr, *newinfo; GtkTreeIter iter; mdl = GTK_TREE_MODEL (view->store); if (gtk_tree_model_get_iter_first (mdl, &iter)) { do { gtk_tree_model_get (mdl, &iter, FINFO, &infoptr, -1); #ifdef USE_GLIB2_10 newinfo = (FileInfo *) g_slice_alloc (sizeof(FileInfo)); g_memmove (newinfo, infoptr, sizeof(FileInfo)); #else newinfo = (FileInfo *) g_memdup (infoptr, sizeof(FileInfo)); #endif entries = g_list_prepend (entries, newinfo); //faster } while (gtk_tree_model_iter_next (mdl, &iter)); entries = g_list_reverse (entries); } } gboolean retval; GtkListStore *store = e2_filelist_fill_store (entries, view); if (store != NULL) { //there was a valid store created // printd (DEBUG, "new liststore ready"); //can't re-assign the filter model to new store, must replace model //make sure the store and base model stay alive after the filtermodel goes g_object_ref (G_OBJECT (view->store)); view->model = gtk_tree_model_filter_new (GTK_TREE_MODEL (store), NULL); gtk_tree_view_set_model (GTK_TREE_VIEW (view->treeview), view->model); g_object_unref (G_OBJECT (store)); g_object_unref (G_OBJECT (view->model)); //arrange to cleanup the old store at an idle time gboolean newtimer = (app.used_stores == NULL); app.used_stores = g_slist_append (app.used_stores, view->store); if (newtimer) { printd (DEBUG, "setup to clear stores later"); g_idle_add (e2_filelist_clear_old_stores, NULL); } view->store = store; sortable = GTK_TREE_SORTABLE (store); //allow non-sorted display using GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID // gtk_tree_sortable_set_default_sort_func (sortable, NULL, NULL, NULL); gint array_row = (view == &app.pane1.view) ? 0 : 1; gint *order_array = displayed_col_order[array_row]; gint i, m; for (i = 0; i < MAX_COLUMNS; i++) { m = order_array[i]; //get the model/'external' index if (m == FILENAME && view->extsort) gtk_tree_sortable_set_sort_func (sortable, FILENAME, (GtkTreeIterCompareFunc) e2_fileview_ext_sort, &view->sort_order, NULL); else gtk_tree_sortable_set_sort_func (sortable, m, e2_all_columns[m].sort_func, &view->sort_order, NULL); } // printd (DEBUG, "resort new data"); //sort the view using the current sort column & direction gtk_tree_sortable_set_sort_column_id (sortable, view->sort_column, view->sort_order); // printd (DEBUG, "resort finished"); //setup for status-line counters //FIXME if entries list can be just the changed items //entries count adjusted for ".." ("." filtered from list) view->total_items = g_list_length (entries) - 1; //may be -1 //fuse-created dirs may not have any ".." entry, but we need one gboolean addup = FALSE; FileInfo *infoptr; GtkTreeIter iter; //sorting will have placed any existing ".." at start of store //FIXME neater to do this in read-dir function if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter)) { gtk_tree_model_get (GTK_TREE_MODEL (store), &iter, FINFO, &infoptr, -1); if (strcmp (infoptr->filename, "..")) addup = TRUE; } else addup = TRUE; if (addup) { #ifdef USE_GLIB2_10 infoptr = (FileInfo *) g_slice_alloc (sizeof (FileInfo)); // infoptr = ALLOCATE (FileInfo); #else infoptr = ALLOCATE (FileInfo); #endif CHECKALLOCATEDFATAL (infoptr); g_strlcpy (infoptr->filename, "..", sizeof (infoptr->filename)); #ifdef E2_VFSTMP //FIXME properly handle root of v-dir #endif #ifdef E2_VFS VPATH fake = { G_DIR_SEPARATOR_S, view->spacedata }; e2_fs_stat (&fake, &infoptr->statbuf E2_ERR_NONE()); #else e2_fs_stat (G_DIR_SEPARATOR_S, &infoptr->statbuf E2_ERR_NONE()); #endif gtk_list_store_insert_with_values (store, &iter, 0, FILENAME, "../", SIZE, "0", PERM, "drwxrwxrwx", //CHECKME translate ? OWNER, "", GROUP, "", MODIFIED, "", ACCESSED, "", CHANGED, "", NAMEKEY, g_utf8_collate_key ("..", -1), FINFO, infoptr, FORECOLOR, e2_option_color_get ("color-ft-dir"), // VISIBLE, TRUE, -1); view->total_items++; } view->filtered_before = FALSE; //trigger re-attachment of filter func e2_fileview_refilter_list (view); //FIXME view->dir_mtime = sb.st_mtime; // view->dir_ctime = sb.st_ctime; retval = TRUE; } else //liststore creation error { // FIXME warn user printd (WARN, "liststore creation failed for %s", view->dir); cleanup: if (entries != NULL) g_list_foreach (entries, (GFunc) _e2_fileview_cleaninfo, NULL); cleanup2: retval = FALSE; } if (entries != NULL) g_list_free (entries); //data in the list is in liststore pointers, freed later e2_window_set_cursor (GDK_LEFT_PTR); #ifdef E2_REFRESH_DEBUG printd (DEBUG, "enable refresh, prepare list"); #endif //reinstate standard FAM e2_fs_FAM_more_monitor_dir (view->dir); e2_filelist_enable_refresh (); // printd (DEBUG, "end prepare list for %s", view->dir); return retval; } /* * @brief change-directory completion-watch timer-function @param data pointer to data relating to the cd being monitored @return FALSE when ready to shutdown timer */ /* gboolean e2_fileview_cd_watch (E2_CDwatch *data) { if (app.timers[CDWAIT_T] > 0) return TRUE; //the cd is still blocked //can't use E2_PaneRuntime* directly, due to include circularity problem E2_PaneRuntime *rt = (data->view == curr_view) ? curr_pane : other_pane; if (!cd_blocked && g_str_has_prefix (rt->path, data->newpath)) { *data->completed_flag = CD_SUCCESS; g_free (data->newpath); DEALLOCATE (E2_CDwatch, data); return FALSE; } //stop after 100 tries = 10 seconds //FIXME use a timeout mechanism consistent with the one for non-local dir reading if (++data->repeats == 100) { *data->completed_flag = CD_TIMEOUT; g_free (data->newpath); DEALLOCATE (E2_CDwatch, data); return FALSE; } return TRUE; } */ /** @brief change-directory timer-function This can be called for both dirs, at session start at least It handles any requested cd for either pane @param data pointer to cd data for the change @return TRUE if prior cd is not complete, or FALSE to shutdown timer */ gboolean e2_fileview_cd_manage (E2_Listman *data) { E2_Listman *workdata; pthread_t thisID; pthread_attr_t attr; gboolean one_active, flag; guint fails = 0; LISTS_LOCK flag = app.pane1.view.listcontrols.cd_requested || app.pane2.view.listcontrols.cd_requested; data->cd_requested = TRUE; //mark this one to block any repeats printd (DEBUG, "managing cd to %s", data->newpath); LISTS_UNLOCK if (flag) { //this is a timer callback which escaped the termination process below //or (unlikely) is for the former-other pane and happened to arrive here //at a useless moment printd (NOTICE, "aborted cd timer, another one has precedence"); //, data->timer); return FALSE; } //immediately stop the timer which called here, to prevent _any_ chance of //parallel instances while (g_source_remove_by_user_data (data)) { printd (DEBUG, "shut down a cd-timer for this pane"); } e2_utf8_set_name_conversion_if_requested (); //#ifndef E2_STATUS_DEMAND // e2_window_disable_status_update (); //prevents bad behaviour ?? //#endif //in case the active pane changes while we're here, get a snapshot one_active = (curr_view == &app.pane1.view); do { //try active pane first workdata = (one_active) ? &app.pane1.view.listcontrols : &app.pane2.view.listcontrols; LISTS_LOCK if (workdata->cd_requested) { if (!( g_atomic_int_get (&workdata->cd_working) || g_atomic_int_get (&workdata->refresh_working) #ifdef E2_STATUS_BLOCK || g_atomic_int_get (&app.status_working) #endif )) { LISTS_UNLOCK printd (NOTICE, "Changing active pane to %s", workdata->newpath); pthread_attr_init (&attr); pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); if (pthread_create (&thisID, &attr, (gpointer(*)(gpointer))_e2_fileview_change_dir, workdata) == 0) { printd (DEBUG,"change-dir-thread (ID=%lu) started", thisID); //do this here too in case the thread gets underway later LISTS_LOCK workdata->cd_requested = FALSE; LISTS_UNLOCK } else fails++; pthread_attr_destroy (&attr); } else //busy if ( (one_active && g_atomic_int_get (&app.pane2.view.listcontrols.cd_working)) ||(!one_active && g_atomic_int_get (&app.pane1.view.listcontrols.cd_working)) ) { // if ( (one_active && app.pane2.view.listcontrols.timer == 0) // ||(!one_active && app.pane1.view.listcontrols.timer == 0) ) // { //there is no active timer for the other pane (should never happen) printd (DEBUG,"active pane blocakge, restarting timer"); //data->timer = g_timeout_add_full (G_PRIORITY_HIGH, 90, (GSourceFunc) e2_fileview_cd_manage, data, NULL); // } LISTS_UNLOCK return FALSE; //wait for a timer to call back here } else { LISTS_UNLOCK } } else { LISTS_UNLOCK } //then try inactive pane workdata = (one_active) ? &app.pane2.view.listcontrols : &app.pane1.view.listcontrols; LISTS_LOCK if (workdata->cd_requested) { if (!( g_atomic_int_get (&workdata->cd_working) || g_atomic_int_get (&workdata->refresh_working) #ifdef E2_STATUS_BLOCK || g_atomic_int_get (&app.status_working) #endif )) { LISTS_UNLOCK printd (NOTICE, "Changing INactive pane to %s", workdata->newpath); pthread_attr_init (&attr); pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); if (pthread_create (&thisID, &attr, (gpointer(*)(gpointer))_e2_fileview_change_dir, workdata) == 0) { printd (DEBUG,"change-dir-thread (ID=%lu) started", thisID); LISTS_LOCK workdata->cd_requested = FALSE; LISTS_UNLOCK } else fails++; pthread_attr_destroy (&attr); } else //busy if ( (one_active && g_atomic_int_get (&app.pane1.view.listcontrols.cd_working)) ||(!one_active && g_atomic_int_get (&app.pane2.view.listcontrols.cd_working)) ) { // if ( (one_active && app.pane1.view.listcontrols.timer == 0) // ||(!one_active && app.pane2.view.listcontrols.timer == 0) ) // { //there is no active timer for the other pane (should never happen) printd (DEBUG,"INactive pane blocakge, restarting timer"); //data->timer = g_timeout_add_full (G_PRIORITY_HIGH, 90, (GSourceFunc) e2_fileview_cd_manage, data, NULL); // } LISTS_UNLOCK return FALSE; //wait for a timer to call back here } else { LISTS_UNLOCK } } else { LISTS_UNLOCK } LISTS_LOCK flag = app.pane1.view.listcontrols.cd_requested || app.pane2.view.listcontrols.cd_requested; LISTS_UNLOCK } while (flag && fails < 2); if (flag && fails == 2) { // gdk_threads_enter (); // gchar *msg = ; // e2_output_print_error (msg, FALSE); // gdk_threads_leave (); printd (WARN, "CD-thread creation error"); } //#ifndef E2_STATUS_DEMAND // printd (DEBUG,"resume status checking"); // e2_window_enable_status_update (-1); //#endif // LISTS_LOCK // data->timer = 0; //now it's ok to allow other timers // LISTS_UNLOCK return FALSE; } /** @brief get new directory contents for a view This is a thread-function. Assumes that BGL ("big gtk lock", gdk mutex) is off/open @param cddata data structure with parameters for the view to be changed @return NULL always */ static gpointer _e2_fileview_change_dir (E2_Listman *cddata) { E2_DirHistoryEntry *hist_entry; #ifdef E2_REFRESH_DEBUG printd (DEBUG, "disable refresh, _e2_fileview_change_dir"); #endif e2_filelist_disable_one_refresh ((cddata == &app.pane1.view.listcontrols) ? PANE1:PANE2); //suspend overlapping refreshes ASAP e2_utils_block_thread_signals (); //block all allowed signals to this thread LISTS_LOCK gchar *newpath = cddata->newpath; //duplicate in case user cd's again, free it if cd fails //with repeated fast cd's, sometimes we arrive here with a NULL path still in place if (newpath == NULL) { LISTS_UNLOCK e2_filelist_enable_one_refresh ((cddata == &app.pane1.view.listcontrols) ? PANE1:PANE2); return NULL; } cddata->newpath = NULL; //from now, non-NULL signals a new cd request //CHECKME ok to overlap calls, hence defer setting these flags ? g_atomic_int_set (&cddata->cd_working, 1); //prevent re-entrant cd's cddata->cd_requested = FALSE; //this may preceed the change in e2_fileview_cd_manage() ViewInfo *view = cddata->view; gboolean history = cddata->history; gboolean hook = cddata->hook; LISTS_UNLOCK printd (DEBUG, "_e2_fileview_change_dir: %s", newpath); /* #ifdef E2_VFSTMP //FIXME better check for usable dir //also, handle vfs flags and local dirs that only use async IO //some OS have MNT_SYNCHRONOUS and MNT_ASYNC in a statfs.f_flags if (view->spacedata == NULL) { //not a v-dir #endif if (e2_fs_dir_is_native (newpath, FALSE)) CHECKME parent-checking too? view->dirtype = FS_LOCAL; else view->dirtype = FS_FUSE; #ifdef E2_VFSTMP } #endif */ E2_PaneRuntime *rt = (view == curr_view) ? curr_pane : other_pane; #ifdef E2_VFSTMP //FIXME check that e2_fs_cd_isok works for v-dirs #endif gdk_threads_enter (); //e2_fs_cd_isok () may display message if (! e2_fs_cd_isok (newpath E2_ERR_NONE())) //can't go there CHECKME no delay when non-mounted ? { gdk_threads_leave (); g_free (newpath); // LISTS_LOCK g_atomic_int_set (&cddata->cd_working, 0); // LISTS_UNLOCK e2_filelist_enable_one_refresh ((view == &app.pane1.view) ? PANE1:PANE2); // e2_pane_flag_history (rt, FALSE); //zap any history update request return NULL; } #ifndef SYNC_DEBUG gdk_threads_leave (); #endif #ifndef E2_STATUS_BLOCK if (view == curr_view) e2_window_disable_status_update (); //prevents bad behaviour ?? #endif //update row history for current dir //at session-start, view->dir = "", hence no match found in list //this update will be redundant if the cd fails or is aborted, but that's ok //we do it now anyway, to take up some time in case a competing activity is underway #ifdef E2_VFSTMP //CHECKME confirm v-dir history list is still in place VPATH ddata = { view->dir, view->spacedata }; //CHECKME path encoding ? vpath *oldvdir = g_hash_table_lookup (app.vdir_cache, &ddata); if (G_LIKELY(oldvdir != NULL)) hist_entry = g_hash_table_lookup (app.dir_history, oldvdir); else hist_entry = NULL; #else hist_entry = g_hash_table_lookup (app.dir_history, view->dir); #endif if (G_LIKELY (hist_entry != NULL)) //should never fail { /*even though the sort-order and/or dir contents might change before we re-visit here, if there is a selection now, remember its start, or else remember the start of the current display*/ /*this func may be called during a window-rebuild, in which case the old dir will the same as the new one, and the selection and visible rect data will be meaningless*/ GtkTreePath *path; if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (view->treeview), 0, 0, &path, NULL, NULL, NULL)) { hist_entry->toprow = *gtk_tree_path_get_indices (path); gtk_tree_path_free (path); } else //dir is empty hist_entry->toprow = 0; GtkTreeSelection *sel = view->selection; GtkTreeModel *model; GList *selpaths = gtk_tree_selection_get_selected_rows (sel, &model); if (selpaths != NULL) { GtkTreeIter iter; path = (GtkTreePath *) selpaths->data; //get the 1st selected item's row hist_entry->selrow = *gtk_tree_path_get_indices (path); // if (0) TAGGING ALL OF SELECTION // { #ifdef TAG_ALL GList *rowpaths; if (hist_entry->selitems != NULL) g_hash_table_destroy (hist_entry->selitems); hist_entry->selitems = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); for (rowpaths = selpaths; rowpaths != NULL; rowpaths = rowpaths->next) { FileInfo *info; path = (GtkTreePath *) rowpaths->data; if (gtk_tree_model_get_iter (model, &iter, path)) { gtk_tree_model_get (model, &iter, FINFO, &info, -1); //info doesn't persist g_hash_table_insert (hist_entry->selitems, g_strdup(info->filename), GINT_TO_POINTER(1)); } gtk_tree_path_free (path); } // } // else // { //just 1st item's name #else if (gtk_tree_model_get_iter (model, &iter, path)) { FileInfo *info; gtk_tree_model_get (model, &iter, FINFO, &info, -1); g_strlcpy (hist_entry->firstname, info->filename, sizeof(hist_entry->firstname)); } g_list_foreach (selpaths, (GFunc) gtk_tree_path_free, NULL); #endif // } g_list_free (selpaths); } else { hist_entry->selrow = -1; *hist_entry->firstname = '\0'; // if (TAGGING ALL OF SELECTION) // { #ifdef TAG_ALL if (hist_entry->selitems != NULL) { g_hash_table_destroy (hist_entry->selitems); hist_entry->selitems = NULL; } #endif // } } if (view->case_sensitive_names & E2FSCASE_NOCACHE) hist_entry->case_sensitive_names = E2FSCASE_UNKNOWN; else hist_entry->case_sensitive_names = view->case_sensitive_names; } /*checks for overlapping cd/refresh/recreate now done externally //pause until any in-progess e2_fileview_prepare_list() is completed //CHECKME should this involve some test for actual-treeview-completion ? LISTS_LOCK if (cddata->listing) { LISTS_UNLOCK printd (DEBUG, "waiting for completion of another treeview update before changing dir"); app.timers[REFRESHWAIT_T] = g_timeout_add (100, (GSourceFunc) _e2_fileview_check_completion, &cddata->listing); gtk_main (); } LISTS_UNLOCK */ GHashTable *selnames = view->selected_names; //remember in case cd fails view->selected_names = NULL; //signal no need to reselect anything //now update the local path store, with backup in case of error #ifdef E2_VFS ddata.path = newpath; //CHECKME path encoding ? ddata.spacedata = view->spacedata; vpath *newvdir = g_hash_table_lookup (app.vdir_cache, &ddata); if (newvdir == NULL) { //CHECKME newvdir = ALLOCATE (vpath); #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (newvdir, FIXME;) #else if (newvdir != NULL) { #endif *newvdir = ddata; g_hash_table_insert (app.vdir_cache, newvdir, newvdir); #if !(CHECKALLOCATEDWARN) } #endif } #endif gchar *oldpath = g_strdup (view->dir); //E2_VFSTMPOK LISTS_LOCK #ifdef E2_VFS view->vdir = newvdir; // view->spacedata = NO CHANGE; #endif g_strlcpy (view->dir, newpath, sizeof (view->dir)); cddata->refreshtype = E2_CHANGE; //signal for list creator LISTS_UNLOCK #ifndef SYNC_DEBUG gdk_threads_enter (); #endif //get the replacement filelist if (!e2_fileview_prepare_list (view)) { gdk_threads_leave (); printd (WARN, "prepare list failed"); view->selected_names = selnames; #ifdef E2_VFS view->vdir = oldvdir; // view->spacedata = NO CHANGE; #endif g_strlcpy (view->dir, oldpath, sizeof (view->dir)); g_free (oldpath); g_free (newpath); // e2_pane_flag_history (rt, FALSE); //zap any history update request } else { // printd (DEBUG, "prepare list succeeded"); g_free (oldpath); //now ready to update dirline to new path (and also dirline history if appropriate) if (rt->toolbar.has_command_line) e2_command_line_change_dir (newpath, rt); //expects BGL on //scroll to approximately the position from last time view->row = 0; //default position GtkTreeModel *mdl = view->model; GtkTreeIter iter; if (gtk_tree_model_get_iter_first (mdl, &iter)) { #ifdef E2_VFSTMP //CHECKME confirm v-dir history list is still in place hist_entry = g_hash_table_lookup (app.dir_history, newvdir); #else hist_entry = g_hash_table_lookup (app.dir_history, view->dir); #endif gboolean flag = e2_option_bool_get ("select-first-item"); if (G_LIKELY(hist_entry != NULL)) { //new dir is in history list GtkTreePath *path; view->case_sensitive_names = hist_entry->case_sensitive_names; if (hist_entry->selrow > -1) { //during unpacking of an archive etc all data from last time may not be there //don't select or scroll if not enough data now (CHECKME items - 1 instead ? gint items = //if ( gtk_tree_model_iter_n_children (mdl, NULL); if (items //0,1,... 1-based <= hist_entry->selrow //0,1,... 0-based ) { hist_entry->selrow = -1; if (items > 0) { e2_fileview_focus_row (view, items-1, FALSE, FALSE, FALSE, FALSE); } } if (hist_entry->selrow > -1) { GtkTreeSelection *sel = view->selection; //FIXME share these variables GtkTreeModel *model = view->model; // if (RESELECT EVERYTHING POSSIBLE) // { #ifdef TAG_ALL do { FileInfo *info; gtk_tree_model_get (model, &iter, FINFO, &info, -1); //We only check for name, no other statbuf parameters are stored ATM if (g_hash_table_lookup(hist_entry->selitems, info->filename) != NULL) gtk_tree_selection_select_iter (sel, &iter); } while (gtk_tree_model_iter_next (model, &iter)); // } // else // { #else do { FileInfo *info; gtk_tree_model_get (model, &iter, FINFO, &info, -1); //We only check for name, no other statbuf parameters are stored ATM if (strcmp(hist_entry->firstname, info->filename) == 0) { gtk_tree_selection_select_iter (sel, &iter); break; } } while (gtk_tree_model_iter_next (model, &iter)); #endif // } gint selrow; //get the 1st selected item's row GList *selpaths = gtk_tree_selection_get_selected_rows (sel, &model); if (selpaths != NULL) { path = (GtkTreePath *) selpaths->data; /*hist_entry->*/selrow = *gtk_tree_path_get_indices (path); g_list_foreach (selpaths, (GFunc) gtk_tree_path_free, NULL); g_list_free (selpaths); } else selrow = -1; //select the 1st row we selected last time //(tuff luck if there's been content change or sort-order change) //can't use e2_fileview_focus_row () as that is bad near end of list //move back past any filtered-out iter while (/*hist_entry->*/selrow > -1 && !gtk_tree_model_iter_nth_child (mdl, &iter, NULL, /*hist_entry->*/selrow) ) /*hist_entry->*/selrow --; if (/*hist_entry->*/selrow > -1) { view->row = /*hist_entry->*/selrow; path = gtk_tree_model_get_path (mdl, &iter); if (view->row > 0) gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view->treeview), path, NULL, TRUE, 0.382, 0.0); #ifndef TAG_ALL gtk_tree_view_set_cursor (GTK_TREE_VIEW (view->treeview), path, NULL, FALSE); #endif gtk_tree_path_free (path); } else { view->row = 0; //default value } } } else if (hist_entry->toprow > 0) { //goto (approximately) the top of the visible window as it was last time while (!gtk_tree_model_iter_nth_child (mdl, &iter, NULL, hist_entry->toprow) && hist_entry->toprow > 0) hist_entry->toprow --; if (hist_entry->toprow > 0) { path = gtk_tree_model_get_path (mdl, &iter); //focus before scrolling, or else scroll doesn't work properly gtk_tree_view_set_cursor (GTK_TREE_VIEW (view->treeview), path, NULL, FALSE); gtk_tree_selection_unselect_path (view->selection, path); gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (view->treeview), path, NULL, TRUE, 0.0, 0.0); gtk_tree_path_free (path); } else e2_fileview_focus_row (view, 0, FALSE, FALSE, FALSE, FALSE); } else //last time here, it was empty e2_fileview_focus_row (view, 0, flag, FALSE, FALSE, FALSE); } else //the new dir has never been opened in this session e2_fileview_focus_row (view, 0, flag, FALSE, FALSE, FALSE); } #ifdef SYNC_DEBUG //don't wait for gtk to update at idle-time gdk_window_process_updates ( #ifdef USE_GTK2_14 gtk_widget_get_window (view->treeview), #else view->treeview->window, #endif TRUE); #endif gdk_threads_leave (); #ifdef E2_STATUS_REF extern gint statref_count; #endif if (view == curr_view #ifdef E2_STATUS_REF && statref_count == 0 #endif #ifdef E2_STATUS_BLOCK && g_atomic_int_get (&app.status_working) == 0 #endif ) e2_window_update_status_bar (NULL); //expects BGL off //ancillary tasks if (selnames != NULL) { //setup to get rid of any old hash g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) e2_fileview_treehash_free, selnames, NULL); } //this is calculated instead of passed, as circularity prevents a //E2_PaneRuntime * in any declaration in e2_fileview.h // E2_PaneRuntime *rt = (view == curr_view) ? curr_pane : other_pane; //clear any pending refresh hanging around from the old dir //CHECKME kernel reports too ? LISTS_LOCK cddata->refresh_requested = FALSE; LISTS_UNLOCK //update path data for cache etc //change FAM place ASAP #ifdef E2_VFSTMP //v-dir monitoring when supported if (view->spacedata != NULL && view->spacedata->monitored) { LISTS_LOCK oldpath = rt->path; rt->path = newpath; //DON'T FREE ME LISTS_UNLOCK //startup alteration-monitoring for the new dir //(or if need be, for its target if it's a link) //FIXME e2_vfs_?? (oldpath, rt); g_free (oldpath); } else if (view->spacedata == NULL) { //local dir #endif #ifdef E2_FAM //startup alteration-monitoring for the new dir //(or if need be, for its target if it's a link) e2_fs_FAM_change (rt->path, rt); #endif //def E2_FAM //now it's safe to update the path store for cache etc LISTS_LOCK g_free (rt->path); rt->path = newpath; LISTS_UNLOCK #ifdef E2_VFSTMP } #endif //run refocus hooklist (BGL open) if (view == curr_view) { #ifdef E2_VFSTMP //CHECKME hook for v-dir #else e2_hook_list_run (&app.hook_pane_focus_changed, curr_pane); #endif } //run cd hooklist (BGL open) if (hook #ifdef E2_VFSTMP //CHECKME mkdir dialog for v-dir's && (view->spacedata != NULL || view->spacedata->dirtype == FS_LOCAL || view->spacedata->dirtype == FS_FUSE) #endif ) { #ifdef E2_VFSTMP //FIXME hook process for v-dir's #else e2_hook_list_run (&rt->hook_change_dir, newpath); //not rt->path if that can be altered in any other thread #endif } #ifdef E2_VFSTMP //CHECKME relevant list for v-dir still in place #endif // printd (DEBUG, "_e2_fileview_change_dir update history"); //update goto-line-after-opening data #ifdef E2_VFSTMP hist_entry = g_hash_table_lookup (app.dir_history, newvdir); #else hist_entry = g_hash_table_lookup (app.dir_history, view->dir); #endif if (hist_entry == NULL) { //need a new entry for the history list hist_entry = ALLOCATE0 (E2_DirHistoryEntry); CHECKALLOCATEDWARNT (hist_entry, ); if (hist_entry != NULL) { //just set the [v]path, for now - other data set when dir is departed g_strlcpy (hist_entry->path, view->dir, sizeof(hist_entry->path)); #ifdef E2_VFS hist_entry->spacedata = view->spacedata; #endif g_hash_table_insert (app.dir_history, #ifdef E2_VFS newvdir, #else g_strdup (view->dir), #endif hist_entry); } } #ifdef E2_VFSTMP //CHECKME relevant list for v-dir still in place #endif if (history) { GList *tmp; //update the pane visited-dirs history HISTORY_LOCK //when this list was cached, independent copies of paths were needed //e2_list_update_history (newpath, &rt->opendirs, NULL, 30, TRUE); //now ... if ((tmp = g_list_find_custom (rt->opendirs, hist_entry, (GCompareFunc)e2_list_strcmp)) != NULL) //if new entry is already in history, just adjust the index rt->opendir_cur = g_list_position (rt->opendirs, tmp); else { if (rt->opendirs != NULL && rt->opendir_cur > 0) { //split off items before (i.e. visited after) current, and free them //e2_list_nth_break (&rt->opendirs, rt->opendir_cur, NULL, &rt->opendirs); //reverse order of items before and including (i.e. visited now or after) current GList *last = g_list_nth (rt->opendirs, rt->opendir_cur); GList *tail = last->next; if (tail != NULL) last->next = NULL; //not at end of list now, truncate rt->opendirs = g_list_reverse (rt->opendirs); if (tail != NULL) rt->opendirs = g_list_concat (rt->opendirs, tail); rt->opendir_cur = 0; } rt->opendirs = g_list_prepend (rt->opendirs, hist_entry); //treat as a string // if (rt->opendir_cur == 0) //CHECKME needed ? // { //list is one longer now if (g_list_length (rt->opendirs) > 40) { tmp = g_list_last (rt->opendirs); //do not clear the data, it's shared with app.dir_history rt->opendirs = g_list_delete_link (rt->opendirs, tmp); } // } // else //at least one member of list was removed before adding this one // rt->opendir_cur = 0; //and this is now current } HISTORY_UNLOCK } } #ifndef E2_STATUS_BLOCK if (view == curr_view) e2_window_enable_status_update (-1); //revert disable which prevents bad behaviour ?? #endif // LISTS_LOCK g_atomic_int_set (&view->listcontrols.cd_working, 0); // LISTS_UNLOCK #ifdef DEBUG_MESSAGES if (app.reconvert_requested) printd (DEBUG, "repoint encoding conversion funcs if necessary, after CD"); else printd (DEBUG, "NO need for encoding conversion change after CD"); #endif #ifdef E2_REFRESH_DEBUG printd (DEBUG, "enable refresh, change-dir"); #endif e2_filelist_enable_one_refresh ((view == &app.pane1.view) ? PANE1:PANE2); printd (DEBUG, "_e2_fileview_change_dir ends"); return NULL; } //#ifdef E2_NEWREFRESH /* * @brief idle function to run refresh hooklist NOTE if any registered hook-function does anything to the filesystem that triggers another refresh, then an infinite loop will happen ! So this is probably not much use ... @param view pointer to data struct for the view whose filelist was refreshed @return FALSE, to stop the callbacks */ /*static gboolean _e2_fileview_run_hooks (ViewInfo *view) { WATCH OUT FOR REENTRANT CALLING THIS ? e2_hook_list_run (&view->hook_refresh, view); return FALSE; } */ /** @brief incrementally refresh list store for @a view This is a thread function If the directory associated with @a view is no longer accessible, a suitable alternative is found and displayed. Expects BGL to be open, on arrival here @param view data structure for view being processed @return NULL (FALSE) if the refresh fails (or cd to elsewhere), pointerised 1 if it succeeds */ gpointer e2_fileview_refresh_list (ViewInfo *view) { gboolean busy; // LISTS_LOCK busy = g_atomic_int_get (&view->listcontrols.norefresh) || g_atomic_int_get (&view->listcontrols.refresh_working); // LISTS_UNLOCK #ifdef E2_STATUS_BLOCK if (!busy) busy = (view == curr_view && g_atomic_int_get (&app.status_working)); #endif if (busy) { printd (DEBUG, "BLOCKED start refresh filelist for %s", view->dir); return NULL; //refresh is disabled, or the view is still being processed from last time } printd (DEBUG, "start refresh filelist for %s", view->dir); #ifdef E2_VFSTMP if ((curr_view->spacedata == other_view->spacedata) && !strcmp (curr_view->dir, other_view->dir)) { //setup to process both views in sequence, and clear any refresh_requested flags } #endif // gchar *utf = g_strdup (view->dir); //don't clobber view->dir #ifdef E2_VFSTMP //FIXME valid path check for v-dirs //FIXME get path to goto when when dir is v-dir, may include a space-change #endif gdk_threads_enter (); //in case of downstream error message if (e2_fs_get_valid_path (&utf, TRUE E2_ERR_NONE())) g_free (utf); else { //desired path not accessible, go to somewhere that is .. gchar *msg = g_strdup_printf (_("Cannot access %s, going to %s instead"), view->dir, utf); #ifdef E2_VFSTMP //FIXME cater for any space-change message #endif e2_output_print_error (msg, TRUE); gdk_threads_leave (); E2_PaneRuntime *rt = (view == curr_view) ? curr_pane : other_pane; #ifdef E2_VFSTMP //FIXME if needed PlaceInfo *spacedata = NULL; e2_pane_change_space_pointer (rt, spacedata); #endif e2_pane_change_dir (rt, utf); g_free (utf); return GINT_TO_POINTER (2); //non-NULL to signal no more refresh needed } #ifdef E2_REFRESH_DEBUG printd (DEBUG, "disable refresh, fileview_refresh list"); #endif // LISTS_LOCK g_atomic_int_set (&view->listcontrols.refresh_working, 1); // LISTS_UNLOCK E2_ListChoice pnum = (view == &app.pane1.view) ? PANE1:PANE2; e2_filelist_disable_one_refresh (pnum); e2_window_set_cursor (GDK_WATCH); gdk_threads_leave (); #ifndef E2_STATUS_BLOCK if (view == curr_view) e2_window_disable_status_update (); //prevents bad behaviour ?? #endif gchar *local = D_FILENAME_TO_LOCALE (view->dir); //always dup, to avoid dirchange race //construct relevant items data for store-filler //get a fresh set of directory item-names from source //FIXME if possible, just get relevant items for the refresh // printd (DEBUG, "grab directory names data"); #ifdef E2_VFS VPATH ddata = { local, view->spacedata }; GList *entries = (GList *)e2_fs_dir_foreach (&ddata, #else GList *entries = (GList *)e2_fs_dir_foreach (local, #endif E2_DIRWATCH_CHECK, //this is irrelevant for non-local dirs NULL, NULL, NULL E2_ERR_NONE()); /* each ->data in entries is a FileInfo. They are cleared when no longer needed. Some go into replacement liststore data and are cleared with the store. Others that belong to unchanged items are cleared when it's confirmed that they are the same */ if (!E2DREAD_FAILED (entries)) { guint i, indx, itemcount = g_list_length (entries); GList *member; E2_RefreshInfo *modes = (E2_RefreshInfo *) #ifdef USE_GLIB2_10 //can't use ALLOCATE0 cuz > 1 item g_slice_alloc0 (sizeof (E2_RefreshInfo) * itemcount); #elif defined (USE_GLIB2_8) g_try_malloc0 (sizeof (E2_RefreshInfo) * itemcount); #else //don't use calloc() so that g_free() is ok to clean up g_try_malloc (itemcount, sizeof (E2_RefreshInfo)); #endif #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (modes, FIXME;) //properly clean entries list if allocation failed #else if (modes != NULL) { #endif #ifndef USE_GLIB2_8 memset (modes, 0, sizeof (E2_RefreshInfo) * itemcount); #endif //FIXME rationalise this with modes creation if (!_e2_fileview_make_all_infos (local, &entries)) { //cleanup of entries (possibly with mixed data types) done downstream //don't goto accessible path until next refresh, if any g_free (local); #ifdef USE_GLIB2_10 //can't use DEALLOCATE cuz > 1 item g_slice_free1 (sizeof (E2_RefreshInfo) * itemcount, modes); #else g_free (modes); #endif // LISTS_LOCK g_atomic_int_set (&view->listcontrols.refresh_working, 0); // LISTS_UNLOCK gdk_threads_enter (); e2_window_set_cursor (GDK_LEFT_PTR); gdk_threads_leave (); e2_filelist_enable_one_refresh (pnum); return NULL; } //even when FAM is working, some things (e.g. treedialog) must poll for //changes, so set times reflecting the completed poll struct stat sb; #ifdef E2_VFS VPATH data = { local, view->spacedata }; if (e2_fs_stat (&data, &sb E2_ERR_NONE())) //through links #else if (e2_fs_stat (local, &sb E2_ERR_NONE())) //through links #endif { printd (WARN, "Unable to stat directory: %s", view->dir); g_free (local); #ifdef USE_GLIB2_10 //can't use DEALLOCATE cuz > 1 item g_slice_free1 (sizeof (E2_RefreshInfo) * itemcount, modes); #else g_free (modes); #endif // LISTS_LOCK g_atomic_int_set (&view->listcontrols.refresh_working, 0); // LISTS_UNLOCK gdk_threads_enter (); e2_window_set_cursor (GDK_LEFT_PTR); gdk_threads_leave (); e2_filelist_enable_one_refresh (pnum); return GINT_TO_POINTER (3); //no more refresh needed } view->dir_mtime = sb.st_mtime; view->dir_ctime = sb.st_ctime; //construct hash for faster matching current and new items // printd (DEBUG, "create matches hash"); //do not free keys when destroying, they're not copies GHashTable *newlookup = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); i = 0; for (member = entries; member != NULL; member = member->next) { g_hash_table_insert (newlookup, ((FileInfo *)member->data)->filename, GUINT_TO_POINTER (i)); i++; } //this bit may separately apply to both views when they're the same i = 0; guint newindx = 0; gpointer pindx, orig_key; GList *updates = NULL; FileInfo *currinfoptr, *newinfoptr; GtkTreeIter iter; GtkTreeModel *mdl = GTK_TREE_MODEL (view->store); // printd (DEBUG, "check all current FileInfos"); //walk current FileInfo's, deleting as we go, logging any that are changed //(there will always be at least one entry, "..") gtk_tree_model_get_iter_first (mdl, &iter); do { loopstart: gtk_tree_model_get (mdl, &iter, FINFO, &currinfoptr, -1); if (g_hash_table_lookup_extended (newlookup, currinfoptr->filename, &orig_key, &pindx)) { indx = GPOINTER_TO_UINT (pindx); newinfoptr = (FileInfo *) g_list_nth_data (entries, indx); //FIXME vfs may not have same data as local //FIXME make this faster if (newinfoptr->statbuf.st_atime != currinfoptr->statbuf.st_atime || newinfoptr->statbuf.st_size != currinfoptr->statbuf.st_size || newinfoptr->statbuf.st_mtime != currinfoptr->statbuf.st_mtime || newinfoptr->statbuf.st_ctime != currinfoptr->statbuf.st_ctime || newinfoptr->statbuf.st_mode != currinfoptr->statbuf.st_mode || newinfoptr->statbuf.st_uid != currinfoptr->statbuf.st_uid || newinfoptr->statbuf.st_gid != currinfoptr->statbuf.st_gid ) { modes [indx].oldindx = i; modes [indx].oldmode = REFRESH_CHANGE; modes [indx].newindx = newindx++; updates = g_list_append (updates, newinfoptr); } else { //we don't want to use or add this one, later modes [indx].oldmode = REFRESH_KEEP; //so now get rid of data to prevent leakage #ifdef USE_GLIB2_10 g_slice_free1 (sizeof (FileInfo), newinfoptr); // DEALLOCATE (FileInfo, newinfoptr); #else DEALLOCATE (FileInfo, newinfoptr); #endif } i++; } else { //current item is not hashed i.e. gone from dir gdk_threads_enter (); gboolean more = gtk_list_store_remove (view->store, &iter); gdk_threads_leave (); if (more) goto loopstart; //prevent double-iter-next, i unchanged else break; } } while (gtk_tree_model_iter_next (mdl, &iter)); //tag any additions, and clean up redundant FileInfos to prevent leakage for (i = 0; i < itemcount; i++) { if (modes[i].oldmode == REFRESH_ADD) //not previously detected = default (addition) { modes [i].newindx = newindx++; //this will match the store index when created updates = g_list_append (updates, g_list_nth_data (entries, i)); } } if (updates != NULL) { // printd (DEBUG, "create replacements store"); //make store just with update information //(for each view because the indices are different) GtkListStore *newstore = e2_filelist_fill_store (updates, view); if (newstore != NULL) { //FIXME handle both views as appropriate GtkTreeSortable *sortable = GTK_TREE_SORTABLE (view->store); //speedups ... //remember the current sorting arrangements gtk_tree_sortable_get_sort_column_id (sortable, &view->sort_column, &view->sort_order); //this kills the current selection, no evident way to prevent that // g_object_ref (G_OBJECT (view->model)); // gtk_tree_view_set_model (GTK_TREE_VIEW (view->treeview), NULL); //turn off model sorting before any rows are changed/added // CHECKME does this 'unsorting' update the screen in gtk 2.16 ? gdk_threads_enter(); //CHECKME disconnecting model should be enough gtk_tree_sortable_set_sort_column_id (sortable, GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, GTK_SORT_ASCENDING); gdk_threads_leave(); //walk modes array amending/appending relevant rows (maybe both views) // printd (DEBUG, "update current store"); gchar *name, *size, *perm, *owner, *group, *mod, *access, *change, *key; GdkColor *foreground; for (i = 0; i < itemcount; i++) { if (modes[i].oldmode == REFRESH_CHANGE || modes[i].oldmode == REFRESH_ADD) { //with bad unmounting, there may be no valid data if (!gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (newstore), &iter, NULL, modes[i].newindx)) continue; gtk_tree_model_get (GTK_TREE_MODEL (newstore), &iter, FILENAME, &name, SIZE, &size, PERM, &perm, OWNER, &owner, GROUP, &group, MODIFIED, &mod, ACCESSED, &access, CHANGED, &change, NAMEKEY, &key, FINFO, &newinfoptr, FORECOLOR, &foreground, -1); if (modes[i].oldmode == REFRESH_CHANGE) { gtk_tree_model_iter_nth_child (GTK_TREE_MODEL (view->store), &iter, NULL, modes[i].oldindx); gtk_tree_model_get (GTK_TREE_MODEL (view->store), &iter, FINFO, &currinfoptr, -1); //simply replace all changeable contents /*if model not disconnected*/ gdk_threads_enter (); gtk_list_store_set (view->store, &iter, SIZE, size, PERM, perm, OWNER, owner, GROUP, group, MODIFIED, mod, ACCESSED, access, CHANGED, change, FORECOLOR, foreground, //maybe changed executable status -1); /*if model not disconnected*/ gdk_threads_leave (); //copy this so original can be cleared currinfoptr->statbuf = newinfoptr->statbuf; } else { //append //the new FileInfo will be destroyed with its store //so get a copy for the main liststore // currinfoptr = ALLOCATE (FileInfo); #ifdef USE_GLIB2_10 currinfoptr = (FileInfo *) g_slice_alloc (sizeof (FileInfo)); //currinfoptr = ALLOCATE (FileInfo); #else currinfoptr = ALLOCATE (FileInfo); #endif #if (CHECKALLOCATEDWARN) CHECKALLOCATEDWARN (currinfoptr, FIXME;) #else if (currinfoptr != NULL) { #endif *currinfoptr = *newinfoptr; //gtk >= 2.10 can handle &iter = NULL /*if model not disconnected*/ gdk_threads_enter (); gtk_list_store_insert_with_values (view->store, &iter, -1, FILENAME, name, SIZE, size, PERM, perm, OWNER, owner, GROUP, group, MODIFIED, mod, ACCESSED, access, CHANGED, change, NAMEKEY, key, FINFO, currinfoptr, FORECOLOR, foreground, -1); /*if model not disconnected*/ gdk_threads_leave (); #if !(CHECKALLOCATEDWARN) } #endif } g_free (name); g_free (size); g_free (perm); g_free (owner); g_free (group); g_free (mod); g_free (access); g_free (change); g_free (key); } } gdk_threads_enter (); /*if model not disconnected*/ //re-sort the view using the current sort column & direction gtk_tree_sortable_set_sort_column_id (sortable, view->sort_column, view->sort_order); // gtk_tree_view_set_model (GTK_TREE_VIEW (view->treeview), view->model); //CHECKME correct one ? // g_object_unref(G_OBJECT (view->model)); // gdk_threads_enter (); e2_fileview_refilter_list (view); gdk_threads_leave (); // g_idle_add ((GSourceFunc)_e2_fileview_run_hooks, view); //fixme do this when other view is processed gboolean newtimer = (app.used_stores == NULL); //FIXME not all entries member->data are cleared with newstore app.used_stores = g_slist_append (app.used_stores, newstore); if (newtimer) { printd (DEBUG, "setup to clear stores later"); g_idle_add (e2_filelist_clear_old_stores, NULL); } } //no data free, the data are same as entries list data g_list_free (updates); //the store is enough for other view ? FIXME idle ? } else //updates = NULL { //this is normally done in e2_fileview_refilter_list() // printd (DEBUG, "no need to update any item still in filelist"); if (view->row > 0) { guint visible_rows = gtk_tree_model_iter_n_children (view->model, NULL); if (view->row > visible_rows - 1) view->row = visible_rows - 1; //set to last line of list if (view->row < 0) view->row = 0; //if there was no selection, or the former selection is gone (after filter or deletion) //we want relative cursor-moves to stay in the same vicinity if (visible_rows > 0 && gtk_tree_selection_count_selected_rows (view->selection) == 0) { GtkTreePath *tpath = gtk_tree_path_new_from_indices (view->row, -1); gdk_threads_enter (); //this "marks" the item, but mere selection is not enough for this to work gtk_tree_view_set_cursor (GTK_TREE_VIEW (view->treeview), tpath, NULL, FALSE); gtk_tree_selection_unselect_path (view->selection, tpath); gdk_threads_leave (); gtk_tree_path_free (tpath); } } } view->total_items = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (view->store), NULL) - 1; //omit the ".." entry from the count g_hash_table_destroy (newlookup); //CHECKME better at idle ? #ifdef USE_GLIB2_10 //can't use DEALLOCATE cuz > 1 item g_slice_free1 (sizeof (E2_RefreshInfo) * itemcount, modes); #else g_free (modes); #endif #if !(CHECKALLOCATEDWARN) } #endif #ifdef SYNC_DEBUG //don't wait for gtk to update at idle-time gdk_threads_enter (); gdk_window_process_updates ( #ifdef USE_GTK2_14 gtk_widget_get_window (view->treeview), #else view->treeview->window, #endif TRUE); gdk_threads_leave (); #endif g_free (local); g_list_free (entries); } //else //FIXME warn user about error in reading data, with BGL closed //signal to the world ... #ifndef E2_STATUS_BLOCK if (view == curr_view) e2_window_enable_status_update (-1); //revert disable which prevents bad behaviour ?? #endif // LISTS_LOCK g_atomic_int_set (&view->listcontrols.refresh_working, 0); // LISTS_UNLOCK gdk_threads_enter (); e2_window_set_cursor (GDK_LEFT_PTR); gdk_threads_leave (); #ifdef DEBUG_MESSAGES if (app.reconvert_requested) printd (DEBUG, "repoint encoding conversion funcs if necessary, after frefresh"); else printd (DEBUG, "NO need for encoding conversion change after refresh"); #endif #ifdef E2_REFRESH_DEBUG printd (DEBUG, "enable refresh, fileview_refresh list"); #endif e2_filelist_enable_one_refresh (pnum); // printd (DEBUG, "finish refresh filelist for %s", view->dir); return GINT_TO_POINTER (1); //normal exit, no more refresh } /* #else //ndef E2_NEWREFRESH / * * @brief reload list store for @a view and re-select relevant items This records the names of currently-selected items in the view, reloads the directory contents (which clears the old selection), finds the rows in the new store that match the selected names, and selects them. If the directory associated with @a view is no longer accessible, a suitable alternative is found and displayed. Downstream functions expect gtk's BGL to be closed, hence this func does too = gdk mutex management must be handled by the caller as appropriate. @param view data structure for view being processed @return * / void e2_fileview_refresh_list (ViewInfo *view) { if (view->listing) return; //the filelist is still being processed from last time printd (DEBUG, "start refresh filelist for %s", view->dir); gchar *utf = g_strdup (view->dir); //don't clobber view->dir #ifdef E2_VFSTMP //CHECKME confirm valid path func works for v-dirs //FIXME goto dir when not mounted local #endif if (e2_fs_get_valid_path (&utf, TRUE E2_ERR_NONE())) g_free (utf); else { //desired path not accessible, go to somewhere that is .. gchar *msg = g_strdup_printf ("Cannot access %s, going to %s instead", view->dir, utf); #ifdef E2_VFSTMP //FIXME cater for space-change message #endif e2_output_print_error (msg, TRUE); #ifdef E2_VFSTMP //FIXME if needed PlaceInfo *spacedata = ??; e2_pane_change_space_pointer (rt, spacedata); #endif E2_PaneRuntime *rt = (view == curr_view ) ? curr_pane : other_pane; e2_pane_change_dir (rt, utf); g_free (utf); return; } if (view->selected_names != NULL) { //setup to get rid of any old hash g_idle_add_full (G_PRIORITY_LOW, (GSourceFunc) e2_fileview_treehash_free, view->selected_names, NULL); } //log the names of selected items in this view // printd (DEBUG, "log any selected items"); view->selected_names = e2_fileview_log_selected_names (view); //clear and refill the store //FIXME support incremental updates // printd (DEBUG, "replace list content"); view->refreshtype = E2_REFRESH; if (!e2_fileview_prepare_list (mounted, view)) return; GtkTreeModel *mdl = gtk_tree_view_get_model (GTK_TREE_VIEW (view->treeview)); GtkTreeIter iter; if (gtk_tree_model_get_iter_first (mdl, &iter)) { //restore scroll positions / * Not clear why this works, but .... the adjustments' values are unchanged, but the sw doesn't know about them unless signals are issued * / GtkAdjustment *adj; adj = gtk_tree_view_get_vadjustment (GTK_TREE_VIEW (view->treeview)); gtk_adjustment_value_changed (adj); // adj = gtk_tree_view_get_hadjustment (GTK_TREE_VIEW (view->treeview)); // gtk_adjustment_value_changed (adj); } if (view->selected_names != NULL) { // printd (DEBUG, "reselect items"); // clear out any old selection // gtk_tree_selection_unselect_all (view->selection); //try to reselect items, then at least clean the hash e2_fileview_reselect_names (view); // printd (DEBUG, "finish reselection for %s", view->dir); } printd (DEBUG, "finish refresh filelist for %s", view->dir); } #endif //ndef E2_NEWREFRESH */ /** @brief create filelist for the pane related to @a view This creates the backend list store, and related scrolled window containing a treeview @param view runtime data for view being processed @return a scrolled window widget */ GtkWidget *e2_fileview_create_list (ViewInfo *view) { //create liststore framework for the pane view->store = e2_filelist_make_store (); //setup column-order array gint array_row = (view == &app.pane1.view) ? 0 : 1; e2_fileview_translate_cols_array (stored_col_order[array_row], displayed_col_order[array_row], MAX_COLUMNS); gint *width_array = col_width_store[array_row]; gint *order_array = displayed_col_order[array_row]; //element of cache-name strings gchar *panename = (view == &app.pane1.view) ? app.pane1.name : app.pane2.name; gchar *fontstr = (e2_option_bool_get ("custom-list-font")) ? e2_option_str_get ("list-font") : NULL; //NULL will cause default font /*now create tree view column order is determined by array displayed_col_order[]. columns are named "0" to "7" for later searching */ gchar id[] = {'0', '\0'}; //for column header titles, used to check which column //unfiltered model needed for getting the sortable GtkTreeModel *mdl = GTK_TREE_MODEL (view->store); view->model = gtk_tree_model_filter_new (mdl, NULL); GtkWidget *tvw = gtk_tree_view_new_with_model (view->model); g_object_unref (G_OBJECT (view->store)); //kill the ref from model creation g_object_unref (G_OBJECT (view->model)); //kill the ref from treeview creation view->filtered_before = FALSE; //start with fresh filters //save it for others to use view->treeview = tvw; GtkTreeSortable *sortable = GTK_TREE_SORTABLE (view->store); //allow unsorted display using GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID // gtk_tree_sortable_set_default_sort_func (sortable, NULL, NULL, NULL); //set general treeview properties gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (tvw), TRUE); gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (tvw), //useless with managed backcolors e2_option_bool_get ("panes-hinted")); //#ifdef USE_GTK2_10 //FIXME use this if it's as functional as E2_ALTLEFTMOUSE, seems not so // gtk_tree_view_set_rubber_banding (GTK_TREE_VIEW (tvw), TRUE); //"drag-selection" //#endif //cache the view's selection pointer CHECKME maybe a problem with filtermodel GtkTreeSelection *sel = view->selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tvw)); gtk_tree_selection_set_mode (sel, GTK_SELECTION_MULTIPLE); #ifdef E2_SELTXT_RECOLOR if (e2_option_bool_get ("selectedtext-colorchange")) gtk_tree_selection_set_select_function (sel, (void *) _e2_fileview_selection_func, view, NULL); #endif /* //when both panes are "same", make the slave-pane columns un-draggable gboolean fixed_order = ( (e2_option_bool_get ("pane1-uses-other") && view == app.pane1.view) || (e2_option_bool_get ("pane2-uses-other") && view == app.pane2.view) ); */ gint i; //set columns' properties for (i = 0; i < MAX_COLUMNS; i++) { gint m; guint show_this; gchar *option_name; E2_OptionSet *set; GtkCellRenderer *renderer; GtkTreeViewColumn *column; m = order_array[i]; //get the model/'external' index gtk_tree_sortable_set_sort_func (sortable, m, e2_all_columns[m].sort_func, &view->sort_order, NULL); //get the column visibility option_name = g_strdup_printf ("%s-show-column%d", panename, m); // e2_option_bool_get defaults to FALSE set = e2_option_get (option_name); show_this = (set != NULL && e2_option_bool_get_direct (set)) ? 1:0; g_free (option_name); //create header widget, to support styling and avoid gtk bug with arrows GtkWidget *headerbox = gtk_hbox_new (FALSE, 1); GtkWidget *label = gtk_label_new (gettext(e2_all_columns[m].title)); //same title for each pane //expand = TRUE prevents col width < title width + sort indicator (if any) //CHECKME padding = 0 when label expand = FALSE (or else it crashes when col is too narrow to show it all) gtk_box_pack_start (GTK_BOX(headerbox), label, FALSE, FALSE, 0); gtk_widget_show_all (headerbox); //keep the arrow hidden for now if (m == FILENAME) //remember the filename column label in case we need to use it for active-pane flagging view->name_label = GTK_LABEL (label); //set the current sort arrow GtkArrowType arrow; if (m == view->sort_column && view->sort_order == GTK_SORT_DESCENDING) arrow = GTK_ARROW_UP; else arrow = GTK_ARROW_DOWN; view->sort_arrows[m] = gtk_arrow_new (arrow, GTK_SHADOW_IN); //this doesn't show the indicator at the end of the header ! (and different exp, fill don't help) gtk_box_pack_end (GTK_BOX(headerbox), view->sort_arrows[m], FALSE, FALSE, 0); renderer = gtk_cell_renderer_text_new (); g_object_set (G_OBJECT (renderer), "yalign", 0.0, "ypad", 0, //supposedly default, but this makes the rows closer "font", fontstr, //this property not stored in model, as it doesn't change on the fly ... NULL); // gtk_cell_renderer_text_set_fixed_height_from_font // (GTK_CELL_RENDERER_TEXT (renderer), 1); //right-align the size column only if (m == SIZE) g_object_set (G_OBJECT (renderer), "xalign", 1.0, "xpad", 0, NULL); id[0] = m+'0'; column = gtk_tree_view_column_new_with_attributes ( id, //this is for seaching & sorting, displayed title is in widget label renderer, "text", m, "foreground-gdk", FORECOLOR, "cell-background-gdk", BACKCOLOR, //this column is generally NULL'd, except for drop-target highlighting NULL); gint thiswidth = width_array[m]; //doesn't like 0 widths set by gtk when a col is hidden if (thiswidth == 0) thiswidth = e2_all_columns[m].size; /* It would be nice to allocate spare treeview space to the filename column (TRUE "expand" property) instead of the last-shown column. However we then also need to manage the column's "minimum" property so that the column can't be resized out of existence. Sadly there's no API for detecting only manual (dragged) column-width changes, or other reasonable work-around that we can find, to allow the minimum to be dynamically revised in accord with user's changes */ g_object_set (column, "sizing", GTK_TREE_VIEW_COLUMN_FIXED, "resizable", TRUE, "fixed-width", thiswidth, "visible", show_this, "clickable", TRUE, "reorderable", TRUE, "widget", headerbox, NULL); gtk_tree_view_append_column (GTK_TREE_VIEW (tvw), column); #ifdef EDIT_INPLACE if (m == FILENAME) { //setup for in-cell name-changing g_object_set (G_OBJECT (renderer), "editable", TRUE, NULL); g_signal_connect (G_OBJECT (renderer), "edited", G_CALLBACK (_e2_fileview_name_edited_cb), view); g_signal_connect (G_OBJECT (renderer), "editing-started", G_CALLBACK (_e2_fileview_edit_start_cb), view); g_signal_connect (G_OBJECT (renderer), "editing-canceled", G_CALLBACK (_e2_fileview_edit_cancel_cb), view); } #endif //workaround for distinguishing header clicks from drags //uses 'private' button, cuz (headerbox with event-box) doesn't work #ifdef USE_GTK2_14 GtkWidget *button = gtk_widget_get_ancestor (headerbox, GTK_TYPE_BUTTON); # ifdef DEBUG_MESSAGES if (button == NULL) printd (ERROR, "missing column-header button"); else { # endif g_signal_connect (G_OBJECT (button), "button-press-event", G_CALLBACK (_e2_fileview_column_header_buttonpress_cb), view); g_signal_connect (G_OBJECT (button), "button-release-event", G_CALLBACK (_e2_fileview_column_header_buttonrel_cb), view); # ifdef DEBUG_MESSAGES } # endif #else g_signal_connect (G_OBJECT (column->button), "button-press-event", G_CALLBACK (_e2_fileview_column_header_buttonpress_cb), view); g_signal_connect (G_OBJECT (column->button), "button-release-event", G_CALLBACK (_e2_fileview_column_header_buttonrel_cb), view); #endif g_signal_connect (G_OBJECT (column), "clicked", G_CALLBACK (_e2_fileview_column_header_clicked_cb), view); } //setup inital sort parameters, ascending name col // view->sort_order = GTK_SORT_ASCENDING; // gtk_widget_show (view->sort_arrows[0]); //inital sort parameters set from cache gtk_tree_sortable_set_sort_column_id (sortable, view->sort_column, view->sort_order); gtk_widget_show (view->sort_arrows[view->sort_column]); #ifndef E2_MOUSECUSTOM //FIXME do this once, not for both panes button2updir = e2_option_bool_get ("button2-updir"); #endif g_signal_connect (tvw, "columns-changed", G_CALLBACK(_e2_fileview_col_change_cb), view); //by default, type-ahead searching is enabled on column 0 //disable default search if vim etc bindings used, (then need Ctrl f to start search) ? //gtk_tree_view_set_enable_search (GTK_TREE_VIEW (tvw), FALSE); gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW (tvw), (GtkTreeViewSearchEqualFunc)_e2_fileview_match_filename, view, NULL); //DnD connections // gtk_drag_source_set (tvw, GDK_BUTTON1_MASK, target_table, n_targets, //-1 if XDS is last, // GDK_ACTION_COPY); //can't use 2 of this fn, it seems gtk_drag_source_set (tvw, GDK_BUTTON1_MASK | GDK_BUTTON2_MASK, target_table, n_targets, //-1, //the last target (XDS) is supported for dest only GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK); gtk_drag_dest_set (tvw, GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP, target_table, n_targets, GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK); g_signal_connect (G_OBJECT (tvw), "drag-begin", G_CALLBACK (e2_dnd_drag_begin_cb), view); g_signal_connect (G_OBJECT (tvw), "drag-data-get", G_CALLBACK (e2_dnd_drag_data_get_cb), view); g_signal_connect (G_OBJECT (tvw), "drag-motion", G_CALLBACK (e2_dnd_drag_motion_cb), view); g_signal_connect (G_OBJECT (tvw), "drag-leave", G_CALLBACK (e2_dnd_drag_leave_cb), view); //needed if GTK_DEST_DEFAULT_DROP not set, above // g_signal_connect (G_OBJECT (tvw), "drag-drop", // G_CALLBACK(e2_dnd_drag_drop_cb), view); g_signal_connect (G_OBJECT (tvw), "drag-data-received", G_CALLBACK(e2_dnd_drag_data_received_cb), view); //FIXME do these once only // atom_text_uri_list = gdk_atom_intern (target_table[0,0], FALSE); // atom_text_plain = gdk_atom_intern (target_table[1,0], FALSE); // atom_XdndDirectSave0 = gdk_atom_intern (target_table[2,0], FALSE); // g_signal_connect (G_OBJECT (tvw), "drag-data-delete", // G_CALLBACK (e2_dnd_drag_delete_cb), view); //for E2_ALTLEFTMOUSE, double-clicks are handled in the button-press callback //BUT gtk issues this signal when is pressed during a type-ahead //search, which we want g_signal_connect (tvw, "row-activated", G_CALLBACK (_e2_fileview_row_activated_cb), view); //these signals are used whether or not E2_MOUSECUSTOM applies //NOTE also emitted when a column-header-separator is pressed/released g_signal_connect (G_OBJECT (tvw), "button-press-event", G_CALLBACK (_e2_fileview_button_press_cb), view); g_signal_connect (G_OBJECT (tvw), "button-release-event", G_CALLBACK (_e2_fileview_button_release_cb), view); //see register_all // e2_fileview_register_pointerbindings (tvw, view); g_signal_connect (G_OBJECT (tvw), "key-press-event", G_CALLBACK (_e2_fileview_key_press_cb), view); //includes translation g_signal_connect (G_OBJECT (tvw), "key-release-event", G_CALLBACK (e2_utils_key_translate_cb), NULL); // e2_fileview_register_keybindings (tvw, view); see register_all // g_signal_connect (G_OBJECT (tvw), "popup-menu", handled by keypress cb now // G_CALLBACK (_e2_fileview_popup_cb), view); g_signal_connect (G_OBJECT (tvw), "cursor-changed", G_CALLBACK (_e2_fileview_cursor_change_cb), view); GtkWidget *sw = e2_widget_get_sw (GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, GTK_SHADOW_ETCHED_IN); gtk_container_add (GTK_CONTAINER (sw), tvw); gtk_widget_show (tvw); gtk_widget_show (sw); return sw; }