diff options
Diffstat (limited to 'app/widgets/gimpaction-history.c')
-rw-r--r-- | app/widgets/gimpaction-history.c | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/app/widgets/gimpaction-history.c b/app/widgets/gimpaction-history.c new file mode 100644 index 0000000..37cdc03 --- /dev/null +++ b/app/widgets/gimpaction-history.c @@ -0,0 +1,503 @@ +/* GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * gimpaction-history.c + * Copyright (C) 2013 Jehan <jehan at girinstud.io> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "config.h" + +#include <string.h> + +#include <gtk/gtk.h> + +#include "libgimpbase/gimpbase.h" +#include "libgimpconfig/gimpconfig.h" +#include "libgimpmath/gimpmath.h" + +#include "widgets-types.h" + +#include "config/gimpguiconfig.h" + +#include "core/gimp.h" + +#include "gimpuimanager.h" +#include "gimpaction.h" +#include "gimpaction-history.h" + + +#define GIMP_ACTION_HISTORY_FILENAME "action-history" + +/* History items are stored in a queue, sorted by frequency (number of times + * the action was activated), from most frequent to least frequent. Each item, + * in addition to the corresponding action name and its index in the queue, + * stores a "delta": the difference in frequency between it, and the next item + * in the queue; note that the frequency itself is not stored anywhere. + * + * To keep items from remaining at the top of the queue for too long, the delta + * is capped above, such the the maximal delta of the first item is MAX_DELTA, + * and the maximal delta of each subsequent item is the maximal delta of the + * previous item, times MAX_DELTA_FALLOFF. + * + * When an action is activated, its frequency grows by 1, meaning that the + * delta of the corresponding item is incremented (if below the maximum), and + * the delta of the previous item is decremented (if above 0). If the delta of + * the previous item is already 0, then, before the above, the current and + * previous items swap frequencies, and the current item is moved up the queue + * until the preceding item's frequency is greater than 0 (or until it reaches + * the front of the queue). + */ +#define MAX_DELTA 5 +#define MAX_DELTA_FALLOFF 0.95 + + +enum +{ + HISTORY_ITEM = 1 +}; + +typedef struct +{ + gchar *action_name; + gint index; + gint delta; +} GimpActionHistoryItem; + +static struct +{ + Gimp *gimp; + GQueue *items; + GHashTable *links; +} history; + + +static GimpActionHistoryItem * gimp_action_history_item_new (const gchar *action_name, + gint index, + gint delta); +static void gimp_action_history_item_free (GimpActionHistoryItem *item); + +static gint gimp_action_history_item_max_delta (gint index); + + +/* public functions */ + +void +gimp_action_history_init (Gimp *gimp) +{ + GimpGuiConfig *config; + GFile *file; + GScanner *scanner; + GTokenType token; + gint delta = 0; + + g_return_if_fail (GIMP_IS_GIMP (gimp)); + + config = GIMP_GUI_CONFIG (gimp->config); + + if (history.gimp != NULL) + { + g_warning ("%s: must be run only once.", G_STRFUNC); + return; + } + + history.gimp = gimp; + history.items = g_queue_new (); + history.links = g_hash_table_new (g_str_hash, g_str_equal); + + file = gimp_directory_file (GIMP_ACTION_HISTORY_FILENAME, NULL); + + if (gimp->be_verbose) + g_print ("Parsing '%s'\n", gimp_file_get_utf8_name (file)); + + scanner = gimp_scanner_new_gfile (file, NULL); + g_object_unref (file); + + if (! scanner) + return; + + g_scanner_scope_add_symbol (scanner, 0, "history-item", + GINT_TO_POINTER (HISTORY_ITEM)); + + token = G_TOKEN_LEFT_PAREN; + + while (g_scanner_peek_next_token (scanner) == token) + { + token = g_scanner_get_next_token (scanner); + + switch (token) + { + case G_TOKEN_LEFT_PAREN: + token = G_TOKEN_SYMBOL; + break; + + case G_TOKEN_SYMBOL: + if (scanner->value.v_symbol == GINT_TO_POINTER (HISTORY_ITEM)) + { + gchar *action_name; + + token = G_TOKEN_STRING; + + if (g_scanner_peek_next_token (scanner) != token) + break; + + if (! gimp_scanner_parse_string (scanner, &action_name)) + break; + + token = G_TOKEN_INT; + + if (g_scanner_peek_next_token (scanner) != token || + ! gimp_scanner_parse_int (scanner, &delta)) + { + g_free (action_name); + break; + } + + if (! gimp_action_history_is_excluded_action (action_name) && + ! g_hash_table_contains (history.links, action_name)) + { + GimpActionHistoryItem *item; + + item = gimp_action_history_item_new ( + action_name, + g_queue_get_length (history.items), + delta); + + g_queue_push_tail (history.items, item); + + g_hash_table_insert (history.links, + item->action_name, + g_queue_peek_tail_link (history.items)); + } + + g_free (action_name); + } + token = G_TOKEN_RIGHT_PAREN; + break; + + case G_TOKEN_RIGHT_PAREN: + token = G_TOKEN_LEFT_PAREN; + + if (g_queue_get_length (history.items) >= config->action_history_size) + goto done; + break; + + default: /* do nothing */ + break; + } + } + + done: + gimp_scanner_destroy (scanner); +} + +void +gimp_action_history_exit (Gimp *gimp) +{ + GimpGuiConfig *config; + GimpActionHistoryItem *item; + GList *actions; + GFile *file; + GimpConfigWriter *writer; + gint i; + + g_return_if_fail (GIMP_IS_GIMP (gimp)); + + config = GIMP_GUI_CONFIG (gimp->config); + + file = gimp_directory_file (GIMP_ACTION_HISTORY_FILENAME, NULL); + + if (gimp->be_verbose) + g_print ("Writing '%s'\n", gimp_file_get_utf8_name (file)); + + writer = gimp_config_writer_new_gfile (file, TRUE, "GIMP action-history", + NULL); + g_object_unref (file); + + for (actions = history.items->head, i = 0; + actions && i < config->action_history_size; + actions = g_list_next (actions), i++) + { + item = actions->data; + + gimp_config_writer_open (writer, "history-item"); + gimp_config_writer_string (writer, item->action_name); + gimp_config_writer_printf (writer, "%d", item->delta); + gimp_config_writer_close (writer); + } + + gimp_config_writer_finish (writer, "end of action-history", NULL); + + gimp_action_history_clear (gimp); + + g_clear_pointer (&history.links, g_hash_table_unref); + g_clear_pointer (&history.items, g_queue_free); + history.gimp = NULL; +} + +void +gimp_action_history_clear (Gimp *gimp) +{ + GimpActionHistoryItem *item; + + g_return_if_fail (GIMP_IS_GIMP (gimp)); + + g_hash_table_remove_all (history.links); + + while ((item = g_queue_pop_head (history.items))) + gimp_action_history_item_free (item); +} + +/* Search all history actions which match "keyword" with function + * match_func(action, keyword). + * + * @return a list of GtkAction*, to free with: + * g_list_free_full (result, (GDestroyNotify) g_object_unref); + */ +GList * +gimp_action_history_search (Gimp *gimp, + GimpActionMatchFunc match_func, + const gchar *keyword) +{ + GimpGuiConfig *config; + GimpUIManager *manager; + GList *actions; + GList *result = NULL; + gint i; + + g_return_val_if_fail (GIMP_IS_GIMP (gimp), NULL); + g_return_val_if_fail (match_func != NULL, NULL); + + config = GIMP_GUI_CONFIG (gimp->config); + manager = gimp_ui_managers_from_name ("<Image>")->data; + + for (actions = history.items->head, i = 0; + actions && i < config->action_history_size; + actions = g_list_next (actions), i++) + { + GimpActionHistoryItem *item = actions->data; + GimpAction *action; + + action = gimp_ui_manager_find_action (manager, NULL, item->action_name); + if (action == NULL) + continue; + + if (! gimp_action_is_visible (action) || + (! gimp_action_is_sensitive (action) && + ! config->search_show_unavailable)) + continue; + + if (match_func (action, keyword, NULL, gimp)) + result = g_list_prepend (result, g_object_ref (action)); + } + + return g_list_reverse (result); +} + +/* gimp_action_history_is_blacklisted_action: + * + * Returns whether an action should be excluded from both + * history and search results. + */ +gboolean +gimp_action_history_is_blacklisted_action (const gchar *action_name) +{ + if (gimp_action_is_gui_blacklisted (action_name)) + return TRUE; + + return (g_str_has_suffix (action_name, "-set") || + g_str_has_suffix (action_name, "-accel") || + g_str_has_prefix (action_name, "context-") || + g_str_has_prefix (action_name, "filters-recent-") || + g_strcmp0 (action_name, "dialogs-action-search") == 0); +} + +/* gimp_action_history_is_excluded_action: + * + * Returns whether an action should be excluded from history. + * + * Some actions should not be logged in the history, but should + * otherwise appear in the search results, since they correspond + * to different functions at different times, or since their + * label may interfere with more relevant, but less frequent, + * actions. + */ +gboolean +gimp_action_history_is_excluded_action (const gchar *action_name) +{ + if (gimp_action_history_is_blacklisted_action (action_name)) + return TRUE; + + return (g_strcmp0 (action_name, "edit-undo") == 0 || + g_strcmp0 (action_name, "edit-strong-undo") == 0 || + g_strcmp0 (action_name, "edit-redo") == 0 || + g_strcmp0 (action_name, "edit-strong-redo") == 0 || + g_strcmp0 (action_name, "filters-repeat") == 0 || + g_strcmp0 (action_name, "filters-reshow") == 0); +} + +/* Called whenever a GimpAction is activated. + * It allows us to log all used actions. + */ +void +gimp_action_history_action_activated (GimpAction *action) +{ + GimpGuiConfig *config; + const gchar *action_name; + GList *link; + GimpActionHistoryItem *item; + + /* Silently return when called at the wrong time, like when the + * activated action was "quit" and the history is already gone. + */ + if (! history.gimp) + return; + + config = GIMP_GUI_CONFIG (history.gimp->config); + + if (config->action_history_size == 0) + return; + + action_name = gimp_action_get_name (action); + + /* Some specific actions are of no log interest. */ + if (gimp_action_history_is_excluded_action (action_name)) + return; + + g_return_if_fail (action_name != NULL); + + /* Remove excessive items. */ + while (g_queue_get_length (history.items) > config->action_history_size) + { + item = g_queue_pop_tail (history.items); + + g_hash_table_remove (history.links, item->action_name); + + gimp_action_history_item_free (item); + } + + /* Look up the action in the history. */ + link = g_hash_table_lookup (history.links, action_name); + + /* If the action is not in the history, insert it + * at the back of the history queue, possibly + * replacing the last item. + */ + if (! link) + { + if (g_queue_get_length (history.items) == config->action_history_size) + { + item = g_queue_pop_tail (history.items); + + g_hash_table_remove (history.links, item->action_name); + + gimp_action_history_item_free (item); + } + + item = gimp_action_history_item_new ( + action_name, + g_queue_get_length (history.items), + 0); + + g_queue_push_tail (history.items, item); + link = g_queue_peek_tail_link (history.items); + + g_hash_table_insert (history.links, item->action_name, link); + } + else + { + item = link->data; + } + + /* Update the history, according to the logic described + * in the comment at the beginning of the file. + */ + if (item->index > 0) + { + GList *prev_link = g_list_previous (link); + GimpActionHistoryItem *prev_item = prev_link->data; + + if (prev_item->delta == 0) + { + for (; prev_link; prev_link = g_list_previous (prev_link)) + { + prev_item = prev_link->data; + + if (prev_item->delta > 0) + break; + + prev_item->index++; + item->index--; + + prev_item->delta = item->delta; + item->delta = 0; + } + + g_queue_unlink (history.items, link); + + if (prev_link) + { + link->prev = prev_link; + link->next = prev_link->next; + + link->prev->next = link; + link->next->prev = link; + + history.items->length++; + } + else + { + g_queue_push_head_link (history.items, link); + } + } + + if (item->index > 0) + prev_item->delta--; + } + + if (item->delta < gimp_action_history_item_max_delta (item->index)) + item->delta++; +} + + +/* private functions */ + +static GimpActionHistoryItem * +gimp_action_history_item_new (const gchar *action_name, + gint index, + gint delta) +{ + GimpActionHistoryItem *item = g_slice_new (GimpActionHistoryItem); + + item->action_name = g_strdup (action_name); + item->index = index; + item->delta = CLAMP (delta, 0, gimp_action_history_item_max_delta (index)); + + return item; +} + +static void +gimp_action_history_item_free (GimpActionHistoryItem *item) +{ + g_free (item->action_name); + + g_slice_free (GimpActionHistoryItem, item); +} + +static gint +gimp_action_history_item_max_delta (gint index) +{ + return floor (MAX_DELTA * exp (log (MAX_DELTA_FALLOFF) * index)); +} |