changeset 3385:d1b43bf0e67d

merge
author Cristi Magherusan <majeru@atheme-project.org>
date Thu, 09 Aug 2007 16:28:42 +0300
parents 7ac9c5c6b44e (current diff) a97fb19a0148 (diff)
children 3e9bc5fd5c36
files src/audacious/Makefile
diffstat 21 files changed, 1165 insertions(+), 131 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Wed Aug 08 22:04:41 2007 +0300
+++ b/.hgtags	Thu Aug 09 16:28:42 2007 +0300
@@ -1,2 +1,3 @@
 55a4a6da92a5e5bc68e352e47e0c9259818a1f92 audacious-1.4.0-dr1
 40b4b64dfb42c81a6677532097c41037026b0b86 audacious-1.4.0-DR1
+91a5f34b07803a8bae1e162446e40c08ddf33273 audacious-1.4.0-dr2
--- a/src/audacious/Makefile	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/Makefile	Thu Aug 09 16:28:42 2007 +0300
@@ -53,6 +53,7 @@
 	plugin.h \
 	strings.h \
 	titlestring.h \
+	tuple.h \
 	ui_fileinfopopup.h \
 	ui_lastfm.h\
 	ui_preferences.h \
@@ -97,6 +98,8 @@
 	signals.c \
 	strings.c \
 	titlestring.c \
+	tuple.c \
+	tuple_formatter.c \
 	skin.c \
 	ui_about.c \
 	ui_albumart.c \
@@ -145,7 +148,7 @@
 CFLAGS += -I../libaudclient
 DBUS_BINDINGS = dbus-server-bindings.h dbus-client-bindings.h
 OBJECTIVE_LIBS_NOINST += $(DBUS_BINDINGS)
-LIBDEP += ../libaudclient/libaudclient.so
+LIBDEP += ../libaudclient/libaudclient$(SHARED_SUFFIX)
 LDADD += -L../libaudclient -laudclient
 endif
 
