/* $Id$ Copyright (C) 2004-2011 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_filelist.c @brief directory content liststore functions This file contains functions related to creation, filling and emptying a liststore of directory content data, and funcs for the associated model. Also filtering of store content, and other related utilties. */ /** \page refresh filelist refreshing \section automatic automatic refreshing Automatic refreshing of filelists occurs if the value of configuration option "auto-refresh" is TRUE. Core infrastructure is a timer - app.timers[DIRTYCHECK_T]. This is used for either 'default' change-monitoring (a polling process), or enhanced monitoring (using gamin, or a relevant kernel capability) if such is built-in and actually working. The default mechanism for this is periodic polling of the mtime and ctime of the directories currently displayed in the filelists. If enhanced monitoring is used, suitable polling is handled by the relevant backend. The kernel-backend, or the poller (via e2_fs_FAM_check_dirty()), sets dirty-flag(s) for filelist(s), as appropriate. Then for both panes: If its dirty-flag is set: Clear its dirty-flag, ready for downstream use again If function-argument (userdata) is non-NULL (the usual for direct calls) or a repeat-count (via _e2_filelist_refresh_ok()) is suitable: Set refresh-requested flag for the pane Then if either refresh-requested flag is now set (maybe from elsewhere, see below): If timer REFRESHBEGIN_T is active, abort it Start timer REFRESHBEGIN_T with callback to _e2_filelist_refresh_manage() _e2_filelist_refresh_manage() polls both panes' refresh-requested flags, and, if and when not blocked by some other process that affects the pane's content, initiates (and joins to) a thread to refresh the relevant filelist. This process iterates until all requests have been handled, so there can be > 1 refresh before the function returns. With enhanced monitoring, the same interface is used to gather information about filelists and the config file, if the latter is being monitored. However any config-file-change is merely flagged, ready for processing by the corresponding process. \section manual manual refreshing Either or both panes' refresh-requested flag can be set at any time via a call to e2_filelist_request_refresh(). If the immediate flag is not set, that request will be acted upon next time e2_filelist_check_dirty() is called, manually or as a timer callback. e2_filelist_check_dirty() can be called manually, usually with a non-NULL argument. This will check for and refresh pane(s) flagged as 'dirty' or 'refresh-requested'. /section blocking refresh blocking Refreshing of either or both panes is contstrained by corresponding ref-counts, which can be increased or decreased by calls to e2_filelist_[dis|en]able_refresh() and/or to e2_filelist_[dis|en]able_one_refresh() Refreshing of either pane is blocked while either pane is still being refreshed as a result of a prior update \section config configuration file changes Automatic refreshing of config-file changes happens if the value of configuration option "auto-refresh-config" is TRUE. Core infrastructure is a timer - app.timers[CONFIG_T]. It has a longer intervals than the filelist timer. It is used for either 'default' change-monitoring, or enhanced monitoring (using gamin, or a relevant kernel capability) if such is built-in and actually working. The default mechanism is periodic polling of the mtime of the config file (the default or as specified in a startup parameter). If enhanced monitoring is used, suitable polling is handled by the relevant backend. */ #include "e2_filelist.h" #include #include #include #include #include #include #include "e2_option.h" #include "e2_task.h" #include "e2_dialog.h" #ifdef E2_SELTXT_RECOLOR extern GdkColor selectedtext; #endif #ifdef E2_FAM_DNOTIFY #include extern sigset_t dnotify_signal_set; #endif //filelist-needs-refresh flags, used by timers and threads, always do atomic access #ifdef E2_FAM_KERNEL volatile #endif gint p1dirty = 0; #ifdef E2_FAM_KERNEL volatile #endif gint p2dirty = 0; //refresh deferral counters static gint pane1repeats = 0; static gint pane2repeats = 0; //for managing timer suspension time_t last_work_time = 0; extern #ifdef E2_FAM_KERNEL volatile #endif gint cfgdirty; extern volatile gint cfg_refresh_refcount; /** @brief "event" signal callback which resumes polling for whether a refresh is needed @param widget pointer to main-window widget @param UNUSED event pointer to event data @param userdata parameter specified when callback was connected, helps to identify the signal @return FALSE always, so the event will propogate */ gboolean e2_filelist_repoll (GtkWidget *widget, GdkEvent *event, gpointer userdata) { g_signal_handlers_disconnect_by_func ((gpointer)widget, e2_filelist_repoll, userdata); last_work_time = time (NULL); printd (DEBUG, "Resume operation of DIRTYCHECK_T timer"); #ifdef E2_FAM_KERNEL if (app.monitor_type == E2_MONITOR_DEFAULT) { #endif app.timers[DIRTYCHECK_T] = #ifdef USE_GLIB2_14 g_timeout_add_seconds (E2_FILESCHECK_INTERVAL_S, #else g_timeout_add (E2_FILESCHECK_INTERVAL, #endif (GSourceFunc) e2_filelist_check_dirty, NULL); #ifdef E2_FAM_KERNEL } else e2_fs_FAM_resume (); #endif return FALSE; } /** @brief determine whether pane defined by @a pane is the active one @param pane enumerator for pane to be checked @return TRUE if @a pane is the active one */ static gboolean _e2_filelist_check_active_pane (E2_ListChoice pane) { gboolean active; switch (pane) { case PANE1: active = (curr_view == &app.pane1.view); break; case PANE2: active = (curr_view == &app.pane2.view); break; case PANEINACTIVE: active = FALSE; break; default: active = TRUE; break; } return active; } /** @brief disable refreshing of a single pane filelist This is mainly intended to block changes of filesystem CWD when a process is working in a displayed directory @param pane enumerator for pane to be blocked @return */ void e2_filelist_disable_one_refresh (E2_ListChoice pane) { if (_e2_filelist_check_active_pane (pane)) g_atomic_int_add (&curr_view->listcontrols.refresh_refcount, 1); else g_atomic_int_add (&other_view->listcontrols.refresh_refcount, 1); } /** @brief enable refreshing of a single pane filelist @param pane enumerator for pane to be unblocked @return */ void e2_filelist_enable_one_refresh (E2_ListChoice pane) { if (_e2_filelist_check_active_pane (pane)) { if (g_atomic_int_exchange_and_add (&curr_view->listcontrols.refresh_refcount, -1) < 1) g_atomic_int_set (&curr_view->listcontrols.refresh_refcount, 0); } else { if (g_atomic_int_exchange_and_add (&other_view->listcontrols.refresh_refcount, -1) < 1) g_atomic_int_set (&other_view->listcontrols.refresh_refcount, 0); } } /** @brief disable -refresh action @param from the button, menu item etc which was activated @param art action runtime data @return TRUE always */ gboolean e2_filelist_disable_refresh_action (gpointer from, E2_ActionRuntime *art) { e2_filelist_disable_refresh (); return TRUE; } /** @brief enable -refresh action @param from the button, menu item etc which was activated @param art action runtime data @return TRUE always */ gboolean e2_filelist_enable_refresh_action (gpointer from, E2_ActionRuntime *art) { e2_filelist_enable_refresh (); return TRUE; } /** @brief reset polling of config data and pane filelists @return */ void e2_filelist_reset_refresh (void) { g_atomic_int_set (&app.pane1.view.listcontrols.refresh_refcount, 0); g_atomic_int_set (&app.pane2.view.listcontrols.refresh_refcount, 0); g_atomic_int_set (&cfg_refresh_refcount, 0); } /* These functions must encapsulate any instructions that access the FileInfo structs produced when a selection is interrogated, because a refresh in the middle of such an operation would free FileInfo structs as a side-effect. */ /** @brief disable refreshing of pane filelists and config data Increases all refresh ref-counts, which will prevent any refresh from happening @return */ void e2_filelist_disable_refresh (void) { if (e2_option_bool_get ("auto-refresh")) { g_atomic_int_add (&curr_view->listcontrols.refresh_refcount, 1); g_atomic_int_add (&other_view->listcontrols.refresh_refcount, 1); } //same as e2_option_disable_config_checks() if (e2_option_bool_get ("auto-refresh-config")) g_atomic_int_add (&cfg_refresh_refcount, 1); } /** @brief re-enable polling of pane filelists and config data Decreases all ref-counts, which will enable refresh for any that are now 0 @return */ void e2_filelist_enable_refresh (void) { if (e2_option_bool_get ("auto-refresh")) { if (g_atomic_int_exchange_and_add (&curr_view->listcontrols.refresh_refcount, -1) < 1) g_atomic_int_set (&curr_view->listcontrols.refresh_refcount, 0); if (g_atomic_int_exchange_and_add (&other_view->listcontrols.refresh_refcount, -1) < 1) g_atomic_int_set (&other_view->listcontrols.refresh_refcount, 0); } //same as e2_option_enable_config_checks() if (e2_option_bool_get ("auto-refresh-config")) { if (g_atomic_int_exchange_and_add (&cfg_refresh_refcount, -1) < 1) g_atomic_int_set (&cfg_refresh_refcount, 0); } } /** @brief idle function to run the refresh-hooklist of @s view This happens after the end of the refresh timer callback, so in principle, another refresh, or a cd, could start before the hook funcs are called. So they should check for those races, and respond accordingly. @param view pointer to view data struct @return FALSE to abort the source */ static gboolean _e2_filelist_propogate_refresh (ViewInfo *view) { e2_hook_list_run (&view->hook_refresh, view); return FALSE; } /** @brief timer destroy function @param data pointerised index of the timer that ended @return */ void e2_filelist_timer_shutdown (gpointer data) { guint index = GPOINTER_TO_UINT (data); app.timers[index] = 0; #ifdef E2_REFRESH_DEBUG if (index == REFRESHBEGIN_T) printd (DEBUG, "timer REFRESHBEGIN_T id set to 0"); #endif } #if 1 /** @brief thread-function which performs filelist refresh if it can If refreshing is blocked, there is no deferral @param view pointer to data struct for the filelist to be refreshed @return NULL always */ static gpointer _e2_filelist_refresh_view (ViewInfo *view) { e2_utils_block_thread_signals (); if (g_atomic_int_get (&view->listcontrols.refresh_refcount) == 0) { //this refresh is not disabled if (g_atomic_int_compare_and_exchange (&view->listcontrols.refresh_requested, 1, 0)) { gboolean hook = FALSE; gboolean ready; retest: ready = !( g_atomic_int_get (&view->listcontrols.refresh_working) || //or busy g_atomic_int_get (&view->listcontrols.cd_working) #ifdef E2_STATUS_BLOCK || g_atomic_int_get (&app.status_working) #endif ); if (ready) { #ifdef E2_VFSTMP //FIXME dir when not mounted local #else printd (DEBUG, "accepting request to refresh %s", view->dir); #endif //refresh function expects BGL open if (e2_fileview_refresh_list (view) == GINT_TO_POINTER(1)) { hook = TRUE; //send it downstream //maybe again/now/still want to do this pane if (g_atomic_int_get (&view->listcontrols.refresh_refcount) == 0 && g_atomic_int_compare_and_exchange (&view->listcontrols.refresh_requested, 1, 0)) { printd (DEBUG, "Sequential refresh requested for %s", view->dir); usleep (10000); goto retest; } } } //initiate refresh hooklist here, not downstream, so that refreshes are batched, //and in idle to avoid any UI impact //(i.e. small risk of cd / another refresh starting before hook is called) if (hook) g_idle_add ((GSourceFunc)_e2_filelist_propogate_refresh, view); } } return NULL; } #else //original approach to refreshing /** @brief timer callback function which refreshes filelist(s) as appropriate, as soon as any block is gone @param unused_data UNUSED pointer set when timer was initiated (actually, it has the timer index, REFRESHBEGIN_T) @return FALSE when there's nothing left to refresh or more waiting is needed */ static gboolean _e2_filelist_refresh_manage (gpointer unused_data) { ViewInfo *cv, *ov; pthread_t thisID; gpointer result; gboolean cvreq, ovreq, cvdone, ovdone, cvhook, ovhook; gint ovsave; LISTS_LOCK //log these, in case active pane is changed during this process cv = curr_view; ov = other_view; //ensure that refresh is started if either pane needs it now cvreq = g_atomic_int_get (&cv->listcontrols.refresh_requested); ovreq = g_atomic_int_get (&ov->listcontrols.refresh_requested); cvhook = ovhook = FALSE; ovsave = 0; e2_utf8_set_name_conversion_if_requested (); if (cvreq) { retestc: cvdone = !( g_atomic_int_get (&cv->listcontrols.refresh_refcount) || //this refresh is disabled g_atomic_int_get (&cv->listcontrols.refresh_working) || //or busy g_atomic_int_get (&cv->listcontrols.cd_working) #ifdef E2_STATUS_BLOCK || g_atomic_int_get (&app.status_working) #endif ); if (cvdone) { LISTS_UNLOCK #ifdef E2_VFSTMP //FIXME dir when not mounted local #else printd (DEBUG, "accepting request to refresh %s", cv->dir); #endif //refresh function expects BGL open if (pthread_create (&thisID, NULL, (gpointer(*)(gpointer))e2_fileview_refresh_list, cv) == 0) { g_atomic_int_set (&cv->listcontrols.refresh_requested, 0); //not needed downstream pthread_join (thisID, &result); //only 1 refresh at a time if (result == GINT_TO_POINTER (1)) cvhook = TRUE; //send it downstream else if (result == NULL) //try again g_atomic_int_set (&cv->listcontrols.refresh_requested, 1); //revert } else printd (WARN, "Failed to create list refresh thread"); LISTS_LOCK } } else cvdone = FALSE; if (ovreq) { retesto: ovdone = !( g_atomic_int_get (&ov->listcontrols.refresh_refcount) || g_atomic_int_get (&ov->listcontrols.refresh_working) || g_atomic_int_get (&ov->listcontrols.cd_working) #ifdef E2_STATUS_BLOCK || g_atomic_int_get (&app.status_working) #endif ); if (ovdone) { LISTS_UNLOCK #ifdef E2_VFSTMP //FIXME dir when not mounted local #else printd (DEBUG, "accepting request to refresh %s", ov->dir); #endif if (pthread_create (&thisID, NULL, (gpointer(*)(gpointer))e2_fileview_refresh_list, ov) == 0) { g_atomic_int_set (&ov->listcontrols.refresh_requested, 0); //not needed downstream pthread_join (thisID, &result); //only 1 refresh at a time if (result == GINT_TO_POINTER (1)) ovhook = TRUE; else if (result == NULL) //try again g_atomic_int_set (&ov->listcontrols.refresh_requested, 1); } else printd (WARN, "Failed to create list refresh thread"); LISTS_LOCK } } else ovdone = FALSE; if (cvreq || ovreq) { if (ovdone) ovsave++; //save satus //maybe again/now/still want to do one or both panes //but only go back now if not blocked before if (cvdone && g_atomic_int_get (&cv->listcontrols.refresh_requested)) { usleep (10000); ovreq = FALSE; //don't (yet) repeat ov (ok cuz cvreq is still TRUE) goto retestc; //sets ovdone FALSE } if (ovsave > 0 && g_atomic_int_get (&ov->listcontrols.refresh_requested)) { usleep (10000); ovreq = TRUE; //reinstate after any cv repeat ovsave = 0; goto retesto; } #ifdef E2_REFRESH_DEBUG printd (DEBUG, "start timer REFRESHBEGIN_T"); #endif app.timers[REFRESHBEGIN_T] = g_timeout_add_full (G_PRIORITY_HIGH, 200, (GSourceFunc) _e2_filelist_refresh_manage, GUINT_TO_POINTER (REFRESHBEGIN_T), (GDestroyNotify) e2_filelist_timer_shutdown); } LISTS_UNLOCK //initiate refresh hooklists here, not downstream, so that refreshes are batched, //and in idle to avoid any UI impact //(i.e. small risk of cd / another refresh starting before hook is called) if (cvhook) g_idle_add ((GSourceFunc)_e2_filelist_propogate_refresh, curr_view); if (ovhook) g_idle_add ((GSourceFunc)_e2_filelist_propogate_refresh, other_view); return FALSE; } #endif /** @brief log request, and possibly initiate, filelist(s) refresh if it/they currently show @a dir If a refresh is initiated, either or both flagged lists will be refreshed as soon as any in-progress re-list (either or both panes) is completed @param dir the dir to be refreshed (utf8 string) @param immediate TRUE to intiate a refresh @return TRUE if a refresh was initiated */ gboolean e2_filelist_request_refresh (gchar *dir, gboolean immediate) { gboolean matched = FALSE; #ifdef E2_VFSTMP //CHECKME v-dir ok #endif if (strcmp (app.pane1.view.dir, dir) == 0) { matched = TRUE; g_atomic_int_set (&app.pane1.view.listcontrols.refresh_requested, 1); } #ifdef E2_VFSTMP //CHECKME v-dir ok #endif if (strcmp (app.pane2.view.dir, dir) == 0) { matched = TRUE; g_atomic_int_set (&app.pane2.view.listcontrols.refresh_requested, 1); } if (matched && immediate) { #ifdef E2_REFRESH_DEBUG printd (DEBUG, "In e2_filelist_request_refresh, goto e2_filelist_check_dirty"); #endif e2_filelist_check_dirty (GINT_TO_POINTER (1)); } #ifdef E2_REFRESH_DEBUG else printd (DEBUG, "In e2_filelist_request_refresh, NO e2_filelist_check_dirty"); #endif return matched; } /** @brief decide whether to lengthen the interval between filelist refresh requests This is called when a 'dirty' flag has been detected @param pane enumerator for pane 1 or pane 2, which was reported dirty @return TRUE to process the current request now */ static gboolean _e2_filelist_refresh_ok (E2_ListChoice pane) { gboolean retval; /* Ideally this would check CPU resource-usage for this app and all children, and only proceed if there's enough(?) idle capacity Instead we assume small dirs can be processed without much adverse effect */ gint times, itemcount = (pane == PANE1) ? //these are counts of total items, not filtered items gtk_tree_model_iter_n_children (GTK_TREE_MODEL (app.pane1.view.store), NULL): gtk_tree_model_iter_n_children (GTK_TREE_MODEL (app.pane2.view.store), NULL); if (itemcount < 51) times = 1; else if (itemcount < 251) times = 2; else times = 3; if (pane == PANE1) { if (++pane1repeats >= times) { printd (DEBUG, "Pane 1 refresh interval count = %d", times); pane1repeats = 0; retval = TRUE; } else retval = FALSE; } else { if (++pane2repeats >= times) { printd (DEBUG, "Pane 2 refresh interval count = %d", times); pane2repeats = 0; retval = TRUE; } else retval = FALSE; } return retval; } /** @brief initiate refresh of filepane(s) contents if requested (elsewhere) or if changed content has been, or is now, detected Among other uses, this is the timer callback function for filelists auto refresh In certain circumstances any dirty flag might be ignored, or deferred to lengthen the effective interval between refreshes. A directory that is 'gone' will have been flagged as 'dirty', and later the list-refesh function handles choosing a replacement dir. Non-NULL @a userdata forces refresh of both panes ASAP. If this is a timer callback, the cfgdirty flag may be refreshed, but not otherwise acted upon. If there has been no refresh during the last QUIET_SECONDS, then app.timers[DIRTYCHECK_T] is shut down. Otherwise, the time of the last refresh is updated. This expects gtk's BGL to be off/open @param userdata data specified when the timer was initialised (NULL), or when called directly, usually non-NULL @return TRUE so the timer keeps working */ gboolean e2_filelist_check_dirty (gpointer userdata) { static gboolean busy = FALSE; gboolean dop1, dop2; if (busy) { if (userdata == NULL) { //timer callback, not a specific request #ifdef E2_REFRESH_DEBUG printd (DEBUG, "e2_filelist_check_dirty is busy, exit immediately"); #endif return TRUE; //no re-entrant usage, run the timer again } else { while (busy) usleep (50000); } } busy = TRUE; #ifdef E2_REFRESH_DEBUG printd (DEBUG, "In e2_filelist_check_dirty()"); #endif if (userdata == NULL) { //a timer callback /* if E2_FAM_KERNEL backend is working, that will have already set *dirty flag(s) as appropriate*/ #ifdef E2_FAM_KERNEL if (app.monitor_type == E2_MONITOR_DEFAULT) { #endif e2_fs_FAM_check_dirty (&p1dirty, &p2dirty #ifdef E2_FAM //cfgdirty flag is logged, but not otherwise processed here , &cfgdirty #endif ); #ifdef E2_FAM_KERNEL } #endif } else { //specific request, start fresh pane1repeats = 0; pane2repeats = 0; } //ASAP check and clear flags used by backend dop1 = g_atomic_int_compare_and_exchange (&p1dirty, 1, 0); dop2 = g_atomic_int_compare_and_exchange (&p2dirty, 1, 0); if (dop1 && (userdata != NULL || _e2_filelist_refresh_ok (PANE1))) { g_atomic_int_set (&app.pane1.view.listcontrols.refresh_requested, 1); #ifdef E2_REFRESH_DEBUG printd (DEBUG,"refresh-requested flag set for pane 1"); #endif } else { #ifdef E2_REFRESH_DEBUG if (dop1) printd (DEBUG,"dirty set FALSE for pane 1"); else printd (DEBUG,"pane 1 is clean"); #endif dop1 = g_atomic_int_get (&app.pane1.view.listcontrols.refresh_requested); } if (dop2 && (userdata != NULL || _e2_filelist_refresh_ok (PANE2))) { g_atomic_int_set (&app.pane2.view.listcontrols.refresh_requested, 1); #ifdef E2_REFRESH_DEBUG printd (DEBUG,"refresh-requested flag set for pane 2"); #endif } else { #ifdef E2_REFRESH_DEBUG if (dop2) printd (DEBUG,"dirty set FALSE for pane 2"); else printd (DEBUG,"pane 2 is clean"); #endif dop2 = g_atomic_int_get (&app.pane2.view.listcontrols.refresh_requested); } if (dop1 || dop2) { last_work_time = time (NULL); #if 1 pthread_t thisID; pthread_attr_t attr; e2_utf8_set_name_conversion_if_requested (); pthread_attr_init (&attr); pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); //priority for active pane if (g_atomic_int_get (&curr_view->listcontrols.refresh_requested)) { if (pthread_create (&thisID, &attr, (gpointer(*)(gpointer))_e2_filelist_refresh_view, curr_view) != 0) { //TODO handle error printd (WARN,"refresh-dir-thread-create error!"); } } if (g_atomic_int_get (&other_view->listcontrols.refresh_requested)) { if (pthread_create (&thisID, &attr, (gpointer(*)(gpointer))_e2_filelist_refresh_view, other_view) != 0) { //TODO handle error printd (WARN,"refresh-dir-thread-create error!"); } } pthread_attr_destroy (&attr); #else if (app.timers[REFRESHBEGIN_T] > 0) { //clear any incomplete refresh request #ifdef E2_REFRESH_DEBUG printd (DEBUG, "stop timer REFRESHBEGIN_T"); #endif g_source_remove (app.timers[REFRESHBEGIN_T]); } //the delay between attempts will be lengthened later, if appropriate //CHECKME we may already be in a timer callback here - are nested timers acceptable to glib/gtk ? #ifdef E2_REFRESH_DEBUG printd (DEBUG, "Start timer REFRESHBEGIN_T to initiate refresh"); #endif app.timers[REFRESHBEGIN_T] = g_timeout_add_full (G_PRIORITY_HIGH, 5, (GSourceFunc) _e2_filelist_refresh_manage, GUINT_TO_POINTER (REFRESHBEGIN_T), (GDestroyNotify) e2_filelist_timer_shutdown); #endif } else { if (time (NULL) >= last_work_time + QUIET_SECONDS) { printd (DEBUG, "Suspend operation of timer DIRTYCHECK_T"); g_source_remove (app.timers[DIRTYCHECK_T]); app.timers[DIRTYCHECK_T] = 0; //arrange to restart upon activity g_signal_connect (G_OBJECT (app.main_window), "event", G_CALLBACK (e2_filelist_repoll), GUINT_TO_POINTER (DIRTYCHECK_T)); } } busy = FALSE; return TRUE; } /** @brief stop change-monitoring Either stop a timer which polls for various 'dirty' indicator(s), or stop the 'kernel-backend' process(es) in which case it expects app.monitor_type to be set appropriately No check here for "auto-refresh" option status @return */ void e2_filelist_stop_refresh_checks (void) { #ifdef E2_FAM_KERNEL //stop backend working e2_fs_FAM_cancel_monitor_dir (&app.pane1); e2_fs_FAM_cancel_monitor_dir (&app.pane2); #endif //this timer is used whatever app.monitor_type is, with E2_FAM_KERNEL //TODO stopping it also shuts down cfg monitoring, which shares the mechanism if (app.timers[DIRTYCHECK_T] != 0) { #ifdef E2_REFRESH_DEBUG printd (DEBUG, "stop timer DIRTYCHECK_T"); #endif g_source_remove (app.timers[DIRTYCHECK_T]); app.timers[DIRTYCHECK_T] = 0; } g_atomic_int_set (&app.pane1.view.listcontrols.refresh_refcount, 0); g_atomic_int_set (&app.pane2.view.listcontrols.refresh_refcount, 0); } /** @brief start change-monitoring Either start a timer which polls for various 'dirty' indicator(s), or start the 'kernel-backend' process(es)- in which case it expects app.monitor_type to be set appropriately No check here for "auto-refresh" option status The timer (if any) may later be stopped permanently if change-monitoring is abandoned, probably via a config dialog. But it won't be stopped merely if change-monitoring is disabled i.e. suspended @return */ void e2_filelist_start_refresh_checks (void) { g_atomic_int_set (&app.pane1.view.listcontrols.refresh_refcount, 0); g_atomic_int_set (&app.pane2.view.listcontrols.refresh_refcount, 0); #ifdef E2_FAM_KERNEL /* app.monitor_type setup needs a prior call to setup kernel-monitoring if that's in the build */ if (app.monitor_type == E2_MONITOR_DEFAULT) { if (app.timers[DIRTYCHECK_T] == 0) { # ifdef E2_REFRESH_DEBUG printd (DEBUG, "start timer DIRTYCHECK_T"); # endif app.timers[DIRTYCHECK_T] = # ifdef USE_GLIB2_14 g_timeout_add_seconds (E2_FILESCHECK_INTERVAL_S, # else g_timeout_add (E2_FILESCHECK_INTERVAL, # endif (GSourceFunc) e2_filelist_check_dirty, NULL); } } else { //start backend working e2_fs_FAM_change (app.pane1.path, &app.pane1); e2_fs_FAM_change (app.pane2.path, &app.pane2); e2_fs_FAM_resume (); } #else //ndef E2_FAM_KERNEL if (app.timers[DIRTYCHECK_T] == 0) { # ifdef E2_REFRESH_DEBUG printd (DEBUG, "start timer DIRTYCHECK_T"); # endif app.timers[DIRTYCHECK_T] = # ifdef USE_GLIB2_14 g_timeout_add_seconds (E2_FILESCHECK_INTERVAL_S, # else g_timeout_add (E2_FILESCHECK_INTERVAL, # endif (GSourceFunc) e2_filelist_check_dirty, NULL); } #endif } /** @brief idle function to clear old liststores @param user_data UNUSED data specified when the idle was setup @return FALSE, to stop the callbacks */ gboolean e2_filelist_clear_old_stores (gpointer user_data) { GSList *tmp; #ifdef DEBUG_MESSAGES gint debug = g_slist_length (app.used_stores); #endif for (tmp = app.used_stores; tmp != NULL; tmp = tmp->next) { GtkListStore *store = tmp->data; GtkTreeModel *mdl = GTK_TREE_MODEL (store); GtkTreeIter iter; if (gtk_tree_model_get_iter_first (mdl, &iter)) { //it's not empty already //clear file info data structs referred to in the store //CHECKME need to clear anything else? FileInfo *info; do { gtk_tree_model_get (mdl, &iter, FINFO, &info, -1); #ifdef USE_GLIB2_10 g_slice_free1 (sizeof(FileInfo), info); //DEALLOCATE (FileInfo, info); #else DEALLOCATE (FileInfo, info); #endif } while (gtk_tree_model_iter_next (mdl, &iter)); } //CHECKME clear filtermodel ? g_object_unref (G_OBJECT (store)); } g_slist_free (app.used_stores); app.used_stores = NULL; printd (DEBUG, "%d old liststore(s) cleared", debug); return FALSE; } /** @brief create and populate filelist-compatible list store with rows for each item in @a entries @param entries list of FileInfo's to be processed @param view pointer to data struct for the view to which the filled store will apply @return pointer to the liststore, or NULL if a problem occurs */ GtkListStore *e2_filelist_fill_store (GList *entries, ViewInfo *view) { // printd (DEBUG, "start store fill"); GtkListStore *store = e2_filelist_make_store (); if (store == NULL) return NULL; guint i; GList *tmp; FileInfo *infoptr; GtkTreeIter iter; struct tm *tm_ptr; struct passwd *pwd_buf; struct group *grp_buf; gchar size_buf[20]; //enough for 999 Tb gchar modified_buf[25]; gchar accessed_buf[25]; gchar changed_buf[25]; // gchar perm_buf[11]; gchar uid_buf[20]; gchar gid_buf[20]; gchar *buf[NAMEKEY+1]; //pointers to strings to be inserted into each row gchar *freeme; gchar *__utf__; //for e2_utf8_from_locale_fast() macro //get format parameters //format for dates display gchar *strf_string; switch (e2_option_int_get ("date-string")) { case 1: strf_string = "%d/%m/%y %H:%M"; //standard break; case 2: strf_string = "%m/%d/%y %H:%M"; //american break; case 3: strf_string = "%Y-%m-%dT%H:%M"; //ISO8601 break; case 4: strf_string = "%x %X"; //localised break; case 5: strf_string = e2_option_str_get ("custom-date-format"); if (strf_string != NULL && *strf_string != '\0') //no reliable check for format validity ? break; default: strf_string = "%b %d %H:%M"; break; } //get format for size display gchar *comma; switch (e2_option_int_get ("size-string")) { case 1: { comma = nl_langinfo (THOUSEP); if (comma == NULL || *comma == '\0') comma = ","; break; } default: comma = NULL; //signal to use the condensed version break; } gboolean anyconvert = FALSE; gboolean caseignore = ! e2_option_bool_get ("namesort-case-sensitive"); /* #ifdef USE_GTK3_0 GdkRBGA *normal; GtkStyleContext *context; context = gtk_widget_get_style_context (curr_view->treeview); gtk_style_context_get (context, GTK_STATE_NORMAL, GTK_STYLE_PROPERTY_COLOR, &normal, NULL); GdkColor normal2 = {0, (guint16)(65535 * normal->red), (guint16)(65535 * normal->green), (guint16)(65535 * normal->blue) } ; GdkColor *default_color = &normal2; gdk_rgba_free (normal); #else GtkStyle *style = gtk_rc_get_style (curr_view->treeview); GdkColor *default_color = &style->text[GTK_STATE_NORMAL]; #endif */ GdkColor *default_color = NULL; #ifdef E2_ASSISTED GdkColor *back_color = (e2_option_bool_get ("color-background-set")) ? e2_option_color_get ("color-background") : NULL; #endif GdkColor *link_color = e2_option_color_get ("color-ft-link"); GdkColor *dir_color = e2_option_color_get ("color-ft-dir"); GdkColor *dev_color = e2_option_color_get ("color-ft-dev"); GdkColor *sock_color = e2_option_color_get ("color-ft-socket"); GdkColor *exec_color = e2_option_color_get ("color-ft-exec"); //iterate through the listed FileInfo's for (tmp = entries; tmp != NULL; tmp = tmp->next) { gboolean convert; //also a cleanup flag gchar *s; register char c; infoptr = (FileInfo *) tmp->data; //with bad unmount behaviour, there can be no actual data if (infoptr == NULL) continue; //CHECKME remove the item from list ? //scan the namestring for non-ASCII char(s) convert = FALSE; for (s = infoptr->filename; (c = *s) != '\0'; s++) { if (c < 0) //non-ASCII { convert = TRUE; anyconvert = TRUE; break; } } //convert to UTF-8 if appropriate if (convert) { buf[FILENAME] = g_filename_display_name (infoptr->filename); /* if (g_utf8_validate (infoptr->filename, -1, NULL)) { convert = FALSE; buf[FILENAME] = infoptr->filename; } else { //_not display_ so that it can be casefolded and/or collated properly ?? buf[FILENAME] = g_locale_to_utf8 (infoptr->filename, -1, NULL, NULL, NULL); if (buf[FILENAME] == NULL) buf[FILENAME] = e2_utf8_from_locale_fallback (infoptr->filename); } */ } else buf[FILENAME] = infoptr->filename; if (caseignore) { freeme = g_utf8_casefold (buf[FILENAME], -1); #ifdef USE_GTK2_8 buf[NAMEKEY] = g_utf8_collate_key_for_filename (freeme, -1); #else buf[NAMEKEY] = g_utf8_collate_key (freeme, -1); #endif g_free (freeme); } else #ifdef USE_GTK2_8 buf[NAMEKEY] = g_utf8_collate_key_for_filename (buf[FILENAME], -1); #else buf[NAMEKEY] = g_utf8_collate_key (buf[FILENAME], -1); #endif //dirlink check done when info was created if (infoptr->statbuf.st_mode & (S_IFDIR | E2_DIRLNK)) { //directories (and links to dirs) get a trailing separator s = buf[FILENAME]; if (convert) { guint l = strlen (s); if ((s = (gchar *)realloc (s, l+2)) != NULL) { buf[FILENAME] = s; s += l; *s++ = G_DIR_SEPARATOR; *s = '\0'; } } else { convert = TRUE; //cleanup this one too buf[FILENAME] = e2_utils_strcat (s, G_DIR_SEPARATOR_S); } } if (comma == NULL) { //use condensed size format if (infoptr->statbuf.st_size < 1024) //less than 1k { g_snprintf(size_buf, sizeof(size_buf), "%"PRIu64, infoptr->statbuf.st_size); } else if (infoptr->statbuf.st_size < 1048576) //less than a meg { g_snprintf(size_buf, sizeof(size_buf), "%.1f%s", (gfloat) infoptr->statbuf.st_size / 1024.0, _("k")); } else //a meg or more if (infoptr->statbuf.st_size < 1073741824) { g_snprintf(size_buf, sizeof(size_buf), "%.1f%s", (gfloat) infoptr->statbuf.st_size / 1048576.0, _("M")); } /* else //a gig or more { g_snprintf(size_buf, sizeof(size_buf), "%.1f%s", (gfloat) infoptr->statbuf.st_size / 1073741824.0, _("G")); } */ } else { //use actual size, with commas g_snprintf(size_buf, sizeof(size_buf), "%"PRIu64, infoptr->statbuf.st_size); guint len = strlen (size_buf); guint ths = len-1; //0-based index while (ths > 2 && len < sizeof(size_buf)) { for (i = len-1; i > ths-3; i--) size_buf[i+1] = size_buf[i]; size_buf[i+1] = *comma; size_buf[++len] = '\0'; ths = i; } } buf[SIZE] = size_buf; //content is ascii/utf-8, no need to convert or free buf[PERM] = e2_fs_get_perm_string (infoptr->statbuf.st_mode); //string is already utf if ((pwd_buf = getpwuid (infoptr->statbuf.st_uid)) == NULL) { g_snprintf (uid_buf, sizeof(uid_buf), "%d", (guint) infoptr->statbuf.st_uid); buf[OWNER] = g_strdup (uid_buf); //content is ascii number, no need to convert to utf } else { buf[OWNER] = e2_utf8_from_locale_fast (pwd_buf->pw_name); } if ((grp_buf = getgrgid (infoptr->statbuf.st_gid)) == NULL) { g_snprintf(gid_buf, sizeof(gid_buf), "%d", (guint) infoptr->statbuf.st_gid); buf[GROUP] = g_strdup (gid_buf); //content is ascii number, no need to convert to utf } else { buf[GROUP] = e2_utf8_from_locale_fast (grp_buf->gr_name); } tm_ptr = localtime (&(infoptr->statbuf.st_mtime)); strftime (modified_buf, sizeof(modified_buf), strf_string, tm_ptr); buf[MODIFIED] = e2_utf8_from_locale_fast (modified_buf); tm_ptr = localtime (&(infoptr->statbuf.st_atime)); strftime (accessed_buf, sizeof(accessed_buf), strf_string, tm_ptr); buf[ACCESSED] = e2_utf8_from_locale_fast (accessed_buf); tm_ptr = localtime(&(infoptr->statbuf.st_ctime)); strftime (changed_buf, sizeof(changed_buf), strf_string, tm_ptr); buf[CHANGED] = e2_utf8_from_locale_fast (changed_buf); GdkColor *foreground; switch (infoptr->statbuf.st_mode & S_IFMT) { case S_IFLNK: foreground = link_color; break; case S_IFDIR: foreground = dir_color; break; case S_IFCHR: case S_IFBLK: foreground = dev_color; break; case S_IFSOCK: foreground = sock_color; break; case S_IFREG: //show as executable if _anyone_ has that permission if ((S_IXUSR & infoptr->statbuf.st_mode) || (S_IXGRP & infoptr->statbuf.st_mode) || (S_IXOTH & infoptr->statbuf.st_mode)) foreground = exec_color; else { #ifdef E2_RAINBOW //find extension of current item //localised text, but assumes . is ascii //hashed extension-strings used for comparison have been localised foreground = NULL; //in case there's no '.' at all gchar *ext = infoptr->filename + 1; //+1 in case item is hidden while ((ext = strchr (ext,'.')) != NULL) { ext++; //pass the '.' //use its color, if any foreground = g_hash_table_lookup (app.colors, ext); if (foreground != NULL) break; } if (foreground == NULL) foreground = default_color; #else foreground = default_color; #endif } break; default: foreground = default_color; break; } //gtk >= 2.10 can handle &iter = NULL gtk_list_store_insert_with_values (store, &iter, -1, FILENAME, buf[FILENAME], SIZE, buf[SIZE], PERM, buf[PERM], OWNER, buf[OWNER], GROUP, buf[GROUP], MODIFIED, buf[MODIFIED], ACCESSED, buf[ACCESSED], CHANGED, buf[CHANGED], NAMEKEY, buf[NAMEKEY], FINFO, infoptr, FORECOLOR, foreground, #ifdef E2_ASSISTED BACKCOLOR, back_color, #endif // VISIBLE, TRUE, -1); //cleanup if (convert) g_free (buf[FILENAME]); for (i = PERM; i <= NAMEKEY; i++) g_free (buf[i]); } if (anyconvert != view->convert) { view->convert = anyconvert; //flag need for change of conversion funcs at a suitable time app.reconvert_requested = TRUE; } /* // gettimeofday (&strt, NULL); //FOR DEBUGGING, check time this load takes // gettimeofday (&nd, NULL); if (nd.tv_usec < strt.tv_usec) { int nsec = (strt.tv_usec - nd.tv_usec) / 1000000 + 1; strt.tv_usec -= 1000000 * nsec; strt.tv_sec += nsec; } if (nd.tv_usec - strt.tv_usec > 1000000) { int nsec = (strt.tv_usec - nd.tv_usec) / 1000000; strt.tv_usec += 1000000 * nsec; strt.tv_sec -= nsec; } result.tv_sec = result.tv_sec + nd.tv_sec - strt.tv_sec; result.tv_usec = result.tv_usec + nd.tv_usec - strt.tv_usec; if (result.tv_usec > 1000000) { int nsec = result.tv_usec / 1000000 + 1; result.tv_usec -= 1000000 * nsec; result.tv_sec += nsec; } // printd (DEBUG, "populating rows took %f seconds", result.tv_sec + result.tv_usec / 1000000.0 ); */ // printd (DEBUG, "populating rows finished"); return store; } /** @brief create list store structure Creates basic structure of list store, for panes filelists No rows or data are added. @return the new liststore */ GtkListStore *e2_filelist_make_store (void) { GtkListStore *store = gtk_list_store_new (MODEL_COLUMNS, G_TYPE_STRING, //FILENAME G_TYPE_STRING, //SIZE G_TYPE_STRING, //PERM G_TYPE_STRING, //OWNER G_TYPE_STRING, //GROUP G_TYPE_STRING, //MODIFIED G_TYPE_STRING, //ACCESSED G_TYPE_STRING, //CHANGED // the rest are not displayed G_TYPE_STRING, //NAMEKEY for i18n name sorts G_TYPE_POINTER, //FINFO pr to FileInfo for the item GDK_TYPE_COLOR, //FORECOLOR line colour GDK_TYPE_COLOR //BACKCOLOR line colour // G_TYPE_BOOLEAN //VISIBLE whether filtered or not ); return store; } #ifdef STORECOPY /** @brief helper function to copy each FileInfo stored in a filelist store @param model the store to be updated @param tpath UNUSED tree path of iter being processed ? @param iter pointer to iter being processed @param user_data UNUSED data specified when foreach was called @return FALSE if the process can continue */ static gboolean _e2_filelist_copy_storeinfos (GtkTreeModel *model, GtkTreePath *tpath, GtkTreeIter *iter, gpointer user_data) { FileInfo *info; gtk_tree_model_get (model, iter, FINFO, &info, -1); //FIXME duplicate the data #ifdef USE_GLIB2_14 info = (FileInfo *) g_slice_copy (sizeof (FileInfo), info); gtk_list_store_set (GTK_LIST_STORE (model), iter, FINFO, info, -1); #else FileInfo *info2 = ALLOCATE (FileInfo); CHECKALLOCATEDWARN (info2, return TRUE;) memcpy (info2, info, sizeof (FileInfo)); gtk_list_store_set (GTK_LIST_STORE (model), iter, FINFO, info2, -1); #endif // gtk_tree_path_free (tpath); CRASHER return FALSE; } /** @brief copy a filelist liststore with all its contents @param original the store to be copied @return the new liststore */ GtkListStore *e2_filelist_copy_store (GtkListStore *original) { gpointer copied; e2_tree_store_copy (GTK_TREE_MODEL (original), FALSE, &copied); gtk_tree_model_foreach (GTK_TREE_MODEL ((GtkListStore *)copied), (GtkTreeModelForeachFunc) _e2_filelist_copy_storeinfos, NULL); return ((GtkListStore *)copied); } #endif