mirror of
https://gitlab.gnome.org/GNOME/gimp.git
synced 2025-07-03 17:33:25 +00:00

I encountered this one when closing GIMP. Issue #9591 seems to be the same crash, though when just closing the Colors dockable. I fail to reproduce willingly now, though it looks like the problem could happen in some cases when the stored GimpContext is freed before GimpColorHistory is freed. Let's increment the reference to make sure the referenced object stays alive as long the history is alive.
595 lines
21 KiB
C
595 lines
21 KiB
C
/* GIMP - The GNU Image Manipulation Program
|
|
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
|
|
*
|
|
* gimpcolorhistory.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 "libgimpbase/gimpbase.h"
|
|
#include "libgimpcolor/gimpcolor.h"
|
|
#include "libgimpwidgets/gimpwidgets.h"
|
|
|
|
#include "widgets-types.h"
|
|
|
|
#include "config/gimpcoreconfig.h"
|
|
|
|
#include "core/gimp.h"
|
|
#include "core/gimp-palettes.h"
|
|
#include "core/gimpcontext.h"
|
|
#include "core/gimpimage.h"
|
|
#include "app/core/gimpimage-colormap.h"
|
|
#include "core/gimppalettemru.h"
|
|
|
|
#include "gimpcolorhistory.h"
|
|
|
|
#include "gimp-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 gimp_color_history_constructed (GObject *object);
|
|
static void gimp_color_history_finalize (GObject *object);
|
|
static void gimp_color_history_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec);
|
|
static void gimp_color_history_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec);
|
|
|
|
/* GtkWidget methods */
|
|
static GtkSizeRequestMode gimp_color_history_get_request_mode (GtkWidget *widget);
|
|
static void gimp_color_history_get_preferred_width_for_height (GtkWidget *widget,
|
|
gint height,
|
|
gint *minimum_width,
|
|
gint *natural_width);
|
|
static void gimp_color_history_get_preferred_height_for_width (GtkWidget *widget,
|
|
gint width,
|
|
gint *minimum_height,
|
|
gint *natural_height);
|
|
static void gimp_color_history_size_allocate (GtkWidget *widget,
|
|
GdkRectangle *allocation);
|
|
|
|
/* Signal handlers */
|
|
static void gimp_color_history_color_clicked (GtkWidget *widget,
|
|
GimpColorHistory *history);
|
|
|
|
static void gimp_color_history_palette_dirty (GimpColorHistory *history);
|
|
|
|
static void gimp_color_history_color_changed (GtkWidget *widget,
|
|
gpointer data);
|
|
|
|
static void gimp_color_history_image_changed (GimpContext *context,
|
|
GimpImage *image,
|
|
GimpColorHistory *history);
|
|
|
|
/* Utils */
|
|
static void gimp_color_history_reorganize (GimpColorHistory *history);
|
|
|
|
|
|
G_DEFINE_TYPE (GimpColorHistory, gimp_color_history, GTK_TYPE_GRID)
|
|
|
|
#define parent_class gimp_color_history_parent_class
|
|
|
|
static guint history_signals[LAST_SIGNAL] = { 0 };
|
|
|
|
static void
|
|
gimp_color_history_class_init (GimpColorHistoryClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
|
|
object_class->constructed = gimp_color_history_constructed;
|
|
object_class->set_property = gimp_color_history_set_property;
|
|
object_class->get_property = gimp_color_history_get_property;
|
|
object_class->finalize = gimp_color_history_finalize;
|
|
|
|
widget_class->get_request_mode = gimp_color_history_get_request_mode;
|
|
widget_class->get_preferred_width_for_height = gimp_color_history_get_preferred_width_for_height;
|
|
widget_class->get_preferred_height_for_width = gimp_color_history_get_preferred_height_for_width;
|
|
widget_class->size_allocate = gimp_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 (GimpColorHistoryClass, color_selected),
|
|
NULL, NULL, NULL,
|
|
G_TYPE_NONE, 1,
|
|
GEGL_TYPE_COLOR);
|
|
|
|
g_object_class_install_property (object_class, PROP_CONTEXT,
|
|
g_param_spec_object ("context", NULL, NULL,
|
|
GIMP_TYPE_CONTEXT,
|
|
GIMP_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,
|
|
GIMP_PARAM_READWRITE |
|
|
G_PARAM_CONSTRUCT));
|
|
|
|
gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (klass), "GimpColorHistory");
|
|
|
|
klass->color_selected = NULL;
|
|
}
|
|
|
|
static void
|
|
gimp_color_history_init (GimpColorHistory *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
|
|
gimp_color_history_constructed (GObject *object)
|
|
{
|
|
GimpColorHistory *history = GIMP_COLOR_HISTORY (object);
|
|
GimpPalette *palette;
|
|
|
|
G_OBJECT_CLASS (parent_class)->constructed (object);
|
|
|
|
palette = gimp_palettes_get_color_history (history->context->gimp);
|
|
|
|
g_signal_connect_object (palette, "dirty",
|
|
G_CALLBACK (gimp_color_history_palette_dirty),
|
|
G_OBJECT (history), G_CONNECT_SWAPPED);
|
|
}
|
|
|
|
static void
|
|
gimp_color_history_finalize (GObject *object)
|
|
{
|
|
GimpColorHistory *history = GIMP_COLOR_HISTORY (object);
|
|
|
|
if (history->context)
|
|
g_signal_handlers_disconnect_by_func (history->context,
|
|
gimp_color_history_image_changed,
|
|
history);
|
|
g_clear_object (&history->context);
|
|
|
|
if (history->active_image)
|
|
g_signal_handlers_disconnect_by_func (history->active_image,
|
|
G_CALLBACK (gimp_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
|
|
gimp_color_history_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GimpColorHistory *history = GIMP_COLOR_HISTORY (object);
|
|
|
|
switch (property_id)
|
|
{
|
|
case PROP_CONTEXT:
|
|
if (history->context)
|
|
g_signal_handlers_disconnect_by_func (history->context,
|
|
gimp_color_history_image_changed,
|
|
history);
|
|
|
|
if (history->active_image)
|
|
{
|
|
g_signal_handlers_disconnect_by_func (history->active_image,
|
|
G_CALLBACK (gimp_color_history_palette_dirty),
|
|
history);
|
|
history->active_image = NULL;
|
|
}
|
|
|
|
history->context = g_value_dup_object (value);
|
|
|
|
if (history->context)
|
|
{
|
|
g_signal_connect (history->context, "image-changed",
|
|
G_CALLBACK (gimp_color_history_image_changed),
|
|
history);
|
|
|
|
history->active_image = gimp_context_get_image (history->context);
|
|
|
|
if (history->active_image)
|
|
{
|
|
g_signal_connect_swapped (history->active_image, "notify::base-type",
|
|
G_CALLBACK (gimp_color_history_palette_dirty),
|
|
history);
|
|
g_signal_connect_swapped (history->active_image, "colormap-changed",
|
|
G_CALLBACK (gimp_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++)
|
|
{
|
|
GeglColor *black = gegl_color_new ("black");
|
|
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 = gimp_color_area_new (black, GIMP_COLOR_AREA_SMALL_CHECKS,
|
|
GDK_BUTTON2_MASK);
|
|
gimp_color_area_set_color_config (GIMP_COLOR_AREA (color_area),
|
|
history->context->gimp->config->color_management);
|
|
gtk_container_add (GTK_CONTAINER (button), color_area);
|
|
gtk_widget_show (color_area);
|
|
|
|
g_signal_connect (button, "clicked",
|
|
G_CALLBACK (gimp_color_history_color_clicked),
|
|
history);
|
|
|
|
g_signal_connect (color_area, "color-changed",
|
|
G_CALLBACK (gimp_color_history_color_changed),
|
|
GINT_TO_POINTER (i));
|
|
|
|
history->buttons[i] = button;
|
|
history->color_areas[i] = color_area;
|
|
|
|
g_object_unref (black);
|
|
}
|
|
|
|
gimp_color_history_palette_dirty (history);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gimp_color_history_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GimpColorHistory *history = GIMP_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
|
|
gimp_color_history_get_request_mode (GtkWidget *widget)
|
|
{
|
|
return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
|
|
}
|
|
|
|
static void
|
|
gimp_color_history_get_preferred_width_for_height (GtkWidget *widget,
|
|
gint height,
|
|
gint *minimum_width,
|
|
gint *natural_width)
|
|
{
|
|
GimpColorHistory *history = GIMP_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
|
|
gimp_color_history_get_preferred_height_for_width (GtkWidget *widget,
|
|
gint width,
|
|
gint *minimum_height,
|
|
gint *natural_height)
|
|
{
|
|
GimpColorHistory *history = GIMP_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
|
|
gimp_color_history_size_allocate (GtkWidget *widget,
|
|
GdkRectangle *allocation)
|
|
{
|
|
GimpColorHistory *history = GIMP_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;
|
|
gimp_color_history_reorganize (history);
|
|
}
|
|
|
|
allocation->height = height;
|
|
GTK_WIDGET_CLASS (parent_class)->size_allocate (widget, allocation);
|
|
}
|
|
|
|
|
|
/* Public Functions */
|
|
|
|
GtkWidget *
|
|
gimp_color_history_new (GimpContext *context,
|
|
gint history_size)
|
|
{
|
|
GimpColorHistory *history;
|
|
|
|
g_return_val_if_fail (GIMP_IS_CONTEXT (context), NULL);
|
|
|
|
history = g_object_new (GIMP_TYPE_COLOR_HISTORY,
|
|
"context", context,
|
|
"history-size", history_size,
|
|
NULL);
|
|
|
|
return GTK_WIDGET (history);
|
|
}
|
|
|
|
/* Color history callback. */
|
|
|
|
static void
|
|
gimp_color_history_color_clicked (GtkWidget *widget,
|
|
GimpColorHistory *history)
|
|
{
|
|
GimpColorArea *color_area;
|
|
GeglColor *color;
|
|
|
|
color_area = GIMP_COLOR_AREA (gtk_bin_get_child (GTK_BIN (widget)));
|
|
|
|
color = gimp_color_area_get_color (color_area);
|
|
g_signal_emit (history, history_signals[COLOR_SELECTED], 0, color);
|
|
g_object_unref (color);
|
|
}
|
|
|
|
/* Color history palette callback. */
|
|
|
|
static void
|
|
gimp_color_history_palette_dirty (GimpColorHistory *history)
|
|
{
|
|
GimpPalette *palette;
|
|
GimpPalette *colormap_palette = NULL;
|
|
GimpImageBaseType base_type = GIMP_RGB;
|
|
gint i;
|
|
|
|
palette = gimp_palettes_get_color_history (history->context->gimp);
|
|
if (history->active_image)
|
|
{
|
|
base_type = gimp_image_get_base_type (history->active_image);
|
|
if (base_type == GIMP_INDEXED)
|
|
colormap_palette = gimp_image_get_colormap_palette (history->active_image);
|
|
}
|
|
|
|
for (i = 0; i < history->history_size; i++)
|
|
{
|
|
GimpPaletteEntry *entry = gimp_palette_get_entry (palette, i);
|
|
GeglColor *black = gegl_color_new ("black");
|
|
GeglColor *color = entry ? entry->color : black;
|
|
gboolean oog = FALSE;
|
|
|
|
g_signal_handlers_block_by_func (history->color_areas[i],
|
|
gimp_color_history_color_changed,
|
|
GINT_TO_POINTER (i));
|
|
|
|
gimp_color_area_set_color (GIMP_COLOR_AREA (history->color_areas[i]), color);
|
|
|
|
/* Now that palette colors can be any model and any space, just looking at
|
|
* whether they are out of [0; 1] range is not enough (a same color could
|
|
* be in or out range depending on the color space it is stored as).
|
|
* What we are really looking for is:
|
|
* 1. Whether they are out of the palette (indexed image case);
|
|
* 2. Whether they are out of the active image's space
|
|
* (independently of their own space).
|
|
*/
|
|
if (colormap_palette)
|
|
oog = (! gimp_palette_find_entry (colormap_palette, color, NULL));
|
|
else if (history->active_image)
|
|
oog = gimp_color_is_out_of_gamut (color, gimp_image_get_layer_space (history->active_image));
|
|
else
|
|
oog = gimp_color_is_out_of_self_gamut (color);
|
|
|
|
gimp_color_area_set_out_of_gamut (GIMP_COLOR_AREA (history->color_areas[i]), oog);
|
|
|
|
g_signal_handlers_unblock_by_func (history->color_areas[i],
|
|
gimp_color_history_color_changed,
|
|
GINT_TO_POINTER (i));
|
|
|
|
g_object_unref (black);
|
|
}
|
|
}
|
|
|
|
/* Color area callbacks. */
|
|
|
|
static void
|
|
gimp_color_history_color_changed (GtkWidget *widget,
|
|
gpointer data)
|
|
{
|
|
GimpColorHistory *history;
|
|
GimpPalette *palette;
|
|
GeglColor *color;
|
|
|
|
history = GIMP_COLOR_HISTORY (gtk_widget_get_ancestor (widget,
|
|
GIMP_TYPE_COLOR_HISTORY));
|
|
|
|
palette = gimp_palettes_get_color_history (history->context->gimp);
|
|
|
|
color = gimp_color_area_get_color (GIMP_COLOR_AREA (widget));
|
|
|
|
gimp_palette_set_entry_color (palette, GPOINTER_TO_INT (data), color, FALSE);
|
|
}
|
|
|
|
static void
|
|
gimp_color_history_image_changed (GimpContext *context,
|
|
GimpImage *image,
|
|
GimpColorHistory *history)
|
|
{
|
|
/* Update active image. */
|
|
if (history->active_image)
|
|
g_signal_handlers_disconnect_by_func (history->active_image,
|
|
G_CALLBACK (gimp_color_history_palette_dirty),
|
|
history);
|
|
history->active_image = image;
|
|
if (image)
|
|
{
|
|
g_signal_connect_swapped (image, "notify::base-type",
|
|
G_CALLBACK (gimp_color_history_palette_dirty),
|
|
history);
|
|
g_signal_connect_swapped (image, "colormap-changed",
|
|
G_CALLBACK (gimp_color_history_palette_dirty),
|
|
history);
|
|
}
|
|
|
|
/* Update the palette. */
|
|
gimp_color_history_palette_dirty (history);
|
|
}
|
|
|
|
static void
|
|
gimp_color_history_reorganize (GimpColorHistory *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);
|
|
}
|
|
}
|