date-chooser: Add the split-month-year property
This allows unifying the month and year selectors into a single month and year selector by setting it to false.
This commit is contained in:
committed by
Georges Basile Stavracas Neto
parent
db0f07136c
commit
78931df46c
@@ -40,7 +40,9 @@ struct _GcalDateChooser
|
|||||||
AdwBin parent;
|
AdwBin parent;
|
||||||
|
|
||||||
GtkWidget *month_choice;
|
GtkWidget *month_choice;
|
||||||
|
GtkWidget *popover_month_choice;
|
||||||
GtkWidget *year_choice;
|
GtkWidget *year_choice;
|
||||||
|
GtkWidget *combined_choice;
|
||||||
GtkWidget *grid;
|
GtkWidget *grid;
|
||||||
|
|
||||||
GtkWidget *day_grid;
|
GtkWidget *day_grid;
|
||||||
@@ -61,6 +63,7 @@ struct _GcalDateChooser
|
|||||||
gboolean show_week_numbers;
|
gboolean show_week_numbers;
|
||||||
gboolean show_selected_week;
|
gboolean show_selected_week;
|
||||||
gboolean show_events;
|
gboolean show_events;
|
||||||
|
gboolean split_month_year;
|
||||||
|
|
||||||
GcalRangeTree *events;
|
GcalRangeTree *events;
|
||||||
|
|
||||||
@@ -94,9 +97,10 @@ enum
|
|||||||
PROP_SHOW_WEEK_NUMBERS,
|
PROP_SHOW_WEEK_NUMBERS,
|
||||||
PROP_SHOW_SELECTED_WEEK,
|
PROP_SHOW_SELECTED_WEEK,
|
||||||
PROP_SHOW_EVENTS,
|
PROP_SHOW_EVENTS,
|
||||||
|
PROP_SPLIT_MONTH_YEAR,
|
||||||
PROP_DATE,
|
PROP_DATE,
|
||||||
PROP_CONTEXT,
|
PROP_CONTEXT,
|
||||||
NUM_PROPERTIES = PROP_SHOW_EVENTS + 1,
|
NUM_PROPERTIES = PROP_SPLIT_MONTH_YEAR + 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
static guint signals[LAST_SIGNAL] = { 0, };
|
static guint signals[LAST_SIGNAL] = { 0, };
|
||||||
@@ -108,6 +112,22 @@ static const guint month_length[2][13] =
|
|||||||
{ 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
|
{ 0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
get_combined_choice_visible (gpointer user_data,
|
||||||
|
gboolean show_heading,
|
||||||
|
gboolean split_month_year)
|
||||||
|
{
|
||||||
|
return show_heading && !split_month_year;
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean
|
||||||
|
get_split_choice_visible (gpointer user_data,
|
||||||
|
gboolean show_heading,
|
||||||
|
gboolean split_month_year)
|
||||||
|
{
|
||||||
|
return show_heading && split_month_year;
|
||||||
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
leap (guint year)
|
leap (guint year)
|
||||||
{
|
{
|
||||||
@@ -266,6 +286,24 @@ format_month (GcalMultiChoice *choice,
|
|||||||
return g_strdup (gcal_get_month_name (value));
|
return g_strdup (gcal_get_month_name (value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static gchar *
|
||||||
|
format_month_year (GcalMultiChoice *choice,
|
||||||
|
gint value,
|
||||||
|
gpointer data)
|
||||||
|
{
|
||||||
|
GcalDateChooser *self = GCAL_DATE_CHOOSER (data);
|
||||||
|
g_autoptr (GDateTime) now = g_date_time_new_now (g_date_time_get_timezone (self->date));
|
||||||
|
gint year, month;
|
||||||
|
|
||||||
|
year = value / 12;
|
||||||
|
month = value % 12;
|
||||||
|
|
||||||
|
if (g_date_time_get_year (now) == year)
|
||||||
|
return g_strdup (gcal_get_month_name (month));
|
||||||
|
else
|
||||||
|
return g_strdup_printf ("%s %d", gcal_get_month_name (month), year);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
calendar_init_month_display (GcalDateChooser *self)
|
calendar_init_month_display (GcalDateChooser *self)
|
||||||
{
|
{
|
||||||
@@ -283,6 +321,8 @@ calendar_init_month_display (GcalDateChooser *self)
|
|||||||
|
|
||||||
gcal_multi_choice_set_choices (GCAL_MULTI_CHOICE (self->month_choice),
|
gcal_multi_choice_set_choices (GCAL_MULTI_CHOICE (self->month_choice),
|
||||||
(const gchar**) months);
|
(const gchar**) months);
|
||||||
|
gcal_multi_choice_set_choices (GCAL_MULTI_CHOICE (self->popover_month_choice),
|
||||||
|
(const gchar**) months);
|
||||||
|
|
||||||
for (i = 0; i < 12; i++)
|
for (i = 0; i < 12; i++)
|
||||||
g_free (months[i]);
|
g_free (months[i]);
|
||||||
@@ -291,6 +331,15 @@ calendar_init_month_display (GcalDateChooser *self)
|
|||||||
format_month,
|
format_month,
|
||||||
self,
|
self,
|
||||||
NULL);
|
NULL);
|
||||||
|
gcal_multi_choice_set_format_callback (GCAL_MULTI_CHOICE (self->popover_month_choice),
|
||||||
|
format_month,
|
||||||
|
self,
|
||||||
|
NULL);
|
||||||
|
|
||||||
|
gcal_multi_choice_set_format_callback (GCAL_MULTI_CHOICE (self->combined_choice),
|
||||||
|
format_month_year,
|
||||||
|
self,
|
||||||
|
NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@@ -591,6 +640,10 @@ calendar_set_property (GObject *obj,
|
|||||||
gcal_date_chooser_set_show_events (self, g_value_get_boolean (value));
|
gcal_date_chooser_set_show_events (self, g_value_get_boolean (value));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case PROP_SPLIT_MONTH_YEAR:
|
||||||
|
gcal_date_chooser_set_split_month_year (self, g_value_get_boolean (value));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
|
||||||
break;
|
break;
|
||||||
@@ -635,6 +688,10 @@ calendar_get_property (GObject *obj,
|
|||||||
g_value_set_boolean (value, self->show_events);
|
g_value_set_boolean (value, self->show_events);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case PROP_SPLIT_MONTH_YEAR:
|
||||||
|
g_value_set_boolean (value, self->split_month_year);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
|
||||||
break;
|
break;
|
||||||
@@ -642,13 +699,13 @@ calendar_get_property (GObject *obj,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
multi_choice_changed (GcalDateChooser *self)
|
multi_choice_changed (GcalDateChooser *self,
|
||||||
|
gint year,
|
||||||
|
gint month)
|
||||||
{
|
{
|
||||||
g_autoptr (GDateTime) date = NULL;
|
g_autoptr (GDateTime) date = NULL;
|
||||||
gint year, month, day;
|
gint day;
|
||||||
|
|
||||||
year = gcal_multi_choice_get_value (GCAL_MULTI_CHOICE (self->year_choice));
|
|
||||||
month = gcal_multi_choice_get_value (GCAL_MULTI_CHOICE (self->month_choice)) + 1;
|
|
||||||
day = g_date_time_get_day_of_month (self->date);
|
day = g_date_time_get_day_of_month (self->date);
|
||||||
|
|
||||||
/* Make sure the day is valid at that month */
|
/* Make sure the day is valid at that month */
|
||||||
@@ -658,6 +715,28 @@ multi_choice_changed (GcalDateChooser *self)
|
|||||||
gcal_date_chooser_set_date (GCAL_VIEW (self), date);
|
gcal_date_chooser_set_date (GCAL_VIEW (self), date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
combined_multi_choice_changed (GcalDateChooser *self)
|
||||||
|
{
|
||||||
|
gint year, month;
|
||||||
|
|
||||||
|
year = gcal_multi_choice_get_value (GCAL_MULTI_CHOICE (self->combined_choice)) / 12;
|
||||||
|
month = gcal_multi_choice_get_value (GCAL_MULTI_CHOICE (self->combined_choice)) % 12 + 1;
|
||||||
|
|
||||||
|
multi_choice_changed (self, year, month);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
split_multi_choice_changed (GcalDateChooser *self)
|
||||||
|
{
|
||||||
|
gint year, month;
|
||||||
|
|
||||||
|
year = gcal_multi_choice_get_value (GCAL_MULTI_CHOICE (self->year_choice));
|
||||||
|
month = gcal_multi_choice_get_value (GCAL_MULTI_CHOICE (self->month_choice)) + 1;
|
||||||
|
|
||||||
|
multi_choice_changed (self, year, month);
|
||||||
|
}
|
||||||
|
|
||||||
static gboolean
|
static gboolean
|
||||||
on_drop_target_drop_cb (GtkDropTarget *target,
|
on_drop_target_drop_cb (GtkDropTarget *target,
|
||||||
const GValue *value,
|
const GValue *value,
|
||||||
@@ -723,8 +802,6 @@ gcal_date_chooser_class_init (GcalDateChooserClass *class)
|
|||||||
object_class->set_property = calendar_set_property;
|
object_class->set_property = calendar_set_property;
|
||||||
object_class->get_property = calendar_get_property;
|
object_class->get_property = calendar_get_property;
|
||||||
|
|
||||||
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-date-chooser.ui");
|
|
||||||
|
|
||||||
properties[PROP_SHOW_HEADING] = g_param_spec_boolean ("show-heading",
|
properties[PROP_SHOW_HEADING] = g_param_spec_boolean ("show-heading",
|
||||||
"Show Heading",
|
"Show Heading",
|
||||||
"If TRUE, a heading is displayed",
|
"If TRUE, a heading is displayed",
|
||||||
@@ -760,6 +837,12 @@ gcal_date_chooser_class_init (GcalDateChooserClass *class)
|
|||||||
TRUE,
|
TRUE,
|
||||||
G_PARAM_READWRITE);
|
G_PARAM_READWRITE);
|
||||||
|
|
||||||
|
properties[PROP_SPLIT_MONTH_YEAR] = g_param_spec_boolean ("split-month-year",
|
||||||
|
"Split Month Year",
|
||||||
|
"If TRUE, the month and year selectors are split",
|
||||||
|
TRUE,
|
||||||
|
G_PARAM_READWRITE);
|
||||||
|
|
||||||
g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);
|
g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);
|
||||||
|
|
||||||
g_object_class_override_property (object_class, PROP_DATE, "active-date");
|
g_object_class_override_property (object_class, PROP_DATE, "active-date");
|
||||||
@@ -773,11 +856,18 @@ gcal_date_chooser_class_init (GcalDateChooserClass *class)
|
|||||||
NULL,
|
NULL,
|
||||||
G_TYPE_NONE, 0);
|
G_TYPE_NONE, 0);
|
||||||
|
|
||||||
|
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/event-editor/gcal-date-chooser.ui");
|
||||||
|
|
||||||
|
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, combined_choice);
|
||||||
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, month_choice);
|
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, month_choice);
|
||||||
|
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, popover_month_choice);
|
||||||
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, year_choice);
|
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, year_choice);
|
||||||
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, grid);
|
gtk_widget_class_bind_template_child (widget_class, GcalDateChooser, grid);
|
||||||
|
|
||||||
gtk_widget_class_bind_template_callback (widget_class, multi_choice_changed);
|
gtk_widget_class_bind_template_callback (widget_class, combined_multi_choice_changed);
|
||||||
|
gtk_widget_class_bind_template_callback (widget_class, get_combined_choice_visible);
|
||||||
|
gtk_widget_class_bind_template_callback (widget_class, get_split_choice_visible);
|
||||||
|
gtk_widget_class_bind_template_callback (widget_class, split_multi_choice_changed);
|
||||||
|
|
||||||
gtk_widget_class_set_css_name (widget_class, "datechooser");
|
gtk_widget_class_set_css_name (widget_class, "datechooser");
|
||||||
}
|
}
|
||||||
@@ -821,6 +911,7 @@ gcal_date_chooser_init (GcalDateChooser *self)
|
|||||||
self->show_week_numbers = TRUE;
|
self->show_week_numbers = TRUE;
|
||||||
self->show_selected_week = TRUE;
|
self->show_selected_week = TRUE;
|
||||||
self->show_events = TRUE;
|
self->show_events = TRUE;
|
||||||
|
self->split_month_year = TRUE;
|
||||||
|
|
||||||
self->date = g_date_time_new_now_local ();
|
self->date = g_date_time_new_now_local ();
|
||||||
g_date_time_get_ymd (self->date, &self->this_year, NULL, NULL);
|
g_date_time_get_ymd (self->date, &self->this_year, NULL, NULL);
|
||||||
@@ -943,6 +1034,7 @@ gcal_date_chooser_init (GcalDateChooser *self)
|
|||||||
g_date_time_get_ymd (self->date, &year, &month, NULL);
|
g_date_time_get_ymd (self->date, &year, &month, NULL);
|
||||||
gcal_multi_choice_set_value (GCAL_MULTI_CHOICE (self->year_choice), year);
|
gcal_multi_choice_set_value (GCAL_MULTI_CHOICE (self->year_choice), year);
|
||||||
gcal_multi_choice_set_value (GCAL_MULTI_CHOICE (self->month_choice), month - 1);
|
gcal_multi_choice_set_value (GCAL_MULTI_CHOICE (self->month_choice), month - 1);
|
||||||
|
gcal_multi_choice_set_value (GCAL_MULTI_CHOICE (self->combined_choice), year * 12 + month - 1);
|
||||||
calendar_update_selected_day_display (self);
|
calendar_update_selected_day_display (self);
|
||||||
|
|
||||||
drop_target = gtk_drop_target_new (G_TYPE_STRING, GDK_ACTION_COPY);
|
drop_target = gtk_drop_target_new (G_TYPE_STRING, GDK_ACTION_COPY);
|
||||||
@@ -1067,3 +1159,21 @@ gcal_date_chooser_get_show_events (GcalDateChooser *self)
|
|||||||
{
|
{
|
||||||
return self->show_events;
|
return self->show_events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
gcal_date_chooser_set_split_month_year (GcalDateChooser *self,
|
||||||
|
gboolean setting)
|
||||||
|
{
|
||||||
|
if (self->split_month_year == setting)
|
||||||
|
return;
|
||||||
|
|
||||||
|
self->split_month_year = setting;
|
||||||
|
|
||||||
|
g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SPLIT_MONTH_YEAR]);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean
|
||||||
|
gcal_date_chooser_get_split_month_year (GcalDateChooser *self)
|
||||||
|
{
|
||||||
|
return self->split_month_year;
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ gboolean gcal_date_chooser_get_show_events (GcalDateChoose
|
|||||||
void gcal_date_chooser_set_show_events (GcalDateChooser *self,
|
void gcal_date_chooser_set_show_events (GcalDateChooser *self,
|
||||||
gboolean setting);
|
gboolean setting);
|
||||||
|
|
||||||
|
gboolean gcal_date_chooser_get_split_month_year (GcalDateChooser *self);
|
||||||
|
|
||||||
|
void gcal_date_chooser_set_split_month_year (GcalDateChooser *self,
|
||||||
|
gboolean setting);
|
||||||
|
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
|
|
||||||
#endif /* __GCAL_DATE_CHOOSER_H__ */
|
#endif /* __GCAL_DATE_CHOOSER_H__ */
|
||||||
|
|||||||
@@ -7,13 +7,18 @@
|
|||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GcalMultiChoice" id="month_choice">
|
<object class="GcalMultiChoice" id="month_choice">
|
||||||
<property name="visible" bind-source="GcalDateChooser" bind-property="show-heading" bind-flags="sync-create"/>
|
|
||||||
<property name="halign">fill</property>
|
<property name="halign">fill</property>
|
||||||
<property name="min-value">0</property>
|
<property name="min-value">0</property>
|
||||||
<property name="max-value">11</property>
|
<property name="max-value">11</property>
|
||||||
<property name="animate">False</property>
|
<property name="animate">False</property>
|
||||||
<property name="value">0</property>
|
<property name="value">0</property>
|
||||||
<signal name="notify::value" handler="multi_choice_changed" swapped="yes"/>
|
<binding name="visible">
|
||||||
|
<closure type="gboolean" function="get_split_choice_visible">
|
||||||
|
<lookup name="show-heading">GcalDateChooser</lookup>
|
||||||
|
<lookup name="split-month-year">GcalDateChooser</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
|
<signal name="notify::value" handler="split_multi_choice_changed" swapped="yes"/>
|
||||||
<layout>
|
<layout>
|
||||||
<property name="column">0</property>
|
<property name="column">0</property>
|
||||||
<property name="row">0</property>
|
<property name="row">0</property>
|
||||||
@@ -22,19 +27,47 @@
|
|||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GcalMultiChoice" id="year_choice">
|
<object class="GcalMultiChoice" id="year_choice">
|
||||||
<property name="visible" bind-source="GcalDateChooser" bind-property="show-heading" bind-flags="sync-create"/>
|
<binding name="visible">
|
||||||
|
<closure type="gboolean" function="get_split_choice_visible">
|
||||||
|
<lookup name="show-heading">GcalDateChooser</lookup>
|
||||||
|
<lookup name="split-month-year">GcalDateChooser</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
<property name="halign">fill</property>
|
<property name="halign">fill</property>
|
||||||
<property name="min-value">0</property>
|
<property name="min-value">1</property>
|
||||||
<property name="max-value">120000</property>
|
<property name="max-value">9999</property>
|
||||||
<property name="animate">False</property>
|
<property name="animate">False</property>
|
||||||
<property name="value">0</property>
|
<property name="value">0</property>
|
||||||
<signal name="notify::value" handler="multi_choice_changed" swapped="yes"/>
|
<signal name="notify::value" handler="split_multi_choice_changed" swapped="yes"/>
|
||||||
<layout>
|
<layout>
|
||||||
<property name="column">1</property>
|
<property name="column">1</property>
|
||||||
<property name="row">0</property>
|
<property name="row">0</property>
|
||||||
</layout>
|
</layout>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GcalMultiChoice" id="combined_choice">
|
||||||
|
<binding name="visible">
|
||||||
|
<closure type="gboolean" function="get_combined_choice_visible">
|
||||||
|
<lookup name="show-heading">GcalDateChooser</lookup>
|
||||||
|
<lookup name="split-month-year">GcalDateChooser</lookup>
|
||||||
|
</closure>
|
||||||
|
</binding>
|
||||||
|
<property name="halign">fill</property>
|
||||||
|
<!-- From January 1 (month 12) to December 9999 (month 119999) -->
|
||||||
|
<property name="min-value">12</property>
|
||||||
|
<property name="max-value">119999</property>
|
||||||
|
<property name="popover">popover</property>
|
||||||
|
<property name="animate">False</property>
|
||||||
|
<property name="value">0</property>
|
||||||
|
<signal name="notify::value" handler="combined_multi_choice_changed" swapped="yes"/>
|
||||||
|
<layout>
|
||||||
|
<property name="column">0</property>
|
||||||
|
<property name="column-span">2</property>
|
||||||
|
<property name="row">0</property>
|
||||||
|
</layout>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkGrid" id="grid">
|
<object class="GtkGrid" id="grid">
|
||||||
<property name="row-homogeneous">True</property>
|
<property name="row-homogeneous">True</property>
|
||||||
@@ -49,4 +82,33 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<object class="GtkPopover" id="popover">
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="spacing">12</property>
|
||||||
|
<child>
|
||||||
|
<object class="GcalMultiChoice" id="popover_month_choice">
|
||||||
|
<property name="min-value" bind-source="month_choice" bind-property="min-value" bind-flags="sync-create|bidirectional" />
|
||||||
|
<property name="max-value" bind-source="month_choice" bind-property="max-value" bind-flags="sync-create|bidirectional" />
|
||||||
|
<property name="animate" bind-source="month_choice" bind-property="animate" bind-flags="sync-create|bidirectional" />
|
||||||
|
<property name="value" bind-source="month_choice" bind-property="value" bind-flags="sync-create|bidirectional" />
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GcalMultiChoice">
|
||||||
|
<property name="min-value" bind-source="year_choice" bind-property="min-value" bind-flags="sync-create|bidirectional" />
|
||||||
|
<property name="max-value" bind-source="year_choice" bind-property="max-value" bind-flags="sync-create|bidirectional" />
|
||||||
|
<property name="animate" bind-source="year_choice" bind-property="animate" bind-flags="sync-create|bidirectional" />
|
||||||
|
<property name="value" bind-source="year_choice" bind-property="value" bind-flags="sync-create|bidirectional" />
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<style>
|
||||||
|
<class name="activatable"/>
|
||||||
|
<class name="month"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
</interface>
|
</interface>
|
||||||
|
|||||||
Reference in New Issue
Block a user