# HG changeset patch
# Node ID f1d8ecfd9f43
# Parent  4659a7247aa7
diff --git a/libpurple/purpletags.c b/libpurple/purpletags.c
--- a/libpurple/purpletags.c
+++ b/libpurple/purpletags.c
@@ -24,6 +24,13 @@
 
 #include "util.h"
 
+enum {
+	SIG_ADDED,
+	SIG_REMOVED,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS];
+
 struct _PurpleTags {
 	GObject parent;
 
@@ -33,6 +40,45 @@
 G_DEFINE_TYPE(PurpleTags, purple_tags, G_TYPE_OBJECT)
 
 /******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_tags_real_add(PurpleTags *tags, const char *tag, const char *name,
+                     const char *value)
+{
+	/* Remove any existing tags with this value. */
+	purple_tags_remove(tags, tag);
+
+	/* Add the new tag. */
+	tags->tags = g_list_append(tags->tags, g_strdup(tag));
+
+	/* Finally emit the signal. */
+	g_signal_emit(tags, signals[SIG_ADDED], 0, tag, name, value);
+}
+
+static gboolean
+purple_tags_real_remove(PurpleTags *tags, const char *tag, const char *name,
+                        const char *value)
+{
+	/* Walk through the tags looking for the one that was passed in. */
+	for(GList *l = tags->tags; l != NULL; l = l->next) {
+		gchar *etag = l->data;
+
+		/* If we found it, remove it and exit early. */
+		if(purple_strequal(etag, tag)) {
+			g_free(etag);
+			tags->tags = g_list_delete_link(tags->tags, l);
+
+			g_signal_emit(tags, signals[SIG_REMOVED], 0, tag, name, value);
+
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 static void
@@ -53,6 +99,66 @@
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 
 	obj_class->dispose = purple_tags_dispose;
+
+	/**
+	 * PurpleTags::added:
+	 * @tags: The instance.
+	 * @tag: The tag value.
+	 * @name: The name of the tag.
+	 * @value: The value of the tag.
+	 *
+	 * Emitted when a tag is added. The tag as well as its name and value are
+	 * provided to be as flexible as possible.
+	 *
+	 * > NOTE: When a duplicate tag is added, the original one will be removed
+	 * > which will emit the [signal@Tags::removed] signal and then the new tag
+	 * > will be added which will emit [signal@Tags::added].
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_ADDED] = g_signal_new_class_handler(
+		"added",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		3,
+		G_TYPE_STRING,
+		G_TYPE_STRING,
+		G_TYPE_STRING);
+
+	/**
+	 * PurpleTags::removed:
+	 * @tags: The instance.
+	 * @tag: The tag value.
+	 * @name: The name of the tag.
+	 * @value: The value of the tag.
+	 *
+	 * Emitted when a tag is removed. The tag as well as its name and value are
+	 * provided to be as flexible as possible.
+	 *
+	 * > NOTE: When a duplicate tag is added, the original one will be removed
+	 * > which will emit the [signal@Tags::removed] signal and then the new tag
+	 * > will be added which will emit [signal@Tags::added].
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_REMOVED] = g_signal_new_class_handler(
+		"removed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		3,
+		G_TYPE_STRING,
+		G_TYPE_STRING,
+		G_TYPE_STRING);
 }
 
 /******************************************************************************
@@ -112,14 +218,16 @@
 
 void
 purple_tags_add(PurpleTags *tags, const gchar *tag) {
+	char *name = NULL;
+	char *value = NULL;
+
 	g_return_if_fail(PURPLE_IS_TAGS(tags));
 	g_return_if_fail(tag != NULL);
 
-	/* Remove any existing tags with this value. */
-	purple_tags_remove(tags, tag);
-
-	/* Add the new tag. */
-	tags->tags = g_list_append(tags->tags, g_strdup(tag));
+	purple_tag_parse(tag, &name, &value);
+	purple_tags_real_add(tags, tag, name, value);
+	g_clear_pointer(&name, g_free);
+	g_clear_pointer(&value, g_free);
 }
 
 void
@@ -137,45 +245,46 @@
 		tag = g_strdup(name);
 	}
 
-	purple_tags_add(tags, tag);
+	purple_tags_real_add(tags, tag, name, value);
 
 	g_free(tag);
 }
 
 gboolean
 purple_tags_remove(PurpleTags *tags, const gchar *tag) {
+	gchar *name = NULL;
+	gchar *value = NULL;
+	gboolean ret = FALSE;
+
 	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
 	g_return_val_if_fail(tag != NULL, FALSE);
 
-	for(GList *l = tags->tags; l != NULL; l = l->next) {
-		gchar *etag = l->data;
+	purple_tag_parse(tag, &name, &value);
+	ret = purple_tags_real_remove(tags, tag, name, value);
+	g_clear_pointer(&name, g_free);
+	g_clear_pointer(&value, g_free);
 
-		if(purple_strequal(etag, tag)) {
-			g_free(etag);
-			tags->tags = g_list_delete_link(tags->tags, l);
-
-			return TRUE;
-		}
-	}
-
-	return FALSE;
+	return ret;
 }
 
 gboolean
 purple_tags_remove_with_value(PurpleTags *tags, const char *name, const char *value) {
+	char *tag = NULL;
 	gboolean ret = FALSE;
 
 	g_return_val_if_fail(PURPLE_IS_TAGS(tags), FALSE);
 	g_return_val_if_fail(name != NULL, FALSE);
 
+	/* If there's no value, the tag and name are the same so we can avoid an
+	 * unnecessary allocation.
+	 */
 	if(value == NULL) {
-		ret = purple_tags_remove(tags, name);
-	} else {
-		char *tag = g_strdup_printf("%s:%s", name, value);
+		return purple_tags_real_remove(tags, name, name, NULL);
+	}
 
-		ret = purple_tags_remove(tags, tag);
-		g_free(tag);
-	}
+	tag = g_strdup_printf("%s:%s", name, value);
+	ret = purple_tags_real_remove(tags, tag, name, value);
+	g_free(tag);
 
 	return ret;
 }
