DisOrder 4.1
authorRichard Kettlewell <rjk@greenend.org.uk>
Sun, 6 Jun 2010 12:25:18 +0000 (13:25 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sun, 6 Jun 2010 12:25:18 +0000 (13:25 +0100)
57 files changed:
.bzrignore
CHANGES.html
README
README.developers
cgi/actions.c
cgi/cgimain.c
cgi/disorder-cgi.h
cgi/macros-disorder.c
clients/Makefile.am
clients/test-eclient.c [deleted file]
configure.ac
debian/changelog
disobedience/Makefile.am
disobedience/TODO [deleted file]
disobedience/added.c [new file with mode: 0644]
disobedience/choose-menu.c [new file with mode: 0644]
disobedience/choose-search.c [new file with mode: 0644]
disobedience/choose.c
disobedience/choose.h [new file with mode: 0644]
disobedience/client.c
disobedience/control.c
disobedience/disobedience.c
disobedience/disobedience.h
disobedience/help.c
disobedience/log.c
disobedience/login.c
disobedience/lookup.c [new file with mode: 0644]
disobedience/menu.c
disobedience/misc.c
disobedience/popup.c [new file with mode: 0644]
disobedience/popup.h [new file with mode: 0644]
disobedience/properties.c
disobedience/queue-generic.c [new file with mode: 0644]
disobedience/queue-generic.h [new file with mode: 0644]
disobedience/queue-menu.c [new file with mode: 0644]
disobedience/queue.c
disobedience/recent.c [new file with mode: 0644]
disobedience/users.c
doc/disobedience.1.in
doc/disorder_protocol.5.in
lib/Makefile.am
lib/eclient.c
lib/eclient.h
lib/email.c [new file with mode: 0644]
lib/eventdist.c [new file with mode: 0644]
lib/eventdist.h [new file with mode: 0644]
lib/kvp.c
lib/kvp.h
lib/sendmail.h
lib/trackdb.c
lib/trackname.h
lib/tracksort.c [new file with mode: 0644]
libtests/Makefile.am
libtests/t-eventdist.c [new file with mode: 0644]
scripts/setup.in
server/server.c
templates/choose.tmpl

index 99a65d9..22ff521 100644 (file)
@@ -194,3 +194,4 @@ libtests/t-dateparse
 libtests/Makefile
 cgi/Makefile
 libtests/index.html
+libtests/t-eventdist
index a1a0b9a..2a3c894 100644 (file)
@@ -57,6 +57,52 @@ span.command {
 
 <p>This file documents recent user-visible changes to DisOrder.</p>
 
+<h2>Changes up to version 4.1</h2>
+
+<div class=section>
+
+  <h3>Disobedience</h3>
+  
+    <div class=section>
+  
+      <p>Disobedience has been largely rewritten:</p>
+
+      <ul>
+        
+        <li>All the tabs now use native GTK+ list/tree widgets, resulting in
+        greater speed in some cases and more consistency with other GTK+
+        applications.</li>
+
+        <li>You can now use type-ahead find in the choose tab.  The initiation
+        of a search is delayed slightly to avoid lots of updates when you're
+        half way through entering search terms.</li>
+
+        <li>The choose tab now shows track lengths.</li>
+    
+        <li>Many buttons are now more reliably made insensitive when they can't
+        be used.</li>
+
+        <li>You can now play tracks off the recent tab.</li>
+        
+      </ul>
+      
+      <p>Disobedience attempts to cope with servers from older versions, up to
+      a point, but this is not well tested and it's best to keep the server
+      fully up to date.</p>
+
+    </div>
+     
+  <h3>Server</h3>
+  
+    <div class=section>
+
+      <p>When a track shares a directory with its alias, the real track name is
+      now returned instead of the alias (the opposite way round to the previous
+      behaviour).</p>
+      
+    </div>
+</div>
+
 <h2>Changes up to version 4.0.2</h2>
 
 <div class=section>
diff --git a/README b/README
index 6885cd9..8cd2c35 100644 (file)
--- a/README
+++ b/README
@@ -34,9 +34,9 @@ Build dependencies:
   libao            0.8.6
   libasound        1.0.13
   libFLAC          1.1.2
-  GNU C            4.1.2
-  GNU Make         3.81
-  GNU Sed          4.1.5
+  GNU C            4.1.2               }
+  GNU Make         3.81                } Non-GNU versions will NOT work
+  GNU Sed          4.1.5               }
   Python           2.4.4               (optional)
   GTK+             2.8.20              (if you want the GTK+ client)
   GLIB             2.12.4              (if you want the GTK+ client)
index 5a5dd89..d0c1cdc 100644 (file)
@@ -14,6 +14,8 @@ Dependencies:
                     libao-dev libmad0-dev libasound2-dev libdb4.3-dev \
                     libflac-dev
 
+     (Use the bzr from backports, the one in etch is obsolete.)
+
    * On FreeBSD you'll need at least these packages:
         autotools
         bash
@@ -138,10 +140,7 @@ Web Interface:
      keep it that way.  Clever use of CSS is OK provided it works well on the
      mainstream browsers.
 
-   * I know that the web template syntax is rather nasty.  Perhaps it will be
-     improved in a future version.
-
-   * Update templates/help.html for any changes you make.
+   * Update templates/help.tmpl for any changes you make.
 
 Disobedience:
 
@@ -188,12 +187,9 @@ Code And Patches:
      (But if your new feature only makes sense on a given platform then
      obviously its new dependencies don't need to be available elsewhere.)
 
-   * GCC is stated as a dependency.  In fact the code is mostly standard C,
-     with C99 initializers, long long and possibly the occasional // comment as
-     the main departures from C89.  Additional GCCisms will be accepted if it's
-     impractical to avoid them.  At least one active user is still using GCC
-     2.95, so extensions that only appear in later versions are to be avoided
-     for the time being.
+   * GCCisms such as typeof and C99isms such as mixed declarations and named
+     structure initializers are used; the configure script asks for -std=gnu99
+     by default.  Some supported platforms are still on GCC 4.0.
 
    * Please submit patches either using 'diff -u', or by publishing a bzr
      branch somewhere I can get at it.
index e9995b9..4728503 100644 (file)
@@ -240,7 +240,7 @@ static void act_play(void) {
   const char *track, *dir;
   char **tracks;
   int ntracks, n;
-  struct dcgi_entry *e;
+  struct tracksort_data *tsd;
   
   if(dcgi_client) {
     if((track = cgi_get("track"))) {
@@ -248,15 +248,9 @@ static void act_play(void) {
     } else if((dir = cgi_get("dir"))) {
       if(disorder_files(dcgi_client, dir, 0, &tracks, &ntracks))
         ntracks = 0;
-      e = xmalloc(ntracks * sizeof (struct dcgi_entry));
-      for(n = 0; n < ntracks; ++n) {
-        e[n].track = tracks[n];
-        e[n].sort = trackname_transform("track", tracks[n], "sort");
-        e[n].display = trackname_transform("track", tracks[n], "display");
-      }
-      qsort(e, ntracks, sizeof (struct dcgi_entry), dcgi_compare_entry);
+      tsd = tracksort_init(ntracks, tracks, "track");
       for(n = 0; n < ntracks; ++n)
-        disorder_play(dcgi_client, e[n].track);
+        disorder_play(dcgi_client, tsd[n].track);
     }
   }
   redirect(0);
@@ -434,7 +428,7 @@ static void act_register(void) {
   }
   /* We could well do better address validation but for now we'll just do the
    * minimum */
-  if(!strchr(email, '@')) {
+  if(!email_valid(email)) {
     login_error("bademail");
     return;
   }
@@ -523,7 +517,7 @@ static void act_edituser(void) {
     }
   } else
     password = password2 = 0;
-  if(email && !strchr(email, '@')) {
+  if(email && !email_valid(email)) {
     login_error("bademail");
     return;
   }
@@ -742,7 +736,7 @@ void dcgi_expand(const char *name, int header) {
   if(!(found = mx_find(p, 0/*report*/)))
     fatal(errno, "cannot find %s", p);
   if(header) {
-    if(printf("Content-Type: text/html\n"
+    if(printf("Content-Type: text/html; charset=UTF-8\n"
               "%s\n"
               "\n", dcgi_cookie_header()) < 0)
       fatal(errno, "error writing to stdout");
index 253f82e..e96bbab 100644 (file)
@@ -33,7 +33,7 @@ int main(int argc, char **argv) {
   /* TODO we could make disorder/ACTION equivalent to disorder?action=ACTION */
   if(getenv("PATH_INFO")) {
     /* TODO it might be nice to link back to the right place... */
-    printf("Content-Type: text/html\n");
+    printf("Content-Type: text/html; charset=UTF-8\n");
     printf("Status: 404\n");
     printf("\n");
     printf("<p>Sorry, PATH_INFO not supported.</p>\n");
index 3f19fe2..3cabe9b 100644 (file)
@@ -60,16 +60,6 @@ extern char *dcgi_cookie;
 extern const char *dcgi_error_string;
 extern const char *dcgi_status_string;
 
-/** @brief Entry in a list of tracks or directories */
-struct dcgi_entry {
-  /** @brief Track name */
-  const char *track;
-  /** @brief Sort key */
-  const char *sort;
-  /** @brief Display key */
-  const char *display;
-};
-
 /** @brief Compare two @ref entry objects */
 int dcgi_compare_entry(const void *a, const void *b);
 
index d549e10..5ea7413 100644 (file)
@@ -814,15 +814,6 @@ static int exp_image(int attribute((unused)) nargs,
   return sink_writes(output, cgi_sgmlquote(url)) < 0 ? -1 : 0;
 }
 
-/** @brief Compare two @ref entry objects */
-int dcgi_compare_entry(const void *a, const void *b) {
-  const struct dcgi_entry *ea = a, *eb = b;
-
-  return compare_tracks(ea->sort, eb->sort,
-                       ea->display, eb->display,
-                       ea->track, eb->track);
-}
-
 /** @brief Implementation of exp_tracks() and exp_dirs() */
 static int exp__files_dirs(int nargs,
                            const struct mx_node **args,
@@ -837,7 +828,7 @@ static int exp__files_dirs(int nargs,
   char **tracks, *dir, *re;
   int n, ntracks, rc;
   const struct mx_node *m;
-  struct dcgi_entry *e;
+  struct tracksort_data *tsd;
 
   if((rc = mx_expandstr(args[0], &dir, u, "argument #0 (DIR)")))
     return rc;
@@ -855,24 +846,18 @@ static int exp__files_dirs(int nargs,
   if(fn(dcgi_client, dir, re, &tracks, &ntracks))
     return 0;
   /* Sort it.  NB trackname_transform() does not go to the server. */
-  e = xcalloc(ntracks, sizeof *e);
-  for(n = 0; n < ntracks; ++n) {
-    e[n].track = tracks[n];
-    e[n].sort = trackname_transform(type, tracks[n], "sort");
-    e[n].display = trackname_transform(type, tracks[n], "display");
-  }
-  qsort(e, ntracks, sizeof (struct dcgi_entry), dcgi_compare_entry);
+  tsd = tracksort_init(ntracks, tracks, type);
   /* Expand the subsiduary templates.  We chuck in @sort and @display because
    * it is particularly easy to do so. */
   for(n = 0; n < ntracks; ++n)
     if((rc = mx_expand(mx_rewritel(m,
                                    "index", make_index(n),
                                    "parity", n % 2 ? "odd" : "even",
-                                   "track", e[n].track,
+                                   "track", tsd[n].track,
                                    "first", n == 0 ? "true" : "false",
                                    "last", n + 1 == ntracks ? "false" : "true",
-                                   "sort", e[n].sort,
-                                   "display", e[n].display,
+                                   "sort", tsd[n].sort,
+                                   "display", tsd[n].display,
                                    (char *)0),
                        output, u)))
       return rc;
index 58316b4..0ff3477 100644 (file)
@@ -19,7 +19,7 @@
 #
 
 bin_PROGRAMS=disorder disorderfm disorder-playrtp
-noinst_PROGRAMS=test-eclient filename-bytes
+noinst_PROGRAMS=filename-bytes
 noinst_SCRIPTS=dump2wav
 
 AM_CPPFLAGS=-I${top_srcdir}/lib -I../lib
@@ -44,12 +44,6 @@ disorder_playrtp_DEPENDENCIES=$(LIBOBJS) ../lib/libdisorder.a
 
 filename_bytes_SOURCES=filename-bytes.c
 
-test_eclient_SOURCES=test-eclient.c \
-       ../lib/memgc.c
-test_eclient_LDADD=../lib/libdisorder.a \
-       $(LIBGC) $(LIBGCRYPT) $(LIBPCRE)
-test_eclient_DEPENDENCIES=../lib/libdisorder.a
-
 install-exec-hook:
        $(LIBTOOL) --mode=finish $(DESTDIR)$(libdir)
 
diff --git a/clients/test-eclient.c b/clients/test-eclient.c
deleted file mode 100644 (file)
index b5742a3..0000000
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * This file is part of DisOrder.
- * Copyright (C) 2006 Richard Kettlewell
- *
- * 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; either version 2 of the License, or
- * (at your option) any later version.
- *
- * 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, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
- * USA
- */
-
-#include "common.h"
-
-#include <sys/select.h>
-#include <errno.h>
-#include <time.h>
-
-#include "queue.h"
-#include "mem.h"
-#include "log.h"
-#include "eclient.h"
-#include "configuration.h"
-#include "syscalls.h"
-#include "wstat.h"
-#include "charset.h"
-
-/* TODO: a more comprehensive test */
-
-static fd_set rfd, wfd;
-static int maxfd;
-static disorder_eclient *clients[1024];
-static char **tracks;
-static disorder_eclient *c;
-static char u_value;
-static int quit;
-
-static const char *modes[] = { "none", "read", "write", "read write" };
-
-static void cb_comms_error(void *u, const char *msg) {
-  assert(u == &u_value);
-  fprintf(stderr, "! comms error: %s\n", msg);
-}
-
-static void cb_protocol_error(void *u,
-                             void attribute((unused)) *v,
-                             int attribute((unused)) code,
-                             const char *msg) {
-  assert(u == &u_value);
-  fprintf(stderr, "! protocol error: %s\n", msg);
-}
-
-static void cb_poll(void *u, disorder_eclient *c_, int fd, unsigned mode) {
-  assert(u == &u_value);
-  assert(fd >= 0);
-  assert(fd < 1024);                   /* bodge */
-  fprintf(stderr, "  poll callback %d %s\n", fd, modes[mode]);
-  if(mode & DISORDER_POLL_READ)
-    FD_SET(fd, &rfd);
-  else
-    FD_CLR(fd, &rfd);
-  if(mode & DISORDER_POLL_WRITE)
-    FD_SET(fd, &wfd);
-  else
-    FD_CLR(fd, &wfd);
-  clients[fd] = mode ? c_ : 0;
-  if(fd > maxfd) maxfd = fd;
-}
-
-static void cb_report(void attribute((unused)) *u,
-                     const char attribute((unused)) *msg) {
-}
-
-static const disorder_eclient_callbacks callbacks = {
-  cb_comms_error,
-  cb_protocol_error,
-  cb_poll,
-  cb_report
-};
-
-/* cheap plastic event loop */
-static void loop(void) {
-  int n;
-  
-  while(!quit) {
-    fd_set r = rfd, w = wfd;
-    n = select(maxfd + 1, &r, &w, 0, 0);
-    if(n < 0) {
-      if(errno == EINTR) continue;
-      fatal(errno, "select");
-    }
-    for(n = 0; n <= maxfd; ++n)
-      if(clients[n] && (FD_ISSET(n, &r) || FD_ISSET(n, &w)))
-       disorder_eclient_polled(clients[n],
-                               ((FD_ISSET(n, &r) ? DISORDER_POLL_READ : 0)
-                                |(FD_ISSET(n, &w) ? DISORDER_POLL_WRITE : 0)));
-  }
-  printf(". quit\n");
-}
-
-static void done(void) {
-  printf(". done\n");
-  disorder_eclient_close(c);
-  quit = 1;
-}
-
-static void play_completed(void *v) {
-  assert(v == tracks);
-  printf("* played: %s\n", *tracks);
-  ++tracks;
-  if(*tracks) {
-    if(disorder_eclient_play(c, *tracks, play_completed, tracks))
-      exit(1);
-  } else
-    done();
-}
-
-static void version_completed(void *v, const char *value) {
-  printf("* version: %s\n", value);
-  if(v) {
-    if(*tracks) {
-      if(disorder_eclient_play(c, *tracks, play_completed, tracks))
-       exit(1);
-    } else
-      done();
-  }
-}
-
-/* TODO: de-dupe with disorder.c */
-static void print_queue_entry(const struct queue_entry *q) {
-  if(q->track) xprintf("track %s\n", nullcheck(utf82mb(q->track)));
-  if(q->id) xprintf("  id %s\n", nullcheck(utf82mb(q->id)));
-  if(q->submitter) xprintf("  submitted by %s at %s",
-                          nullcheck(utf82mb(q->submitter)), ctime(&q->when));
-  if(q->played) xprintf("  played at %s", ctime(&q->played));
-  if(q->state == playing_started
-     || q->state == playing_paused) xprintf("  %lds so far",  q->sofar);
-  else if(q->expected) xprintf("  might start at %s", ctime(&q->expected));
-  if(q->scratched) xprintf("  scratched by %s\n",
-                          nullcheck(utf82mb(q->scratched)));
-  else xprintf("  %s\n", playing_states[q->state]);
-  if(q->wstat) xprintf("  %s\n", wstat(q->wstat));
-}
-
-static void recent_completed(void *v, struct queue_entry *q) {
-  assert(v == 0);
-  for(; q; q = q->next)
-    print_queue_entry(q);
-  if(disorder_eclient_version(c, version_completed, (void *)"")) exit(1);
-}
-
-int main(int argc, char **argv) {
-  assert(argc > 0);
-  mem_init();
-  debugging = 0;                       /* turn on for even more verbosity */
-  if(config_read(0)) fatal(0, "config_read failed");
-  tracks = &argv[1];
-  c = disorder_eclient_new(&callbacks, &u_value);
-  assert(c != 0);
-  /* stack up several version commands to test pipelining */
-  if(disorder_eclient_version(c, version_completed, 0)) exit(1);
-  if(disorder_eclient_version(c, version_completed, 0)) exit(1);
-  if(disorder_eclient_version(c, version_completed, 0)) exit(1);
-  if(disorder_eclient_version(c, version_completed, 0)) exit(1);
-  if(disorder_eclient_version(c, version_completed, 0)) exit(1);
-  if(disorder_eclient_recent(c, recent_completed, 0)) exit(1);
-  loop();
-  exit(0);
-}
-
-/*
-Local Variables:
-c-basic-offset:2
-comment-column:40
-End:
-*/
index fecfb7f..9e9a7c7 100644 (file)
@@ -20,9 +20,9 @@
 # USA
 #
 
-AC_INIT([disorder], [4.0.2], [richard+disorder@sfere.greenend.org.uk])
+AC_INIT([disorder], [4.1], [richard+disorder@sfere.greenend.org.uk])
 AC_CONFIG_AUX_DIR([config.aux])
-AM_INIT_AUTOMAKE(disorder, [4.0.2])
+AM_INIT_AUTOMAKE(disorder, [4.1])
 AC_CONFIG_SRCDIR([server/disorderd.c])
 AM_CONFIG_HEADER([config.h])
 
@@ -46,6 +46,13 @@ AC_PROG_CC
 AC_SET_MAKE
 if test "x$GCC" = xyes; then
   gcc_werror=-Werror
+  case "$CC" in
+  *-std=* )
+    ;;
+  * )
+    CC="${CC} -std=gnu99"
+    ;;
+  esac
 else
   AC_MSG_ERROR([GNU C is required to build this program])
   gcc_werror=""
index 9a4bb40..7799d2c 100644 (file)
@@ -1,3 +1,9 @@
+disorder (4.1) unstable; urgency=low
+
+  * DisOrder 4.1
+
+ -- Richard Kettlewell <rjk@greenend.org.uk>  Sat, 28 Jun 2008 14:35:20 +0100
+
 disorder (4.0.2) unstable; urgency=low
 
   * Correct web browser linkage from Disobedience.
index 8c49273..08e465c 100644 (file)
@@ -26,9 +26,10 @@ AM_CFLAGS=$(GLIB_CFLAGS) $(GTK_CFLAGS)
 PNGS:=$(shell export LC_COLLATE=C;echo ${top_srcdir}/images/*.png)
 
 disobedience_SOURCES=disobedience.h disobedience.c client.c queue.c    \
-                 choose.c misc.c control.c properties.c menu.c \
-                 log.c progress.c login.c rtp.c help.c \
-                 ../lib/memgc.c settings.c users.c
+       recent.c added.c queue-generic.c queue-generic.h queue-menu.c   \
+       choose.c choose-menu.c choose-search.c popup.c misc.c           \
+       control.c properties.c menu.c log.c progress.c login.c rtp.c    \
+       help.c ../lib/memgc.c settings.c users.c lookup.c
 disobedience_LDADD=../lib/libdisorder.a $(LIBPCRE) $(LIBGC) $(LIBGCRYPT) \
        $(LIBASOUND) $(COREAUDIO) $(LIBDB)
 disobedience_LDFLAGS=$(GTK_LIBS)
@@ -47,14 +48,15 @@ disobedience.html: ../doc/disobedience.1 $(top_srcdir)/scripts/htmlman
 misc.o: images.h
 
 images.h: $(PNGS)
+       set -e;                                                         \
        exec > @$.new;                                                  \
        for png in $(PNGS); do                                          \
-         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;                   \
+         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;           \
          gdk-pixbuf-csource --raw --name=image_$$name $$png;           \
        done;                                                           \
        echo "static const struct image images[] = {";                  \
        for png in $(PNGS); do                                          \
-         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;                   \
+         name=`echo $$png | $(GNUSED) 's,.*/,,;s,\.png,,;'`;           \
          echo "  { \"$$name.png\", image_$$name },";                   \
        done;                                                           \
        echo "};"
diff --git a/disobedience/TODO b/disobedience/TODO
deleted file mode 100644 (file)
index 31168db..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-properties
-       make return be the same as OK
-search
-       select tracks by tag
-
-general:
-       disobedience doesn't like starting up if the server isn't running.
diff --git a/disobedience/added.c b/disobedience/added.c
new file mode 100644 (file)
index 0000000..ce747c4
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2006-2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "disobedience.h"
+#include "popup.h"
+#include "queue-generic.h"
+
+/** @brief Called with an updated list of newly-added tracks
+ *
+ * This is called with a raw list of track names but the rest of @ref
+ * disobedience/queue-generic.c requires @ref queue_entry structures
+ * with a valid and unique @c id field.  This function fakes it.
+ */
+static void added_completed(void attribute((unused)) *v,
+                            const char *err,
+                            int nvec, char **vec) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
+  }
+  /* Convert the vector result to a queue linked list */
+  struct queue_entry *q, *qh, *qlast = 0, **qq = &qh;
+  int n;
+  
+  for(n = 0; n < nvec; ++n) {
+    q = xmalloc(sizeof *q);
+    q->prev = qlast;
+    q->track = vec[n];
+    q->id = vec[n];             /* unique because a track is only added once */
+    *qq = q;
+    qq = &q->next;
+    qlast = q;
+  }
+  *qq = 0;
+  ql_new_queue(&ql_added, qh);
+  /* Tell anyone who cares */
+  event_raise("added-list-changed", qh);
+}
+
+/** @brief Update the newly-added list */
+static void added_changed(const char attribute((unused)) *event,
+                          void attribute((unused)) *eventdata,
+                          void attribute((unused)) *callbackdata) {
+  D(("added_changed"));
+
+  gtk_label_set_text(GTK_LABEL(report_label),
+                     "updating newly added track list");
+  disorder_eclient_new_tracks(client, added_completed, 0/*all*/, 0);
+}
+
+/** @brief Called at startup */
+static void added_init(void) {
+  event_register("rescan-complete", added_changed, 0);
+}
+
+/** @brief Columns for the new tracks list */
+static const struct queue_column added_columns[] = {
+  { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
+  { "Album",  column_namepart, "album",  COL_EXPAND|COL_ELLIPSIZE },
+  { "Title",  column_namepart, "title",  COL_EXPAND|COL_ELLIPSIZE },
+  { "Length", column_length,   0,        COL_RIGHT }
+};
+
+/** @brief Pop-up menu for new tracks list */
+static struct menuitem added_menuitems[] = {
+  { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
+  { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
+  { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+  { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+};
+
+struct queuelike ql_added = {
+  .name = "added",
+  .init = added_init,
+  .columns = added_columns,
+  .ncolumns = sizeof added_columns / sizeof *added_columns,
+  .menuitems = added_menuitems,
+  .nmenuitems = sizeof added_menuitems / sizeof *added_menuitems,
+};
+
+GtkWidget *added_widget(void) {
+  return init_queuelike(&ql_added);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/disobedience/choose-menu.c b/disobedience/choose-menu.c
new file mode 100644 (file)
index 0000000..cc1d1a3
--- /dev/null
@@ -0,0 +1,266 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "disobedience.h"
+#include "popup.h"
+#include "choose.h"
+
+/** @brief Popup menu */
+static GtkWidget *choose_menu;
+
+/** @brief Recursion step for choose_get_visible()
+ * @param parent A visible node, or NULL for the root
+ * @param callback Called for each visible node
+ * @param userdata Passed to @p callback
+ *
+ * If @p callback returns nonzero, the walk stops immediately.
+ */
+static int choose_visible_recurse(GtkTreeIter *parent,
+                                  int (*callback)(GtkTreeIter *it,
+                                                  int isfile,
+                                                  void *userdata),
+                                  void *userdata) {
+  int expanded;
+  if(parent) {
+    /* Skip placeholders */
+    if(choose_is_placeholder(parent))
+      return 0;
+    const int isfile = choose_is_file(parent);
+    if(callback(parent, isfile, userdata))
+      return 1;
+    if(isfile)
+      return 0;                 /* Files never have children */
+    GtkTreePath *parent_path
+      = gtk_tree_model_get_path(GTK_TREE_MODEL(choose_store),
+                                parent);
+    expanded = gtk_tree_view_row_expanded(GTK_TREE_VIEW(choose_view),
+                                          parent_path);
+    gtk_tree_path_free(parent_path);
+  } else
+    expanded = 1;
+  /* See if parent is expanded */
+  if(expanded) {
+    /* Parent is expanded, visit all its children */
+    GtkTreeIter it[1];
+    gboolean itv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
+                                                it,
+                                                parent);
+    while(itv) {
+      if(choose_visible_recurse(it, callback, userdata))
+        return TRUE;
+      itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
+    }
+  }
+  return 0;
+}
+
+static void choose_visible_visit(int (*callback)(GtkTreeIter *it,
+                                                 int isfile,
+                                                 void *userdata),
+                                 void *userdata) {
+  choose_visible_recurse(NULL, callback, userdata);
+}
+
+static int choose_selectall_sensitive_callback
+   (GtkTreeIter attribute((unused)) *it,
+     int isfile,
+     void *userdata) {
+  if(isfile) {
+    *(int *)userdata = 1;
+    return 1;
+  }
+  return 0;
+}
+
+/** @brief Should 'select all' be sensitive?
+ *
+ * Yes if there are visible files.
+ */
+static int choose_selectall_sensitive(void attribute((unused)) *extra) {
+  int files = 0;
+  choose_visible_visit(choose_selectall_sensitive_callback, &files);
+  return files > 0;
+}
+
+static int choose_selectall_activate_callback
+    (GtkTreeIter *it,
+     int isfile,
+     void attribute((unused)) *userdata) {
+  if(isfile)
+    gtk_tree_selection_select_iter(choose_selection, it);
+  else
+    gtk_tree_selection_unselect_iter(choose_selection, it);
+  return 0;
+}
+
+/** @brief Activate select all
+ *
+ * Selects all files and deselects everything else.
+ */
+static void choose_selectall_activate(GtkMenuItem attribute((unused)) *item,
+                                      gpointer attribute((unused)) userdata) {
+  choose_visible_visit(choose_selectall_activate_callback, 0);
+}
+
+/** @brief Should 'select none' be sensitive
+ *
+ * Yes if anything is selected.
+ */
+static int choose_selectnone_sensitive(void attribute((unused)) *extra) {
+  return gtk_tree_selection_count_selected_rows(choose_selection) > 0;
+}
+
+/** @brief Activate select none */
+static void choose_selectnone_activate(GtkMenuItem attribute((unused)) *item,
+                                       gpointer attribute((unused)) userdata) {
+  gtk_tree_selection_unselect_all(choose_selection);
+}
+
+static void choose_play_sensitive_callback(GtkTreeModel attribute((unused)) *model,
+                                           GtkTreePath attribute((unused)) *path,
+                                           GtkTreeIter *iter,
+                                           gpointer data) {
+  int *filesp = data;
+
+  if(*filesp == -1)
+    return;
+  if(choose_is_dir(iter))
+    *filesp = -1;
+  else if(choose_is_file(iter))
+    ++*filesp;
+}
+
+/** @brief Should 'play' be sensitive?
+ *
+ * Yes if tracks are selected and no directories are */
+static int choose_play_sensitive(void attribute((unused)) *extra) {
+  int files = 0;
+  
+  gtk_tree_selection_selected_foreach(choose_selection,
+                                      choose_play_sensitive_callback,
+                                      &files);
+  return files > 0;
+}
+
+static void choose_gather_selected_files_callback(GtkTreeModel attribute((unused)) *model,
+                                                  GtkTreePath attribute((unused)) *path,
+                                                  GtkTreeIter *iter,
+                                                  gpointer data) {
+  struct vector *v = data;
+
+  if(choose_is_file(iter))
+    vector_append(v, choose_get_track(iter));
+}
+
+  
+static void choose_play_activate(GtkMenuItem attribute((unused)) *item,
+                                 gpointer attribute((unused)) userdata) {
+  struct vector v[1];
+  vector_init(v);
+  gtk_tree_selection_selected_foreach(choose_selection,
+                                      choose_gather_selected_files_callback,
+                                      v);
+  for(int n = 0; n < v->nvec; ++n)
+    disorder_eclient_play(client, v->vec[n], choose_play_completed, 0);
+}
+  
+static int choose_properties_sensitive(void *extra) {
+  return choose_play_sensitive(extra);
+}
+  
+static void choose_properties_activate(GtkMenuItem attribute((unused)) *item,
+                                       gpointer attribute((unused)) userdata) {
+  struct vector v[1];
+  vector_init(v);
+  gtk_tree_selection_selected_foreach(choose_selection,
+                                      choose_gather_selected_files_callback,
+                                      v);
+  properties(v->nvec, (const char **)v->vec);
+}
+
+/** @brief Pop-up menu for choose */
+static struct menuitem choose_menuitems[] = {
+  {
+    "Play track",
+    choose_play_activate,
+    choose_play_sensitive,
+    0,
+    0
+  },
+  {
+    "Track properties",
+    choose_properties_activate,
+    choose_properties_sensitive,
+    0,
+    0
+  },
+  {
+    "Select all tracks",
+    choose_selectall_activate,
+    choose_selectall_sensitive,
+    0,
+    0
+  },
+  {
+    "Deselect all tracks",
+    choose_selectnone_activate,
+    choose_selectnone_sensitive,
+    0,
+    0
+  },
+};
+
+const struct tabtype choose_tabtype = {
+  choose_properties_sensitive,
+  choose_selectall_sensitive,
+  choose_selectnone_sensitive,
+  choose_properties_activate,
+  choose_selectall_activate,
+  choose_selectnone_activate,
+  0,
+  0
+};
+
+/** @brief Called when a mouse button is pressed or released */
+gboolean choose_button_event(GtkWidget attribute((unused)) *widget,
+                             GdkEventButton *event,
+                             gpointer attribute((unused)) user_data) {
+  if(event->type == GDK_BUTTON_RELEASE && event->button == 2) {
+    /* Middle click release - play track */
+    ensure_selected(GTK_TREE_VIEW(choose_view), event);
+    choose_play_activate(NULL, NULL);
+  } else if(event->type == GDK_BUTTON_PRESS && event->button == 3) {
+    /* Right click press - pop up the menu */
+    ensure_selected(GTK_TREE_VIEW(choose_view), event);
+    popup(&choose_menu, event,
+          choose_menuitems, sizeof choose_menuitems / sizeof *choose_menuitems,
+          0);
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/disobedience/choose-search.c b/disobedience/choose-search.c
new file mode 100644 (file)
index 0000000..d822250
--- /dev/null
@@ -0,0 +1,556 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file disobedience/search.c
+ * @brief Search support
+ */
+#include "disobedience.h"
+#include "choose.h"
+
+int choose_auto_expanding;
+
+GtkWidget *choose_search_entry;
+static GtkWidget *choose_next;
+static GtkWidget *choose_prev;
+static GtkWidget *choose_clear;
+
+/** @brief True if a search command is in flight */
+static int choose_searching;
+
+/** @brief True if in-flight search is now known to be obsolete */
+static int choose_search_obsolete;
+
+/** @brief Current search terms */
+static char *choose_search_terms;
+
+/** @brief Hash of all search result */
+static hash *choose_search_hash;
+
+/** @brief List of invisible search results
+ *
+ * This only lists search results not yet known to be visible, and is
+ * gradually depleted.
+ */
+static char **choose_search_results;
+
+/** @brief Length of @ref choose_search_results */
+static int choose_n_search_results;
+
+/** @brief Row references for search results */
+static GtkTreeRowReference **choose_search_references;
+
+/** @brief Length of @ref choose_search_references */
+static int choose_n_search_references;
+
+/** @brief Event handle for monitoring newly inserted tracks */
+static event_handle choose_inserted_handle;
+
+/** @brief Time of last search entry keypress (or 0.0) */
+static struct timeval choose_search_last_keypress;
+
+/** @brief Timeout ID for search delay */
+static guint choose_search_timeout_id;
+
+static void choose_search_entry_changed(GtkEditable *editable,
+                                        gpointer user_data);
+
+int choose_is_search_result(const char *track) {
+  return choose_search_hash && hash_find(choose_search_hash, track);
+}
+
+static int is_prefix(const char *dir, const char *track) {
+  size_t nd = strlen(dir);
+
+  if(nd < strlen(track)
+     && track[nd] == '/'
+     && !strncmp(track, dir, nd))
+    return 1;
+  else
+    return 0;
+}
+
+/** @brief Do some work towards making @p track visible
+ * @return True if we made it visible or it was missing
+ */
+static int choose_make_one_visible(const char *track) {
+  //fprintf(stderr, " choose_make_one_visible %s\n", track);
+  /* We walk through nodes at the top level looking for directories that are
+   * prefixes of the target track.
+   *
+   * - if we find one and it's expanded we walk through its children
+   * - if we find one and it's NOT expanded then we expand it, and arrange
+   *   to be revisited
+   * - if we don't find one then we're probably out of date
+   */
+  GtkTreeIter it[1];
+  gboolean itv = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(choose_store),
+                                               it);
+  while(itv) {
+    const char *dir = choose_get_track(it);
+
+    //fprintf(stderr, "  %s\n", dir);
+    if(!dir) {
+      /* Placeholder */
+      itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
+      continue;
+    }
+    GtkTreePath *path = gtk_tree_model_get_path(GTK_TREE_MODEL(choose_store),
+                                                it);
+    if(!strcmp(dir, track)) {
+      /* We found the track.  If everything above it was expanded, it will be
+       * too.  So we can report it as visible. */
+      //fprintf(stderr, "   found %s\n", track);
+      choose_search_references[choose_n_search_references++]
+        = gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store), path);
+      gtk_tree_path_free(path);
+      return 1;
+    }
+    if(is_prefix(dir, track)) {
+      /* We found a prefix of the target track. */
+      //fprintf(stderr, "   %s is a prefix\n", dir);
+      const gboolean expanded
+        = gtk_tree_view_row_expanded(GTK_TREE_VIEW(choose_view), path);
+      if(expanded) {
+        //fprintf(stderr, "   is apparently expanded\n");
+        /* This directory is expanded, let's make like Augustus Gibbons and
+         * take it to the next level. */
+        GtkTreeIter child[1];           /* don't know if parent==iter allowed */
+        itv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
+                                           child,
+                                           it);
+        *it = *child;
+        if(choose_is_placeholder(it)) {
+          //fprintf(stderr, "   %s is expanded, has a placeholder child\n", dir);
+          /* We assume that placeholder children of expanded rows are about to
+           * be replaced */
+          gtk_tree_path_free(path);
+          return 0;
+        }
+      } else {
+        //fprintf(stderr, "   requesting expansion of %s\n", dir);
+        /* Track is below a non-expanded directory.  So let's expand it.
+         * choose_make_visible() will arrange a revisit in due course.
+         *
+         * We mark the row as auto-expanded.
+         */
+        ++choose_auto_expanding;
+        gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view),
+                                 path,
+                                 FALSE/*open_all*/);
+        gtk_tree_path_free(path);
+        --choose_auto_expanding;
+        return 0;
+      }
+    } else
+      itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
+    gtk_tree_path_free(path);
+  }
+  /* If we reach the end then we didn't find the track at all. */
+  fprintf(stderr, "choose_make_one_visible: could not find %s\n",
+          track);
+  return 1;
+}
+
+/** @brief Compare two GtkTreeRowReferences
+ *
+ * Not very efficient since it does multiple memory operations per
+ * comparison!
+ */
+static int choose_compare_references(const void *av, const void *bv) {
+  GtkTreeRowReference *a = *(GtkTreeRowReference **)av;
+  GtkTreeRowReference *b = *(GtkTreeRowReference **)bv;
+  GtkTreePath *pa = gtk_tree_row_reference_get_path(a);
+  GtkTreePath *pb = gtk_tree_row_reference_get_path(b);
+  const int rc = gtk_tree_path_compare(pa, pb);
+  gtk_tree_path_free(pa);
+  gtk_tree_path_free(pb);
+  return rc;
+}
+
+/** @brief Make @p path visible
+ * @param path Row reference to make visible
+ * @param row_align Row alignment (or -ve)
+ * @return 0 on success, nonzero if @p ref has gone stale
+ *
+ * If @p row_align is negative no row alignemt is performed.  Otherwise
+ * it must be between 0 (the top) and 1 (the bottom).
+ *
+ * TODO: if the row is already visible do nothing.
+ */
+static int choose_make_path_visible(GtkTreePath *path,
+                                    gfloat row_align) {
+  /* Make sure that the target's parents are all expanded */
+  gtk_tree_view_expand_to_path(GTK_TREE_VIEW(choose_view), path);
+  /* Make sure the target is visible */
+  gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(choose_view), path, NULL,
+                               row_align >= 0.0,
+                               row_align,
+                               0);
+  return 0;
+}
+
+/** @brief Make @p ref visible
+ * @param ref Row reference to make visible
+ * @param row_align Row alignment (or -ve)
+ * @return 0 on success, nonzero if @p ref has gone stale
+ *
+ * If @p row_align is negative no row alignemt is performed.  Otherwise
+ * it must be between 0 (the top) and 1 (the bottom).
+ */
+static int choose_make_ref_visible(GtkTreeRowReference *ref,
+                                   gfloat row_align) {
+  GtkTreePath *path = gtk_tree_row_reference_get_path(ref);
+  if(!path)
+    return -1;
+  choose_make_path_visible(path, row_align);
+  gtk_tree_path_free(path);
+  return 0;
+}
+
+/** @brief Do some work towards ensuring that all search results are visible
+ *
+ * Assumes there's at least one results!
+ */
+static void choose_make_visible(const char attribute((unused)) *event,
+                                void attribute((unused)) *eventdata,
+                                void attribute((unused)) *callbackdata) {
+  //fprintf(stderr, "choose_make_visible\n");
+  int remaining = 0;
+
+  for(int n = 0; n < choose_n_search_results; ++n) {
+    if(!choose_search_results[n])
+      continue;
+    if(choose_make_one_visible(choose_search_results[n]))
+      choose_search_results[n] = 0;
+    else
+      ++remaining;
+  }
+  //fprintf(stderr, "remaining=%d\n", remaining);
+  if(remaining) {
+    /* If there's work left to be done make sure we get a callback when
+     * something changes */
+    if(!choose_inserted_handle)
+      choose_inserted_handle = event_register("choose-more-tracks",
+                                              choose_make_visible, 0);
+  } else {
+    /* Suppress callbacks if there's nothing more to do */
+    event_cancel(choose_inserted_handle);
+    choose_inserted_handle = 0;
+    /* We've expanded everything, now we can mess with the cursor */
+    //fprintf(stderr, "sort %d references\n", choose_n_search_references);
+    qsort(choose_search_references,
+          choose_n_search_references,
+          sizeof (GtkTreeRowReference *),
+          choose_compare_references);
+    choose_make_ref_visible(choose_search_references[0], 0.5);
+  }
+}
+
+/** @brief Called with search results */
+static void choose_search_completed(void attribute((unused)) *v,
+                                    const char *err,
+                                    int nvec, char **vec) {
+  //fprintf(stderr, "choose_search_completed\n");
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
+  }
+  choose_searching = 0;
+  /* If the search was obsoleted initiate another one */
+  if(choose_search_obsolete) {
+    choose_search_obsolete = 0;
+    choose_search_entry_changed(0, 0);
+    return;
+  }
+  //fprintf(stderr, "*** %d search results\n", nvec);
+  /* We're actually going to use these search results.  Autocollapse anything
+   * left over from the old search. */
+  choose_auto_collapse();
+  choose_search_hash = hash_new(1);
+  if(nvec) {
+    for(int n = 0; n < nvec; ++n)
+      hash_add(choose_search_hash, vec[n], "", HASH_INSERT);
+    /* Stash results for choose_make_visible */
+    choose_n_search_results = nvec;
+    choose_search_results = vec;
+    /* Make a big-enough buffer for the results row reference list */
+    choose_n_search_references = 0;
+    choose_search_references = xcalloc(nvec, sizeof (GtkTreeRowReference *));
+    /* Start making rows visible */
+    choose_make_visible(0, 0, 0);
+    gtk_widget_set_sensitive(choose_next, TRUE);
+    gtk_widget_set_sensitive(choose_prev, TRUE);
+  } else {
+    gtk_widget_set_sensitive(choose_next, FALSE);
+    gtk_widget_set_sensitive(choose_prev, FALSE);
+    choose_n_search_results = 0;
+    choose_search_results = 0;
+    choose_n_search_references = 0;
+    choose_search_references = 0;
+  }
+  event_raise("search-results-changed", 0);
+}
+
+/** @brief Actually initiate a search */
+static void initiate_search(void) {
+  //fprintf(stderr, "initiate_search\n");
+  /* If a search is in flight don't initiate a new one until it comes back */
+  if(choose_searching) {
+    choose_search_obsolete = 1;
+    return;
+  }
+  char *terms = xstrdup(gtk_entry_get_text(GTK_ENTRY(choose_search_entry)));
+  /* Strip leading and trailing space */
+  while(*terms == ' ')
+    ++terms;
+  char *e = terms + strlen(terms);
+  while(e > terms && e[-1] == ' ')
+    --e;
+  *e = 0;
+  if(choose_search_terms && !strcmp(terms, choose_search_terms)) {
+    /* Search terms have not actually changed in any way that matters */
+    return;
+  }
+  /* Remember the current terms */
+  choose_search_terms = terms;
+  if(!*terms) {
+    /* Nothing to search for.  Fake a completion call. */
+    choose_search_completed(0, 0, 0, 0);
+    return;
+  }
+  if(disorder_eclient_search(client, choose_search_completed, terms, 0)) {
+    /* Bad search terms.  Fake a completion call. */
+    choose_search_completed(0, 0, 0, 0);
+    return;
+  }
+  choose_searching = 1;
+}
+
+static gboolean choose_search_timeout(gpointer attribute((unused)) data) {
+  struct timeval now;
+  xgettimeofday(&now, NULL);
+  /*fprintf(stderr, "%ld.%06d choose_search_timeout\n",
+          now.tv_sec, now.tv_usec);*/
+  if(tvdouble(now) - tvdouble(choose_search_last_keypress)
+         < SEARCH_DELAY_MS / 1000.0) {
+    //fprintf(stderr, " ... too soon\n");
+    return TRUE;                        /* Call me again later */
+  }
+  //fprintf(stderr, " ... let's go\n");
+  choose_search_last_keypress.tv_sec = 0;
+  choose_search_last_keypress.tv_usec = 0;
+  choose_search_timeout_id = 0;
+  initiate_search();
+  return FALSE;
+}
+
+/** @brief Called when the search entry changes */
+static void choose_search_entry_changed
+    (GtkEditable attribute((unused)) *editable,
+     gpointer attribute((unused)) user_data) {
+  xgettimeofday(&choose_search_last_keypress, NULL);
+  /*fprintf(stderr, "%ld.%06d choose_search_entry_changed\n",
+          choose_search_last_keypress.tv_sec,
+          choose_search_last_keypress.tv_usec);*/
+  /* If there's already a timeout, remove it */
+  if(choose_search_timeout_id) {
+    g_source_remove(choose_search_timeout_id);
+    choose_search_timeout_id = 0;
+  }
+  /* Add a new timeout */
+  choose_search_timeout_id = g_timeout_add(SEARCH_DELAY_MS / 10,
+                                           choose_search_timeout,
+                                           0);
+  /* We really wanted to tell Glib what time we wanted the callback at rather
+   * than asking for calls at given intervals.  But there's no interface for
+   * that, and defining a new source for it seems like overkill if we can
+   * reasonably avoid it. */
+}
+
+/** @brief Identify first and last visible paths
+ *
+ * We'd like to use gtk_tree_view_get_visible_range() for this, but that was
+ * introduced in GTK+ 2.8, and Fink only has 2.6 (which is around three years
+ * out of date at time of writing), and I'm not yet prepared to rule out Fink
+ * support.
+ */
+static gboolean choose_get_visible_range(GtkTreeView *tree_view,
+                                         GtkTreePath **startpathp,
+                                         GtkTreePath **endpathp) {
+  GdkRectangle visible_tc[1];
+
+  /* Get the visible rectangle in tree coordinates */
+  gtk_tree_view_get_visible_rect(tree_view, visible_tc);
+  /*fprintf(stderr, "visible: %dx%x at %dx%d\n",
+          visible_tc->width, visible_tc->height,
+          visible_tc->x, visible_tc->y);*/
+  if(startpathp) {
+    /* Convert top-left visible point to widget coordinates */
+    int x_wc, y_wc;
+    gtk_tree_view_tree_to_widget_coords(tree_view,
+                                        visible_tc->x, visible_tc->y,
+                                        &x_wc, &y_wc);
+    //fprintf(stderr, " start widget coords: %dx%d\n", x_wc, y_wc);
+    gtk_tree_view_get_path_at_pos(tree_view,
+                                  x_wc, y_wc,
+                                  startpathp,
+                                  NULL,
+                                  NULL, NULL);
+  }
+  if(endpathp) {
+    /* Convert bottom-left visible point to widget coordinates */
+    /* Convert top-left visible point to widget coordinates */
+    int x_wc, y_wc;
+    gtk_tree_view_tree_to_widget_coords(tree_view,
+                                        visible_tc->x,
+                                        visible_tc->y + visible_tc->height - 1,
+                                        &x_wc, &y_wc);
+    //fprintf(stderr, " end widget coords: %dx%d\n", x_wc, y_wc);
+    gtk_tree_view_get_path_at_pos(tree_view,
+                                  x_wc, y_wc,
+                                  endpathp,
+                                  NULL,
+                                  NULL, NULL);
+  }
+  return TRUE;
+}
+
+/** @brief Move to the next/prev match
+ * @param direction -1 for prev, +1 for next
+ */
+static void choose_move(int direction) {
+  /* Refocus the main view so typahead find continues to work */
+  gtk_widget_grab_focus(choose_view);
+  /* If there's no results we have nothing to do */
+  if(!choose_n_search_results)
+    return;
+  /* Compute bounds for searching over the array in the right direction */
+  const int first = direction > 0 ? 0 : choose_n_search_references - 1;
+  const int limit = direction > 0 ? choose_n_search_references : -1;
+  /* Find the first/last currently visible row */
+  GtkTreePath *limitpath;
+  if(!choose_get_visible_range(GTK_TREE_VIEW(choose_view),
+                               direction < 0 ? &limitpath : 0,
+                               direction > 0 ? &limitpath : 0))
+    return;
+  /* Find a the first search result later/earlier than it.  They're sorted so
+   * we could actually do much better than this if necessary. */
+  for(int n = first; n != limit; n += direction) {
+    GtkTreePath *path
+      = gtk_tree_row_reference_get_path(choose_search_references[n]);
+    if(!path)
+      continue;
+    /* gtk_tree_path_compare returns -1, 0 or 1 so we compare naively with
+     * direction */
+    if(gtk_tree_path_compare(limitpath, path) + direction == 0) {
+      choose_make_path_visible(path, 0.5);
+      gtk_tree_path_free(path);
+      return;
+    }
+    gtk_tree_path_free(path);
+  }
+  /* We didn't find one.  Loop back to the first/las. */
+  for(int n = first; n != limit; n += direction) {
+    GtkTreePath *path
+      = gtk_tree_row_reference_get_path(choose_search_references[n]);
+    if(!path)
+      continue;
+    choose_make_path_visible(path, 0.5);
+    gtk_tree_path_free(path);
+    return;
+  }
+}
+
+void choose_next_clicked(GtkButton attribute((unused)) *button,
+                         gpointer attribute((unused)) userdata) {
+  choose_move(1);
+}
+
+void choose_prev_clicked(GtkButton attribute((unused)) *button,
+                         gpointer attribute((unused)) userdata) {
+  choose_move(-1);
+}
+
+/** @brief Called when the cancel search button is clicked */
+static void choose_clear_clicked(GtkButton attribute((unused)) *button,
+                                 gpointer attribute((unused)) userdata) {
+  gtk_entry_set_text(GTK_ENTRY(choose_search_entry), "");
+  gtk_widget_grab_focus(choose_view);
+  /* We start things off straight away in this case */
+  initiate_search();
+}
+
+/** @brief Called when the user hits ^F to start a new search */
+void choose_search_new(void) {
+  gtk_editable_select_region(GTK_EDITABLE(choose_search_entry), 0, -1);
+}
+
+/** @brief Create the search widget */
+GtkWidget *choose_search_widget(void) {
+
+  /* Text entry box for search terms */
+  choose_search_entry = gtk_entry_new();
+  gtk_widget_set_style(choose_search_entry, tool_style);
+  g_signal_connect(choose_search_entry, "changed",
+                   G_CALLBACK(choose_search_entry_changed), 0);
+  gtk_tooltips_set_tip(tips, choose_search_entry,
+                       "Enter search terms here; search is automatic", "");
+
+  /* Cancel button to clear the search */
+  choose_clear = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
+  gtk_widget_set_style(choose_clear, tool_style);
+  g_signal_connect(G_OBJECT(choose_clear), "clicked",
+                   G_CALLBACK(choose_clear_clicked), 0);
+  gtk_tooltips_set_tip(tips, choose_clear, "Clear search terms", "");
+
+  /* Up and down buttons to find previous/next results; initially they are not
+   * usable as there are no search results. */
+  choose_prev = iconbutton("up.png", "Previous search result");
+  g_signal_connect(G_OBJECT(choose_prev), "clicked",
+                   G_CALLBACK(choose_prev_clicked), 0);
+  gtk_widget_set_style(choose_prev, tool_style);
+  gtk_widget_set_sensitive(choose_prev, 0);
+  choose_next = iconbutton("down.png", "Next search result");
+  g_signal_connect(G_OBJECT(choose_next), "clicked",
+                   G_CALLBACK(choose_next_clicked), 0);
+  gtk_widget_set_style(choose_next, tool_style);
+  gtk_widget_set_sensitive(choose_next, 0);
+  
+  /* Pack the search tools button together on a line */
+  GtkWidget *hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
+  gtk_box_pack_start(GTK_BOX(hbox), choose_search_entry,
+                     TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
+  gtk_box_pack_start(GTK_BOX(hbox), choose_prev,
+                     FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
+  gtk_box_pack_start(GTK_BOX(hbox), choose_next,
+                     FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
+  gtk_box_pack_start(GTK_BOX(hbox), choose_clear,
+                     FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
+
+  return hbox;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 7c37d44..8d8ac08 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder
- * Copyright (C) 2006-2008 Richard Kettlewell
+ * Copyright (C) 2008 Richard Kettlewell
  *
  * 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
 /** @file disobedience/choose.c
  * @brief Hierarchical track selection and search
  *
- * We don't use the built-in tree widgets because they require that you know
- * the children of a node on demand, and we have to wait for the server to tell
- * us.
- */
-
-#include "disobedience.h"
-#include "timeval.h"
-
-/* Choose track ------------------------------------------------------------ */
-
-#if TDEBUG
-/* Timing */
-static struct {
-  struct timeval total;
-  struct timeval gtkbits;
-  struct timeval menuupdate;
-  struct timeval new_widgets;
-  struct timeval undisplay;
-  struct timeval colors;
-  struct timeval markers;
-  struct timeval location;
-  struct timeval selection;
-} times;
-
-#define BEGIN(WHAT) do {                        \
-  struct timeval started##WHAT, finished##WHAT; \
-  xgettimeofday(&started##WHAT, 0)
-
-#define END(WHAT)                                                       \
-  xgettimeofday(&finished##WHAT, 0);                                    \
-  times.WHAT = tvadd(times.WHAT, tvsub(finished##WHAT, started##WHAT)); \
-} while(0)
-
-#define INIT() memset(&times, 0, sizeof times)
-
-#define REPORT() do {                           \
-  fprintf(stderr, "total=%g\n"                  \
-          "gtkbits=%g\n"                        \
-          "menuupdate=%g\n"                     \
-          "new_widgets=%g\n"                    \
-          "undisplay=%g\n"                      \
-          "colors=%g\n"                         \
-          "markers=%g\n"                        \
-          "location=%g\n"                       \
-          "selection=%g\n"                      \
-          "accumulation=%g\n"                   \
-          "\n",                                 \
-          tvdouble(times.total),                \
-          tvdouble(times.gtkbits),              \
-          tvdouble(times.menuupdate),           \
-          tvdouble(times.new_widgets),          \
-          tvdouble(times.undisplay),            \
-          tvdouble(times.colors),               \
-          tvdouble(times.markers),              \
-          tvdouble(times.location),             \
-          tvdouble(times.selection),            \
-          (tvdouble(times.gtkbits)              \
-           + tvdouble(times.menuupdate)         \
-           + tvdouble(times.new_widgets)        \
-           + tvdouble(times.undisplay)          \
-           + tvdouble(times.colors)             \
-           + tvdouble(times.markers)            \
-           + tvdouble(times.location)           \
-           + tvdouble(times.selection)));       \
-} while(0)
-#else
-#define BEGIN(WHAT) do {
-#define END(WHAT) } while(0)
-#define INIT() ((void)0)
-#define REPORT() ((void)0)
-#endif
-
-WT(label);
-WT(event_box);
-WT(menu);
-WT(menu_item);
-WT(layout);
-WT(vbox);
-WT(arrow);
-WT(hbox);
-WT(button);
-WT(image);
-WT(entry);
-
-/* Types */
-
-struct choosenode;
-
-/** @brief Accumulated information about the tree widget */
-struct displaydata {
-  /** @brief Maximum width required */
-  guint width;
-  /** @brief Maximum height required */
-  guint height;
-};
-
-/* instantiate the node vector type */
-
-VECTOR_TYPE(nodevector, struct choosenode *, xrealloc);
-
-/** @brief Signature of function called when a choosenode is filled */
-typedef void (when_filled_callback)(struct choosenode *cn,
-                                    void *wfu);
-
-/** @brief One node in the virtual filesystem */
-struct choosenode {
-  struct choosenode *parent;            /**< @brief parent node */
-  const char *path;                     /**< @brief full path or 0  */
-  const char *sort;                     /**< @brief sort key */
-  const char *display;                  /**< @brief display name */
-  int pending;                          /**< @brief pending resolve queries */
-  unsigned flags;
-#define CN_EXPANDABLE 0x0001            /**< @brief node is expandable */
-#define CN_EXPANDED 0x0002              /**< @brief node is expanded
-                                         *
-                                         * Expandable items are directories;
-                                         * non-expandable ones are files. */
-#define CN_DISPLAYED 0x0004             /**< @brief widget is displayed in layout */
-#define CN_SELECTED 0x0008              /**< @brief node is selected */
-#define CN_GETTING_FILES 0x0010         /**< @brief files inbound */
-#define CN_RESOLVING_FILES 0x0020       /**< @brief resolved files inbound */
-#define CN_GETTING_DIRS 0x0040          /**< @brief directories inbound */
-#define CN_GETTING_ANY 0x0070           /**< @brief getting something */
-#define CN_CONTINGENT 0x0080            /**< @brief expansion contingent on search */
-  struct nodevector children;           /**< @brief vector of children */
-  void (*fill)(struct choosenode *);    /**< @brief request child fill or 0 for leaf */
-  GtkWidget *container;                 /**< @brief the container for this row */
-  GtkWidget *hbox;                      /**< @brief the hbox for this row */
-  GtkWidget *arrow;                     /**< @brief arrow widget or 0 */
-  GtkWidget *label;                     /**< @brief text label for this node */
-  GtkWidget *marker;                    /**< @brief queued marker */
-
-  when_filled_callback *whenfilled;     /**< @brief called when filled or 0 */
-  void *wfu;                            /**< @brief passed to @c whenfilled */
-  int ymin;                             /**< @brief least Y value */
-  int ymax;                             /**< @brief greatest Y value */
-};
-
-/** @brief One item in the popup menu */
-struct choose_menuitem {
-  /* Parameters */
-  const char *name;                     /**< @brief name */
-
-  /* Callbacks */
-  void (*activate)(GtkMenuItem *menuitem, gpointer user_data);
-  /**< @brief Called to activate the menu item.
-   *
-   * @p user_data is the choosenode the mouse pointer is over. */
-
-  gboolean (*sensitive)(struct choosenode *cn);
-  /* @brief Called to determine whether the menu item should be sensitive.
-   *
-   * TODO? */
-
-  /* State */
-  gulong handlerid;                     /**< @brief signal handler ID */
-  GtkWidget *w;                         /**< @brief menu item widget */
-};
-
-/* Variables */
-
-static GtkWidget *chooselayout;
-static GtkAdjustment *vadjust;
-static GtkWidget *searchentry;          /**< @brief search terms */
-static GtkWidget *nextsearch;           /**< @brief next search result */
-static GtkWidget *prevsearch;           /**< @brief previous search result */
-static struct choosenode *root;
-static GtkWidget *track_menu;           /**< @brief track popup menu */
-static GtkWidget *dir_menu;             /**< @brief directory popup menu */
-static struct choosenode *last_click;   /**< @brief last clicked node for selection */
-static int files_visible;               /**< @brief total files visible */
-static int files_selected;              /**< @brief total files selected */
-static int gets_in_flight;              /**< @brief total gets in flight */
-static int search_in_flight;            /**< @brief a search is underway */
-static int search_obsolete;             /**< @brief the current search is void */
-static char **searchresults;            /**< @brief search results */
-static int nsearchresults;              /**< @brief number of results */
-static int nsearchvisible;      /**< @brief number of search results visible */
-static struct hash *searchhash;         /**< @brief hash of search results */
-static struct progress_window *spw;     /**< @brief progress window */
-static struct choosenode **searchnodes; /**< @brief choosenodes of search results */
-static int suppress_redisplay;          /**< @brief suppress redisplay */
-
-/* Forward Declarations */
-
-static void clear_children(struct choosenode *cn);
-static struct choosenode *newnode(struct choosenode *parent,
-                                  const char *path,
-                                  const char *display,
-                                  const char *sort,
-                                  unsigned flags,
-                                  void (*fill)(struct choosenode *));
-static void fill_root_node(struct choosenode *cn);
-static void fill_directory_node(struct choosenode *cn);
-static void got_files(void *v, int nvec, char **vec);
-static void got_resolved_file(void *v, const char *track);
-static void got_dirs(void *v, int nvec, char **vec);
-
-static void expand_node(struct choosenode *cn, int contingent);
-static void contract_node(struct choosenode *cn);
-static void updated_node(struct choosenode *cn, int redisplay,
-                         const char *why);
-
-static void display_selection(struct choosenode *cn);
-static void clear_selection(struct choosenode *cn);
-
-static void redisplay_tree(const char *why);
-static struct displaydata display_tree(struct choosenode *cn, int x, int y);
-static void undisplay_tree(struct choosenode *cn);
-static void initiate_search(void);
-static void expand_from(struct choosenode *cn);
-static struct choosenode *first_search_result(struct choosenode *cn);
-
-static void clicked_choosenode(GtkWidget attribute((unused)) *widget,
-                               GdkEventButton *event,
-                               gpointer user_data);
-
-static void activate_track_play(GtkMenuItem *menuitem, gpointer user_data);
-static void activate_track_properties(GtkMenuItem *menuitem, gpointer user_data);
-
-static gboolean sensitive_track_play(struct choosenode *cn);
-static gboolean sensitive_track_properties(struct choosenode *cn);
-
-static void activate_dir_play(GtkMenuItem *menuitem, gpointer user_data);
-static void activate_dir_properties(GtkMenuItem *menuitem, gpointer user_data);
-static void activate_dir_select(GtkMenuItem *menuitem, gpointer user_data);
-
-static gboolean sensitive_dir_play(struct choosenode *cn);
-static gboolean sensitive_dir_properties(struct choosenode *cn);
-static gboolean sensitive_dir_select(struct choosenode *cn);
-
-/** @brief Track menu items */
-static struct choose_menuitem track_menuitems[] = {
-  { "Play track", activate_track_play, sensitive_track_play, 0, 0 },
-  { "Track properties", activate_track_properties, sensitive_track_properties, 0, 0 },
-  { 0, 0, 0, 0, 0 }
-};
-
-/** @brief Directory menu items */
-static struct choose_menuitem dir_menuitems[] = {
-  { "Play all tracks", activate_dir_play, sensitive_dir_play, 0, 0 },
-  { "Track properties", activate_dir_properties, sensitive_dir_properties, 0, 0 },
-  { "Select all tracks", activate_dir_select, sensitive_dir_select, 0, 0 },
-  { 0, 0, 0, 0, 0 }
-};
-
-/* Maintaining the data structure ------------------------------------------ */
-
-static char *cnflags(const struct choosenode *cn) {
-  unsigned f = cn->flags, n;
-  struct dynstr d[1];
-  
-  static const char *bits[] = {
-    "expandable",
-    "expanded",
-    "displayed",
-    "selected",
-    "getting_files",
-    "resolving_files",
-    "getting_dirs",
-    "contingent"
-  };
-#define NBITS (sizeof bits / sizeof *bits)
-
-  dynstr_init(d);
-  if(!f)
-    dynstr_append(d, '0');
-  else {
-    for(n = 0; n < NBITS; ++n) {
-      const unsigned bit = 1 << n;
-      if(f & bit) {
-        if(d->nvec)
-          dynstr_append(d, '|');
-        dynstr_append_string(d, bits[n]);
-        f ^= bit;
-      }
-    }
-    if(f) {
-      char buf[32];
-      if(d->nvec)
-        dynstr_append(d, '|');
-      sprintf(buf, "%#x", f);
-      dynstr_append_string(d, buf);
-    }
-  }
-  dynstr_terminate(d);
-  return d->vec;
-}
-
-/** @brief Create a new node */
-static struct choosenode *newnode(struct choosenode *parent,
-                                  const char *path,
-                                  const char *display,
-                                  const char *sort,
-                                  unsigned flags,
-                                  void (*fill)(struct choosenode *)) {
-  struct choosenode *const n = xmalloc(sizeof *n);
-
-  D(("newnode %s %s", path, display));
-  if(flags & CN_EXPANDABLE)
-    assert(fill);
-  else
-    assert(!fill);
-  n->parent = parent;
-  n->path = path;
-  n->display = display;
-  n->sort = sort;
-  n->flags = flags;
-  nodevector_init(&n->children);
-  n->fill = fill;
-  if(parent)
-    nodevector_append(&parent->children, n);
-  return n;
-}
-
-/** @brief Called when a node has been filled
+ * We now use an ordinary GtkTreeStore/GtkTreeView.
  *
- * Response for calling @c whenfilled.
- */
-static void filled(struct choosenode *cn) {
-  when_filled_callback *const whenfilled = cn->whenfilled;
-
-  if(whenfilled) {
-    cn->whenfilled = 0;
-    whenfilled(cn, cn->wfu);
-  }
-  if(nsearchvisible < nsearchresults) {
-    /* There is still search expansion work to do */
-    D(("filled %s %d/%d", cn->path, nsearchvisible, nsearchresults));
-    expand_from(cn);
-  }
-  if(gets_in_flight == 0 && nsearchvisible < nsearchresults)
-    expand_from(root);
-}
-
-/** @brief Fill the root */
-static void fill_root_node(struct choosenode *cn) {
-  struct callbackdata *cbd;
-
-  D(("fill_root_node"));
-  clear_children(cn);
-  /* More de-duping possible here */
-  if(cn->flags & CN_GETTING_ANY)
-    return;
-  gtk_label_set_text(GTK_LABEL(report_label), "getting files");
-  cbd = xmalloc(sizeof *cbd);
-  cbd->u.choosenode = cn;
-  disorder_eclient_dirs(client, got_dirs, "", 0, cbd);
-  cbd = xmalloc(sizeof *cbd);
-  cbd->u.choosenode = cn;
-  disorder_eclient_files(client, got_files, "", 0, cbd);
-  cn->flags |= CN_GETTING_FILES|CN_GETTING_DIRS;
-  gets_in_flight += 2;
-}
-
-/** @brief Delete all the widgets owned by @p cn */
-static void delete_cn_widgets(struct choosenode *cn) {
-  if(cn->arrow) {
-    DW(arrow);
-    gtk_widget_destroy(cn->arrow);
-    cn->arrow = 0;
-  }
-  if(cn->label) {
-    DW(label);
-    gtk_widget_destroy(cn->label);
-    cn->label = 0;
-  }
-  if(cn->marker) {
-    DW(image);
-    gtk_widget_destroy(cn->marker);
-    cn->marker = 0;
-  }
-  if(cn->hbox) {
-    DW(hbox);
-    gtk_widget_destroy(cn->hbox);
-    cn->hbox = 0;
-  }
-  if(cn->container) {
-    DW(event_box);
-    gtk_widget_destroy(cn->container);
-    cn->container = 0;
-  }
-}
-
-/** @brief Recursively clear all the children of @p cn
+ * We don't want to pull the entire tree in memory, but we want directories to
+ * show up as having children.  Therefore we give directories a placeholder
+ * child and replace their children when they are opened.  Placeholders have
+ * TRACK_COLUMN="" and ISFILE_COLUMN=FALSE (so that they don't get check boxes,
+ * lengths, etc).
  *
- * All the widgets at or below @p cn are deleted.  All choosenodes below
- * it are emptied. i.e. we prune the tree at @p cn.
+ * TODO:
+ * - sweep up contracted nodes, replacing their content with a placeholder
  */
-static void clear_children(struct choosenode *cn) {
-  int n;
 
-  D(("clear_children %s", cn->path));
-  /* Recursively clear subtrees */
-  for(n = 0; n < cn->children.nvec; ++n) {
-    clear_children(cn->children.vec[n]);
-    delete_cn_widgets(cn->children.vec[n]);
-  }
-  cn->children.nvec = 0;
-}
+#include "disobedience.h"
+#include "choose.h"
+#include <gdk/gdkkeysyms.h>
 
-/** @brief Called with a list of files just below some node */
-static void got_files(void *v, int nvec, char **vec) {
-  struct callbackdata *cbd = v;
-  struct choosenode *cn = cbd->u.choosenode;
-  int n;
+/** @brief The current selection tree */
+GtkTreeStore *choose_store;
 
-  D(("got_files %d files for %s %s", nvec, cn->path, cnflags(cn)));
-  /* Complicated by the need to resolve aliases.  We can save a bit of effort
-   * by re-using cbd though. */
-  cn->flags &= ~CN_GETTING_FILES;
-  --gets_in_flight;
-  if((cn->pending = nvec)) {
-    cn->flags |= CN_RESOLVING_FILES;
-    for(n = 0; n < nvec; ++n) {
-      disorder_eclient_resolve(client, got_resolved_file, vec[n], cbd);
-      ++gets_in_flight;
-    }
-  }
-  /* If there are no files and the directories are all read by now, we're
-   * done */
-  if(!(cn->flags & CN_GETTING_ANY))
-    filled(cn);
-  if(!gets_in_flight)
-    redisplay_tree("got_files");
-}
+/** @brief The view onto the selection tree */
+GtkWidget *choose_view;
 
-/** @brief Called with an alias resolved filename */
-static void got_resolved_file(void *v, const char *track) {
-  struct callbackdata *cbd = v;
-  struct choosenode *cn = cbd->u.choosenode, *file_cn;
+/** @brief The selection tree's selection */
+GtkTreeSelection *choose_selection;
 
-  D(("resolved %s %s %d left", cn->path, cnflags(cn), cn->pending - 1));
-  /* TODO as below */
-  file_cn = newnode(cn, track,
-                    trackname_transform("track", track, "display"),
-                    trackname_transform("track", track, "sort"),
-                    0/*flags*/, 0/*fill*/);
-  --gets_in_flight;
-  /* Only bother updating when we've got the lot */
-  if(--cn->pending == 0) {
-    cn->flags &= ~CN_RESOLVING_FILES;
-    updated_node(cn, gets_in_flight == 0, "got_resolved_file");
-    if(!(cn->flags & CN_GETTING_ANY))
-      filled(cn);
-  }
-}
+/** @brief Count of file listing operations in flight */
+static int choose_list_in_flight;
 
-/** @brief Called with a list of directories just below some node */
-static void got_dirs(void *v, int nvec, char **vec) {
-  struct callbackdata *cbd = v;
-  struct choosenode *cn = cbd->u.choosenode;
-  int n;
+/** @brief If nonzero autocollapse column won't be set */
+static int choose_suppress_set_autocollapse;
 
-  D(("got_dirs %d dirs for %s %s", nvec, cn->path, cnflags(cn)));
-  /* TODO this depends on local configuration for trackname_transform().
-   * This will work, since the defaults are now built-in, but it'll be
-   * (potentially) different to the server's configured settings.
-   *
-   * Really we want a variant of files/dirs that produces both the
-   * raw filename and the transformed name for a chosen context.
-   */
-  --gets_in_flight;
-  for(n = 0; n < nvec; ++n)
-    newnode(cn, vec[n],
-            trackname_transform("dir", vec[n], "display"),
-            trackname_transform("dir", vec[n], "sort"),
-            CN_EXPANDABLE, fill_directory_node);
-  updated_node(cn, gets_in_flight == 0, "got_dirs");
-  cn->flags &= ~CN_GETTING_DIRS;
-  if(!(cn->flags & CN_GETTING_ANY))
-    filled(cn);
+static char *choose_get_string(GtkTreeIter *iter, int column) {
+  gchar *gs;
+  gtk_tree_model_get(GTK_TREE_MODEL(choose_store), iter,
+                     column, &gs,
+                     -1);
+  char *s = xstrdup(gs);
+  g_free(gs);
+  return s;
 }
-  
-/** @brief Fill a child node */
-static void fill_directory_node(struct choosenode *cn) {
-  struct callbackdata *cbd;
 
-  D(("fill_directory_node %s", cn->path));
-  /* TODO: caching */
-  if(cn->flags & CN_GETTING_ANY)
-    return;
-  assert(report_label != 0);
-  gtk_label_set_text(GTK_LABEL(report_label), "getting files");
-  clear_children(cn);
-  cbd = xmalloc(sizeof *cbd);
-  cbd->u.choosenode = cn;
-  disorder_eclient_dirs(client, got_dirs, cn->path, 0, cbd);
-  cbd = xmalloc(sizeof *cbd);
-  cbd->u.choosenode = cn;
-  disorder_eclient_files(client, got_files, cn->path, 0, cbd);
-  cn->flags |= CN_GETTING_FILES|CN_GETTING_DIRS;
-  gets_in_flight += 2;
+char *choose_get_track(GtkTreeIter *iter) {
+  char *s = choose_get_string(iter, TRACK_COLUMN);
+  return *s ? s : 0;                    /* Placeholder -> NULL */
 }
 
-/** @brief Expand a node */
-static void expand_node(struct choosenode *cn, int contingent) {
-  D(("expand_node %s %d %s", cn->path, contingent, cnflags(cn)));
-  assert(cn->flags & CN_EXPANDABLE);
-  /* If node is already expanded do nothing. */
-  if(cn->flags & CN_EXPANDED) return;
-  /* We mark the node as expanded and request that it fill itself.  When it has
-   * completed it will called updated_node() and we can redraw at that
-   * point. */
-  cn->flags |= CN_EXPANDED;
-  if(contingent)
-    cn->flags |= CN_CONTINGENT;
-  else
-    cn->flags &= ~CN_CONTINGENT;
-  /* If this node is not contingently expanded, mark all its parents back to
-   * the root as not contingent either, so they won't be contracted when the
-   * search results change */
-  if(!contingent) {
-    struct choosenode *cnp;
-
-    for(cnp = cn->parent; cnp; cnp = cnp->parent)
-      cnp->flags &= ~CN_CONTINGENT;
-  }
-  /* TODO: visual feedback */
-  cn->fill(cn);
+char *choose_get_sort(GtkTreeIter *iter) {
+  return choose_get_string(iter, SORT_COLUMN);
 }
 
-/** @brief Make sure all the search results below @p cn are expanded
- * @param cn Node to start at
- */
-static void expand_from(struct choosenode *cn) {
-  int n;
-
-  if(nsearchvisible == nsearchresults)
-    /* We're done */
-    return;
-  /* Are any of the search tracks at/below this point? */
-  if(!(cn == root || hash_find(searchhash, cn->path)))
-    return;
-  D(("expand_from %d/%d visible %s", 
-     nsearchvisible, nsearchresults, cn->path));
-  if(cn->flags & CN_EXPANDABLE) {
-    if(cn->flags & CN_EXPANDED)
-      /* This node is marked as expanded already.  children.nvec might be 0,
-       * indicating that expansion is still underway.  We should get another
-       * callback when it is expanded. */
-      for(n = 0; n < cn->children.nvec && gets_in_flight < 10; ++n)
-        expand_from(cn->children.vec[n]);
-    else {
-      /* This node is not expanded yet */
-      expand_node(cn, 1);
-    }
-  } else {
-    /* This is an actual search result */
-    ++nsearchvisible;
-    progress_window_progress(spw, nsearchvisible, nsearchresults);
-    if(nsearchvisible == nsearchresults) {
-      if(suppress_redisplay) {
-        suppress_redisplay = 0;
-        redisplay_tree("all search results visible");
-      }
-      /* We've got the lot.  We make sure the first result is visible. */
-      cn = first_search_result(root);
-      gtk_adjustment_clamp_page(vadjust, cn->ymin, cn->ymax);
-    }
-  }
+char *choose_get_display(GtkTreeIter *iter) {
+  return choose_get_string(iter, NAME_COLUMN);
 }
 
-/** @brief Contract all contingently expanded nodes below @p cn */
-static void contract_contingent(struct choosenode *cn) {
-  int n;
-
-  if(cn->flags & CN_CONTINGENT)
-    contract_node(cn);
-  else
-    for(n = 0; n < cn->children.nvec; ++n)
-      contract_contingent(cn->children.vec[n]);
+int choose_is_file(GtkTreeIter *iter) {
+  gboolean isfile;
+  gtk_tree_model_get(GTK_TREE_MODEL(choose_store), iter,
+                     ISFILE_COLUMN, &isfile,
+                     -1);
+  return isfile;
 }
 
-/** @brief Contract a node */
-static void contract_node(struct choosenode *cn) {
-  D(("contract_node %s", cn->path));
-  assert(cn->flags & CN_EXPANDABLE);
-  /* If node is already contracted do nothing. */
-  if(!(cn->flags & CN_EXPANDED)) return;
-  cn->flags &= ~(CN_EXPANDED|CN_CONTINGENT);
-  /* Clear selection below this node */
-  clear_selection(cn);
-  /* Zot children.  We never used to do this but the result would be that over
-   * time you'd end up with the entire tree pulled into memory.  If the server
-   * is over a slow network it will make interactivity slightly worse; if
-   * anyone complains we can make it an option. */
-  clear_children(cn);
-  /* We can contract a node immediately. */
-  redisplay_tree("contract_node");
+int choose_is_dir(GtkTreeIter *iter) {
+  gboolean isfile;
+  gtk_tree_model_get(GTK_TREE_MODEL(choose_store), iter,
+                     ISFILE_COLUMN, &isfile,
+                     -1);
+  if(isfile)
+    return FALSE;
+  return !choose_is_placeholder(iter);
 }
 
-/** @brief qsort() callback for ordering choosenodes */
-static int compare_choosenode(const void *av, const void *bv) {
-  const struct choosenode *const *aa = av, *const *bb = bv;
-  const struct choosenode *a = *aa, *b = *bb;
-
-  return compare_tracks(a->sort, b->sort,
-                       a->display, b->display,
-                       a->path, b->path);
+int choose_is_placeholder(GtkTreeIter *iter) {
+  return choose_get_string(iter, TRACK_COLUMN)[0] == 0;
 }
 
-/** @brief Called when an expandable node is updated.   */
-static void updated_node(struct choosenode *cn, int redisplay,
-                         const char *why) {
-  D(("updated_node %s", cn->path));
-  assert(cn->flags & CN_EXPANDABLE);
-  /* It might be that the node has been de-expanded since we requested the
-   * update.  In that case we ignore this notification. */
-  if(!(cn->flags & CN_EXPANDED)) return;
-  /* Sort children */
-  qsort(cn->children.vec, cn->children.nvec, sizeof (struct choosenode *),
-        compare_choosenode);
-  if(redisplay) {
-    char whywhy[1024];
-
-    snprintf(whywhy, sizeof whywhy, "updated_node %s", why);
-    redisplay_tree(whywhy);
-  }
+int choose_can_autocollapse(GtkTreeIter *iter) {
+  gboolean autocollapse;
+  gtk_tree_model_get(GTK_TREE_MODEL(choose_store), iter,
+                     AUTOCOLLAPSE_COLUMN, &autocollapse,
+                     -1);
+  return autocollapse;
 }
 
-/* Searching --------------------------------------------------------------- */
-
-/** @brief Return true if @p track is a search result
+/** @brief Remove node @p it and all its children
+ * @param Iterator, updated to point to next
+ * @return True if iterator remains valid
  *
- * In particular the return value is one more than the index of the track
- * @p searchresults.
+ * TODO is this necessary?  gtk_tree_store_remove() does not document what
+ * happens to children.
  */
-static int is_search_result(const char *track) {
-  void *r;
-
-  if(searchhash && (r = hash_find(searchhash, track)))
-    return 1 + *(int *)r;
-  else
-    return 0;
+static gboolean choose_remove_node(GtkTreeIter *it) {
+  GtkTreeIter child[1];
+  gboolean childv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
+                                                 child,
+                                                 it);
+  while(childv)
+    childv = choose_remove_node(child);
+  return gtk_tree_store_remove(choose_store, it);
+}
+
+/** @brief Update length and state fields */
+static gboolean choose_set_state_callback(GtkTreeModel attribute((unused)) *model,
+                                          GtkTreePath attribute((unused)) *path,
+                                          GtkTreeIter *it,
+                                          gpointer attribute((unused)) data) {
+  if(choose_is_file(it)) {
+    const char *track = choose_get_track(it);
+    const long l = namepart_length(track);
+    char length[64];
+    if(l > 0)
+      byte_snprintf(length, sizeof length, "%ld:%02ld", l / 60, l % 60);
+    else
+      length[0] = 0;
+    gtk_tree_store_set(choose_store, it,
+                       LENGTH_COLUMN, length,
+                       STATE_COLUMN, queued(track),
+                       -1);
+    if(choose_is_search_result(track))
+      gtk_tree_store_set(choose_store, it,
+                         BG_COLUMN, SEARCH_RESULT_BG,
+                         FG_COLUMN, SEARCH_RESULT_FG,
+                         -1);
+    else
+      gtk_tree_store_set(choose_store, it,
+                         BG_COLUMN, (char *)0,
+                         FG_COLUMN, (char *)0,
+                         -1);
+  }
+  return FALSE;                         /* continue walking */
 }
 
-/** @brief Return the first search result at or below @p cn */
-static struct choosenode *first_search_result(struct choosenode *cn) {
-  int n;
-  struct choosenode *r;
-
-  if(cn->flags & CN_EXPANDABLE) {
-    for(n = 0; n < cn->children.nvec; ++n)
-      if((r = first_search_result(cn->children.vec[n])))
-        return r;
-  } else if(is_search_result(cn->path))
-    return cn;
-  return 0;
+/** @brief Called when the queue or playing track change */
+static void choose_set_state(const char attribute((unused)) *event,
+                             void attribute((unused)) *eventdata,
+                             void attribute((unused)) *callbackdata) {
+  gtk_tree_model_foreach(GTK_TREE_MODEL(choose_store),
+                         choose_set_state_callback,
+                         NULL);
 }
 
-/** @brief Called with a list of search results
+/** @brief (Re-)populate a node
+ * @param parent_ref Node to populate or NULL to fill root
+ * @param nvec Number of children to add
+ * @param vec Children
+ * @param files 1 if children are files, 0 if directories
  *
- * This is called from eclient with a (possibly empty) list of search results,
- * and also from initiate_seatch with an always empty list to indicate that
- * we're not searching for anything in particular. */
-static void search_completed(void attribute((unused)) *v,
-                             int nvec, char **vec) {
-  int n;
-  char *s;
-
-  search_in_flight = 0;
-  /* Contract any choosenodes that were only expanded to show search
-   * results */
-  suppress_redisplay = 1;
-  contract_contingent(root);
-  suppress_redisplay = 0;
-  if(search_obsolete) {
-    /* This search has been obsoleted by user input since it started.
-     * Therefore we throw away the result and search again. */
-    search_obsolete = 0;
-    initiate_search();
+ * Adjusts the set of files (or directories) below @p parent_ref to match those
+ * listed in @p nvec and @p vec.
+ *
+ * @parent_ref will be destroyed.
+ */
+static void choose_populate(GtkTreeRowReference *parent_ref,
+                            int nvec, char **vec,
+                            int isfile) {
+  const char *type = isfile ? "track" : "dir";
+  //fprintf(stderr, "%d new children of type %s\n", nvec, type);
+  if(!nvec)
+    goto skip;
+  /* Compute parent_* */
+  GtkTreeIter pit[1], *parent_it;
+  GtkTreePath *parent_path;
+  if(parent_ref) {
+    parent_path = gtk_tree_row_reference_get_path(parent_ref);
+    parent_it = pit;
+    gboolean pitv = gtk_tree_model_get_iter(GTK_TREE_MODEL(choose_store),
+                                            pit, parent_path);
+    assert(pitv);
+    /*fprintf(stderr, "choose_populate %s: parent path is [%s]\n",
+            type,
+            gtk_tree_path_to_string(parent_path));*/
   } else {
-    /* Stash the search results */
-    searchresults = vec;
-    nsearchresults = nvec;
-    if(nvec) {
-      /* Create a new search hash for fast identification of results */
-      searchhash = hash_new(sizeof(int));
-      for(n = 0; n < nvec; ++n) {
-        int *const ip = xmalloc(sizeof (int *));
-        static const int minus_1 = -1;
-        *ip = n;
-        /* The filename itself lives in the hash */
-        hash_add(searchhash, vec[n], ip, HASH_INSERT_OR_REPLACE);
-        /* So do its ancestor directories */
-        for(s = vec[n] + 1; *s; ++s) {
-          if(*s == '/') {
-            *s = 0;
-            hash_add(searchhash, vec[n], &minus_1, HASH_INSERT_OR_REPLACE);
-            *s = '/';
-          }
-        }
-      }
-      /* We don't yet know that the results are visible */
-      nsearchvisible = 0;
-      if(spw) {
-        progress_window_progress(spw, 0, 0);
-        spw = 0;
-      }
-      if(nsearchresults > 50)
-        spw = progress_window_new("Fetching search results");
-      /* Initiate expansion */
-      expand_from(root);
-      /* The search results buttons are usable */
-      gtk_widget_set_sensitive(nextsearch, 1);
-      gtk_widget_set_sensitive(prevsearch, 1);
-      suppress_redisplay = 1;           /* avoid lots of redisplays */
+    parent_path = 0;
+    parent_it = 0;
+    /*fprintf(stderr, "choose_populate %s: populating the root\n",
+            type);*/
+  }
+  /* Both td[] and the current node set are sorted so we can do a single linear
+   * pass to insert new nodes and remove unwanted ones.  The total performance
+   * may be worse than linear depending on the performance of GTK+'s insert and
+   * delete operations. */
+  //fprintf(stderr, "sorting tracks\n");
+  struct tracksort_data *td = tracksort_init(nvec, vec, type);
+  GtkTreeIter it[1];
+  gboolean itv = gtk_tree_model_iter_children(GTK_TREE_MODEL(choose_store),
+                                              it,
+                                              parent_it);
+  int inserted = 0, deleted_placeholder = 0;
+  //fprintf(stderr, "inserting tracks type=%s\n", type);
+  while(nvec > 0 || itv) {
+    /*fprintf(stderr, "td[] = %s, it=%s [%s]\n",
+            nvec > 0 ? td->track : "(none)",
+            itv ? choose_get_track(it) : "(!itv)",
+            itv ? (choose_is_file(it) ? "file" : "dir") : "");*/
+    enum { INSERT, DELETE, SKIP_TREE, SKIP_BOTH } action;
+    const char *track = itv ? choose_get_track(it) : 0;
+    if(itv && !track) {
+      //fprintf(stderr, " placeholder\n");
+      action = DELETE;
+      ++deleted_placeholder;
+    } else if(nvec > 0 && itv) {
+      /* There's both a tree row and a td[] entry */
+      const int cmp = compare_tracks(td->sort, choose_get_sort(it),
+                                     td->display, choose_get_display(it),
+                                     td->track, track);
+      //fprintf(stderr, " cmp=%d\n", cmp);
+      if(cmp < 0)
+        /* td < it, so we insert td before it */
+        action = INSERT;
+      else if(cmp > 0) {
+        /* td > it, so we must either delete it (if the same type) or skip it */
+        if(choose_is_file(it) == isfile)
+          action = DELETE;
+        else
+          action = SKIP_TREE;
+      } else
+        /* td = it, so we step past both */
+        action = SKIP_BOTH;
+    } else if(nvec > 0) {
+      /* We've reached the end of the tree rows, but new have tracks left in
+       * td[] */
+      //fprintf(stderr, " inserting\n");
+      action = INSERT;
     } else {
-      searchhash = 0;                   /* for the gc */
-      redisplay_tree("no search results"); /* remove search markers */
-      /* The search results buttons are not usable */
-      gtk_widget_set_sensitive(nextsearch, 0);
-      gtk_widget_set_sensitive(prevsearch, 0);
+      /* We've reached the end of the new tracks from td[], but there are
+       * further tracks in the tree */
+      //fprintf(stderr, " deleting\n");
+      if(choose_is_file(it) == isfile)
+        action = DELETE;
+      else
+        action = SKIP_TREE;
+    }
+    
+    switch(action) {
+    case INSERT: {
+      //fprintf(stderr, " INSERT %s\n", td->track);
+      /* Insert a new row from td[] before it, or at the end if it is no longer
+       * valid */
+      GtkTreeIter child[1];
+      gtk_tree_store_insert_before(choose_store,
+                                   child, /* new row */
+                                   parent_it, /* parent */
+                                   itv ? it : NULL); /* successor */
+      gtk_tree_store_set(choose_store, child,
+                         NAME_COLUMN, td->display,
+                         ISFILE_COLUMN, isfile,
+                         TRACK_COLUMN, td->track,
+                         SORT_COLUMN, td->sort,
+                         AUTOCOLLAPSE_COLUMN, FALSE,
+                         -1);
+      /* Update length and state; we expect this to kick off length lookups
+       * rather than necessarily get the right value the first time round. */
+      choose_set_state_callback(0, 0, child, 0);
+      /* If we inserted a directory, insert a placeholder too, so it appears to
+       * have children; it will be deleted when we expand the directory. */
+      if(!isfile) {
+        //fprintf(stderr, "  inserting a placeholder\n");
+        GtkTreeIter placeholder[1];
+
+        gtk_tree_store_append(choose_store, placeholder, child);
+        gtk_tree_store_set(choose_store, placeholder,
+                           NAME_COLUMN, "Waddling...",
+                           TRACK_COLUMN, "",
+                           ISFILE_COLUMN, FALSE,
+                           -1);
+      }
+      ++inserted;
+      ++td;
+      --nvec;
+      break;
+    }
+    case SKIP_BOTH:
+      //fprintf(stderr, " SKIP_BOTH\n");
+      ++td;
+      --nvec;
+      /* fall thru */
+    case SKIP_TREE:
+      //fprintf(stderr, " SKIP_TREE\n");
+      itv = gtk_tree_model_iter_next(GTK_TREE_MODEL(choose_store), it);
+      break;
+    case DELETE:
+      //fprintf(stderr, " DELETE\n");
+      itv = choose_remove_node(it);
+      break;
     }
   }
-}
-
-/** @brief Initiate a search 
- *
- * If a search is underway we set @ref search_obsolete and restart the search
- * in search_completed() above.
- */
-static void initiate_search(void) {
-  char *terms, *e;
-
-  /* Find out what the user is after */
-  terms = xstrdup(gtk_entry_get_text(GTK_ENTRY(searchentry)));
-  /* Strip leading and trailing space */
-  while(*terms == ' ') ++terms;
-  e = terms + strlen(terms);
-  while(e > terms && e[-1] == ' ') --e;
-  *e = 0;
-  /* If a search is already underway then mark it as obsolete.  We'll revisit
-   * when it returns. */
-  if(search_in_flight) {
-    search_obsolete = 1;
-    return;
-  }
-  if(*terms) {
-    /* There's still something left.  Initiate the search. */
-    if(disorder_eclient_search(client, search_completed, terms, 0)) {
-      /* The search terms are bad!  We treat this as if there were no search
-       * terms at all.  Some kind of feedback would be handy. */
-      fprintf(stderr, "bad terms [%s]\n", terms); /* TODO */
-      search_completed(0, 0, 0);
-    } else {
-      search_in_flight = 1;
+  /*fprintf(stderr, "inserted=%d deleted_placeholder=%d\n\n",
+          inserted, deleted_placeholder);*/
+  if(parent_ref) {
+    /* If we deleted a placeholder then we must re-expand the row */
+    if(deleted_placeholder) {
+      ++choose_suppress_set_autocollapse;
+      gtk_tree_view_expand_row(GTK_TREE_VIEW(choose_view), parent_path, FALSE);
+      --choose_suppress_set_autocollapse;
     }
-  } else {
-    /* No search terms - we want to see all tracks */
-    search_completed(0, 0, 0);
+    gtk_tree_row_reference_free(parent_ref);
+    gtk_tree_path_free(parent_path);
+  }
+skip:
+  /* We only notify others that we've inserted tracks when there are no more
+   * insertions pending, so that they don't have to keep track of how many
+   * requests they've made.  */
+  if(--choose_list_in_flight == 0) {
+    /* Notify interested parties that we inserted some tracks, AFTER making
+     * sure that the row is properly expanded */
+    //fprintf(stderr, "raising choose-more-tracks\n");
+    event_raise("choose-more-tracks", 0);
+  }
+  //fprintf(stderr, "choose_list_in_flight -> %d-\n", choose_list_in_flight);
+}
+
+static void choose_dirs_completed(void *v,
+                                  const char *err,
+                                  int nvec, char **vec) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
   }
+  choose_populate(v, nvec, vec, 0/*!isfile*/);
 }
 
-/** @brief Called when the cancel search button is clicked */
-static void clearsearch_clicked(GtkButton attribute((unused)) *button,
-                                gpointer attribute((unused)) userdata) {
-  gtk_entry_set_text(GTK_ENTRY(searchentry), "");
-}
-
-/** @brief Called when the 'next search result' button is clicked */
-static void next_clicked(GtkButton attribute((unused)) *button,
-                         gpointer attribute((unused)) userdata) {
-  /* We want to find the highest (lowest ymax) track that is below the current
-   * visible range */
-  int n;
-  const gdouble bottom = gtk_adjustment_get_value(vadjust) + vadjust->page_size;
-  const struct choosenode *candidate = 0;
-
-  for(n = 0; n < nsearchresults; ++n) {
-    const struct choosenode *const cn = searchnodes[n];
-
-    if(cn
-       && cn->ymax > bottom
-       && (candidate == 0
-           || cn->ymax < candidate->ymax))
-      candidate = cn;
+static void choose_files_completed(void *v,
+                                   const char *err,
+                                   int nvec, char **vec) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
   }
-  if(candidate)
-    gtk_adjustment_clamp_page(vadjust, candidate->ymin, candidate->ymax);
+  choose_populate(v, nvec, vec, 1/*isfile*/);
 }
 
-/** @brief Called when the 'previous search result' button is clicked */
-static void prev_clicked(GtkButton attribute((unused)) *button,
-                         gpointer attribute((unused)) userdata) {
-  /* We want to find the lowest (greated ymax) track that is above the current
-   * visible range */
-  int n;
-  const gdouble top = gtk_adjustment_get_value(vadjust);
-  const struct choosenode *candidate = 0;
-
-  for(n = 0; n < nsearchresults; ++n) {
-    const struct choosenode *const cn = searchnodes[n];
-
-    if(cn
-       && cn->ymin  < top
-       && (candidate == 0
-           || cn->ymax > candidate->ymax))
-      candidate = cn;
-  }
-  if(candidate)
-    gtk_adjustment_clamp_page(vadjust, candidate->ymin, candidate->ymax);
+void choose_play_completed(void attribute((unused)) *v,
+                           const char *err) {
+  if(err)
+    popup_protocol_error(0, err);
 }
 
-/* Display functions ------------------------------------------------------- */
-
-/** @brief Update the display */
-static void redisplay_tree(const char *why) {
-  struct displaydata d;
-  guint oldwidth, oldheight;
-
-  D(("redisplay_tree %s", why));
-  if(suppress_redisplay) {
-    /*fprintf(stderr, "redisplay_tree %s suppressed\n", why);*/
+static void choose_state_toggled
+    (GtkCellRendererToggle attribute((unused)) *cell_renderer,
+     gchar *path_str,
+     gpointer attribute((unused)) user_data) {
+  GtkTreeIter it[1];
+  /* Identify the track */
+  gboolean itv =
+    gtk_tree_model_get_iter_from_string(GTK_TREE_MODEL(choose_store),
+                                        it,
+                                        path_str);
+  if(!itv)
     return;
-  }
-  if(gets_in_flight) {
-    /*fprintf(stderr, "redisplay_tree %s suppressed (gets_in_flight)\n", why);*/
+  if(!choose_is_file(it))
     return;
-  }
-  INIT();
-  BEGIN(total);
-  /*fprintf(stderr, "redisplay_tree %s   *** NOT SUPPRESSED ***\n", why);*/
-  /* We'll count these up empirically each time */
-  files_selected = 0;
-  files_visible = 0;
-  /* Correct the layout and find out how much space it uses */
-  MTAG_PUSH("display_tree");
-  searchnodes = nsearchresults ? xcalloc(nsearchresults, 
-                                         sizeof (struct choosenode *)) : 0;
-  d = display_tree(root, 0, 0);
-  MTAG_POP();
-
-  BEGIN(gtkbits);
-  /* We must set the total size or scrolling will not work (it wouldn't be hard
-   * for GtkLayout to figure it out for itself but presumably you're supposed
-   * to be able to have widgets off the edge of the layuot.)
-   *
-   * There is a problem: if we shrink the size then the part of the screen that
-   * is outside the new size but inside the old one is not updated.  I think
-   * this is arguably bug in GTK+ but it's easy to force a redraw if this
-   * region is nonempty.
-   */
-  gtk_layout_get_size(GTK_LAYOUT(chooselayout), &oldwidth, &oldheight);
-  if(oldwidth > d.width || oldheight > d.height)
-    gtk_widget_queue_draw(chooselayout);
-  gtk_layout_set_size(GTK_LAYOUT(chooselayout), d.width, d.height);
-  END(gtkbits);
-  /* Notify the main menu of any recent changes */
-  BEGIN(menuupdate);
-  menu_update(-1);
-  END(menuupdate);
-  END(total);
-  REPORT();
+  const char *track = choose_get_track(it);
+  if(queued(track))
+    return;
+  disorder_eclient_play(client, track, choose_play_completed, 0);
+  
 }
 
-/** @brief Recursive step for redisplay_tree()
- * @param cn Node to display
- * @param x X coordinate for @p cn
- * @param y Y coordinate for @p cn
+/** @brief (Re-)get the children of @p path
+ * @param path Path to target row
+ * @param iter Iterator pointing at target row
  *
- * Makes sure all displayed widgets from CN down exist and are in their proper
- * place and return the maximum space used.
+ * Called from choose_row_expanded() to make sure that the contents are present
+ * and from choose_refill_callback() to (re-)synchronize.
  */
-static struct displaydata display_tree(struct choosenode *cn, int x, int y) {
-  int n, aw;
-  GtkRequisition req;
-  struct displaydata d, cd;
-  GdkPixbuf *pb;
-  const int search_result = is_search_result(cn->path);
-  
-  D(("display_tree %s %d,%d", cn->path, x, y));
-
-  /* An expandable item contains an arrow and a text label.  When you press the
-   * button it flips its expand state.
-   *
-   * A non-expandable item has just a text label and no arrow.
-   */
-  if(!cn->container) {
-    BEGIN(new_widgets);
-    MTAG_PUSH("make_widgets_1");
-    /* Widgets need to be created */
-    NW(hbox);
-    cn->hbox = gtk_hbox_new(FALSE, 1);
-    if(cn->flags & CN_EXPANDABLE) {
-      NW(arrow);
-      cn->arrow = gtk_arrow_new(cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN
-                                                        : GTK_ARROW_RIGHT,
-                                GTK_SHADOW_NONE);
-      cn->marker = 0;
+static void choose_refill_row(GtkTreePath *path,
+                              GtkTreeIter *iter) {
+  const char *track = choose_get_track(iter);
+  disorder_eclient_files(client, choose_files_completed,
+                         track,
+                         NULL,
+                         gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store),
+                                                    path));
+  disorder_eclient_dirs(client, choose_dirs_completed,
+                        track,
+                        NULL,
+                        gtk_tree_row_reference_new(GTK_TREE_MODEL(choose_store),
+                                                   path));
+  /* The row references are destroyed in the _completed handlers. */
+  choose_list_in_flight += 2;
+}
+
+static void choose_row_expanded(GtkTreeView attribute((unused)) *treeview,
+                                GtkTreeIter *iter,
+                                GtkTreePath *path,
+                                gpointer attribute((unused)) user_data) {
+  /*fprintf(stderr, "row-expanded path=[%s]\n\n",
+          gtk_tree_path_to_string(path));*/
+  /* We update a node's contents whenever it is expanded, even if it was
+   * already populated; the effect is that contracting and expanding a node
+   * suffices to update it to the latest state on the server. */
+  choose_refill_row(path, iter);
+  if(!choose_suppress_set_autocollapse) {
+    if(choose_auto_expanding) {
+      /* This was an automatic expansion; mark it the row for auto-collapse. */
+      gtk_tree_store_set(choose_store, iter,
+                         AUTOCOLLAPSE_COLUMN, TRUE,
+                         -1);
+      /*fprintf(stderr, "enable auto-collapse for %s\n",
+              gtk_tree_path_to_string(path));*/
     } else {
-      cn->arrow = 0;
-      if((pb = find_image("notes.png"))) {
-        NW(image);
-        cn->marker = gtk_image_new_from_pixbuf(pb);
-      }
+      /* This was a manual expansion.  Inhibit automatic collapse on this row
+       * and all its ancestors.  */
+      gboolean itv;
+      do {
+        gtk_tree_store_set(choose_store, iter,
+                           AUTOCOLLAPSE_COLUMN, FALSE,
+                           -1);
+        /*fprintf(stderr, "suppress auto-collapse for %s\n",
+                gtk_tree_model_get_string_from_iter(GTK_TREE_MODEL(choose_store),
+                                                    iter));*/
+        GtkTreeIter child = *iter;
+        itv = gtk_tree_model_iter_parent(GTK_TREE_MODEL(choose_store),
+                                         iter,
+                                         &child);
+      } while(itv);
+      /* The effect of this is that if you expand a row that's actually a
+       * sibling of the real target of the auto-expansion, it stays expanded
+       * when you clear a search.  That's find and good, but it _still_ stays
+       * expanded if you expand it and then collapse it.
+       *
+       * An alternative policy would be to only auto-collapse rows that don't
+       * have any expanded children (apart from ones also subject to
+       * auto-collapse).  I'm not sure what the most usable policy is.
+       */
     }
-    MTAG_POP();
-    MTAG_PUSH("make_widgets_2");
-    NW(label);
-    cn->label = gtk_label_new(cn->display);
-    if(cn->arrow)
-      gtk_container_add(GTK_CONTAINER(cn->hbox), cn->arrow);
-    gtk_container_add(GTK_CONTAINER(cn->hbox), cn->label);
-    if(cn->marker)
-      gtk_container_add(GTK_CONTAINER(cn->hbox), cn->marker);
-    MTAG_POP();
-    MTAG_PUSH("make_widgets_3");
-    NW(event_box);
-    cn->container = gtk_event_box_new();
-    gtk_container_add(GTK_CONTAINER(cn->container), cn->hbox);
-    g_signal_connect(cn->container, "button-release-event", 
-                     G_CALLBACK(clicked_choosenode), cn);
-    g_signal_connect(cn->container, "button-press-event", 
-                     G_CALLBACK(clicked_choosenode), cn);
-    g_object_ref(cn->container);
-    /* Show everything by default */
-    gtk_widget_show_all(cn->container);
-    MTAG_POP();
-    END(new_widgets);
-  }
-  assert(cn->container);
-  /* Set colors */
-  BEGIN(colors);
-  if(search_result) {
-    gtk_widget_set_style(cn->container, search_style);
-    gtk_widget_set_style(cn->label, search_style);
-  } else {
-    gtk_widget_set_style(cn->container, layout_style);
-    gtk_widget_set_style(cn->label, layout_style);
-  }
-  END(colors);
-  /* Make sure the icon is right */
-  BEGIN(markers);
-  if(cn->flags & CN_EXPANDABLE)
-    gtk_arrow_set(GTK_ARROW(cn->arrow),
-                  cn->flags & CN_EXPANDED ? GTK_ARROW_DOWN : GTK_ARROW_RIGHT,
-                  GTK_SHADOW_NONE);
-  else if(cn->marker)
-    /* Make sure the queued marker is right */
-    /* TODO: doesn't always work */
-    (queued(cn->path) ? gtk_widget_show : gtk_widget_hide)(cn->marker);
-  END(markers);
-  /* Put the widget in the right place */
-  BEGIN(location);
-  if(cn->flags & CN_DISPLAYED)
-    gtk_layout_move(GTK_LAYOUT(chooselayout), cn->container, x, y);
-  else {
-    gtk_layout_put(GTK_LAYOUT(chooselayout), cn->container, x, y);
-    cn->flags |= CN_DISPLAYED;
-    /* Now chooselayout has a ref to the container */
-    g_object_unref(cn->container);
   }
-  END(location);
-  /* Set the widget's selection status */
-  BEGIN(selection);
-  if(!(cn->flags & CN_EXPANDABLE))
-    display_selection(cn);
-  END(selection);
-  /* Find the size used so we can get vertical positioning right. */
-  gtk_widget_size_request(cn->container, &req);
-  d.width = x + req.width;
-  d.height = y + req.height;
-  cn->ymin = y;
-  cn->ymax = d.height;
-  if(cn->flags & CN_EXPANDED) {
-    /* We'll offset children by the size of the arrow whatever it might be. */
-    assert(cn->arrow);
-    gtk_widget_size_request(cn->arrow, &req);
-    aw = req.width;
-    for(n = 0; n < cn->children.nvec; ++n) {
-      cd = display_tree(cn->children.vec[n], x + aw, d.height);
-      if(cd.width > d.width)
-        d.width = cd.width;
-      d.height = cd.height;
-    }
-  } else {
-    BEGIN(undisplay);
-    for(n = 0; n < cn->children.nvec; ++n)
-      undisplay_tree(cn->children.vec[n]);
-    END(undisplay);
-  }
-  if(!(cn->flags & CN_EXPANDABLE)) {
-    ++files_visible;
-    if(cn->flags & CN_SELECTED)
-      ++files_selected;
-  }
-  /* update the search results array */
-  if(search_result)
-    searchnodes[search_result - 1] = cn;
-  /* report back how much space we used */
-  D(("display_tree %s %d,%d total size %dx%d", cn->path, x, y,
-     d.width, d.height));
-  return d;
 }
 
-/** @brief Remove widgets for newly hidden nodes */
-static void undisplay_tree(struct choosenode *cn) {
-  int n;
+static void choose_auto_collapse_callback(GtkTreeView *tree_view,
+                                          GtkTreePath *path,
+                                          gpointer attribute((unused)) user_data) {
+  GtkTreeIter it[1];
 
-  D(("undisplay_tree %s", cn->path));
-  /* Remove this widget from the display */
-  if(cn->flags & CN_DISPLAYED) {
-    gtk_container_remove(GTK_CONTAINER(chooselayout), cn->container);
-    cn->flags ^= CN_DISPLAYED;
+  gtk_tree_model_get_iter(GTK_TREE_MODEL(choose_store), it, path);
+  if(choose_can_autocollapse(it)) {
+    /*fprintf(stderr, "collapse %s\n",
+            gtk_tree_path_to_string(path));*/
+    gtk_tree_store_set(choose_store, it,
+                       AUTOCOLLAPSE_COLUMN, FALSE,
+                       -1);
+    gtk_tree_view_collapse_row(tree_view, path);
   }
-  /* Remove children too */
-  for(n = 0; n < cn->children.nvec; ++n)
-    undisplay_tree(cn->children.vec[n]);
 }
 
-/* Selection --------------------------------------------------------------- */
-
-/** @brief Mark the widget @p cn according to its selection state */
-static void display_selection(struct choosenode *cn) {
-  /* Need foreground and background colors */
-  gtk_widget_set_state(cn->label, (cn->flags & CN_SELECTED
-                                   ? GTK_STATE_SELECTED : GTK_STATE_NORMAL));
-  gtk_widget_set_state(cn->container, (cn->flags & CN_SELECTED
-                                       ? GTK_STATE_SELECTED : GTK_STATE_NORMAL));
+/** @brief Perform automatic collapse after a search is cleared */
+void choose_auto_collapse(void) {
+  gtk_tree_view_map_expanded_rows(GTK_TREE_VIEW(choose_view),
+                                  choose_auto_collapse_callback,
+                                  0);
 }
 
-/** @brief Set the selection state of a widget
- *
- * Directories can never be selected, we just ignore attempts to do so. */
-static void set_selection(struct choosenode *cn, int selected) {
-  unsigned f = selected ? CN_SELECTED : 0;
+/** @brief Called from choose_refill() with each expanded row */
+static void choose_refill_callback(GtkTreeView attribute((unused)) *tree_view,
+                                   GtkTreePath *path,
+                                   gpointer attribute((unused)) user_data) {
+  GtkTreeIter it[1];
 
-  D(("set_selection %d %s", selected, cn->path));
-  if(!(cn->flags & CN_EXPANDABLE) && (cn->flags & CN_SELECTED) != f) {
-    cn->flags ^= CN_SELECTED;
-    /* Maintain selection count */
-    if(selected)
-      ++files_selected;
-    else
-      --files_selected;
-    display_selection(cn);
-    /* Update main menu sensitivity */
-    menu_update(-1);
-  }
+  gtk_tree_model_get_iter(GTK_TREE_MODEL(choose_store), it, path);
+  choose_refill_row(path, it);
 }
 
-/** @brief Recursively clear all selection bits from CN down */
-static void clear_selection(struct choosenode *cn) {
-  int n;
-
-  set_selection(cn, 0);
-  for(n = 0; n < cn->children.nvec; ++n)
-    clear_selection(cn->children.vec[n]);
-}
-
-/* User actions ------------------------------------------------------------ */
-
-/** @brief Clicked on something
+/** @brief Synchronize all visible data with the server
  *
- * This implements playing, all the modifiers for selection, etc.
+ * Called at startup, when a rescan completes, and via periodic_slow().
  */
-static void clicked_choosenode(GtkWidget attribute((unused)) *widget,
-                               GdkEventButton *event,
-                               gpointer user_data) {
-  struct choosenode *cn = user_data;
-  int ind, last_ind, n;
-
-  D(("clicked_choosenode %s", cn->path));
-  if(event->type == GDK_BUTTON_RELEASE
-     && event->button == 1) {
-    /* Left click */
-    if(cn->flags & CN_EXPANDABLE) {
-      /* This is a directory.  Flip its expansion status. */
-      if(cn->flags & CN_EXPANDED)
-        contract_node(cn);
-      else
-        expand_node(cn, 0/*!contingent*/);
-      last_click = 0;
-    } else {
-      /* This is a file.  Adjust selection status */
-      /* TODO the basic logic here is essentially the same as that in queue.c.
-       * Can we share code at all? */
-      switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
-      case 0:
-        clear_selection(root);
-        set_selection(cn, 1);
-        last_click = cn;
-        break;
-      case GDK_CONTROL_MASK:
-        set_selection(cn, !(cn->flags & CN_SELECTED));
-        last_click = cn;
-        break;
-      case GDK_SHIFT_MASK:
-      case GDK_SHIFT_MASK|GDK_CONTROL_MASK:
-        if(last_click && last_click->parent == cn->parent) {
-          /* Figure out where the current and last clicks are in the list */
-          ind = last_ind = -1;
-          for(n = 0; n < cn->parent->children.nvec; ++n) {
-            if(cn->parent->children.vec[n] == cn)
-              ind = n;
-            if(cn->parent->children.vec[n] == last_click)
-              last_ind = n;
-          }
-          /* Test shouldn't ever fail, but still */
-          if(ind >= 0 && last_ind >= 0) {
-            if(!(event->state & GDK_CONTROL_MASK)) {
-              for(n = 0; n < cn->parent->children.nvec; ++n)
-                set_selection(cn->parent->children.vec[n], 0);
-            }
-            if(ind > last_ind)
-              for(n = last_ind; n <= ind; ++n)
-                set_selection(cn->parent->children.vec[n], 1);
-            else
-              for(n = ind; n <= last_ind; ++n)
-                set_selection(cn->parent->children.vec[n], 1);
-            if(event->state & GDK_CONTROL_MASK)
-              last_click = cn;
-          }
-        }
-        /* TODO trying to select a range that doesn't share a single parent
-         * currently does not work, but it ought to. */
-        break;
-      }
-    }
-  } else if(event->type == GDK_BUTTON_RELEASE
-     && event->button == 2) {
-    /* Middle click - play the pointed track */
-    if(!(cn->flags & CN_EXPANDABLE)) {
-      clear_selection(root);
-      set_selection(cn, 1);
-      gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue");
-      disorder_eclient_play(client, cn->path, 0, 0);
-      last_click = 0;
-    }
-  } else if(event->type == GDK_BUTTON_PRESS
-     && event->button == 3) {
-    struct choose_menuitem *const menuitems =
-      (cn->flags & CN_EXPANDABLE ? dir_menuitems : track_menuitems);
-    GtkWidget *const menu =
-      (cn->flags & CN_EXPANDABLE ? dir_menu : track_menu);
-    /* Right click.  Pop up a menu. */
-    /* If the current file isn't selected, switch the selection to just that.
-     * (If we're looking at a directory then leave the selection alone.) */
-    if(!(cn->flags & CN_EXPANDABLE) && !(cn->flags & CN_SELECTED)) {
-      clear_selection(root);
-      set_selection(cn, 1);
-      last_click = cn;
+static void choose_refill(const char attribute((unused)) *event,
+                          void attribute((unused)) *eventdata,
+                          void attribute((unused)) *callbackdata) {
+  //fprintf(stderr, "choose_refill\n");
+  /* Update the root */
+  disorder_eclient_files(client, choose_files_completed, "", NULL, NULL); 
+  disorder_eclient_dirs(client, choose_dirs_completed, "", NULL, NULL); 
+  choose_list_in_flight += 2;
+  /* Update all expanded rows */
+  gtk_tree_view_map_expanded_rows(GTK_TREE_VIEW(choose_view),
+                                  choose_refill_callback,
+                                  0);
+  //fprintf(stderr, "choose_list_in_flight -> %d+\n", choose_list_in_flight);
+}
+
+/** @brief Called for key-*-event on the main view
+ */
+static gboolean choose_key_event(GtkWidget attribute((unused)) *widget,
+                                 GdkEventKey *event,
+                                 gpointer attribute((unused)) user_data) {
+  /*fprintf(stderr, "choose_key_event type=%d state=%#x keyval=%#x\n",
+          event->type, event->state, event->keyval);*/
+  switch(event->keyval) {
+  case GDK_Page_Up:
+  case GDK_Page_Down:
+  case GDK_Up:
+  case GDK_Down:
+  case GDK_Home:
+  case GDK_End:
+    return FALSE;                       /* We'll take these */
+  case 'f': case 'F':
+    /* ^F is expected to start a search.  We implement this by focusing the
+     * search entry box. */
+    if((event->state & ~(GDK_LOCK_MASK|GDK_SHIFT_MASK)) == GDK_CONTROL_MASK
+       && event->type == GDK_KEY_PRESS) {
+      choose_search_new();
+      return TRUE;                      /* Handled it */
     }
-    /* Set the item sensitivity and callbacks */
-    for(n = 0; menuitems[n].name; ++n) {
-      if(menuitems[n].handlerid)
-        g_signal_handler_disconnect(menuitems[n].w,
-                                    menuitems[n].handlerid);
-      gtk_widget_set_sensitive(menuitems[n].w,
-                               menuitems[n].sensitive(cn));
-      menuitems[n].handlerid = g_signal_connect
-        (menuitems[n].w, "activate", G_CALLBACK(menuitems[n].activate), cn);
+    break;
+  case 'g': case 'G':
+    /* ^G is expected to go the next match.  We simulate a click on the 'next'
+     * button. */
+    if((event->state & ~(GDK_LOCK_MASK|GDK_SHIFT_MASK)) == GDK_CONTROL_MASK
+       && event->type == GDK_KEY_PRESS) {
+      choose_next_clicked(0, 0);
+      return TRUE;                      /* Handled it */
     }
-    set_tool_colors(menu);
-    /* Pop up the menu */
-    gtk_widget_show_all(menu);
-    gtk_menu_popup(GTK_MENU(menu), 0, 0, 0, 0,
-                   event->button, event->time);
-  }
-}
-
-/** @brief Called BY GTK+ to tell us the search entry box has changed */
-static void searchentry_changed(GtkEditable attribute((unused)) *editable,
-                                gpointer attribute((unused)) user_data) {
-  initiate_search();
-}
-
-/* Track menu items -------------------------------------------------------- */
-
-/** @brief Recursive step for gather_selected() */
-static void recurse_selected(struct choosenode *cn, struct vector *v) {
-  int n;
-
-  if(cn->flags & CN_EXPANDABLE) {
-    if(cn->flags & CN_EXPANDED)
-      for(n = 0; n < cn->children.nvec; ++n)
-        recurse_selected(cn->children.vec[n], v);
-  } else {
-    if((cn->flags & CN_SELECTED) && cn->path)
-      vector_append(v, (char *)cn->path);
+    break;
   }
+  gtk_widget_event(user_data, (GdkEvent *)event);
+  return TRUE;                          /* Handled it */
 }
 
-/*** @brief Get a list of all the selected tracks */
-static const char **gather_selected(int *ntracks) {
-  struct vector v;
-
-  vector_init(&v);
-  recurse_selected(root, &v);
-  vector_terminate(&v);
-  if(ntracks) *ntracks = v.nvec;
-  return (const char **)v.vec;
-}
-
-/** @brief Called when the track menu's play option is activated */
-static void activate_track_play(GtkMenuItem attribute((unused)) *menuitem,
-                                gpointer attribute((unused)) user_data) {
-  const char **tracks = gather_selected(0);
-  int n;
-  
-  gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue");
-  for(n = 0; tracks[n]; ++n)
-    disorder_eclient_play(client, tracks[n], 0, 0);
-}
-
-/** @brief Called when the menu's properties option is activated */
-static void activate_track_properties(GtkMenuItem attribute((unused)) *menuitem,
-                                      gpointer attribute((unused)) user_data) {
-  int ntracks;
-  const char **tracks = gather_selected(&ntracks);
-
-  properties(ntracks, tracks);
-}
-
-/** @brief Determine whether the menu's play option should be sensitive */
-static gboolean sensitive_track_play(struct choosenode attribute((unused)) *cn) {
-  return (!!files_selected
-          && (disorder_eclient_state(client) & DISORDER_CONNECTED));
-}
-
-/** @brief Determine whether the menu's properties option should be sensitive */
-static gboolean sensitive_track_properties(struct choosenode attribute((unused)) *cn) {
-  return !!files_selected && (disorder_eclient_state(client) & DISORDER_CONNECTED);
-}
-
-/* Directory menu items ---------------------------------------------------- */
-
-/** @brief Return the file children of @p cn
- *
- * The list is terminated by a null pointer.
- */
-static const char **dir_files(struct choosenode *cn, int *nfiles) {
-  const char **files = xcalloc(cn->children.nvec + 1, sizeof (char *));
-  int n, m;
-
-  for(n = m = 0; n < cn->children.nvec; ++n) 
-    if(!(cn->children.vec[n]->flags & CN_EXPANDABLE))
-      files[m++] = cn->children.vec[n]->path;
-  files[m] = 0;
-  if(nfiles) *nfiles = m;
-  return files;
-}
-
-static void play_dir(struct choosenode *cn,
-                     void attribute((unused)) *wfu) {
-  int ntracks, n;
-  const char **tracks = dir_files(cn, &ntracks);
-  
-  gtk_label_set_text(GTK_LABEL(report_label), "adding track to queue");
-  for(n = 0; n < ntracks; ++n)
-    disorder_eclient_play(client, tracks[n], 0, 0);
-}
-
-static void properties_dir(struct choosenode *cn,
-                           void attribute((unused)) *wfu) {
-  int ntracks;
-  const char **tracks = dir_files(cn, &ntracks);
-  
-  properties(ntracks, tracks);
-}
-
-static void select_dir(struct choosenode *cn,
-                       void attribute((unused)) *wfu) {
-  int n;
-
-  clear_selection(root);
-  for(n = 0; n < cn->children.nvec; ++n) 
-    set_selection(cn->children.vec[n], 1);
-}
-
-/** @brief Ensure @p cn is expanded and then call @p callback */
-static void call_with_dir(struct choosenode *cn,
-                          when_filled_callback *whenfilled,
-                          void *wfu) {
-  if(!(cn->flags & CN_EXPANDABLE))
-    return;                             /* something went wrong */
-  if(cn->flags & CN_EXPANDED)
-    /* @p cn is already open */
-    whenfilled(cn, wfu);
-  else {
-    /* @p cn is not open, arrange for the callback to go off when it is
-     * opened */
-    cn->whenfilled = whenfilled;
-    cn->wfu = wfu;
-    expand_node(cn, 0/*not contingnet upon search*/);
-  }
-}
-
-/** @brief Called when the directory menu's play option is activated */
-static void activate_dir_play(GtkMenuItem attribute((unused)) *menuitem,
-                              gpointer user_data) {
-  struct choosenode *const cn = (struct choosenode *)user_data;
-
-  call_with_dir(cn, play_dir, 0);
-}
-
-/** @brief Called when the directory menu's properties option is activated */
-static void activate_dir_properties(GtkMenuItem attribute((unused)) *menuitem,
-                                    gpointer user_data) {
-  struct choosenode *const cn = (struct choosenode *)user_data;
-
-  call_with_dir(cn, properties_dir, 0);
-}
-
-/** @brief Called when the directory menu's select option is activated */
-static void activate_dir_select(GtkMenuItem attribute((unused)) *menuitem,
-                                gpointer user_data) {
-  struct choosenode *const cn = (struct choosenode *)user_data;
-
-  call_with_dir(cn, select_dir,  0);
-}
-
-/** @brief Determine whether the directory menu's play option should be sensitive */
-static gboolean sensitive_dir_play(struct choosenode attribute((unused)) *cn) {
-  return !!(disorder_eclient_state(client) & DISORDER_CONNECTED);
-}
-
-/** @brief Determine whether the directory menu's properties option should be sensitive */
-static gboolean sensitive_dir_properties(struct choosenode attribute((unused)) *cn) {
-  return !!(disorder_eclient_state(client) & DISORDER_CONNECTED);
-}
-
-/** @brief Determine whether the directory menu's select option should be sensitive */
-static gboolean sensitive_dir_select(struct choosenode attribute((unused)) *cn) {
-  return TRUE;
-}
-
-
-
-/* Main menu plumbing ------------------------------------------------------ */
-
-/** @brief Determine whether the edit menu's properties option should be sensitive */
-static int choose_properties_sensitive(GtkWidget attribute((unused)) *w) {
-  return !!files_selected && (disorder_eclient_state(client) & DISORDER_CONNECTED);
-}
-
-/** @brief Determine whether the edit menu's select all option should be sensitive
- *
- * TODO not implemented,  see also choose_selectall_activate()
- */
-static int choose_selectall_sensitive(GtkWidget attribute((unused)) *w) {
-  return FALSE;
-}
-
-/** @brief Determine whether the edit menu's select none option should be sensitive
- *
- * TODO not implemented,  see also choose_selectnone_activate()
- */
-static int choose_selectnone_sensitive(GtkWidget attribute((unused)) *w) {
-  return FALSE;
-}
-
-/** @brief Called when the edit menu's properties option is activated */
-static void choose_properties_activate(GtkWidget attribute((unused)) *w) {
-  activate_track_properties(0, 0);
-}
-
-/** @brief Called when the edit menu's select all option is activated
- *
- * TODO not implemented, see choose_selectall_sensitive() */
-static void choose_selectall_activate(GtkWidget attribute((unused)) *w) {
-}
-
-/** @brief Called when the edit menu's select none option is activated
- *
- * TODO not implemented, see choose_selectnone_sensitive() */
-static void choose_selectnone_activate(GtkWidget attribute((unused)) *w) {
-}
-
-/** @brief Main menu callbacks for Choose screen */
-static const struct tabtype tabtype_choose = {
-  choose_properties_sensitive,
-  choose_selectall_sensitive,
-  choose_selectnone_sensitive,
-  choose_properties_activate,
-  choose_selectall_activate,
-  choose_selectnone_activate,
-};
-
-/* Public entry points ----------------------------------------------------- */
-
-/** @brief Called to entirely reset the choose screen */
-static void choose_reset(void) {
-  if(root)
-    undisplay_tree(root);
-  root = newnode(0/*parent*/, "<root>", "All files", "",
-                 CN_EXPANDABLE, fill_root_node);
-  expand_node(root, 0);                 /* will call redisplay_tree */
-}
-
-/** @brief Create a track choice widget */
+/** @brief Create the choose tab */
 GtkWidget *choose_widget(void) {
-  int n;
-  GtkWidget *scrolled;
-  GtkWidget *vbox, *hbox, *clearsearch;
-
-  /*
-   *   +--vbox-------------------------------------------------------+
-   *   | +-hbox----------------------------------------------------+ |
-   *   | | searchentry                               | clearsearch | |
-   *   | +---------------------------------------------------------+ |
-   *   | +-scrolled------------------------------------------------+ |
-   *   | | +-chooselayout------------------------------------++--+ | |
-   *   | | | Tree structure is manually layed out in here    ||^^| | |
-   *   | | |                                                 ||  | | |
-   *   | | |                                                 ||  | | |
-   *   | | |                                                 ||  | | |
-   *   | | |                                                 ||vv| | |
-   *   | | +-------------------------------------------------++--+ | |
-   *   | | +-------------------------------------------------+     | |
-   *   | | |<                                               >|     | |
-   *   | | +-------------------------------------------------+     | |
-   *   | +---------------------------------------------------------+ |
-   *   +-------------------------------------------------------------+
-   */
+  /* Create the tree store. */
+  choose_store = gtk_tree_store_new(CHOOSE_COLUMNS,
+                                    G_TYPE_BOOLEAN,
+                                    G_TYPE_STRING,
+                                    G_TYPE_STRING,
+                                    G_TYPE_BOOLEAN,
+                                    G_TYPE_STRING,
+                                    G_TYPE_STRING,
+                                    G_TYPE_STRING,
+                                    G_TYPE_STRING,
+                                    G_TYPE_BOOLEAN);
+
+  /* Create the view */
+  choose_view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(choose_store));
+  gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(choose_view), TRUE);
+  /* Suppress built-in typeahead find, we do our own search support. */
+  gtk_tree_view_set_enable_search(GTK_TREE_VIEW(choose_view), FALSE);
+
+  /* Create cell renderers and columns */
+  /* TODO use a table */
+  {
+    GtkCellRenderer *r = gtk_cell_renderer_toggle_new();
+    GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes
+      ("Queued",
+       r,
+       "active", STATE_COLUMN,
+       "visible", ISFILE_COLUMN,
+       (char *)0);
+    gtk_tree_view_column_set_resizable(c, TRUE);
+    gtk_tree_view_column_set_reorderable(c, TRUE);
+    gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c);
+    g_signal_connect(r, "toggled",
+                     G_CALLBACK(choose_state_toggled), 0);
+  }
+  {
+    GtkCellRenderer *r = gtk_cell_renderer_text_new();
+    GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes
+      ("Length",
+       r,
+       "text", LENGTH_COLUMN,
+       (char *)0);
+    gtk_tree_view_column_set_resizable(c, TRUE);
+    gtk_tree_view_column_set_reorderable(c, TRUE);
+    g_object_set(r, "xalign", (gfloat)1.0, (char *)0);
+    gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c);
+  }
+  {
+    GtkCellRenderer *r = gtk_cell_renderer_text_new();
+    GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes
+      ("Track",
+       r,
+       "text", NAME_COLUMN,
+       "background", BG_COLUMN,
+       "foreground", FG_COLUMN,
+       (char *)0);
+    gtk_tree_view_column_set_resizable(c, TRUE);
+    gtk_tree_view_column_set_reorderable(c, TRUE);
+    g_object_set(c, "expand", TRUE, (char *)0);
+    gtk_tree_view_append_column(GTK_TREE_VIEW(choose_view), c);
+    gtk_tree_view_set_expander_column(GTK_TREE_VIEW(choose_view), c);
+  }
   
-  /* Text entry box for search terms */
-  NW(entry);
-  searchentry = gtk_entry_new();
-  gtk_widget_set_style(searchentry, tool_style);
-  g_signal_connect(searchentry, "changed", G_CALLBACK(searchentry_changed), 0);
-  gtk_tooltips_set_tip(tips, searchentry, "Enter search terms here; search is automatic", "");
-
-  /* Cancel button to clear the search */
-  NW(button);
-  clearsearch = gtk_button_new_from_stock(GTK_STOCK_CANCEL);
-  gtk_widget_set_style(clearsearch, tool_style);
-  g_signal_connect(G_OBJECT(clearsearch), "clicked",
-                   G_CALLBACK(clearsearch_clicked), 0);
-  gtk_tooltips_set_tip(tips, clearsearch, "Clear search terms", "");
-
-  /* Up and down buttons to find previous/next results; initially they are not
-   * usable as there are no search results. */
-  prevsearch = iconbutton("up.png", "Previous search result");
-  g_signal_connect(G_OBJECT(prevsearch), "clicked",
-                   G_CALLBACK(prev_clicked), 0);
-  gtk_widget_set_style(prevsearch, tool_style);
-  gtk_widget_set_sensitive(prevsearch, 0);
-  nextsearch = iconbutton("down.png", "Next search result");
-  g_signal_connect(G_OBJECT(nextsearch), "clicked",
-                   G_CALLBACK(next_clicked), 0);
-  gtk_widget_set_style(nextsearch, tool_style);
-  gtk_widget_set_sensitive(nextsearch, 0);
-
-  /* hbox packs the search tools button together on a line */
-  NW(hbox);
-  hbox = gtk_hbox_new(FALSE/*homogeneous*/, 1/*spacing*/);
-  gtk_box_pack_start(GTK_BOX(hbox), searchentry,
+  /* The selection should support multiple things being selected */
+  choose_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(choose_view));
+  gtk_tree_selection_set_mode(choose_selection, GTK_SELECTION_MULTIPLE);
+
+  /* Catch button presses */
+  g_signal_connect(choose_view, "button-press-event",
+                   G_CALLBACK(choose_button_event), 0);
+  g_signal_connect(choose_view, "button-release-event",
+                   G_CALLBACK(choose_button_event), 0);
+  /* Catch row expansions so we can fill in placeholders */
+  g_signal_connect(choose_view, "row-expanded",
+                   G_CALLBACK(choose_row_expanded), 0);
+
+  event_register("queue-list-changed", choose_set_state, 0);
+  event_register("playing-track-changed", choose_set_state, 0);
+  event_register("search-results-changed", choose_set_state, 0);
+  event_register("lookups-completed", choose_set_state, 0);
+
+  /* After a rescan we update the choose tree.  We get a rescan-complete
+   * automatically at startup and upon connection too. */
+  event_register("rescan-complete", choose_refill, 0);
+
+  /* Make the widget scrollable */
+  GtkWidget *scrolled = scroll_widget(choose_view);
+
+  /* Pack vertically with the search widget */
+  GtkWidget *vbox = gtk_vbox_new(FALSE/*homogenous*/, 1/*spacing*/);
+  gtk_box_pack_start(GTK_BOX(vbox), scrolled,
                      TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
-  gtk_box_pack_start(GTK_BOX(hbox), prevsearch,
-                     FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
-  gtk_box_pack_start(GTK_BOX(hbox), nextsearch,
-                     FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
-  gtk_box_pack_start(GTK_BOX(hbox), clearsearch,
-                     FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
+  gtk_box_pack_end(GTK_BOX(vbox), choose_search_widget(),
+                   FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
   
-  /* chooselayout contains the currently visible subset of the track
-   * namespace */
-  NW(layout);
-  chooselayout = gtk_layout_new(0, 0);
-  gtk_widget_set_style(chooselayout, layout_style);
-  choose_reset();
-  register_reset(choose_reset);
-  /* Create the popup menus */
-  NW(menu);
-  track_menu = gtk_menu_new();
-  g_signal_connect(track_menu, "destroy", G_CALLBACK(gtk_widget_destroyed),
-                   &track_menu);
-  for(n = 0; track_menuitems[n].name; ++n) {
-    NW(menu_item);
-    track_menuitems[n].w = 
-      gtk_menu_item_new_with_label(track_menuitems[n].name);
-    gtk_menu_attach(GTK_MENU(track_menu), track_menuitems[n].w,
-                    0, 1, n, n + 1);
-  }
-  NW(menu);
-  dir_menu = gtk_menu_new();
-  g_signal_connect(dir_menu, "destroy", G_CALLBACK(gtk_widget_destroyed),
-                   &dir_menu);
-  for(n = 0; dir_menuitems[n].name; ++n) {
-    NW(menu_item);
-    dir_menuitems[n].w = 
-      gtk_menu_item_new_with_label(dir_menuitems[n].name);
-    gtk_menu_attach(GTK_MENU(dir_menu), dir_menuitems[n].w,
-                    0, 1, n, n + 1);
-  }
-  /* The layout is scrollable */
-  scrolled = scroll_widget(chooselayout);
-  vadjust = gtk_layout_get_vadjustment(GTK_LAYOUT(chooselayout));
+  g_object_set_data(G_OBJECT(vbox), "type", (void *)&choose_tabtype);
 
-  /* The scrollable layout and the search hbox go together in a vbox */
-  NW(vbox);
-  vbox = gtk_vbox_new(FALSE/*homogenous*/, 1/*spacing*/);
-  gtk_box_pack_start(GTK_BOX(vbox), hbox,
-                     FALSE/*expand*/, FALSE/*fill*/, 0/*padding*/);
-  gtk_box_pack_end(GTK_BOX(vbox), scrolled,
-                   TRUE/*expand*/, TRUE/*fill*/, 0/*padding*/);
+  /* Redirect keyboard activity to the search widget */
+  g_signal_connect(choose_view, "key-press-event",
+                   G_CALLBACK(choose_key_event), choose_search_entry);
+  g_signal_connect(choose_view, "key-release-event",
+                   G_CALLBACK(choose_key_event), choose_search_entry);
 
-  g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_choose);
   return vbox;
 }
 
-/** @brief Called when something we care about here might have changed */
-void choose_update(void) {
-  redisplay_tree("choose_update");
-}
-
 /*
 Local Variables:
 c-basic-offset:2
diff --git a/disobedience/choose.h b/disobedience/choose.h
new file mode 100644 (file)
index 0000000..c101e6b
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#ifndef CHOOSE_H
+#define CHOOSE_H
+
+/** @brief Column numbers */
+enum {
+  /* Visible columns */
+  STATE_COLUMN,                 /* Track state */
+  NAME_COLUMN,                  /* Track name (display context) */
+  LENGTH_COLUMN,                /* Track length */
+  /* Hidden columns */
+  ISFILE_COLUMN,                /* TRUE for a track, FALSE for a directory */
+  TRACK_COLUMN,                 /* Full track name, "" for placeholder */
+  SORT_COLUMN,                  /* Sort key */
+  BG_COLUMN,                    /* Background color */
+  FG_COLUMN,                    /* Foreground color */
+  AUTOCOLLAPSE_COLUMN,          /* TRUE if row should be auto-collapsed */
+
+  CHOOSE_COLUMNS                /* column count */
+};
+
+#ifndef SEARCH_RESULT_BG
+/** @brief Background color for search results */
+# define SEARCH_RESULT_BG "#ffffc0"
+/** @brief Foreground color for search results */
+# define SEARCH_RESULT_FG "black"
+#endif
+
+#ifndef SEARCH_DELAY_MS
+/** @brief Delay between last keypress in search entry and start of search */
+# define SEARCH_DELAY_MS 500            /* milliseconds */
+#endif
+
+extern GtkTreeStore *choose_store;
+extern GtkWidget *choose_view;
+extern GtkTreeSelection *choose_selection;
+extern const struct tabtype choose_tabtype;
+extern int choose_auto_expanding;
+extern GtkWidget *choose_search_entry;
+
+struct choosedata *choose_iter_to_data(GtkTreeIter *iter);
+struct choosedata *choose_path_to_data(GtkTreePath *path);
+gboolean choose_button_event(GtkWidget *widget,
+                             GdkEventButton *event,
+                             gpointer user_data);
+void choose_play_completed(void attribute((unused)) *v,
+                           const char *err);
+char *choose_get_track(GtkTreeIter *iter);
+char *choose_get_sort(GtkTreeIter *iter);
+char *choose_get_display(GtkTreeIter *iter);
+int choose_is_file(GtkTreeIter *iter);
+int choose_is_dir(GtkTreeIter *iter);
+int choose_is_placeholder(GtkTreeIter *iter);
+int choose_can_autocollapse(GtkTreeIter *iter);
+GtkWidget *choose_search_widget(void);
+int choose_is_search_result(const char *track);
+void choose_auto_collapse(void);
+void choose_next_clicked(GtkButton *button,
+                         gpointer userdata);
+void choose_prev_clicked(GtkButton *button,
+                         gpointer userdata);
+void choose_search_new(void);
+
+#endif /* CHOOSE_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 283ce8a..d81aac2 100644 (file)
@@ -120,7 +120,6 @@ static void gtkclient_poll(void *u,
 static void gtkclient_comms_error(void attribute((unused)) *u,
                                  const char *msg) {
   D(("gtkclient_comms_error %s", msg));
-  menu_update(-1);
   gtk_label_set_text(GTK_LABEL(report_label), msg);
 }
 
@@ -148,7 +147,6 @@ static void gtkclient_report(void attribute((unused)) *u,
   if(!msg)
     /* We're idle - clear the report line */
     gtk_label_set_text(GTK_LABEL(report_label), "");
-  menu_update(-1);
 }
 
 /** @brief Repoort an unhandled protocol-level error to the user */
index 57931be..6ed9fb3 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2006, 2007 Richard Kettlewell
+ * Copyright (C) 2006-2008 Richard Kettlewell
  *
  * 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
 
 /* Forward declarations ---------------------------------------------------- */
 
-WT(adjustment);
-WT(hscale);
-WT(hbox);
-WT(button);
-WT(image);
-WT(label);
-WT(vbox);
-
 struct icon;
 
-static void update_pause(const struct icon *);
-static void update_play(const struct icon *);
-static void update_scratch(const struct icon *);
-static void update_random_enable(const struct icon *);
-static void update_random_disable(const struct icon *);
-static void update_enable(const struct icon *);
-static void update_disable(const struct icon *);
-static void update_rtp(const struct icon *);
-static void update_nortp(const struct icon *);
 static void clicked_icon(GtkButton *, gpointer);
 static void clicked_menu(GtkMenuItem *, gpointer userdata);
 static void toggled_menu(GtkCheckMenuItem *, gpointer userdata);
@@ -65,6 +48,13 @@ static void volume_adjusted(GtkAdjustment *a, gpointer user_data);
 static gchar *format_volume(GtkScale *scale, gdouble value);
 static gchar *format_balance(GtkScale *scale, gdouble value);
 
+static void icon_changed(const char *event,
+                         void *evendata,
+                         void *callbackdata);
+static void volume_changed(const char *event,
+                           void *eventdata,
+                           void *callbackdata);
+
 /* Control bar ------------------------------------------------------------- */
 
 /** @brief Guard against feedback */
@@ -72,134 +62,166 @@ int suppress_actions = 1;
 
 /** @brief Definition of an icon
  *
- * The design here is rather mad: rather than changing the image displayed by
- * icons according to their state, we flip the visibility of pairs of icons.
+ * We have two kinds of icon:
+ * - action icons, which just do something but don't have a state as such
+ * - toggle icons, which toggle between two states ("on" and "off").
+ *
+ * The scratch button is an action icon; currently all the others are toggle
+ * icons.
+ *
+ * (All icons can be sensitive or insensitive, separately to the above.)
  */
 struct icon {
-  /** @brief Filename for image */
-  const char *icon;
+  /** @brief Filename for 'on' image */
+  const char *icon_on;
+
+  /** @brief Text for 'on' tooltip */
+  const char *tip_on;
+
+  /** @brief Filename for 'off' image or NULL for an action icon */
+  const char *icon_off;
 
-  /** @brief Text for tooltip */
-  const char *tip;
+  /** @brief Text for 'off tooltip */
+  const char *tip_off;
 
   /** @brief Associated menu item or NULL */
   const char *menuitem;
 
-  /** @brief Called to update button when state may have changed */
-  void (*update)(const struct icon *i);
+  /** @brief Events that change this icon, separated by spaces */
+  const char *events;
 
-  /** @brief @ref eclient.h function to call */
-  int (*action)(disorder_eclient *c,
-                disorder_eclient_no_response *completed,
-                void *v);
+  /** @brief @ref eclient.h function to call to go from off to on
+   *
+   * For action buttons, this should be NULL.
+   */
+  int (*action_go_on)(disorder_eclient *c,
+                      disorder_eclient_no_response *completed,
+                      void *v);
+
+  /** @brief @ref eclient.h function to call to go from on to off
+   *
+   * For action buttons, this action is used.
+   */
+  int (*action_go_off)(disorder_eclient *c,
+                       disorder_eclient_no_response *completed,
+                       void *v);
 
-  /** @brief Flag */
-  unsigned flags;
+  /** @brief Get button state
+   * @return 1 for on, 0 for off
+   */
+  int (*on)(void);
+
+  /** @brief Get button sensitivity
+   * @return 1 for sensitive, 0 for insensitive
+   *
+   * Can be NULL for always sensitive.
+   */
+  int (*sensitive)(void);
   
   /** @brief Pointer to button */
   GtkWidget *button;
 
   /** @brief Pointer to menu item */
   GtkWidget *item;
+
+  GtkWidget *image_on;
+  GtkWidget *image_off;
 };
 
-/** @brief This is the active half of a pair */
-#define ICON_ACTIVE 0x0001
+static int pause_resume_on(void) {
+  return !(last_state & DISORDER_TRACK_PAUSED);
+}
+
+static int pause_resume_sensitive(void) {
+  return !!(last_state & DISORDER_PLAYING)
+    && (last_rights & RIGHT_PAUSE);
+}
+
+static int scratch_sensitive(void) {
+  return !!(last_state & DISORDER_PLAYING)
+    && right_scratchable(last_rights, config->username, playing_track);
+}
+
+static int random_sensitive(void) {
+  return !!(last_rights & RIGHT_GLOBAL_PREFS);
+}
+
+static int random_enabled(void) {
+  return !!(last_state & DISORDER_RANDOM_ENABLED);
+}
+
+static int playing_sensitive(void) {
+  return !!(last_rights & RIGHT_GLOBAL_PREFS);
+}
+
+static int playing_enabled(void) {
+  return !!(last_state & DISORDER_PLAYING_ENABLED);
+}
 
-/** @brief This is the inactive half of a pair */
-#define ICON_INACTIVE 0x0002
+static int rtp_enabled(void) {
+  return rtp_is_running;
+}
+
+static int rtp_sensitive(void) {
+  return rtp_supported;
+}
 
 /** @brief Table of all icons */
 static struct icon icons[] = {
   {
-    "pause.png",                        /* icon */
-    "Pause playing track",              /* tip */
-    "<GdisorderMain>/Control/Playing",  /* menuitem */
-    update_pause,                       /* update */
-    disorder_eclient_pause,             /* action */
-    ICON_ACTIVE,                        /* flags */
-    0,                                  /* button */
-    0                                   /* item */
-  },
-  {
-    "play.png",                         /* icon */
-    "Resume playing track",             /* tip */
-    "<GdisorderMain>/Control/Playing",  /* menuitem */
-    update_play,                        /* update */
-    disorder_eclient_resume,            /* action */
-    ICON_INACTIVE,                      /* flags */
-    0,                                  /* button */
-    0                                   /* item */
-  },
-  {
-    "cross.png",                        /* icon */
-    "Cancel playing track",             /* tip */
-    "<GdisorderMain>/Control/Scratch",  /* menuitem */
-    update_scratch,                     /* update */
-    disorder_eclient_scratch_playing,   /* action */
-    0,                                  /* flags */
-    0,                                  /* button */
-    0                                   /* item */
-  },
-  {
-    "random.png",                       /* icon */
-    "Enable random play",               /* tip */
-    "<GdisorderMain>/Control/Random play", /* menuitem */
-    update_random_enable,               /* update */
-    disorder_eclient_random_enable,     /* action */
-    ICON_INACTIVE,                      /* flags */
-    0,                                  /* button */
-    0                                   /* item */
-  },
-  {
-    "randomcross.png",                  /* icon */
-    "Disable random play",              /* tip */
-    "<GdisorderMain>/Control/Random play", /* menuitem */
-    update_random_disable,              /* update */
-    disorder_eclient_random_disable,    /* action */
-    ICON_ACTIVE,                        /* flags */
-    0,                                  /* button */
-    0                                   /* item */
+    icon_on: "pause.png",
+    tip_on: "Pause playing track",
+    icon_off: "play.png",
+    tip_off: "Resume playing track",
+    menuitem: "<GdisorderMain>/Control/Playing",
+    on: pause_resume_on,
+    sensitive: pause_resume_sensitive,
+    action_go_on: disorder_eclient_resume,
+    action_go_off: disorder_eclient_pause,
+    events: "pause-changed playing-changed rights-changed",
   },
   {
-    "notes.png",                        /* icon */
-    "Enable play",                      /* tip */
-    0,                                  /* menuitem */
-    update_enable,                      /* update */
-    disorder_eclient_enable,            /* action */
-    ICON_INACTIVE,                      /* flags */
-    0,                                  /* button */
-    0                                   /* item */
+    icon_on: "cross.png",
+    tip_on: "Cancel playing track",
+    menuitem: "<GdisorderMain>/Control/Scratch",
+    sensitive: scratch_sensitive,
+    action_go_off: disorder_eclient_scratch_playing,
+    events: "playing-track-changed rights-changed",
   },
   {
-    "notescross.png",                   /* icon */
-    "Disable play",                     /* tip */
-    0,                                  /* menuitem */
-    update_disable,                     /* update */
-    disorder_eclient_disable,           /* action */
-    ICON_ACTIVE,                        /* flags */
-    0,                                  /* button */
-    0                                   /* item */
+    icon_on: "randomcross.png",
+    tip_on: "Disable random play",
+    icon_off: "random.png",
+    tip_off: "Enable random play",
+    menuitem: "<GdisorderMain>/Control/Random play",
+    on: random_enabled,
+    sensitive: random_sensitive,
+    action_go_on: disorder_eclient_random_enable,
+    action_go_off: disorder_eclient_random_disable,
+    events: "random-changed rights-changed",
   },
   {
-    "speaker.png",                      /* icon */
-    "Play network stream",              /* tip */
-    "<GdisorderMain>/Control/Network player", /* menuitem */
-    update_rtp,                         /* update */
-    enable_rtp,                         /* action */
-    ICON_INACTIVE,                      /* flags */
-    0,                                  /* button */
-    0                                   /* item */
+    icon_on: "notescross.png",
+    tip_on: "Disable play",
+    icon_off: "notes.png",
+    tip_off: "Enable play",
+    on: playing_enabled,
+    sensitive: playing_sensitive,
+    action_go_on: disorder_eclient_enable,
+    action_go_off: disorder_eclient_disable,
+    events: "enabled-changed rights-changed",
   },
   {
-    "speakercross.png",                 /* icon */
-    "Stop playing network stream",      /* tip */
-    "<GdisorderMain>/Control/Network player", /* menuitem */
-    update_nortp,                       /* update */
-    disable_rtp,                        /* action */
-    ICON_ACTIVE,                        /* flags */
-    0,                                  /* button */
-    0                                   /* item */
+    icon_on: "speakercross.png",
+    tip_on: "Stop playing network stream",
+    icon_off: "speaker.png",
+    tip_off: "Play network stream",
+    menuitem: "<GdisorderMain>/Control/Network player",
+    on: rtp_enabled,
+    sensitive: rtp_sensitive,
+    action_go_on: enable_rtp,
+    action_go_off: disable_rtp,
+    events: "rtp-changed",
   },
 };
 
@@ -211,73 +233,58 @@ static GtkAdjustment *balance_adj;
 static GtkWidget *volume_widget;
 static GtkWidget *balance_widget;
 
-/** @brief Called whenever last_state changes in any way */
-void control_monitor(void attribute((unused)) *u) {
-  int n;
-  gboolean volume_supported;
-
-  D(("control_monitor"));
-  for(n = 0; n < NICONS; ++n)
-    icons[n].update(&icons[n]);
-  /* Only display volume/balance controls if they will work */
-  if(!rtp_supported
-     || (rtp_supported && mixer_supported(DEFAULT_BACKEND)))
-    volume_supported = TRUE;
-  else
-    volume_supported = FALSE;
-  (volume_supported ? gtk_widget_show : gtk_widget_hide)(volume_widget);
-  (volume_supported ? gtk_widget_show : gtk_widget_hide)(balance_widget);
-}
-
 /** @brief Create the control bar */
 GtkWidget *control_widget(void) {
   GtkWidget *hbox = gtk_hbox_new(FALSE, 1), *vbox;
   int n;
 
-  NW(hbox);
   D(("control_widget"));
   assert(mainmenufactory);              /* ordering must be right */
   for(n = 0; n < NICONS; ++n) {
-    NW(button);
-    icons[n].button = iconbutton(icons[n].icon, icons[n].tip);
+    /* Create the button */
+    icons[n].button = gtk_button_new();
+    gtk_widget_set_style(icons[n].button, tool_style);
+    icons[n].image_on = gtk_image_new_from_pixbuf(find_image(icons[n].icon_on));
+    gtk_widget_set_style(icons[n].image_on, tool_style);
+    g_object_ref(icons[n].image_on);
+    /* If it's a toggle icon, create the 'off' half too */
+    if(icons[n].icon_off) {
+      icons[n].image_off = gtk_image_new_from_pixbuf(find_image(icons[n].icon_off));
+      gtk_widget_set_style(icons[n].image_off, tool_style);
+      g_object_ref(icons[n].image_off);
+    }
     g_signal_connect(G_OBJECT(icons[n].button), "clicked",
                      G_CALLBACK(clicked_icon), &icons[n]);
     /* pop the icon in a vbox so it doesn't get vertically stretch if there are
      * taller things in the control bar */
-    NW(vbox);
     vbox = gtk_vbox_new(FALSE, 0);
     gtk_box_pack_start(GTK_BOX(vbox), icons[n].button, TRUE, FALSE, 0);
     gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 0);
     if(icons[n].menuitem) {
+      /* Find the menu item */
       icons[n].item = gtk_item_factory_get_widget(mainmenufactory,
                                                   icons[n].menuitem);
-      switch(icons[n].flags & (ICON_ACTIVE|ICON_INACTIVE)) {
-      case ICON_ACTIVE:
+      if(icons[n].icon_off)
         g_signal_connect(G_OBJECT(icons[n].item), "toggled",
                          G_CALLBACK(toggled_menu), &icons[n]);
-        break;
-      case ICON_INACTIVE:
-        /* Don't connect two instances of the signal! */
-        break;
-      default:
+      else
         g_signal_connect(G_OBJECT(icons[n].item), "activate",
                          G_CALLBACK(clicked_menu), &icons[n]);
-        break;
-      }
     }
+    /* Make sure the icon is updated when relevant things changed */
+    char **events = split(icons[n].events, 0, 0, 0, 0);
+    while(*events)
+      event_register(*events++, icon_changed, &icons[n]);
+    event_register("connected-changed", icon_changed, &icons[n]);
   }
   /* create the adjustments for the volume control */
-  NW(adjustment);
   volume_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, 0, goesupto,
                                                  goesupto / 20, goesupto / 20,
                                                  0));
-  NW(adjustment);
   balance_adj = GTK_ADJUSTMENT(gtk_adjustment_new(0, -1, 1,
                                                   0.2, 0.2, 0));
   /* the volume control */
-  NW(hscale);
   volume_widget = gtk_hscale_new(volume_adj);
-  NW(hscale);
   balance_widget = gtk_hscale_new(balance_adj);
   gtk_widget_set_style(volume_widget, tool_style);
   gtk_widget_set_style(balance_widget, tool_style);
@@ -302,141 +309,123 @@ GtkWidget *control_widget(void) {
                    G_CALLBACK(format_volume), 0);
   g_signal_connect(G_OBJECT(balance_widget), "format-value",
                    G_CALLBACK(format_balance), 0);
-  register_monitor(control_monitor, 0, -1UL);
+  event_register("volume-changed", volume_changed, 0);
+  event_register("rtp-changed", volume_changed, 0);
   return hbox;
 }
 
 /** @brief Update the volume control when it changes */
-void volume_update(void) {
+static void volume_changed(const char attribute((unused)) *event,
+                           void attribute((unused)) *eventdata,
+                           void attribute((unused)) *callbackdata) {
   double l, r;
+  gboolean volume_supported;
 
-  D(("volume_update"));
-  l = volume_l / 100.0;
-  r = volume_r / 100.0;
+  D(("volume_changed"));
   ++suppress_actions;
-  gtk_adjustment_set_value(volume_adj, volume(l, r) * goesupto);
-  gtk_adjustment_set_value(balance_adj, balance(l, r));
+  /* Only display volume/balance controls if they will work */
+  if(!rtp_supported
+     || (rtp_supported && mixer_supported(DEFAULT_BACKEND)))
+    volume_supported = TRUE;
+  else
+    volume_supported = FALSE;
+  /* TODO: if the server doesn't know how to set the volume [but isn't using
+   * network play] then we should have volume_supported = FALSE */
+  if(volume_supported) {
+    gtk_widget_show(volume_widget);
+    gtk_widget_show(balance_widget);
+    l = volume_l / 100.0;
+    r = volume_r / 100.0;
+    gtk_adjustment_set_value(volume_adj, volume(l, r) * goesupto);
+    gtk_adjustment_set_value(balance_adj, balance(l, r));
+  } else {
+    gtk_widget_hide(volume_widget);
+    gtk_widget_hide(balance_widget);
+  }
   --suppress_actions;
 }
 
 /** @brief Update the state of one of the control icons
- * @param icon Target icon
- * @param visible True if this version of the button should be visible
- * @param usable True if the button is currently usable
- *
- * Several of the icons, rather bizarrely, come in pairs: for instance exactly
- * one of the play and pause buttons is supposed to be visible at any given
- * moment.
- *
- * @p usable need not take into account server availability, that is done
- * automatically.
  */
-static void update_icon(const struct icon *icon,
-                        int visible, int usable) {
+static void icon_changed(const char attribute((unused)) *event,
+                         void attribute((unused)) *evendata,
+                         void *callbackdata) {
+  //fprintf(stderr, "icon_changed (%s)\n", event);
+  const struct icon *const icon = callbackdata;
+  int on = icon->on ? icon->on() : 1;
+  int sensitive = icon->sensitive ? icon->sensitive() : 1;
+  //fprintf(stderr, "sensitive->%d\n", sensitive);
+  GtkWidget *child, *newchild;
+
+  ++suppress_actions;
   /* If the connection is down nothing is ever usable */
   if(!(last_state & DISORDER_CONNECTED))
-    usable = 0;
-  (visible ? gtk_widget_show : gtk_widget_hide)(icon->button);
-  /* Only both updating usability if the button is visible */
-  if(visible)
-    gtk_widget_set_sensitive(icon->button, usable);
+    sensitive = 0;
+  //fprintf(stderr, "(checked connected) sensitive->%d\n", sensitive);
+  /* Replace the child */
+  newchild = on ? icon->image_on : icon->image_off;
+  child = gtk_bin_get_child(GTK_BIN(icon->button));
+  if(child != newchild) {
+    if(child)
+      gtk_container_remove(GTK_CONTAINER(icon->button), child);
+    gtk_container_add(GTK_CONTAINER(icon->button), newchild);
+    gtk_widget_show(newchild);
+  }
+  /* If you disable play or random play NOT via the icon (for instance, via the
+   * edit menu or via a completely separate command line invocation) then the
+   * icon shows up as insensitive.  Hover the mouse over it and the correct
+   * state is immediately displayed.  sensitive and GTK_WIDGET_SENSITIVE show
+   * it to be in the correct state, so I think this is may be a GTK+ bug. */
+  if(icon->tip_on)
+    gtk_tooltips_set_tip(tips, icon->button,
+                           on ? icon->tip_on : icon->tip_off, "");
+  gtk_widget_set_sensitive(icon->button, sensitive);
+  /* Icons with an associated menu item */
   if(icon->item) {
-    /* There's an associated menu item.  These are always visible, but may not
-     * be usable. */
-    if((icon->flags & (ICON_ACTIVE|ICON_INACTIVE)) == ICON_ACTIVE) {
-      /* The active half of a pair */
-      gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(icon->item), visible);
-    }
-    gtk_widget_set_sensitive(icon->item, usable);
+    if(icon->icon_off)
+      gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(icon->item), on);
+    gtk_widget_set_sensitive(icon->item, sensitive);
   }
+  --suppress_actions;
 }
 
-static void update_pause(const struct icon *icon) {
-  const int visible = !(last_state & DISORDER_TRACK_PAUSED);
-  const int usable = !!(last_state & DISORDER_PLAYING); /* TODO: might be a lie */
-  update_icon(icon, visible, usable);
-}
-
-static void update_play(const struct icon *icon) {
-  const int visible = !!(last_state & DISORDER_TRACK_PAUSED);
-  const int usable = !!(last_state & DISORDER_PLAYING);
-  update_icon(icon, visible, usable);
-}
-
-static void update_scratch(const struct icon *icon) {
-  const int visible = 1;
-  const int usable = !!(last_state & DISORDER_PLAYING);
-  update_icon(icon, visible, usable);
-}
-
-static void update_random_enable(const struct icon *icon) {
-  const int visible = !(last_state & DISORDER_RANDOM_ENABLED);
-  const int usable = 1;
-  update_icon(icon, visible, usable);
-}
-
-static void update_random_disable(const struct icon *icon) {
-  const int visible = !!(last_state & DISORDER_RANDOM_ENABLED);
-  const int usable = 1;
-  update_icon(icon, visible, usable);
-}
-
-static void update_enable(const struct icon *icon) {
-  const int visible = !(last_state & DISORDER_PLAYING_ENABLED);
-  const int usable = 1;
-  update_icon(icon, visible, usable);
-}
-
-static void update_disable(const struct icon *icon) {
-  const int visible = !!(last_state & DISORDER_PLAYING_ENABLED);
-  const int usable = 1;
-  update_icon(icon, visible, usable);
-}
-
-static void update_rtp(const struct icon *icon) {
-  const int visible = !rtp_is_running;
-  const int usable = rtp_supported;
-  update_icon(icon, visible, usable);
-}
-
-static void update_nortp(const struct icon *icon) {
-  const int visible = rtp_is_running;
-  const int usable = rtp_supported;
-  update_icon(icon, visible, usable);
+static void icon_action_completed(void attribute((unused)) *v,
+                                  const char *err) {
+  if(err)
+    popup_protocol_error(0, err);
 }
 
 static void clicked_icon(GtkButton attribute((unused)) *button,
                          gpointer userdata) {
   const struct icon *icon = userdata;
 
-  if(!suppress_actions)  
-    icon->action(client, 0, 0);
+  if(suppress_actions)
+    return;
+  if(!icon->on || icon->on())
+    icon->action_go_off(client, icon_action_completed, 0);
+  else
+    icon->action_go_on(client, icon_action_completed, 0);
 }
 
 static void clicked_menu(GtkMenuItem attribute((unused)) *menuitem,
                          gpointer userdata) {
-  const struct icon *icon = userdata;
-
-  if(!suppress_actions)
-    icon->action(client, 0, 0);
+  clicked_icon(NULL, userdata);
 }
 
-static void toggled_menu(GtkCheckMenuItem *menuitem,
+static void toggled_menu(GtkCheckMenuItem attribute((unused)) *menuitem,
                          gpointer userdata) {
-  const struct icon *icon = userdata;
-  size_t n;
+  clicked_icon(NULL, userdata);
+}
 
-  if(suppress_actions)
-    return;
-  /* This is a bit fiddlier than the others, we need to find the action for the
-   * new state.  If the new state is active then we want the ICON_INACTIVE
-   * version and vica versa. */
-  for(n = 0; n < NICONS; ++n)
-    if(icons[n].item == icon->item
-       && !!(icons[n].flags & ICON_INACTIVE) == !!menuitem->active)
-      break;
-  if(n < NICONS)
-    icons[n].action(client, 0, 0);
+/** @brief Called when a volume command completes */
+static void volume_completed(void attribute((unused)) *v,
+                             const char *err,
+                             int attribute((unused)) l,
+                             int attribute((unused)) r) {
+  if(err)
+    popup_protocol_error(0, err);
+  /* We don't set the UI's notion of the volume here, it is set from the log
+   * regardless of the reason it changed */
 }
 
 /** @brief Called when the volume has been adjusted */
@@ -459,8 +448,7 @@ static void volume_adjusted(GtkAdjustment attribute((unused)) *a,
     int l = nearbyint(left(v, b) * 100), r = nearbyint(right(v, b) * 100);
     mixer_control(DEFAULT_BACKEND, &l, &r, 1);
   } else
-    /* We don't want a reply, we'll get the actual new volume from the log. */
-    disorder_eclient_volume(client, 0,
+    disorder_eclient_volume(client, volume_completed,
                             nearbyint(left(v, b) * 100),
                             nearbyint(right(v, b) * 100),
                             0);
index d6aa599..ce6eb03 100644 (file)
@@ -78,6 +78,12 @@ static int nop_in_flight;
 /** @brief True if an rtp-address command is in flight */
 static int rtp_address_in_flight;
 
+/** @brief True if a rights lookup is in flight */
+static int rights_lookup_in_flight;
+
+/** @brief Current rights bitmap */
+rights_type last_rights;
+
 /** @brief Global tooltip group */
 GtkTooltips *tips;
 
@@ -90,11 +96,15 @@ int rtp_supported;
 /** @brief True if RTP play is enabled */
 int rtp_is_running;
 
-/** @brief Linked list of functions to call when we reset login parameters */
-static struct reset_callback_node {
-  struct reset_callback_node *next;
-  reset_callback *callback;
-} *resets;
+/** @brief Server version */
+const char *server_version;
+
+/** @brief Parsed server version */
+long server_version_bytes;
+
+static void check_rtp_address(const char *event,
+                              void *eventdata,
+                              void *callbackdata);
 
 /* Window creation --------------------------------------------------------- */
 
@@ -116,11 +126,15 @@ static gboolean delete_event(GtkWidget attribute((unused)) *widget,
  *
  * Updates the menu settings to correspond to the new page.
  */
-static void tab_switched(GtkNotebook attribute((unused)) *notebook,
+static void tab_switched(GtkNotebook *notebook,
                          GtkNotebookPage attribute((unused)) *page,
                          guint page_num,
                          gpointer attribute((unused)) user_data) {
-  menu_update(page_num);
+  GtkWidget *const tab = gtk_notebook_get_nth_page(notebook, page_num);
+  const struct tabtype *const t = g_object_get_data(G_OBJECT(tab), "type");
+  assert(t != 0);
+  if(t->selected)
+    t->selected();
 }
 
 /** @brief Create the report box */
@@ -188,109 +202,48 @@ static void make_toplevel_window(void) {
   gtk_widget_set_style(toplevel, tool_style);
 }
 
-#if MDEBUG
-static int widget_count, container_count;
-
-static void count_callback(GtkWidget *w,
-                           gpointer attribute((unused)) data) {
-  ++widget_count;
-  if(GTK_IS_CONTAINER(w)) {
-    ++container_count;
-    gtk_container_foreach(GTK_CONTAINER(w), count_callback, 0);
+static void userinfo_rights_completed(void attribute((unused)) *v,
+                                      const char *err,
+                                      const char *value) {
+  rights_type r;
+
+  if(err) {
+    popup_protocol_error(0, err);
+    r = 0;
+  } else {
+    if(parse_rights(value, &r, 0))
+      r = 0;
   }
-}
-
-static void count_widgets(void) {
-  widget_count = 0;
-  container_count = 1;
-  if(toplevel)
-    gtk_container_foreach(GTK_CONTAINER(toplevel), count_callback, 0);
-  fprintf(stderr, "widget count: %8d  container count: %8d\n",
-          widget_count, container_count);
-}
-#endif
-
-#if MTRACK
-const char *mtag = "init";
-static hash *mtrack_hash;
-
-static int *mthfind(const char *tag) {
-  static const int zero = 0;
-  int *cp = hash_find(mtrack_hash, tag);
-  if(!cp) {
-    hash_add(mtrack_hash, tag, &zero, HASH_INSERT);
-    cp = hash_find(mtrack_hash, tag);
+  /* If rights have changed, signal everything that cares */
+  if(r != last_rights) {
+    last_rights = r;
+    ++suppress_actions;
+    event_raise("rights-changed", 0);
+    --suppress_actions;
   }
-  return cp;
+  rights_lookup_in_flight = 0;
 }
 
-static void *trap_malloc(size_t n) {
-  void *ptr = malloc(n + sizeof(char *));
-
-  *(const char **)ptr = mtag;
-  ++*mthfind(mtag);
-  return (char *)ptr + sizeof(char *);
-}
-
-static void trap_free(void *ptr) {
-  const char *tag;
-  if(!ptr)
-    return;
-  ptr = (char *)ptr - sizeof(char *);
-  tag = *(const char **)ptr;
-  --*mthfind(tag);
-  free(ptr);
-}
-
-static void *trap_realloc(void *ptr, size_t n) {
-  if(!ptr)
-    return trap_malloc(n);
-  if(!n) {
-    trap_free(ptr);
-    return 0;
+static void check_rights(void) {
+  if(!rights_lookup_in_flight) {
+    rights_lookup_in_flight = 1;
+    disorder_eclient_userinfo(client,
+                              userinfo_rights_completed,
+                              config->username, "rights",
+                              0);
   }
-  ptr = (char *)ptr - sizeof(char *);
-  ptr = realloc(ptr, n + sizeof(char *));
-  *(const char **)ptr = mtag;
-  return (char *)ptr + sizeof(char *);
-}
-
-static int report_tags_callback(const char *key, void *value,
-                                void attribute((unused)) *u) {
-  fprintf(stderr, "%16s: %d\n", key, *(int *)value);
-  return 0;
-}
-
-static void report_tags(void) {
-  hash_foreach(mtrack_hash, report_tags_callback, 0);
-  fprintf(stderr, "\n");
 }
 
-static const GMemVTable glib_memvtable = {
-  trap_malloc,
-  trap_realloc,
-  trap_free,
-  0,
-  0,
-  0
-};
-#endif
-
 /** @brief Called occasionally */
 static gboolean periodic_slow(gpointer attribute((unused)) data) {
-  D(("periodic"));
+  D(("periodic_slow"));
   /* Expire cached data */
   cache_expire();
   /* Update everything to be sure that the connection to the server hasn't
    * mysteriously gone stale on us. */
   all_update();
-#if MDEBUG
-  count_widgets();
-  fprintf(stderr, "cache size: %zu\n", cache_count());
-#endif
-#if MTRACK
-  report_tags();
-#endif
+  /* Recheck RTP status too */
+  check_rtp_address(0, 0, 0);
   return TRUE;                          /* don't remove me */
 }
 
@@ -318,14 +271,26 @@ static gboolean periodic_fast(gpointer attribute((unused)) data) {
        && (nl != volume_l || nr != volume_r)) {
       volume_l = nl;
       volume_r = nr;
-      volume_update();
+      event_raise("volume-changed", 0);
     }
   }
+  /* Periodically check what our rights are */
+  int recheck_rights = 1;
+  if(server_version_bytes >= 0x04010000)
+    /* Server versions after 4.1 will send updates */
+    recheck_rights = 0;
+  if((server_version_bytes & 0xFF) == 0x01)
+    /* Development servers might do regardless of their version number */
+    recheck_rights = 0;
+  if(recheck_rights)
+    check_rights();
   return TRUE;
 }
 
 /** @brief Called when a NOP completes */
-static void nop_completed(void attribute((unused)) *v) {
+static void nop_completed(void attribute((unused)) *v,
+                          const char attribute((unused)) *err) {
+  /* TODO report the error somewhere */
   nop_in_flight = 0;
 }
 
@@ -340,44 +305,47 @@ static gboolean maybe_send_nop(gpointer attribute((unused)) data) {
     disorder_eclient_nop(client, nop_completed, 0);
   }
   if(rtp_supported) {
-    const int old_state = rtp_is_running;
+    const int rtp_was_running = rtp_is_running;
     rtp_is_running = rtp_running();
-    if(old_state != rtp_is_running)
-      control_monitor(0);
+    if(rtp_was_running != rtp_is_running)
+      event_raise("rtp-changed", 0);
   }
   return TRUE;                          /* keep call me please */
 }
 
 /** @brief Called when a rtp-address command succeeds */
 static void got_rtp_address(void attribute((unused)) *v,
+                            const char *err,
                             int attribute((unused)) nvec,
                             char attribute((unused)) **vec) {
-  ++suppress_actions;
-  rtp_address_in_flight = 0;
-  rtp_supported = 1;
-  rtp_is_running = rtp_running();
-  control_monitor(0);
-  --suppress_actions;
-}
+  const int rtp_was_supported = rtp_supported;
+  const int rtp_was_running = rtp_is_running;
 
-/** @brief Called when a rtp-address command fails */
-static void no_rtp_address(struct callbackdata attribute((unused)) *cbd,
-                           int attribute((unused)) code,
-                           const char attribute((unused)) *msg) {
   ++suppress_actions;
   rtp_address_in_flight = 0;
-  rtp_supported = 0;
-  rtp_is_running = 0;
-  control_monitor(0);
+  if(err) {
+    /* An error just means that we're not using network play */
+    rtp_supported = 0;
+    rtp_is_running = 0;
+  } else {
+    rtp_supported = 1;
+    rtp_is_running = rtp_running();
+  }
+  /*fprintf(stderr, "rtp supported->%d, running->%d\n",
+          rtp_supported, rtp_is_running);*/
+  if(rtp_supported != rtp_was_supported
+     || rtp_is_running != rtp_was_running)
+    event_raise("rtp-changed", 0);
   --suppress_actions;
 }
 
 /** @brief Called to check whether RTP play is available */
-static void check_rtp_address(void) {
+static void check_rtp_address(const char attribute((unused)) *event,
+                              void attribute((unused)) *eventdata,
+                              void attribute((unused)) *callbackdata) {
   if(!rtp_address_in_flight) {
-    struct callbackdata *const cbd = xmalloc(sizeof *cbd);
-    cbd->onerror = no_rtp_address;
-    disorder_eclient_rtp_address(client, got_rtp_address, cbd);
+    //fprintf(stderr, "checking rtp\n");
+    disorder_eclient_rtp_address(client, got_rtp_address, NULL);
   }
 }
 
@@ -409,27 +377,51 @@ static void help(void) {
   exit(0);
 }
 
-/* reset state */
-void reset(void) {
-  struct reset_callback_node *r;
+static void version_completed(void attribute((unused)) *v,
+                              const char attribute((unused)) *err,
+                              const char *ver) {
+  long major, minor, patch, dev;
 
+  if(!ver) {
+    server_version = 0;
+    server_version_bytes = 0;
+    return;
+  }
+  server_version = ver;
+  server_version_bytes = 0;
+  major = strtol(ver, (char **)&ver, 10);
+  if(*ver != '.')
+    return;
+  ++ver;
+  minor = strtol(ver, (char **)&ver, 10);
+  if(*ver == '.') {
+    ++ver;
+    patch = strtol(ver, (char **)&ver, 10);
+  } else
+    patch = 0;
+  if(*ver) {
+    if(*ver == '+') {
+      dev = 1;
+      ++ver;
+    }
+    if(*ver)
+      dev = 2;
+  } else
+    dev = 0;
+  server_version_bytes = (major << 24) + (minor << 16) + (patch << 8) + dev;
+}
+
+void logged_in(void) {
   /* reset the clients */
   disorder_eclient_close(client);
   disorder_eclient_close(logclient);
   rtp_supported = 0;
-  for(r = resets; r; r = r->next)
-    r->callback();
-  /* Might be a new server so re-check */
-  check_rtp_address();
-}
-
-/** @brief Register a reset callback */
-void register_reset(reset_callback *callback) {
-  struct reset_callback_node *const r = xmalloc(sizeof *r);
-
-  r->next = resets;
-  r->callback = callback;
-  resets = r;
+  event_raise("logged-in", 0);
+  /* Force the periodic checks */
+  periodic_slow(0);
+  periodic_fast(0);
+  /* Recheck server version */
+  disorder_eclient_version(client, version_completed, 0);
 }
 
 int main(int argc, char **argv) {
@@ -440,10 +432,6 @@ int main(int argc, char **argv) {
   /* garbage-collect PCRE's memory */
   pcre_malloc = xmalloc;
   pcre_free = xfree;
-#if MTRACK
-  mtrack_hash = hash_new(sizeof (int));
-  g_mem_set_vtable((GMemVTable *)&glib_memvtable);
-#endif
   if(!setlocale(LC_CTYPE, "")) fatal(errno, "error calling setlocale");
   gtkok = gtk_init_check(&argc, &argv);
   while((n = getopt_long(argc, argv, "hVc:dtHC", options, 0)) >= 0) {
@@ -484,17 +472,17 @@ int main(int argc, char **argv) {
                      maybe_send_nop,
                      0/*data*/,
                      0/*notify*/);
-  register_reset(properties_reset);
   /* Start monitoring the log */
   disorder_eclient_log(logclient, &log_callbacks, 0);
-  /* See if RTP play supported */
-  check_rtp_address();
+  /* Initiate all the checks */
+  periodic_fast(0);
+  disorder_eclient_version(client, version_completed, 0);
+  event_register("log-connected", check_rtp_address, 0);
   suppress_actions = 0;
   /* If no password is set yet pop up a login box */
   if(!config->password)
     login_box();
   D(("enter main loop"));
-  MTAG("misc");
   g_main_loop_run(mainloop);
   return 0;
 }
index db1e8e9..1f63108 100644 (file)
 #include "configuration.h"
 #include "hash.h"
 #include "selection.h"
+#include "kvp.h"
+#include "eventdist.h"
+#include "split.h"
+#include "timeval.h"
 
 #include <glib.h>
 #include <gtk/gtk.h>
@@ -84,12 +88,17 @@ struct callbackdata {
  * have some callbacks to set them appropriately.
  */
 struct tabtype {
-  int (*properties_sensitive)(GtkWidget *tab);
-  int (*selectall_sensitive)(GtkWidget *tab);
-  int (*selectnone_sensitive)(GtkWidget *tab);
-  void (*properties_activate)(GtkWidget *tab);
-  void (*selectall_activate)(GtkWidget *tab);
-  void (*selectnone_activate)(GtkWidget *tab);
+  int (*properties_sensitive)(void *extra);
+  int (*selectall_sensitive)(void *extra);
+  int (*selectnone_sensitive)(void *extra);
+  void (*properties_activate)(GtkMenuItem *menuitem,
+                              gpointer user_data);
+  void (*selectall_activate)(GtkMenuItem *menuitem,
+                             gpointer user_data);
+  void (*selectnone_activate)(GtkMenuItem *menuitem,
+                              gpointer user_data);
+  void (*selected)(void);
+  void *extra;
 };
 
 /** @brief Button definitions */
@@ -109,6 +118,7 @@ extern GtkWidget *tabs;                 /* main tabs */
 extern disorder_eclient *client;        /* main client */
 
 extern unsigned long last_state;        /* last reported state */
+extern rights_type last_rights;         /* last reported rights bitmap */
 extern int playing;                     /* true if playing some track */
 extern int volume_l, volume_r;          /* current volume */
 extern double goesupto;                 /* volume upper bound */
@@ -120,8 +130,6 @@ extern GtkItemFactory *mainmenufactory;
 
 extern const disorder_eclient_log_callbacks log_callbacks;
 
-typedef void monitor_callback(void *u);
-
 /* Functions --------------------------------------------------------------- */
 
 disorder_eclient *gtkclient(void);
@@ -134,8 +142,6 @@ void popup_protocol_error(int code,
 void properties(int ntracks, const char **tracks);
 /* Pop up a properties window for a list of tracks */
 
-void properties_reset(void);
-
 GtkWidget *scroll_widget(GtkWidget *child);
 /* Wrap a widget up for scrolling */
 
@@ -166,18 +172,7 @@ GtkWidget *create_buttons_box(struct button *buttons,
                               size_t nbuttons,
                               GtkWidget *box);
 
-void register_monitor(monitor_callback *callback,
-                      void *u,
-                      unsigned long mask);
-/* Register a state monitor */
-
-/** @brief Type signature for a reset callback */
-typedef void reset_callback(void);
-
-void register_reset(reset_callback *callback);
-/* Register a reset callback */
-
-void reset(void);
+void logged_in(void);
 
 void all_update(void);
 /* Update everything */
@@ -186,10 +181,6 @@ void all_update(void);
 
 GtkWidget *menubar(GtkWidget *w);
 /* Create the menu bar */
-     
-void menu_update(int page);
-/* Called whenever the main menu might need to change.  PAGE is the current
- * page if known or -1 otherwise. */
 
 void users_set_sensitive(int sensitive);
 
@@ -198,11 +189,6 @@ void users_set_sensitive(int sensitive);
 GtkWidget *control_widget(void);
 /* Make the controls widget */
 
-void volume_update(void);
-/* Called whenever we think the volume control has changed */
-
-void control_monitor(void *u);
-
 extern int suppress_actions;
 
 /* Queue/Recent/Added */
@@ -213,12 +199,6 @@ GtkWidget *added_widget(void);
 /* Create widgets for displaying the queue, the recently played list and the
  * newly added tracks list */
 
-void queue_update(void);
-void recent_update(void);
-void added_update(void);
-/* Called whenever we think the queue, recent or newly-added list might have
- * changed */
-
 void queue_select_all(struct queuelike *ql);
 void queue_select_none(struct queuelike *ql);
 /* Select all/none on some queue */
@@ -229,12 +209,20 @@ void queue_properties(struct queuelike *ql);
 int queued(const char *track);
 /* Return nonzero iff TRACK is queued or playing */
 
+extern struct queue_entry *playing_track;
+
+/* Lookups */
+const char *namepart(const char *track,
+                     const char *context,
+                     const char *part);
+long namepart_length(const char *track);
+char *namepart_resolve(const char *track);
+
 void namepart_update(const char *track,
                      const char *context,
                      const char *part);
 /* Called when a namepart might have changed */
 
-
 /* Choose */
 
 GtkWidget *choose_widget(void);
@@ -243,6 +231,9 @@ GtkWidget *choose_widget(void);
 void choose_update(void);
 /* Called when we think the choose tree might need updating */
 
+void play_completed(void *v,
+                    const char *err);
+
 /* Login details */
 
 void login_box(void);
@@ -282,35 +273,6 @@ void load_settings(void);
 void set_tool_colors(GtkWidget *w);
 void popup_settings(void);
 
-/* Widget leakage debugging rubbish ---------------------------------------- */
-
-#if MDEBUG
-#define NW(what) do {                                   \
-  if(++current##what % 100 > max##what) {               \
-    fprintf(stderr, "%s:%d: %d %s\n",                   \
-            __FILE__, __LINE__, current##what, #what);  \
-    max##what = current##what;                          \
-  }                                                     \
-} while(0)
-#define WT(what) static int current##what, max##what
-#define DW(what) (--current##what)
-#else
-#define NW(what) do { } while(0)
-#define DW(what) do { } while(0)
-#define WT(what) struct neverused
-#endif
-
-#if MTRACK
-extern const char *mtag;
-#define MTAG(x) do { mtag = x; } while(0)
-#define MTAG_PUSH(x) do { const char *save_mtag = mtag; mtag = x; (void)0
-#define MTAG_POP() mtag = save_mtag; } while(0)
-#else
-#define MTAG(x) do { } while(0)
-#define MTAG_PUSH(x) do {} while(0)
-#define MTAG_POP() do {} while(0)
-#endif
-
 #endif /* DISOBEDIENCE_H */
 
 /*
index f1822b9..a182940 100644 (file)
@@ -1,3 +1,4 @@
+
 /*
  * This file is part of DisOrder
  * Copyright (C) 2007, 2008 Richard Kettlewell
index b0aaace..6779cbd 100644 (file)
@@ -42,52 +42,33 @@ static void log_scratched(void *v, const char *track, const char *user);
 static void log_state(void *v, unsigned long state);
 static void log_volume(void *v, int l, int r);
 static void log_rescanned(void *v);
+static void log_rights_changed(void *v, rights_type r);
 
 /** @brief Callbacks for server state monitoring */
 const disorder_eclient_log_callbacks log_callbacks = {
-  log_connected,
-  log_completed,
-  log_failed,
-  log_moved,
-  log_playing,
-  log_queue,
-  log_recent_added,
-  log_recent_removed,
-  log_removed,
-  log_scratched,
-  log_state,
-  log_volume,
-  log_rescanned
+  .connected = log_connected,
+  .completed = log_completed,
+  .failed = log_failed,
+  .moved = log_moved,
+  .playing = log_playing,
+  .queue = log_queue,
+  .recent_added = log_recent_added,
+  .recent_removed = log_recent_removed,
+  .removed = log_removed,
+  .scratched = log_scratched,
+  .state = log_state,
+  .volume = log_volume,
+  .rescanned = log_rescanned,
+  .rights_changed = log_rights_changed
 };
 
-/** @brief State monitor
- *
- * We keep a linked list of everything that is interested in state changes.
- */
-struct monitor {
-  /** @brief Next monitor */
-  struct monitor *next;
-
-  /** @brief State bits of interest */
-  unsigned long mask;
-
-  /** @brief Function to call if any of @c mask change */
-  monitor_callback *callback;
-
-  /** @brief User data for callback */
-  void *u;
-};
-
-/** @brief List of monitors */
-static struct monitor *monitors;
-
 /** @brief Update everything */
 void all_update(void) {
   ++suppress_actions;
-  queue_update();
-  recent_update();
-  volume_update();
-  added_update();
+  event_raise("queue-changed", 0);
+  event_raise("recent-changed", 0);
+  event_raise("volume-changed", 0);
+  event_raise("rescan-complete", 0);
   --suppress_actions;
 }
 
@@ -96,14 +77,13 @@ void all_update(void) {
  * Depending on server and network state the TCP connection to the server may
  * go up or down many times during the lifetime of Disobedience.  This function
  * is called whenever it connects.
- *
- * The intent is to use the monitor logic to achieve this in future.
  */
 static void log_connected(void attribute((unused)) *v) {
   /* Don't know what we might have missed while disconnected so update
    * everything.  We get this at startup too and this is how we do the initial
    * state fetch. */
   all_update();
+  event_raise("log-connected", 0);
 }
 
 /** @brief Called when the current track finishes playing */
@@ -120,7 +100,7 @@ static void log_failed(void attribute((unused)) *v,
 /** @brief Called when some track is moved within the queue */
 static void log_moved(void attribute((unused)) *v,
                       const char attribute((unused)) *user) {
-  queue_update();
+  event_raise("queue-changed", 0);
 }
 
 static void log_playing(void attribute((unused)) *v,
@@ -131,13 +111,13 @@ static void log_playing(void attribute((unused)) *v,
 /** @brief Called when a track is added to the queue */
 static void log_queue(void attribute((unused)) *v,
                       struct queue_entry attribute((unused)) *q) {
-  queue_update();
+  event_raise("queue-changed", 0);
 }
 
 /** @brief Called when a track is added to the recently-played list */
 static void log_recent_added(void attribute((unused)) *v,
                              struct queue_entry attribute((unused)) *q) {
-  recent_update();
+  event_raise("recent-changed", 0);
 }
 
 /** @brief Called when a track is removed from the recently-played list
@@ -153,8 +133,7 @@ static void log_recent_removed(void attribute((unused)) *v,
 static void log_removed(void attribute((unused)) *v,
                         const char attribute((unused)) *id,
                         const char attribute((unused)) *user) {
-
-  queue_update();
+  event_raise("queue-changed", 0);
 }
 
 /** @brief Called when the current track is scratched */
@@ -163,10 +142,22 @@ static void log_scratched(void attribute((unused)) *v,
                           const char attribute((unused)) *user) {
 }
 
+/** @brief Map from state bits to state change events */
+static const struct {
+  unsigned long bit;
+  const char *event;
+} state_events[] = {
+  { DISORDER_PLAYING_ENABLED, "enabled-changed" },
+  { DISORDER_RANDOM_ENABLED, "random-changed" },
+  { DISORDER_TRACK_PAUSED, "pause-changed" },
+  { DISORDER_PLAYING, "playing-changed" },
+  { DISORDER_CONNECTED, "connected-changed" },
+};
+#define NSTATE_EVENTS (sizeof state_events / sizeof *state_events)
+
 /** @brief Called when a state change occurs */
 static void log_state(void attribute((unused)) *v,
                       unsigned long state) {
-  const struct monitor *m;
   unsigned long changes = state ^ last_state;
   static int first = 1;
 
@@ -180,11 +171,10 @@ static void log_state(void attribute((unused)) *v,
      disorder_eclient_interpret_state(state),
      disorder_eclient_interpret_state(changes)));
   last_state = state;
-  /* Tell anything that cares about the state change */
-  for(m = monitors; m; m = m->next) {
-    if(changes & m->mask)
-      m->callback(m->u);
-  }
+  /* Notify interested parties what has changed */
+  for(unsigned n = 0; n < NSTATE_EVENTS; ++n)
+    if(changes & state_events[n].bit)
+      event_raise(state_events[n].event, 0);
   --suppress_actions;
 }
 
@@ -195,33 +185,23 @@ static void log_volume(void attribute((unused)) *v,
     volume_l = l;
     volume_r = r;
     ++suppress_actions;
-    volume_update();
+    event_raise("volume-changed", 0);
     --suppress_actions;
   }
 }
 
 /** @brief Called when a rescan completes */
 static void log_rescanned(void attribute((unused)) *v) {
-  added_update();
+  event_raise("rescan-complete", 0);
 }
 
-/** @brief Add a monitor to the list
- * @param callback Function to call
- * @param u User data to pass to @p callback
- * @param mask Mask of flags that @p callback cares about
- *
- * Pass @p mask as -1UL to match all flags.
- */
-void register_monitor(monitor_callback *callback,
-                      void *u,
-                      unsigned long mask) {
-  struct monitor *m = xmalloc(sizeof *m);
-
-  m->next = monitors;
-  m->mask = mask;
-  m->callback = callback;
-  m->u = u;
-  monitors = m;
+/** @brief Called when our rights change */
+static void log_rights_changed(void attribute((unused)) *v,
+                               rights_type new_rights) {
+  ++suppress_actions;
+  last_rights = new_rights;
+  event_raise("rights-changed", 0);
+  --suppress_actions;
 }
 
 /*
index 18afce4..b1047d6 100644 (file)
  * window remains.
  *
  * It you hit Cancel then the window disappears without saving anything.
+ *
+ * TODO
+ * - escape and return should work
+ * - cancel/close should be consistent with properties
  */
 
 #include "disobedience.h"
@@ -153,7 +157,7 @@ static void login_ok(GtkButton attribute((unused)) *button,
   if(!disorder_connect(c)) {
     /* Success; save the config and start using it */
     login_save_config();
-    reset();
+    logged_in();
     /* Pop down login window */
     gtk_widget_destroy(login_window);
   } else {
diff --git a/disobedience/lookup.c b/disobedience/lookup.c
new file mode 100644 (file)
index 0000000..9b6a5b5
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "disobedience.h"
+
+static int namepart_lookups_outstanding;
+static const struct cache_type cachetype_string = { 3600 };
+static const struct cache_type cachetype_integer = { 3600 };
+
+/** @brief Called when a namepart lookup has completed or failed
+ *
+ * When there are no lookups in flight a redraw is provoked.  This might well
+ * provoke further lookups.
+ */
+static void namepart_completed_or_failed(void) {
+  --namepart_lookups_outstanding;
+  if(!namepart_lookups_outstanding)
+    /* When all lookups complete, we update any displays that care */
+    event_raise("lookups-completed", 0);
+}
+
+/** @brief Called when a namepart lookup has completed */
+static void namepart_completed(void *v, const char *err, const char *value) {
+  D(("namepart_completed"));
+  if(err) {
+    gtk_label_set_text(GTK_LABEL(report_label), err);
+    value = "?";
+  }
+  const char *key = v;
+  
+  cache_put(&cachetype_string, key, value);
+  namepart_completed_or_failed();
+}
+
+/** @brief Called when a length lookup has completed */
+static void length_completed(void *v, const char *err, long l) {
+  D(("length_completed"));
+  if(err) {
+    gtk_label_set_text(GTK_LABEL(report_label), err);
+    l = -1;
+  }
+  const char *key = v;
+  long *value;
+  
+  D(("namepart_completed"));
+  value = xmalloc(sizeof *value);
+  *value = l;
+  cache_put(&cachetype_integer, key, value);
+  namepart_completed_or_failed();
+}
+
+/** @brief Arrange to fill in a namepart cache entry */
+static void namepart_fill(const char *track,
+                          const char *context,
+                          const char *part,
+                          const char *key) {
+  D(("namepart_fill %s %s %s %s", track, context, part, key));
+  ++namepart_lookups_outstanding;
+  D(("namepart_lookups_outstanding -> %d\n", namepart_lookups_outstanding));
+  disorder_eclient_namepart(client, namepart_completed,
+                            track, context, part, (void *)key);
+}
+
+/** @brief Look up a namepart
+ * @param track Track name
+ * @param context Context
+ * @param part Name part
+ * @param lookup If nonzero, will schedule a lookup for unknown values
+ *
+ * If it is in the cache then just return its value.  If not then look it up
+ * and arrange for the queues to be updated when its value is available. */
+const char *namepart(const char *track,
+                     const char *context,
+                     const char *part) {
+  char *key;
+  const char *value;
+
+  D(("namepart %s %s %s", track, context, part));
+  byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
+                 context, part, track);
+  value = cache_get(&cachetype_string, key);
+  if(!value) {
+    D(("deferring..."));
+    namepart_fill(track, context, part, key);
+    value = "?";
+  }
+  return value;
+}
+
+/** @brief Called from @ref disobedience/properties.c when we know a name part has changed */
+void namepart_update(const char *track,
+                     const char *context,
+                     const char *part) {
+  char *key;
+
+  byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
+                 context, part, track);
+  /* Only refetch if it's actually in the cache. */
+  if(cache_get(&cachetype_string, key))
+    namepart_fill(track, context, part, key);
+}
+
+/** @brief Look up a track length
+ *
+ * If it is in the cache then just return its value.  If not then look it up
+ * and arrange for the queues to be updated when its value is available. */
+long namepart_length(const char *track) {
+  char *key;
+  const long *value;
+
+  D(("getlength %s", track));
+  byte_xasprintf(&key, "length track=%s", track);
+  value = cache_get(&cachetype_integer, key);
+  if(value)
+    return *value;
+  D(("deferring..."));;
+  ++namepart_lookups_outstanding;
+  D(("namepart_lookups_outstanding -> %d\n", namepart_lookups_outstanding));
+  disorder_eclient_length(client, length_completed, track, key);
+  return -1;
+}
+
+/** @brief Resolve a track name
+ *
+ * Returns the supplied track name if it doesn't have the answer yet.
+ */
+char *namepart_resolve(const char *track) {
+  char *key;
+
+  byte_xasprintf(&key, "resolve track=%s", track);
+  const char *value = cache_get(&cachetype_string, key);
+  if(!value) {
+    D(("deferring..."));
+    ++namepart_lookups_outstanding;
+    D(("namepart_lookups_outstanding -> %d\n", namepart_lookups_outstanding));
+    disorder_eclient_resolve(client, namepart_completed,
+                             track, (void *)key);
+    value = track;
+  }
+  return xstrdup(value);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index b80537e..142857c 100644 (file)
@@ -30,7 +30,9 @@ static GtkWidget *properties_widget;
 /** @brief Main menu widgets */
 GtkItemFactory *mainmenufactory;
 
-static void about_popup_got_version(void *v, const char *value);
+static void about_popup_got_version(void *v,
+                                    const char *err,
+                                    const char *value);
 
 /** @brief Called when the quit option is activated
  *
@@ -43,48 +45,24 @@ static void quit_program(gpointer attribute((unused)) callback_data,
   exit(0);
 }
 
-/* TODO can we have a single parameterized callback for all these */
-
-/** @brief Called when the select all option is activated
- *
- * Calls the per-tab select all function.
- */
-static void select_all(gpointer attribute((unused)) callback_data,
-                       guint attribute((unused)) callback_action,
-                       GtkWidget attribute((unused)) *menu_item) {
-  GtkWidget *tab = gtk_notebook_get_nth_page
-    (GTK_NOTEBOOK(tabs), gtk_notebook_current_page(GTK_NOTEBOOK(tabs)));
-  const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type");
-
-  t->selectall_activate(tab);
-}
-
-/** @brief Called when the select none option is activated
- *
- * Calls the per-tab select none function.
- */
-static void select_none(gpointer attribute((unused)) callback_data,
-                        guint attribute((unused)) callback_action,
-                        GtkWidget attribute((unused)) *menu_item) {
-  GtkWidget *tab = gtk_notebook_get_nth_page
-    (GTK_NOTEBOOK(tabs), gtk_notebook_current_page(GTK_NOTEBOOK(tabs)));
-  const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type");
-
-  t->selectnone_activate(tab);
-}
-
-/** @brief Called when the track properties option is activated
+/** @brief Called when an edit menu item is selected
  *
- * Calls the per-tab properties function.
+ * Shared by several menu items; callback_action is expected to be the offset
+ * of the activate member of struct tabtype.
  */
-static void properties_item(gpointer attribute((unused)) callback_data,
-                            guint attribute((unused)) callback_action,
+static void menu_tab_action(gpointer attribute((unused)) callback_data,
+                            guint callback_action,
                             GtkWidget attribute((unused)) *menu_item) {
   GtkWidget *tab = gtk_notebook_get_nth_page
     (GTK_NOTEBOOK(tabs), gtk_notebook_current_page(GTK_NOTEBOOK(tabs)));
   const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type");
 
-  t->properties_activate(tab);
+  void (**activatep)(GtkMenuItem *, gpointer)
+    = (void *)((const char *)t + callback_action);
+  void (*activate)(GtkMenuItem *, gpointer) = *activatep;
+  
+  if(activate)
+    activate(NULL, t->extra);
 }
 
 /** @brief Called when the login option is activated */
@@ -110,27 +88,31 @@ static void settings(gpointer attribute((unused)) callback_data,
 }
 #endif
 
-/** @brief Update menu state
+/** @brief Called when edit menu is shown
  *
  * Determines option sensitivity according to the current tab and adjusts the
  * widgets accordingly.  Knows about @ref DISORDER_CONNECTED so the callbacks
  * need not.
  */
-void menu_update(int page) {
+static void edit_menu_show(GtkWidget attribute((unused)) *widget,
+                           gpointer attribute((unused)) user_data) {
   if(tabs) {
     GtkWidget *tab = gtk_notebook_get_nth_page
       (GTK_NOTEBOOK(tabs),
-       page < 0 ? gtk_notebook_current_page(GTK_NOTEBOOK(tabs)) : page);
+       gtk_notebook_current_page(GTK_NOTEBOOK(tabs)));
     const struct tabtype *t = g_object_get_data(G_OBJECT(tab), "type");
 
     assert(t != 0);
     gtk_widget_set_sensitive(properties_widget,
-                             (t->properties_sensitive(tab)
+                             (t->properties_sensitive
+                              && t->properties_sensitive(t->extra)
                               && (disorder_eclient_state(client) & DISORDER_CONNECTED)));
     gtk_widget_set_sensitive(selectall_widget,
-                             t->selectall_sensitive(tab));
+                             t->selectall_sensitive
+                             && t->selectall_sensitive(t->extra));
     gtk_widget_set_sensitive(selectnone_widget,
-                             t->selectnone_sensitive(tab));
+                             t->selectnone_sensitive
+                             && t->selectnone_sensitive(t->extra));
   }
 }
    
@@ -156,12 +138,15 @@ static void manual_popup(gpointer attribute((unused)) callback_data,
 
 /** @brief Called when version arrives, displays about... popup */
 static void about_popup_got_version(void attribute((unused)) *v,
+                                    const char attribute((unused)) *err,
                                     const char *value) {
   GtkWidget *w;
   char *server_version_string;
   char *short_version_string;
   GtkWidget *hbox, *vbox, *title;
 
+  if(!value)
+    value = "[error]";
   byte_xasprintf(&server_version_string, "Server version %s", value);
   byte_xasprintf(&short_version_string, "Disobedience %s",
                  disorder_short_version_string);
@@ -223,20 +208,11 @@ void users_set_sensitive(int sensitive) {
   gtk_widget_set_sensitive(w, sensitive);
 }
 
-/** @brief Called with current user's rights string */
-static void menu_got_rights(void attribute((unused)) *v, const char *value) {
-  rights_type r;
-
-  if(parse_rights(value, &r, 0))
-    r = 0;
-  users_set_sensitive(!!(r & RIGHT_ADMIN));
-}
-
-/** @brief Called when we need to reset state */
-static void menu_reset(void) {
-  users_set_sensitive(0);               /* until we know better */
-  disorder_eclient_userinfo(client, menu_got_rights, config->username, "rights",
-                            0);
+/** @brief Called when our rights change */
+static void menu_rights_changed(const char attribute((unused)) *event,
+                                void attribute((unused)) *eventdata,
+                                void attribute((unused)) *callbackdata) {
+  users_set_sensitive(!!(last_rights & RIGHT_ADMIN));
 }
 
 /** @brief Create the menu bar widget */
@@ -297,25 +273,25 @@ GtkWidget *menubar(GtkWidget *w) {
     },
     {
       (char *)"/Edit/Select all tracks", /* path */
-      (char *)"<CTRL>A",                /* accelerator */
-      select_all,                       /* callback */
-      0,                                /* callback_action */
+      0,                                /* accelerator */
+      menu_tab_action,                  /* callback */
+      offsetof(struct tabtype, selectall_activate), /* callback_action */
       0,                                /* item_type */
       0                                 /* extra_data */
     },
     {
       (char *)"/Edit/Deselect all tracks", /* path */
-      (char *)"<CTRL><SHIFT>A",         /* accelerator */
-      select_none,                      /* callback */
-      0,                                /* callback_action */
+      0,                                /* accelerator */
+      menu_tab_action,                  /* callback */
+      offsetof(struct tabtype, selectnone_activate), /* callback_action */
       0,                                /* item_type */
       0                                 /* extra_data */
     },
     {
       (char *)"/Edit/Track properties", /* path */
       0,                                /* accelerator */
-      properties_item,                  /* callback */
-      0,                                /* callback_action */
+      menu_tab_action,                  /* callback */
+      offsetof(struct tabtype, properties_activate), /* callback_action */
       0,                                /* item_type */
       0                                 /* extra_data */
     },
@@ -407,8 +383,14 @@ GtkWidget *menubar(GtkWidget *w) {
   assert(selectall_widget != 0);
   assert(selectnone_widget != 0);
   assert(properties_widget != 0);
-  register_reset(menu_reset);
-  menu_reset();
+
+  
+  GtkWidget *edit_widget = gtk_item_factory_get_widget(mainmenufactory,
+                                                       "<GdisorderMain>/Edit");
+  g_signal_connect(edit_widget, "show", G_CALLBACK(edit_menu_show), 0);
+  
+  event_register("rights-changed", menu_rights_changed, 0);
+  users_set_sensitive(0);
   m = gtk_item_factory_get_widget(mainmenufactory,
                                   "<GdisorderMain>");
   set_tool_colors(m);
index da0db60..8c4ed09 100644 (file)
@@ -33,8 +33,6 @@ struct image {
 
 /* Miscellaneous GTK+ stuff ------------------------------------------------ */
 
-WT(cached_image);
-
 /* Functions */
 
 /** @brief Put scrollbars around a widget
@@ -123,7 +121,6 @@ GdkPixbuf *find_image(const char *name) {
         return 0;
       }
     }
-    NW(cached_image);
     cache_put(&image_cache_type, name,  pb);
   }
   return pb;
@@ -167,13 +164,10 @@ GtkWidget *iconbutton(const char *path, const char *tip) {
   GtkWidget *button, *content;
   GdkPixbuf *pb;
 
-  NW(button);
   button = gtk_button_new();
   if((pb = find_image(path))) {
-    NW(image);
     content = gtk_image_new_from_pixbuf(pb);
   } else {
-    NW(label);
     content = gtk_label_new(path);
   }
   gtk_widget_set_style(button, tool_style);
diff --git a/disobedience/popup.c b/disobedience/popup.c
new file mode 100644 (file)
index 0000000..4e09937
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "disobedience.h"
+#include "popup.h"
+
+void popup(GtkWidget **menup,
+           GdkEventButton *event,
+           struct menuitem *items,
+           int nitems,
+           void *extra) {
+  GtkWidget *menu = *menup;
+
+  /* Create the menu if it does not yet exist */
+  if(!menu) {
+    menu = *menup = gtk_menu_new();
+    g_signal_connect(menu, "destroy",
+                     G_CALLBACK(gtk_widget_destroyed), menup);
+    for(int n = 0; n < nitems; ++n) {
+      items[n].w = gtk_menu_item_new_with_label(items[n].name);
+      gtk_menu_attach(GTK_MENU(menu), items[n].w, 0, 1, n, n + 1);
+    }
+    set_tool_colors(menu);
+  }
+  /* Configure item sensitivity */
+  for(int n = 0; n < nitems; ++n) {
+    if(items[n].handlerid)
+      g_signal_handler_disconnect(items[n].w,
+                                  items[n].handlerid);
+    gtk_widget_set_sensitive(items[n].w,
+                             items[n].sensitive(extra));
+    items[n].handlerid = g_signal_connect(items[n].w,
+                                          "activate",
+                                          G_CALLBACK(items[n].activate),
+                                          extra);
+  }
+  /* Pop up the menu */
+  gtk_widget_show_all(menu);
+  gtk_menu_popup(GTK_MENU(menu), 0, 0, 0, 0,
+                 event->button, event->time);
+}
+
+/** @brief Make sure the right thing is selected
+ * @param widget Tree view
+ * @param event Mouse event
+ */
+void ensure_selected(GtkTreeView *treeview,
+                     GdkEventButton *event) {
+  GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
+  /* Get the path of the hovered item */
+  GtkTreePath *path;
+  if(!gtk_tree_view_get_path_at_pos(treeview,
+                                    event->x, event->y,
+                                    &path,
+                                    NULL,
+                                    NULL, NULL))
+    return;                     /* If there isn't one, do nothing */
+  if(!gtk_tree_selection_path_is_selected(selection, path)) {
+    /* We're hovered over one thing but it's not the selected row.  This is
+     * very confusing for the poor old user so we select the hovered row. */
+    gtk_tree_selection_unselect_all(selection);
+    gtk_tree_selection_select_path(selection, path);
+  }
+  gtk_tree_path_free(path);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/disobedience/popup.h b/disobedience/popup.h
new file mode 100644 (file)
index 0000000..165f69b
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#ifndef POPUP_H
+#define POPUP_H
+
+/** @brief A popup menu item */
+struct menuitem {
+  /** @brief Menu item name */
+  const char *name;
+
+  /** @brief Called to activate the menu item */
+  void (*activate)(GtkMenuItem *menuitem,
+                   gpointer user_data);
+  
+  /** @brief Called to determine whether the menu item is usable.
+   *
+   * Returns @c TRUE if it should be sensitive and @c FALSE otherwise.
+   */
+  int (*sensitive)(void *extra);
+
+  /** @brief Signal handler ID */
+  gulong handlerid;
+
+  /** @brief Widget for menu item */
+  GtkWidget *w;
+};
+
+void popup(GtkWidget **menup,
+           GdkEventButton *event,
+           struct menuitem *items,
+           int nitems,
+           void *extra);
+void ensure_selected(GtkTreeView *treeview,
+                     GdkEventButton *event);
+
+#endif /* POPUP_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 96d5c48..16ee97c 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * This file is part of DisOrder.
- * Copyright (C) 2006, 2007 Richard Kettlewell
+ * Copyright (C) 2006-2008 Richard Kettlewell
  *
  * 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
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
-
+/** @file disobedience/properties.c
+ * @brief Track properties editor
+ *
+ * TODO:
+ * - return and escape keys should work 
+ */
 #include "disobedience.h"
 
-/* Track properties -------------------------------------------------------- */
-
 struct prefdata;
 
 static void kickoff_namepart(struct prefdata *f);
@@ -29,7 +32,7 @@ static void completed_namepart(struct prefdata *f);
 static const char *get_edited_namepart(struct prefdata *f);
 static void set_edited_namepart(struct prefdata *f, const char *value);
 static void set_namepart(struct prefdata *f, const char *value);
-static void set_namepart_completed(void *v);
+static void set_namepart_completed(void *v, const char *err);
 
 static void kickoff_string(struct prefdata *f);
 static void completed_string(struct prefdata *f);
@@ -43,7 +46,7 @@ static const char *get_edited_boolean(struct prefdata *f);
 static void set_edited_boolean(struct prefdata *f, const char *value);
 static void set_boolean(struct prefdata *f, const char *value);
 
-static void prefdata_completed(void *v, const char *value);
+static void prefdata_completed(void *v, const char *err, const char *value);
 static void prefdata_onerror(struct callbackdata *cbd,
                              int code,
                              const char *msg);
@@ -55,6 +58,10 @@ static void properties_ok(GtkButton *button, gpointer userdata);
 static void properties_apply(GtkButton *button, gpointer userdata);
 static void properties_cancel(GtkButton *button, gpointer userdata);
 
+static void properties_logged_in(const char *event,
+                                 void *eventdata,
+                                 void *callbackdata);
+
 /** @brief Data for a single preference */
 struct prefdata {
   const char *track;
@@ -157,6 +164,7 @@ static struct prefdata *prefdatas;      /* Current prefdatas */
 static GtkWidget *properties_window;
 static GtkWidget *properties_table;
 static struct progress_window *pw;
+static event_handle properties_event;
 
 static void propagate_clicked(GtkButton attribute((unused)) *button,
                               gpointer userdata) {
@@ -191,6 +199,7 @@ void properties(int ntracks, const char **tracks) {
     popup_msg(GTK_MESSAGE_ERROR, "Too many tracks selected");
     return;
   }
+  properties_event = event_register("logged-in", properties_logged_in, 0);
   /* Create a new properties window */
   properties_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_widget_set_style(properties_window, tool_style);
@@ -324,24 +333,25 @@ static void set_edited_namepart(struct prefdata *f, const char *value) {
 
 static void set_namepart(struct prefdata *f, const char *value) {
   char *s;
-  struct callbackdata *cbd = xmalloc(sizeof *cbd);
 
-  cbd->u.f = f;
   byte_xasprintf(&s, "trackname_display_%s", f->p->part);
   /* We don't know what the default is so can never unset.  This is a bug
    * relative to the original design, which is supposed to only ever allow for
    * non-trivial namepart preferences.  I suppose the server could spot a
    * default being set and translate it into an unset. */
   disorder_eclient_set(client, set_namepart_completed, f->track, s, value,
-                       cbd);
+                       f);
 }
 
 /* Called when we've set a namepart */
-static void set_namepart_completed(void *v) {
-  struct callbackdata *cbd = v;
-  struct prefdata *f = cbd->u.f;
-
-  namepart_update(f->track, "display", f->p->part);
+static void set_namepart_completed(void *v, const char *err) {
+  if(err)
+    popup_protocol_error(0, err);
+  else {
+    struct prefdata *f = v;
+    
+    namepart_update(f->track, "display", f->p->part);
+  }
 }
 
 /* String preferences ------------------------------------------------------ */
@@ -366,15 +376,15 @@ static void set_edited_string(struct prefdata *f, const char *value) {
   gtk_entry_set_text(GTK_ENTRY(f->widget), value);
 }
 
+static void set_string_completed(void attribute((unused)) *v,
+                                 const char *err) {
+  if(err)
+    popup_protocol_error(0, err);
+}
+
 static void set_string(struct prefdata *f, const char *value) {
-  if(strcmp(f->p->default_value, value))
-    /* Different from default, set it */
-    disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part,
-                         value, 0/*v*/);
-  else
-    /* Same as default, just unset */
-    disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part,
-                           0/*v*/);
+  disorder_eclient_set(client, set_string_completed, f->track, f->p->part,
+                       value, 0/*v*/);
 }
 
 /* Boolean preferences ----------------------------------------------------- */
@@ -402,17 +412,14 @@ static void set_edited_boolean(struct prefdata *f, const char *value) {
                                strcmp(value, "0"));
 }
 
+#define set_boolean_completed set_string_completed
+
 static void set_boolean(struct prefdata *f, const char *value) {
   char *s;
 
   byte_xasprintf(&s, "trackname_display_%s", f->p->part);
-  if(strcmp(value, f->p->default_value))
-    disorder_eclient_set(client, 0/*completed*/, f->track, f->p->part, value,
-                         0/*v*/);
-  else
-    /* If default value then delete the pref */
-    disorder_eclient_unset(client, 0/*completed*/, f->track, f->p->part,
-                           0/*v*/);
+  disorder_eclient_set(client, set_boolean_completed,
+                       f->track, f->p->part, value, 0/*v*/);
 }
 
 /* Querying preferences ---------------------------------------------------- */
@@ -434,10 +441,13 @@ static void prefdata_onerror(struct callbackdata *cbd,
 }
 
 /* Got the value of a pref */
-static void prefdata_completed(void *v, const char *value) {
-  struct callbackdata *cbd = v;
-
-  prefdata_completed_common(cbd->u.f, value);
+static void prefdata_completed(void *v, const char *err, const char *value) {
+  if(err) {
+  } else {
+    struct callbackdata *cbd = v;
+    
+    prefdata_completed_common(cbd->u.f, value);
+  }
 }
 
 static void prefdata_completed_common(struct prefdata *f,
@@ -488,13 +498,17 @@ static void properties_apply(GtkButton attribute((unused)) *button,
 static void properties_cancel(GtkButton attribute((unused)) *button,
                               gpointer attribute((unused)) userdata) {
   gtk_widget_destroy(properties_window);
+  event_cancel(properties_event);
+  properties_event = 0;
 }
 
-/** @brief Called on client reset
+/** @brief Called when we've just logged in
  *
  * Destroys the current properties window.
  */
-void properties_reset(void) {
+static void properties_logged_in(const char attribute((unused)) *event,
+                                 void attribute((unused)) *eventdata,
+                                 void attribute((unused)) *callbackdata) {
   if(properties_window)
     gtk_widget_destroy(properties_window);
 }
diff --git a/disobedience/queue-generic.c b/disobedience/queue-generic.c
new file mode 100644 (file)
index 0000000..8c7458a
--- /dev/null
@@ -0,0 +1,466 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2006-2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+/** @file disobedience/queue-generic.c
+ * @brief Queue widgets
+ *
+ * This file provides contains code shared between all the queue-like
+ * widgets - the queue, the recent list and the added tracks list.
+ *
+ * This code is in the process of being rewritten to use the native list
+ * widget.
+ *
+ * There are three @ref queuelike objects: @ref ql_queue, @ref
+ * ql_recent and @ref ql_added.  Each has an associated queue linked
+ * list and a list store containing the contents.
+ *
+ * When new contents turn up we rearrange the list store accordingly.
+ *
+ * NB that while in the server the playing track is not in the queue, in
+ * Disobedience, the playing does live in @c ql_queue.q, despite its different
+ * status to everything else found in that list.
+ *
+ * To do:
+ * - display playing row in a different color?
+ */
+#include "disobedience.h"
+#include "popup.h"
+#include "queue-generic.h"
+
+static struct queuelike *const queuelikes[] = {
+  &ql_queue, &ql_recent, &ql_added
+};
+#define NQUEUELIKES (sizeof queuelikes / sizeof *queuelikes)
+
+/* Track detail lookup ----------------------------------------------------- */
+
+static void queue_lookups_completed(const char attribute((unused)) *event,
+                                   void attribute((unused)) *eventdata,
+                                   void *callbackdata) {
+  struct queuelike *ql = callbackdata;
+  ql_update_list_store(ql);
+}
+
+/* Column formatting -------------------------------------------------------- */
+
+/** @brief Format the 'when' column */
+const char *column_when(const struct queue_entry *q,
+                        const char attribute((unused)) *data) {
+  char when[64];
+  struct tm tm;
+  time_t t;
+
+  D(("column_when"));
+  switch(q->state) {
+  case playing_isscratch:
+  case playing_unplayed:
+  case playing_random:
+    t = q->expected;
+    break;
+  case playing_failed:
+  case playing_no_player:
+  case playing_ok:
+  case playing_scratched:
+  case playing_started:
+  case playing_paused:
+  case playing_quitting:
+    t = q->played;
+    break;
+  default:
+    t = 0;
+    break;
+  }
+  if(t)
+    strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm));
+  else
+    when[0] = 0;
+  return xstrdup(when);
+}
+
+/** @brief Format the 'who' column */
+const char *column_who(const struct queue_entry *q,
+                       const char attribute((unused)) *data) {
+  D(("column_who"));
+  return q->submitter ? q->submitter : "";
+}
+
+/** @brief Format one of the track name columns */
+const char *column_namepart(const struct queue_entry *q,
+                            const char *data) {
+  D(("column_namepart"));
+  return namepart(q->track, "display", data);
+}
+
+/** @brief Format the length column */
+const char *column_length(const struct queue_entry *q,
+                          const char attribute((unused)) *data) {
+  D(("column_length"));
+  long l;
+  time_t now;
+  char *played = 0, *length = 0;
+
+  /* Work out what to say for the length */
+  l = namepart_length(q->track);
+  if(l > 0)
+    byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60);
+  else
+    byte_xasprintf(&length, "?:??");
+  /* For the currently playing track we want to report how much of the track
+   * has been played */
+  if(q == playing_track) {
+    /* log_state() arranges that we re-get the playing data whenever the
+     * pause/resume state changes */
+    if(last_state & DISORDER_TRACK_PAUSED)
+      l = playing_track->sofar;
+    else {
+      time(&now);
+      l = playing_track->sofar + (now - last_playing);
+    }
+    byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length);
+    return played;
+  } else
+    return length;
+}
+
+/* List store maintenance -------------------------------------------------- */
+
+/** @brief Return the @ref queue_entry corresponding to @p iter
+ * @param model Model that owns @p iter
+ * @param iter Tree iterator
+ * @return ID string
+ */
+struct queue_entry *ql_iter_to_q(GtkTreeModel *model,
+                                 GtkTreeIter *iter) {
+  struct queuelike *ql = g_object_get_data(G_OBJECT(model), "ql");
+  GValue v[1];
+  memset(v, 0, sizeof v);
+  gtk_tree_model_get_value(model, iter, ql->ncolumns + QUEUEPOINTER_COLUMN, v);
+  assert(G_VALUE_TYPE(v) == G_TYPE_POINTER);
+  struct queue_entry *const q = g_value_get_pointer(v);
+  g_value_unset(v);
+  return q;
+}
+
+/** @brief Update one row of a list store
+ * @param q Queue entry
+ * @param iter Iterator referring to row or NULL to work it out
+ */
+void ql_update_row(struct queue_entry *q,
+                   GtkTreeIter *iter) { 
+  const struct queuelike *const ql = q->ql; 
+
+  D(("ql_update_row"));
+  /* If no iter was supplied, work it out */
+  GtkTreeIter my_iter[1];
+  if(!iter) {
+    gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), my_iter);
+    struct queue_entry *qq;
+    for(qq = ql->q; qq && q != qq; qq = qq->next)
+      gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), my_iter);
+    if(!qq)
+      return;
+    iter = my_iter;
+  }
+  /* Update all the columns */
+  for(int col = 0; col < ql->ncolumns; ++col)
+    gtk_list_store_set(ql->store, iter,
+                       col, ql->columns[col].value(q,
+                                                   ql->columns[col].data),
+                       -1);
+  gtk_list_store_set(ql->store, iter,
+                     ql->ncolumns + QUEUEPOINTER_COLUMN, q,
+                     -1);
+  if(q == playing_track)
+    gtk_list_store_set(ql->store, iter,
+                       ql->ncolumns + BACKGROUND_COLUMN, BG_PLAYING,
+                       ql->ncolumns + FOREGROUND_COLUMN, FG_PLAYING,
+                       -1);
+  else
+    gtk_list_store_set(ql->store, iter,
+                       ql->ncolumns + BACKGROUND_COLUMN, (char *)0,
+                       ql->ncolumns + FOREGROUND_COLUMN, (char *)0,
+                       -1);
+}
+
+/** @brief Update the list store
+ * @param ql Queuelike to update
+ *
+ * Called when new namepart data is available (and initially).  Doesn't change
+ * the rows, just updates the cell values.
+ */
+void ql_update_list_store(struct queuelike *ql) {
+  D(("ql_update_list_store"));
+  GtkTreeIter iter[1];
+  gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter);
+  for(struct queue_entry *q = ql->q; q; q = q->next) {
+    ql_update_row(q, iter);
+    gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+  }
+}
+
+struct newqueue_data {
+  struct queue_entry *old, *new;
+};
+
+static void record_queue_map(hash *h,
+                             const char *id,
+                             struct queue_entry *old,
+                             struct queue_entry *new) {
+  struct newqueue_data *nqd;
+
+  if(!(nqd = hash_find(h, id))) {
+    static const struct newqueue_data empty[1];
+    hash_add(h, id, empty, HASH_INSERT);
+    nqd = hash_find(h, id);
+  }
+  if(old)
+    nqd->old = old;
+  if(new)
+    nqd->new = new;
+}
+
+#if 0
+static void dump_queue(struct queue_entry *head, struct queue_entry *mark) {
+  for(struct queue_entry *q = head; q; q = q->next) {
+    if(q == mark)
+      fprintf(stderr, "!");
+    fprintf(stderr, "%s", q->id);
+    if(q->next)
+      fprintf(stderr, " ");
+  }
+  fprintf(stderr, "\n");
+}
+
+static void dump_rows(struct queuelike *ql) {
+  GtkTreeIter iter[1];
+  gboolean it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
+                                              iter);
+  while(it) {
+    struct queue_entry *q = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
+    it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+    fprintf(stderr, "%s", q->id);
+    if(it)
+      fprintf(stderr, " ");
+  }
+  fprintf(stderr, "\n");
+}
+#endif
+
+/** @brief Reset the list store
+ * @param ql Queuelike to reset
+ * @param newq New queue contents/ordering
+ *
+ * Updates the queue to match @p newq
+ */
+void ql_new_queue(struct queuelike *ql,
+                  struct queue_entry *newq) {
+  D(("ql_new_queue"));
+  ++suppress_actions;
+
+  /* Tell every queue entry which queue owns it */
+  //fprintf(stderr, "%s: filling in q->ql\n", ql->name);
+  for(struct queue_entry *q = newq; q; q = q->next)
+    q->ql = ql;
+
+  //fprintf(stderr, "%s: constructing h\n", ql->name);
+  /* Construct map from id to new and old structures */
+  hash *h = hash_new(sizeof(struct newqueue_data));
+  for(struct queue_entry *q = ql->q; q; q = q->next)
+    record_queue_map(h, q->id, q, NULL);
+  for(struct queue_entry *q = newq; q; q = q->next)
+    record_queue_map(h, q->id, NULL, q);
+
+  /* The easy bit: delete rows not present any more.  In the same pass we
+   * update the secret column containing the queue_entry pointer. */
+  //fprintf(stderr, "%s: deleting rows...\n", ql->name);
+  GtkTreeIter iter[1];
+  gboolean it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
+                                              iter);
+  int inserted = 0, deleted = 0, kept = 0;
+  while(it) {
+    struct queue_entry *q = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
+    const struct newqueue_data *nqd = hash_find(h, q->id);
+    if(nqd->new) {
+      /* Tell this row that it belongs to the new version of the queue */
+      gtk_list_store_set(ql->store, iter,
+                         ql->ncolumns + QUEUEPOINTER_COLUMN, nqd->new,
+                         -1);
+      it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+      ++kept;
+    } else {
+      /* Delete this row (and move iter to the next one) */
+      //fprintf(stderr, " delete %s", q->id);
+      it = gtk_list_store_remove(ql->store, iter);
+      ++deleted;
+    }
+  }
+
+  /* Now every row's secret column is right, but we might be missing new rows
+   * and they might be in the wrong order */
+
+  /* We're going to have to support arbitrary rearrangements, so we might as
+   * well add new elements at the end. */
+  //fprintf(stderr, "%s: adding rows...\n", ql->name);
+  struct queue_entry *after = 0;
+  for(struct queue_entry *q = newq; q; q = q->next) {
+    const struct newqueue_data *nqd = hash_find(h, q->id);
+    if(!nqd->old) {
+      if(after) {
+        /* Try to insert at the right sort of place */
+        GtkTreeIter where[1];
+        gboolean wit = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
+                                                     where);
+        while(wit && ql_iter_to_q(GTK_TREE_MODEL(ql->store), where) != after)
+          wit = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), where);
+        if(wit)
+          gtk_list_store_insert_after(ql->store, iter, where);
+        else
+          gtk_list_store_append(ql->store, iter);
+      } else
+        gtk_list_store_prepend(ql->store, iter);
+      gtk_list_store_set(ql->store, iter,
+                         ql->ncolumns + QUEUEPOINTER_COLUMN, q,
+                         -1);
+      //fprintf(stderr, " add %s", q->id);
+      ++inserted;
+    }
+    after = newq;
+  }
+
+  /* Now exactly the right set of rows are present, and they have the right
+   * queue_entry pointers in their secret column, but they may be in the wrong
+   * order.
+   *
+   * The current code is simple but amounts to a bubble-sort - we might easily
+   * called gtk_tree_model_iter_next a couple of thousand times.
+   */
+  //fprintf(stderr, "%s: rearranging rows\n", ql->name);
+  //fprintf(stderr, "%s: queue state: ", ql->name);
+  //dump_queue(newq, 0);
+  //fprintf(stderr, "%s: row state: ", ql->name);
+  //dump_rows(ql);
+  it = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store),
+                                              iter);
+  struct queue_entry *rq = newq;        /* r for 'right, correct' */
+  int swaps = 0, searches = 0;
+  while(it) {
+    struct queue_entry *q = ql_iter_to_q(GTK_TREE_MODEL(ql->store), iter);
+    //fprintf(stderr, " rq = %p, q = %p\n", rq, q);
+    //fprintf(stderr, " rq->id = %s, q->id = %s\n", rq->id, q->id);
+
+    if(q != rq) {
+      //fprintf(stderr, "  mismatch\n");
+      GtkTreeIter next[1] = { *iter };
+      gboolean nit = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), next);
+      while(nit) {
+        struct queue_entry *nq = ql_iter_to_q(GTK_TREE_MODEL(ql->store), next);
+        //fprintf(stderr, "   candidate: %s\n", nq->id);
+        if(nq == rq)
+          break;
+        nit = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), next);
+        ++searches;
+      }
+      assert(nit);
+      //fprintf(stderr, "  found it\n");
+      gtk_list_store_swap(ql->store, iter, next);
+      *iter = *next;
+      //fprintf(stderr, "%s: new row state: ", ql->name);
+      //dump_rows(ql);
+      ++swaps;
+    }
+    /* ...and onto the next one */
+    it = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+    rq = rq->next;
+  }
+#if 0
+  fprintf(stderr, "%6s: %3d kept %3d inserted %3d deleted %3d swaps %4d searches\n", ql->name,
+          kept, inserted, deleted, swaps, searches);
+#endif
+  //fprintf(stderr, "done\n");
+  ql->q = newq;
+  /* Set the rest of the columns in new rows */
+  ql_update_list_store(ql);
+  --suppress_actions;
+}
+
+/** @brief Initialize a @ref queuelike */
+GtkWidget *init_queuelike(struct queuelike *ql) {
+  D(("init_queuelike"));
+  /* Create the list store.  We add an extra column to hold a pointer to the
+   * queue_entry. */
+  GType *types = xcalloc(ql->ncolumns + EXTRA_COLUMNS, sizeof (GType));
+  for(int n = 0; n < ql->ncolumns + EXTRA_COLUMNS; ++n)
+    types[n] = G_TYPE_STRING;
+  types[ql->ncolumns + QUEUEPOINTER_COLUMN] = G_TYPE_POINTER;
+  ql->store = gtk_list_store_newv(ql->ncolumns + EXTRA_COLUMNS, types);
+  g_object_set_data(G_OBJECT(ql->store), "ql", (void *)ql);
+
+  /* Create the view */
+  ql->view = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ql->store));
+  gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(ql->view), TRUE);
+
+  /* Create cell renderers and label columns */
+  for(int n = 0; n < ql->ncolumns; ++n) {
+    GtkCellRenderer *r = gtk_cell_renderer_text_new();
+    if(ql->columns[n].flags & COL_ELLIPSIZE)
+      g_object_set(r, "ellipsize", PANGO_ELLIPSIZE_END, (char *)0);
+    if(ql->columns[n].flags & COL_RIGHT)
+      g_object_set(r, "xalign", (gfloat)1.0, (char *)0);
+    GtkTreeViewColumn *c = gtk_tree_view_column_new_with_attributes
+      (ql->columns[n].name,
+       r,
+       "text", n,
+       "background", ql->ncolumns + BACKGROUND_COLUMN,
+       "foreground", ql->ncolumns + FOREGROUND_COLUMN,
+       (char *)0);
+    gtk_tree_view_column_set_resizable(c, TRUE);
+    gtk_tree_view_column_set_reorderable(c, TRUE);
+    if(ql->columns[n].flags & COL_EXPAND)
+      g_object_set(c, "expand", TRUE, (char *)0);
+    gtk_tree_view_append_column(GTK_TREE_VIEW(ql->view), c);
+  }
+
+  /* The selection should support multiple things being selected */
+  ql->selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(ql->view));
+  gtk_tree_selection_set_mode(ql->selection, GTK_SELECTION_MULTIPLE);
+
+  /* Catch button presses */
+  g_signal_connect(ql->view, "button-press-event",
+                   G_CALLBACK(ql_button_release), ql);
+
+  /* TODO style? */
+
+  ql->init();
+
+  /* Update display text when lookups complete */
+  event_register("lookups-completed", queue_lookups_completed, ql);
+  
+  GtkWidget *scrolled = scroll_widget(ql->view);
+  g_object_set_data(G_OBJECT(scrolled), "type", (void *)ql_tabtype(ql));
+  return scrolled;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/disobedience/queue-generic.h b/disobedience/queue-generic.h
new file mode 100644 (file)
index 0000000..1888602
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2006-2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#ifndef QUEUE_GENERIC_H
+#define QUEUE_GENERIC_H
+
+/** @brief Definition of a column */
+struct queue_column {
+  /** @brief Column name */
+  const char *name;
+
+  /** @brief Compute value for this column */
+  const char *(*value)(const struct queue_entry *q,
+                       const char *data);
+
+  /** @brief Passed to value() */
+  const char *data;
+
+  /** @brief Flags word */
+  unsigned flags;
+};
+
+/** @brief Ellipsize column if too wide */
+#define COL_ELLIPSIZE 0x0001
+
+/** @brief Set expand property */
+#define COL_EXPAND 0x0002
+
+/** @brief Right-algin column */
+#define COL_RIGHT 0x0004
+
+/** @brief Definition of a queue-like window */
+struct queuelike {
+
+  /* Things filled in by the caller: */
+
+  /** @brief Name for this tab */
+  const char *name;
+  
+  /** @brief Initialization function */
+  void (*init)(void);
+
+  /** @brief Columns */
+  const struct queue_column *columns;
+
+  /** @brief Number of columns in this queuelike */
+  int ncolumns;
+
+  /** @brief Items for popup menu */
+  struct menuitem *menuitems;
+
+  /** @brief Number of menu items */
+  int nmenuitems;
+
+  /* Dynamic state: */
+
+  /** @brief The head of the queue */
+  struct queue_entry *q;
+
+  /* Things created by the implementation: */
+  
+  /** @brief The list store */
+  GtkListStore *store;
+
+  /** @brief The tree view */
+  GtkWidget *view;
+
+  /** @brief The selection */
+  GtkTreeSelection *selection;
+  
+  /** @brief The popup menu */
+  GtkWidget *menu;
+
+  /** @brief Menu callbacks */
+  struct tabtype tabtype;
+};
+
+enum {
+  QUEUEPOINTER_COLUMN,
+  FOREGROUND_COLUMN,
+  BACKGROUND_COLUMN,
+
+  EXTRA_COLUMNS
+};
+
+/* TODO probably need to set "horizontal-separator" to 0, but can't find any
+ * coherent description of how to set style properties in isolation. */
+#define BG_PLAYING 0
+#define FG_PLAYING 0
+
+#ifndef BG_PLAYING
+# define BG_PLAYING "#e0ffe0"
+# define FG_PLAYING "black"
+#endif
+
+extern struct queuelike ql_queue;
+extern struct queuelike ql_recent;
+extern struct queuelike ql_added;
+
+extern time_t last_playing;
+
+int ql_selectall_sensitive(void *extra);
+void ql_selectall_activate(GtkMenuItem *menuitem,
+                           gpointer user_data);
+int ql_selectnone_sensitive(void *extra);
+void ql_selectnone_activate(GtkMenuItem *menuitem,
+                            gpointer user_data);
+int ql_properties_sensitive(void *extra);
+void ql_properties_activate(GtkMenuItem *menuitem,
+                            gpointer user_data);
+int ql_scratch_sensitive(void *extra);
+void ql_scratch_activate(GtkMenuItem *menuitem,
+                         gpointer user_data);
+int ql_remove_sensitive(void *extra);
+void ql_remove_activate(GtkMenuItem *menuitem,
+                        gpointer user_data);
+int ql_play_sensitive(void *extra);
+void ql_play_activate(GtkMenuItem *menuitem,
+                      gpointer user_data);
+gboolean ql_button_release(GtkWidget *widget,
+                           GdkEventButton *event,
+                           gpointer user_data);
+GtkWidget *init_queuelike(struct queuelike *ql);
+void ql_update_list_store(struct queuelike *ql) ;
+void ql_update_row(struct queue_entry *q,
+                   GtkTreeIter *iter);
+void ql_new_queue(struct queuelike *ql,
+                  struct queue_entry *newq);
+const char *column_when(const struct queue_entry *q,
+                        const char *data);
+const char *column_who(const struct queue_entry *q,
+                       const char *data);
+const char *column_namepart(const struct queue_entry *q,
+                            const char *data);
+const char *column_length(const struct queue_entry *q,
+                          const char *data);
+struct tabtype *ql_tabtype(struct queuelike *ql);
+struct queue_entry *ql_iter_to_q(GtkTreeModel *model,
+                                 GtkTreeIter *iter);
+
+#endif /* QUEUE_GENERIC_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/disobedience/queue-menu.c b/disobedience/queue-menu.c
new file mode 100644 (file)
index 0000000..75ddeb6
--- /dev/null
@@ -0,0 +1,212 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2006-2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "disobedience.h"
+#include "popup.h"
+#include "queue-generic.h"
+
+/* Select All */
+
+int ql_selectall_sensitive(void *extra) {
+  struct queuelike *ql = extra;
+  return !!ql->q;
+}
+
+void ql_selectall_activate(GtkMenuItem attribute((unused)) *menuitem,
+                           gpointer user_data) {
+  struct queuelike *ql = user_data;
+
+  gtk_tree_selection_select_all(ql->selection);
+}
+
+/* Select None */
+
+int ql_selectnone_sensitive(void *extra) {
+  struct queuelike *ql = extra;
+  return gtk_tree_selection_count_selected_rows(ql->selection) > 0;
+}
+
+void ql_selectnone_activate(GtkMenuItem attribute((unused)) *menuitem,
+                            gpointer user_data) {
+  struct queuelike *ql = user_data;
+
+  gtk_tree_selection_unselect_all(ql->selection);
+}
+
+/* Properties */
+
+int ql_properties_sensitive(void *extra) {
+  struct queuelike *ql = extra;
+  return gtk_tree_selection_count_selected_rows(ql->selection) > 0;
+}
+
+void ql_properties_activate(GtkMenuItem attribute((unused)) *menuitem,
+                            gpointer user_data) {
+  struct queuelike *ql = user_data;
+  struct vector v[1];
+  GtkTreeIter iter[1];
+
+  vector_init(v);
+  gtk_tree_model_get_iter_first(GTK_TREE_MODEL(ql->store), iter);
+  for(struct queue_entry *q = ql->q; q; q = q->next) {
+    if(gtk_tree_selection_iter_is_selected(ql->selection, iter))
+      vector_append(v, (char *)q->track);
+    gtk_tree_model_iter_next(GTK_TREE_MODEL(ql->store), iter);
+  }
+  if(v->nvec)
+    properties(v->nvec, (const char **)v->vec);
+}
+
+/* Scratch */
+
+int ql_scratch_sensitive(void attribute((unused)) *extra) {
+  return !!(last_state & DISORDER_PLAYING)
+    && right_scratchable(last_rights, config->username, playing_track);
+}
+
+static void ql_scratch_completed(void attribute((unused)) *v,
+                                 const char *err) {
+  if(err)
+    popup_protocol_error(0, err);
+}
+
+void ql_scratch_activate(GtkMenuItem attribute((unused)) *menuitem,
+                         gpointer attribute((unused)) user_data) {
+  disorder_eclient_scratch_playing(client, ql_scratch_completed, 0);
+}
+
+/* Remove */
+
+static void ql_remove_sensitive_callback(GtkTreeModel *model,
+                                         GtkTreePath attribute((unused)) *path,
+                                         GtkTreeIter *iter,
+                                         gpointer data) {
+  struct queue_entry *q = ql_iter_to_q(model, iter);
+  const int removable = (q != playing_track
+                         && right_removable(last_rights, config->username, q));
+  int *const counts = data;
+  ++counts[removable];
+}
+
+int ql_remove_sensitive(void *extra) {
+  struct queuelike *ql = extra;
+  int counts[2] = { 0, 0 };
+  gtk_tree_selection_selected_foreach(ql->selection,
+                                      ql_remove_sensitive_callback,
+                                      counts);
+  /* Remove will work if we have at least some removable tracks selected, and
+   * no unremovable ones */
+  return counts[1] > 0 && counts[0] == 0;
+}
+
+static void ql_remove_completed(void attribute((unused)) *v,
+                                const char *err) {
+  if(err)
+    popup_protocol_error(0, err);
+}
+
+static void ql_remove_activate_callback(GtkTreeModel *model,
+                                        GtkTreePath attribute((unused)) *path,
+                                        GtkTreeIter *iter,
+                                        gpointer attribute((unused)) data) {
+  struct queue_entry *q = ql_iter_to_q(model, iter);
+
+  disorder_eclient_remove(client, q->id, ql_remove_completed, q);
+}
+
+void ql_remove_activate(GtkMenuItem attribute((unused)) *menuitem,
+                        gpointer user_data) {
+  struct queuelike *ql = user_data;
+  gtk_tree_selection_selected_foreach(ql->selection,
+                                      ql_remove_activate_callback,
+                                      0);
+}
+
+/* Play */
+
+int ql_play_sensitive(void *extra) {
+  struct queuelike *ql = extra;
+  return (last_rights & RIGHT_PLAY)
+    && gtk_tree_selection_count_selected_rows(ql->selection) > 0;
+}
+
+static void ql_play_completed(void attribute((unused)) *v, const char *err) {
+  if(err)
+    popup_protocol_error(0, err);
+}
+
+static void ql_play_activate_callback(GtkTreeModel *model,
+                                      GtkTreePath attribute((unused)) *path,
+                                      GtkTreeIter *iter,
+                                      gpointer attribute((unused)) data) {
+  struct queue_entry *q = ql_iter_to_q(model, iter);
+
+  disorder_eclient_play(client, q->track, ql_play_completed, q);
+}
+
+void ql_play_activate(GtkMenuItem attribute((unused)) *menuitem,
+                         gpointer user_data) {
+  struct queuelike *ql = user_data;
+  gtk_tree_selection_selected_foreach(ql->selection,
+                                      ql_play_activate_callback,
+                                      0);
+}
+
+/** @brief Called when a button is released over a queuelike */
+gboolean ql_button_release(GtkWidget *widget,
+                           GdkEventButton *event,
+                           gpointer user_data) {
+  struct queuelike *ql = user_data;
+
+  if(event->type == GDK_BUTTON_PRESS
+     && event->button == 3) {
+    /* Right button click. */
+    ensure_selected(GTK_TREE_VIEW(widget), event);
+    popup(&ql->menu, event, ql->menuitems, ql->nmenuitems, ql);
+    return TRUE;                        /* hide the click from other widgets */
+  }
+
+  return FALSE;
+}
+
+struct tabtype *ql_tabtype(struct queuelike *ql) {
+  static const struct tabtype queuelike_tabtype = {
+    ql_properties_sensitive,
+    ql_selectall_sensitive,
+    ql_selectnone_sensitive,
+    ql_properties_activate,
+    ql_selectall_activate,
+    ql_selectnone_activate,
+    0,
+    0
+  };
+
+  ql->tabtype = queuelike_tabtype;
+  ql->tabtype.extra = ql;
+  return &ql->tabtype;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 6008e4e..d69b34d 100644 (file)
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
  * USA
  */
-/** @file disobedience/queue.c
- * @brief Queue widgets
- *
- * This file provides both the queue widget and the recently-played widget.
- *
- * A queue layout is structured as follows:
- *
- * <pre>
- *  vbox
- *   titlescroll
- *    titlelayout
- *     titlecells[col]                 eventbox (made by wrap_queue_cell)
- *      titlecells[col]->child         label (from columns[])
- *   mainscroll
- *    mainlayout
- *     cells[row * N + c]              eventbox (made by wrap_queue_cell)
- *      cells[row * N + c]->child      label (from column constructors)
- * </pre>
- *
- * titlescroll never has any scrollbars.  Instead whenever mainscroll's
- * horizontal adjustment is changed, queue_scrolled adjusts titlescroll to
- * match, forcing the title and the queue to pan in sync but allowing the queue
- * to scroll independently.
- *
- * Whenever the queue changes everything below mainlayout is thrown away and
- * reconstructed from scratch.  Name lookups are cached, so this doesn't imply
- * lots of disorder protocol traffic.
- *
- * The last cell on each row is the padding cell, and this extends ridiculously
- * far to the right.  (Can we do better?)
- *
- * When drag and drop is active we create extra eventboxes to act as dropzones.
- * These only exist while the drag proceeds, as otherwise they steal events
- * from more deserving widgets.  (It might work to hide them when not in use
- * too but this way around the d+d code is a bit more self-contained.)
- *
- * NB that while in the server the playing track is not in the queue, in
- * Disobedience, the playing does live in @c ql_queue.q, despite its different
- * status to everything else found in that list.
- */
-
 #include "disobedience.h"
-#include "charset.h"
-
-/** @brief Horizontal padding for queue cells */
-#define HCELLPADDING 4
-
-/** @brief Vertical padding for queue cells */
-#define VCELLPADDING 2
-
-/* Queue management -------------------------------------------------------- */
-
-WT(label);
-WT(event_box);
-WT(menu);
-WT(menu_item);
-WT(layout);
-WT(vbox);
-
-struct queuelike;
-
-static void add_drag_targets(struct queuelike *ql);
-static void remove_drag_targets(struct queuelike *ql);
-static void redisplay_queue(struct queuelike *ql);
-static GtkWidget *column_when(const struct queuelike *ql,
-                              const struct queue_entry *q,
-                              const char *data);
-static GtkWidget *column_who(const struct queuelike *ql,
-                             const struct queue_entry *q,
-                             const char *data);
-static GtkWidget *column_namepart(const struct queuelike *ql,
-                                  const struct queue_entry *q,
-                                  const char *data);
-static GtkWidget *column_length(const struct queuelike *ql,
-                                const struct queue_entry *q,
-                                const char *data);
-static int draggable_row(const struct queue_entry *q);
-
-static const struct tabtype tabtype_queue; /* forward */
-
-static const GtkTargetEntry dragtargets[] = {
-  { (char *)"disobedience-queue", GTK_TARGET_SAME_APP, 0 }
-};
-#define NDRAGTARGETS (int)(sizeof dragtargets / sizeof *dragtargets)
-
-/** @brief Definition of a column */
-struct column {
-  const char *name;                     /* Column name */
-  GtkWidget *(*widget)(const struct queuelike *ql,
-                       const struct queue_entry *q,
-                       const char *data); /* Make a label for this column */
-  const char *data;                     /* Data to pass to widget() */
-  gfloat xalign;                        /* Alignment of the label */
-};
-
-/** @brief Table of columns for queue and recently played list */
-static const struct column maincolumns[] = {
-  { "When",   column_when,     0,        1 },
-  { "Who",    column_who,      0,        0 },
-  { "Artist", column_namepart, "artist", 0 },
-  { "Album",  column_namepart, "album",  0 },
-  { "Title",  column_namepart, "title",  0 },
-  { "Length", column_length,   0,        1 }
-};
-
-/** @brief Number of columns in queue and recnetly played list */
-#define NMAINCOLUMNS (int)(sizeof maincolumns / sizeof *maincolumns)
-
-/** @brief Table of columns for recently added tracks */
-static const struct column addedcolumns[] = {
-  { "Artist", column_namepart, "artist", 0 },
-  { "Album",  column_namepart, "album",  0 },
-  { "Title",  column_namepart, "title",  0 },
-  { "Length", column_length,   0,        1 }
-};
-
-/** @brief Number of columns in recently added list */
-#define NADDEDCOLUMNS (int)(sizeof addedcolumns / sizeof *addedcolumns)
-
-/** @brief Maximum number of column in any @ref queuelike */
-#define MAXCOLUMNS (NMAINCOLUMNS > NADDEDCOLUMNS ? NMAINCOLUMNS : NADDEDCOLUMNS)
-
-/** @brief Data passed to menu item activation handlers */
-struct menuiteminfo {
-  struct queuelike *ql;                 /**< @brief which queue we're dealing with */
-  struct queue_entry *q;                /**< @brief hovered entry or 0 */
-};
-
-/** @brief An item in the queue's popup menu */
-struct queue_menuitem {
-  /** @brief Menu item name */
-  const char *name;
-
-  /** @brief Called to activate the menu item
-   *
-   * The user data is the queue entry that the pointer was over when the menu
-   * popped up. */
-  void (*activate)(GtkMenuItem *menuitem,
-                   gpointer user_data);
-  
-  /** @brief Called to determine whether the menu item is usable.
-   *
-   * Returns @c TRUE if it should be sensitive and @c FALSE otherwise.  @p q
-   * points to the queue entry the pointer is over.
-   */
-  int (*sensitive)(struct queuelike *ql,
-                   struct queue_menuitem *m,
-                   struct queue_entry *q);
-
-  /** @brief Signal handler ID */
-  gulong handlerid;
-
-  /** @brief Widget for menu item */
-  GtkWidget *w;
-};
-
-/** @brief A queue-like object
- *
- * There are (currently) three of these: @ref ql_queue, @ref ql_recent and @ref
- * ql_added.
- */
-struct queuelike {
-  /** @brief Called when an update completes */
-  void (*notify)(void);
-
-  /** @brief Called to fix up the queue after update
-   * @param q The list passed back from the server
-   * @return Assigned to @c ql->q
-   */
-  struct queue_entry *(*fixup)(struct queue_entry *q);
-
-  /* Widgets */
-  GtkWidget *mainlayout;                /**< @brief main layout */
-  GtkWidget *mainscroll;                /**< @brief scroller for main layout */
-  GtkWidget *titlelayout;               /**< @brief title layout */
-  GtkWidget *titlecells[MAXCOLUMNS + 1]; /**< @brief title cells */
-  GtkWidget **cells;                    /**< @brief all the cells */
-  GtkWidget *menu;                      /**< @brief popup menu */
-  struct queue_menuitem *menuitems;     /**< @brief menu items */
-  GtkWidget *dragmark;                  /**< @brief drag destination marker */
-  GtkWidget **dropzones;                /**< @brief drag targets */
-  int ndropzones;                       /**< @brief number of drag targets */
-
-  /* State */
-  struct queue_entry *q;                /**< @brief head of queue */
-  struct queue_entry *last_click;       /**< @brief last click */
-  int nrows;                            /**< @brief number of rows */
-  int mainrowheight;                    /**< @brief height of one row */
-  hash *selection;                      /**< @brief currently selected items */
-  int swallow_release;                  /**< @brief swallow button release from drag */
-
-  const struct column *columns;         /**< @brief Table of columns */
-  int ncolumns;                         /**< @brief Number of columns */
-};
-
-static struct queuelike ql_queue; /**< @brief The main queue */
-static struct queuelike ql_recent; /*< @brief Recently-played tracks */
-static struct queuelike ql_added; /*< @brief Newly added tracks */
-static struct queue_entry *actual_queue; /**< @brief actual queue */
-static struct queue_entry *playing_track;     /**< @brief currenty playing */
-static time_t last_playing = (time_t)-1; /**< @brief when last got playing */
-static int namepart_lookups_outstanding;
-static int  namepart_completions_deferred; /* # of completions not processed */
-static const struct cache_type cachetype_string = { 3600 };
-static const struct cache_type cachetype_integer = { 3600 };
-static GtkWidget *playing_length_label;
-
-/* Debugging --------------------------------------------------------------- */
-
-#if 0
-static void describe_widget(const char *name, GtkWidget *w, int indent) {
-  int ww, wh, wx, wy;
-
-  if(name)
-    fprintf(stderr, "%*s[%s]: '%s'\n", indent, "",
-            name, gtk_widget_get_name(w));
-  gdk_window_get_position(w->window, &wx, &wy);
-  gdk_drawable_get_size(GDK_DRAWABLE(w->window), &ww, &wh);
-  fprintf(stderr, "%*s window %p: %dx%d at %dx%d\n",
-          indent, "", w->window, ww, wh, wx, wy);
-}
-
-static void dump_layout(const struct queuelike *ql) {
-  GtkWidget *w;
-  char s[20];
-  int row, col;
-  const struct queue_entry *q;
-  
-  describe_widget("mainscroll", ql->mainscroll, 0);
-  describe_widget("mainlayout", ql->mainlayout, 1);
-  for(q = ql->q, row = 0; q; q = q->next, ++row)
-    for(col = 0; col < ql->ncolumns + 1; ++col)
-      if((w = ql->cells[row * (ql->ncolumns + 1) + col])) {
-        sprintf(s, "%dx%d", row, col);
-        describe_widget(s, w, 2);
-        if(GTK_BIN(w)->child)
-          describe_widget(0, w, 3);
-      }
-}
-#endif
-
-/* Track detail lookup ----------------------------------------------------- */
-
-/** @brief Called when a namepart lookup has completed or failed */
-static void namepart_completed_or_failed(void) {
-  D(("namepart_completed_or_failed"));
-  --namepart_lookups_outstanding;
-  if(!namepart_lookups_outstanding) {
-    redisplay_queue(&ql_queue);
-    redisplay_queue(&ql_recent);
-    redisplay_queue(&ql_added);
-    namepart_completions_deferred = 0;
-  }
-}
-
-/** @brief Called when A namepart lookup has completed */
-static void namepart_completed(void *v, const char *value) {
-  struct callbackdata *cbd = v;
-
-  D(("namepart_completed"));
-  cache_put(&cachetype_string, cbd->u.key, value);
-  ++namepart_completions_deferred;
-  namepart_completed_or_failed();
-}
-
-/** @brief Called when a length lookup has completed */
-static void length_completed(void *v, long l) {
-  struct callbackdata *cbd = v;
-  long *value;
-
-  D(("namepart_completed"));
-  value = xmalloc(sizeof *value);
-  *value = l;
-  cache_put(&cachetype_integer, cbd->u.key, value);
-  ++namepart_completions_deferred;
-  namepart_completed_or_failed();
-}
-
-/** @brief Called when a length or namepart lookup has failed */
-static void namepart_protocol_error(
-  struct callbackdata attribute((unused)) *cbd,
-  int attribute((unused)) code,
-  const char *msg) {
-  D(("namepart_protocol_error"));
-  gtk_label_set_text(GTK_LABEL(report_label), msg);
-  namepart_completed_or_failed();
-}
-
-/** @brief Arrange to fill in a namepart cache entry */
-static void namepart_fill(const char *track,
-                          const char *context,
-                          const char *part,
-                          const char *key) {
-  struct callbackdata *cbd;
-
-  ++namepart_lookups_outstanding;
-  cbd = xmalloc(sizeof *cbd);
-  cbd->onerror = namepart_protocol_error;
-  cbd->u.key = key;
-  disorder_eclient_namepart(client, namepart_completed,
-                            track, context, part, cbd);
-}
-
-/** @brief Look up a namepart
- *
- * If it is in the cache then just return its value.  If not then look it up
- * and arrange for the queues to be updated when its value is available. */
-static const char *namepart(const char *track,
-                            const char *context,
-                            const char *part) {
-  char *key;
-  const char *value;
-
-  D(("namepart %s %s %s", track, context, part));
-  byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
-                 context, part, track);
-  value = cache_get(&cachetype_string, key);
-  if(!value) {
-    D(("deferring..."));
-    /* stick a value in the cache so we don't issue another lookup if we
-     * revisit */
-    cache_put(&cachetype_string, key, value = "?");
-    namepart_fill(track, context, part, key);
-  }
-  return value;
-}
-
-/** @brief Called from @ref disobedience/properties.c when we know a name part has changed */
-void namepart_update(const char *track,
-                     const char *context,
-                     const char *part) {
-  char *key;
-
-  byte_xasprintf(&key, "namepart context=%s part=%s track=%s",
-                 context, part, track);
-  /* Only refetch if it's actually in the cache */
-  if(cache_get(&cachetype_string, key))
-    namepart_fill(track, context, part, key);
-}
-
-/** @brief Look up a track length
- *
- * If it is in the cache then just return its value.  If not then look it up
- * and arrange for the queues to be updated when its value is available. */
-static long getlength(const char *track) {
-  char *key;
-  const long *value;
-  struct callbackdata *cbd;
-  static const long bogus = -1;
-
-  D(("getlength %s", track));
-  byte_xasprintf(&key, "length track=%s", track);
-  value = cache_get(&cachetype_integer, key);
-  if(!value) {
-    D(("deferring..."));;
-    cache_put(&cachetype_integer, key, value = &bogus);
-    ++namepart_lookups_outstanding;
-    cbd = xmalloc(sizeof *cbd);
-    cbd->onerror = namepart_protocol_error;
-    cbd->u.key = key;
-    disorder_eclient_length(client, length_completed, track, cbd);
-  }
-  return *value;
-}
-
-/* Column constructors ----------------------------------------------------- */
-
-/** @brief Format the 'when' column */
-static GtkWidget *column_when(const struct queuelike attribute((unused)) *ql,
-                              const struct queue_entry *q,
-                              const char attribute((unused)) *data) {
-  char when[64];
-  struct tm tm;
-  time_t t;
-
-  D(("column_when"));
-  switch(q->state) {
-  case playing_isscratch:
-  case playing_unplayed:
-  case playing_random:
-    t = q->expected;
-    break;
-  case playing_failed:
-  case playing_no_player:
-  case playing_ok:
-  case playing_scratched:
-  case playing_started:
-  case playing_paused:
-  case playing_quitting:
-    t = q->played;
-    break;
-  default:
-    t = 0;
-    break;
-  }
-  if(t)
-    strftime(when, sizeof when, "%H:%M", localtime_r(&t, &tm));
-  else
-    when[0] = 0;
-  NW(label);
-  return gtk_label_new(when);
-}
-
-/** @brief Format the 'who' column */
-static GtkWidget *column_who(const struct queuelike attribute((unused)) *ql,
-                             const struct queue_entry *q,
-                             const char attribute((unused)) *data) {
-  D(("column_who"));
-  NW(label);
-  return gtk_label_new(q->submitter ? q->submitter : "");
-}
-
-/** @brief Format one of the track name columns */
-static GtkWidget *column_namepart(const struct queuelike
-                                               attribute((unused)) *ql,
-                                  const struct queue_entry *q,
-                                  const char *data) {
-  D(("column_namepart"));
-  NW(label);
-  return gtk_label_new(truncate_for_display(namepart(q->track, "display", data),
-                                            config->short_display));
-}
-
-/** @brief Compute the length field */
-static const char *text_length(const struct queue_entry *q) {
-  long l;
-  time_t now;
-  char *played = 0, *length = 0;
-
-  /* Work out what to say for the length */
-  l = getlength(q->track);
-  if(l > 0)
-    byte_xasprintf(&length, "%ld:%02ld", l / 60, l % 60);
-  else
-    byte_xasprintf(&length, "?:??");
-  /* For the currently playing track we want to report how much of the track
-   * has been played */
-  if(q == playing_track) {
-    /* log_state() arranges that we re-get the playing data whenever the
-     * pause/resume state changes */
-    if(last_state & DISORDER_TRACK_PAUSED)
-      l = playing_track->sofar;
-    else {
-      time(&now);
-      l = playing_track->sofar + (now - last_playing);
-    }
-    byte_xasprintf(&played, "%ld:%02ld/%s", l / 60, l % 60, length);
-    return played;
-  } else
-    return length;
-}
-
-/** @brief Format the length column */
-static GtkWidget *column_length(const struct queuelike attribute((unused)) *ql,
-                                const struct queue_entry *q,
-                                const char attribute((unused)) *data) {
-  D(("column_length"));
-  if(q == playing_track) {
-    assert(!playing_length_label);
-    NW(label);
-    playing_length_label = gtk_label_new(text_length(q));
-    /* Zot playing_length_label when it is destroyed */
-    g_signal_connect(playing_length_label, "destroy",
-                     G_CALLBACK(gtk_widget_destroyed), &playing_length_label);
-    return playing_length_label;
-  } else {
-    NW(label);
-    return gtk_label_new(text_length(q));
-  }
-  
-}
-
-/** @brief Apply a new queue contents, transferring the selection from the old value */
-static void update_queue(struct queuelike *ql, struct queue_entry *newq) {
-  struct queue_entry *q;
-
-  D(("update_queue"));
-  /* Propagate last_click across the change */
-  if(ql->last_click) {
-    for(q = newq; q; q = q->next) {
-      if(!strcmp(q->id, ql->last_click->id)) 
+#include "popup.h"
+#include "queue-generic.h"
+
+/** @brief The actual queue */
+static struct queue_entry *actual_queue;
+static struct queue_entry *actual_playing_track;
+
+/** @brief The playing track */
+struct queue_entry *playing_track;
+
+/** @brief When we last got the playing track */
+time_t last_playing;
+
+static void queue_completed(void *v,
+                            const char *err,
+                            struct queue_entry *q);
+static void playing_completed(void *v,
+                              const char *err,
+                              struct queue_entry *q);
+
+/** @brief Called when either the actual queue or the playing track change */
+static void queue_playing_changed(void) {
+
+  /* Check that the playing track isn't in the queue.  There's a race here due
+   * to the fact that we issue the two commands at slightly different times.
+   * If it goes wrong we re-issue and try again, so that we never offer up an
+   * inconsistent state. */
+  if(actual_playing_track) {
+    struct queue_entry *q;
+    for(q = actual_queue; q; q = q->next)
+      if(!strcmp(q->id, actual_playing_track->id))
         break;
-      ql->last_click = q;
+    if(q) {
+      disorder_eclient_playing(client, playing_completed, 0);
+      disorder_eclient_queue(client, queue_completed, 0);
+      return;
     }
   }
-  /* Tell every queue entry which queue owns it */
-  for(q = newq; q; q = q->next)
-    q->ql = ql;
-  /* Switch to the new queue */
-  ql->q = newq;
-  /* Clean up any selected items that have fallen off */
-  for(q = ql->q; q; q = q->next)
-    selection_live(ql->selection, q->id);
-  selection_cleanup(ql->selection);
-}
-
-/** @brief Wrap up a widget for putting into the queue or title
- * @param label Label to contain
- * @param style Pointer to style to use
- * @param wp Updated with maximum width (or NULL)
- * @return New widget
- */
-static GtkWidget *wrap_queue_cell(GtkWidget *label,
-                                  GtkStyle *style,
-                                  int *wp) {
-  GtkRequisition req;
-  GtkWidget *bg;
-
-  D(("wrap_queue_cell"));
-  /* Padding should be in the label so there are no gaps in the
-   * background */
-  gtk_misc_set_padding(GTK_MISC(label), HCELLPADDING, VCELLPADDING);
-  /* Event box is just to hold a background color */
-  NW(event_box);
-  bg = gtk_event_box_new();
-  gtk_container_add(GTK_CONTAINER(bg), label);
-  if(wp) {
-    /* Update maximum width */
-    gtk_widget_size_request(label, &req);
-    if(req.width > *wp) *wp = req.width;
+  
+  struct queue_entry *q = xmalloc(sizeof *q);
+  if(actual_playing_track) {
+    *q = *actual_playing_track;
+    q->next = actual_queue;
+    playing_track = q;
+  } else {
+    playing_track = NULL;
+    q = actual_queue;
   }
-  /* Set colors */
-  gtk_widget_set_style(bg, style);
-  gtk_widget_set_style(label, style);
-  return bg;
+  time(&last_playing);          /* for column_length() */
+  ql_new_queue(&ql_queue, q);
+  /* Tell anyone who cares */
+  event_raise("queue-list-changed", q);
+  event_raise("playing-track-changed", q);
 }
 
-/** @brief Create the wrapped widget for a cell in the queue display */
-static GtkWidget *get_queue_cell(struct queuelike *ql,
-                                 const struct queue_entry *q,
-                                 int row,
-                                 int col,
-                                 GtkStyle *style,
-                                 int *wp) {
-  GtkWidget *label;
-  D(("get_queue_cell %d %d", row, col));
-  label = ql->columns[col].widget(ql, q, ql->columns[col].data);
-  gtk_misc_set_alignment(GTK_MISC(label), ql->columns[col].xalign, 0);
-  return wrap_queue_cell(label, style, wp);
-}
-
-/** @brief Add a padding cell to the end of a row */
-static GtkWidget *get_padding_cell(GtkStyle *style) {
-  D(("get_padding_cell"));
-  NW(label);
-  return wrap_queue_cell(gtk_label_new(""), style, 0);
-}
-
-/* User button press and menu ---------------------------------------------- */
-
-/** @brief Update widget states in order to reflect the selection status */
-static void set_widget_states(struct queuelike *ql) {
-  struct queue_entry *q;
-  int row, col;
-
-  for(q = ql->q, row = 0; q; q = q->next, ++row) {
-    for(col = 0; col < ql->ncolumns + 1; ++col)
-      gtk_widget_set_state(ql->cells[row * (ql->ncolumns + 1) + col],
-                           selection_selected(ql->selection, q->id) ?
-                           GTK_STATE_SELECTED : GTK_STATE_NORMAL);
+/** @brief Update the queue itself */
+static void queue_completed(void attribute((unused)) *v,
+                            const char *err,
+                            struct queue_entry *q) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
   }
-  /* Might need to change sensitivity of 'Properties' in main menu */
-  menu_update(-1);
-}
-
-/** @brief Ordering function for queue entries */
-static int queue_before(const struct queue_entry *a,
-                        const struct queue_entry *b) {
-  while(a && a != b)
-    a = a->next;
-  return !!a;
+  actual_queue = q;
+  queue_playing_changed();
 }
 
-/** @brief A button was pressed and released */
-static gboolean queuelike_button_released(GtkWidget attribute((unused)) *widget,
-                                          GdkEventButton *event,
-                                          gpointer user_data) {
-  struct queue_entry *q = user_data, *qq;
-  struct queuelike *ql = q->ql;
-  struct menuiteminfo *mii;
-  int n;
-  
-  /* Might be a release left over from a drag */
-  if(ql->swallow_release) {
-    ql->swallow_release = 0;
-    return FALSE;                       /* propagate */
-  }
-
-  if(event->type == GDK_BUTTON_PRESS
-     && event->button == 3) {
-    /* Right button click.
-     * If the current item is not selected then switch the selection to just
-     * this item */
-    if(q && !selection_selected(ql->selection, q->id)) {
-      selection_empty(ql->selection);
-      selection_set(ql->selection, q->id, 1);
-      ql->last_click = q;
-      set_widget_states(ql);
-    }
-    /* Set the sensitivity of each menu item and (re-)establish the signal
-     * handlers */
-    for(n = 0; ql->menuitems[n].name; ++n) {
-      if(ql->menuitems[n].handlerid)
-        g_signal_handler_disconnect(ql->menuitems[n].w,
-                                    ql->menuitems[n].handlerid);
-      gtk_widget_set_sensitive(ql->menuitems[n].w,
-                               ql->menuitems[n].sensitive(ql,
-                                                          &ql->menuitems[n],
-                                                          q));
-      mii = xmalloc(sizeof *mii);
-      mii->ql = ql;
-      mii->q = q;
-      ql->menuitems[n].handlerid = g_signal_connect
-        (ql->menuitems[n].w, "activate",
-         G_CALLBACK(ql->menuitems[n].activate), mii);
-    }
-    /* Update the menu according to context */
-    gtk_widget_show_all(ql->menu);
-    gtk_menu_popup(GTK_MENU(ql->menu), 0, 0, 0, 0,
-                   event->button, event->time);
-    return TRUE;                        /* hide the click from other widgets */
-  }
-  if(event->type == GDK_BUTTON_RELEASE
-     && event->button == 1) {
-    /* no modifiers: select this, unselect everything else, set last click
-     * +ctrl: flip selection of this, set last click
-     * +shift: select from last click to here, don't set last click
-     * +ctrl+shift: select from last click to here, set last click
-     */
-    switch(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK)) {
-    case 0:
-      selection_empty(ql->selection);
-      selection_set(ql->selection, q->id, 1);
-      ql->last_click = q;
-      break;
-    case GDK_CONTROL_MASK:
-      selection_flip(ql->selection, q->id);
-      ql->last_click = q;
-      break;
-    case GDK_SHIFT_MASK:
-    case GDK_SHIFT_MASK|GDK_CONTROL_MASK:
-      if(ql->last_click) {
-        if(!(event->state & GDK_CONTROL_MASK))
-          selection_empty(ql->selection);
-        selection_set(ql->selection, q->id, 1);
-        qq = q;
-        if(queue_before(ql->last_click, q))
-          while(qq != ql->last_click) {
-            qq = qq->prev;
-            selection_set(ql->selection, qq->id, 1);
-          }
-        else
-          while(qq != ql->last_click) {
-            qq = qq->next;
-            selection_set(ql->selection, qq->id, 1);
-          }
-        if(event->state & GDK_CONTROL_MASK)
-          ql->last_click = q;
-      }
-      break;
-    }
-    set_widget_states(ql);
-    gtk_widget_queue_draw(ql->mainlayout);
+/** @brief Update the playing track */
+static void playing_completed(void attribute((unused)) *v,
+                              const char *err,
+                              struct queue_entry *q) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
   }
-  return FALSE;                         /* propagate */
+  actual_playing_track = q;
+  queue_playing_changed();
 }
 
-/** @brief A button was pressed or released on the mainlayout
+/** @brief Schedule an update to the queue
  *
- * For debugging only at the moment. */
-static gboolean mainlayout_button(GtkWidget attribute((unused)) *widget,
-                                  GdkEventButton attribute((unused)) *event,
-                                  gpointer attribute((unused)) user_data) {
-  return FALSE;                         /* propagate */
-}
-
-/** @brief Select all entries in a queue */
-void queue_select_all(struct queuelike *ql) {
-  struct queue_entry *qq;
-
-  for(qq = ql->q; qq; qq = qq->next)
-    selection_set(ql->selection, qq->id, 1);
-  ql->last_click = 0;
-  set_widget_states(ql);
-}
-
-/** @brief Deselect all entries in a queue */
-void queue_select_none(struct queuelike *ql) {
-  struct queue_entry *qq;
-
-  for(qq = ql->q; qq; qq = qq->next)
-    selection_set(ql->selection, qq->id, 0);
-  ql->last_click = 0;
-  set_widget_states(ql);
-}
-
-/** @brief Pop up properties for selected tracks */
-void queue_properties(struct queuelike *ql) {
-  struct vector v;
-  const struct queue_entry *qq;
-
-  vector_init(&v);
-  for(qq = ql->q; qq; qq = qq->next)
-    if(selection_selected(ql->selection, qq->id))
-      vector_append(&v, (char *)qq->track);
-  if(v.nvec)
-    properties(v.nvec, (const char **)v.vec);
+ * Called whenever a track is added to it or removed from it.
+ */
+static void queue_changed(const char attribute((unused)) *event,
+                           void  attribute((unused)) *eventdata,
+                           void  attribute((unused)) *callbackdata) {
+  D(("queue_changed"));
+  gtk_label_set_text(GTK_LABEL(report_label), "updating queue");
+  disorder_eclient_queue(client, queue_completed, 0);
 }
 
-/* Drag and drop rearrangement --------------------------------------------- */
-
-/** @brief Return nonzero if @p is a draggable row
+/** @brief Schedule an update to the playing track
  *
- * Only tracks in the main queue are draggable (and the currently playing track
- * is not draggable).
+ * Called whenever it changes
  */
-static int draggable_row(const struct queue_entry *q) {
-  return q->ql == &ql_queue && q != playing_track;
+static void playing_changed(const char attribute((unused)) *event,
+                            void  attribute((unused)) *eventdata,
+                            void  attribute((unused)) *callbackdata) {
+  D(("playing_changed"));
+  gtk_label_set_text(GTK_LABEL(report_label), "updating playing track");
+  disorder_eclient_playing(client, playing_completed, 0);
 }
 
-/** @brief Called when a drag begings */
-static void queue_drag_begin(GtkWidget attribute((unused)) *widget, 
-                             GdkDragContext attribute((unused)) *dc,
-                             gpointer data) {
-  struct queue_entry *q = data;
-  struct queuelike *ql = q->ql;
-
-  /* Make sure the playing track is not selected, since it cannot be dragged */
+/** @brief Called regularly
+ *
+ * Updates the played-so-far field
+ */
+static gboolean playing_periodic(gpointer attribute((unused)) data) {
+  /* If there's a track playing, update its row */
   if(playing_track)
-    selection_set(ql->selection, playing_track->id, 0);
-  /* If the dragged item is not in the selection then change the selection to
-   * just that */
-  if(!selection_selected(ql->selection, q->id)) {
-    selection_empty(ql->selection);
-    selection_set(ql->selection, q->id, 1);
-    set_widget_states(ql);
-  }
-  /* Ignore the eventual button release */
-  ql->swallow_release = 1;
-  /* Create dropzones */
-  add_drag_targets(ql);
-}
-
-/** @brief Convert @p id back into a queue entry and a screen row number */
-static struct queue_entry *findentry(struct queuelike *ql,
-                                     const char *id,
-                                     int *rowp) {
-  int row;
-  struct queue_entry *q;
-
-  if(id) {
-    for(q = ql->q, row = 0; q && strcmp(q->id, id); q = q->next, ++row)
-      ;
-  } else {
-    q = 0;
-    row = playing_track ? 0 : -1;
-  }
-  if(rowp) *rowp = row;
-  return q;
-}
-
-/** @brief Called when data is dropped */
-static gboolean queue_drag_drop(GtkWidget attribute((unused)) *widget,
-                                GdkDragContext *drag_context,
-                                gint attribute((unused)) x,
-                                gint attribute((unused)) y,
-                                guint when,
-                                gpointer user_data) {
-  struct queuelike *ql = &ql_queue;
-  const char *id = user_data;
-  struct vector vec;
-  struct queue_entry *q;
-
-  if(!id || (playing_track && !strcmp(id, playing_track->id)))
-    id = "";
-  vector_init(&vec);
-  for(q = ql->q; q; q = q->next)
-    if(q != playing_track && selection_selected(ql->selection, q->id))
-      vector_append(&vec, (char *)q->id);
-  disorder_eclient_moveafter(client, id, vec.nvec, (const char **)vec.vec,
-                             0/*completed*/, 0/*v*/);
-  gtk_drag_finish(drag_context, TRUE, TRUE, when);
-  /* Destroy dropzones */
-  remove_drag_targets(ql);
+    ql_update_row(playing_track, 0);
   return TRUE;
 }
 
-/** @brief Called when we enter, or move within, a drop zone */
-static gboolean queue_drag_motion(GtkWidget attribute((unused)) *widget,
-                                  GdkDragContext *drag_context,
-                                  gint attribute((unused)) x,
-                                  gint attribute((unused)) y,
-                                  guint when,
-                                  gpointer user_data) {
-  struct queuelike *ql = &ql_queue;
-  const char *id = user_data;
-  int row;
-  struct queue_entry *q = findentry(ql, id, &row);
+/** @brief Called at startup */
+static void queue_init(void) {
+  /* Arrange a callback whenever the playing state changes */ 
+  event_register("playing-changed", playing_changed, 0);
+  /* We reget both playing track and queue at pause/resume so that start times
+   * can be computed correctly */
+  event_register("pause-changed", playing_changed, 0);
+  event_register("pause-changed", queue_changed, 0);
+  /* Reget the queue whenever it changes */
+  event_register("queue-changed", queue_changed, 0);
+  /* ...and once a second anyway */
+  g_timeout_add(1000/*ms*/, playing_periodic, 0);
+}
+
+/** @brief Columns for the queue */
+static const struct queue_column queue_columns[] = {
+  { "When",   column_when,     0,        COL_RIGHT },
+  { "Who",    column_who,      0,        0 },
+  { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
+  { "Album",  column_namepart, "album",  COL_EXPAND|COL_ELLIPSIZE },
+  { "Title",  column_namepart, "title",  COL_EXPAND|COL_ELLIPSIZE },
+  { "Length", column_length,   0,        COL_RIGHT }
+};
 
-  if(!id || q) {
-    if(!ql->dragmark) {
-      NW(event_box);
-      ql->dragmark = gtk_event_box_new();
-      g_signal_connect(ql->dragmark, "destroy",
-                       G_CALLBACK(gtk_widget_destroyed), &ql->dragmark);
-      gtk_widget_set_size_request(ql->dragmark, 10240, row ? 4 : 2);
-      gtk_widget_set_style(ql->dragmark, drag_style);
-      gtk_layout_put(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, 
-                     (row + 1) * ql->mainrowheight - !!row);
-    } else
-      gtk_layout_move(GTK_LAYOUT(ql->mainlayout), ql->dragmark, 0, 
-                      (row + 1) * ql->mainrowheight - !!row);
-    gtk_widget_show(ql->dragmark);
-    gdk_drag_status(drag_context, GDK_ACTION_MOVE, when);
-    return TRUE;
-  } else
-    /* ID has gone AWOL */
-    return FALSE;
-}                              
+/** @brief Pop-up menu for queue */
+static struct menuitem queue_menuitems[] = {
+  { "Track properties", ql_properties_activate, ql_properties_sensitive, 0, 0 },
+  { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+  { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+  { "Scratch playing track", ql_scratch_activate, ql_scratch_sensitive, 0, 0 },
+  { "Remove track from queue", ql_remove_activate, ql_remove_sensitive, 0, 0 },
+};
 
-/** @brief Called when we leave a drop zone */
-static void queue_drag_leave(GtkWidget attribute((unused)) *widget,
-                             GdkDragContext attribute((unused)) *drag_context,
-                             guint attribute((unused)) when,
-                             gpointer attribute((unused)) user_data) {
-  struct queuelike *ql = &ql_queue;
-  
-  if(ql->dragmark)
-    gtk_widget_hide(ql->dragmark);
-}
+struct queuelike ql_queue = {
+  .name = "queue",
+  .init = queue_init,
+  .columns = queue_columns,
+  .ncolumns = sizeof queue_columns / sizeof *queue_columns,
+  .menuitems = queue_menuitems,
+  .nmenuitems = sizeof queue_menuitems / sizeof *queue_menuitems
+};
 
-/** @brief Add a drag target
- * @param ql The queue-like (in practice this is always @ref ql_queue)
- * @param y The Y coordinate to place the drag target
- * @param id Track to insert moved tracks after, or NULL
+/* Drag and drop has to be figured out experimentally, because it is not well
+ * documented.
+ *
+ * First you get a row-inserted.  The path argument points to the destination
+ * row but this will not yet have had its values set.  The source row is still
+ * present.  AFAICT the iter argument points to the same place.
  *
- * Adds a drop zone at Y coordinate @p y, which is assumed to lie between two
- * tracks (or before the start of the queue or after the end of the queue).  If
- * tracks are dragged into this dropzone then they will be moved @em after
- * track @p id, or to the start of the queue if @p id is NULL.
+ * Then you get a row-deleted.  The path argument identifies the row that was
+ * deleted.  By this stage the row inserted above has acquired its values.
  *
- * We remember all the dropzones in @c ql->dropzones so they can be destroyed
- * later.
+ * A complication is that the deletion will move the inserted row.  For
+ * instance, if you do a drag that moves row 1 down to after the track that was
+ * formerly on row 9, in the row-inserted call it will show up as row 10, but
+ * in the row-deleted call, row 1 will have been deleted thus making the
+ * inserted row be row 9.
+ *
+ * So when we see the row-inserted we have no idea what track to move.
+ * Therefore we stash it until we see a row-deleted.
  */
-static void add_drag_target(struct queuelike *ql, int y,
-                            const char *id) {
-  GtkWidget *eventbox;
-
-  NW(event_box);
-  eventbox = gtk_event_box_new();
-  /* Make the target zone invisible */
-  gtk_event_box_set_visible_window(GTK_EVENT_BOX(eventbox), FALSE);
-  /* Make it large enough */
-  gtk_widget_set_size_request(eventbox, 10240, 
-                              y ? ql->mainrowheight : ql->mainrowheight / 2);
-  /* Position it */
-  gtk_layout_put(GTK_LAYOUT(ql->mainlayout), eventbox, 0,
-                 y ? y - ql->mainrowheight / 2 : 0);
-  /* Mark it as capable of receiving drops */
-  gtk_drag_dest_set(eventbox,
-                    0,
-                    dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE);
-  g_signal_connect(eventbox, "drag-drop",
-                   G_CALLBACK(queue_drag_drop), (char *)id);
-  /* Monitor drag motion */
-  g_signal_connect(eventbox, "drag-motion",
-                   G_CALLBACK(queue_drag_motion), (char *)id);
-  g_signal_connect(eventbox, "drag-leave",
-                   G_CALLBACK(queue_drag_leave), (char *)id);
-  /* The widget needs to be shown to receive drags */
-  gtk_widget_show(eventbox);
-  /* Remember the drag targets */
-  ql->dropzones[ql->ndropzones] = eventbox;
-  g_signal_connect(eventbox, "destroy",
-                   G_CALLBACK(gtk_widget_destroyed),
-                   &ql->dropzones[ql->ndropzones]);
-  ++ql->ndropzones;
-}
 
-/** @brief Create dropzones for dragging into */
-static void add_drag_targets(struct queuelike *ql) {
-  int y;
-  struct queue_entry *q;
+/** @brief Target row for drag */
+static int queue_drag_target = -1;
 
-  /* Create an array to store the widgets */
-  ql->dropzones = xcalloc(ql->nrows, sizeof (GtkWidget *));
-  ql->ndropzones = 0;
-  y = 0;
-  /* Add a drag target before the first row provided it's not the playing
-   * track */
-  if(!playing_track || ql->q != playing_track)
-    add_drag_target(ql, 0, 0);
-  /* Put a drag target at the bottom of every row */
-  for(q = ql->q; q; q = q->next) {
-    y += ql->mainrowheight;
-    add_drag_target(ql, y, q->id);
+static void queue_move_completed(void attribute((unused)) *v,
+                                 const char *err) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
   }
+  /* The log should tell us the queue changed so we do no more here */
 }
 
-/** @brief Remove the dropzones */
-static void remove_drag_targets(struct queuelike *ql) {
-  int n;
-
-  for(n = 0; n < ql->ndropzones; ++n) {
-    if(ql->dropzones[n]) {
-      DW(event_box);
-      gtk_widget_destroy(ql->dropzones[n]);
+static void queue_row_deleted(GtkTreeModel *treemodel,
+                              GtkTreePath *path,
+                              gpointer attribute((unused)) user_data) {
+  if(!suppress_actions) {
+#if 0
+    char *ps = gtk_tree_path_to_string(path);
+    fprintf(stderr, "row-deleted path=%s queue_drag_target=%d\n",
+            ps, queue_drag_target);
+    GtkTreeIter j[1];
+    gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
+    int row = 0;
+    while(jt) {
+      struct queue_entry *q = ql_iter_to_q(treemodel, j);
+      fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
+      jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
     }
-    assert(ql->dropzones[n] == 0);
-  }
-}
-
-/* Layout ------------------------------------------------------------------ */
-
-/** @brief Redisplay a queue */
-static void redisplay_queue(struct queuelike *ql) {
-  struct queue_entry *q;
-  int row, col;
-  GList *c, *children;
-  GtkStyle *style;
-  GtkRequisition req;  
-  GtkWidget *w;
-  int maxwidths[MAXCOLUMNS], x, y, titlerowheight;
-  int totalwidth = 10240;               /* TODO: can we be less blunt */
-
-  D(("redisplay_queue"));
-  /* Eliminate all the existing widgets and start from scratch */
-  for(c = children = gtk_container_get_children(GTK_CONTAINER(ql->mainlayout));
-      c;
-      c = c->next) {
-    /* Destroy both the label and the eventbox */
-    if(GTK_BIN(c->data)->child) {
-      DW(label);
-      gtk_widget_destroy(GTK_BIN(c->data)->child);
+    g_free(ps);
+#endif
+    if(queue_drag_target < 0) {
+      error(0, "unsuppressed row-deleted with no row-inserted");
+      return;
     }
-    DW(event_box);
-    gtk_widget_destroy(GTK_WIDGET(c->data));
-  }
-  g_list_free(children);
-  /* Adjust the row count */
-  for(q = ql->q, ql->nrows = 0; q; q = q->next)
-    ++ql->nrows;
-  /* We need to create all the widgets before we can position them */
-  ql->cells = xcalloc(ql->nrows * (ql->ncolumns + 1), sizeof *ql->cells);
-  /* Minimum width is given by the column headings */
-  for(col = 0; col < ql->ncolumns; ++col) {
-    /* Reset size so we don't inherit last iteration's maximum size */
-    gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child, -1, -1);
-    gtk_widget_size_request(GTK_BIN(ql->titlecells[col])->child, &req);
-    maxwidths[col] = req.width;
-  }
-  /* Find the vertical size of the title bar */
-  gtk_widget_size_request(GTK_BIN(ql->titlecells[0])->child, &req);
-  titlerowheight = req.height;
-  y = 0;
-  if(ql->nrows) {
-    /* Construct the widgets */
-    for(q = ql->q, row = 0; q; q = q->next, ++row) {
-      /* Figure out the widget name for this row */
-      if(q == playing_track) style = active_style;
-      else style = row % 2 ? even_style : odd_style;
-      /* Make the widget for each column */
-      for(col = 0; col <= ql->ncolumns; ++col) {
-        /* Create and store the widget */
-        if(col < ql->ncolumns)
-          w = get_queue_cell(ql, q, row, col, style, &maxwidths[col]);
-        else
-          w = get_padding_cell(style);
-        ql->cells[row * (ql->ncolumns + 1) + col] = w;
-        /* Maybe mark it draggable */
-        if(draggable_row(q)) {
-          gtk_drag_source_set(w, GDK_BUTTON1_MASK,
-                              dragtargets, NDRAGTARGETS, GDK_ACTION_MOVE);
-          g_signal_connect(w, "drag-begin", G_CALLBACK(queue_drag_begin), q);
-        }
-        /* Catch button presses */
-        g_signal_connect(w, "button-release-event",
-                         G_CALLBACK(queuelike_button_released), q);
-        g_signal_connect(w, "button-press-event",
-                         G_CALLBACK(queuelike_button_released), q);
-      }
+    int drag_source = gtk_tree_path_get_indices(path)[0];
+
+    /* If the drag is downwards (=towards higher row numbers) then the target
+     * will have been moved upwards (=towards lower row numbers) by one row. */
+    if(drag_source < queue_drag_target)
+      --queue_drag_target;
+    
+    /* Find the track to move */
+    GtkTreeIter src[1];
+    gboolean srcv = gtk_tree_model_iter_nth_child(treemodel, src, NULL,
+                                                  queue_drag_target);
+    if(!srcv) {
+      error(0, "cannot get iterator to drag target %d", queue_drag_target);
+      queue_playing_changed();
+      queue_drag_target = -1;
+      return;
     }
-    /* ...and of each row in the main layout */
-    gtk_widget_size_request(GTK_BIN(ql->cells[0])->child, &req);
-    ql->mainrowheight = req.height;
-    /* Now we know the maximum width of each column we can set the size of
-     * everything and position it */
-    for(row = 0, q = ql->q; row < ql->nrows; ++row, q = q->next) {
-      x = 0;
-      for(col = 0; col < ql->ncolumns; ++col) {
-        w = ql->cells[row * (ql->ncolumns + 1) + col];
-        gtk_widget_set_size_request(GTK_BIN(w)->child,
-                                    maxwidths[col], -1);
-        gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y);
-        x += maxwidths[col];
-      }
-      w = ql->cells[row * (ql->ncolumns + 1) + col];
-      gtk_widget_set_size_request(GTK_BIN(w)->child,
-                                  totalwidth - x, -1);
-      gtk_layout_put(GTK_LAYOUT(ql->mainlayout), w, x, y);
-      y += ql->mainrowheight;
+    struct queue_entry *srcq = ql_iter_to_q(treemodel, src);
+    assert(srcq);
+    //fprintf(stderr, "move %s %s\n", srcq->id, srcq->track);
+    
+    /* Don't allow the currently playing track to be moved.  As above, we put
+     * the queue back into the right order straight away. */
+    if(srcq == playing_track) {
+      //fprintf(stderr, "cannot move currently playing track\n");
+      queue_playing_changed();
+      queue_drag_target = -1;
+      return;
     }
-  }
-  /* Titles */
-  x = 0;
-  for(col = 0; col < ql->ncolumns; ++col) {
-    gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child,
-                                maxwidths[col], -1);
-    gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0);
-    x += maxwidths[col];
-  }
-  gtk_widget_set_size_request(GTK_BIN(ql->titlecells[col])->child,
-                              totalwidth - x, -1);
-  gtk_layout_move(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], x, 0);
-  /* Set the states */
-  set_widget_states(ql);
-  /* Make sure it's all visible */
-  gtk_widget_show_all(ql->mainlayout);
-  gtk_widget_show_all(ql->titlelayout);
-  /* Layouts might shrink to arrange for the area they shrink out of to be
-   * redrawn */
-  gtk_widget_queue_draw(ql->mainlayout);
-  gtk_widget_queue_draw(ql->titlelayout);
-  /* Adjust the size of the layout */
-  gtk_layout_set_size(GTK_LAYOUT(ql->mainlayout), x, y);
-  gtk_layout_set_size(GTK_LAYOUT(ql->titlelayout), x, titlerowheight);
-  gtk_widget_set_size_request(ql->titlelayout, -1, titlerowheight);
-}
-
-/** @brief Called with new queue/recent contents */ 
-static void queuelike_completed(void *v, struct queue_entry *q) {
-  struct callbackdata *cbd = v;
-  struct queuelike *ql = cbd->u.ql;
-
-  D(("queuelike_complete"));
-  /* Install the new queue */
-  update_queue(ql, ql->fixup ? ql->fixup(q) : q);
-  /* Update the display */
-  redisplay_queue(ql);
-  if(ql->notify)
-    ql->notify();
-  /* Update sensitivity of main menu items */
-  menu_update(-1);
-}
-
-/** @brief Called with a new currently playing track */
-static void playing_completed(void attribute((unused)) *v,
-                              struct queue_entry *q) {
-  struct callbackdata cbd;
-  D(("playing_completed"));
-  playing_track = q;
-  /* Record when we got the playing track data so we know how old the 'sofar'
-   * field is */
-  time(&last_playing);
-  cbd.u.ql = &ql_queue;
-  queuelike_completed(&cbd, actual_queue);
-}
 
-/** @brief Called when the queue is scrolled */
-static void queue_scrolled(GtkAdjustment *adjustment,
-                           gpointer user_data) {
-  GtkAdjustment *titleadj = user_data;
-
-  D(("queue_scrolled"));
-  gtk_adjustment_set_value(titleadj, adjustment->value);
+    /* Find the destination */
+    struct queue_entry *dstq;
+    if(queue_drag_target) {
+      GtkTreeIter dst[1];
+      gboolean dstv = gtk_tree_model_iter_nth_child(treemodel, dst, NULL,
+                                                    queue_drag_target - 1);
+      if(!dstv) {
+        error(0, "cannot get iterator to drag target predecessor %d",
+              queue_drag_target - 1);
+        queue_playing_changed();
+        queue_drag_target = -1;
+        return;
+      }
+      dstq = ql_iter_to_q(treemodel, dst);
+      assert(dstq);
+      if(dstq == playing_track)
+        dstq = 0;
+    } else
+      dstq = 0;
+    /* NB if the user attempts to move a queued track before the currently
+     * playing track we assume they just missed a bit, and put it after. */
+    //fprintf(stderr, " target %s %s\n", dstq ? dstq->id : "(none)", dstq ? dstq->track : "(none)");
+    /* Now we know what is to be moved.  We need to know the preceding queue
+     * entry so we can move it. */
+    disorder_eclient_moveafter(client,
+                               dstq ? dstq->id : "",
+                               1, &srcq->id,
+                               queue_move_completed, NULL);
+    queue_drag_target = -1;
+  }
 }
 
-/** @brief Create a queuelike thing (queue/recent) */
-static GtkWidget *queuelike(struct queuelike *ql,
-                            struct queue_entry *(*fixup)(struct queue_entry *),
-                            void (*notify)(void),
-                            struct queue_menuitem *menuitems,
-                            const struct column *columns,
-                            int ncolumns) {
-  GtkWidget *vbox, *mainscroll, *titlescroll, *label;
-  GtkAdjustment *mainadj, *titleadj;
-  int col, n;
-
-  D(("queuelike"));
-  ql->fixup = fixup;
-  ql->notify = notify;
-  ql->menuitems = menuitems;
-  ql->mainrowheight = !0;                /* else division by 0 */
-  ql->selection = selection_new();
-  ql->columns = columns;
-  ql->ncolumns = ncolumns;
-  /* Create the layouts */
-  NW(layout);
-  ql->mainlayout = gtk_layout_new(0, 0);
-  gtk_widget_set_style(ql->mainlayout, layout_style);
-  NW(layout);
-  ql->titlelayout = gtk_layout_new(0, 0);
-  gtk_widget_set_style(ql->titlelayout, title_style);
-  /* Scroll the layouts */
-  ql->mainscroll = mainscroll = scroll_widget(ql->mainlayout);
-  titlescroll = scroll_widget(ql->titlelayout);
-  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(titlescroll),
-                                 GTK_POLICY_NEVER, GTK_POLICY_NEVER);
-  mainadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(mainscroll));
-  titleadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(titlescroll));
-  g_signal_connect(mainadj, "changed", G_CALLBACK(queue_scrolled), titleadj);
-  g_signal_connect(mainadj, "value-changed", G_CALLBACK(queue_scrolled), titleadj);
-  /* Fill the titles and put them anywhere */
-  for(col = 0; col < ql->ncolumns; ++col) {
-    NW(label);
-    label = gtk_label_new(ql->columns[col].name);
-    gtk_misc_set_alignment(GTK_MISC(label), ql->columns[col].xalign, 0);
-    ql->titlecells[col] = wrap_queue_cell(label, title_style, 0);
-    gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0);
-  }
-  ql->titlecells[col] = get_padding_cell(title_style);
-  gtk_layout_put(GTK_LAYOUT(ql->titlelayout), ql->titlecells[col], 0, 0);
-  /* Pack the lot together in a vbox */
-  NW(vbox);
-  vbox = gtk_vbox_new(0, 0);
-  gtk_box_pack_start(GTK_BOX(vbox), titlescroll, 0, 0, 0);
-  gtk_box_pack_start(GTK_BOX(vbox), mainscroll, 1, 1, 0);
-  /* Create the popup menu */
-  NW(menu);
-  ql->menu = gtk_menu_new();
-  g_signal_connect(ql->menu, "destroy",
-                   G_CALLBACK(gtk_widget_destroyed), &ql->menu);
-  for(n = 0; menuitems[n].name; ++n) {
-    NW(menu_item);
-    menuitems[n].w = gtk_menu_item_new_with_label(menuitems[n].name);
-    gtk_menu_attach(GTK_MENU(ql->menu), menuitems[n].w, 0, 1, n, n + 1);
-  }
-  g_object_set_data(G_OBJECT(vbox), "type", (void *)&tabtype_queue);
-  g_object_set_data(G_OBJECT(vbox), "queue", ql);
-  /* Catch button presses */
-  g_signal_connect(ql->mainlayout, "button-release-event",
-                   G_CALLBACK(mainlayout_button), ql);
+static void queue_row_inserted(GtkTreeModel attribute((unused)) *treemodel,
+                               GtkTreePath *path,
+                               GtkTreeIter attribute((unused)) *iter,
+                               gpointer attribute((unused)) user_data) {
+  if(!suppress_actions) {
 #if 0
-  g_signal_connect(ql->mainlayout, "button-press-event",
-                   G_CALLBACK(mainlayout_button), ql);
+    char *ps = gtk_tree_path_to_string(path);
+    GtkTreeIter piter[1];
+    gboolean pi = gtk_tree_model_get_iter(treemodel, piter, path);
+    struct queue_entry *pq = pi ? ql_iter_to_q(treemodel, piter) : 0;
+    struct queue_entry *iq = ql_iter_to_q(treemodel, iter);
+
+    fprintf(stderr, "row-inserted path=%s pi=%d pq=%p path=%s iq=%p iter=%s\n",
+            ps,
+            pi,
+            pq,
+            (pi
+             ? (pq ? pq->track : "(pq=0)")
+             : "(pi=FALSE)"),
+            iq,
+            iq ? iq->track : "(iq=0)");
+
+    GtkTreeIter j[1];
+    gboolean jt = gtk_tree_model_get_iter_first(treemodel, j);
+    int row = 0;
+    while(jt) {
+      struct queue_entry *q = ql_iter_to_q(treemodel, j);
+      fprintf(stderr, " %2d %s\n", row++, q ? q->track : "(no q)");
+      jt = gtk_tree_model_iter_next(GTK_TREE_MODEL(ql_queue.store), j);
+    }
+    g_free(ps);
 #endif
-  set_tool_colors(ql->menu);
-  return vbox;
-}
-
-/* Popup menu items -------------------------------------------------------- */
-
-/** @brief Count the number of items selected */
-static int queue_count_selected(const struct queuelike *ql) {
-  return hash_count(ql->selection);
-}
-
-/** @brief Count the number of items selected */
-static int queue_count_entries(const struct queuelike *ql) {
-  int nitems = 0;
-  const struct queue_entry *q;
-
-  for(q = ql->q; q; q = q->next)
-    ++nitems;
-  return nitems;
-}
-
-/** @brief Count the number of items selected, excluding the playing track if
- * there is one */
-static int count_selected_nonplaying(const struct queuelike *ql) {
-  int nselected = queue_count_selected(ql);
-
-  if(ql->q == playing_track && selection_selected(ql->selection, ql->q->id))
-    --nselected;
-  return nselected;
-}
-
-/** @brief Determine whether the scratch option should be sensitive */
-static int scratch_sensitive(struct queuelike attribute((unused)) *ql,
-                             struct queue_menuitem attribute((unused)) *m,
-                             struct queue_entry attribute((unused)) *q) {
-  /* We can scratch if the playing track is selected */
-  return (playing_track
-          && (disorder_eclient_state(client) & DISORDER_CONNECTED)
-          && selection_selected(ql->selection, playing_track->id));
-}
-
-/** @brief Scratch the playing track */
-static void scratch_activate(GtkMenuItem attribute((unused)) *menuitem,
-                             gpointer attribute((unused)) user_data) {
-  if(playing_track)
-    disorder_eclient_scratch(client, playing_track->id, 0, 0);
-}
-
-/** @brief Determine whether the remove option should be sensitive */
-static int remove_sensitive(struct queuelike *ql,
-                            struct queue_menuitem attribute((unused)) *m,
-                            struct queue_entry *q) {
-  /* We can remove if we're hovering over a particular track or any non-playing
-   * tracks are selected */
-  return ((disorder_eclient_state(client) & DISORDER_CONNECTED)
-          && ((q
-               && q != playing_track)
-              || count_selected_nonplaying(ql)));
-}
-
-/** @brief Remove selected track(s) */
-static void remove_activate(GtkMenuItem attribute((unused)) *menuitem,
-                            gpointer user_data) {
-  const struct menuiteminfo *mii = user_data;
-  struct queue_entry *q = mii->q;
-  struct queuelike *ql = mii->ql;
-
-  if(count_selected_nonplaying(mii->ql)) {
-    /* Remove selected tracks */
-    for(q = ql->q; q; q = q->next)
-      if(selection_selected(ql->selection, q->id) && q != playing_track)
-        disorder_eclient_remove(client, q->id, 0, 0);
-  } else if(q)
-    /* Remove just the hovered track */
-    disorder_eclient_remove(client, q->id, 0, 0);
-}
-
-/** @brief Determine whether the properties menu option should be sensitive */
-static int properties_sensitive(struct queuelike *ql,
-                                struct queue_menuitem attribute((unused)) *m,
-                                struct queue_entry attribute((unused)) *q) {
-  /* "Properties" is sensitive if at least something is selected */
-  return (hash_count(ql->selection) > 0
-          && (disorder_eclient_state(client) & DISORDER_CONNECTED));
-}
-
-/** @brief Pop up properties for the selected tracks */
-static void properties_activate(GtkMenuItem attribute((unused)) *menuitem,
-                                gpointer user_data) {
-  const struct menuiteminfo *mii = user_data;
-  
-  queue_properties(mii->ql);
-}
-
-/** @brief Determine whether the select all menu option should be sensitive */
-static int selectall_sensitive(struct queuelike *ql,
-                               struct queue_menuitem attribute((unused)) *m,
-                               struct queue_entry attribute((unused)) *q) {
-  /* Sensitive if there is anything to select */
-  return !!ql->q;
-}
-
-/** @brief Select all tracks */
-static void selectall_activate(GtkMenuItem attribute((unused)) *menuitem,
-                               gpointer user_data) {
-  const struct menuiteminfo *mii = user_data;
-  queue_select_all(mii->ql);
-}
-
-/** @brief Determine whether the select none menu option should be sensitive */
-static int selectnone_sensitive(struct queuelike *ql,
-                                struct queue_menuitem attribute((unused)) *m,
-                                struct queue_entry attribute((unused)) *q) {
-  /* Sensitive if there is anything selected */
-  return hash_count(ql->selection) != 0;
-}
-
-/** @brief Select no tracks */
-static void selectnone_activate(GtkMenuItem attribute((unused)) *menuitem,
-                               gpointer user_data) {
-  const struct menuiteminfo *mii = user_data;
-  queue_select_none(mii->ql);
-}
-
-/** @brief Determine whether the play menu option should be sensitive */
-static int play_sensitive(struct queuelike *ql,
-                          struct queue_menuitem attribute((unused)) *m,
-                          struct queue_entry attribute((unused)) *q) {
-  /* "Play" is sensitive if at least something is selected */
-  return (hash_count(ql->selection) > 0
-          && (disorder_eclient_state(client) & DISORDER_CONNECTED));
-}
-
-/** @brief Play the selected tracks */
-static void play_activate(GtkMenuItem attribute((unused)) *menuitem,
-                          gpointer user_data) {
-  const struct menuiteminfo *mii = user_data;
-  struct queue_entry *q = mii->q;
-  struct queuelike *ql = mii->ql;
-
-  if(queue_count_selected(ql)) {
-    /* Play selected tracks */
-    for(q = ql->q; q; q = q->next)
-      if(selection_selected(ql->selection, q->id))
-        disorder_eclient_play(client, q->track, 0, 0);
-  } else if(q)
-    /* Nothing is selected, so play the hovered track */
-    disorder_eclient_play(client, q->track, 0, 0);
-}
-
-/* The queue --------------------------------------------------------------- */
-
-/** @brief Fix up the queue by sticking the currently playing track on the front */
-static struct queue_entry *fixup_queue(struct queue_entry *q) {
-  D(("fixup_queue"));
-  actual_queue = q;
-  if(playing_track) {
-    if(actual_queue)
-      actual_queue->prev = playing_track;
-    playing_track->next = actual_queue;
-    return playing_track;
-  } else
-    return actual_queue;
-}
-
-/** @brief Adjust track played label
- *
- *  Called regularly to adjust the so-far played label (redrawing the whole
- * queue once a second makes disobedience occupy >10% of the CPU on my Athlon
- * which is ureasonable expensive) */
-static gboolean adjust_sofar(gpointer attribute((unused)) data) {
-  if(playing_length_label && playing_track)
-    gtk_label_set_text(GTK_LABEL(playing_length_label),
-                       text_length(playing_track));
-  return TRUE;
-}
-
-/** @brief Popup menu for the queue
- *
- * Properties first so that finger trouble is less dangerous. */
-static struct queue_menuitem queue_menu[] = {
-  { "Track properties", properties_activate, properties_sensitive, 0, 0 },
-  { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 },
-  { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 },
-  { "Scratch track", scratch_activate, scratch_sensitive, 0, 0 },
-  { "Remove track from queue", remove_activate, remove_sensitive, 0, 0 },
-  { 0, 0, 0, 0, 0 }
-};
-
-/** @brief Called whenever @ref DISORDER_PLAYING or @ref DISORDER_TRACK_PAUSED changes
- *
- * We monitor pause/resume as well as whether the track is playing in order to
- * keep the time played so far up to date correctly.  See playing_completed().
- */
-static void playing_update(void attribute((unused)) *v) {
-  D(("playing_update"));
-  gtk_label_set_text(GTK_LABEL(report_label), "updating playing track");
-  disorder_eclient_playing(client, playing_completed, 0);
-}
-
-/** @brief Create the queue widget */
-GtkWidget *queue_widget(void) {
-  D(("queue_widget"));
-  /* Arrange periodic update of the so-far played field */
-  g_timeout_add(1000/*ms*/, adjust_sofar, 0);
-  /* Arrange a callback whenever the playing state changes */ 
-  register_monitor(playing_update, 0, DISORDER_PLAYING|DISORDER_TRACK_PAUSED);
-  register_reset(queue_update);
-  /* We pass choose_update() as our notify function since the choose screen
-   * marks tracks that are playing/in the queue. */
-  return queuelike(&ql_queue, fixup_queue, choose_update, queue_menu,
-                   maincolumns, NMAINCOLUMNS);
-}
-
-/** @brief Arrange an update of the queue widget
- *
- * Called when a track is added to the queue, removed from the queue (by user
- * cmmand or because it is to be played) or moved within the queue
- */
-void queue_update(void) {
-  struct callbackdata *cbd;
-
-  D(("queue_update"));
-  cbd = xmalloc(sizeof *cbd);
-  cbd->onerror = 0;
-  cbd->u.ql = &ql_queue;
-  gtk_label_set_text(GTK_LABEL(report_label), "updating queue");
-  disorder_eclient_queue(client, queuelike_completed, cbd);
-}
-
-/* Recently played tracks -------------------------------------------------- */
-
-/** @brief Fix up the recently played list
- *
- * It's in the wrong order!  TODO fix this globally */
-static struct queue_entry *fixup_recent(struct queue_entry *q) {
-  struct queue_entry *qr = 0,  *qn;
-
-  D(("fixup_recent"));
-  while(q) {
-    qn = q->next;
-    /* Swap next/prev pointers */
-    q->next = q->prev;
-    q->prev = qn;
-    /* Remember last node for new head */
-    qr = q;
-    /* Next node */
-    q = qn;
+    queue_drag_target = gtk_tree_path_get_indices(path)[0];
   }
-  return qr;
 }
 
-/** @brief Pop-up menu for recently played list */
-static struct queue_menuitem recent_menu[] = {
-  { "Track properties", properties_activate, properties_sensitive,0, 0 },
-  { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 },
-  { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 },
-  { 0, 0, 0, 0, 0 }
-};
-
-/** @brief Create the recently-played list */
-GtkWidget *recent_widget(void) {
-  D(("recent_widget"));
-  register_reset(recent_update);
-  return queuelike(&ql_recent, fixup_recent, 0, recent_menu,
-                   maincolumns, NMAINCOLUMNS);
-}
-
-/** @brief Update the recently played list
- *
- * Called whenever a track is added to it or removed from it.
- */
-void recent_update(void) {
-  struct callbackdata *cbd;
-
-  D(("recent_update"));
-  cbd = xmalloc(sizeof *cbd);
-  cbd->onerror = 0;
-  cbd->u.ql = &ql_recent;
-  gtk_label_set_text(GTK_LABEL(report_label), "updating recently played list");
-  disorder_eclient_recent(client, queuelike_completed, cbd);
-}
-
-/* Newly added tracks ------------------------------------------------------ */
-
-/** @brief Pop-up menu for recently played list */
-static struct queue_menuitem added_menu[] = {
-  { "Track properties", properties_activate, properties_sensitive, 0, 0 },
-  { "Play track", play_activate, play_sensitive, 0, 0 },
-  { "Select all tracks", selectall_activate, selectall_sensitive, 0, 0 },
-  { "Deselect all tracks", selectnone_activate, selectnone_sensitive, 0, 0 },
-  { 0, 0, 0, 0, 0 }
-};
-
-/** @brief Create the newly-added list */
-GtkWidget *added_widget(void) {
-  D(("added_widget"));
-  register_reset(added_update);
-  return queuelike(&ql_added, 0/*fixup*/, 0/*notify*/, added_menu,
-                   addedcolumns, NADDEDCOLUMNS);
-}
-
-/** @brief Called with an updated list of newly-added tracks
- *
- * This is called with a raw list of track names but the rest of @ref
- * disobedience/queue.c requires @ref queue_entry structures with a valid and
- * unique @c id field.  This function fakes it.
- */
-static void new_completed(void *v, int nvec, char **vec) {
-  struct queue_entry *q, *qh, *qlast = 0, **qq = &qh;
-  int n;
-
-  for(n = 0; n < nvec; ++n) {
-    q = xmalloc(sizeof *q);
-    q->prev = qlast;
-    q->track = vec[n];
-    q->id = vec[n];
-    *qq = q;
-    qq = &q->next;
-    qlast = q;
-  }
-  *qq = 0;
-  queuelike_completed(v, qh);
-}
-
-/** @brief Update the newly-added list */
-void added_update(void) {
-  struct callbackdata *cbd;
-  D(("added_updae"));
-
-  cbd = xmalloc(sizeof *cbd);
-  cbd->onerror = 0;
-  cbd->u.ql = &ql_added;
-  gtk_label_set_text(GTK_LABEL(report_label),
-                     "updating newly added track list");
-  disorder_eclient_new_tracks(client, new_completed, 0/*all*/, cbd);
-}
-
-/* Main menu plumbing ------------------------------------------------------ */
-
-static int queue_properties_sensitive(GtkWidget *w) {
-  return (!!queue_count_selected(g_object_get_data(G_OBJECT(w), "queue"))
-          && (disorder_eclient_state(client) & DISORDER_CONNECTED));
-}
-
-static int queue_selectall_sensitive(GtkWidget *w) {
-  return !!queue_count_entries(g_object_get_data(G_OBJECT(w), "queue"));
-}
-
-static int queue_selectnone_sensitive(GtkWidget *w) {
-  struct queuelike *const ql = g_object_get_data(G_OBJECT(w), "queue");
-
-  return hash_count(ql->selection) != 0;
-}
-
-static void queue_properties_activate(GtkWidget *w) {
-  queue_properties(g_object_get_data(G_OBJECT(w), "queue"));
-}
-
-static void queue_selectall_activate(GtkWidget *w) {
-  queue_select_all(g_object_get_data(G_OBJECT(w), "queue"));
-}
+GtkWidget *queue_widget(void) {
+  GtkWidget *const w = init_queuelike(&ql_queue);
 
-static void queue_selectnone_activate(GtkWidget *w) {
-  queue_select_none(g_object_get_data(G_OBJECT(w), "queue"));
+  /* Enable drag+drop */
+  gtk_tree_view_set_reorderable(GTK_TREE_VIEW(ql_queue.view), TRUE);
+  g_signal_connect(ql_queue.store,
+                   "row-inserted",
+                   G_CALLBACK(queue_row_inserted), &ql_queue);
+  g_signal_connect(ql_queue.store,
+                   "row-deleted",
+                   G_CALLBACK(queue_row_deleted), &ql_queue);
+  return w;
 }
 
-static const struct tabtype tabtype_queue = {
-  queue_properties_sensitive,
-  queue_selectall_sensitive,
-  queue_selectnone_sensitive,
-  queue_properties_activate,
-  queue_selectall_activate,
-  queue_selectnone_activate,
-};
-
-/* Other entry points ------------------------------------------------------ */
-
 /** @brief Return nonzero if @p track is in the queue */
 int queued(const char *track) {
   struct queue_entry *q;
 
   D(("queued %s", track));
+  /* Queue will contain resolved name */
+  track = namepart_resolve(track);
   for(q = ql_queue.q; q; q = q->next)
     if(!strcmp(q->track, track))
       return 1;
diff --git a/disobedience/recent.c b/disobedience/recent.c
new file mode 100644 (file)
index 0000000..788b824
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2006-2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "disobedience.h"
+#include "popup.h"
+#include "queue-generic.h"
+
+/** @brief Update the recently played list */
+static void recent_completed(void attribute((unused)) *v,
+                             const char *err,
+                             struct queue_entry *q) {
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
+  }
+  /* The recent list is backwards compared to what we wanted */
+  struct queue_entry *qr = 0, *qn;
+  while(q) {
+    qn = q->next;
+    /* Swap next/prev pointers */
+    q->next = q->prev;
+    q->prev = qn;
+    /* Remember last node for new head */
+    qr = q;
+    /* Next node */
+    q = qn;
+  }
+  /* Update the display */
+  ql_new_queue(&ql_recent, qr);
+  /* Tell anyone who cares */
+  event_raise("recent-list-changed", qr);
+}
+
+/** @brief Schedule an update to the recently played list
+ *
+ * Called whenever a track is added to it or removed from it.
+ */
+static void recent_changed(const char attribute((unused)) *event,
+                           void  attribute((unused)) *eventdata,
+                           void  attribute((unused)) *callbackdata) {
+  D(("recent_changed"));
+  gtk_label_set_text(GTK_LABEL(report_label), "updating recently played list");
+  disorder_eclient_recent(client, recent_completed, 0);
+}
+
+/** @brief Called at startup */
+static void recent_init(void) {
+  /* Whenever the recent list changes on the server, re-fetch it */
+  event_register("recent-changed", recent_changed, 0);
+}
+
+/** @brief Columns for the recently-played list */
+static const struct queue_column recent_columns[] = {
+  { "When",   column_when,     0,        COL_RIGHT },
+  { "Who",    column_who,      0,        0 },
+  { "Artist", column_namepart, "artist", COL_EXPAND|COL_ELLIPSIZE },
+  { "Album",  column_namepart, "album",  COL_EXPAND|COL_ELLIPSIZE },
+  { "Title",  column_namepart, "title",  COL_EXPAND|COL_ELLIPSIZE },
+  { "Length", column_length,   0,        COL_RIGHT }
+};
+
+/** @brief Pop-up menu for recently played list */
+static struct menuitem recent_menuitems[] = {
+  { "Track properties", ql_properties_activate, ql_properties_sensitive,0, 0 },
+  { "Play track", ql_play_activate, ql_play_sensitive, 0, 0 },
+  { "Select all tracks", ql_selectall_activate, ql_selectall_sensitive, 0, 0 },
+  { "Deselect all tracks", ql_selectnone_activate, ql_selectnone_sensitive, 0, 0 },
+};
+
+struct queuelike ql_recent = {
+  .name = "recent",
+  .init = recent_init,
+  .columns = recent_columns,
+  .ncolumns = sizeof recent_columns / sizeof *recent_columns,
+  .menuitems = recent_menuitems,
+  .nmenuitems = sizeof recent_menuitems / sizeof *recent_menuitems,
+};
+
+GtkWidget *recent_widget(void) {
+  return init_queuelike(&ql_recent);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index fe90433..a835ced 100644 (file)
  * When you select 'add' a new empty set of details are displayed to be edited.
  * Again Apply will commit them.
  *
- * TODO: it would be really nice if the Username entry could be removed and new
- * user names entered in the list, rather off in the details panel.  This may
- * be possible with a sufficiently clever GtkCellRenderer.
+ * TODO:
+ * - enter new username in the GtkTreeView
+ * - escape and enter keys should work
+ * - should have a cancel or close button, consistent with properties and login
  */
 
 #include "disobedience.h"
 #include "bits.h"
+#include "sendmail.h"
+
+static void users_details_sensitize_all(void);
+static void users_set_report(const char *msg);
 
 static GtkWidget *users_window;
 static GtkListStore *users_list;
@@ -53,6 +58,7 @@ static GtkWidget *users_details_email;
 static GtkWidget *users_details_password;
 static GtkWidget *users_details_password2;
 static GtkWidget *users_details_rights[32];
+static GtkWidget *users_reporter;
 static int users_details_row;
 static const char *users_selected;
 static const char *users_deferred_select;
@@ -109,10 +115,16 @@ static int users_find_user(const char *user,
  *
  * If users_deferred_select is set then that user is selected.
  */
-static void users_got_list(void attribute((unused)) *v, int nvec, char **vec) {
+static void users_got_list(void attribute((unused)) *v,
+                           const char *err,
+                           int nvec, char **vec) {
   int n;
   GtkTreeIter iter;
 
+  if(err) {
+    popup_protocol_error(0, err);
+    return;
+  }
   /* Present users in alphabetical order */
   qsort(vec, nvec, sizeof (char *), usercmp);
   /* Set the list contents */
@@ -158,6 +170,11 @@ static void users_detail_generic(const char *title,
                    1, 1);               /* x/ypadding */
 }
 
+static void users_entry_changed(GtkEditable attribute((unused)) *editable,
+                                gpointer attribute((unused)) user_data) {
+  users_details_sensitize_all();
+}
+
 /** @brief Add a row to the user details table
  * @param entryp Where to put GtkEntry
  * @param title Label for this row
@@ -172,6 +189,8 @@ static void users_add_detail(GtkWidget **entryp,
 
   if(!(entry = *entryp)) {
     *entryp = entry = gtk_entry_new();
+    g_signal_connect(entry, "changed",
+                     G_CALLBACK(users_entry_changed), 0);
     users_detail_generic(title, entry);
   }
   gtk_entry_set_visibility(GTK_ENTRY(entry),
@@ -213,6 +232,7 @@ static void users_details_sensitize(rights_type r) {
 /** @brief Set sensitivity of everything in sight */
 static void users_details_sensitize_all(void) {
   int n;
+  const char *report = 0;
 
   for(n = 0; n < 32; ++n)
     if(users_details_rights[n])
@@ -224,8 +244,42 @@ static void users_details_sensitize_all(void) {
   users_details_sensitize(RIGHT_MOVE_ANY);
   users_details_sensitize(RIGHT_REMOVE_ANY);
   users_details_sensitize(RIGHT_SCRATCH_ANY);
-  gtk_widget_set_sensitive(users_apply_button, users_mode != MODE_NONE);
+  int apply_sensitive = 1;
+  if(users_mode == MODE_NONE)
+    apply_sensitive = 0;
+  else {
+    const char *name = gtk_entry_get_text(GTK_ENTRY(users_details_name));
+    const char *email = gtk_entry_get_text(GTK_ENTRY(users_details_email));
+    const char *pw = gtk_entry_get_text(GTK_ENTRY(users_details_password));
+    const char *pw2 = gtk_entry_get_text(GTK_ENTRY(users_details_password2));
+    /* Username must be filled in */
+    if(!*name) {
+      apply_sensitive = 0;
+      if(!report)
+        report = "Must fill in username";
+    }
+    /* Passwords must be nontrivial and match */
+    if(!*pw) {
+      apply_sensitive = 0;
+      if(!report)
+        report = "Must fill in password";
+    }
+    if(strcmp(pw, pw2)) {
+      apply_sensitive = 0;
+      if(!report)
+        report = "Passwords must match";
+    }
+    /* Email address must be somewhat valid */
+    if(*email) {
+      if(!email_valid(email)) {
+        apply_sensitive = 0;
+        report = "Invalid email address";
+      }
+    }
+  }
+  gtk_widget_set_sensitive(users_apply_button, apply_sensitive);
   gtk_widget_set_sensitive(users_delete_button, !!users_selected);
+  users_set_report(report);
 }
 
 /** @brief Called when an _ALL widget is toggled
@@ -380,42 +434,38 @@ static rights_type users_get_rights(void) {
   return r;
 }
 
-/** @brief Called when various things fail */
-static void users_op_failed(struct callbackdata attribute((unused)) *cbd,
-                            int attribute((unused)) code,
-                            const char *msg) {
-  popup_submsg(users_window, GTK_MESSAGE_ERROR, msg);
+/** @brief Called when a user setting has been edited */
+static void users_edituser_completed(void attribute((unused)) *v,
+                                     const char *err) {
+  if(err)
+    popup_submsg(users_window, GTK_MESSAGE_ERROR, err);
 }
 
 /** @brief Called when a new user has been created */
-static void users_adduser_completed(void *v) {
-  struct callbackdata *cbd = v;
-
-  /* Now the user is created we can go ahead and set the email address */
-  if(*cbd->u.edituser.email) {
-    struct callbackdata *ncbd = xmalloc(sizeof *cbd);
-    ncbd->onerror = users_op_failed;
-    disorder_eclient_edituser(client, NULL, cbd->u.edituser.user,
-                              "email", cbd->u.edituser.email, ncbd);
+static void users_adduser_completed(void *v,
+                                    const char *err) {
+  if(err) {
+    popup_submsg(users_window, GTK_MESSAGE_ERROR, err);
+    mode(ADD);                          /* Let the user try again */
+  } else {
+    const struct kvp *const kvp = v;
+    const char *user = kvp_get(kvp, "user");
+    const char *email = kvp_get(kvp, "email"); /* maybe NULL */
+
+    /* Now the user is created we can go ahead and set the email address */
+    if(email)
+      disorder_eclient_edituser(client, users_edituser_completed, user,
+                                "email", email, NULL);
+    /* Refresh the list of users */
+    disorder_eclient_users(client, users_got_list, 0);
+    /* We'll select the newly created user */
+    users_deferred_select = user;
   }
-  /* Refresh the list of users */
-  disorder_eclient_users(client, users_got_list, 0);
-  /* We'll select the newly created user */
-  users_deferred_select = cbd->u.edituser.user;
-}
-
-/** @brief Called if creating a new user fails */
-static void users_adduser_failed(struct callbackdata attribute((unused)) *cbd,
-                                 int attribute((unused)) code,
-                                 const char *msg) {
-  popup_submsg(users_window, GTK_MESSAGE_ERROR, msg);
-  mode(ADD);                            /* Let the user try again */
 }
 
 /** @brief Called when the 'Apply' button is pressed */
 static void users_apply(GtkButton attribute((unused)) *button,
                         gpointer attribute((unused)) userdata) {
-  struct callbackdata *cbd;
   const char *password;
   const char *password2;
   const char *name;
@@ -447,15 +497,14 @@ static void users_apply(GtkButton attribute((unused)) *button,
       popup_submsg(users_window, GTK_MESSAGE_ERROR, "Invalid email address");
       return;
     }
-    cbd = xmalloc(sizeof *cbd);
-    cbd->onerror = users_adduser_failed;
-    cbd->u.edituser.user = name;
-    cbd->u.edituser.email = email;
-    disorder_eclient_adduser(client, users_adduser_completed,
+    disorder_eclient_adduser(client,
+                             users_adduser_completed,
                              name,
                              password,
                              rights_string(users_get_rights()),
-                             cbd);
+                             kvp_make("user", name,
+                                      "email", email,
+                                      (char *)0));
     /* We switch to no-op mode while creating the user */
     mode(NONE);
     break;
@@ -472,26 +521,30 @@ static void users_apply(GtkButton attribute((unused)) *button,
       popup_submsg(users_window, GTK_MESSAGE_ERROR, "Invalid email address");
       return;
     }
-    cbd = xmalloc(sizeof *cbd);
-    cbd->onerror = users_op_failed;
-    disorder_eclient_edituser(client, NULL, users_selected,
-                              "email", email, cbd);
-    disorder_eclient_edituser(client, NULL, users_selected,
-                              "password", password, cbd);
-    disorder_eclient_edituser(client, NULL, users_selected,
-                              "rights", rights_string(users_get_rights()), cbd);
+    disorder_eclient_edituser(client, users_edituser_completed, users_selected,
+                              "email", email, NULL);
+    disorder_eclient_edituser(client, users_edituser_completed, users_selected,
+                              "password", password, NULL);
+    disorder_eclient_edituser(client, users_edituser_completed, users_selected,
+                              "rights", rights_string(users_get_rights()), NULL);
     /* We remain in edit mode */
     break;
   }
 }
 
 /** @brief Called when a user has been deleted */
-static void users_deleted(void *v) {
-  const struct callbackdata *const cbd = v;
-  GtkTreeIter iter[1];
-
-  if(!users_find_user(cbd->u.user, iter))    /* Find the user... */
-    gtk_list_store_remove(users_list, iter); /* ...and remove them */
+static void users_delete_completed(void *v,
+                                   const char *err) {
+  if(err)
+    popup_submsg(users_window, GTK_MESSAGE_ERROR, err);
+  else {
+    const struct kvp *const kvp = v;
+    const char *const user = kvp_get(kvp, "user");
+    GtkTreeIter iter[1];
+    
+    if(!users_find_user(user, iter))           /* Find the user... */
+      gtk_list_store_remove(users_list, iter); /* ...and remove them */
+  }
 }
 
 /** @brief Called when the 'Delete' button is pressed */
@@ -499,7 +552,6 @@ static void users_delete(GtkButton attribute((unused)) *button,
                         gpointer attribute((unused)) userdata) {
   GtkWidget *yesno;
   int res;
-  struct callbackdata *cbd;
 
   if(!users_selected)
     return;
@@ -513,22 +565,35 @@ static void users_delete(GtkButton attribute((unused)) *button,
   res = gtk_dialog_run(GTK_DIALOG(yesno));
   gtk_widget_destroy(yesno);
   if(res == GTK_RESPONSE_YES) {
-    cbd = xmalloc(sizeof *cbd);
-    cbd->onerror = users_op_failed;
-    cbd->u.user = users_selected;
-    disorder_eclient_deluser(client, users_deleted, cbd->u.user, cbd);
+    disorder_eclient_deluser(client, users_delete_completed, users_selected,
+                             kvp_make("user", users_selected,
+                                      (char *)0));
   }
 }
 
-static void users_got_email(void attribute((unused)) *v, const char *value) {
+static void users_got_email(void attribute((unused)) *v,
+                            const char *err,
+                            const char *value) {
+  if(err)
+    popup_protocol_error(0, err);
   users_email = value;
 }
 
-static void users_got_rights(void attribute((unused)) *v, const char *value) {
+static void users_got_rights(void attribute((unused)) *v,
+                             const char *err,
+                             const char *value) {
+  if(err)
+    popup_protocol_error(0, err);
   users_rights = value;
 }
 
-static void users_got_password(void attribute((unused)) *v, const char *value) {
+static void users_got_password(void attribute((unused)) *v,
+                               const char *err,
+                               const char *value) {
+  if(err)
+    popup_protocol_error(0, err);
+  /* TODO if an error occurred gathering user info, we should react in some
+   * different way */
   users_password = value;
   users_makedetails(users_selected,
                     users_email,
@@ -575,6 +640,21 @@ static void users_selection_changed(GtkTreeSelection attribute((unused)) *treese
   mode(NONE);                           /* not editing *yet* */
 }
 
+static GtkWidget *users_make_reporter() {
+  if(!users_reporter) {
+    users_reporter = gtk_label_new("");
+    gtk_label_set_ellipsize(GTK_LABEL(users_reporter), PANGO_ELLIPSIZE_END);
+    gtk_misc_set_alignment(GTK_MISC(users_reporter), 0.99, 0);
+    g_signal_connect(users_reporter, "destroy",
+                     G_CALLBACK(gtk_widget_destroyed), &users_reporter);
+  }
+  return users_reporter;
+}
+
+static void users_set_report(const char *msg) {
+  gtk_label_set_text(GTK_LABEL(users_make_reporter()), msg ? msg : "");
+}
+
 /** @brief Table of buttons below the user list */
 static struct button users_buttons[] = {
   {
@@ -603,6 +683,9 @@ void manage_users(void) {
     gtk_window_present(GTK_WINDOW(users_window));
     return;
   }
+  /* Destroy old widgets */
+  if(users_reporter)
+    gtk_widget_destroy(users_reporter);
   /* Create the window */
   users_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_widget_set_style(users_window, tool_style);
@@ -656,6 +739,10 @@ void manage_users(void) {
   vbox2 = gtk_vbox_new(FALSE, 0);
   gtk_box_pack_start(GTK_BOX(vbox2), users_details_table,
                      TRUE/*expand*/, TRUE/*fill*/, 0);
+  gtk_box_pack_start(GTK_BOX(vbox2), gtk_hseparator_new(),
+                     FALSE/*expand*/, FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(vbox2), users_make_reporter(),
+                     FALSE/*expand*/, FALSE, 0);
   gtk_box_pack_start(GTK_BOX(vbox2), hbox2,
                      FALSE/*expand*/, FALSE, 0);
   
index 07d6fb3..a59c7d9 100644 (file)
@@ -103,8 +103,9 @@ The balance slider indicates the current balance and can be used to adjust it.
 means the only the right speaker.
 .SS "Queue Tab"
 This displays the currently playing track and the queue.
-The currently playing track is at the top and has a green background.
-Queued tracks appear below it and have alternating red and white backgrounds.
+The currently playing track is at the top, and can be distinguished by
+the constantly updating timer.
+Queued tracks appear below it.
 .PP
 The left button can be use to select and deselect tracks.
 On its own it just selects the pointed track and deselects everything else.
@@ -115,6 +116,9 @@ and deselects everything else.
 With both CTRL and SHIFT it selects everything from the last click to the
 current position without deselecting anything.
 .PP
+Tracks can be moved within the queue by dragging them to a new position with
+the left button.
+.PP
 The right button pops up a menu.
 This has the following options:
 .TP
@@ -137,7 +141,8 @@ Remove the selected tracks from the queue.
 .SS "Recent Tab"
 This displays recently played tracks, the most recent at the top.
 .PP
-The left button functions as above.
+The left button functions as above, except that drag-and-drop rearrangement
+is not possible.
 The right button pops up a menu with the following options:
 .TP
 .B Properties
@@ -161,7 +166,7 @@ If they are playing or in the queue then a notes icon appears next to them.
 Left clicking on a file will select it.
 As with the queue tab you can use SHIFT and CTRL to select multiple files.
 .PP
-The text box at the top is a search form.
+The text box at the bottom is a search form.
 If you enter search terms here then tracks containing all those words will be
 highlighted.
 You can also limit the results to tracks with particular tags, by including
@@ -362,10 +367,6 @@ setting up with Disobedience, tools such as
 .BR disorder (1)
 should work as well.
 .SH BUGS
-Disobedience is newly introduced with DisOrder 2.0.
-There are bound to be bugs.
-Please send feedback.
-.PP
 There is no particular provision for multiple users of the same computer
 sharing a single \fBdisorder\-playrtp\fR process.
 This shouldn't be too much of a problem in practice but something could
@@ -373,6 +374,11 @@ perhaps be done given demand.
 .PP
 Try to do remote user management when the server is configured to refuse this
 produces rather horrible error behavior.
+.PP
+Only one track can be dragged at a time.
+.PP
+Resizing columns doesn't work very well.
+This is a GTK+ bug.
 .SH FILES
 .TP
 .I ~/.disorder/HOSTNAME\-rtp
index f4c5b2f..f2efe25 100644 (file)
@@ -620,12 +620,27 @@ A track started playing.
 .B resume
 The current track was resumed.
 .TP
+.B rights_changed \fIRIGHTS\fR
+User's rights were changed.
+.TP
 .B scratched
 The current track was scratched.
 .PP
 To simplify client implementation, \fBstate\fR commands reflecting the current
 state are sent at the start of the log.
 .RE
+.TB
+.B user_add \fIUSERNAME\fR
+A user was created.
+.TP
+.B user_delete \fIUSERNAME\fR
+A user was deleted.
+.TP
+.B user_edit \fIUSERNAME\fR \fIPROPERTY\fR
+Some property of a user was edited.
+.TP
+.B user_confirm \fIUSERNAME\fR
+A user's login was confirmed (via the web interface).
 .TP
 .B volume \fILEFT\fR \fIRIGHT\fR
 The volume changed.
@@ -634,6 +649,9 @@ The volume changed.
 is as defined in
 .B "TRACK INFORMATION"
 above.
+.PP
+The \fBuser-*\fR messages are only sent to admin users, and are not sent over
+non-local connections unless \fBremote_userman\fR is enabled.
 .SH "CHARACTER ENCODING"
 All data sent by both server and client is encoded using UTF-8.
 Moreover it must be valid UTF-8, i.e. non-minimal sequences are not
index ce28475..f2943cc 100644 (file)
@@ -44,6 +44,8 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        dateparse.c dateparse.h xgetdate.c              \
        defs.c defs.h                                   \
        eclient.c eclient.h                             \
+       email.c                                         \
+       eventdist.c eventdist.h                         \
        event.c event.h                                 \
        eventlog.c eventlog.h                           \
        filepart.c filepart.h                           \
@@ -79,6 +81,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        timeval.h                                       \
        $(TRACKDB) trackdb.h trackdb-int.h              \
        trackname.c trackorder.c trackname.h            \
+       tracksort.c                                     \
        url.h url.c                                     \
        user.h user.c                                   \
        unicode.h unicode.c                             \
index a9805f7..48ae7e2 100644 (file)
@@ -180,6 +180,11 @@ static void logentry_scratched(disorder_eclient *c, int nvec, char **vec);
 static void logentry_state(disorder_eclient *c, int nvec, char **vec);
 static void logentry_volume(disorder_eclient *c, int nvec, char **vec);
 static void logentry_rescanned(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_add(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_confirm(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_delete(disorder_eclient *c, int nvec, char **vec);
+static void logentry_user_edit(disorder_eclient *c, int nvec, char **vec);
+static void logentry_rights_changed(disorder_eclient *c, int nvec, char **vec);
 
 /* Tables ********************************************************************/
 
@@ -205,8 +210,13 @@ static const struct logentry_handler logentry_handlers[] = {
   LE(recent_removed, 1, 1),
   LE(removed, 1, 2),
   LE(rescanned, 0, 0),
+  LE(rights_changed, 1, 1),
   LE(scratched, 2, 2),
   LE(state, 1, 1),
+  LE(user_add, 1, 1),
+  LE(user_confirm, 1, 1),
+  LE(user_delete, 1, 1),
+  LE(user_edit, 2, 2),
   LE(volume, 2, 2)
 };
 
@@ -519,6 +529,7 @@ static void maybe_connected(disorder_eclient *c) {
 
 /* Authentication ************************************************************/
 
+/** @brief Called with the greeting from the server */
 static void authbanner_opcallback(disorder_eclient *c,
                                   struct operation *op) {
   size_t nonce_len;
@@ -577,6 +588,7 @@ static void authbanner_opcallback(disorder_eclient *c,
                 (char *)0);
 }
 
+/** @brief Called with the response to the @c user command */
 static void authuser_opcallback(disorder_eclient *c,
                                 struct operation *op) {
   char *r;
@@ -832,50 +844,71 @@ static void stash_command(disorder_eclient *c,
 
 /* Command support ***********************************************************/
 
+static const char *errorstring(disorder_eclient *c) {
+  char *s;
+
+  byte_xasprintf(&s, "%s: %s: %d", c->ident, c->line, c->rc);
+  return s;
+}
+
 /* for commands with a quoted string response */ 
 static void string_response_opcallback(disorder_eclient *c,
                                        struct operation *op) {
+  disorder_eclient_string_response *completed
+    = (disorder_eclient_string_response *)op->completed;
+    
   D(("string_response_callback"));
   if(c->rc / 100 == 2 || c->rc == 555) {
     if(op->completed) {
       if(c->rc == 555)
-        ((disorder_eclient_string_response *)op->completed)(op->v, NULL);
+        completed(op->v, NULL, NULL);
       else if(c->protocol >= 2) {
         char **rr = split(c->line + 4, 0, SPLIT_QUOTES, 0, 0);
         
         if(rr && *rr)
-          ((disorder_eclient_string_response *)op->completed)(op->v, *rr);
+          completed(op->v, NULL, *rr);
         else
-          protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+          /* TODO error message a is bit lame but generally indicates a server
+           * bug rather than anything the user can address */
+          completed(op->v, "error parsing response", NULL);
       } else
-        ((disorder_eclient_string_response *)op->completed)(op->v,
-                                                            c->line + 4);
+        completed(op->v, NULL, c->line + 4);
     }
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), NULL);
 }
 
 /* for commands with a simple integer response */ 
 static void integer_response_opcallback(disorder_eclient *c,
                                         struct operation *op) {
+  disorder_eclient_integer_response *completed
+    = (disorder_eclient_integer_response *)op->completed;
+
   D(("string_response_callback"));
   if(c->rc / 100 == 2) {
-    if(op->completed)
-      ((disorder_eclient_integer_response *)op->completed)
-        (op->v, strtol(c->line + 4, 0, 10));
+    long n;
+    int e;
+
+    e = xstrtol(&n, c->line + 4, 0, 10);
+    if(e)
+      completed(op->v, strerror(e), 0);
+    else
+      completed(op->v, 0, n);
   } else
-    protocol_error(c, op,  c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0);
 }
 
 /* for commands with no response */
 static void no_response_opcallback(disorder_eclient *c,
                                    struct operation *op) {
+  disorder_eclient_no_response *completed
+    = (disorder_eclient_no_response *)op->completed;
+
   D(("no_response_callback"));
-  if(c->rc / 100 == 2) {
-    if(op->completed)
-      ((disorder_eclient_no_response *)op->completed)(op->v);
-  } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+  if(c->rc / 100 == 2)
+    completed(op->v, NULL);
+  else
+    completed(op->v, errorstring(c));
 }
 
 /* error callback for queue_unmarshall */
@@ -883,13 +916,17 @@ static void eclient_queue_error(const char *msg,
                                 void *u) {
   struct operation *op = u;
 
+  /* TODO don't use protocol_error here */
   protocol_error(op->client, op, -1, "error parsing queue entry: %s", msg);
 }
 
 /* for commands that expect a queue dump */
 static void queue_response_opcallback(disorder_eclient *c,
                                       struct operation *op) {
+  disorder_eclient_queue_response *const completed
+    = (disorder_eclient_queue_response *)op->completed;
   int n;
+  int parse_failed = 0;
   struct queue_entry *q, *qh = 0, **qtail = &qh, *qlast = 0;
   
   D(("queue_response_callback"));
@@ -898,22 +935,29 @@ static void queue_response_opcallback(disorder_eclient *c,
     for(n = 0; n < c->vec.nvec; ++n) {
       q = xmalloc(sizeof *q);
       D(("queue_unmarshall %s", c->vec.vec[n]));
-      if(!queue_unmarshall(q, c->vec.vec[n], eclient_queue_error, op)) {
+      if(!queue_unmarshall(q, c->vec.vec[n], NULL, op)) {
         q->prev = qlast;
         *qtail = q;
         qtail = &q->next;
         qlast = q;
-      }
+      } else
+        parse_failed = 1;
     }
-    if(op->completed)
-      ((disorder_eclient_queue_response *)op->completed)(op->v, qh);
+    /* Currently we pass the partial queue to the callback along with the
+     * error.  This might not be very useful in practice... */
+    if(parse_failed)
+      completed(op->v, "cannot parse result", qh);
+    else
+      completed(op->v, 0, qh);
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0);
 } 
 
 /* for 'playing' */
 static void playing_response_opcallback(disorder_eclient *c,
                                         struct operation *op) {
+  disorder_eclient_queue_response *const completed
+    = (disorder_eclient_queue_response *)op->completed;
   struct queue_entry *q;
 
   D(("playing_response_callback"));
@@ -921,51 +965,52 @@ static void playing_response_opcallback(disorder_eclient *c,
     switch(c->rc % 10) {
     case 2:
       if(queue_unmarshall(q = xmalloc(sizeof *q), c->line + 4,
-                          eclient_queue_error, c))
-        return;
+                          NULL, c))
+        completed(op->v, "cannot parse result", 0);
+      else
+        completed(op->v, 0, q);
       break;
     case 9:
-      q = 0;
+      completed(op->v, 0, 0);
       break;
     default:
-      protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
-      return;
+      completed(op->v, errorstring(c), 0);
+      break;
     }
-    if(op->completed)
-      ((disorder_eclient_queue_response *)op->completed)(op->v, q);
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0);
 }
 
 /* for commands that expect a list of some sort */
 static void list_response_opcallback(disorder_eclient *c,
                                      struct operation *op) {
+  disorder_eclient_list_response *const completed =
+    (disorder_eclient_list_response *)op->completed;
+
   D(("list_response_callback"));
-  if(c->rc / 100 == 2) {
-    if(op->completed)
-      ((disorder_eclient_list_response *)op->completed)(op->v,
-                                                        c->vec.nvec,
-                                                        c->vec.vec);
-  } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+  if(c->rc / 100 == 2)
+    completed(op->v, NULL, c->vec.nvec, c->vec.vec);
+  else
+    completed(op->v, errorstring(c), 0, 0);
 }
 
 /* for volume */
 static void volume_response_opcallback(disorder_eclient *c,
                                        struct operation *op) {
+  disorder_eclient_volume_response *completed
+    = (disorder_eclient_volume_response *)op->completed;
   int l, r;
 
   D(("volume_response_callback"));
   if(c->rc / 100 == 2) {
     if(op->completed) {
       if(sscanf(c->line + 4, "%d %d", &l, &r) != 2 || l < 0 || r < 0)
-        protocol_error(c, op, -1, "%s: invalid volume response: %s",
-                       c->ident, c->line);
+        completed(op->v, "cannot parse volume response", 0, 0);
       else
-        ((disorder_eclient_volume_response *)op->completed)(op->v, l, r);
+        completed(op->v, 0, l, r);
     }
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0, 0);
 }
 
 static int simple(disorder_eclient *c,
@@ -1233,16 +1278,19 @@ int disorder_eclient_new_tracks(disorder_eclient *c,
 
 static void rtp_response_opcallback(disorder_eclient *c,
                                     struct operation *op) {
+  disorder_eclient_list_response *const completed =
+    (disorder_eclient_list_response *)op->completed;
   D(("rtp_response_opcallback"));
   if(c->rc / 100 == 2) {
-    if(op->completed) {
-      int nvec;
-      char **vec = split(c->line + 4, &nvec, SPLIT_QUOTES, 0, 0);
+    int nvec;
+    char **vec = split(c->line + 4, &nvec, SPLIT_QUOTES, 0, 0);
 
-      ((disorder_eclient_list_response *)op->completed)(op->v, nvec, vec);
-    }
+    if(vec)
+      completed(op->v, NULL, nvec, vec);
+    else
+      completed(op->v, "error parsing response", 0, 0);
   } else
-    protocol_error(c, op, c->rc, "%s: %s", c->ident, c->line);
+    completed(op->v, errorstring(c), 0, 0);
 }
 
 /** @brief Determine the RTP target address
@@ -1380,6 +1428,7 @@ static void log_opcallback(disorder_eclient *c,
 /* error callback for log line parsing */
 static void logline_error(const char *msg, void *u) {
   disorder_eclient *c = u;
+  /* TODO don't use protocol_error here */
   protocol_error(c, c->ops, -1, "error parsing log line: %s", msg);
 }
 
@@ -1395,16 +1444,23 @@ static void logline(disorder_eclient *c, const char *line) {
                                          * reported */
   if(sscanf(vec[0], "%"SCNxMAX, &when) != 1) {
     /* probably the wrong side of a format change */
+    /* TODO don't use protocol_error here */
     protocol_error(c, c->ops, -1, "invalid log timestamp '%s'", vec[0]);
     return;
   }
   /* TODO: do something with the time */
+  //fprintf(stderr, "log key: %s\n", vec[1]);
   n = TABLE_FIND(logentry_handlers, name, vec[1]);
-  if(n < 0) return;                     /* probably a future command */
+  if(n < 0) {
+    //fprintf(stderr, "...not found\n");
+    return;                     /* probably a future command */
+  }
   vec += 2;
   nvec -= 2;
-  if(nvec < logentry_handlers[n].min || nvec > logentry_handlers[n].max)
+  if(nvec < logentry_handlers[n].min || nvec > logentry_handlers[n].max) {
+    //fprintf(stderr, "...wrong # args\n");
     return;
+  }
   logentry_handlers[n].handler(c, nvec, vec);
 }
 
@@ -1491,6 +1547,39 @@ static void logentry_scratched(disorder_eclient *c,
     c->log_callbacks->state(c->log_v, c->statebits | DISORDER_CONNECTED);
 }
 
+static void logentry_user_add(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_add)
+    c->log_callbacks->user_add(c->log_v, vec[0]);
+}
+
+static void logentry_user_confirm(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_confirm)
+    c->log_callbacks->user_confirm(c->log_v, vec[0]);
+}
+
+static void logentry_user_delete(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_delete)
+    c->log_callbacks->user_delete(c->log_v, vec[0]);
+}
+
+static void logentry_user_edit(disorder_eclient *c,
+                              int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->user_edit)
+    c->log_callbacks->user_edit(c->log_v, vec[0], vec[1]);
+}
+
+static void logentry_rights_changed(disorder_eclient *c,
+                                    int attribute((unused)) nvec, char **vec) {
+  if(c->log_callbacks->rights_changed) {
+    rights_type r;
+    if(!parse_rights(vec[0], &r, 0/*report*/))
+      c->log_callbacks->rights_changed(c->log_v, r);
+  }
+}
+
 static const struct {
   unsigned long bit;
   const char *enable;
index 157ad59..efb6b2d 100644 (file)
@@ -24,6 +24,8 @@
 #ifndef ECLIENT_H
 #define ECLIENT_H
 
+#include "rights.h"
+
 /* Asynchronous client interface */
 
 /** @brief Handle type */
@@ -42,9 +44,13 @@ struct queue_entry;
  * These must all be valid.
  */
 typedef struct disorder_eclient_callbacks {
-  /** @brief Called when a communication error (e.g. connected refused) occurs.
+  /** @brief Called when a communication error occurs.
    * @param u from disorder_eclient_new()
    * @param msg error message
+   *
+   * This might be called at any time, and indicates a low-level error,
+   * e.g. connection refused by the server.  It does not mean that any requests
+   * made of the owning eclient will not be fulfilled at some point.
    */
   void (*comms_error)(void *u, const char *msg);
   
@@ -52,6 +58,11 @@ typedef struct disorder_eclient_callbacks {
    * @param u from disorder_eclient_new()
    * @param v from failed command, or NULL if during setup
    * @param msg error message
+   *
+   * This call is obsolete at least in its current form, in which it is used to
+   * report most errors from most requests.  Ultimately requests-specific
+   * errors will be reported in a request-specific way rather than via this
+   * generic callback.
    */
   void (*protocol_error)(void *u, void *v, int code, const char *msg);
 
@@ -84,19 +95,78 @@ typedef struct disorder_eclient_callbacks {
 typedef struct disorder_eclient_log_callbacks {
   /** @brief Called on (re-)connection */
   void (*connected)(void *v);
-  
+
+  /** @brief Called when @p track finished playing successfully */
   void (*completed)(void *v, const char *track);
+
+  /** @brief Called when @p track fails for some reason */
   void (*failed)(void *v, const char *track, const char *status);
+
+  /** @brief Called when @p user moves some track or tracks in the queue
+   *
+   * Fetch the queue again to find out what the new order is - the
+   * rearrangement could in principle be arbitrarily complicated.
+   */
   void (*moved)(void *v, const char *user);
+
+  /** @brief Called when @p track starts playing
+   *
+   * @p user might be 0.
+   */
   void (*playing)(void *v, const char *track, const char *user/*maybe 0*/);
+
+  /** @brief Called when @p q is added to the queue
+   *
+   * Fetch the queue again to find out where the in the queue it was added.
+   */   
   void (*queue)(void *v, struct queue_entry *q);
+
+  /** @brief Called when @p q is added to the recent list */
   void (*recent_added)(void *v, struct queue_entry *q);
+
+  /** @brief Called when @p id is removed from the recent list */
   void (*recent_removed)(void *v, const char *id);
+
+  /** @brief Called when @id is removed from the queue
+   *
+   * @p user might be 0.
+   */
   void (*removed)(void *v, const char *id, const char *user/*maybe 0*/);
+
+  /** @brief Called when @p track is scratched */
   void (*scratched)(void *v, const char *track, const char *user);
+
+  /** @brief Called with the current state whenever it changes
+   *
+   * State bits are:
+   * - @ref DISORDER_PLAYING_ENABLED
+   * - @ref DISORDER_RANDOM_ENABLED
+   * - @ref DISORDER_TRACK_PAUSED
+   * - @ref DISORDER_PLAYING
+   * - @ref DISORDER_CONNECTED
+   */
   void (*state)(void *v, unsigned long state);
+
+  /** @brief Called when the volume changes */
   void (*volume)(void *v, int left, int right);
+
+  /** @brief Called when a rescan completes */
   void (*rescanned)(void *v);
+
+  /** @brief Called when a user is created (admins only) */
+  void (*user_add)(void *v, const char *user);
+
+  /** @brief Called when a user is confirmed (admins only) */
+  void (*user_confirm)(void *v, const char *user);
+
+  /** @brief Called when a user is deleted (admins only) */
+  void (*user_delete)(void *v, const char *user);
+
+  /** @brief Called when a user is edited (admins only) */
+  void (*user_edit)(void *v, const char *user, const char *property);
+
+  /** @brief Called when your rights change */
+  void (*rights_changed)(void *v, rights_type new_rights);
 } disorder_eclient_log_callbacks;
 
 /* State bits */
@@ -135,32 +205,89 @@ struct kvp;
 struct sink;
 
 /* Completion callbacks.  These provide the result of operations to the caller.
- * It is always allowed for these to be null pointers if you don't care about
- * the result. */
+ * Unlike in earlier releases, these are not allowed to be NULL. */
 
-typedef void disorder_eclient_no_response(void *v);
-/* completion callback with no data */
+/** @brief Trivial completion callback
+ * @param v User data
+ * @param err Error string or NULL on succes
+ */
+typedef void disorder_eclient_no_response(void *v,
+                                          const char *err);
 
 /** @brief String result completion callback
  * @param v User data
- * @param value or NULL
+ * @param err Error string or NULL on succes
+ * @param value Result or NULL
+ *
+ * @p error will be NULL on success.  In this case @p value will be the result
+ * (which might be NULL for disorder_eclient_get(),
+ * disorder_eclient_get_global() and disorder_eclient_userinfo()).
  *
- * @p value can be NULL for disorder_eclient_get(),
- * disorder_eclient_get_global() and disorder_eclient_userinfo().
+ * @p error will be non-NULL on failure.  In this case @p value is always NULL.
  */
-typedef void disorder_eclient_string_response(void *v, const char *value);
+typedef void disorder_eclient_string_response(void *v,
+                                              const char *err,
+                                              const char *value);
 
-typedef void disorder_eclient_integer_response(void *v, long value);
-/* completion callback with a integer result */
-
-typedef void disorder_eclient_volume_response(void *v, int l, int r);
-/* completion callback with a pair of integer results */
+/** @brief String result completion callback
+ * @param v User data
+ * @param err Error string or NULL on succes
+ * @param value Result or 0
+ *
+ * @p error will be NULL on success.  In this case @p value will be the result.
+ *
+ * @p error will be non-NULL on failure.  In this case @p value is always 0.
+ */
+typedef void disorder_eclient_integer_response(void *v,
+                                               const char *err,
+                                               long value);
+/** @brief Volume completion callback
+ * @param v User data
+ * @param err Error string or NULL on success
+ * @param l Left channel volume
+ * @param r Right channel volume
+ *
+ * @p error will be NULL on success.  In this case @p l and @p r will be the
+ * result.
+ *
+ * @p error will be non-NULL on failure.  In this case @p l and @p r are always
+ * 0.
+ */
+typedef void disorder_eclient_volume_response(void *v,
+                                              const char *err,
+                                              int l, int r);
 
-typedef void disorder_eclient_queue_response(void *v, struct queue_entry *q);
-/* completion callback for queue/recent listing */
+/** @brief Queue request completion callback
+ * @param v User data
+ * @param err Error string or NULL on success
+ * @param q Head of queue data list
+ *
+ * @p error will be NULL on success.  In this case @p q will be the (head of
+ * the) result.
+ *
+ * @p error will be non-NULL on failure.  In this case @p q may be NULL but
+ * MIGHT also be some subset of the queue.  For consistent behavior it should
+ * be ignored in the error case.
+ */
+typedef void disorder_eclient_queue_response(void *v,
+                                             const char *err,
+                                             struct queue_entry *q);
 
-typedef void disorder_eclient_list_response(void *v, int nvec, char **vec);
-/* completion callback for file listing etc */
+/** @brief List request completion callback
+ * @param v User data
+ * @param err Error string or NULL on success
+ * @param nvec Number of elements in response list
+ * @param vec Pointer to response list
+ *
+ * @p error will be NULL on success.  In this case @p nvec and @p vec will give
+ * the result.
+ *
+ * @p error will be non-NULL on failure.  In this case @p nvec and @p vec will
+ * be 0 and NULL.
+ */
+typedef void disorder_eclient_list_response(void *v,
+                                            const char *err,
+                                            int nvec, char **vec);
 
 disorder_eclient *disorder_eclient_new(const disorder_eclient_callbacks *cb,
                                        void *u);
diff --git a/lib/email.c b/lib/email.c
new file mode 100644 (file)
index 0000000..fb2f744
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file lib/email.c
+ * @brief Email addresses
+ */
+
+#include "common.h"
+
+#include "sendmail.h"
+
+/** @brief Test email address validity
+ * @param address to verify
+ * @return 1 if it might be valid, 0 if it is definitely not
+ *
+ * This function doesn't promise to tell you whether an address is deliverable,
+ * it just does basic syntax checks.
+ */
+int email_valid(const char *address) {
+  /* There must be an '@' sign */
+  const char *at = strchr(address, '@');
+  if(!at)
+    return 0;
+  /* There must be only one of them */
+  if(strchr(at + 1, '@'))
+    return 0;
+  /* It mustn't be the first or last character */
+  if(at == address || !at[1])
+    return 0;
+  /* Local part must be valid */
+  /* TODO */
+  /* Domain part must be valid */
+  /* TODO */
+  return 1;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/eventdist.c b/lib/eventdist.c
new file mode 100644 (file)
index 0000000..b2d27d9
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#include "common.h"
+
+#include "mem.h"
+#include "eventdist.h"
+#include "hash.h"
+
+struct event_data {
+  struct event_data *next;
+  const char *event;
+  event_handler *callback;
+  void *callbackdata;
+};
+
+static hash *events;
+
+/** @brief Register an event handler
+ * @param event Event type to handle
+ * @param callback Function to call when event occurs
+ * @param callbackdata Passed to @p callback
+ * @return Handle for this registration (for use with event_cancel())
+ */
+event_handle event_register(const char *event,
+                            event_handler *callback,
+                            void *callbackdata) {
+  static const struct event_data *null;
+  struct event_data *ed = xmalloc(sizeof *ed), **head;
+
+  if(!events)
+    events = hash_new(sizeof (struct event_data *));
+  if(!(head = hash_find(events, event))) {
+    hash_add(events, event, &null, HASH_INSERT);
+    head = hash_find(events, event);
+  }
+  ed->next = *head;
+  ed->event = xstrdup(event);
+  ed->callback = callback;
+  ed->callbackdata = callbackdata;
+  *head = ed;
+  return ed;
+}
+
+/** @brief Stop handling an event
+ * @param handle Registration to cancel (as returned from event_register())
+ *
+ * @p handle is allowed to be NULL.
+ */
+void event_cancel(event_handle handle) {
+  struct event_data **head, **edp;
+  
+  if(!handle)
+    return;
+  assert(events);
+  head = hash_find(events, handle->event);
+  for(edp = head; *edp && *edp != handle; edp = &(*edp)->next)
+    ;
+  assert(*edp == handle);
+  *edp = handle->next;
+}
+
+/** @brief Raise an event
+ * @param event Event type to raise
+ * @param eventdata Event-specific data
+ */
+void event_raise(const char *event,
+                 void *eventdata) {
+  struct event_data *ed, **head;
+  if(!events)
+    return;
+  if(!(head = hash_find(events, event)))
+    return;
+  for(ed = *head; ed; ed = ed->next)
+    ed->callback(event, eventdata, ed->callbackdata);
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/eventdist.h b/lib/eventdist.h
new file mode 100644 (file)
index 0000000..a25929c
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+#ifndef EVENTDIST_H
+#define EVENTDIST_H
+
+/** @brief Signature for event handlers
+ * @param event Event type
+ * @param eventdata Event-specific data
+ * @param callbackdata Handler-specific data (as passed to event_register())
+ */
+typedef void event_handler(const char *event,
+                           void *eventdata,
+                           void *callbackdata);
+
+/** @brief Handle identifying an event monitor */
+typedef struct event_data *event_handle;
+
+event_handle event_register(const char *event,
+                            event_handler *callback,
+                            void *callbackdata);
+void event_cancel(event_handle handle);
+void event_raise(const char *event,
+                 void *eventdata);
+
+#endif /* EVENTDIST_H */
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 9cd0554..c56249d 100644 (file)
--- a/lib/kvp.c
+++ b/lib/kvp.c
@@ -201,6 +201,25 @@ const char *kvp_get(const struct kvp *kvp, const char *name) {
   return kvp ? kvp->value : 0;
 }
 
+struct kvp *kvp_make(const char *name, ...) {
+  const char *value;
+  struct kvp *kvp = 0, *k;
+  va_list ap;
+
+  va_start(ap, name);
+  while(name) {
+    value = va_arg(ap, const char *);
+    k = xmalloc(sizeof *k);
+    k->name = name;
+    k->value = value ? xstrdup(value) : value;
+    k->next = kvp;
+    kvp = k;
+    name = va_arg(ap, const char *);
+  }
+  va_end(ap);
+  return kvp;
+}
+
 /*
 Local Variables:
 c-basic-offset:2
index 0daa9d2..dcdca5f 100644 (file)
--- a/lib/kvp.h
+++ b/lib/kvp.h
@@ -56,6 +56,7 @@ char *urlencodestring(const char *s);
 /* return the url-encoded form of @s@ */
 
 char *urldecodestring(const char *s, size_t ns);
+struct kvp *kvp_make(const char *key, ...);
 
 #endif /* KVP_H */
 
index e8d87a0..958cd79 100644 (file)
@@ -35,6 +35,7 @@ pid_t sendmail_subprocess(const char *sender,
                           const char *encoding,
                           const char *content_type,
                           const char *body);
+int email_valid(const char *address);
 
 #endif /* SENDMAIL_H */
 
index 4009358..718970a 100644 (file)
@@ -60,6 +60,7 @@
 #include "unicode.h"
 #include "unidata.h"
 #include "base64.h"
+#include "sendmail.h"
 
 #define RESCAN "disorder-rescan"
 #define DEADLOCK "disorder-deadlock"
@@ -508,8 +509,13 @@ void trackdb_close(void) {
 
 /* generic db routines *******************************************************/
 
-/* fetch and decode a database entry.  Returns 0, DB_NOTFOUND or
- * DB_LOCK_DEADLOCK. */
+/** @brief Fetch and decode a database entry
+ * @param db Database
+ * @param track Track name
+ * @param kp Where to put decoded list (or NULL if you don't care)
+ * @param tid Owning transaction
+ * @return 0, @c DB_NOTFOUND or @c DB_LOCK_DEADLOCK
+ */
 int trackdb_getdata(DB *db,
                     const char *track,
                     struct kvp **kp,
@@ -520,10 +526,12 @@ int trackdb_getdata(DB *db,
   switch(err = db->get(db, tid, make_key(&key, track),
                        prepare_data(&data), 0)) {
   case 0:
-    *kp = kvp_urldecode(data.data, data.size);
+    if(kp)
+      *kp = kvp_urldecode(data.data, data.size);
     return 0;
   case DB_NOTFOUND:
-    *kp = 0;
+    if(kp)
+      *kp = 0;
     return err;
   case DB_LOCK_DEADLOCK:
     error(0, "error querying database: %s", db_strerror(err));
@@ -1852,7 +1860,7 @@ static int do_list(struct vector *v, const char *dir,
   char *ptr;
   int err;
   size_t l, last_dir_len = 0;
-  char *last_dir = 0, *track, *alias;
+  char *last_dir = 0, *track;
   struct kvp *p;
 
   dl = strlen(dir);
@@ -1885,12 +1893,35 @@ static int do_list(struct vector *v, const char *dir,
         if((err = trackdb_getdata(trackdb_prefsdb,
                                   track, &p, tid)) == DB_LOCK_DEADLOCK)
           goto deadlocked;
+        /* There's an awkward question here...
+         *
+         * If a track shares a directory with its alias then we could
+         * do one of three things:
+         * - report both.  Looks ridiculuous in most UIs.
+         * - report just the alias.  Remarkably inconvenient to write
+         *   UI code for!
+         * - report just the real name.  Ugly if the UI doesn't prettify
+         *   names via the name parts.
+         */
+#if 1
+        /* If this file is an alias for a track in the same directory then we
+         * skip it */
+        struct kvp *t = kvp_urldecode(d.data, d.size);
+        const char *alias_target = kvp_get(t, "_alias_for");
+        if(!(alias_target
+             && !strcmp(d_dirname(alias_target),
+                        d_dirname(track))))
+         if(track_matches(dl, k.data, k.size, re))
+           vector_append(v, track);
+#else
        /* if this file has an alias in the same directory then we skip it */
+           char *alias;
         if((err = compute_alias(&alias, track, p, tid)))
           goto deadlocked;
         if(!(alias && !strcmp(d_dirname(alias), d_dirname(track))))
          if(track_matches(dl, k.data, k.size, re))
            vector_append(v, track);
+#endif
       }
     }
     err = cursor->c_get(cursor, &k, &d, DB_NEXT);
@@ -2391,12 +2422,24 @@ static char **trackdb_new_tid(int *ntracksp,
   DBT k, d;
   int err = 0;
   struct vector tracks[1];
+  hash *h = hash_new(1);
 
   vector_init(tracks);
   c = trackdb_opencursor(trackdb_noticeddb, tid);
   while((maxtracks <= 0 || tracks->nvec < maxtracks)
-        && !(err = c->c_get(c, prepare_data(&k), prepare_data(&d), DB_PREV)))
-    vector_append(tracks, xstrndup(d.data, d.size));
+        && !(err = c->c_get(c, prepare_data(&k), prepare_data(&d), DB_PREV))) {
+    char *const track = xstrndup(d.data, d.size);
+    /* Don't add any track more than once */
+    if(hash_add(h, track, "", HASH_INSERT))
+      continue;
+    /* See if the track still exists */
+    err = trackdb_getdata(trackdb_tracksdb, track, NULL/*kp*/, tid);
+    if(err == DB_NOTFOUND)
+      continue;                         /* It doesn't, skip it */
+    if(err == DB_LOCK_DEADLOCK)
+      break;                            /* Doh */
+    vector_append(tracks, track);
+  }
   switch(err) {
   case 0:                               /* hit maxtracks */
   case DB_NOTFOUND:                     /* ran out of tracks */
@@ -2678,6 +2721,7 @@ int trackdb_adduser(const char *user,
            user, rights, email);
     else
       info("created user '%s' with rights '%s'", user, rights);
+    eventlog("user_add", user, (char *)0);
     return 0;
   }
 }
@@ -2695,6 +2739,7 @@ int trackdb_deluser(const char *user) {
     return -1;
   }
   info("deleted user '%s'", user);
+  eventlog("user_delete", user, (char *)0);
   return 0;
 }
 
@@ -2756,8 +2801,8 @@ int trackdb_edituserinfo(const char *user,
     }
   } else if(!strcmp(key, "email")) {
     if(*value) {
-      if(!strchr(value, '@')) {
-        error(0, "invalid email address '%s' for user '%s'", user, value);
+      if(!email_valid(value)) {
+        error(0, "invalid email address '%s' for user '%s'", value, user);
         return -1;
       }
     } else
@@ -2774,8 +2819,10 @@ int trackdb_edituserinfo(const char *user,
   if(e) {
     error(0, "unknown user '%s'", user);
     return -1;
-  } else
+  } else {
+    eventlog("user_edit", user, key, (char *)0);
     return 0;
+  }
 }
 
 /** @brief List all users
@@ -2841,6 +2888,7 @@ int trackdb_confirm(const char *user, const char *confirmation,
   switch(e) {
   case 0:
     info("registration confirmed for user '%s'", user);
+    eventlog("user_confirm", user, (char *)0);
     return 0;
   case DB_NOTFOUND:
     error(0, "confirmation for nonexistent user '%s'", user);
index 129e1b5..3aa7122 100644 (file)
@@ -57,6 +57,20 @@ static inline int compare_path(const char *ap, const char *bp) {
                          (const unsigned char *)bp, strlen(bp));
 }
 
+/** @brief Entry in a list of tracks or directories */
+struct tracksort_data {
+  /** @brief Track name */
+  const char *track;
+  /** @brief Sort key */
+  const char *sort;
+  /** @brief Display key */
+  const char *display;
+};
+
+struct tracksort_data *tracksort_init(int nvec,
+                                      char **vec,
+                                      const char *type);
+
 #endif /* TRACKNAME_H */
 
 /*
diff --git a/lib/tracksort.c b/lib/tracksort.c
new file mode 100644 (file)
index 0000000..ded4c84
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "common.h"
+
+#include "trackname.h"
+#include "mem.h"
+
+/** @brief Compare two @ref entry objects */
+static int tracksort_compare(const void *a, const void *b) {
+  const struct tracksort_data *ea = a, *eb = b;
+
+  return compare_tracks(ea->sort, eb->sort,
+                       ea->display, eb->display,
+                       ea->track, eb->track);
+}
+
+struct tracksort_data *tracksort_init(int ntracks,
+                                      char **tracks,
+                                      const char *type) {
+  struct tracksort_data *td = xcalloc(ntracks, sizeof *td);
+  for(int n = 0; n < ntracks; ++n) {
+    td[n].track = tracks[n];
+    td[n].sort = trackname_transform(type, tracks[n], "sort");
+    td[n].display = trackname_transform(type, tracks[n], "display");
+  }
+  qsort(td, ntracks, sizeof *td, tracksort_compare);
+  return td;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 5c7cc54..c569287 100644 (file)
@@ -22,7 +22,7 @@ TESTS=t-addr t-arcfour t-basen t-bits t-cache t-casefold t-charset    \
        t-cookies t-dateparse t-event t-filepart t-hash t-heap t-hex    \
        t-kvp t-mime t-printf t-regsub t-selection t-signame t-sink     \
        t-split t-syscalls t-trackname t-unicode t-url t-utf8 t-vector  \
-       t-words t-wstat t-macros t-cgi
+       t-words t-wstat t-macros t-cgi t-eventdist
 
 noinst_PROGRAMS=$(TESTS)
 
@@ -61,6 +61,7 @@ t_utf8_SOURCES=t-utf8.c test.c test.h
 t_vector_SOURCES=t-vector.c test.c test.h
 t_words_SOURCES=t-words.c test.c test.h
 t_wstat_SOURCES=t-wstat.c test.c test.h
+t_eventdist_SOURCES=t-eventdist.c test.c test.h
 
 check-report: before-check check make-coverage-reports
 before-check:
diff --git a/libtests/t-eventdist.c b/libtests/t-eventdist.c
new file mode 100644 (file)
index 0000000..1deeb0d
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * 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; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "test.h"
+#include "eventdist.h"
+
+static int wibbles, wobbles, wobble2s;
+
+static void on_wibble(const char *event, void *eventdata, void *callbackdata) {
+  check_string(event, "wibble");
+  check_string(eventdata, "wibble_eventdata");
+  check_string(callbackdata, "wibble_data");
+  ++wibbles;
+}
+
+static void on_wobble(const char *event, void *eventdata, void *callbackdata) {
+  check_string(event, "wobble");
+  check_string(eventdata, "wobble_eventdata");
+  check_string(callbackdata, "wobble_data");
+  ++wobbles;
+}
+
+static void on_wobble2(const char *event, void *eventdata, void *callbackdata) {
+  check_string(event, "wobble");
+  check_string(eventdata, "wobble_eventdata");
+  check_string(callbackdata, "wobble2_data");
+  ++wobble2s;
+}
+
+static void test_eventdist(void) {
+  event_handle wibble_handle, wobble_handle, wobble2_handle;
+  /* Raising unregistered events should be safe */
+  event_raise("wibble", NULL);
+  ++tests;
+  
+  wibble_handle = event_register("wibble", on_wibble, (void *)"wibble_data");
+  wobble_handle = event_register("wobble", on_wobble, (void *)"wobble_data");
+  wobble2_handle = event_register("wobble", on_wobble2, (void *)"wobble2_data");
+  event_raise("wibble", (void *) "wibble_eventdata");
+  check_integer(wibbles, 1);
+  check_integer(wobbles, 0);
+  check_integer(wobble2s, 0);
+
+  event_raise("wobble", (void *)"wobble_eventdata");
+  check_integer(wibbles, 1);
+  check_integer(wobbles, 1);
+  check_integer(wobble2s, 1);
+
+  event_raise("wobble", (void *)"wobble_eventdata");
+  check_integer(wibbles, 1);
+  check_integer(wobbles, 2);
+  check_integer(wobble2s, 2);
+
+  event_cancel(wobble_handle);
+  
+  event_raise("wibble", (void *)"wibble_eventdata");
+  check_integer(wibbles, 2);
+  check_integer(wobbles, 2);
+  check_integer(wobble2s, 2);
+  
+  event_raise("wobble", (void *)"wobble_eventdata");
+  check_integer(wibbles, 2);
+  check_integer(wobbles, 2);
+  check_integer(wobble2s, 3);
+}
+
+TEST(eventdist);
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
index 48f87c2..68d9a4a 100755 (executable)
@@ -151,23 +151,31 @@ if [ -z "$roots" ]; then
     echo "(enter one or more directories separated by spaces)"
     read -r roots
     ok=true
+    anyroots=false
     for root in $roots; do
       if [ ! -d $root ]; then
        echo "'$root' does not exist"
        ok=false
+      else
+        anyroots=true
       fi
     done
-    if $ok; then
+    if $anyroots && $ok; then
       break
     fi
   done
 fi
 
 if [ -z "$encoding" ]; then
-  echo 
-  echo "What filesystem encoding should I assume for track names?"
-  echo "(e.g. UTF-8, ISO-8859-1, ...)"
-  read -r encoding
+  while :; do
+    echo 
+    echo "What filesystem encoding should I assume for track names?"
+    echo "(e.g. UTF-8, ISO-8859-1, ...)"
+    read -r encoding
+    if [ ! -z "$encoding" ]; then
+      break
+    fi
+  done
 fi
 
 if [ -z "$port" ]; then
@@ -180,7 +188,7 @@ if [ -z "$port" ]; then
     none )
       break
       ;;
-    [^0-9] )
+    [^0-9] | "" )
       echo "'$port' is not a valid port number"
       continue
       ;;
@@ -225,7 +233,7 @@ if [ "x$play" = xnetwork ]; then
       none )
        break
        ;;
-      [^0-9] )
+      [^0-9] | "" )
        echo "'$mcast_port' is not a valid port number"
        continue
        ;;
@@ -269,7 +277,7 @@ fi
 echo
 echo "Proposed DisOrder setup:"
 echo " Music directory:       $roots"
-if [ $port = none ]; then
+if [ "$port" = none ]; then
   echo " Do not listen on a TCP port"
 else
   echo " TCP port to listen on: $port"
index 09f5098..c4c889a 100644 (file)
@@ -876,8 +876,19 @@ static void logclient(const char *msg, void *user) {
   if(!c->w || !c->r) {
     /* This connection has gone up in smoke for some reason */
     eventlog_remove(c->lo);
+    c->lo = 0;
     return;
   }
+  /* user_* messages are restricted */
+  if(!strncmp(msg, "user_", 5)) {
+    /* They are only sent to admin users */
+    if(!(c->rights & RIGHT_ADMIN))
+      return;
+    /* They are not sent over TCP connections unless remote user-management is
+     * enabled */
+    if(!config->remote_userman && !(c->rights & RIGHT__LOCAL))
+      return;
+  }
   sink_printf(ev_writer_sink(c->w), "%"PRIxMAX" %s\n",
              (uintmax_t)time(0), msg);
 }
@@ -1234,10 +1245,21 @@ static int c_edituser(struct conn *c,
       /* Update rights for this user */
       rights_type r;
 
-      if(parse_rights(vec[2], &r, 1))
-       for(d = connections; d; d = d->next)
-         if(!strcmp(d->who, vec[0]))
+      if(!parse_rights(vec[2], &r, 1)) {
+        const char *new_rights = rights_string(r);
+       for(d = connections; d; d = d->next) {
+         if(!strcmp(d->who, vec[0])) {
+            /* Update rights */
            d->rights = r;
+            /* Notify any log connections */
+            if(d->lo)
+              sink_printf(ev_writer_sink(d->w),
+                          "%"PRIxMAX" rights_changed %s\n",
+                          (uintmax_t)time(0),
+                          quoteutf8(new_rights));
+          }
+        }
+      }
     }
     sink_writes(ev_writer_sink(c->w), "250 OK\n");
   } else {
@@ -1405,7 +1427,7 @@ static int c_reminder(struct conn *c,
     return 1;
   }
   if(!(email = kvp_get(k, "email"))
-     || !strchr(email, '@')) {
+     || !email_valid(email)) {
     error(0, "user '%s' has no valid email address", vec[0]);
     sink_writes(ev_writer_sink(c->w), "550 Cannot send a reminder email\n");
     return 1;
@@ -1751,6 +1773,7 @@ static int listen_callback(ev_source *ev,
   D(("server listen_callback fd %d (%s)", fd, l->name));
   nonblock(fd);
   cloexec(fd);
+  c->next = connections;
   c->tag = tags++;
   c->ev = ev;
   c->w = ev_writer_new(ev, fd, writer_error, c,
@@ -1762,6 +1785,7 @@ static int listen_callback(ev_source *ev,
   c->reader = reader_callback;
   c->l = l;
   c->rights = 0;
+  connections = c;
   gcry_randomize(c->nonce, sizeof c->nonce, GCRY_STRONG_RANDOM);
   sink_printf(ev_writer_sink(c->w), "231 %d %s %s\n",
              2,
index 4c2547b..11d0e95 100644 (file)
@@ -167,7 +167,7 @@ USA
       <p class=entry>
        <a href="@url?action=choose&#38;dir=@urlquote{@track}">
         <img class=button src="@image{directory}" alt="">
-        @display
+        @quote{@display}
        </a>
       </p>}
     </div>
@@ -191,7 +191,7 @@ USA
        }@#
        <a href="@url?action=play&#38;track=@urlquote{@track}&#38;back=@urlquote{@thisurl}"
           title="@label{choose.play}">
-        @display
+        @quote{@display}
        </a>
        @if{@eq{@trackstate{@track}}{playing}}
           {[<b>playing</b>]}