- First do not write the protocol-version (and therefore don't read it either nor compare it to the current one). This file does not depend on the protocol version. - Secondly when reading an unknown identifier, simply print a message on stderr for debugging/reference, but ignore it up to the next closing parenthese. - Finally do not use the file-version as a terminal error as well. When we read a shortcutsrc with the wrong version, we still try to read it as best as we can. There might be issues, but it's better than fully dropping the whole list of shortcuts as it's a pretty important file (people might have spent a lot of time tweaking their shortcuts!).
342 lines
10 KiB
C
342 lines
10 KiB
C
/* GIMP - The GNU Image Manipulation Program
|
|
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
|
|
*
|
|
* shortcuts-rc.c
|
|
* Copyright (C) 2023 Jehan
|
|
*
|
|
* 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 <gegl.h>
|
|
#include <gtk/gtk.h>
|
|
|
|
#include "libgimpbase/gimpbase.h"
|
|
#include "libgimpconfig/gimpconfig.h"
|
|
|
|
#include "menus-types.h"
|
|
|
|
#include "widgets/gimpaction.h"
|
|
#include "widgets/gimpradioaction.h"
|
|
|
|
#include "shortcuts-rc.h"
|
|
|
|
#include "gimp-intl.h"
|
|
|
|
|
|
#define SHORTCUTS_RC_FILE_VERSION 1
|
|
|
|
|
|
/*
|
|
* All deserialize functions return G_TOKEN_LEFT_PAREN on success,
|
|
* or the GTokenType they would have expected but didn't get,
|
|
* or G_TOKEN_ERROR if the function already set an error itself.
|
|
*/
|
|
|
|
static GTokenType shortcuts_action_deserialize (GScanner *scanner,
|
|
GtkApplication *application);
|
|
|
|
|
|
enum
|
|
{
|
|
FILE_VERSION = 1,
|
|
ACTION,
|
|
};
|
|
|
|
|
|
gboolean
|
|
shortcuts_rc_parse (GtkApplication *application,
|
|
GFile *file,
|
|
GError **error)
|
|
{
|
|
GScanner *scanner;
|
|
gint file_version = SHORTCUTS_RC_FILE_VERSION;
|
|
GTokenType token;
|
|
|
|
g_return_val_if_fail (GTK_IS_APPLICATION (application), FALSE);
|
|
g_return_val_if_fail (G_IS_FILE (file), FALSE);
|
|
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
|
|
|
|
scanner = gimp_scanner_new_file (file, error);
|
|
|
|
if (! scanner)
|
|
return FALSE;
|
|
|
|
g_scanner_scope_add_symbol (scanner, 0,
|
|
"file-version",
|
|
GINT_TO_POINTER (FILE_VERSION));
|
|
g_scanner_scope_add_symbol (scanner, 0,
|
|
"action", GINT_TO_POINTER (ACTION));
|
|
|
|
token = G_TOKEN_LEFT_PAREN;
|
|
|
|
while (g_scanner_peek_next_token (scanner) == token ||
|
|
(token == G_TOKEN_SYMBOL &&
|
|
g_scanner_peek_next_token (scanner) == G_TOKEN_IDENTIFIER))
|
|
{
|
|
token = g_scanner_get_next_token (scanner);
|
|
|
|
switch (token)
|
|
{
|
|
case G_TOKEN_LEFT_PAREN:
|
|
token = G_TOKEN_SYMBOL;
|
|
break;
|
|
|
|
case G_TOKEN_SYMBOL:
|
|
switch (GPOINTER_TO_INT (scanner->value.v_symbol))
|
|
{
|
|
case FILE_VERSION:
|
|
token = G_TOKEN_INT;
|
|
if (gimp_scanner_parse_int (scanner, &file_version))
|
|
token = G_TOKEN_RIGHT_PAREN;
|
|
break;
|
|
|
|
case ACTION:
|
|
g_scanner_set_scope (scanner, ACTION);
|
|
token = shortcuts_action_deserialize (scanner, application);
|
|
g_scanner_set_scope (scanner, 0);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case G_TOKEN_IDENTIFIER:
|
|
g_printerr ("%s: ignoring unknown symbol '%s'.\n", G_STRFUNC, scanner->value.v_string);
|
|
while ((token = g_scanner_get_next_token (scanner)) != G_TOKEN_EOF)
|
|
{
|
|
if (token == G_TOKEN_RIGHT_PAREN)
|
|
break;
|
|
}
|
|
token = G_TOKEN_LEFT_PAREN;
|
|
break;
|
|
|
|
case G_TOKEN_RIGHT_PAREN:
|
|
token = G_TOKEN_LEFT_PAREN;
|
|
break;
|
|
|
|
default: /* do nothing */
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (file_version != SHORTCUTS_RC_FILE_VERSION)
|
|
{
|
|
g_printerr (_("Wrong shortcutsrc (%s) file format version: %d (expected: %d). "
|
|
"We tried to load shortcuts as well as possible.\n"),
|
|
gimp_file_get_utf8_name (file),
|
|
file_version, SHORTCUTS_RC_FILE_VERSION);
|
|
}
|
|
if (token != G_TOKEN_LEFT_PAREN)
|
|
{
|
|
if (token != G_TOKEN_ERROR)
|
|
{
|
|
g_scanner_get_next_token (scanner);
|
|
g_scanner_unexp_token (scanner, token, NULL, NULL, NULL,
|
|
_("fatal parse error"), TRUE);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
gimp_scanner_unref (scanner);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
shortcuts_rc_write (GtkApplication *application,
|
|
GFile *file,
|
|
GError **error)
|
|
{
|
|
GimpConfigWriter *writer;
|
|
gchar **actions;
|
|
|
|
g_return_val_if_fail (GTK_IS_APPLICATION (application), FALSE);
|
|
g_return_val_if_fail (G_IS_FILE (file), FALSE);
|
|
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
|
|
|
|
writer = gimp_config_writer_new_from_file (file,
|
|
FALSE,
|
|
"GIMP shortcutsrc\n\n"
|
|
"If you delete this file, all shortcuts "
|
|
"will be reset to defaults.",
|
|
error);
|
|
if (! writer)
|
|
return FALSE;
|
|
|
|
actions = g_action_group_list_actions (G_ACTION_GROUP (application));
|
|
|
|
gimp_config_writer_open (writer, "file-version");
|
|
gimp_config_writer_printf (writer, "%d", SHORTCUTS_RC_FILE_VERSION);
|
|
gimp_config_writer_close (writer);
|
|
|
|
gimp_config_writer_linefeed (writer);
|
|
|
|
for (gint i = 0; actions[i] != NULL; i++)
|
|
{
|
|
GimpAction *action;
|
|
gchar **accels;
|
|
gchar *detailed_name;
|
|
gboolean commented = FALSE;
|
|
|
|
action = (GimpAction *) g_action_map_lookup_action (G_ACTION_MAP (application), actions[i]);
|
|
|
|
if (GIMP_IS_RADIO_ACTION (action))
|
|
{
|
|
gint value;
|
|
|
|
g_object_get ((GObject *) action,
|
|
"value",
|
|
&value,
|
|
NULL);
|
|
detailed_name = g_strdup_printf ("app.%s(%i)", actions[i],
|
|
value);
|
|
|
|
}
|
|
else
|
|
{
|
|
detailed_name = g_strdup_printf ("app.%s", actions[i]);
|
|
}
|
|
|
|
accels = gtk_application_get_accels_for_action (application, detailed_name);
|
|
|
|
if (gimp_action_use_default_accels (action))
|
|
commented = TRUE;
|
|
|
|
gimp_config_writer_comment_mode (writer, commented);
|
|
|
|
gimp_config_writer_open (writer, "action");
|
|
gimp_config_writer_string (writer, actions[i]);
|
|
|
|
for (gint j = 0; accels[j]; j++)
|
|
gimp_config_writer_string (writer, accels[j]);
|
|
|
|
gimp_config_writer_close (writer);
|
|
gimp_config_writer_comment_mode (writer, FALSE);
|
|
|
|
g_strfreev (accels);
|
|
g_free (detailed_name);
|
|
}
|
|
|
|
g_strfreev (actions);
|
|
|
|
return gimp_config_writer_finish (writer, "end of shortcutsrc", error);
|
|
}
|
|
|
|
|
|
/* Private functions */
|
|
|
|
static GTokenType
|
|
shortcuts_action_deserialize (GScanner *scanner,
|
|
GtkApplication *application)
|
|
{
|
|
GStrvBuilder *builder;
|
|
gchar *action_name;
|
|
gchar *accel;
|
|
gchar **accels;
|
|
|
|
if (! gimp_scanner_parse_string (scanner, &action_name))
|
|
return G_TOKEN_STRING;
|
|
|
|
builder = g_strv_builder_new ();
|
|
while (gimp_scanner_parse_string (scanner, &accel))
|
|
{
|
|
gchar **dup_actions;
|
|
gboolean add_accel = TRUE;
|
|
guint accelerator_key = 0;
|
|
GdkModifierType accelerator_mods = 0;
|
|
|
|
gtk_accelerator_parse (accel, &accelerator_key, &accelerator_mods);
|
|
if (accelerator_key == 0 && accelerator_mods == 0)
|
|
{
|
|
g_printerr ("INFO: invalid accelerator '%s' on '%s'.\n"
|
|
" Removing this accelerator.\n",
|
|
accel, action_name);
|
|
g_free (accel);
|
|
continue;
|
|
}
|
|
dup_actions = gtk_application_get_actions_for_accel (application, accel);
|
|
|
|
for (gint i = 0; dup_actions[i] != NULL; i++)
|
|
{
|
|
GimpAction *conflict_action;
|
|
gchar *left_paren_ptr = strchr (dup_actions[i], '(');
|
|
|
|
if (left_paren_ptr)
|
|
*left_paren_ptr = '\0'; /* ignore target part of detailed name */
|
|
|
|
/* dup_actions[i] will be the detailed name prefixed with "app." */
|
|
if (g_strcmp0 (dup_actions[i] + 4, action_name) == 0)
|
|
continue;
|
|
|
|
conflict_action = (GimpAction *) g_action_map_lookup_action (G_ACTION_MAP (application),
|
|
dup_actions[i] + 4);
|
|
if (gimp_action_use_default_accels (conflict_action))
|
|
{
|
|
/* We might simply not have scanned this action's accelerators yet
|
|
* in the shortcutsrc file. Just delete its current accelerators
|
|
* without any message.
|
|
*/
|
|
gimp_action_set_accels (conflict_action, NULL);
|
|
}
|
|
else
|
|
{
|
|
g_printerr ("INFO: duplicate accelerator '%s' on '%s' and '%s'.\n"
|
|
" Removing the accelerator from '%s'.\n",
|
|
accel, action_name, dup_actions[i], action_name);
|
|
add_accel = FALSE;
|
|
break;
|
|
}
|
|
}
|
|
g_strfreev (dup_actions);
|
|
|
|
if (add_accel)
|
|
g_strv_builder_add (builder, accel);
|
|
|
|
g_free (accel);
|
|
}
|
|
accels = g_strv_builder_end (builder);
|
|
|
|
if (g_action_group_has_action (G_ACTION_GROUP (application), action_name))
|
|
{
|
|
GimpAction *action;
|
|
gchar *detailed_name;
|
|
|
|
action = (GimpAction *) g_action_map_lookup_action (G_ACTION_MAP (application),
|
|
action_name);
|
|
|
|
detailed_name = g_strdup_printf ("app.%s", action_name);
|
|
gimp_action_set_accels (action, (const gchar **) accels);
|
|
g_free (detailed_name);
|
|
}
|
|
else
|
|
{
|
|
/* Don't set a breaking error, just output on stderr, so that we can make a
|
|
* notice while still loading other actions.
|
|
*/
|
|
g_printerr ("INFO: not existing action '%s' was ignored from the shortcutsrc file.\n",
|
|
action_name);
|
|
}
|
|
|
|
g_strv_builder_unref (builder);
|
|
g_free (action_name);
|
|
g_strfreev (accels);
|
|
|
|
if (! gimp_scanner_parse_token (scanner, G_TOKEN_RIGHT_PAREN))
|
|
return G_TOKEN_RIGHT_PAREN;
|
|
|
|
return G_TOKEN_LEFT_PAREN;
|
|
}
|