# HG changeset patch
# Node ID 31096454cb8f
# Parent  25a2452792c1
diff --git a/libpurple/protocols/demo/purpledemoprotocolactions.c b/libpurple/protocols/demo/purpledemoprotocolactions.c
--- a/libpurple/protocols/demo/purpledemoprotocolactions.c
+++ b/libpurple/protocols/demo/purpledemoprotocolactions.c
@@ -502,13 +502,13 @@ purple_demo_protocol_request_fields_acti
 	                                        _("default"), FALSE);
 	purple_request_field_set_validator(field,
 	                                   purple_request_field_alphanumeric_validator,
-	                                   NULL);
+	                                   NULL, NULL);
 	purple_request_group_add_field(group, field);
 	field = purple_request_field_string_new("email", _("An email"),
 	                                        _("me@example.com"), FALSE);
 	purple_request_field_set_validator(field,
 	                                   purple_request_field_email_validator,
-	                                   NULL);
+	                                   NULL, NULL);
 	purple_request_group_add_field(group, field);
 	field = purple_request_field_int_new("int", _("An integer"), 123, -42, 1337);
 	purple_request_group_add_field(group, field);
diff --git a/libpurple/purplerequestfield.h b/libpurple/purplerequestfield.h
--- a/libpurple/purplerequestfield.h
+++ b/libpurple/purplerequestfield.h
@@ -54,13 +54,23 @@ struct _PurpleRequestFieldClass {
 
 	/*< public >*/
 	gboolean (*is_filled)(PurpleRequestField *field);
+	gboolean (*is_valid)(PurpleRequestField *field, char **errmsg);
 
 	/*< private >*/
 	gpointer reserved[4];
 };
 
-typedef gboolean (*PurpleRequestFieldValidator)(PurpleRequestField *field,
-	gchar **errmsg, gpointer user_data);
+/**
+ * PurpleRequestFieldValidator:
+ * @field: The field.
+ * @errmsg: (nullable) (optional) (out): A location to store an error message
+ *          if the field is invalid.
+ * @user_data: (closure): The data passed to
+ *             [method@Purple.RequestField.set_validator].
+ *
+ * A callback to check whether a field is valid.
+ */
+typedef gboolean (*PurpleRequestFieldValidator)(PurpleRequestField *field, char **errmsg, gpointer user_data);
 
 G_BEGIN_DECLS
 