--- a/src/audacious/dbus-service.h	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/dbus-service.h	Thu Aug 09 16:28:42 2007 +0300
@@ -28,6 +28,7 @@
 
 typedef struct {
     GObject parent;
+	DBusGProxy *proxy;
 } RemoteObject, MprisRoot, MprisPlayer, MprisTrackList;
 
 typedef struct {
@@ -50,10 +51,13 @@
 gboolean mpris_player_pause(MprisPlayer *obj, GError **error);
 gboolean mpris_player_stop(MprisPlayer *obj, GError **error);
 gboolean mpris_player_play(MprisPlayer *obj, GError **error);
+gboolean mpris_player_repeat(MprisPlayer *obj, gboolean rpt, GError **error);
 gboolean mpris_player_quit(MprisPlayer *obj, GError **error);
-gboolean mpris_player_repeat(MprisPlayer *obj, gboolean rpt, GError **error);
+gboolean mpris_player_disconnect(MprisPlayer *obj, GError **error);
 gboolean mpris_player_get_status(MprisPlayer *obj, gint *status,
                                  GError **error);
+gboolean mpris_player_get_metadata(MprisTrackList *obj, gint pos,
+                                   GHashTable *metadata, GError **error);
 gboolean mpris_player_get_caps(MprisPlayer *obj, gint *capabilities,
                                  GError **error);
 gboolean mpris_player_volume_set(MprisPlayer *obj, gint vol, GError **error);
@@ -63,21 +67,23 @@
 gboolean mpris_player_position_get(MprisPlayer *obj, gint *pos,
                                    GError **error);
 enum {
-    CAPS_CHANGE_SIG,
     TRACK_CHANGE_SIG,
     STATUS_CHANGE_SIG,
+    CAPS_CHANGE_SIG,
+    DISCONNECTED,
     LAST_SIG
 };
-gboolean mpris_player_emit_caps_change(MprisPlayer *obj, GError **error);
 gboolean mpris_player_emit_track_change(MprisPlayer *obj, GError **error);
 gboolean mpris_player_emit_status_change(MprisPlayer *obj, GError **error);
+gboolean mpris_player_emit_caps_change(MprisPlayer *obj, GError **error);
+gboolean mpris_player_emit_disconnected(MprisPlayer *obj, GError **error);
 
 // MPRIS /TrackList
 gboolean mpris_tracklist_get_metadata(MprisTrackList *obj, gint pos,
                                       GHashTable *metadata, GError **error);
 gboolean mpris_tracklist_get_current_track(MprisTrackList *obj, gint *pos,
                                            GError **error);
-gboolean mpris_tracklist_get_length(MprisTrackList *obj, gint *pos,
+gboolean mpris_tracklist_get_length(MprisTrackList *obj, gint *length,
                                     GError **error);
 gboolean mpris_tracklist_add_track(MprisTrackList *obj, gchar *uri,
                                    gboolean play, GError **error);
--- a/src/audacious/dbus.c	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/dbus.c	Thu Aug 09 16:28:42 2007 +0300
@@ -175,32 +175,54 @@
 
 // MPRIS /Player
 gboolean mpris_player_next(MprisPlayer *obj, GError **error) {
-    return audacious_rc_advance(obj, error);
+	playlist_next(playlist_get_active());
+    return TRUE;
 }
 gboolean mpris_player_prev(MprisPlayer *obj, GError **error) {
-    return audacious_rc_reverse(obj, error);
+	playlist_prev(playlist_get_active());
+    return TRUE;
 }
 gboolean mpris_player_pause(MprisPlayer *obj, GError **error) {
-    return audacious_rc_pause(obj, error);
+	playback_pause();
+    return TRUE;
 }
 gboolean mpris_player_stop(MprisPlayer *obj, GError **error) {
-    return audacious_rc_stop(obj, error);
+	ip_data.stop = TRUE;
+    playback_stop();
+    ip_data.stop = FALSE;
+    mainwin_clear_song_info();
+    return TRUE;
 }
 gboolean mpris_player_play(MprisPlayer *obj, GError **error) {
-    return audacious_rc_play(obj, error);
-}
-gboolean mpris_player_quit(MprisPlayer *obj, GError **error) {
-    return audacious_rc_quit(obj, error);
+	if (playback_get_paused())
+        playback_pause();
+    else if (playlist_get_length(playlist_get_active()))
+        playback_initiate();
+    else
+        mainwin_eject_pushed();
+    return TRUE;
 }
 gboolean mpris_player_repeat(MprisPlayer *obj, gboolean rpt, GError **error) {
     mainwin_repeat_pushed(rpt);
     mainwin_set_noplaylistadvance(rpt);
     return TRUE;
 }
+gboolean mpris_player_quit(MprisPlayer *obj, GError **error) {
+	// TODO: emit disconnected signal
+	mainwin_quit_cb();
+    return TRUE;
+}
+gboolean mpris_player_disconnect(MprisPlayer *obj, GError **error) {
+	return FALSE;
+}
 gboolean mpris_player_get_status(MprisPlayer *obj, gint *status,
                                  GError **error) {
     return FALSE;
 }
+gboolean mpris_player_get_metadata(MprisTrackList *obj, gint pos,
+                                   GHashTable *metadata, GError **error) {
+	return FALSE;
+}
 gboolean mpris_player_get_caps(MprisPlayer *obj, gint *capabilities,
                                  GError **error) {
     return FALSE;
@@ -236,6 +258,11 @@
     return TRUE;
 }
 
+gboolean mpris_player_emit_disconnected(MprisPlayer *obj, GError **error) {
+	g_signal_emit(obj, signals[DISCONNECTED], 0, NULL);
+	return TRUE;
+}
+
 // MPRIS /TrackList
 gboolean mpris_tracklist_get_metadata(MprisTrackList *obj, gint pos,
                                       GHashTable *metadata, GError **error) {
@@ -243,11 +270,13 @@
 }
 gboolean mpris_tracklist_get_current_track(MprisTrackList *obj, gint *pos,
                                            GError **error) {
-    return audacious_rc_position(obj, pos, error);
+	*pos = playlist_get_position(playlist_get_active());
+    return TRUE;
 }
-gboolean mpris_tracklist_get_length(MprisTrackList *obj, gint *pos,
+gboolean mpris_tracklist_get_length(MprisTrackList *obj, gint *length,
                                     GError **error) {
-    return FALSE;
+	*length = playlist_get_length(playlist_get_active());
+    return TRUE;
 }
 gboolean mpris_tracklist_add_track(MprisTrackList *obj, gchar *uri,
                                    gboolean play, GError **error) {
@@ -261,7 +290,8 @@
 }
 gboolean mpris_tracklist_del_track(MprisTrackList *obj, gint pos,
                                    GError **error) {
-    return FALSE;
+	playlist_delete_index(playlist_get_active(), pos);
+    return TRUE;
 }
 gboolean mpris_tracklist_loop(MprisTrackList *obj, gboolean loop,
                               GError **error) {
@@ -269,7 +299,8 @@
 }
 gboolean mpris_tracklist_random(MprisTrackList *obj, gboolean random,
                                 GError **error) {
-    return FALSE;
+	mainwin_shuffle_pushed(!cfg.shuffle);
+    return TRUE;
 }
 
 // Audacious General Information
--- a/src/audacious/dbus.h	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/dbus.h	Thu Aug 09 16:28:42 2007 +0300
@@ -25,8 +25,20 @@
 #define AUDACIOUS_DBUS_PATH         "/org/atheme/audacious"
 #define AUDACIOUS_DBUS_INTERFACE    "org.atheme.audacious"
 #define AUDACIOUS_DBUS_SERVICE_MPRIS    "org.mpris.audacious"
+#define AUDACIOUS_DBUS_INTERFACE_MPRIS 	"org.freedesktop.MediaPlayer"
 #define AUDACIOUS_DBUS_PATH_MPRIS_ROOT      "/"
 #define AUDACIOUS_DBUS_PATH_MPRIS_PLAYER    "/Player"
 #define AUDACIOUS_DBUS_PATH_MPRIS_TRACKLIST "/TrackList"
 
+#define NONE                  = 0
+#define CAN_GO_NEXT           = 1 << 0
+#define CAN_GO_PREV           = 1 << 1
+#define CAN_PAUSE             = 1 << 2
+#define CAN_PLAY              = 1 << 3
+#define CAN_SEEK              = 1 << 4
+#define CAN_RESTORE_CONTEXT   = 1 << 5
+#define CAN_PROVIDE_METADATA  = 1 << 6
+#define PROVIDES_TIMING       = 1 << 7
+
+
 #endif // !_AUDDBUS_H
--- a/src/audacious/mpris_player.xml	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/mpris_player.xml	Thu Aug 09 16:28:42 2007 +0300
@@ -3,6 +3,7 @@
 <!--
  - Audacious: A cross-platform multimedia player
  - Copyright (c) 2007 William Pitcock
+ - Copyright (c) 2007 Ben Tucker
  -
  - This program is free software; you can redistribute it and/or modify
  - it under the terms of the GNU General Public License as published by
@@ -34,51 +35,49 @@
         <method name="Play">
             <annotation name="org.freedesktop.DBus.GLib.NoReply" value=""/>
         </method>
-        <method name="Quit">
-            <annotation name="org.freedesktop.DBus.GLib.NoReply" value=""/>
-        </method>
-
         <method name="Repeat">
             <annotation name="org.freedesktop.DBus.GLib.NoReply" value=""/>
             <arg type="b" direction="in" />
         </method>
-
+        <method name="Quit">
+            <annotation name="org.freedesktop.DBus.GLib.NoReply" value=""/>
+        </method>
+        <method name="Disconnect">
+            <annotation name="org.freedesktop.DBus.GLib.NoReply" value=""/>
+        </method>
         <method name="GetStatus">
             <arg type="i" direction="out" />
         </method>
-
+        <method name="GetMetadata">
+            <arg type="a{sv}" direction="out" />
+        </method>
         <method name="GetCaps">
             <arg type="i" direction="out" />
         </method>
-
         <method name="VolumeSet">
             <annotation name="org.freedesktop.DBus.GLib.NoReply" value=""/>
             <arg type="i" direction="in" />
         </method>
-
         <method name="VolumeGet">
             <arg type="i" direction="out" />
         </method>
-
         <method name="PositionSet">
             <annotation name="org.freedesktop.DBus.GLib.NoReply" value=""/>
             <arg type="i" direction="in" />
         </method>
-
         <method name="PositionGet">
             <arg type="i" direction="out" />
         </method>
 
-        <signal name="CapsChange">
-            <arg type="i" />
-        </signal>
-
         <signal name="TrackChange">
             <arg type="a{sv}" />
         </signal>
-
         <signal name="StatusChange">
             <arg type="i" />
         </signal>
+        <signal name="CapsChange">
+            <arg type="i" />
+        </signal>
+		<signal name="Disconnected" />
     </interface>
 </node>
--- a/src/audacious/mpris_tracklist.xml	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/mpris_tracklist.xml	Thu Aug 09 16:28:42 2007 +0300
@@ -3,6 +3,7 @@
 <!--
  - Audacious: A cross-platform multimedia player
  - Copyright (c) 2007 William Pitcock
+ - Copyright (c) 2007 Ben Tucker
  -
  - This program is free software; you can redistribute it and/or modify
  - it under the terms of the GNU General Public License as published by
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/audacious/tuple.c	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,191 @@
+/*
+ * Audacious
+ * Copyright (c) 2006-2007 Audacious team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 3 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses>.
+ *
+ * The Audacious team does not consider modular code linking to
+ * Audacious or using our public API to be a derived work.
+ */
+
+#include <glib.h>
+#include <mowgli.h>
+
+#include "tuple.h"
+
+struct _Tuple {
+    mowgli_object_t parent;
+    mowgli_dictionary_t *dict;
+};
+
+typedef struct {
+    TupleValueType type;
+    union {
+        gchar *string;
+        gint integer;
+    } value;
+} TupleValue;
+
+static mowgli_heap_t *tuple_heap = NULL;
+static mowgli_heap_t *tuple_value_heap = NULL;
+static mowgli_object_class_t tuple_klass;
+
+/* iterative destructor of tuple values. */
+static void
+tuple_value_destroy(mowgli_dictionary_elem_t *delem, gpointer privdata)
+{
+    TupleValue *value = (TupleValue *) delem->data;
+
+    if (value->type == TUPLE_STRING)
+        g_free(value->value.string);
+
+    mowgli_heap_free(tuple_value_heap, value);
+}
+
+static void
+tuple_destroy(gpointer data)
+{
+    Tuple *tuple = (Tuple *) data;
+
+    mowgli_dictionary_destroy(tuple->dict, tuple_value_destroy, NULL);
+    mowgli_heap_free(tuple_heap, tuple);
+}
+
+Tuple *
+tuple_new(void)
+{
+    Tuple *tuple;
+
+    if (tuple_heap == NULL)
+    {
+        tuple_heap = mowgli_heap_create(sizeof(Tuple), 256, BH_NOW);
+        tuple_value_heap = mowgli_heap_create(sizeof(TupleValue), 512, BH_NOW);
+        mowgli_object_class_init(&tuple_klass, "audacious.tuple", tuple_destroy, FALSE);
+    }
+
+    /* FIXME: use mowgli_object_bless_from_class() in mowgli 0.4
+       when it is released --nenolod */
+    tuple = mowgli_heap_alloc(tuple_heap);
+    mowgli_object_init(mowgli_object(tuple), NULL, &tuple_klass, NULL);
+
+    tuple->dict = mowgli_dictionary_create(g_ascii_strcasecmp);
+
+    return tuple;
+}
+
+gboolean
+tuple_associate_string(Tuple *tuple, const gchar *field, const gchar *string)
+{
+    TupleValue *value;
+
+    g_return_val_if_fail(tuple != NULL, FALSE);
+    g_return_val_if_fail(field != NULL, FALSE);
+    g_return_val_if_fail(string != NULL, FALSE);
+
+    if (mowgli_dictionary_find(tuple->dict, field))
+        tuple_disassociate(tuple, field);
+
+    value = mowgli_heap_alloc(tuple_value_heap);
+    value->type = TUPLE_STRING;
+    value->value.string = g_strdup(string);
+
+    mowgli_dictionary_add(tuple->dict, field, value);
+
+    return TRUE;
+}
+
+gboolean
+tuple_associate_int(Tuple *tuple, const gchar *field, gint integer)
+{
+    TupleValue *value;
+
+    g_return_val_if_fail(tuple != NULL, FALSE);
+    g_return_val_if_fail(field != NULL, FALSE);
+
+    if (mowgli_dictionary_find(tuple->dict, field))
+        tuple_disassociate(tuple, field);
+
+    value = mowgli_heap_alloc(tuple_value_heap);
+    value->type = TUPLE_INT;
+    value->value.integer = integer;
+
+    mowgli_dictionary_add(tuple->dict, field, value);
+
+    return TRUE;
+}
+
+void
+tuple_disassociate(Tuple *tuple, const gchar *field)
+{
+    TupleValue *value;
+
+    g_return_if_fail(tuple != NULL);
+    g_return_if_fail(field != NULL);
+
+    /* why _delete()? because _delete() returns the dictnode's data on success */
+    if ((value = mowgli_dictionary_delete(tuple->dict, field)) == NULL)
+        return;
+
+    if (value->type == TUPLE_STRING)
+        g_free(value->value.string);
+
+    mowgli_heap_free(tuple_value_heap, value);
+}
+
+TupleValueType
+tuple_get_value_type(Tuple *tuple, const gchar *field)
+{
+    TupleValue *value;
+
+    g_return_val_if_fail(tuple != NULL, TUPLE_UNKNOWN);
+    g_return_val_if_fail(field != NULL, TUPLE_UNKNOWN);
+
+    if ((value = mowgli_dictionary_retrieve(tuple->dict, field)) == NULL)
+        return TUPLE_UNKNOWN;
+
+    return value->type;
+}
+
+const gchar *
+tuple_get_string(Tuple *tuple, const gchar *field)
+{
+    TupleValue *value;
+
+    g_return_val_if_fail(tuple != NULL, NULL);
+    g_return_val_if_fail(field != NULL, NULL);
+
+    if ((value = mowgli_dictionary_retrieve(tuple->dict, field)) == NULL)
+        return NULL;
+
+    if (value->type != TUPLE_STRING)
+        mowgli_throw_exception_val(audacious.tuple.invalid_type_request, NULL);
+
+    return value->value.string;
+}
+
+int
+tuple_get_int(Tuple *tuple, const gchar *field)
+{
+    TupleValue *value;
+
+    g_return_val_if_fail(tuple != NULL, 0);
+    g_return_val_if_fail(field != NULL, 0);
+
+    if ((value = mowgli_dictionary_retrieve(tuple->dict, field)) == NULL)
+        return 0;
+
+    if (value->type != TUPLE_INT)
+        mowgli_throw_exception_val(audacious.tuple.invalid_type_request, 0);
+
+    return value->value.integer;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/audacious/tuple.h	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,44 @@
+/*
+ * Audacious
+ * Copyright (c) 2006-2007 Audacious team
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 3 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses>.
+ *
+ * The Audacious team does not consider modular code linking to
+ * Audacious or using our public API to be a derived work.
+ */
+
+#ifndef __AUDACIOUS_TUPLE_H__
+#define __AUDACIOUS_TUPLE_H__
+
+#include <glib.h>
+#include <mowgli.h>
+
+struct _Tuple;
+typedef struct _Tuple Tuple;
+
+typedef enum {
+    TUPLE_STRING,
+    TUPLE_INT,
+    TUPLE_UNKNOWN
+} TupleValueType;
+
+Tuple *tuple_new(void);
+gboolean tuple_associate_string(Tuple *tuple, const gchar *field, const gchar *string);
+gboolean tuple_associate_int(Tuple *tuple, const gchar *field, gint integer);
+void tuple_disassociate(Tuple *tuple, const gchar *field);
+TupleValueType tuple_get_value_type(Tuple *tuple, const gchar *field);
+const gchar *tuple_get_string(Tuple *tuple, const gchar *field);
+int tuple_get_int(Tuple *tuple, const gchar *field);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/audacious/tuple_formatter.c	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,446 @@
+/*
+ * Audacious
+ * Copyright (c) 2007 William Pitcock
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 3 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses>.
+ *
+ * The Audacious team does not consider modular code linking to
+ * Audacious or using our public API to be a derived work.
+ */
+
+#include <glib.h>
+#include <mowgli.h>
+
+#include "config.h"
+#include "tuple.h"
+#include "tuple_formatter.h"
+
+/*
+ * the tuple formatter:
+ *
+ * this is a data-driven meta-language which eventually hopes to be
+ * turing complete.
+ *
+ * language constructs follow the following basic rules:
+ *   - begin with ${
+ *   - end with }
+ *
+ * language constructs:
+ *   - ${field}: prints a field
+ *   - ${?field:expr}: evaluates expr if field exists
+ *   - ${=field,"value"}: defines field in the currently iterated
+ *                        tuple as string value of "value"
+ *   - ${=field,value}: defines field in the currently iterated
+ *                      tuple as integer value of "value"
+ *   - ${==field,field:expr}: evaluates expr if both fields are the same
+ *   - ${!=field,field:expr}: evaluates expr if both fields are not the same
+ *   - ${(empty)?field:expr}: evaluates expr if field is empty or does not exist
+ *   - %{function:args,arg2,...}: runs function and inserts the result.
+ *
+ * everything else is treated as raw text.
+ * additionally, plugins can add additional instructions and functions!
+ */
+
+typedef struct {
+    Tuple *tuple;
+    GString *str;
+} TupleFormatterContext;
+
+/* processes a construct, e.g. "${?artist:artist is defined}" would
+   return "artist is defined" if artist is defined. */
+gchar *
+tuple_formatter_process_construct(Tuple *tuple, const gchar *string)
+{
+    TupleFormatterContext *ctx;
+    const gchar *iter;
+    gchar *out;
+
+    g_return_val_if_fail(tuple != NULL, NULL);
+    g_return_val_if_fail(string != NULL, NULL);
+
+    ctx = g_new0(TupleFormatterContext, 1);
+    ctx->str = g_string_new("");
+
+    /* parsers are ugly */
+    for (iter = string; *iter != '\0'; iter++)
+    {
+        /* if it's raw text, just copy the byte */
+        if (*iter != '$' && *iter != '%')
+            g_string_append_c(ctx->str, *iter);
+        else if (*iter == '$' && *(iter + 1) == '{')
+        {
+            GString *expression = g_string_new("");
+            GString *argument = g_string_new("");
+            GString *sel = expression;
+            gchar *result;
+            gint level = 0;
+
+            for (iter += 2; *iter != '\0'; iter++)
+            {
+                if (*iter == ':')
+                {
+                    sel = argument;
+                    continue;
+                }
+
+                if (g_str_has_prefix(iter, "${") == TRUE || g_str_has_prefix(iter, "%{") == TRUE)
+                {
+                    if (sel == argument)
+                    {
+                        g_string_append_c(sel, *iter);
+                        level++;
+                    }
+                }
+                else if (*iter == '}' && (sel == argument && --level > 0))
+                    g_string_append_c(sel, *iter);
+                else if (*iter == '}' && ((sel != argument) || (sel == argument && level <= 0)))
+                {
+                    if (sel == argument)
+                        iter++;
+                    break;
+                }
+                else
+                    g_string_append_c(sel, *iter);
+            }
+
+            if (expression->len == 0)
+            {
+                g_string_free(expression, TRUE);
+                g_string_free(argument, TRUE);
+                continue;
+            }
+
+            result = tuple_formatter_process_expr(tuple, expression->str, argument->len ? argument->str : NULL);
+            if (result != NULL)
+            {
+                g_string_append(ctx->str, result);
+                g_free(result);
+            }
+
+            g_string_free(expression, TRUE);
+            g_string_free(argument, TRUE);
+
+            if (*iter == '\0')
+                break;
+        }
+        else if (*iter == '%' && *(iter + 1) == '{')
+        {
+            GString *expression = g_string_new("");
+            GString *argument = g_string_new("");
+            GString *sel = expression;
+            gchar *result;
+            gint level = 0;
+
+            for (iter += 2; *iter != '\0'; iter++)
+            {
+                if (*iter == ':')
+                {
+                    sel = argument;
+                    continue;
+                }
+
+                if (g_str_has_prefix(iter, "${") == TRUE || g_str_has_prefix(iter, "%{") == TRUE)
+                {
+                    if (sel == argument)
+                    {
+                        g_string_append_c(sel, *iter);
+                        level++;
+                    }
+                }
+                else if (*iter == '}' && (sel == argument && --level > 0))
+                    g_string_append_c(sel, *iter);
+                else if (*iter == '}' && ((sel != argument) || (sel == argument && level <= 0)))
+                {
+                    if (sel == argument)
+                        iter++;
+                    break;
+                }
+                else
+                    g_string_append_c(sel, *iter);
+            }
+
+            if (expression->len == 0)
+            {
+                g_string_free(expression, TRUE);
+                g_string_free(argument, TRUE);
+                continue;
+            }
+
+            result = tuple_formatter_process_function(tuple, expression->str, argument->len ? argument->str : NULL);
+            if (result != NULL)
+            {
+                g_string_append(ctx->str, result);
+                g_free(result);
+            }
+
+            g_string_free(expression, TRUE);
+            g_string_free(argument, TRUE);
+
+            if (*iter == '\0')
+                break;
+        }
+    }
+
+    out = g_strdup(ctx->str->str);
+    g_string_free(ctx->str, TRUE);
+    g_free(ctx);
+
+    return out;
+}
+
+static GList *tuple_formatter_expr_list = NULL;
+
+typedef struct {
+    const gchar *name;
+    gboolean (*func)(Tuple *tuple, const gchar *expression);
+} TupleFormatterExpression;
+
+/* processes an expression and optional argument pair. */
+gchar *
+tuple_formatter_process_expr(Tuple *tuple, const gchar *expression, 
+    const gchar *argument)
+{
+    TupleFormatterExpression *expr = NULL;
+    GList *iter;
+
+    g_return_val_if_fail(tuple != NULL, NULL);
+    g_return_val_if_fail(expression != NULL, NULL);
+
+    for (iter = tuple_formatter_expr_list; iter != NULL; iter = iter->next)
+    {
+        TupleFormatterExpression *tmp = (TupleFormatterExpression *) iter->data;
+
+        if (g_str_has_prefix(expression, tmp->name) == TRUE)
+        {
+            expr = tmp;
+            expression += strlen(tmp->name);
+        }
+    }
+
+    /* ${artist} */
+    if (expr == NULL && argument == NULL)
+    {
+        TupleValueType type = tuple_get_value_type(tuple, expression);
+
+        switch(type)
+        {
+        case TUPLE_STRING:
+             return g_strdup(tuple_get_string(tuple, expression));
+             break;
+        case TUPLE_INT:
+             return g_strdup_printf("%d", tuple_get_int(tuple, expression));
+             break;
+        case TUPLE_UNKNOWN:
+        default:
+             return NULL;
+        }
+    }
+    else if (expr != NULL)
+    {
+        if (expr->func(tuple, expression) == TRUE && argument != NULL)
+            return tuple_formatter_process_construct(tuple, argument);
+    }
+
+    return NULL;
+}
+
+static GList *tuple_formatter_func_list = NULL;
+
+typedef struct {
+    const gchar *name;
+    gchar *(*func)(Tuple *tuple, gchar **args);
+} TupleFormatterFunction;
+
+/* processes a function */
+gchar *
+tuple_formatter_process_function(Tuple *tuple, const gchar *expression, 
+    const gchar *argument)
+{
+    TupleFormatterFunction *expr = NULL;
+    GList *iter;
+
+    g_return_val_if_fail(tuple != NULL, NULL);
+    g_return_val_if_fail(expression != NULL, NULL);
+
+    for (iter = tuple_formatter_func_list; iter != NULL; iter = iter->next)
+    {
+        TupleFormatterFunction *tmp = (TupleFormatterFunction *) iter->data;
+
+        if (g_str_has_prefix(expression, tmp->name) == TRUE)
+        {
+            expr = tmp;
+            expression += strlen(tmp->name);
+        }
+    }
+
+    if (expr != NULL)
+    {
+        gchar **args;
+        gchar *ret;
+
+        if (argument)
+            args = g_strsplit(argument, ",", 10);
+        else
+            args = NULL;
+
+        ret = expr->func(tuple, args);
+
+        if (args)
+            g_strfreev(args);
+
+        return ret;
+    }
+
+    return NULL;
+}
+
+/* registers a formatter */
+void
+tuple_formatter_register_expression(const gchar *keyword,
+	gboolean (*func)(Tuple *tuple, const gchar *argument))
+{
+    TupleFormatterExpression *expr;
+
+    g_return_if_fail(keyword != NULL);
+    g_return_if_fail(func != NULL);
+
+    expr = g_new0(TupleFormatterExpression, 1);
+    expr->name = keyword;
+    expr->func = func;
+
+    tuple_formatter_expr_list = g_list_append(tuple_formatter_expr_list, expr);
+}
+
+/* registers a function */
+void
+tuple_formatter_register_function(const gchar *keyword,
+	gchar *(*func)(Tuple *tuple, gchar **argument))
+{
+    TupleFormatterFunction *expr;
+
+    g_return_if_fail(keyword != NULL);
+    g_return_if_fail(func != NULL);
+
+    expr = g_new0(TupleFormatterFunction, 1);
+    expr->name = keyword;
+    expr->func = func;
+
+    tuple_formatter_func_list = g_list_append(tuple_formatter_func_list, expr);
+}
+
+/* builtin-keyword: ${?arg}, returns TRUE if <arg> exists. */
+static gboolean
+tuple_formatter_expression_exists(Tuple *tuple, const gchar *expression)
+{
+    return (tuple_get_value_type(tuple, expression) != TUPLE_UNKNOWN) ? TRUE : FALSE;
+}
+
+/* builtin-keyword: ${==arg1,arg2}, returns TRUE if <arg1> and <arg2> match. */
+static gboolean
+tuple_formatter_expression_match(Tuple *tuple, const gchar *expression)
+{
+    gchar **args = g_strsplit(expression, ",", 2);
+    gchar *arg1, *arg2;
+    gint ret;
+
+    if (tuple_get_value_type(tuple, args[0]) == TUPLE_UNKNOWN)
+    {
+        g_strfreev(args);
+        return FALSE;
+    }
+
+    if (tuple_get_value_type(tuple, args[1]) == TUPLE_UNKNOWN)
+    {
+        g_strfreev(args);
+        return FALSE;
+    }
+
+    if (tuple_get_value_type(tuple, args[0]) == TUPLE_STRING)
+        arg1 = g_strdup(tuple_get_string(tuple, args[0]));
+    else
+        arg1 = g_strdup_printf("%d", tuple_get_int(tuple, args[0]));
+
+    if (tuple_get_value_type(tuple, args[1]) == TUPLE_STRING)
+        arg2 = g_strdup(tuple_get_string(tuple, args[1]));
+    else
+        arg2 = g_strdup_printf("%d", tuple_get_int(tuple, args[1]));
+
+    ret = g_ascii_strcasecmp(arg1, arg2);
+    g_free(arg1);
+    g_free(arg2);
+    g_strfreev(args);
+
+    return ret ? FALSE : TRUE;
+}
+
+/* builtin-keyword: ${!=arg1,arg2}. returns TRUE if <arg1> and <arg2> don't match. */
+static gboolean
+tuple_formatter_expression_nonmatch(Tuple *tuple, const gchar *expression)
+{
+    return tuple_formatter_expression_match(tuple, expression) ^ 1;
+}
+
+/* builtin-keyword: ${empty?}. returns TRUE if <arg> is empty. */
+static gboolean
+tuple_formatter_expression_empty(Tuple *tuple, const gchar *expression)
+{
+    gboolean ret = TRUE;
+    const gchar *iter;
+    TupleValueType type = tuple_get_value_type(tuple, expression);
+
+    if (type == TUPLE_UNKNOWN)
+        return TRUE;
+
+    if (type == TUPLE_INT && tuple_get_int(tuple, expression) != 0)
+        return FALSE;
+
+    iter = tuple_get_string(tuple, expression);
+
+    while (ret && *iter != '\0')
+    {
+        if (*iter == ' ')
+            iter++;
+        else
+            ret = FALSE;
+    }
+
+    return ret;
+}
+
+/* builtin function: %{audacious-version} */
+static gchar *
+tuple_formatter_function_version(Tuple *tuple, gchar **args)
+{
+    return g_strdup(PACKAGE_NAME " " PACKAGE_VERSION);
+}
+
+/* processes a string containing instructions. does initialization phases
+   if not already done */
+gchar *
+tuple_formatter_process_string(Tuple *tuple, const gchar *string)
+{
+    static gboolean initialized = FALSE;
+
+    if (initialized == FALSE)
+    {
+        tuple_formatter_register_expression("?", tuple_formatter_expression_exists);
+        tuple_formatter_register_expression("==", tuple_formatter_expression_match);
+        tuple_formatter_register_expression("!=", tuple_formatter_expression_nonmatch);
+        tuple_formatter_register_expression("(empty)?", tuple_formatter_expression_empty);
+
+        tuple_formatter_register_function("audacious-version", tuple_formatter_function_version);
+        initialized = TRUE;
+    }
+
+    return tuple_formatter_process_construct(tuple, string);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/audacious/tuple_formatter.h	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,40 @@
+/*
+ * Audacious
+ * Copyright (c) 2007 William Pitcock
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 3 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses>.
+ *
+ * The Audacious team does not consider modular code linking to
+ * Audacious or using our public API to be a derived work.
+ */
+
+#ifndef __AUDACIOUS_TUPLE_FORMATTER_H__
+#define __AUDACIOUS_TUPLE_FORMATTER_H__
+
+#include <glib.h>
+#include <mowgli.h>
+
+#include "tuple.h"
+
+gchar *tuple_formatter_process_string(Tuple *tuple, const gchar *string);
+void tuple_formatter_register_expression(const gchar *keyword,
+        gboolean (*func)(Tuple *tuple, const gchar *argument));
+void tuple_formatter_register_function(const gchar *keyword,
+        gchar *(*func)(Tuple *tuple, gchar **argument));
+gchar *tuple_formatter_process_expr(Tuple *tuple, const gchar *expression,
+    const gchar *argument);
+gchar *tuple_formatter_process_function(Tuple *tuple, const gchar *expression,
+    const gchar *argument);
+gchar *tuple_formatter_process_construct(Tuple *tuple, const gchar *string);
+
+#endif
--- a/src/audacious/ui_fileinfopopup.c	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/ui_fileinfopopup.c	Thu Aug 09 16:28:42 2007 +0300
@@ -470,9 +470,9 @@
         filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_artist", "");
         filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_album", "");
         filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_genre", "");
-        filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_track", "");
+        filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_tracknum", "");
         filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_year", "");
-        filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_length", "");
+        filepopup_entry_set_text(GTK_WIDGET(filepopup_win), "label_tracklen", "");
 
         gtk_window_resize(GTK_WINDOW(filepopup_win), 1, 1);
     }
--- a/src/audacious/ui_main.c	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/ui_main.c	Thu Aug 09 16:28:42 2007 +0300
@@ -523,7 +523,7 @@
 static void
 mainwin_refresh_visible(void)
 {
-    if (!bmp_active_skin)
+    if (!bmp_active_skin || !cfg.player_visible)
         return;
 
     gtk_widget_show_all(mainwin);
--- a/src/audacious/ui_playlist.c	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/ui_playlist.c	Thu Aug 09 16:28:42 2007 +0300
@@ -50,7 +50,6 @@
 #include "strings.h"
 #include "ui_equalizer.h"
 #include "ui_fileopener.h"
-#include "ui_fileinfopopup.h"
 #include "ui_main.h"
 #include "ui_manager.h"
 #include "util.h"
@@ -75,9 +74,6 @@
 
 static gboolean playlistwin_hint_flag = FALSE;
 
-static GtkWidget *playlistwin_infopopup = NULL;
-static guint playlistwin_infopopup_sid = 0;
-
 static GtkWidget *playlistwin_slider = NULL;
 static GtkWidget *playlistwin_time_min, *playlistwin_time_sec;
 static GtkWidget *playlistwin_info, *playlistwin_sinfo;
@@ -91,8 +87,6 @@
 static gboolean playlistwin_select_search_kp_cb( GtkWidget *entry , GdkEventKey *event ,
                                                  gpointer searchdlg_win );
 
-static gboolean playlistwin_fileinfopopup_probe(gpointer * filepopup_win);
-
 static gboolean playlistwin_resizing = FALSE;
 static gint playlistwin_resize_x, playlistwin_resize_y;
 
@@ -1597,8 +1591,6 @@
     playlistwin_create_widgets();
     playlistwin_update_info(playlist_get_active());
 
-    playlistwin_infopopup = audacious_fileinfopopup_create();
-
     gtk_window_add_accel_group(GTK_WINDOW(playlistwin), ui_manager_get_accel_group());
 }
 
@@ -1617,10 +1609,6 @@
     playlistwin_set_toprow(0);
     playlist_check_pos_current(playlist_get_active());
 
-    if ( playlistwin_infopopup_sid == 0 )
-      playlistwin_infopopup_sid = g_timeout_add(
-        50 , (GSourceFunc)playlistwin_fileinfopopup_probe , playlistwin_infopopup );
-
     gtk_widget_show_all(playlistwin);
     if (!cfg.playlist_shaded)
         gtk_widget_hide(playlistwin_sinfo);
@@ -1640,13 +1628,6 @@
     UI_SKINNED_BUTTON(mainwin_pl)->inside = FALSE;
     gtk_widget_queue_draw(mainwin_pl);
 
-    /* no point in probing for playlistwin_infopopup trigger when the playlistwin is hidden */
-    if ( playlistwin_infopopup_sid != 0 )
-    {
-      g_source_remove( playlistwin_infopopup_sid );
-      playlistwin_infopopup_sid = 0;
-    }
-
     if ( cfg.player_visible )
     {
       gtk_window_present(GTK_WINDOW(mainwin));
@@ -1975,74 +1956,3 @@
             return FALSE;
     }
 }
-
-
-/* fileinfopopup callback for playlistwin */
-static gboolean
-playlistwin_fileinfopopup_probe(gpointer * filepopup_win)
-{
-    gint x, y, pos;
-    TitleInput *tuple;
-    static gint prev_x = 0, prev_y = 0, ctr = 0, prev_pos = -1;
-    static gint shaded_pos = -1, shaded_prev_pos = -1;
-    gboolean skip = FALSE;
-    GdkWindow *win;
-    Playlist *playlist = playlist_get_active();
-
-    win = gdk_window_at_pointer(NULL, NULL);
-    gdk_window_get_pointer(GDK_WINDOW(playlistwin->window), &x, &y, NULL);
-    pos = ui_skinned_playlist_get_position(playlistwin_list, x - 12, y - 20);
-
-    if (win == NULL
-        || cfg.show_filepopup_for_tuple == FALSE
-        || UI_SKINNED_PLAYLIST(playlistwin_list)->tooltips == FALSE
-        || pos != prev_pos
-        || win != playlistwin_list->window)
-    {
-        prev_pos = pos;
-        ctr = 0;
-        audacious_fileinfopopup_hide(GTK_WIDGET(filepopup_win), NULL);
-        return TRUE;
-    }
-
-    if (prev_x == x && prev_y == y)
-        ctr++;
-    else
-    {
-        ctr = 0;
-        prev_x = x;
-        prev_y = y;
-        audacious_fileinfopopup_hide(GTK_WIDGET(filepopup_win), NULL);
-        return TRUE;
-    }
-
-    if (playlistwin_is_shaded())
-    {
-        shaded_pos = playlist_get_position(playlist);
-        if (shaded_prev_pos != shaded_pos)
-            skip = TRUE;
-    }
-
-    if (ctr >= cfg.filepopup_delay && (skip == TRUE || GTK_WIDGET_VISIBLE(GTK_WIDGET(filepopup_win)) != TRUE)) {
-        if (pos == -1 && !playlistwin_is_shaded())
-        {
-            audacious_fileinfopopup_hide(GTK_WIDGET(filepopup_win), NULL);
-            return TRUE;
-        }
-        /* shaded mode */
-        else
-        {
-            tuple = playlist_get_tuple(playlist, shaded_pos);
-            audacious_fileinfopopup_hide(GTK_WIDGET(filepopup_win), NULL);
-            audacious_fileinfopopup_show_from_tuple(GTK_WIDGET(filepopup_win), tuple);
-            shaded_prev_pos = shaded_pos;
-        }
-
-        prev_pos = pos;
-
-        tuple = playlist_get_tuple(playlist, pos);
-        audacious_fileinfopopup_show_from_tuple(GTK_WIDGET(filepopup_win), tuple);
-    }
-
-    return TRUE;
-}
--- a/src/audacious/ui_skinned_playlist.c	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/ui_skinned_playlist.c	Thu Aug 09 16:28:42 2007 +0300
@@ -50,6 +50,7 @@
 #include "playback.h"
 #include "playlist.h"
 #include "ui_manager.h"
+#include "ui_fileinfopopup.h"
 
 #include "debug.h"
 static PangoFontDescription *playlist_list_font = NULL;
@@ -90,7 +91,12 @@
 static gboolean ui_skinned_playlist_button_press   (GtkWidget *widget, GdkEventButton *event);
 static gboolean ui_skinned_playlist_button_release (GtkWidget *widget, GdkEventButton *event);
 static gboolean ui_skinned_playlist_motion_notify  (GtkWidget *widget, GdkEventMotion *event);
+static gboolean ui_skinned_playlist_leave_notify   (GtkWidget *widget, GdkEventCrossing *event);
 static void ui_skinned_playlist_redraw             (UiSkinnedPlaylist *playlist);
+static gboolean ui_skinned_playlist_popup_show     (gpointer data);
+static void ui_skinned_playlist_popup_hide         (GtkWidget *widget);
+static void ui_skinned_playlist_popup_timer_start  (GtkWidget *widget);
+static void ui_skinned_playlist_popup_timer_stop   (GtkWidget *widget);
 
 static GtkWidgetClass *parent_class = NULL;
 static guint playlist_signals[LAST_SIGNAL] = { 0 };
@@ -134,6 +140,7 @@
     widget_class->button_press_event = ui_skinned_playlist_button_press;
     widget_class->button_release_event = ui_skinned_playlist_button_release;
     widget_class->motion_notify_event = ui_skinned_playlist_motion_notify;
+    widget_class->leave_notify_event = ui_skinned_playlist_leave_notify;
 
     klass->redraw = ui_skinned_playlist_redraw;
 
@@ -153,7 +160,14 @@
     playlist->prev_selected = -1;
     playlist->prev_min = -1;
     playlist->prev_max = -1;
-    playlist->tooltips = TRUE;
+
+    g_object_set_data(G_OBJECT(playlist), "timer_id", GINT_TO_POINTER(0));
+    g_object_set_data(G_OBJECT(playlist), "timer_active", GINT_TO_POINTER(0));
+
+    GtkWidget *popup = audacious_fileinfopopup_create();
+    g_object_set_data(G_OBJECT(playlist), "popup", popup);
+    g_object_set_data(G_OBJECT(playlist), "popup_active", GINT_TO_POINTER(0));
+    g_object_set_data(G_OBJECT(playlist), "popup_position", GINT_TO_POINTER(-1));
 }
 
 GtkWidget* ui_skinned_playlist_new(GtkWidget *fixed, gint x, gint y, gint w, gint h) {
@@ -204,7 +218,7 @@
     attributes.window_type = GDK_WINDOW_CHILD;
     attributes.event_mask = gtk_widget_get_events(widget);
     attributes.event_mask |= GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK | 
-                             GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK;
+                             GDK_LEAVE_NOTIFY_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK;
     attributes.visual = gtk_widget_get_visual(widget);
     attributes.colormap = gtk_widget_get_colormap(widget);
 
@@ -797,7 +811,6 @@
     if (nr == -1)
         return FALSE;
 
-    pl->tooltips = FALSE;
     if (event->button == 3) {
         GList* selection = playlist_get_selected(playlist);
         if (g_list_find(selection, GINT_TO_POINTER(nr)) == NULL) {
@@ -847,6 +860,8 @@
         priv->dragging = TRUE;
     }
     playlistwin_update_list(playlist);
+    ui_skinned_playlist_popup_hide(widget);
+    ui_skinned_playlist_popup_timer_stop(widget);
 
     return TRUE;
 }
@@ -858,9 +873,11 @@
         priv->dragging = FALSE;
         priv->auto_drag_down = FALSE;
         priv->auto_drag_up = FALSE;
-        UI_SKINNED_PLAYLIST(widget)->tooltips = TRUE;
         gtk_widget_queue_draw(widget);
     }
+
+    ui_skinned_playlist_popup_hide(widget);
+    ui_skinned_playlist_popup_timer_stop(widget);
     return TRUE;
 }
 
@@ -906,10 +923,28 @@
             playlistwin_update_list(playlist_get_active());
         }
         priv->drag_pos = nr;
