591 lines
21 KiB
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);
|
|
}
|
|
}
|