mirror of
https://gitlab.gnome.org/GNOME/gimp.git
synced 2025-07-03 17:33:25 +00:00

windows‑display* actions are used to switch open displays and are not meant to be remapped. They are implemented so they can be reassigned each time a display is opened, closed, or moved, in order to keep them aligned with the order of displays. This commit hides them from the Shortcuts dialog to prevent remapping and avoidinconsistent behavior. To avoid hiding these actions completely and making them harder to discover, this commit removes them only from the shortcuts dialog while still showing them in the action search. (see #11685)
509 lines
14 KiB
C
509 lines
14 KiB
C
/* 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);
|
|
}
|
|
|
|
/**
|
|
* gimp_action_history_search:
|
|
* @gimp:
|
|
* @match_func:
|
|
* @keyword:
|
|
*
|
|
* 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));
|
|
}
|