PIKApp/app/widgets/pikacolorhistory.c

591 lines
21 KiB
C

/* 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 <jehan@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 <gegl.h>
#include <gtk/gtk.h>
#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);
}
}