diff --git a/libpurple/purplerequestfield.h b/libpurple/purplerequestfield.h
--- a/libpurple/purplerequestfield.h
+++ b/libpurple/purplerequestfield.h
@@ -53,6 +53,7 @@ struct _PurpleRequestFieldClass {
 	GObjectClass parent_class;
 
 	/*< public >*/
+	gboolean (*is_filled)(PurpleRequestField *field);
 
 	/*< private >*/
 	gpointer reserved[4];
@@ -193,7 +194,11 @@ gboolean purple_request_field_is_require
  * purple_request_field_is_filled:
  * @field: The field.
  *
- * Checks, if specified field has value.
+ * Returns whether the field is currently filled.
+ *
+ * Note: For subclassers, if this is not overridden, then the field is assumed
+ * to always be filled. If the filled status changes, then subclasses should
+ * notify on [property@RequestField:filled].
  *
  * Returns: TRUE if the field has value, or FALSE.
  */
# HG changeset patch
# Node ID 25a2452792c1
# Parent  4b7432db2ebd
diff --git a/libpurple/purplerequestfield.c b/libpurple/purplerequestfield.c
--- a/libpurple/purplerequestfield.c
+++ b/libpurple/purplerequestfield.c
@@ -21,7 +21,6 @@
 #include <glib/gi18n-lib.h>
 
 #include "request.h"
-#include "request/purplerequestfieldstring.h"
 #include "purpleprivate.h"
 
 typedef struct {
@@ -50,6 +49,7 @@ enum {
 	PROP_TYPE_HINT,
 	PROP_TOOLTIP,
 	PROP_REQUIRED,
+	PROP_FILLED,
 	PROP_IS_VALIDATABLE,
 	N_PROPERTIES,
 };
@@ -107,6 +107,10 @@ purple_request_field_get_property(GObjec
 			g_value_set_boolean(value,
 			                    purple_request_field_is_required(field));
 			break;
+		case PROP_FILLED:
+			g_value_set_boolean(value,
+			                    purple_request_field_is_filled(field));
+			break;
 		case PROP_IS_VALIDATABLE:
 			g_value_set_boolean(value,
 			                    purple_request_field_is_validatable(field));
@@ -280,6 +284,19 @@ purple_request_field_class_init(PurpleRe
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
 	/**
+	 * PurpleRequestField:filled:
+	 *
+	 * Whether the field has been filled.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_FILLED] = g_param_spec_boolean(
+		"filled", "filled",
+		"Whether the field has been filled.",
+		TRUE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
 	 * PurpleRequestField:is-validatable:
 	 *
 	 * Whether the field can be validated by the requestor.
@@ -473,14 +490,17 @@ purple_request_field_is_required(PurpleR
 
 gboolean
 purple_request_field_is_filled(PurpleRequestField *field) {
+	PurpleRequestFieldClass *klass = NULL;
+	gboolean filled = TRUE;
+
 	g_return_val_if_fail(PURPLE_IS_REQUEST_FIELD(field), FALSE);
 
-	if(PURPLE_IS_REQUEST_FIELD_STRING(field)) {
-		PurpleRequestFieldString *sfield = PURPLE_REQUEST_FIELD_STRING(field);
-		return !purple_strempty(purple_request_field_string_get_value(sfield));
+	klass = PURPLE_REQUEST_FIELD_GET_CLASS(field);
+	if(klass != NULL && klass->is_filled != NULL) {
+		filled = klass->is_filled(field);
 	}
 
-	return TRUE;
+	return filled;
 }
 
 void
diff --git a/libpurple/request/purplerequestfieldstring.c b/libpurple/request/purplerequestfieldstring.c
--- a/libpurple/request/purplerequestfieldstring.c
+++ b/libpurple/request/purplerequestfieldstring.c
@@ -57,6 +57,16 @@ purple_request_field_string_set_multilin
 }
 
 /******************************************************************************
+ * PurpleRequestField Implementation
+ *****************************************************************************/
+static gboolean
+purple_request_field_string_is_filled(PurpleRequestField *field) {
+	PurpleRequestFieldString *strfield = PURPLE_REQUEST_FIELD_STRING(field);
+
+	return !purple_strempty(strfield->value);
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 G_DEFINE_TYPE(PurpleRequestFieldString, purple_request_field_string,
@@ -138,8 +148,11 @@ purple_request_field_string_init(G_GNUC_
 
 static void
 purple_request_field_string_class_init(PurpleRequestFieldStringClass *klass) {
+	PurpleRequestFieldClass *field_class = PURPLE_REQUEST_FIELD_CLASS(klass);
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 
+	field_class->is_filled = purple_request_field_string_is_filled;
+
 	obj_class->finalize = purple_request_field_string_finalize;
 	obj_class->get_property = purple_request_field_string_get_property;
 	obj_class->set_property = purple_request_field_string_set_property;
@@ -237,14 +250,25 @@ void
 purple_request_field_string_set_value(PurpleRequestFieldString *field,
                                       const char *value)
 {
+	gboolean before, after;
+
 	g_return_if_fail(PURPLE_IS_REQUEST_FIELD_STRING(field));
 
-	if(!purple_strequal(field->value, value)) {
-		g_free(field->value);
-		field->value = g_strdup(value);
+	if(purple_strequal(field->value, value)) {
+		return;
+	}
 
-		g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALUE]);
+	before = purple_request_field_string_is_filled(PURPLE_REQUEST_FIELD(field));
+	g_free(field->value);
+	field->value = g_strdup(value);
+	after = purple_request_field_string_is_filled(PURPLE_REQUEST_FIELD(field));
+
+	g_object_freeze_notify(G_OBJECT(field));
+	g_object_notify_by_pspec(G_OBJECT(field), properties[PROP_VALUE]);
+	if(before != after) {
+		g_object_notify(G_OBJECT(field), "filled");
 	}
+	g_object_thaw_notify(G_OBJECT(field));
 }
 
 void
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -26,6 +26,7 @@ PROGS = [
     'protocol_xfer',
     'purplepath',
     'queued_output_stream',
+    'request_field',
     'str',
     'tags',
     'util',
diff --git a/libpurple/tests/test_request_field.c b/libpurple/tests/test_request_field.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_request_field.c
@@ -0,0 +1,129 @@
+/*
+ * 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_field_notify_filled_cb(G_GNUC_UNUSED GObject *obj,
+                                    G_GNUC_UNUSED GParamSpec *pspec,
+                                    gpointer data)
+{
+	gboolean *called = data;
+
+	*called = TRUE;
+}
+
+static void
+test_request_field_filled_string(void) {
+	PurpleRequestField *field = NULL;
+	gboolean called = FALSE;
+
+	field = purple_request_field_string_new("test-string", "Test string", NULL,
+	                                        FALSE);
+	g_signal_connect(field, "notify::filled",
+	                 G_CALLBACK(test_request_field_notify_filled_cb), &called);
+	g_assert_false(purple_request_field_is_filled(field));
+
+	/* Passing same value should not trigger. */
+	called = FALSE;
+	purple_request_field_string_set_value(PURPLE_REQUEST_FIELD_STRING(field),
+	                                      NULL);
+	g_assert_false(called);
+	g_assert_false(purple_request_field_is_filled(field));
+
+	/* Passing an empty string should not trigger, as NULL and "" are
+	 * considered the same. */
+	called = FALSE;
+	purple_request_field_string_set_value(PURPLE_REQUEST_FIELD_STRING(field),
+	                                      "");
+	g_assert_false(called);
+	g_assert_false(purple_request_field_is_filled(field));
+
+	/* Now that there's a change from empty to filled, notify should occur. */
+	called = FALSE;
+	purple_request_field_string_set_value(PURPLE_REQUEST_FIELD_STRING(field),
+	                                      "text");
+	g_assert_true(called);
+	g_assert_true(purple_request_field_is_filled(field));
+
+	/* Passing same value should not trigger. */
+	called = FALSE;
+	purple_request_field_string_set_value(PURPLE_REQUEST_FIELD_STRING(field),
+	                                      "text");
+	g_assert_false(called);
+	g_assert_true(purple_request_field_is_filled(field));
+
+	/* And then going back to empty should notify. */
+	called = FALSE;
+	purple_request_field_string_set_value(PURPLE_REQUEST_FIELD_STRING(field),
+	                                      "");
+	g_assert_true(called);
+	g_assert_false(purple_request_field_is_filled(field));
+
+	g_object_unref(field);
+}
+
+static void
+test_request_field_filled_nonstring(void) {
+	/* Anything that's not a string should always be considered filled and
+	 * never notify. */
+	PurpleRequestField *field = NULL;
+	gboolean called = FALSE;
+
+	field = purple_request_field_int_new("test-int", "Test int", 50, 0, 100);
+	g_signal_connect(field, "notify::filled",
+	                 G_CALLBACK(test_request_field_notify_filled_cb), &called);
+	g_assert_true(purple_request_field_is_filled(field));
+
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field), 50);
+	g_assert_false(called);
+	g_assert_true(purple_request_field_is_filled(field));
+
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field), 0);
+	g_assert_false(called);
+	g_assert_true(purple_request_field_is_filled(field));
+
+	called = FALSE;
+	purple_request_field_int_set_value(PURPLE_REQUEST_FIELD_INT(field), 100);
+	g_assert_false(called);
+	g_assert_true(purple_request_field_is_filled(field));
+
+	g_object_unref(field);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_add_func("/request-field/filled-string",
+	                test_request_field_filled_string);
+	g_test_add_func("/request-field/filled-nonstring",
+	                test_request_field_filled_nonstring);
+
+	return g_test_run();
+}