+    } else {
+        gint pos = ui_skinned_playlist_get_position(widget, event->x, event->y);
+        gint cur_pos = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "popup_position"));
+        if (pos != cur_pos) {
+            g_object_set_data(G_OBJECT(widget), "popup_position", GINT_TO_POINTER(pos));
+            ui_skinned_playlist_popup_hide(widget);
+            ui_skinned_playlist_popup_timer_stop(widget);
+            if (pos != -1)
+                ui_skinned_playlist_popup_timer_start(widget);
+        }
     }
+
     return TRUE;
 }
 
+static gboolean ui_skinned_playlist_leave_notify(GtkWidget *widget, GdkEventCrossing *event) {
+    ui_skinned_playlist_popup_hide(widget);
+    ui_skinned_playlist_popup_timer_stop(widget);
+
+    return FALSE;
+}
+
 static void ui_skinned_playlist_redraw(UiSkinnedPlaylist *playlist) {
     UiSkinnedPlaylistPrivate *priv = UI_SKINNED_PLAYLIST_GET_PRIVATE(playlist);
 
@@ -972,3 +1007,49 @@
     priv->resize_width += w;
     priv->resize_height += h;
 }
+
+static gboolean ui_skinned_playlist_popup_show(gpointer data) {
+    GtkWidget *widget = data;
+    gint pos = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "popup_position"));
+
+    if (GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "timer_active")) == 1 && pos != -1) {
+        TitleInput *tuple;
+        Playlist *pl_active = playlist_get_active();
+        GtkWidget *popup = g_object_get_data(G_OBJECT(widget), "popup");
+
+        tuple = playlist_get_tuple(pl_active, pos);
+        if ((tuple == NULL) || (tuple->length < 1)) {
+           gchar *title = playlist_get_songtitle(pl_active, pos);
+           audacious_fileinfopopup_show_from_title(popup, title);
+           g_free(title);
+        } else {
+           audacious_fileinfopopup_show_from_tuple(popup , tuple);
+        }
+        g_object_set_data(G_OBJECT(widget), "popup_active" , GINT_TO_POINTER(1));
+    }
+
+    ui_skinned_playlist_popup_timer_stop(widget);
+    return FALSE;
+}
+
+static void ui_skinned_playlist_popup_hide(GtkWidget *widget) {
+    if (GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "popup_active")) == 1) {
+        GtkWidget *popup = g_object_get_data(G_OBJECT(widget), "popup");
+        g_object_set_data(G_OBJECT(widget), "popup_active", GINT_TO_POINTER(0));
+        audacious_fileinfopopup_hide(popup, NULL);
+    }
+}
+
+static void ui_skinned_playlist_popup_timer_start(GtkWidget *widget) {
+    gint timer_id = g_timeout_add(cfg.filepopup_delay*100, ui_skinned_playlist_popup_show, widget);
+    g_object_set_data(G_OBJECT(widget), "timer_id", GINT_TO_POINTER(timer_id));
+    g_object_set_data(G_OBJECT(widget), "timer_active", GINT_TO_POINTER(1));
+}
+
+static void ui_skinned_playlist_popup_timer_stop(GtkWidget *widget) {
+    if (GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "timer_active")) == 1)
+        g_source_remove(GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "timer_id")));
+
+    g_object_set_data(G_OBJECT(widget), "timer_id", GINT_TO_POINTER(0));
+    g_object_set_data(G_OBJECT(widget), "timer_active", GINT_TO_POINTER(0));
+}
--- a/src/audacious/ui_skinned_playlist.h	Wed Aug 08 22:04:41 2007 +0300
+++ b/src/audacious/ui_skinned_playlist.h	Thu Aug 09 16:28:42 2007 +0300
@@ -48,7 +48,6 @@
     gint        first;
     gint        num_visible;
     gint        prev_selected, prev_min, prev_max;
