/*
* Copyright © 2025 Red Hat , Inc
*
* This library is free software ; you can redistribute it and / or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation ; either
* version 2 . 1 of the License , or ( at your option ) any later version .
*
* This library 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
* Lesser General Public License for more details .
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library . If not , see < http : //www.gnu.org/licenses/>.
*
* SPDX - License - Identifier : LGPL - 2 . 1 - or - later
*
* Authors : Matthias Clasen < mclasen @ redhat . com >
*/
#include "icon-editor-window.h"
#include "paintable-editor.h"
#include "path-paintable.h"
#include "border-paintable.h"
#include "state-editor.h"
#include "gtk/svg/gtksvgelementprivate.h"
#include "gtk/svg/gtksvgpaintprivate.h"
#include "gtk/svg/gtksvgserializeprivate.h"
#include <glib/gstdio.h>
struct _IconEditorWindow
{
GtkApplicationWindow parent;
GFile *file;
PathPaintable *paintable;
PathPaintable *orig_paintable;
GBinding *playing_binding;
gboolean changed;
gboolean show_sidebar;
gboolean show_thumbnails;
gboolean show_bounds;
gboolean show_spines;
gboolean show_grid;
gboolean invert_colors;
gboolean playing;
gboolean compat_classes;
float weight;
unsigned int state;
GtkStack *main_stack;
GtkImage *empty_logo;
union {
GtkImage *images[24 ];
struct {
GtkImage *image48_0, *image48_1, *image48_2, *image48_3;
GtkImage *image48_4, *image48_5, *image48_6, *image48_7;
GtkImage *image24_0, *image24_1, *image24_2, *image24_3;
GtkImage *image24_4, *image24_5, *image24_6, *image24_7;
GtkImage *image24_8, *image24_9, *image24_10, *image24_11;
GtkImage *image24_12, *image24_13, *image24_14, *image24_15;
};
};
union {
GtkImage *examples[6 ];
struct {
GtkImage *example1, *example2, *example3;
GtkImage *example4, *example5, *example6;
};
};
PaintableEditor *paintable_editor;
GtkCssProvider *paintable_style;
};
struct _IconEditorWindowClass
{
GtkApplicationWindowClass parent_class;
};
enum
{
PROP_PAINTABLE = 1 ,
PROP_CHANGED,
PROP_SHOW_SIDEBAR,
PROP_SHOW_BOUNDS,
PROP_SHOW_SPINES,
PROP_SHOW_GRID,
PROP_INVERT_COLORS,
PROP_WEIGHT,
PROP_STATE,
PROP_PLAYING,
PROP_COMPAT_CLASSES,
NUM_PROPERTIES,
};
static GParamSpec *properties[NUM_PROPERTIES];
G_DEFINE_TYPE(IconEditorWindow, icon_editor_window, GTK_TYPE_APPLICATION_WINDOW);
/* {{{ Setters */
static void
icon_editor_window_set_show_sidebar (IconEditorWindow *self,
gboolean show_sidebar)
{
if (self->show_sidebar == show_sidebar)
return ;
if (show_sidebar)
{
GAction *action;
action = g_action_map_lookup_action (G_ACTION_MAP (self), "close" );
g_simple_action_set_enabled (G_SIMPLE_ACTION (action), TRUE );
gtk_stack_set_visible_child_name (self->main_stack, "content" );
}
self->show_sidebar = show_sidebar;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_SIDEBAR]);
}
static void
icon_editor_window_set_show_bounds (IconEditorWindow *self,
gboolean show_bounds)
{
if (self->show_bounds == show_bounds)
return ;
self->show_bounds = show_bounds;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_BOUNDS]);
}
static void
icon_editor_window_set_show_spines (IconEditorWindow *self,
gboolean show_spines)
{
if (self->show_spines == show_spines)
return ;
self->show_spines = show_spines;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_SPINES]);
}
static void
icon_editor_window_set_show_grid (IconEditorWindow *self,
gboolean show_grid)
{
if (self->show_grid == show_grid)
return ;
self->show_grid = show_grid;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SHOW_GRID]);
}
static void
icon_editor_window_set_invert_colors (IconEditorWindow *self,
gboolean invert_colors)
{
GtkSettings *settings;
if (self->invert_colors == invert_colors)
return ;
self->invert_colors = invert_colors;
settings = gtk_widget_get_settings (GTK_WIDGET (self));
if (invert_colors)
{
GtkInterfaceColorScheme color_scheme;
g_object_get (settings, "gtk-interface-color-scheme" , &color_scheme, NULL);
if (color_scheme == GTK_INTERFACE_COLOR_SCHEME_DARK)
color_scheme = GTK_INTERFACE_COLOR_SCHEME_LIGHT;
else
color_scheme = GTK_INTERFACE_COLOR_SCHEME_DARK;
g_object_set (settings, "gtk-interface-color-scheme" , color_scheme, NULL);
}
else
gtk_settings_reset_property (settings, "gtk-interface-color-scheme" );
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_INVERT_COLORS]);
}
static void
update_icon_paintable_style (IconEditorWindow *self,
float weight)
{
char *css;
css = g_strdup_printf ("image.icon-paintable-preview { -gtk-icon-weight: %f; }" , weight);
if (!self->paintable_style)
{
self->paintable_style = gtk_css_provider_new ();
gtk_style_context_add_provider_for_display (gtk_widget_get_display (GTK_WIDGET (self)),
GTK_STYLE_PROVIDER (self->paintable_style),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
}
gtk_css_provider_load_from_string (self->paintable_style, css);
g_free (css);
}
static void
icon_editor_window_set_weight (IconEditorWindow *self,
float weight)
{
if (self->weight == weight)
return ;
self->weight = weight;
path_paintable_set_weight (self->paintable, weight);
update_icon_paintable_style (self, weight);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WEIGHT]);
}
static void
icon_editor_window_set_state (IconEditorWindow *self,
unsigned int state)
{
if (self->state == state)
return ;
self->state = state;
path_paintable_set_state (self->paintable, state);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STATE]);
}
static void
icon_editor_window_set_changed (IconEditorWindow *self,
gboolean changed)
{
GAction *action;
if (self->changed == changed)
return ;
self->changed = changed;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CHANGED]);
action = g_action_map_lookup_action (G_ACTION_MAP (self), "save" );
g_simple_action_set_enabled (G_SIMPLE_ACTION (action), changed);
action = g_action_map_lookup_action (G_ACTION_MAP (self), "revert" );
g_simple_action_set_enabled (G_SIMPLE_ACTION (action), changed);
}
static void
icon_editor_window_set_playing (IconEditorWindow *self,
gboolean playing)
{
if (self->playing == playing)
return ;
self->playing = playing;
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PLAYING]);
}
static void
icon_editor_window_set_compat_classes (IconEditorWindow *self,
gboolean compat_classes)
{
if (self->compat_classes == compat_classes)
return ;
self->compat_classes = compat_classes;
icon_editor_window_set_changed (self, TRUE );
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMPAT_CLASSES]);
}
/* }}} */
/* {{{ Callbacks, utilities */
static void
toggle_sidebar (IconEditorWindow *self)
{
icon_editor_window_set_show_sidebar (self, !self->show_sidebar);
}
static void
paintable_changed (IconEditorWindow *self)
{
gboolean changed;
changed = !gtk_svg_equal (path_paintable_get_svg (self->paintable),
path_paintable_get_svg (self->orig_paintable));
icon_editor_window_set_changed (self, changed);
}
static void
show_error (IconEditorWindow *self,
const char *title,
const char *detail)
{
g_autoptr (GtkAlertDialog) alert = NULL;
alert = gtk_alert_dialog_new ("%s" , title);
gtk_alert_dialog_set_detail (alert, detail);
gtk_alert_dialog_show (alert, GTK_WINDOW (self));
}
static gboolean
is_regular_file (GFile *file)
{
const char *uri;
if (file == NULL)
return FALSE ;
uri = g_file_get_uri (file);
if (uri != NULL && g_str_has_prefix (uri, "resource://"))
return FALSE ;
return TRUE ;
}
/* }}} */
/* {{{ Opening/Importing */
static void
load_error (IconEditorWindow *self,
const char *message)
{
show_error (self, "Loading failed" , message);
}
static void
set_random_icons (IconEditorWindow *self)
{
const char *names[] = {
"bookmark-new-symbolic" ,
"color-select-symbolic" ,
"document-open-recent-symbolic" ,
"document-open-symbolic" ,
"document-save-as-symbolic" ,
"document-save-symbolic" ,
"edit-clear-all-symbolic" ,
"edit-clear-symbolic-rtl" ,
"edit-clear-symbolic" ,
"edit-copy-symbolic" ,
"edit-cut-symbolic" ,
"edit-delete-symbolic" ,
"edit-find-symbolic" ,
"edit-paste-symbolic" ,
"edit-select-all-symbolic" ,
"find-location-symbolic" ,
"folder-new-symbolic" ,
"function-linear-symbolic" ,
"go-down-symbolic" ,
"go-next-symbolic-rtl" ,
"go-next-symbolic" ,
"go-previous-symbolic-rtl" ,
"go-previous-symbolic" ,
"go-up-symbolic" ,
"info-outline-symbolic" ,
"insert-image-symbolic" ,
"insert-object-symbolic" ,
"list-add-symbolic" ,
"list-remove-all-symbolic" ,
"list-remove-symbolic" ,
"media-eject-symbolic" ,
"media-playback-pause-symbolic" ,
"media-playback-start-symbolic" ,
"media-playback-stop-symbolic" ,
"media-record-symbolic" ,
"object-select-symbolic" ,
"open-menu-symbolic" ,
"pan-down-symbolic" ,
"pan-end-symbolic-rtl" ,
"pan-end-symbolic" ,
"pan-start-symbolic-rtl" ,
"pan-start-symbolic" ,
"pan-up-symbolic" ,
"system-run-symbolic" ,
"system-search-symbolic" ,
"value-decrease-symbolic" ,
"value-increase-symbolic" ,
"view-conceal-symbolic" ,
"view-grid-symbolic" ,
"view-list-symbolic" ,
"view-more-symbolic" ,
"view-refresh-symbolic" ,
"view-reveal-symbolic" ,
"window-close-symbolic" ,
"window-maximize-symbolic" ,
"window-minimize-symbolic" ,
"window-restore-symbolic" ,
"zoom-in-symbolic" ,
"zoom-original-symbolic" ,
"zoom-out-symbolic" ,
"emoji-activities-symbolic" ,
"emoji-body-symbolic" ,
"emoji-flags-symbolic" ,
"emoji-food-symbolic" ,
"emoji-nature-symbolic" ,
"emoji-objects-symbolic" ,
"emoji-people-symbolic" ,
"emoji-recent-symbolic" ,
"emoji-symbols-symbolic" ,
"emoji-travel-symbolic" ,
"audio-volume-high-symbolic" ,
"audio-volume-low-symbolic" ,
"audio-volume-medium-symbolic" ,
"audio-volume-muted-symbolic" ,
"caps-lock-symbolic" ,
"changes-allow-symbolic" ,
"changes-prevent-symbolic" ,
"dialog-error-symbolic" ,
"dialog-information-symbolic" ,
"dialog-password-symbolic" ,
"dialog-question-symbolic" ,
"dialog-warning-symbolic" ,
"display-brightness-symbolic" ,
"media-playlist-repeat-symbolic" ,
"orientation-landscape-inverse-symbolic" ,
"orientation-landscape-symbolic" ,
"orientation-portrait-inverse-symbolic" ,
"orientation-portrait-symbolic" ,
"process-working-symbolic" ,
"switch-off-symbolic" ,
"switch-on-symbolic" ,
};
g_autoptr (GtkBitset) used = gtk_bitset_new_empty ();
GtkImage *image;
for (unsigned int i = 0 ; i < 24 ; i++)
{
uint32_t r;
g_autofree char *path;
g_autoptr (GtkSvg) paintable = NULL;
do {
r = g_random_int_range (0 , (uint32_t) G_N_ELEMENTS (names));
} while (gtk_bitset_contains (used, r));
path = g_strconcat ("/org/gtk/libgtk/icons/" , names[r], ".svg" , NULL);
paintable = gtk_svg_new_from_resource (path);
gtk_svg_set_state (paintable, 0 );
gtk_svg_play (paintable);
g_object_bind_property (self, "weight" ,
paintable, "weight" ,
G_BINDING_SYNC_CREATE);
gtk_image_set_from_paintable (self->images[i], GDK_PAINTABLE (paintable));
gtk_widget_set_tooltip_text (GTK_WIDGET (self->images[i]), names[r]);
gtk_bitset_add (used, r);
}
image = self->images[g_random_int_range (0 , 3 )];
gtk_image_set_from_paintable (image, GDK_PAINTABLE (self->paintable));
gtk_widget_set_tooltip_text (GTK_WIDGET (image), NULL);
image = self->images[g_random_int_range (4 , 7 )];
gtk_image_set_from_paintable (image, GDK_PAINTABLE (self->paintable));
gtk_widget_set_tooltip_text (GTK_WIDGET (image), NULL);
image = self->images[g_random_int_range (8 , 15 )];
gtk_image_set_from_paintable (image, GDK_PAINTABLE (self->paintable));
gtk_widget_set_tooltip_text (GTK_WIDGET (image), NULL);
image = self->images[g_random_int_range (16 , 23 )];
gtk_image_set_from_paintable (image, GDK_PAINTABLE (self->paintable));
gtk_widget_set_tooltip_text (GTK_WIDGET (image), NULL);
}
static void
icon_editor_window_set_paintable (IconEditorWindow *self,
PathPaintable *paintable)
{
if (self->paintable == paintable)
return ;
if (self->paintable)
{
g_signal_handlers_disconnect_by_func (self->paintable, paintable_changed, self);
g_clear_object (&self->playing_binding);
}
g_set_object (&self->paintable, paintable);
if (self->paintable)
{
path_paintable_set_frame_clock (self->paintable, gtk_widget_get_frame_clock (GTK_WIDGET (self)));
icon_editor_window_set_state (self, path_paintable_get_state (paintable));
path_paintable_set_weight (self->paintable, self->weight);
g_signal_connect_swapped (self->paintable, "changed" ,
G_CALLBACK (paintable_changed), self);
self->playing_binding = g_object_bind_property (self, "playing" ,
self->paintable, "playing" ,
G_BINDING_SYNC_CREATE);
set_random_icons (self);
if (svg_element_get_n_children (path_paintable_get_svg (self->paintable)->content) > 0 )
icon_editor_window_set_show_sidebar (self, TRUE );
}
g_clear_object (&self->orig_paintable);
if (paintable)
self->orig_paintable = path_paintable_copy (paintable);
icon_editor_window_set_changed (self, FALSE );
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PAINTABLE]);
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CHANGED]);
}
static gboolean
load_bytes (IconEditorWindow *self,
GBytes *bytes)
{
g_autoptr (GError) error = NULL;
g_autoptr (PathPaintable) paintable = NULL;
if (!g_utf8_validate (g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes), NULL))
{
load_error (self, "Invalid UTF-8" );
return FALSE ;
}
paintable = path_paintable_new_from_bytes (bytes, &error);
if (!paintable)
{
load_error (self, error->message);
return FALSE ;
}
icon_editor_window_set_paintable (self, paintable);
return TRUE ;
}
static gboolean
load_file_contents (IconEditorWindow *self,
GFile *file)
{
g_autoptr (GBytes) bytes = NULL;
g_autoptr (GError) error = NULL;
bytes = g_file_load_bytes (file, NULL, NULL, &error);
if (!bytes)
{
load_error (self, error->message);
return FALSE ;
}
return load_bytes (self, bytes);
}
static gboolean
file_drop (GtkDropTarget *target,
const GValue *value,
double x,
double y,
gpointer user_data)
{
if (G_VALUE_HOLDS (value, G_TYPE_FILE))
{
GtkWidget *self = gtk_event_controller_get_widget (GTK_EVENT_CONTROLLER (target));
GFile *file = g_value_get_object (value);
icon_editor_window_load (ICON_EDITOR_WINDOW (self), file);
return TRUE ;
}
return FALSE ;
}
static void
open_response_cb (GObject *source,
GAsyncResult *result,
void *user_data)
{
GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
IconEditorWindow *self = user_data;
g_autoptr (GFile) file = NULL;
g_autoptr (GError) error = NULL;
file = gtk_file_dialog_open_finish (dialog, result, &error);
if (!file)
{
if (!g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
load_error (self, error->message);
return ;
}
icon_editor_window_load (self, file);
}
static void
show_open_filechooser (IconEditorWindow *self)
{
g_autoptr (GtkFileDialog) dialog = NULL;
g_autoptr (GListStore) filters = NULL;
GtkFileFilter *filter;
filters = g_list_store_new (GTK_TYPE_FILE_FILTER);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, "All files" );
gtk_file_filter_add_pattern (filter, "*" );
g_list_store_append (filters, filter);
g_object_unref (filter);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, "SVG files" );
gtk_file_filter_add_mime_type (filter, "image/svg+xml" );
g_list_store_append (filters, filter);
g_object_unref (filter);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, "GTK icons" );
gtk_file_filter_add_mime_type (filter, "image/x-gtk-path-animation" );
gtk_file_filter_add_pattern (filter, "*.gpa" );
g_list_store_append (filters, filter);
g_object_unref (filter);
dialog = gtk_file_dialog_new ();
gtk_file_dialog_set_title (dialog, "Open icon file" );
gtk_file_dialog_set_filters (dialog, G_LIST_MODEL (filters));
if (is_regular_file (self->file))
{
gtk_file_dialog_set_initial_file (dialog, self->file);
}
else
{
g_autoptr (GFile) cwd = g_file_new_for_path ("." );
gtk_file_dialog_set_initial_folder (dialog, cwd);
}
gtk_file_dialog_open (dialog, GTK_WINDOW (self),
NULL, open_response_cb, self);
}
static gboolean
icon_editor_window_load_file (IconEditorWindow *self,
GFile *file)
{
g_autofree char *basename = NULL;
if (!load_file_contents (self, file))
return FALSE ;
g_set_object (&self->file, file);
basename = g_file_get_basename (file);
gtk_window_set_title (GTK_WINDOW (self), basename);
return TRUE ;
}
/* }}} */
/* {{{ Saving/Exporting */
static void
set_compat (SvgElement *shape,
gpointer data)
{
IconEditorWindow *self = data;
if (!svg_element_type_is_graphical (svg_element_get_type (shape)))
return ;
svg_element_parse_classes (shape, NULL);
if (self->compat_classes)
{
SvgValue *value;
GStrvBuilder *builder;
builder = g_strv_builder_new ();
value = ref_value (shape, SVG_PROPERTY_FILL);
switch ((unsigned int ) svg_paint_get_kind (value))
{
case PAINT_SYMBOLIC:
switch (svg_paint_get_symbolic (value))
{
case GTK_SYMBOLIC_COLOR_FOREGROUND:
g_strv_builder_add_many (builder, "foreground" , "foreground-fill" , NULL);
break ;
case GTK_SYMBOLIC_COLOR_ERROR:
g_strv_builder_add_many (builder, "error" , "error-fill" , NULL);
break ;
case GTK_SYMBOLIC_COLOR_WARNING:
g_strv_builder_add_many (builder, "warning" , "warning-fill" , NULL);
break ;
case GTK_SYMBOLIC_COLOR_SUCCESS:
g_strv_builder_add_many (builder, "success" , "success-fill" , NULL);
break ;
case GTK_SYMBOLIC_COLOR_ACCENT:
default :
break ;
}
break ;
case PAINT_NONE:
g_strv_builder_add (builder, "transparent-fill" );
break ;
default :
break ;
}
svg_value_unref (value);
value = ref_value (shape, SVG_PROPERTY_STROKE);
switch ((unsigned int ) svg_paint_get_kind (value))
{
case PAINT_SYMBOLIC:
switch (svg_paint_get_symbolic (value))
{
case GTK_SYMBOLIC_COLOR_FOREGROUND:
g_strv_builder_add (builder, "foreground-stroke" );
break ;
case GTK_SYMBOLIC_COLOR_ERROR:
g_strv_builder_add (builder, "error-stroke" );
break ;
case GTK_SYMBOLIC_COLOR_WARNING:
g_strv_builder_add (builder, "warning-stroke" );
break ;
case GTK_SYMBOLIC_COLOR_SUCCESS:
g_strv_builder_add (builder, "success-stroke" );
break ;
case GTK_SYMBOLIC_COLOR_ACCENT:
default :
break ;
}
break ;
default :
break ;
}
svg_value_unref (value);
svg_element_take_classes (shape, g_strv_builder_end (builder));
}
}
static void
apply_compat_classes (IconEditorWindow *window,
GtkSvg *svg)
{
svg_element_foreach (svg->content, set_compat, window);
}
static void
save_error (IconEditorWindow *self,
const char *message)
{
show_error (self, "Saving failed" , message);
}
static void
save_to_file (IconEditorWindow *self,
GFile *file)
{
GtkSvg *svg;
g_autoptr (GBytes) bytes = NULL;
g_autoptr (GError) error = NULL;
svg = path_paintable_get_svg (self->paintable);
apply_compat_classes (self, svg);
bytes = gtk_svg_serialize (svg);
if (!g_file_replace_contents (file,
g_bytes_get_data (bytes, NULL),
g_bytes_get_size (bytes),
NULL, FALSE ,
G_FILE_CREATE_NONE,
NULL,
NULL,
&error))
{
save_error (self, error->message);
return ;
}
g_set_object (&self->file, file);
g_set_object (&self->orig_paintable, path_paintable_copy (self->paintable));
icon_editor_window_set_changed (self, FALSE );
}
typedef void (* FollowupCallback) (IconEditorWindow *self,
gpointer data);
typedef struct
{
FollowupCallback callback;
gpointer data;
GDestroyNotify destroy;
} FollowupData;
static void
followup_data_free (gpointer data)
{
FollowupData *fd = data;
if (fd->destroy)
fd->destroy (fd->data);
g_free (data);
}
static void
save_response_cb (GObject *source,
GAsyncResult *result,
void *user_data)
{
GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
IconEditorWindow *self = user_data;
g_autoptr (GFile) file = NULL;
g_autoptr (GError) error = NULL;
FollowupData *data = g_object_get_data (G_OBJECT (dialog), "followup" );
file = gtk_file_dialog_save_finish (dialog, result, &error);
if (!file)
{
if (!g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
save_error (self, error->message);
return ;
}
save_to_file (self, file);
if (data)
data->callback (self, data->data);
}
static void
show_save_filechooser_with_followup (IconEditorWindow *self,
FollowupData *data)
{
g_autoptr (GtkFileDialog) dialog = NULL;
dialog = gtk_file_dialog_new ();
gtk_file_dialog_set_title (dialog, "Save icon" );
if (is_regular_file (self->file))
{
gtk_file_dialog_set_initial_file (dialog, self->file);
}
else
{
g_autoptr (GFile) cwd = g_file_new_for_path ("." );
gtk_file_dialog_set_initial_folder (dialog, cwd);
gtk_file_dialog_set_initial_name (dialog, "demo.gpa" );
}
if (data)
g_object_set_data_full (G_OBJECT (dialog), "followup" , data, followup_data_free);
gtk_file_dialog_save (dialog,
GTK_WINDOW (self),
NULL,
save_response_cb, self);
}
static void
show_save_filechooser (IconEditorWindow *self)
{
show_save_filechooser_with_followup (self, NULL);
}
static void
export_to_file (IconEditorWindow *self,
GFile *file)
{
GtkSvg *svg;
g_autoptr (GBytes) bytes = NULL;
g_autoptr (GError) error = NULL;
svg = path_paintable_get_svg (self->paintable);
apply_compat_classes (self, svg);
bytes = gtk_svg_serialize_full (svg,
NULL, 0 ,
GTK_SVG_SERIALIZE_EXPAND_GPA_ATTRS |
GTK_SVG_SERIALIZE_NO_COMPAT);
if (!g_file_replace_contents (file,
g_bytes_get_data (bytes, NULL),
g_bytes_get_size (bytes),
NULL, FALSE ,
G_FILE_CREATE_NONE,
NULL,
NULL,
&error))
{
save_error (self, error->message);
return ;
}
}
static void
export_response_cb (GObject *source,
GAsyncResult *result,
void *user_data)
{
GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
IconEditorWindow *self = user_data;
g_autoptr (GFile) file = NULL;
g_autoptr (GError) error = NULL;
file = gtk_file_dialog_save_finish (dialog, result, &error);
if (!file)
{
if (!g_error_matches (error, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED))
save_error (self, error->message);
return ;
}
export_to_file (self, file);
}
static void
show_export_filechooser (IconEditorWindow *self)
{
g_autoptr (GtkFileDialog) dialog = NULL;
g_autoptr (GFile) cwd = g_file_new_for_path ("." );
dialog = gtk_file_dialog_new ();
gtk_file_dialog_set_title (dialog, "Export as SVG" );
gtk_file_dialog_set_initial_folder (dialog, cwd);
gtk_file_dialog_set_initial_name (dialog, "demo.svg" );
gtk_file_dialog_save (dialog,
GTK_WINDOW (self),
NULL,
export_response_cb, self);
}
/* }}} */
/* {{{ GtkWindow implementation */
static void
quit_alert_done (GObject *source,
GAsyncResult *result,
gpointer data)
{
GtkAlertDialog *alert = GTK_ALERT_DIALOG (source);
IconEditorWindow *self = ICON_EDITOR_WINDOW (data);
g_autoptr (GError) error = NULL;
int res;
res = gtk_alert_dialog_choose_finish (alert, result, &error);
if (res == -1 )
return ;
if (res == 0 )
{
/* Don't try to save resource files */
if (is_regular_file (self->file))
save_to_file (self, self->file);
else
show_save_filechooser (self);
}
else if (res == 1 )
{
self->changed = FALSE ;
gtk_window_close (GTK_WINDOW (self));
}
}
static gboolean
icon_editor_window_close_request (GtkWindow *window)
{
IconEditorWindow *self = ICON_EDITOR_WINDOW (window);
if (self->changed)
{
g_autoptr (GtkAlertDialog) alert = NULL;
const char *buttons[] = { "Save" , "Quit" , NULL };
alert = gtk_alert_dialog_new ("Unsaved changes" );
gtk_alert_dialog_set_detail (alert, "The icon contains unsaved changes." );
gtk_alert_dialog_set_modal (alert, TRUE );
gtk_alert_dialog_set_buttons (alert, buttons);
gtk_alert_dialog_set_default_button (alert, 0 );
gtk_alert_dialog_set_cancel_button (alert, 0 );
gtk_alert_dialog_choose (alert, window, NULL, quit_alert_done, self);
return TRUE ;
}
return FALSE ;
}
/* }}} */
/* {{{ Actions */
static void
file_open (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
show_open_filechooser (self);
}
static void
file_save (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
if (is_regular_file (self->file))
save_to_file (self, self->file);
else
show_save_filechooser (self);
}
static void
file_save_as (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
show_save_filechooser (self);
}
static void
file_export (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
show_export_filechooser (self);
}
static void
back_to_empty (IconEditorWindow *self)
{
g_autoptr (PathPaintable) paintable = path_paintable_new ();
GAction *action;
icon_editor_window_set_paintable (self, paintable);
icon_editor_window_set_show_sidebar (self, FALSE );
gtk_stack_set_visible_child_name (self->main_stack, "empty" );
action = g_action_map_lookup_action (G_ACTION_MAP (self), "close" );
g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE );
}
static void
close_alert_done (GObject *source,
GAsyncResult *result,
gpointer data)
{
GtkAlertDialog *alert = GTK_ALERT_DIALOG (source);
IconEditorWindow *self = ICON_EDITOR_WINDOW (data);
g_autoptr (GError) error = NULL;
int res;
res = gtk_alert_dialog_choose_finish (alert, result, &error);
if (res == -1 )
return ;
if (res == 0 )
{
if (is_regular_file (self->file))
save_to_file (self, self->file);
else
show_save_filechooser (self);
}
else if (res == 1 )
{
back_to_empty (self);
}
}
static void
file_close (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
if (self->changed)
{
g_autoptr (GtkAlertDialog) alert = NULL;
const char *buttons[] = { "Save" , "Close" , NULL };
alert = gtk_alert_dialog_new ("Unsaved changes" );
gtk_alert_dialog_set_detail (alert, "The icon contains unsaved changes." );
gtk_alert_dialog_set_modal (alert, TRUE );
gtk_alert_dialog_set_buttons (alert, buttons);
gtk_alert_dialog_set_default_button (alert, 0 );
gtk_alert_dialog_choose (alert, GTK_WINDOW (self), NULL, close_alert_done, self);
}
else
{
back_to_empty (self);
}
}
static void
revert_changes (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
if (!self->paintable)
return ;
path_paintable_set_state (self->orig_paintable, path_paintable_get_state (self->paintable));
icon_editor_window_set_paintable (self, self->orig_paintable);
}
static void
add_element (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
paintable_editor_add_element (self->paintable_editor);
}
static void
edit_states (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
StateEditor *editor;
editor = state_editor_new ();
gtk_window_set_transient_for (GTK_WINDOW (editor), GTK_WINDOW (self));
state_editor_set_paintable (editor, self->paintable);
gtk_window_present (GTK_WINDOW (editor));
}
static void
show_sidebar (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
icon_editor_window_set_show_sidebar (self, TRUE );
}
static void
open_example (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
g_autofree char *uri = NULL;
g_autoptr (GFile) file = NULL;
uri = g_strconcat ("resource:///org/gtk/Shaper/",
g_variant_get_string (parameter, NULL),
NULL);
file = g_file_new_for_uri (uri);
icon_editor_window_load (self, file);
}
static void
reshuffle (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
set_random_icons (self);
}
static void
set_sidebar_contents (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
IconEditorWindow *self = user_data;
paintable_editor_set_show_xml (self->paintable_editor, strcmp (g_variant_get_string (parameter, NULL), "xml" ) == 0 );
g_simple_action_set_state (action, parameter);
}
static GActionEntry win_entries[] = {
{ "open" , file_open, NULL, NULL, NULL },
{ "save" , file_save, NULL, NULL, NULL },
{ "save-as" , file_save_as, NULL, NULL, NULL },
{ "export" , file_export, NULL, NULL, NULL },
{ "revert" , revert_changes, NULL, NULL, NULL },
{ "close" , file_close, NULL, NULL, NULL },
{ "add-element" , add_element, NULL, NULL, NULL },
{ "edit-states" , edit_states, NULL, NULL, NULL },
{ "show-sidebar" , show_sidebar, NULL, NULL, NULL },
{ "open-example" , open_example, "s" , NULL, NULL },
{ "reshuffle" , reshuffle, NULL, NULL, NULL },
{ "set-sidebar-contents" , NULL, "s" , "'controls'" , set_sidebar_contents },
};
/* }}} */
/* {{{ GObject boilerplate */
static void
icon_editor_window_init (IconEditorWindow *self)
{
g_autoptr (GPropertyAction) action = NULL;
g_autoptr (PathPaintable) paintable = path_paintable_new ();
self->weight = 400 ;
self->state = 0 ;
self->playing = TRUE ;
self->compat_classes = TRUE ;
gtk_widget_init_template (GTK_WIDGET (self));
g_action_map_add_action_entries (G_ACTION_MAP (self),
win_entries, G_N_ELEMENTS (win_entries),
self);
g_set_object (&action, g_property_action_new ("invert-colors" , self, "invert-colors" ));
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
g_set_object (&action, g_property_action_new ("show-bounds" , self, "show-bounds" ));
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
g_set_object (&action, g_property_action_new ("show-spines" , self, "show-spines" ));
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
g_set_object (&action, g_property_action_new ("show-grid" , self, "show-grid" ));
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
g_set_object (&action, g_property_action_new ("set-playing" , self, "playing" ));
g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (action));
g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (self), "save" )), FALSE );
g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (self), "revert" )), FALSE );
g_simple_action_set_enabled (G_SIMPLE_ACTION (g_action_map_lookup_action (G_ACTION_MAP (self), "close" )), FALSE );
icon_editor_window_set_paintable (self, paintable);
}
static void
icon_editor_window_set_property (GObject *object,
unsigned int prop_id,
const GValue *value,
GParamSpec *pspec)
{
IconEditorWindow *self = ICON_EDITOR_WINDOW (object);
switch (prop_id)
{
case PROP_CHANGED:
icon_editor_window_set_changed (self, g_value_get_boolean (value));
break ;
case PROP_PAINTABLE:
icon_editor_window_set_paintable (self, g_value_get_object (value));
break ;
case PROP_SHOW_SIDEBAR:
icon_editor_window_set_show_sidebar (self, g_value_get_boolean (value));
break ;
case PROP_SHOW_BOUNDS:
icon_editor_window_set_show_bounds (self, g_value_get_boolean (value));
break ;
case PROP_SHOW_SPINES:
icon_editor_window_set_show_spines (self, g_value_get_boolean (value));
break ;
case PROP_SHOW_GRID:
icon_editor_window_set_show_grid (self, g_value_get_boolean (value));
break ;
case PROP_INVERT_COLORS:
icon_editor_window_set_invert_colors (self, g_value_get_boolean (value));
break ;
case PROP_WEIGHT:
icon_editor_window_set_weight (self, g_value_get_float (value));
break ;
case PROP_STATE:
icon_editor_window_set_state (self, g_value_get_uint (value));
break ;
case PROP_PLAYING:
icon_editor_window_set_playing (self, g_value_get_boolean (value));
break ;
case PROP_COMPAT_CLASSES:
icon_editor_window_set_compat_classes (self, g_value_get_boolean (value));
break ;
default :
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break ;
}
}
static void
icon_editor_window_get_property (GObject *object,
unsigned int prop_id,
GValue *value,
GParamSpec *pspec)
{
IconEditorWindow *self = ICON_EDITOR_WINDOW (object);
switch (prop_id)
{
case PROP_PAINTABLE:
g_value_set_object (value, self->paintable);
break ;
case PROP_CHANGED:
g_value_set_boolean (value, self->changed);
break ;
case PROP_SHOW_SIDEBAR:
g_value_set_boolean (value, self->show_sidebar);
break ;
case PROP_SHOW_BOUNDS:
g_value_set_boolean (value, self->show_bounds);
break ;
case PROP_SHOW_SPINES:
g_value_set_boolean (value, self->show_spines);
break ;
case PROP_SHOW_GRID:
g_value_set_boolean (value, self->show_grid);
break ;
case PROP_INVERT_COLORS:
g_value_set_boolean (value, self->invert_colors);
break ;
case PROP_WEIGHT:
g_value_set_float (value, self->weight);
break ;
case PROP_STATE:
g_value_set_uint (value, self->state);
break ;
case PROP_PLAYING:
g_value_set_boolean (value, self->playing);
break ;
case PROP_COMPAT_CLASSES:
g_value_set_boolean (value, self->compat_classes);
break ;
default :
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break ;
}
}
static void
icon_editor_window_dispose (GObject *object)
{
gtk_widget_dispose_template (GTK_WIDGET (object), ICON_EDITOR_WINDOW_TYPE);
G_OBJECT_CLASS (icon_editor_window_parent_class)->dispose (object);
}
static void
icon_editor_window_finalize (GObject *object)
{
IconEditorWindow *self = ICON_EDITOR_WINDOW (object);
if (self->paintable)
g_signal_handlers_disconnect_by_func (self->paintable, paintable_changed, self);
g_clear_object (&self->paintable);
g_clear_object (&self->orig_paintable);
g_clear_object (&self->file);
g_clear_object (&self->paintable_style);
G_OBJECT_CLASS (icon_editor_window_parent_class)->finalize (object);
}
static void
icon_editor_window_realize (GtkWidget *widget)
{
IconEditorWindow *self = ICON_EDITOR_WINDOW (widget);
GtkApplication *app = gtk_window_get_application (GTK_WINDOW (self));
g_autofree char *path = NULL;
g_autoptr (GFile) file = NULL;
g_autoptr (GtkIconPaintable) logo = NULL;
GdkFrameClock *clock;
GTK_WIDGET_CLASS (icon_editor_window_parent_class)->realize (widget);
path = g_strconcat ("resource:///org/gtk/Shaper/",
g_application_get_application_id (G_APPLICATION (app)),
".svg" ,
NULL);
file = g_file_new_for_uri (path);
logo = gtk_icon_paintable_new_for_file (file, 128 , 1 );
gtk_image_set_from_paintable (self->empty_logo, GDK_PAINTABLE (logo));
clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
for (unsigned int i = 0 ; i < 6 ; i++)
gtk_svg_set_frame_clock (GTK_SVG (gtk_image_get_paintable (GTK_IMAGE (self->examples[i]))), clock);
if (self->paintable)
path_paintable_set_frame_clock (self->paintable, clock);
}
static void
icon_editor_window_class_init (IconEditorWindowClass *class )
{
GObjectClass *object_class = G_OBJECT_CLASS (class );
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class );
GtkWindowClass *window_class = GTK_WINDOW_CLASS (class );
g_type_ensure (PAINTABLE_EDITOR_TYPE);
g_type_ensure (BORDER_PAINTABLE_TYPE);
object_class->set_property = icon_editor_window_set_property;
object_class->get_property = icon_editor_window_get_property;
object_class->dispose = icon_editor_window_dispose;
object_class->finalize = icon_editor_window_finalize;
widget_class->realize = icon_editor_window_realize;
window_class->close_request = icon_editor_window_close_request;
properties[PROP_PAINTABLE] =
g_param_spec_object ("paintable" , NULL, NULL,
PATH_PAINTABLE_TYPE,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_CHANGED] =
g_param_spec_boolean ("changed" , NULL, NULL,
FALSE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_SHOW_SIDEBAR] =
g_param_spec_boolean ("show-sidebar" , NULL, NULL,
FALSE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_SHOW_BOUNDS] =
g_param_spec_boolean ("show-bounds" , NULL, NULL,
FALSE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_SHOW_SPINES] =
g_param_spec_boolean ("show-spines" , NULL, NULL,
FALSE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_SHOW_GRID] =
g_param_spec_boolean ("show-grid" , NULL, NULL,
FALSE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_INVERT_COLORS] =
g_param_spec_boolean ("invert-colors" , NULL, NULL,
FALSE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_WEIGHT] =
g_param_spec_float ("weight" , NULL, NULL,
1 .f, 1000 .f, 400 .f,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_STATE] =
g_param_spec_uint ("state" , NULL, NULL,
0 , G_MAXUINT, 0 ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_PLAYING] =
g_param_spec_boolean ("playing" , NULL, NULL,
TRUE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
properties[PROP_COMPAT_CLASSES] =
g_param_spec_boolean ("compat-classes" , NULL, NULL,
TRUE ,
G_PARAM_READWRITE | G_PARAM_STATIC_NAME);
g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);
gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/Shaper/icon-editor-window.ui" );
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, main_stack);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, empty_logo);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_0);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_1);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_2);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_3);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_4);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_5);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_6);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image48_7);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_0);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_1);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_2);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_3);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_4);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_5);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_6);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_7);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_8);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_9);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_10);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_11);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_12);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_13);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_14);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, image24_15);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, paintable_editor);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, example1);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, example2);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, example3);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, example4);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, example5);
gtk_widget_class_bind_template_child (widget_class, IconEditorWindow, example6);
gtk_widget_class_bind_template_callback (widget_class, show_open_filechooser);
gtk_widget_class_bind_template_callback (widget_class, toggle_sidebar);
gtk_widget_class_bind_template_callback (widget_class, file_drop);
gtk_widget_class_bind_template_callback (widget_class, gtk_widget_activate);
}
/* }}} */
/* {{{ Public API */
IconEditorWindow *
icon_editor_window_new (IconEditorApplication *application)
{
return g_object_new (ICON_EDITOR_WINDOW_TYPE,
"application" , application,
NULL);
}
static void
open_alert_done (GObject *source,
GAsyncResult *result,
gpointer data)
{
GtkAlertDialog *alert = GTK_ALERT_DIALOG (source);
IconEditorWindow *self = ICON_EDITOR_WINDOW (data);
g_autoptr (GError) error = NULL;
int res;
GFile *file = G_FILE (g_object_get_data (G_OBJECT (alert), "file" ));
res = gtk_alert_dialog_choose_finish (alert, result, &error);
if (res == -1 )
return ;
if (res == 0 )
{
/* Don't try to save resource files */
if (is_regular_file (self->file))
{
save_to_file (self, self->file);
icon_editor_window_load_file (self, file);
}
else
{
FollowupData *fd = g_new (FollowupData, 1 );
fd->callback = (FollowupCallback) icon_editor_window_load_file;
fd->data = g_object_ref (file);
fd->destroy = g_object_unref;
show_save_filechooser_with_followup (self, fd);
}
}
else if (res == 1 )
{
icon_editor_window_load_file (self, file);
}
}
gboolean
icon_editor_window_load (IconEditorWindow *self,
GFile *file)
{
if (self->changed)
{
g_autoptr (GtkAlertDialog) alert = NULL;
const char *buttons[] = { "Save" , "Continue" , NULL };
alert = gtk_alert_dialog_new ("Unsaved changes" );
g_object_set_data_full (G_OBJECT (alert), "file" , g_object_ref (file), g_object_unref);
gtk_alert_dialog_set_detail (alert, "The icon contains unsaved changes." );
gtk_alert_dialog_set_modal (alert, TRUE );
gtk_alert_dialog_set_buttons (alert, buttons);
gtk_alert_dialog_set_default_button (alert, 0 );
gtk_alert_dialog_set_cancel_button (alert, 0 );
gtk_alert_dialog_choose (alert, GTK_WINDOW (self), NULL, open_alert_done, self);
return TRUE ;
}
return icon_editor_window_load_file (self, file);
}
/* }}} */
/* vim:set foldmethod=marker: */
Messung V0.5 in Prozent C=100 H=93 G=96
¤ Dauer der Verarbeitung: 0.20 Sekunden
(vorverarbeitet am 2026-07-02)
¤
*© Formatika GbR, Deutschland