gimp/app/widgets/gimpaction-history.c

510 lines
14 KiB
C
Raw Normal View History

/* 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 "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_file (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_unref (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_from_file (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);
}
app: show unavailable actions in Action Search after available ones. Some people had been complaining that they couldn't find some actions in some case, which was only because they were in states where the actions were non-sensitive. So it was "normal" (i.e. not a bug), yet I can see how it can be disturbing especially when we don't realize that an action is meant to be inactive in some given case. Of course the option to show all actions already existed in the Preferences. But as most options in Preferences, this is hardly discoverable and many people only use default settings. Moreover showing hidden action made the action search cluttered with non-sensitive actions in the middle of sensitive ones. This change gets rid of the "Show unavailable actions" settings and always show all matching actions. In order not to clutter the list with useless results, I simply updated the display logics to always show non-sensitive action after sensitive ones. Note that even non-sensitive actions will still be ordered in a better-match-on-top logics, yet they will be after sensitive actions. So the top results will be the best matches among sensitive actions (action in history), followed by various levels of matches (actions with matching labels, tooltips, different order matches, etc.); then they will be followed by best matches among non-sensitive actions, followed by the same levels of matches as sensitive ones. This way, we still keep a very relevant result and there is no need to have a settings for this.
2020-10-26 16:40:19 +01:00
/**
* gimp_action_history_search:
* @gimp:
* @match_func:
* @keyword:
*
app: show unavailable actions in Action Search after available ones. Some people had been complaining that they couldn't find some actions in some case, which was only because they were in states where the actions were non-sensitive. So it was "normal" (i.e. not a bug), yet I can see how it can be disturbing especially when we don't realize that an action is meant to be inactive in some given case. Of course the option to show all actions already existed in the Preferences. But as most options in Preferences, this is hardly discoverable and many people only use default settings. Moreover showing hidden action made the action search cluttered with non-sensitive actions in the middle of sensitive ones. This change gets rid of the "Show unavailable actions" settings and always show all matching actions. In order not to clutter the list with useless results, I simply updated the display logics to always show non-sensitive action after sensitive ones. Note that even non-sensitive actions will still be ordered in a better-match-on-top logics, yet they will be after sensitive actions. So the top results will be the best matches among sensitive actions (action in history), followed by various levels of matches (actions with matching labels, tooltips, different order matches, etc.); then they will be followed by best matches among non-sensitive actions, followed by the same levels of matches as sensitive ones. This way, we still keep a very relevant result and there is no need to have a settings for this.
2020-10-26 16:40:19 +01:00
* Search all history #GimpAction which match @keyword with function
* @match_func(action, keyword).
* It will also return inactive actions, but will discard non-visible
* actions.
*
* returns: a #GList of #GimpAction, which must be freed 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;
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);
for (actions = history.items->head, i = 0;
actions && i < config->action_history_size;
actions = g_list_next (actions), i++)
{
GimpActionHistoryItem *item = actions->data;
GAction *action;
action = g_action_map_lookup_action (G_ACTION_MAP (gimp->app), item->action_name);
if (action == NULL)
continue;
g_return_val_if_fail (GIMP_IS_ACTION (action), NULL);
if (! gimp_action_is_visible (GIMP_ACTION (action)))
continue;
if (match_func (GIMP_ACTION (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_action_search_blacklisted (action_name))
return TRUE;
return (g_str_has_suffix (action_name, "-set") ||
g_str_has_prefix (action_name, "context-") ||
g_str_has_suffix (action_name, "-internal") ||
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;
g_return_if_fail (GIMP_IS_ACTION (action));
/* 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));
}