PIKApp/libpikawidgets/pikawidgets.c

677 lines
23 KiB
C
Raw Permalink Normal View History

2023-09-26 00:35:21 +02:00
/* LIBPIKA - The PIKA Library
* Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
*
* pikawidgets.c
* Copyright (C) 2000 Michael Natterer <mitch@gimp.org>
*
* 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 3 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
* Library 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
* <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include <gegl.h>
#include <gtk/gtk.h>
#include "libpikacolor/pikacolor.h"
#include "libpikamath/pikamath.h"
#include "libpikabase/pikabase.h"
#include "pikawidgets.h"
#include "libpika/libpika-intl.h"
/**
* SECTION: pikawidgets
* @title: PikaWidgets
* @short_description: A collection of convenient widget constructors,
* standard callbacks and helper functions.
*
* A collection of convenient widget constructors, standard callbacks
* and helper functions.
**/
/**
* pika_int_radio_group_new: (skip)
* @in_frame: %TRUE if you want a #GtkFrame around the
* radio button group.
* @frame_title: (nullable):
* The title of the Frame or %NULL if you don't want a
* title.
* @radio_button_callback: (scope notified): The callback each button's
* "toggled" signal will be connected with.
* @radio_button_callback_data: (closure radio_button_callback):
* The data which will be passed to g_signal_connect().
* @radio_button_callback_destroy: (destroy radio_button_callback_data):
* @initial: The @item_data of the initially pressed radio button.
* @...: A %NULL-terminated @va_list describing
* the radio buttons.
*
* Convenience function to create a group of radio buttons embedded into
* a #GtkFrame or #GtkBox. This function does the same thing as
* pika_radio_group_new2(), but it takes integers as @item_data instead of
* pointers, since that is a very common case (mapping an enum to a radio
* group).
*
* Returns: (transfer full): A #GtkFrame or #GtkBox (depending on @in_frame).
**/
GtkWidget *
pika_int_radio_group_new (gboolean in_frame,
const gchar *frame_title,
GCallback radio_button_callback,
gpointer radio_button_callback_data,
GDestroyNotify radio_button_callback_destroy,
gint initial, /* item_data */
/* specify radio buttons as va_list:
* const gchar *label,
* gint item_data,
* GtkWidget **widget_ptr,
*/
...)
{
GtkWidget *vbox;
GtkWidget *button;
GSList *group;
/* radio button variables */
const gchar *label;
gint item_data;
gpointer item_ptr;
GtkWidget **widget_ptr;
va_list args;
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 2);
group = NULL;
if (radio_button_callback_destroy)
g_object_weak_ref (G_OBJECT (vbox),
(GWeakNotify) radio_button_callback_destroy,
radio_button_callback_data);
/* create the radio buttons */
va_start (args, initial);
label = va_arg (args, const gchar *);
while (label)
{
item_data = va_arg (args, gint);
widget_ptr = va_arg (args, GtkWidget **);
item_ptr = GINT_TO_POINTER (item_data);
if (label != GINT_TO_POINTER (1))
button = gtk_radio_button_new_with_mnemonic (group, label);
else
button = gtk_radio_button_new (group);
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (button));
gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
if (item_data)
{
g_object_set_data (G_OBJECT (button), "pika-item-data", item_ptr);
/* backward compatibility */
g_object_set_data (G_OBJECT (button), "user_data", item_ptr);
}
if (widget_ptr)
*widget_ptr = button;
if (initial == item_data)
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
g_signal_connect (button, "toggled",
radio_button_callback,
radio_button_callback_data);
gtk_widget_show (button);
label = va_arg (args, const gchar *);
}
va_end (args);
if (in_frame)
{
GtkWidget *frame;
frame = pika_frame_new (frame_title);
gtk_container_add (GTK_CONTAINER (frame), vbox);
gtk_widget_show (vbox);
return frame;
}
return vbox;
}
/**
* pika_int_radio_group_set_active:
* @radio_button: Pointer to a #GtkRadioButton.
* @item_data: The @item_data of the radio button you want to select.
*
* Calls gtk_toggle_button_set_active() with the radio button that was created
* with a matching @item_data. This function does the same thing as
* pika_radio_group_set_active(), but takes integers as @item_data instead
* of pointers.
**/
void
pika_int_radio_group_set_active (GtkRadioButton *radio_button,
gint item_data)
{
GtkWidget *button;
GSList *group;
g_return_if_fail (GTK_IS_RADIO_BUTTON (radio_button));
for (group = gtk_radio_button_get_group (radio_button);
group;
group = g_slist_next (group))
{
button = GTK_WIDGET (group->data);
if (g_object_get_data (G_OBJECT (button), "pika-item-data") ==
GINT_TO_POINTER (item_data))
{
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
return;
}
}
}
static void
pika_random_seed_update (GtkWidget *widget,
gpointer data)
{
GtkWidget *spinbutton = data;
/* Generate a new seed if the "New Seed" button was clicked or
* of the "Randomize" toggle is activated
*/
if (! GTK_IS_TOGGLE_BUTTON (widget) ||
gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)))
{
gtk_spin_button_set_value (GTK_SPIN_BUTTON (spinbutton),
(guint) g_random_int ());
}
}
/**
* pika_random_seed_new:
* @seed: A pointer to the variable which stores the random seed.
* @random_seed: A pointer to a boolean indicating whether seed should be
* initialised randomly or not.
*
* Creates a widget that allows the user to control how the random number
* generator is initialized.
*
* Returns: (transfer full): A #GtkBox containing a #GtkSpinButton for
* the seed and a #GtkButton for setting a random seed.
**/
GtkWidget *
pika_random_seed_new (guint *seed,
gboolean *random_seed)
{
GtkWidget *hbox;
GtkWidget *toggle;
GtkWidget *spinbutton;
GtkAdjustment *adj;
GtkWidget *button;
g_return_val_if_fail (seed != NULL, NULL);
g_return_val_if_fail (random_seed != NULL, NULL);
hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);
/* If we're being asked to generate a random seed, generate one. */
if (*random_seed)
*seed = g_random_int ();
adj = gtk_adjustment_new (*seed, 0, (guint32) -1, 1, 10, 0);
spinbutton = pika_spin_button_new (adj, 1.0, 0);
gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (spinbutton), TRUE);
gtk_box_pack_start (GTK_BOX (hbox), spinbutton, FALSE, FALSE, 0);
gtk_widget_show (spinbutton);
g_signal_connect (adj, "value-changed",
G_CALLBACK (pika_uint_adjustment_update),
seed);
pika_help_set_help_data (spinbutton,
_("Use this value for random number generator "
"seed - this allows you to repeat a "
"given \"random\" operation"), NULL);
button = gtk_button_new_with_mnemonic (_("_New Seed"));
g_object_set (gtk_bin_get_child (GTK_BIN (button)),
"margin-start", 2,
"margin-end", 2,
NULL);
gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, FALSE, 0);
gtk_widget_show (button);
/* Send spinbutton as data so that we can change the value in
* pika_random_seed_update()
*/
g_signal_connect (button, "clicked",
G_CALLBACK (pika_random_seed_update),
spinbutton);
pika_help_set_help_data (button,
_("Seed random number generator with a generated "
"random number"),
NULL);
toggle = gtk_check_button_new_with_mnemonic (_("_Randomize"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), *random_seed);
gtk_box_pack_start (GTK_BOX (hbox), toggle, FALSE, FALSE, 0);
gtk_widget_show (toggle);
g_signal_connect (toggle, "toggled",
G_CALLBACK (pika_toggle_button_update),
random_seed);
/* Need to create a new seed when the "Randomize" toggle is activated */
g_signal_connect (toggle, "toggled",
G_CALLBACK (pika_random_seed_update),
spinbutton);
g_object_set_data (G_OBJECT (hbox), "spinbutton", spinbutton);
g_object_set_data (G_OBJECT (hbox), "button", button);
g_object_set_data (G_OBJECT (hbox), "toggle", toggle);
g_object_bind_property (toggle, "active",
spinbutton, "sensitive",
G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
g_object_bind_property (toggle, "active",
button, "sensitive",
G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
return hbox;
}
typedef struct
{
PikaChainButton *chainbutton;
gboolean chain_constrains_ratio;
gdouble orig_x;
gdouble orig_y;
gdouble last_x;
gdouble last_y;
} PikaCoordinatesData;
static void
pika_coordinates_callback (GtkWidget *widget,
PikaCoordinatesData *data)
{
gdouble new_x;
gdouble new_y;
new_x = pika_size_entry_get_refval (PIKA_SIZE_ENTRY (widget), 0);
new_y = pika_size_entry_get_refval (PIKA_SIZE_ENTRY (widget), 1);
if (pika_chain_button_get_active (data->chainbutton))
{
if (data->chain_constrains_ratio)
{
if ((data->orig_x != 0) && (data->orig_y != 0))
{
g_signal_handlers_block_by_func (widget,
pika_coordinates_callback,
data);
if (ROUND (new_x) != ROUND (data->last_x))
{
data->last_x = new_x;
new_y = (new_x * data->orig_y) / data->orig_x;
pika_size_entry_set_refval (PIKA_SIZE_ENTRY (widget), 1,
new_y);
data->last_y
= pika_size_entry_get_refval (PIKA_SIZE_ENTRY (widget), 1);
}
else if (ROUND (new_y) != ROUND (data->last_y))
{
data->last_y = new_y;
new_x = (new_y * data->orig_x) / data->orig_y;
pika_size_entry_set_refval (PIKA_SIZE_ENTRY (widget), 0,
new_x);
data->last_x
= pika_size_entry_get_refval (PIKA_SIZE_ENTRY (widget), 0);
}
g_signal_handlers_unblock_by_func (widget,
pika_coordinates_callback,
data);
}
}
else
{
if (new_x != data->last_x)
{
pika_size_entry_set_refval (PIKA_SIZE_ENTRY (widget), 1, new_x);
data->last_y = data->last_x
= pika_size_entry_get_refval (PIKA_SIZE_ENTRY (widget), 1);
}
else if (new_y != data->last_y)
{
pika_size_entry_set_refval (PIKA_SIZE_ENTRY (widget), 0, new_y);
data->last_x = data->last_y
= pika_size_entry_get_refval (PIKA_SIZE_ENTRY (widget), 0);
}
}
}
else
{
if (new_x != data->last_x)
data->last_x = new_x;
if (new_y != data->last_y)
data->last_y = new_y;
}
}
static void
pika_coordinates_data_free (PikaCoordinatesData *data)
{
g_slice_free (PikaCoordinatesData, data);
}
static void
pika_coordinates_chainbutton_toggled (PikaChainButton *button,
PikaSizeEntry *entry)
{
if (pika_chain_button_get_active (button))
{
PikaCoordinatesData *data;
data = g_object_get_data (G_OBJECT (entry), "coordinates-data");
data->orig_x = pika_size_entry_get_refval (entry, 0);
data->orig_y = pika_size_entry_get_refval (entry, 1);
}
}
/**
* pika_coordinates_new:
* @unit: The initial unit of the #PikaUnitMenu.
* @unit_format: A printf-like unit-format string as is used with
* pika_unit_menu_new().
* @menu_show_pixels: %TRUE if the #PikaUnitMenu should contain an item
* for PIKA_UNIT_PIXEL.
* @menu_show_percent: %TRUE if the #PikaUnitMenu should contain an item
* for PIKA_UNIT_PERCENT.
* @spinbutton_width: The horizontal size of the #PikaSizeEntry's
* #GtkSpinButton's.
* @update_policy: The update policy for the #PikaSizeEntry.
* @chainbutton_active: %TRUE if the attached #PikaChainButton should be
* active.
* @chain_constrains_ratio: %TRUE if the chainbutton should constrain the
* fields' aspect ratio. If %FALSE, the values will
* be constrained.
* @xlabel: The label for the X coordinate.
* @x: The initial value of the X coordinate.
* @xres: The horizontal resolution in DPI.
* @lower_boundary_x: The lower boundary of the X coordinate.
* @upper_boundary_x: The upper boundary of the X coordinate.
* @xsize_0: The X value which will be treated as 0%.
* @xsize_100: The X value which will be treated as 100%.
* @ylabel: The label for the Y coordinate.
* @y: The initial value of the Y coordinate.
* @yres: The vertical resolution in DPI.
* @lower_boundary_y: The lower boundary of the Y coordinate.
* @upper_boundary_y: The upper boundary of the Y coordinate.
* @ysize_0: The Y value which will be treated as 0%.
* @ysize_100: The Y value which will be treated as 100%.
*
* Convenience function that creates a #PikaSizeEntry with two fields for x/y
* coordinates/sizes with a #PikaChainButton attached to constrain either the
* two fields' values or the ratio between them.
*
* Returns: (transfer full): The new #PikaSizeEntry.
**/
GtkWidget *
pika_coordinates_new (PikaUnit unit,
const gchar *unit_format,
gboolean menu_show_pixels,
gboolean menu_show_percent,
gint spinbutton_width,
PikaSizeEntryUpdatePolicy update_policy,
gboolean chainbutton_active,
gboolean chain_constrains_ratio,
const gchar *xlabel,
gdouble x,
gdouble xres,
gdouble lower_boundary_x,
gdouble upper_boundary_x,
gdouble xsize_0, /* % */
gdouble xsize_100, /* % */
const gchar *ylabel,
gdouble y,
gdouble yres,
gdouble lower_boundary_y,
gdouble upper_boundary_y,
gdouble ysize_0, /* % */
gdouble ysize_100 /* % */)
{
PikaCoordinatesData *data;
GtkAdjustment *adjustment;
GtkWidget *spinbutton;
GtkWidget *sizeentry;
GtkWidget *chainbutton;
adjustment = gtk_adjustment_new (1, 0, 1, 1, 10, 0);
spinbutton = pika_spin_button_new (adjustment, 1.0, 2);
gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (spinbutton), TRUE);
if (spinbutton_width > 0)
{
if (spinbutton_width < 17)
gtk_entry_set_width_chars (GTK_ENTRY (spinbutton), spinbutton_width);
else
gtk_widget_set_size_request (spinbutton, spinbutton_width, -1);
}
sizeentry = pika_size_entry_new (1, unit, unit_format,
menu_show_pixels,
menu_show_percent,
FALSE,
spinbutton_width,
update_policy);
pika_size_entry_add_field (PIKA_SIZE_ENTRY (sizeentry),
GTK_SPIN_BUTTON (spinbutton), NULL);
gtk_grid_attach (GTK_GRID (sizeentry), spinbutton, 1, 0, 1, 1);
gtk_widget_show (spinbutton);
pika_size_entry_set_unit (PIKA_SIZE_ENTRY (sizeentry),
(update_policy == PIKA_SIZE_ENTRY_UPDATE_RESOLUTION) ||
(menu_show_pixels == FALSE) ?
PIKA_UNIT_INCH : PIKA_UNIT_PIXEL);
pika_size_entry_set_resolution (PIKA_SIZE_ENTRY (sizeentry), 0, xres, TRUE);
pika_size_entry_set_resolution (PIKA_SIZE_ENTRY (sizeentry), 1, yres, TRUE);
pika_size_entry_set_refval_boundaries (PIKA_SIZE_ENTRY (sizeentry), 0,
lower_boundary_x, upper_boundary_x);
pika_size_entry_set_refval_boundaries (PIKA_SIZE_ENTRY (sizeentry), 1,
lower_boundary_y, upper_boundary_y);
if (menu_show_percent)
{
pika_size_entry_set_size (PIKA_SIZE_ENTRY (sizeentry), 0,
xsize_0, xsize_100);
pika_size_entry_set_size (PIKA_SIZE_ENTRY (sizeentry), 1,
ysize_0, ysize_100);
}
pika_size_entry_set_refval (PIKA_SIZE_ENTRY (sizeentry), 0, x);
pika_size_entry_set_refval (PIKA_SIZE_ENTRY (sizeentry), 1, y);
pika_size_entry_attach_label (PIKA_SIZE_ENTRY (sizeentry),
xlabel, 0, 0, 0.0);
pika_size_entry_attach_label (PIKA_SIZE_ENTRY (sizeentry),
ylabel, 1, 0, 0.0);
chainbutton = pika_chain_button_new (PIKA_CHAIN_RIGHT);
if (chainbutton_active)
pika_chain_button_set_active (PIKA_CHAIN_BUTTON (chainbutton), TRUE);
gtk_grid_attach (GTK_GRID (sizeentry), chainbutton, 2, 0, 1, 2);
gtk_widget_show (chainbutton);
data = g_slice_new (PikaCoordinatesData);
data->chainbutton = PIKA_CHAIN_BUTTON (chainbutton);
data->chain_constrains_ratio = chain_constrains_ratio;
data->orig_x = x;
data->orig_y = y;
data->last_x = x;
data->last_y = y;
g_object_set_data_full (G_OBJECT (sizeentry), "coordinates-data",
data,
(GDestroyNotify) pika_coordinates_data_free);
g_signal_connect (sizeentry, "value-changed",
G_CALLBACK (pika_coordinates_callback),
data);
g_object_set_data (G_OBJECT (sizeentry), "chainbutton", chainbutton);
g_signal_connect (chainbutton, "toggled",
G_CALLBACK (pika_coordinates_chainbutton_toggled),
sizeentry);
return sizeentry;
}
/*
* Standard Callbacks
*/
/**
* pika_toggle_button_update:
* @widget: A #GtkToggleButton.
* @data: A pointer to a #gint variable which will store the value of
* gtk_toggle_button_get_active().
**/
void
pika_toggle_button_update (GtkWidget *widget,
gpointer data)
{
gint *toggle_val = (gint *) data;
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)))
*toggle_val = TRUE;
else
*toggle_val = FALSE;
}
/**
* pika_radio_button_update:
* @widget: A #GtkRadioButton.
* @data: A pointer to a #gint variable which will store the value of
* GPOINTER_TO_INT (g_object_get_data (@widget, "pika-item-data")).
**/
void
pika_radio_button_update (GtkWidget *widget,
gpointer data)
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)))
{
gint *toggle_val = (gint *) data;
*toggle_val = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (widget),
"pika-item-data"));
}
}
/**
* pika_int_adjustment_update:
* @adjustment: A #GtkAdjustment.
* @data: A pointer to a #gint variable which will store the
* @adjustment's value.
*
* Note that the #GtkAdjustment's value (which is a #gdouble) will be
* rounded with RINT().
**/
void
pika_int_adjustment_update (GtkAdjustment *adjustment,
gpointer data)
{
gint *val = (gint *) data;
*val = RINT (gtk_adjustment_get_value (adjustment));
}
/**
* pika_uint_adjustment_update:
* @adjustment: A #GtkAdjustment.
* @data: A pointer to a #guint variable which will store the
* @adjustment's value.
*
* Note that the #GtkAdjustment's value (which is a #gdouble) will be rounded
* with (#guint) (value + 0.5).
**/
void
pika_uint_adjustment_update (GtkAdjustment *adjustment,
gpointer data)
{
guint *val = (guint *) data;
*val = (guint) (gtk_adjustment_get_value (adjustment) + 0.5);
}
/**
* pika_float_adjustment_update:
* @adjustment: A #GtkAdjustment.
* @data: A pointer to a #gfloat variable which will store the
* @adjustment's value.
**/
void
pika_float_adjustment_update (GtkAdjustment *adjustment,
gpointer data)
{
gfloat *val = (gfloat *) data;
*val = gtk_adjustment_get_value (adjustment);
}
/**
* pika_double_adjustment_update:
* @adjustment: A #GtkAdjustment.
* @data: A pointer to a #gdouble variable which will store the
* @adjustment's value.
**/
void
pika_double_adjustment_update (GtkAdjustment *adjustment,
gpointer data)
{
gdouble *val = (gdouble *) data;
*val = gtk_adjustment_get_value (adjustment);
}