# HG changeset patch
# Node ID e52a13d61c8c
# Parent  a8eb36b853a9
diff --git a/finch/gntrequest.c b/finch/gntrequest.c
--- a/finch/gntrequest.c
+++ b/finch/gntrequest.c
@@ -379,7 +379,7 @@ request_fields_cb(GntWidget *button, Pur
 
 	if (!g_object_get_data(G_OBJECT(button), "cancellation-function") &&
 			(!purple_request_page_all_required_filled(page) ||
-			!purple_request_page_all_valid(page))) {
+			!purple_request_page_is_valid(page))) {
 		purple_notify_error(button, _("Error"),
 			_("You must properly fill all the required fields."),
 			_("The required fields are underlined."), NULL);
diff --git a/libpurple/purplerequestgroup.h b/libpurple/purplerequestgroup.h
--- a/libpurple/purplerequestgroup.h
+++ b/libpurple/purplerequestgroup.h
@@ -107,6 +107,18 @@ GList *purple_request_group_get_fields(P
  */
 PurpleRequestPage *purple_request_group_get_page(PurpleRequestGroup *group);
 
+/**
+ * purple_request_group_is_valid:
+ * @group:  The field.
+ *
+ * Returns whether or not all fields are valid.
+ *
+ * Returns: TRUE if all fields in the group are valid, FALSE otherwise.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_request_group_is_valid(PurpleRequestGroup *group);
+
 G_END_DECLS
 
 #endif /* PURPLE_REQUEST_GROUP_H */
diff --git a/libpurple/purplerequestgroup.c b/libpurple/purplerequestgroup.c
--- a/libpurple/purplerequestgroup.c
+++ b/libpurple/purplerequestgroup.c
@@ -32,11 +32,13 @@ struct _PurpleRequestGroup {
 	char *title;
 
 	GList *fields;
+	GHashTable *invalid_fields;
 };
 
 enum {
 	PROP_0,
 	PROP_TITLE,
+	PROP_VALID,
 	N_PROPERTIES,
 };
 static GParamSpec *properties[N_PROPERTIES] = {NULL, };
@@ -53,6 +55,31 @@ purple_request_group_set_title(PurpleReq
 }
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_request_group_notify_field_cb(GObject *obj,
+                                     G_GNUC_UNUSED GParamSpec *pspec,
+                                     gpointer data)
+{
+	PurpleRequestGroup *group = PURPLE_REQUEST_GROUP(data);
+	PurpleRequestField *field = PURPLE_REQUEST_FIELD(obj);
+	gboolean before, after;
+
+	before = purple_request_group_is_valid(group);
+	if(purple_request_field_is_valid(field, NULL)) {
+		g_hash_table_remove(group->invalid_fields, field);
+	} else {
+		g_hash_table_add(group->invalid_fields, field);
+	}
+	after = purple_request_group_is_valid(group);
+
+	if(before != after) {
+		g_object_notify_by_pspec(G_OBJECT(group), properties[PROP_VALID]);
+	}
+}
+
+/******************************************************************************
  * GListModel Implementation
  *****************************************************************************/
 static GType
@@ -104,6 +131,9 @@ purple_request_group_get_property(GObjec
 		case PROP_TITLE:
 			g_value_set_string(value, purple_request_group_get_title(group));
 			break;
+		case PROP_VALID:
+			g_value_set_boolean(value, purple_request_group_is_valid(group));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
 			break;
@@ -133,12 +163,14 @@ purple_request_group_finalize(GObject *o
 	g_free(group->title);
 
 	g_list_free_full(group->fields, g_object_unref);
+	g_clear_pointer(&group->invalid_fields, g_hash_table_destroy);
 
 	G_OBJECT_CLASS(purple_request_group_parent_class)->finalize(obj);
 }
 
 static void
-purple_request_group_init(G_GNUC_UNUSED PurpleRequestGroup *group) {
+purple_request_group_init(PurpleRequestGroup *group) {
+	group->invalid_fields = g_hash_table_new(g_direct_hash, g_direct_equal);
 }
 
 static void
@@ -162,6 +194,19 @@ purple_request_group_class_init(PurpleRe
 		NULL,
 		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurpleRequestGroup:valid:
+	 *
+	 * Whether all fields in a group are valid.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_VALID] = g_param_spec_boolean(
+		"valid", "valid",
+		"Whether all fields in a group are valid.",
+		TRUE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -216,6 +261,10 @@ purple_request_group_add_field(PurpleReq
 	position = g_list_length(group->fields);
 	group->fields = g_list_append(group->fields, field);
 
+	purple_request_group_notify_field_cb(G_OBJECT(field), NULL, group);
+	g_signal_connect(field, "notify::valid",
+	                 G_CALLBACK(purple_request_group_notify_field_cb), group);
+
 	if(PURPLE_IS_REQUEST_PAGE(group->page)) {
 		_purple_request_page_add_field(group->page, field);
 	}
@@ -248,3 +297,10 @@ purple_request_group_get_page(PurpleRequ
 
 	return group->page;
 }
+
+gboolean
+purple_request_group_is_valid(PurpleRequestGroup *group) {
+	g_return_val_if_fail(PURPLE_IS_REQUEST_GROUP(group), FALSE);
+
+	return g_hash_table_size(group->invalid_fields) == 0;
+}
diff --git a/libpurple/purplerequestpage.h b/libpurple/purplerequestpage.h
--- a/libpurple/purplerequestpage.h
+++ b/libpurple/purplerequestpage.h
@@ -122,16 +122,16 @@ gboolean purple_request_page_is_field_re
 gboolean purple_request_page_all_required_filled(PurpleRequestPage *page);
 
 /**
- * purple_request_page_all_valid:
+ * purple_request_page_is_valid:
  * @page: The fields page.
  *
  * Returns whether or not all fields are valid.
  *
- * Returns: TRUE if all fields are valid, or FALSE.
+ * Returns: TRUE if all fields in the page are valid, FALSE otherwise.
  *
  * Since: 3.0.0
  */
-gboolean purple_request_page_all_valid(PurpleRequestPage *page);
+gboolean purple_request_page_is_valid(PurpleRequestPage *page);
 
 /**
  * purple_request_page_get_field:
diff --git a/libpurple/purplerequestpage.c b/libpurple/purplerequestpage.c
--- a/libpurple/purplerequestpage.c
+++ b/libpurple/purplerequestpage.c
@@ -34,6 +34,7 @@ struct _PurpleRequestPage {
 	GObject parent;
 
 	GList *groups;
+	GHashTable *invalid_groups;
 
 	GHashTable *fields;
 
@@ -42,6 +43,38 @@ struct _PurpleRequestPage {
 	GList *validated_fields;
 };
 
+enum {
+	PROP_0,
+	PROP_VALID,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_request_page_notify_group_cb(GObject *obj,
+                                    G_GNUC_UNUSED GParamSpec *pspec,
+                                    gpointer data)
+{
+	PurpleRequestPage *page = PURPLE_REQUEST_PAGE(data);
+	PurpleRequestGroup *group = PURPLE_REQUEST_GROUP(obj);
+	gboolean before, after;
+
+	before = purple_request_page_is_valid(page);
+	if(purple_request_group_is_valid(group)) {
+		g_hash_table_remove(page->invalid_groups, group);
+	} else {
+		g_hash_table_add(page->invalid_groups, group);
+	}
+	after = purple_request_page_is_valid(page);
+
+	if(before != after) {
+		g_object_notify_by_pspec(G_OBJECT(page), properties[PROP_VALID]);
+	}
+}
+
 /******************************************************************************
  * GListModel Implementation
  *****************************************************************************/
@@ -85,10 +118,27 @@ G_DEFINE_TYPE_WITH_CODE(PurpleRequestPag
                                               purple_request_page_list_model_init))
 
 static void
+purple_request_page_get_property(GObject *obj, guint param_id, GValue *value,
+                                 GParamSpec *pspec)
+{
+	PurpleRequestPage *page = PURPLE_REQUEST_PAGE(obj);
+
+	switch(param_id) {
+		case PROP_VALID:
+			g_value_set_boolean(value, purple_request_page_is_valid(page));
+			break;
+		default:
+			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+			break;
+	}
+}
+
+static void
 purple_request_page_finalize(GObject *obj) {
 	PurpleRequestPage *page = PURPLE_REQUEST_PAGE(obj);
 
 	g_list_free_full(page->groups, g_object_unref);
+	g_clear_pointer(&page->invalid_groups, g_hash_table_destroy);
 	g_list_free(page->required_fields);
 	g_list_free(page->validated_fields);
 	g_hash_table_destroy(page->fields);
@@ -99,6 +149,7 @@ purple_request_page_finalize(GObject *ob
 static void
 purple_request_page_init(PurpleRequestPage *page) {
 	page->fields = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+	page->invalid_groups = g_hash_table_new(g_direct_hash, g_direct_equal);
 }
 
 static void
@@ -106,6 +157,22 @@ purple_request_page_class_init(PurpleReq
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 
 	obj_class->finalize = purple_request_page_finalize;
+	obj_class->get_property = purple_request_page_get_property;
+
+	/**
+	 * PurpleRequestPage:valid:
+	 *
+	 * Whether all fields in a page are valid.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_VALID] = g_param_spec_boolean(
+		"valid", "valid",
+		"Whether all fields in a page are valid.",
+		TRUE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
 /******************************************************************************
@@ -177,6 +244,10 @@ purple_request_page_add_group(PurpleRequ
 
 	_purple_request_group_set_page(group, page);
 
+	purple_request_page_notify_group_cb(G_OBJECT(group), NULL, page);
+	g_signal_connect(group, "notify::valid",
+	                 G_CALLBACK(purple_request_page_notify_group_cb), page);
+
 	for (l = purple_request_group_get_fields(group);
 		 l != NULL;
 		 l = l->next) {
@@ -248,19 +319,10 @@ purple_request_page_all_required_filled(
 }
 
 gboolean
-purple_request_page_all_valid(PurpleRequestPage *page) {
-	GList *l;
-
+purple_request_page_is_valid(PurpleRequestPage *page) {
 	g_return_val_if_fail(PURPLE_IS_REQUEST_PAGE(page), FALSE);
 
-	for(l = page->validated_fields; l != NULL; l = l->next) {
-		PurpleRequestField *field = PURPLE_REQUEST_FIELD(l->data);
-
-		if (!purple_request_field_is_valid(field, NULL))
-			return FALSE;
-	}
-
-	return TRUE;
+	return g_hash_table_size(page->invalid_groups) == 0;
 }
 
 PurpleRequestField *
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -27,6 +27,8 @@ PROGS = [
     'purplepath',
     'queued_output_stream',
     'request_field',
+    'request_group',
+    'request_page',
     'str',
     'tags',
     'util',
diff --git a/libpurple/tests/test_request_group.c b/libpurple/tests/test_request_group.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_request_group.c
@@ -0,0 +1,103 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 2 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
+ * Lesser 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 <glib.h>
+
+#include <purple.h>
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_request_group_valid_changed_cb(G_GNUC_UNUSED GObject *obj,
+                                    G_GNUC_UNUSED GParamSpec *pspec,
+                                    gpointer data)
+{
+	gboolean *called = data;
+
+	*called = TRUE;
+}
+
+static void
+test_request_group_valid(void) {
+	PurpleRequestGroup *group = NULL;
+	PurpleRequestField *field1 = NULL, *field2 = NULL, *field3 = NULL;
+	gboolean called = FALSE;
+
+	group = purple_request_group_new("test-group");
+	g_signal_connect(group, "notify::valid",
+	                 G_CALLBACK(test_request_group_valid_changed_cb), &called);
+
+	/* Empty groups are always valid. */
+	g_assert_true(purple_request_group_is_valid(group));
+
+	/* An added valid field keeps the group valid. */
+	called = FALSE;
+	field1 = purple_request_field_int_new("test-int", "Test int", 50, 0, 100);
+	purple_request_group_add_field(group, field1);
+	g_assert_true(purple_request_group_is_valid(group));
+	g_assert_false(called);
+
+	/* Making the field invalid makes the group invalid. */
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field1), -42);
+	g_assert_false(purple_request_group_is_valid(group));
+	g_assert_true(called);
+
+	/* Adding an invalid field keeps the group invalid. */
+	called = FALSE;
+	field2 = purple_request_field_int_new("invalid", "Invalid", -42, 0, 100);
+	purple_request_group_add_field(group, field2);
+	g_assert_false(purple_request_group_is_valid(group));
+	g_assert_false(called);
+
+	/* Adding a valid field to an already invalid group does not change it to
+	 * valid accidentally. */
+	called = FALSE;
+	field3 = purple_request_field_int_new("valid", "Valid", 42, 0, 100);
+	purple_request_group_add_field(group, field3);
+	g_assert_false(purple_request_group_is_valid(group));
+	g_assert_false(called);
+
+	/* Making one field valid while others are still invalid keeps the group
+	 * invalid. */
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field1), 42);
+	g_assert_false(purple_request_group_is_valid(group));
+	g_assert_false(called);
+
+	/* Making last invalid field valid makes the group valid again. */
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field2), 42);
+	g_assert_true(purple_request_group_is_valid(group));
+	g_assert_true(called);
+
+	g_object_unref(group);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/request-group/valid", test_request_group_valid);
+
+	return g_test_run();
+}
diff --git a/libpurple/tests/test_request_page.c b/libpurple/tests/test_request_page.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_request_page.c
@@ -0,0 +1,194 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 2 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
+ * Lesser 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 <glib.h>
+
+#include <purple.h>
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+test_request_page_string_validator(PurpleRequestField *field, char **errmsg,
+                                   G_GNUC_UNUSED gpointer data)
+{
+	/* Validator for a string field that is valid if the value is "valid". */
+	PurpleRequestFieldString *strfield = PURPLE_REQUEST_FIELD_STRING(field);
+	const char *value = NULL;
+	gboolean result = TRUE;
+
+	value = purple_request_field_string_get_value(strfield);
+	if(!purple_strequal(value, "valid")) {
+		if(errmsg != NULL) {
+			*errmsg = g_strdup_printf("String value is not valid: %s", value);
+		}
+		result = FALSE;
+	}
+
+	return result;
+}
+
+static PurpleRequestGroup *
+test_request_page_new_valid_group(const char *name) {
+	PurpleRequestGroup *group = NULL;
+	PurpleRequestField *field = NULL;
+	char *field_name = NULL;
+
+	group = purple_request_group_new(name);
+
+	/* Field is valid, making the group valid. */
+	field_name = g_strdup_printf("%s-string", name);
+	field = purple_request_field_string_new(field_name, field_name, "valid",
+	                                        FALSE);
+	purple_request_field_set_validator(field,
+	                                   test_request_page_string_validator,
+	                                   NULL, NULL);
+	g_free(field_name);
+	purple_request_group_add_field(group, field);
+
+	return group;
+}
+
+static PurpleRequestGroup *
+test_request_page_new_invalid_group(const char *name) {
+	PurpleRequestGroup *group = NULL;
+	PurpleRequestField *field = NULL;
+	char *field_name = NULL;
+
+	group = purple_request_group_new(name);
+
+	/* Field is invalid, making the group invalid. */
+	field_name = g_strdup_printf("%s-string", name);
+	field = purple_request_field_string_new(field_name, field_name, "invalid",
+	                                        FALSE);
+	purple_request_field_set_validator(field,
+	                                   test_request_page_string_validator,
+	                                   NULL, NULL);
+	g_free(field_name);
+	purple_request_group_add_field(group, field);
+
+	return group;
+}
+
+static void
+test_request_page_make_group_valid(PurpleRequestGroup *group) {
+	GListModel *model = G_LIST_MODEL(group);
+	guint n_items;
+
+	n_items = g_list_model_get_n_items(model);
+	for(guint index = 0; index < n_items; index++) {
+		PurpleRequestFieldString *field = g_list_model_get_item(model, index);
+		purple_request_field_string_set_value(field, "valid");
+		g_object_unref(field);
+	}
+}
+
+static void
+test_request_page_make_group_invalid(PurpleRequestGroup *group) {
+	GListModel *model = G_LIST_MODEL(group);
+	guint n_items;
+
+	n_items = g_list_model_get_n_items(model);
+	for(guint index = 0; index < n_items; index++) {
+		PurpleRequestFieldString *field = g_list_model_get_item(model, index);
+		purple_request_field_string_set_value(field, "invalid");
+		g_object_unref(field);
+	}
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_request_page_valid_changed_cb(G_GNUC_UNUSED GObject *obj,
+                                   G_GNUC_UNUSED GParamSpec *pspec,
+                                   gpointer data)
+{
+	gboolean *called = data;
+
+	*called = TRUE;
+}
+
+static void
+test_request_page_valid(void) {
+	PurpleRequestPage *page = NULL;
+	PurpleRequestGroup *group1 = NULL, *group2 = NULL, *group3 = NULL;
+	gboolean called = FALSE;
+
+	page = purple_request_page_new();
+	g_signal_connect(page, "notify::valid",
+	                 G_CALLBACK(test_request_page_valid_changed_cb), &called);
+
+	/* Empty pages are always valid. */
+	g_assert_true(purple_request_page_is_valid(page));
+
+	/* An added valid group keeps the page valid. */
+	called = FALSE;
+	group1 = test_request_page_new_valid_group("group1");
+	purple_request_page_add_group(page, group1);
+	g_assert_true(purple_request_page_is_valid(page));
+	g_assert_false(called);
+
+	/* Making the group invalid makes the page invalid. */
+	called = FALSE;
+	test_request_page_make_group_invalid(group1);
+	g_assert_false(purple_request_page_is_valid(page));
+	g_assert_true(called);
+
+	/* Adding an invalid group keeps the page invalid. */
+	called = FALSE;
+	group2 = test_request_page_new_invalid_group("group2");
+	purple_request_page_add_group(page, group2);
+	g_assert_false(purple_request_page_is_valid(page));
+	g_assert_false(called);
+
+	/* Adding a valid group to an already invalid page does not change it to
+	 * valid accidentally. */
+	called = FALSE;
+	group3 = test_request_page_new_valid_group("group3");
+	purple_request_page_add_group(page, group3);
+	g_assert_false(purple_request_page_is_valid(page));
+	g_assert_false(called);
+
+	/* Making one group valid while others are still invalid keeps the group
+	 * invalid. */
+	called = FALSE;
+	test_request_page_make_group_valid(group1);
+	g_assert_false(purple_request_page_is_valid(page));
+	g_assert_false(called);
+
+	/* Making last invalid group valid makes the page valid again. */
+	called = FALSE;
+	test_request_page_make_group_valid(group2);
+	g_assert_true(purple_request_page_is_valid(page));
+	g_assert_true(called);
+
+	g_object_unref(page);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/request-page/valid", test_request_page_valid);
+
+	return g_test_run();
+}
diff --git a/pidgin/gtkrequest.c b/pidgin/gtkrequest.c
--- a/pidgin/gtkrequest.c
+++ b/pidgin/gtkrequest.c
@@ -235,7 +235,7 @@ req_field_changed_common(G_GNUC_UNUSED G
 
 	gtk_widget_set_sensitive(req_data->ok_button,
 		purple_request_page_all_required_filled(page) &&
-		purple_request_page_all_valid(page));
+		purple_request_page_is_valid(page));
 }
 
 static void
@@ -2208,7 +2208,7 @@ pidgin_request_fields(const char *title,
 		gtk_widget_set_sensitive(data->ok_button, FALSE);
 	}
 
-	if(!purple_request_page_all_valid(page)) {
+	if(!purple_request_page_is_valid(page)) {
 		gtk_widget_set_sensitive(data->ok_button, FALSE);
 	}
 