diff --git a/libpurple/tests/test_tags.c b/libpurple/tests/test_tags.c
--- a/libpurple/tests/test_tags.c
+++ b/libpurple/tests/test_tags.c
@@ -25,26 +25,53 @@
 #include <purple.h>
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+test_purple_tags_counter_cb(G_GNUC_UNUSED PurpleTags *tags,
+                            G_GNUC_UNUSED const char *tag,
+                            G_GNUC_UNUSED const char *name,
+                            G_GNUC_UNUSED const char *value,
+                            gpointer data)
+{
+	guint *counter = data;
+
+	/* Increment the counter so we know we were called. */
+	*counter = *counter + 1;
+}
+
+/******************************************************************************
  * Tests
  *****************************************************************************/
 static void
 test_purple_tags_lookup_exists(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
 	gboolean found = FALSE;
 	const gchar *value = NULL;
+	guint counter = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &counter);
 
 	purple_tags_add(tags, "foo");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+	g_assert_cmpuint(counter, ==, 1);
 	value = purple_tags_lookup(tags, "foo", &found);
 	g_assert_null(value);
 	g_assert_true(found);
 
 	purple_tags_add(tags, "bar:baz");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 2);
+	g_assert_cmpuint(counter, ==, 2);
 	value = purple_tags_lookup(tags, "bar", &found);
 	g_assert_cmpstr(value, ==, "baz");
 	g_assert_true(found);
 
 	/* make sure that a name of pur doesn't match a tag of purple */
 	purple_tags_add(tags, "purple");
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 3);
+	g_assert_cmpuint(counter, ==, 3);
 	value = purple_tags_lookup(tags, "pur", &found);
 	g_assert_null(value);
 	g_assert_false(found);