-    gboolean    tooltips;
     gboolean    drag_motion;
     gint        drag_motion_x, drag_motion_y;
     gint        fheight;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tests/Makefile	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,38 @@
+include ../../mk/rules.mk
+include ../../mk/init.mk
+include ../../mk/objective.mk
+
+OBJECTIVE_LIBS_NOINST = tuple_formatter_test \
+	tuple_formatter_functor_test
+
+LDFLAGS += $(AUDLDFLAGS)
+LDADD = \
+	$(DBUS_LIBS) \
+	$(GTK_LIBS)      \
+	$(MOWGLI_LIBS)	 \
+	$(LIBGLADE_LIBS)
+
+CFLAGS += \
+	$(GTK_CFLAGS)      \
+	$(DBUS_CFLAGS)     \
+	$(LIBGLADE_CFLAGS) \
+	$(BEEP_DEFINES)    \
+	$(ARCH_DEFINES)    \
+	$(MOWGLI_CFLAGS)   \
+	-I.. -I../..   \
+	-I../intl -I../audacious
+
+COMMON_OBJS = test_harness.o
+TFT_OBJS = $(COMMON_OBJS) tuple_formatter_test.o ../audacious/tuple.o ../audacious/tuple_formatter.o
+tuple_formatter_test: $(TFT_OBJS)
+	$(CC) $(LDFLAGS) $(TFT_OBJS) $(LDADD) -o $@
+	@printf "%10s     %-20s\n" LINK $@
+	./$@
+	@printf "%10s     %-20s\n" TEST-PASS $@
+
+TFFT_OBJS = $(COMMON_OBJS) tuple_formatter_functor_test.o ../audacious/tuple.o ../audacious/tuple_formatter.o
+tuple_formatter_functor_test: $(TFFT_OBJS)
+	$(CC) $(LDFLAGS) $(TFFT_OBJS) $(LDADD) -o $@
+	@printf "%10s     %-20s\n" LINK $@
+	./$@
+	@printf "%10s     %-20s\n" TEST-PASS $@
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tests/README	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,11 @@
+This directory contains some tests. To run them, type "make".
+
+Any needed uncompiled files in the audacious core will be compiled
+with the proper CFLAGS. Don't worry about that.
+
+You can help make this directory more useful by writing tests as
+you fix bugs. Unless they're in the UI, autotesting that may be
+problematic.
+
+To write a test, simply edit an existing test and the Makefile
+as required.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tests/test_harness.c	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,38 @@
+/*
+ * Audacious
+ * Copyright (c) 2007 William Pitcock
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 3 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses>.
+ *
+ * The Audacious team does not consider modular code linking to
+ * Audacious or using our public API to be a derived work.
+ */
+
+#include <glib.h>
+#include <mowgli.h>
+
+extern int test_run(gint argc, const gchar *argv[]);
+
+int
+main(gint argc, const gchar *argv[])
+{
+    g_thread_init(NULL);
+
+    mowgli_init();
+
+    if (!g_thread_supported())
+        mowgli_log("Warning: GThread not supported. Some tests may fail.");
+
+    return test_run(argc, argv);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tests/tuple_formatter_functor_test.c	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,71 @@
+/*
+ * Audacious
+ * Copyright (c) 2007 William Pitcock
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 3 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses>.
+ *
+ * The Audacious team does not consider modular code linking to
+ * Audacious or using our public API to be a derived work.
+ */
+
+#include <glib.h>
+#include <mowgli.h>
+
+#include "tuple.h"
+#include "tuple_formatter.h"
+
+static gboolean
+test_functor(Tuple *tuple, const char *expr)
+{
+    return TRUE;
+}
+
+int
+test_run(int argc, const char *argv[])
+{
+    Tuple *tuple;
+    gchar *tstr;
+
+    tuple_formatter_register_expression("(true)", test_functor);
+
+    tuple = tuple_new();
+    tuple_associate_string(tuple, "splork", "moo");
+
+    tstr = tuple_formatter_process_string(tuple, "${(true):${splork}}");
+    if (g_ascii_strcasecmp(tstr, "moo"))
+    {
+        g_print("fail 1: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "%{audacious-version}");
+    if (g_str_has_prefix(tstr, "audacious") == FALSE)
+    {
+        g_print("fail 2: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${(true):%{audacious-version}}");
+    if (g_str_has_prefix(tstr, "audacious") == FALSE)
+    {
+        g_print("fail 3: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    mowgli_object_unref(tuple);
+
+    return EXIT_SUCCESS;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tests/tuple_formatter_test.c	Thu Aug 09 16:28:42 2007 +0300
@@ -0,0 +1,112 @@
+/*
+ * Audacious
+ * Copyright (c) 2007 William Pitcock
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; under version 3 of the License.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses>.
+ *
+ * The Audacious team does not consider modular code linking to
+ * Audacious or using our public API to be a derived work.
+ */
+
+#include <glib.h>
+#include <mowgli.h>
+
+#include "tuple.h"
+#include "tuple_formatter.h"
+
+int
+test_run(int argc, const char *argv[])
+{
+    Tuple *tuple;
+    gchar *tstr;
+
+    tuple = tuple_new();
+    tuple_associate_string(tuple, "splork", "moo");
+    tuple_associate_int(tuple, "splorkerz", 42);
+
+    tstr = tuple_formatter_process_string(tuple, "${splork} ${splorkerz}");
+    if (g_ascii_strcasecmp(tstr, "moo 42"))
+    {
+        g_print("fail 1: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${?fizz:${splork}} ${splorkerz}");
+    if (g_ascii_strcasecmp(tstr, " 42"))
+    {
+        g_print("fail 2: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${?splork:${splork}} ${splorkerz}");
+    if (g_ascii_strcasecmp(tstr, "moo 42"))
+    {
+        g_print("fail 3: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${==splork,splork:fields given matched}");
+    if (g_ascii_strcasecmp(tstr, "fields given matched"))
+    {
+        g_print("fail 4: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${==splork,splork:${splork}}");
+    if (g_ascii_strcasecmp(tstr, "moo"))
+    {
+        g_print("fail 5: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${!=splork,splorkerz:fields did not match}");
+    if (g_ascii_strcasecmp(tstr, "fields did not match"))
+    {
+        g_print("fail 6: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${!=splork,splorkerz:${splorkerz}}");
+    if (g_ascii_strcasecmp(tstr, "42"))
+    {
+        g_print("fail 7: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${!=splork,splork:${splorkerz}}");
+    if (g_ascii_strcasecmp(tstr, ""))
+    {
+        g_print("fail 8: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    tstr = tuple_formatter_process_string(tuple, "${(empty)?splorky:${splorkerz}}");
+    if (g_ascii_strcasecmp(tstr, "42"))
+    {
+        g_print("fail 9: '%s'\n", tstr);
+        return EXIT_FAILURE;
+    }
+    g_free(tstr);
+
+    mowgli_object_unref(tuple);
+
+    return EXIT_SUCCESS;
+}