@@ -207,13 +217,14 @@ gboolean purple_request_field_is_filled(
 /**
  * purple_request_field_set_validator:
  * @field:     The field.
- * @validator: (scope notified): The validator callback, NULL to disable validation.
- * @user_data: The data to pass to the callback.
+ * @validator: (scope notified) (closure user_data): The validator callback, or
+ *             NULL to disable additional validation.
+ * @user_data: The data to pass to the validator callback.
+ * @destroy_data: A cleanup function for @user_data.
  *
- * Sets validator for a single field.
+ * Set an additional validator for a field.
  */
-void purple_request_field_set_validator(PurpleRequestField *field,
-	PurpleRequestFieldValidator validator, void *user_data);
+void purple_request_field_set_validator(PurpleRequestField *field, PurpleRequestFieldValidator validator, gpointer user_data, GDestroyNotify destroy_data);
 
 /**
  * purple_request_field_is_validatable:
@@ -228,8 +239,8 @@ gboolean purple_request_field_is_validat
 /**
  * purple_request_field_is_valid:
  * @field:  The field.
- * @errmsg: If non-NULL, the memory area, where the pointer to validation
- *         failure message will be set.
+ * @errmsg: (nullable) (optional) (out): If non-NULL, the memory area, where
+ *          the validation failure message will be returned.
  *
  * Checks, if specified field is valid.
  *
diff --git a/libpurple/purplerequestfield.c b/libpurple/purplerequestfield.c
--- a/libpurple/purplerequestfield.c
+++ b/libpurple/purplerequestfield.c
@@ -36,8 +36,7 @@ typedef struct {
 
 	char *tooltip;
 
-	PurpleRequestFieldValidator validator;
-	void *validator_data;
+	GClosure *validator;
 } PurpleRequestFieldPrivate;
 
 enum {
@@ -50,6 +49,7 @@ enum {
 	PROP_TOOLTIP,
 	PROP_REQUIRED,
 	PROP_FILLED,
+	PROP_VALID,
 	PROP_IS_VALIDATABLE,
 	N_PROPERTIES,
 };
@@ -111,6 +111,10 @@ purple_request_field_get_property(GObjec
 			g_value_set_boolean(value,
 			                    purple_request_field_is_filled(field));
 			break;
+		case PROP_VALID:
+			g_value_set_boolean(value,
+			                    purple_request_field_is_valid(field, NULL));
+			break;
 		case PROP_IS_VALIDATABLE:
 			g_value_set_boolean(value,
 			                    purple_request_field_is_validatable(field));
@@ -170,6 +174,7 @@ purple_request_field_finalize(GObject *o
 	g_free(priv->label);
 	g_free(priv->type_hint);
 	g_free(priv->tooltip);
+	g_clear_pointer(&priv->validator, g_closure_unref);
 
 	G_OBJECT_CLASS(purple_request_field_parent_class)->finalize(obj);
 }
@@ -297,6 +302,19 @@ purple_request_field_class_init(PurpleRe
 		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
 
 	/**
+	 * PurpleRequestField:valid:
+	 *
+	 * Whether the field has a valid value.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_VALID] = g_param_spec_boolean(
+		"valid", "valid",
+		"Whether the field has a valid value.",
+		TRUE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * PurpleRequestField:is-validatable:
 	 *
 	 * Whether the field can be validated by the requestor.
@@ -505,7 +523,9 @@ purple_request_field_is_filled(PurpleReq
 
 void
 purple_request_field_set_validator(PurpleRequestField *field,
-	PurpleRequestFieldValidator validator, void *user_data)
+                                   PurpleRequestFieldValidator validator,
+                                   gpointer user_data,
+                                   GDestroyNotify destroy_data)
 {
 	PurpleRequestFieldPrivate *priv = NULL;
 
@@ -513,8 +533,14 @@ purple_request_field_set_validator(Purpl
 
 	priv = purple_request_field_get_instance_private(field);
 
-	priv->validator = validator;
-	priv->validator_data = validator ? user_data : NULL;
+	g_clear_pointer(&priv->validator, g_closure_unref);
+	if(validator != NULL) {
+		priv->validator = g_cclosure_new(G_CALLBACK(validator), user_data,
+		                                 (GClosureNotify)G_CALLBACK(destroy_data));
+		g_closure_ref(priv->validator);
+		g_closure_sink(priv->validator);
+		g_closure_set_marshal(priv->validator, g_cclosure_marshal_generic);
+	}
 
 	if(PURPLE_IS_REQUEST_GROUP(priv->group)) {
 		_purple_request_group_set_field_validator(priv->group, field,
@@ -522,6 +548,7 @@ purple_request_field_set_validator(Purpl
 	}
 
 	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_IS_VALIDATABLE]);
+	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALID]);
 }
 
 gboolean
@@ -539,25 +566,49 @@ purple_request_field_is_validatable(Purp
 gboolean
 purple_request_field_is_valid(PurpleRequestField *field, gchar **errmsg)
 {
+	PurpleRequestFieldClass *klass = NULL;
 	PurpleRequestFieldPrivate *priv = NULL;
-	gboolean valid;
+	gboolean valid = TRUE;
 
 	g_return_val_if_fail(PURPLE_IS_REQUEST_FIELD(field), FALSE);
 
-	priv = purple_request_field_get_instance_private(field);
+	if(errmsg != NULL) {
+		*errmsg = NULL;
+	}
 
-	if(!priv->validator) {
+	if(!purple_request_field_is_required(field) &&
+	   !purple_request_field_is_filled(field))
+	{
 		return TRUE;
 	}
 
-	if (!purple_request_field_is_required(field) &&
-		!purple_request_field_is_filled(field))
-		return TRUE;
+	klass = PURPLE_REQUEST_FIELD_GET_CLASS(field);
+	if(klass != NULL && klass->is_valid != NULL) {
+		valid = klass->is_valid(field, errmsg);
+	}
 
-	valid = priv->validator(field, errmsg, priv->validator_data);
+	priv = purple_request_field_get_instance_private(field);
+	if(valid && priv->validator != NULL) {
+		GValue result = G_VALUE_INIT;
+		GValue params[] = {G_VALUE_INIT, G_VALUE_INIT};
+		g_value_init(&result, G_TYPE_BOOLEAN);
+		g_value_set_instance(g_value_init(&params[0],
+		                                  PURPLE_TYPE_REQUEST_FIELD),
+		                     field);
+		g_value_set_pointer(g_value_init(&params[1], G_TYPE_POINTER), errmsg);
+		g_closure_invoke(priv->validator, &result,
+		                 G_N_ELEMENTS(params), params, NULL);
+		valid = g_value_get_boolean(&result);
+		g_value_unset(&result);
+		for(gsize i = 0; i < G_N_ELEMENTS(params); i++) {
+			g_value_unset(&params[i]);
+		}
+	}
 
-	if (valid && errmsg)
-		*errmsg = NULL;
+	if(!valid && errmsg != NULL && *errmsg == NULL) {
+		*errmsg = g_strdup("Internal validation failed without setting an "
+		                   "error message.");
+	}
 
 	return valid;
 }
diff --git a/libpurple/request/purplerequestfieldaccount.c b/libpurple/request/purplerequestfieldaccount.c
--- a/libpurple/request/purplerequestfieldaccount.c
+++ b/libpurple/request/purplerequestfieldaccount.c
@@ -218,6 +218,7 @@ purple_request_field_account_set_value(P
 
 	if(g_set_object(&field->account, value)) {
 		g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALUE]);
+		g_object_notify(G_OBJECT(field), "valid");
 	}
 }
 
diff --git a/libpurple/request/purplerequestfieldbool.c b/libpurple/request/purplerequestfieldbool.c
--- a/libpurple/request/purplerequestfieldbool.c
+++ b/libpurple/request/purplerequestfieldbool.c
@@ -173,7 +173,8 @@ purple_request_field_bool_set_value(Purp
 
 	field->value = value;
 
-	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_DEFAULT_VALUE]);
+	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALUE]);
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 gboolean
diff --git a/libpurple/request/purplerequestfieldchoice.c b/libpurple/request/purplerequestfieldchoice.c
--- a/libpurple/request/purplerequestfieldchoice.c
+++ b/libpurple/request/purplerequestfieldchoice.c
@@ -208,6 +208,7 @@ purple_request_field_choice_set_value(Pu
 	field->value = value;
 
 	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALUE]);
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 gpointer
diff --git a/libpurple/request/purplerequestfieldint.c b/libpurple/request/purplerequestfieldint.c
--- a/libpurple/request/purplerequestfieldint.c
+++ b/libpurple/request/purplerequestfieldint.c
@@ -45,6 +45,32 @@ enum {
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
 
 /******************************************************************************
+ * PurpleRequestField Implementation
+ *****************************************************************************/
+static gboolean
+purple_request_field_int_is_valid(PurpleRequestField *field, char **errmsg) {
+	PurpleRequestFieldInt *intfield = PURPLE_REQUEST_FIELD_INT(field);
+
+	if(intfield->value < intfield->lower_bound) {
+		if(errmsg != NULL) {
+			*errmsg = g_strdup_printf("Int value %d exceeds lower bound %d",
+			                          intfield->value, intfield->lower_bound);
+		}
+		return FALSE;
+	}
+
+	if(intfield->value > intfield->upper_bound) {
+		if(errmsg != NULL) {
+			*errmsg = g_strdup_printf("Int value %d exceeds upper bound %d",
+			                          intfield->value, intfield->upper_bound);
+		}
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 G_DEFINE_TYPE(PurpleRequestFieldInt, purple_request_field_int,
@@ -112,8 +138,10 @@ purple_request_field_int_init(G_GNUC_UNU
 
 static void
 purple_request_field_int_class_init(PurpleRequestFieldIntClass *klass) {
+	PurpleRequestFieldClass *request_class = PURPLE_REQUEST_FIELD_CLASS(klass);
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 
+	request_class->is_valid = purple_request_field_int_is_valid;
 	obj_class->get_property = purple_request_field_int_get_property;
 	obj_class->set_property = purple_request_field_int_set_property;
 
@@ -221,6 +249,7 @@ purple_request_field_int_set_lower_bound
 	field->lower_bound = lower_bound;
 
 	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_LOWER_BOUND]);
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 void
@@ -236,18 +265,13 @@ purple_request_field_int_set_upper_bound
 	field->upper_bound = upper_bound;
 
 	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_UPPER_BOUND]);
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 void
 purple_request_field_int_set_value(PurpleRequestFieldInt *field, int value) {
 	g_return_if_fail(PURPLE_IS_REQUEST_FIELD_INT(field));
 
-	if(value < field->lower_bound || value > field->upper_bound) {
-		g_warning("Int value %d out of bounds (%d, %d)", value,
-		          field->lower_bound, field->upper_bound);
-		return;
-	}
-
 	if(field->value == value) {
 		return;
 	}
@@ -255,6 +279,7 @@ purple_request_field_int_set_value(Purpl
 	field->value = value;
 
 	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALUE]);
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 int
diff --git a/libpurple/request/purplerequestfieldlist.c b/libpurple/request/purplerequestfieldlist.c
--- a/libpurple/request/purplerequestfieldlist.c
+++ b/libpurple/request/purplerequestfieldlist.c
@@ -190,6 +190,7 @@ purple_request_field_list_add_icon(Purpl
 	kvp = purple_key_value_pair_new_full(item, g_strdup(icon_path), g_free);
 	field->items = g_list_append(field->items, kvp);
 	g_hash_table_insert(field->item_data, g_strdup(item), data);
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 void
@@ -208,6 +209,7 @@ purple_request_field_list_add_selected(P
 	field->selected = g_list_append(field->selected, g_strdup(item));
 
 	g_hash_table_add(field->selected_table, g_strdup(item));
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 void
@@ -218,6 +220,7 @@ purple_request_field_list_clear_selected
 	field->selected = NULL;
 
 	g_hash_table_remove_all(field->selected_table);
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 void
@@ -242,6 +245,8 @@ purple_request_field_list_set_selected(P
 		field->selected = g_list_append(field->selected, g_strdup(selected));
 		g_hash_table_add(field->selected_table, g_strdup(selected));
 	}
+
+	g_object_notify(G_OBJECT(field), "valid");
 }
 
 gboolean
diff --git a/libpurple/request/purplerequestfieldstring.h b/libpurple/request/purplerequestfieldstring.h
--- a/libpurple/request/purplerequestfieldstring.h
+++ b/libpurple/request/purplerequestfieldstring.h
@@ -134,7 +134,7 @@ gboolean purple_request_field_string_is_
  *
  * Validates a field which should contain an email address.
  *
- * See purple_request_field_set_validator().
+ * See [method@Purple.RequestField.set_validator].
  *
  * Returns: TRUE, if field contains valid email address.
  */
@@ -149,7 +149,7 @@ gboolean purple_request_field_email_vali
  *
  * Validates a field which should contain alphanumeric content.
  *
- * See purple_request_field_set_validator().
+ * See [method@Purple.RequestField.set_validator].
  *
  * Returns: TRUE, if field contains only alphanumeric characters.
  */
diff --git a/libpurple/request/purplerequestfieldstring.c b/libpurple/request/purplerequestfieldstring.c
--- a/libpurple/request/purplerequestfieldstring.c
+++ b/libpurple/request/purplerequestfieldstring.c
@@ -265,6 +265,7 @@ purple_request_field_string_set_value(Pu
 
 	g_object_freeze_notify(G_OBJECT(field));
 	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALUE]);
+	g_object_notify(G_OBJECT(field), "valid");
 	if(before != after) {
 		g_object_notify(G_OBJECT(field), "filled");
 	}
diff --git a/libpurple/tests/test_request_field.c b/libpurple/tests/test_request_field.c
--- a/libpurple/tests/test_request_field.c
+++ b/libpurple/tests/test_request_field.c
@@ -113,6 +113,96 @@ test_request_field_filled_nonstring(void
 	g_object_unref(field);
 }
 
+static void
+test_request_field_valid_int(void) {
+	PurpleRequestField *field = NULL;
+	char *errmsg = NULL;
+	gboolean result;
+
+	field = purple_request_field_int_new("test-int", "Test int", 50, 0, 100);
+	result = purple_request_field_is_valid(field, &errmsg);
+	g_assert_null(errmsg);
+	g_assert_true(result);
+
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field), -42);
+	result = purple_request_field_is_valid(field, &errmsg);
+	g_assert_cmpstr(errmsg, ==, "Int value -42 exceeds lower bound 0");
+	g_assert_false(result);
+	g_free(errmsg);
+
+	/* Don't crash if no error message is requested. */
+	result = purple_request_field_is_valid(field, NULL);
+	g_assert_false(result);
+
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field), 1337);
+	result = purple_request_field_is_valid(field, &errmsg);
+	g_assert_cmpstr(errmsg, ==, "Int value 1337 exceeds upper bound 100");
+	g_assert_false(result);
+	g_free(errmsg);
+
+	/* Don't crash if no error message is requested. */
+	result = purple_request_field_is_valid(field, NULL);
+	g_assert_false(result);
+
+	g_object_unref(field);
+}
+
+static gboolean
+test_request_field_validator_is_even(PurpleRequestField *field, char **errmsg,
+                                     G_GNUC_UNUSED gpointer data)
+{
+	gboolean *called = data;
+	gint value;
+
+	*called = TRUE;
+	g_return_val_if_fail(PURPLE_IS_REQUEST_FIELD_INT(field), FALSE);
+
+	value = purple_request_field_int_get_value(PURPLE_REQUEST_FIELD_INT(field));
+
+	if(value % 2 != 0) {
+		*errmsg = g_strdup_printf("Value %d is not even.", value);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static void
+test_request_field_valid_custom(void) {
+	PurpleRequestField *field = NULL;
+	char *errmsg = NULL;
+	gboolean result;
+	gboolean called = FALSE;
+
+	field = purple_request_field_int_new("test-int", "Test int", 50, 0, 100);
+	purple_request_field_set_validator(field,
+	                                   test_request_field_validator_is_even,
+	                                   &called, NULL);
+	result = purple_request_field_is_valid(field, &errmsg);
+	g_assert_cmpstr(errmsg, ==, NULL);
+	g_assert_true(result);
+
+	/* Default validator (i.e., the bounds) is checked first. */
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field), -42);
+	result = purple_request_field_is_valid(field, &errmsg);
+	g_assert_cmpstr(errmsg, ==, "Int value -42 exceeds lower bound 0");
+	g_assert_false(result);
+	g_assert_false(called);
+	g_free(errmsg);
+
+	/* But if default validator passes, then the custom one is checked. */
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field), 23);
+	result = purple_request_field_is_valid(field, &errmsg);
+	g_assert_cmpstr(errmsg, ==, "Value 23 is not even.");
+	g_assert_false(result);
+	g_assert_true(called);
+	g_free(errmsg);
+
+	g_object_unref(field);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -125,5 +215,9 @@ main(gint argc, gchar *argv[]) {
 	g_test_add_func("/request-field/filled-nonstring",
 	                test_request_field_filled_nonstring);
 
+	g_test_add_func("/request-field/valid-int", test_request_field_valid_int);
+	g_test_add_func("/request-field/valid-custom",
+	                test_request_field_valid_custom);
+
 	return g_test_run();
 }
diff --git a/pidgin/gtkdialogs.c b/pidgin/gtkdialogs.c
--- a/pidgin/gtkdialogs.c
+++ b/pidgin/gtkdialogs.c
@@ -96,7 +96,8 @@ pidgin_dialogs_im(void)
 	field = purple_request_field_string_new("screenname", _("_Name"), NULL, FALSE);
 	purple_request_field_set_type_hint(field, "screenname");
 	purple_request_field_set_required(field, TRUE);
-	purple_request_field_set_validator(field, pidgin_dialogs_im_name_validator, page);
+	purple_request_field_set_validator(field, pidgin_dialogs_im_name_validator,
+	                                   page, NULL);
 	purple_request_group_add_field(group, field);
 
 	field = purple_request_field_account_new("account", _("_Account"), NULL);