@@ -67,12 +94,22 @@
 
 static void
 test_purple_tags_add_remove_bare(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
+	guint added = 0;
+	guint removed = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &added);
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &removed);
 
 	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(added, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	purple_tags_remove(tags, "tag1");
+	g_assert_cmpuint(removed, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
 
 	g_clear_object(&tags);
@@ -80,12 +117,24 @@
 
 static void
 test_purple_tags_add_duplicate_bare(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
+	guint added = 0;
+	guint removed = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &added);
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &removed);
 
 	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(added, ==, 1);
+	g_assert_cmpuint(removed, ==, 0);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	purple_tags_add(tags, "tag1");
+	g_assert_cmpuint(added, ==, 2);
+	g_assert_cmpuint(removed, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	g_clear_object(&tags);
@@ -93,10 +142,16 @@
 
 static void
 test_purple_tags_remove_non_existent_bare(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
 	gboolean ret = FALSE;
+	guint counter = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &counter);
 
 	ret = purple_tags_remove(tags, "tag1");
+	g_assert_cmpuint(counter, ==, 0);
 	g_assert_false(ret);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
 
@@ -105,13 +160,23 @@
 
 static void
 test_purple_tags_add_remove(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
 	gboolean ret = FALSE;
+	guint added = 0;
+	guint removed = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &added);
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &removed);
 
 	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(added, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	ret = purple_tags_remove(tags, "tag1:purple");
+	g_assert_cmpuint(removed, ==, 1);
 	g_assert_true(ret);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
 
@@ -120,13 +185,25 @@
 
 static void
 test_purple_tags_add_remove_with_null_value(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
 	gboolean ret = FALSE;
+	guint added = 0;
+	guint removed = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &added);
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &removed);
 
 	purple_tags_add_with_value(tags, "tag1", NULL);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
+	g_assert_cmpuint(added, ==, 1);
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	ret = purple_tags_remove_with_value(tags, "tag1", NULL);
+	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
+	g_assert_cmpuint(removed, ==, 1);
 	g_assert_true(ret);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
 
@@ -135,13 +212,23 @@
 
 static void
 test_purple_tags_add_remove_with_value(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
 	gboolean ret = FALSE;
+	guint added = 0;
+	guint removed = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &added);
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &removed);
 
 	purple_tags_add_with_value(tags, "tag1", "purple");
+	g_assert_cmpuint(added, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	ret = purple_tags_remove_with_value(tags, "tag1", "purple");
+	g_assert_cmpuint(removed, ==, 1);
 	g_assert_true(ret);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
 
@@ -150,12 +237,24 @@
 
 static void
 test_purple_tags_add_duplicate_with_value(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
+	guint added = 0;
+	guint removed = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &added);
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &removed);
 
 	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(added, ==, 1);
+	g_assert_cmpuint(removed, ==, 0);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	purple_tags_add(tags, "tag1:purple");
+	g_assert_cmpuint(added, ==, 2);
+	g_assert_cmpuint(removed, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	g_clear_object(&tags);
@@ -163,11 +262,17 @@
 
 static void
 test_purple_tags_add_with_value(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
 	const char *value = NULL;
 	gboolean found = FALSE;
+	guint counter = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &counter);
 
 	purple_tags_add_with_value(tags, "tag1", "purple");
+	g_assert_cmpuint(counter, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	value = purple_tags_lookup(tags, "tag1", &found);
@@ -179,11 +284,17 @@
 
 static void
 test_purple_tags_add_with_value_null(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
 	const char *value = NULL;
 	gboolean found = FALSE;
+	guint counter = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "added", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &counter);
 
 	purple_tags_add_with_value(tags, "tag1", NULL);
+	g_assert_cmpuint(counter, ==, 1);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 1);
 
 	value = purple_tags_lookup(tags, "tag1", &found);
@@ -195,9 +306,15 @@
 
 static void
 test_purple_tags_remove_non_existent_with_value(void) {
-	PurpleTags *tags = purple_tags_new();
+	PurpleTags *tags = NULL;
+	guint counter = 0;
+
+	tags = purple_tags_new();
+	g_signal_connect(tags, "removed", G_CALLBACK(test_purple_tags_counter_cb),
+	                 &counter);
 
 	purple_tags_remove(tags, "tag1:purple");
+	g_assert_cmpuint(counter, ==, 0);
 	g_assert_cmpuint(purple_tags_get_count(tags), ==, 0);
 
 	g_clear_object(&tags);
