/* PIKA - Photo and Image Kooker Application * a rebranding of The GNU Image Manipulation Program (created with heckimp) * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio * * Original copyright, applying to most contents (license remains unchanged): * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * pikacolorhistory.c * Copyright (C) 2015 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 . */ #include "config.h" #include #include #include "libpikabase/pikabase.h" #include "libpikawidgets/pikawidgets.h" #include "widgets-types.h" #include "config/pikacoreconfig.h" #include "core/pika.h" #include "core/pika-palettes.h" #include "core/pikacontext.h" #include "core/pikaimage.h" #include "app/core/pikaimage-colormap.h" #include "core/pikapalettemru.h" #include "pikacolorhistory.h" #include "pika-intl.h" enum { COLOR_SELECTED, LAST_SIGNAL }; enum { PROP_0, PROP_CONTEXT, PROP_HISTORY_SIZE, }; #define DEFAULT_HISTORY_SIZE 12 #define COLOR_AREA_SIZE 20 #define CHANNEL_EPSILON 1e-3 /* GObject methods */ static void pika_color_history_constructed (GObject *object); static void pika_color_history_finalize (GObject *object); static void pika_color_history_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void pika_color_history_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); /* GtkWidget methods */ static GtkSizeRequestMode pika_color_history_get_request_mode (GtkWidget *widget); static void pika_color_history_get_preferred_width_for_height (GtkWidget *widget, gint height, gint *minimum_width, gint *natural_width); static void pika_color_history_get_preferred_height_for_width (GtkWidget *widget, gint width, gint *minimum_height, gint *natural_height); static void pika_color_history_size_allocate (GtkWidget *widget, GdkRectangle *allocation); /* Signal handlers */ static void pika_color_history_color_clicked (GtkWidget *widget, PikaColorHistory *history); static void pika_color_history_palette_dirty (PikaColorHistory *history); static void pika_color_history_color_changed (GtkWidget *widget, gpointer data); static void pika_color_history_image_changed (PikaContext *context, PikaImage *image, PikaColorHistory *history); /* Utils */ static void pika_color_history_reorganize (PikaColorHistory *history); G_DEFINE_TYPE (PikaColorHistory, pika_color_history, GTK_TYPE_GRID) #define parent_class pika_color_history_parent_class static guint history_signals[LAST_SIGNAL] = { 0 }; static void pika_color_history_class_init (PikaColorHistoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->constructed = pika_color_history_constructed; object_class->set_property = pika_color_history_set_property; object_class->get_property = pika_color_history_get_property; object_class->finalize = pika_color_history_finalize; widget_class->get_request_mode = pika_color_history_get_request_mode; widget_class->get_preferred_width_for_height = pika_color_history_get_preferred_width_for_height; widget_class->get_preferred_height_for_width = pika_color_history_get_preferred_height_for_width; widget_class->size_allocate = pika_color_history_size_allocate; history_signals[COLOR_SELECTED] = g_signal_new ("color-selected", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (PikaColorHistoryClass, color_selected), NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_POINTER); g_object_class_install_property (object_class, PROP_CONTEXT, g_param_spec_object ("context", NULL, NULL, PIKA_TYPE_CONTEXT, PIKA_PARAM_READWRITE | G_PARAM_CONSTRUCT)); g_object_class_install_property (object_class, PROP_HISTORY_SIZE, g_param_spec_int ("history-size", NULL, NULL, 2, G_MAXINT, DEFAULT_HISTORY_SIZE, PIKA_PARAM_READWRITE | G_PARAM_CONSTRUCT)); gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "PikaColorHistory"); klass->color_selected = NULL; } static void pika_color_history_init (PikaColorHistory *history) { history->color_areas = NULL; history->buttons = NULL; history->n_rows = 1; gtk_grid_set_row_spacing (GTK_GRID (history), 2); gtk_grid_set_column_spacing (GTK_GRID (history), 2); } static void pika_color_history_constructed (GObject *object) { PikaColorHistory *history = PIKA_COLOR_HISTORY (object); PikaPalette *palette; G_OBJECT_CLASS (parent_class)->constructed (object); palette = pika_palettes_get_color_history (history->context->pika); g_signal_connect_object (palette, "dirty", G_CALLBACK (pika_color_history_palette_dirty), G_OBJECT (history), G_CONNECT_SWAPPED); } static void pika_color_history_finalize (GObject *object) { PikaColorHistory *history = PIKA_COLOR_HISTORY (object); if (history->context) g_signal_handlers_disconnect_by_func (history->context, pika_color_history_image_changed, history); if (history->active_image) g_signal_handlers_disconnect_by_func (history->active_image, G_CALLBACK (pika_color_history_palette_dirty), history); g_clear_pointer (&history->color_areas, g_free); g_clear_pointer (&history->buttons, g_free); G_OBJECT_CLASS (parent_class)->finalize (object); } static void pika_color_history_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PikaColorHistory *history = PIKA_COLOR_HISTORY (object); switch (property_id) { case PROP_CONTEXT: if (history->context) g_signal_handlers_disconnect_by_func (history->context, pika_color_history_image_changed, history); if (history->active_image) { g_signal_handlers_disconnect_by_func (history->active_image, G_CALLBACK (pika_color_history_palette_dirty), history); history->active_image = NULL; } history->context = g_value_get_object (value); if (history->context) { g_signal_connect (history->context, "image-changed", G_CALLBACK (pika_color_history_image_changed), history); history->active_image = pika_context_get_image (history->context); if (history->active_image) { g_signal_connect_swapped (history->active_image, "notify::base-type", G_CALLBACK (pika_color_history_palette_dirty), history); g_signal_connect_swapped (history->active_image, "colormap-changed", G_CALLBACK (pika_color_history_palette_dirty), history); } } break; case PROP_HISTORY_SIZE: { GtkWidget *button; GtkWidget *color_area; gint i; history->history_size = g_value_get_int (value); /* Destroy previous color buttons. */ gtk_container_foreach (GTK_CONTAINER (history), (GtkCallback) gtk_widget_destroy, NULL); history->buttons = g_realloc_n (history->buttons, history->history_size, sizeof (GtkWidget*)); history->color_areas = g_realloc_n (history->color_areas, history->history_size, sizeof (GtkWidget*)); for (i = 0; i < history->history_size; i++) { PikaRGB black = { 0.0, 0.0, 0.0, 1.0 }; gint row, column; column = i % (history->history_size / history->n_rows); row = i / (history->history_size / history->n_rows); button = gtk_button_new (); gtk_widget_set_size_request (button, COLOR_AREA_SIZE, COLOR_AREA_SIZE); gtk_grid_attach (GTK_GRID (history), button, column, row, 1, 1); gtk_widget_show (button); color_area = pika_color_area_new (&black, PIKA_COLOR_AREA_SMALL_CHECKS, GDK_BUTTON2_MASK); pika_color_area_set_color_config (PIKA_COLOR_AREA (color_area), history->context->pika->config->color_management); gtk_container_add (GTK_CONTAINER (button), color_area); gtk_widget_show (color_area); g_signal_connect (button, "clicked", G_CALLBACK (pika_color_history_color_clicked), history); g_signal_connect (color_area, "color-changed", G_CALLBACK (pika_color_history_color_changed), GINT_TO_POINTER (i)); history->buttons[i] = button; history->color_areas[i] = color_area; } pika_color_history_palette_dirty (history); } break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void pika_color_history_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PikaColorHistory *history = PIKA_COLOR_HISTORY (object); switch (property_id) { case PROP_CONTEXT: g_value_set_object (value, history->context); break; case PROP_HISTORY_SIZE: g_value_set_int (value, history->history_size); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static GtkSizeRequestMode pika_color_history_get_request_mode (GtkWidget *widget) { return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; } static void pika_color_history_get_preferred_width_for_height (GtkWidget *widget, gint height, gint *minimum_width, gint *natural_width) { PikaColorHistory *history = PIKA_COLOR_HISTORY (widget); GtkWidget *button; gint button_width = COLOR_AREA_SIZE; button = gtk_grid_get_child_at (GTK_GRID (widget), 0, 0); if (button) button_width = MAX (gtk_widget_get_allocated_width (button), COLOR_AREA_SIZE); /* This is a bit of a trick. Though the height actually depends on the * width, I actually just request for about half the expected width if * we were to align all color buttons on one line. This way, it is * possible to resize the widget smaller than it currently is, hence * allowing reorganization of icons. */ *minimum_width = button_width * (1 + history->history_size / 2); *natural_width = *minimum_width; } static void pika_color_history_get_preferred_height_for_width (GtkWidget *widget, gint width, gint *minimum_height, gint *natural_height) { PikaColorHistory *history = PIKA_COLOR_HISTORY (widget); GtkWidget *button; gint button_width = COLOR_AREA_SIZE; gint button_height = COLOR_AREA_SIZE; gint height; button = gtk_grid_get_child_at (GTK_GRID (widget), 0, 0); if (button) { button_width = MAX (gtk_widget_get_allocated_width (button), COLOR_AREA_SIZE); button_height = MAX (gtk_widget_get_allocated_height (button), COLOR_AREA_SIZE); } if (width > button_width * history->history_size + 2 * (history->history_size - 1)) height = button_height; else height = 2 * button_height + 2; *minimum_height = height; *natural_height = height; } static void pika_color_history_size_allocate (GtkWidget *widget, GdkRectangle *allocation) { PikaColorHistory *history = PIKA_COLOR_HISTORY (widget); GtkWidget *button; gint button_width = COLOR_AREA_SIZE; gint button_height = COLOR_AREA_SIZE; gint height; gint n_rows; button = gtk_grid_get_child_at (GTK_GRID (widget), 0, 0); if (button) { button_width = MAX (gtk_widget_get_allocated_width (button), COLOR_AREA_SIZE); button_height = MAX (gtk_widget_get_allocated_height (button), COLOR_AREA_SIZE); } if (allocation->width > button_width * history->history_size + 2 * (history->history_size - 1)) { n_rows = 1; height = button_height; } else { n_rows = 2; height = 2 * button_height + 2; } if (n_rows != history->n_rows) { history->n_rows = n_rows; pika_color_history_reorganize (history); } allocation->height = height; GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation); } /* Public Functions */ GtkWidget * pika_color_history_new (PikaContext *context, gint history_size) { PikaColorHistory *history; g_return_val_if_fail (PIKA_IS_CONTEXT (context), NULL); history = g_object_new (PIKA_TYPE_COLOR_HISTORY, "context", context, "history-size", history_size, NULL); return GTK_WIDGET (history); } /* Color history callback. */ static void pika_color_history_color_clicked (GtkWidget *widget, PikaColorHistory *history) { PikaColorArea *color_area; PikaRGB color; color_area = PIKA_COLOR_AREA (gtk_bin_get_child (GTK_BIN (widget))); pika_color_area_get_color (color_area, &color); g_signal_emit (history, history_signals[COLOR_SELECTED], 0, &color); } /* Color history palette callback. */ static void pika_color_history_palette_dirty (PikaColorHistory *history) { PikaPalette *palette; PikaPalette *colormap_palette = NULL; PikaImageBaseType base_type = PIKA_RGB; gint i; palette = pika_palettes_get_color_history (history->context->pika); if (history->active_image) { base_type = pika_image_get_base_type (history->active_image); if (base_type == PIKA_INDEXED) colormap_palette = pika_image_get_colormap_palette (history->active_image); } for (i = 0; i < history->history_size; i++) { PikaPaletteEntry *entry = pika_palette_get_entry (palette, i); PikaRGB black = { 0.0, 0.0, 0.0, 1.0 }; PikaRGB color = entry ? entry->color : black; gboolean oog = FALSE; g_signal_handlers_block_by_func (history->color_areas[i], pika_color_history_color_changed, GINT_TO_POINTER (i)); pika_color_area_set_color (PIKA_COLOR_AREA (history->color_areas[i]), &color); if (/* Common out-of-gamut case */ (color.r < 0.0 || color.r > 1.0 || color.g < 0.0 || color.g > 1.0 || color.b < 0.0 || color.b > 1.0) || /* Indexed images */ (colormap_palette && ! pika_palette_find_entry (colormap_palette, &color, NULL)) || /* Grayscale images */ (base_type == PIKA_GRAY && (ABS (color.r - color.g) > CHANNEL_EPSILON || ABS (color.r - color.b) > CHANNEL_EPSILON || ABS (color.g - color.b) > CHANNEL_EPSILON))) oog = TRUE; pika_color_area_set_out_of_gamut (PIKA_COLOR_AREA (history->color_areas[i]), oog); g_signal_handlers_unblock_by_func (history->color_areas[i], pika_color_history_color_changed, GINT_TO_POINTER (i)); } } /* Color area callbacks. */ static void pika_color_history_color_changed (GtkWidget *widget, gpointer data) { PikaColorHistory *history; PikaPalette *palette; PikaRGB color; history = PIKA_COLOR_HISTORY (gtk_widget_get_ancestor (widget, PIKA_TYPE_COLOR_HISTORY)); palette = pika_palettes_get_color_history (history->context->pika); pika_color_area_get_color (PIKA_COLOR_AREA (widget), &color); pika_palette_set_entry_color (palette, GPOINTER_TO_INT (data), &color, FALSE); } static void pika_color_history_image_changed (PikaContext *context, PikaImage *image, PikaColorHistory *history) { /* Update active image. */ if (history->active_image) g_signal_handlers_disconnect_by_func (history->active_image, G_CALLBACK (pika_color_history_palette_dirty), history); history->active_image = image; if (image) { g_signal_connect_swapped (image, "notify::base-type", G_CALLBACK (pika_color_history_palette_dirty), history); g_signal_connect_swapped (image, "colormap-changed", G_CALLBACK (pika_color_history_palette_dirty), history); } /* Update the palette. */ pika_color_history_palette_dirty (history); } static void pika_color_history_reorganize (PikaColorHistory *history) { GtkWidget *button; gint i; g_return_if_fail (history->buttons[0] && GTK_IS_BUTTON (history->buttons[0])); for (i = 0; i < history->history_size; i++) { gint row, column; column = i % (history->history_size / history->n_rows); row = i / (history->history_size / history->n_rows); button = history->buttons[i]; g_object_ref (button); gtk_container_remove (GTK_CONTAINER (history), button); gtk_grid_attach (GTK_GRID (history), button, column, row, 1, 1); } }