Logo Search packages:      
Sourcecode: pan version File versions  Download package

articlelist.c

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * Pan - A Newsreader for Gtk+
 * Copyright (C) 2002  Charles Kerr <charles@rebelbase.com>
 *
 * 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; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <config.h>

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#include <glib.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtkctree.h>
#include <gtk/gtkmain.h>
#include <gtk/gtkvbox.h>
#include <gtk/gtkscrolledwindow.h>

#include <pan/base/acache.h>
#include <pan/base/argset.h>
#include <pan/base/article-thread.h>
#include <pan/base/article.h>
#include <pan/base/debug.h>
#include <pan/base/file-headers.h>
#include <pan/base/group.h>
#include <pan/base/log.h>
#include <pan/base/message-identifier.h>
#include <pan/base/pan-glib-extensions.h>
#include <pan/base/pan-i18n.h>
#include <pan/base/serverlist.h>
#include <pan/base/status-item.h>

#include <pan/action.h>
#include <pan/article-actions.h>
#include <pan/articlelist.h>
#include <pan/article-toolbar.h>
#include <pan/flagset.h>
#include <pan/grouplist.h>
#include <pan/group-action.h>
#include <pan/globals.h>
#include <pan/header-pane-renderer.h>
#include <pan/messageidset.h>
#include <pan/prefs.h>
#include <pan/queue.h>
#include <pan/task-bodies.h>
#include <pan/task-save.h>
#include <pan/task-headers.h>
#include <pan/text.h>
#include <pan/util.h>

#include <pan/filters/filter-top.h>
#include <pan/filters/filter-score.h>
#include <pan/filters/score.h>

/***
****
****  Abandon hope, all ye who enter here
****
***/

/**********************************************************
 * Private Variables
 **********************************************************/

/* Since freeze/thaw has some overhead, don't do it if there
 * are only a few nodes to refresh.  10u is an arbitrary number. */
#define FREEZE_THRESHOLD 10u

/* state */
static Group * my_group                          = NULL;
static GHashTable * message_id_to_node           = NULL;
static int sort_type                             = ARTICLE_SORT_SUBJECT;
static GStaticMutex article_ctree_lock           = G_STATIC_MUTEX_INIT;
static GStaticMutex hash_mutex                   = G_STATIC_MUTEX_INIT;
static GStaticMutex switch_mutex                 = G_STATIC_MUTEX_INIT;
static GtkCTree * _article_ctree                 = NULL;
static GtkCList * _article_clist                 = NULL;
static MessageIdSet * messages_needing_refresh   = NULL;
static guint refresh_dirty_messages_timeout_id   = 0u;

static PString cur_message_id;
static PString prev_message_id;

static HeaderPaneRenderer * _renderer            = NULL;
static GtkWidget * threads_popup_menu = NULL;
static GtkItemFactory * threads_popup_factory = NULL;
static GtkItemFactoryEntry threads_popup_entries[];
static int threads_popup_entries_qty;

static gboolean _articlelist_repopulating = FALSE;

enum {
      REFRESH_TREE          = (1<<0),
      REFRESH_SCORE         = (1<<2),
      REFRESH_FILTER        = (1<<3),
      REFRESH_ALL           = (REFRESH_TREE | REFRESH_SCORE | REFRESH_FILTER)
};

/**********************************************************
 * Private Functions
 **********************************************************/

static int
fire_group_changed (gpointer group);

static void
articlelist_menu_popup_nolock (GdkEventButton* bevent);

static void
articlelist_repopulate_nolock (Group              * group,
                               Article           ** article_buf,
                               int                  article_qty,
                               GPtrArray          * sel_articles);

static GtkCTreeNode* articlelist_get_node_from_message_id (const PString * message_id);

static void article_ctree_destroy_cb (void);

static void header_pane_refresh (int actions);

/***
****
****
****  SELECTIONS
****
****
***/


void
articlelist_select_all_nolock (void)
{
      gtk_ctree_select_recursive (_article_ctree, NULL);
}

void
articlelist_deselect_all_nolock (void)
{
      gtk_ctree_unselect_recursive (_article_ctree, NULL);
}

static Article*
articlelist_get_article_from_node (GtkCTreeNode* node)
{
      Article* article = NULL;

      if (node != NULL)
            article = ARTICLE(
                    gtk_ctree_node_get_row_data (
                          _article_ctree, node));

      return article;
}

static GPtrArray*
articlelist_get_selected_nodes_nolock (void)
{
      GPtrArray * a;
      const GList * l;
      debug_enter ("articlelist_get_selected_nodes_nolock");

            a = g_ptr_array_sized_new (64);
      for (l=_article_clist->selection; l!=NULL; l=l->next)
            g_ptr_array_add (a, GTK_CTREE_NODE(l->data));

      debug_exit ("articlelist_get_selected_nodes_nolock");
      return a;
}

static GtkCTreeNode*
articlelist_get_selected_node (void)
{
      GtkCTreeNode* node = NULL;

      const GList* list =  _article_clist->selection;
      if (list != NULL)
            node = GTK_CTREE_NODE(list->data);

      return node;
}


static GPtrArray*
_articlelist_get_selected_articles_nolock (void)
{
      GPtrArray * retval;
      GList * list;
      debug_enter ("articlelist_get_selected_articles_nolock");

      retval = g_ptr_array_sized_new (256);
      list = _article_clist->selection;
      for (; list!=NULL; list=list->next)
      {
            GtkCTreeNode * n = GTK_CTREE_NODE(list->data);
            Article * a = articlelist_get_article_from_node(n);
            if (a != NULL)
                  g_ptr_array_add (retval, a);
      }

      debug_exit ("articlelist_get_selected_articles_nolock");
      return retval;
}

static void
append_article_to_gptr_array (Article ** articles, guint article_qty, gpointer ptr_array_gpointer)
{
      pan_g_ptr_array_append ((GPtrArray*)ptr_array_gpointer, (gpointer*)articles, article_qty);
}

static GPtrArray*
_articlelist_get_active_articles_nolock (void)
{
      guint i;
      GPtrArray * nodes;
      GPtrArray * retval;
      GPtrArray * parents;
      debug_enter ("articlelist_get_active_articles_nolock");

      parents = g_ptr_array_sized_new (128);
      retval = g_ptr_array_sized_new (128);

      /* add collapsed && !leaves to "parents", all others to "retval" */
      nodes = articlelist_get_selected_nodes_nolock ();
      for (i=0; i<nodes->len; ++i)
      {
            GtkCTreeNode * n = GTK_CTREE_NODE (g_ptr_array_index (nodes, i));
            Article * a = articlelist_get_article_from_node(n);
            GtkCTreeRow * row;

            if (a == NULL)
                  continue;

            row = GTK_CTREE_ROW (n);
            if (row->is_leaf || row->expanded)
                  g_ptr_array_add (retval, a);
            else
                  g_ptr_array_add (parents, a);
      }

      /* if any parents, get the entire subthreads and add to retval */
      article_forall_in_threads ((Article**)parents->pdata, parents->len, GET_SUBTHREAD,
                                 append_article_to_gptr_array, retval);

      /* cleanup */
      g_ptr_array_free (parents, TRUE);
      g_ptr_array_free (nodes, TRUE);

      debug_exit ("articlelist_get_active_articles_nolock");
      return retval;
}

void
header_pane_foreach_selected (ArticleForeach foreach, gpointer user_data)
{
      guint i;
      GPtrArray * a = _articlelist_get_selected_articles_nolock ();
      for (i=0; i!=a->len; ++i)
            (foreach)(ARTICLE(g_ptr_array_index(a,i)), user_data);
      g_ptr_array_free (a, TRUE);
}

static void
header_pane_first_selected_or_null (ArticleForeach foreach, gpointer user_data)
{
      Article * article = articlelist_get_article_from_node (articlelist_get_selected_node());
      (foreach)(article, user_data);
}
void
header_pane_first_selected (ArticleForeach foreach, gpointer user_data)
{
      Article * article = articlelist_get_article_from_node (articlelist_get_selected_node());
      if (article != NULL)
            (foreach)(article, user_data);
}

void
header_pane_forall_selected (ArticleForall forall, gpointer user_data, gboolean call_forall_even_if_none_selected)
{
      GPtrArray * a = _articlelist_get_selected_articles_nolock ();
      if (call_forall_even_if_none_selected || a->len)
            (forall)((Article**)a->pdata, a->len, user_data);
      g_ptr_array_free (a, TRUE);
}

void
header_pane_foreach_active (ArticleForeach foreach, gpointer user_data)
{
      guint i;
      GPtrArray * a = _articlelist_get_active_articles_nolock ();
      for (i=0; i!=a->len; ++i)
            (foreach)(ARTICLE(g_ptr_array_index(a,i)), user_data);
      g_ptr_array_free (a, TRUE);
}

void
header_pane_forall_active (ArticleForall forall, gpointer user_data)
{
      GPtrArray * a = _articlelist_get_active_articles_nolock ();
      if (a->len > 0)
            (forall)((Article**)a->pdata, a->len, user_data);
      g_ptr_array_free (a, TRUE);
}

guint
articlelist_get_selected_count_nolock (void)
{
      guint retval;
      debug_enter ("articlelist_get_selected_count_nolock");

      retval = g_list_length (_article_clist->selection);

      debug_exit ("articlelist_get_selected_count_nolock");
      return retval;

}

static void
mark_gboolean_user_data_as_true (Article * article, gpointer gboolean_user_data)
{
      *((gboolean*)gboolean_user_data) = TRUE;
}

gboolean
header_pane_has_selection (void)
{
      gboolean has_selection = FALSE;
      header_pane_first_selected (mark_gboolean_user_data_as_true, &has_selection);
      return has_selection;
}


static void
articlelist_set_selected_nodes_nolock (GtkCTreeNode **nodes, guint node_qty)
{
      guint i;
      GtkCTreeNode * first = NULL;
      GPtrArray * old_nodes;
      debug_enter ("articlelist_set_selected_nodes_nolock");

      /* sanity clause */
      g_return_if_fail (nodes>0 || nodes!=NULL);

      old_nodes = articlelist_get_selected_nodes_nolock ();

      /* don't bother with FREEZE_THRESHOLD here -- we need to freeze/thaw
       * anyway to make the focus_row further down get drawn onscreen */
      gtk_clist_freeze (_article_clist);

      /* deselect the old nodes */
      for (i=0; i<old_nodes->len; ++i) {
            GtkCTreeNode * node = (GtkCTreeNode*) g_ptr_array_index (old_nodes, i);
            gtk_ctree_unselect (_article_ctree, node);
      }

      /* select the new nodes */
      for (i=0; i<node_qty; ++i) {
            GtkCTreeNode * node = nodes [i];
            if (node != NULL) {
                  gtk_ctree_select (_article_ctree, node);
                  if (first == NULL)
                        first = node;
            }
      }

      if (first!=NULL)
      {
            double hadj;
            GtkCTreeNode * n;
            gboolean expansion_done = FALSE;

            /* make sure it's not hidden by a collapsed tree */
            for (n=GTK_CTREE_ROW(first)->parent; n; n=GTK_CTREE_ROW(n)->parent) {
                  if (!GTK_CTREE_ROW(n)->expanded) {
                        expansion_done = TRUE;
                        gtk_ctree_expand(_article_ctree, n);
                  }
            }

            /* this is a workaround for http://bugzilla.gnome.org/show_bug.cgi?id=98452 */
            if (expansion_done && !GTK_WIDGET_MAPPED(GTK_WIDGET(Pan.article_ctree)))
                  gtk_widget_queue_resize (GTK_WIDGET(Pan.article_ctree));

            /* hack to sync the focus row on new selection - thanks to Julien Plissonneau Duquene. */
            if (_article_clist->selection != NULL)
                  _article_clist->focus_row = g_list_position (_article_clist->row_list,
                                                     (GList*)(_article_clist->selection->data));

            /* center it in the middle of the pane */
            hadj = gtk_clist_get_hadjustment (_article_clist)->value;
            gtk_ctree_node_moveto (_article_ctree, first, 0, (gfloat)0.5, (gfloat)0.0);
            gtk_adjustment_set_value (gtk_clist_get_hadjustment(_article_clist), hadj);
      }

      gtk_clist_thaw (_article_clist);

      g_ptr_array_free (old_nodes, TRUE);
      debug_exit ("articlelist_set_selected_nodes_nolock");
}

static void
select_these_articles (Article ** articles, guint article_qty, gpointer unused)
{
      guint i;
      guint node_qty = 0; 
      GtkCTreeNode ** nodes = g_new (GtkCTreeNode*, article_qty);

      for (i=0; i!=article_qty; ++i)
      {
            GtkCTreeNode * node = articlelist_get_node_from_message_id (&articles[i]->message_id);
            if (node != NULL)
                  nodes[node_qty++] = node;
      }

      articlelist_set_selected_nodes_nolock (nodes, node_qty);
      g_free (nodes);
}
static void
select_these_articles_and_their_threads (Article ** articles, guint article_qty, gpointer thread_get_gpointer)
{
      const ThreadGet thread_get = GPOINTER_TO_INT (thread_get_gpointer);
      article_forall_in_threads (articles, article_qty, thread_get, select_these_articles, NULL);
}
void
articlelist_add_replies_to_selection_nolock (void)
{
      header_pane_forall_selected (select_these_articles_and_their_threads, GINT_TO_POINTER(GET_SUBTHREAD), FALSE);
}
void
articlelist_add_thread_to_selection_nolock (void)
{
      header_pane_forall_selected (select_these_articles_and_their_threads, GINT_TO_POINTER(GET_WHOLE_THREAD), FALSE);
}

static int _button_click_count = -1;
static int _mb = -1;
static gboolean _modifiers = FALSE;

static void
select_node_nolock (GtkCTreeNode * node)
{
      GtkCTreeNode * sel = NULL;

      if (node != NULL)
            sel = articlelist_get_selected_node ();
      if (node != sel)
            articlelist_set_selected_nodes_nolock (&node, 1);
}

static void
activate_node_nolock (GtkCTreeNode * node)
{
      _mb = 1;
      _modifiers = FALSE;
      _button_click_count = 2;
      select_node_nolock (node);
}


/***
****
****
****  NAVIGATION
****
****
***/

typedef void (*NodeFunc)(GtkCTreeNode * node, gpointer user_data);

static GtkCTreeNode*
tree_node_next (GtkCTreeNode * node)
{
      /* sanity clause */
      g_return_val_if_fail (node!=NULL, NULL);

      /* if nothing to read... */
      if (_article_clist->rows <2)
            return node;

      /* traverse the children */
      if (GTK_CTREE_ROW(node)->children)
            return GTK_CTREE_ROW(node)->children;

      /* traverse the siblings */
      {
            GtkCTreeNode * n;
            for (n=node; n!=NULL; n=GTK_CTREE_ROW(n)->parent)
                  if (GTK_CTREE_ROW(n)->sibling)
                        return GTK_CTREE_ROW(n)->sibling;
      }

      /* if all else fails, return the first node */
      return gtk_ctree_node_nth (_article_ctree, 0);
}

static GtkCTreeNode*
tree_node_back (GtkCTreeNode * node)
{
      GtkCTreeNode * parent;
      GtkCTreeNode * sibling;
      GtkCTreeNode * zeroth = gtk_ctree_node_nth (_article_ctree, 0);
      const int row_qty = _article_clist->rows;

      if (node == NULL)
            node = zeroth;

      if (node == NULL)
            return NULL;

      if (node == zeroth)
            return gtk_ctree_node_nth (_article_ctree, row_qty-1);

      /* get parent */  
      parent = GTK_CTREE_ROW(node)->parent;
      if (!parent)
            return NULL;

      /* parent's first child */
      sibling=GTK_CTREE_ROW(parent)->children;
      if (sibling==node) /* this is the first child */
            return parent;

      /* previous sibling of node */
      while (GTK_CTREE_ROW(sibling)->sibling != node)
            sibling = GTK_CTREE_ROW(sibling)->sibling;

      /* find the absolutely last child of the older sibling */
      for (;;) {
            GtkCTreeNode* tmp = GTK_CTREE_ROW(sibling)->children;
            if (!tmp)
                  return sibling;

            /* last child of sibling */
            while (GTK_CTREE_ROW(tmp)->sibling != NULL)
                  tmp = GTK_CTREE_ROW(tmp)->sibling;

            sibling = tmp;
      }

      pan_warn_if_reached ();
      return NULL;
}

/**
***
**/

static void
articlelist_read_article (Article * article)
{
      pstring_copy (&prev_message_id, &cur_message_id);

      if (article == NULL)
      {
            pstring_clear (&cur_message_id);
            text_set_from_identifiers (NULL, NULL, NULL, NULL, 0);
      }
      else
      {
            pstring_copy (&cur_message_id, &article->message_id);

            if (article->parts < 2)
            {
                  MessageIdentifier * mid = message_identifier_new_from_article (article);
                  text_set_from_identifiers (article->group->server,
                                             group_get_acache_key(article->group),
                                             group_get_default_charset(article->group),
                                             &mid, 1);
                  g_object_unref (mid);
            }
            else
            {
                  int i;
                  GSList * l;
                  GPtrArray * articles;
                  MessageIdentifier ** mids;

                  /* make an array of weeded Articles */
                  articles = g_ptr_array_sized_new (64);
                  g_ptr_array_add (articles, article);
                  for (l=article->threads; l!=NULL; l=l->next)
                        g_ptr_array_add (articles, l->data);
                  task_save_weed_duplicates (articles);

                  /* make an array of MessageIdentifiers */
                  mids = g_newa (MessageIdentifier*, articles->len);
                  for (i=0; i<articles->len; ++i)
                        mids[i] = message_identifier_new_from_article (ARTICLE(g_ptr_array_index(articles,i)));
                  text_set_from_identifiers (article->group->server,
                                             group_get_acache_key(article->group),
                                             group_get_default_charset(article->group),
                                             mids, articles->len);
            
                  /* cleanup */
                  g_ptr_array_free (articles, TRUE);
                  for (i=0; i<articles->len; ++i)
                        g_object_unref (mids[i]);
            }
      }
}

static void
articlelist_activate_article (Article * a)
{
        if (a==NULL || !article_is_valid(a))
      {
            /* no article */
            articlelist_read_article (NULL);
      }
      else if (a->part==1 && a->parts>3 && a->multipart_state==MULTIPART_STATE_ALL)
      {
            /* clicked on a big multipart; pop up the save dialog */
            article_action_selected_save_as ();
      }
      else if (a->part<2 && (queue_is_online() || acache_has_message (group_get_acache_key(a->group), &a->message_id)))
      {
            /* clicked on an article that we've got, or that we can get */
            articlelist_read_article (a);
      }
}

static void
header_pane_download_all_single_part_except (Article ** articles, guint article_qty, gpointer except_me)
{
      guint i;
      GPtrArray * a;
       
      a = g_ptr_array_sized_new (article_qty);
      for (i=0; i!=article_qty; ++i)
            if (articles[i]!=except_me && articles[i]->parts<2)
                  g_ptr_array_add (a, articles[i]);

      if (a->len != 0u)
            queue_add (TASK(task_bodies_new_from_articles ((const Article **)a->pdata, a->len)));

      g_ptr_array_free (a, TRUE);
}

static void
articlelist_read_article_and_download_other_active (Article * article)
{
      articlelist_read_article (article);

      header_pane_forall_active (header_pane_download_all_single_part_except, article);
}

void
articlelist_read_selected (void)
{
      header_pane_first_selected ((ArticleForeach)articlelist_read_article_and_download_other_active, NULL);
}

static void
articlelist_activate_article_and_download_other_active (Article * article)
{
      articlelist_activate_article (article);

      header_pane_forall_active (header_pane_download_all_single_part_except, article);
}

void
articlelist_activate_selected (void)
{
      header_pane_first_selected ((ArticleForeach)articlelist_activate_article_and_download_other_active, NULL);
}

/***
****
***/

typedef GtkCTreeNode*   (*NodeIteratorFunc)  (GtkCTreeNode * node);

static void
header_pane_find_next_iterated_node_from (NodeIteratorFunc     node_iterator_func,
                                          ArticleTest          test_func,
                                          gpointer             test_func_user_data,
                                          NodeFunc             node_func,
                                          gpointer             node_func_user_data,
                                          GtkCTreeNode       * start_node,
                                          gboolean             loop_around_when_end_reached)
{
      gboolean match = FALSE;
      GtkCTreeNode * march;

      /* sanity clause */
      g_return_if_fail (start_node!=NULL);

      /* walk through the nodes */
      march = start_node;
      for (;;)
      {
            const Article * article;

            /* have we looped around to where we started? */
            march = (node_iterator_func)(march);
            if (march == start_node)
                  break;

            /* is there an article? */
            article = articlelist_get_article_from_node (march);
            if (article==NULL && !loop_around_when_end_reached)
                  break;

            /* do we have a match? */
            match = article!=NULL && (test_func)(article, test_func_user_data);
            if (match)
                  break;
      }

      if (match)
            (node_func)(march, node_func_user_data);
}

static void
header_pane_find_next_iterated_node (NodeIteratorFunc     node_iterator_func,
                                     ArticleTest          test_func,
                                     gpointer             test_func_user_data,
                                     NodeFunc             node_func,
                                     gpointer             node_func_user_data,
                                     gboolean             loop_around_when_end_reached)
{
      GtkCTreeNode * start_node;

      /* get the starting node */
      start_node = articlelist_get_selected_node ();
      if (start_node == NULL)
            start_node = gtk_ctree_node_nth (_article_ctree, 0);

      /* if we have a starting node, start iterating */
      if (start_node != NULL)
            header_pane_find_next_iterated_node_from (node_iterator_func,
                                                      test_func, test_func_user_data,
                                                      node_func, node_func_user_data,
                                                      start_node,
                                                      loop_around_when_end_reached);
}

/***
****
***/

static void
activate_node (GtkCTreeNode * node, gpointer user_data)
{
      pan_lock ();
      activate_node_nolock (node);
      pan_unlock ();
}

static void
select_node (GtkCTreeNode * node, gpointer user_data)
{
      pan_lock ();
      select_node_nolock (node);
      pan_unlock ();
}

void
header_pane_select_next_if (ArticleTest test_func, gpointer test_func_user_data)
{
      header_pane_find_next_iterated_node (tree_node_next, test_func, test_func_user_data, select_node, NULL, TRUE);
}

void
header_pane_select_prev_if (ArticleTest test_func, gpointer test_func_user_data)
{
      header_pane_find_next_iterated_node (tree_node_back, test_func, test_func_user_data, select_node, NULL, TRUE);
}

static void
header_pane_read_next_if (ArticleTest test_func, gpointer test_func_user_data)
{
      header_pane_find_next_iterated_node (tree_node_next, test_func, test_func_user_data, activate_node, NULL, TRUE);
}

static void
header_pane_read_prev_if (ArticleTest test_func, gpointer test_func_user_data)
{
      header_pane_find_next_iterated_node (tree_node_back, test_func, test_func_user_data, activate_node, NULL, TRUE);
}

/**
***
**/

static gboolean
test_article_for_ok (const Article * article, gpointer user_data)
{
      return article!=NULL && article_is_valid (article) && !article->error_flag;
}

void
header_pane_read_next (void)
{
      header_pane_read_next_if (test_article_for_ok, NULL);
}

static gboolean
test_article_for_unread (const Article * article, gpointer user_data)
{
      return test_article_for_ok(article,NULL) && !article_is_read(article);
}

void
header_pane_read_next_unread (void)
{
      header_pane_read_next_if (test_article_for_unread, NULL);
}

static gboolean
test_article_for_new (const Article * article, gpointer user_data)
{
      return test_article_for_ok(article,NULL) && article_is_new(article);
}

void
header_pane_read_next_new (void)
{
      header_pane_read_next_if (test_article_for_new, NULL);
}

static gboolean
test_article_for_unread_and_positive_score (const Article * article, gpointer user_data)
{
      return test_article_for_unread(article,NULL) && article->score>0;
}

void
header_pane_read_next_score (void)
{
      header_pane_read_next_if (test_article_for_unread_and_positive_score, NULL);
}

static gboolean
test_article_for_different_thread (const Article * article, gpointer other_article)
{
      return test_article_for_ok(article,NULL) && (other_article==NULL || !articles_are_in_same_thread(article,ARTICLE(other_article)));
}
static void
read_thread_after_this_article (Article * article, gpointer unused)
{
      header_pane_read_next_if (test_article_for_different_thread, article);
}
void
header_pane_read_next_thread (void)
{
      header_pane_first_selected_or_null (read_thread_after_this_article, NULL);
}

static gboolean
test_article_for_unread_in_different_thread (const Article * article, gpointer other_article)
{
      return test_article_for_different_thread(article,other_article) && !article_is_read(article);
}
static void
read_unread_after_this_thread (Article * article, gpointer unused)
{
      header_pane_read_next_if (test_article_for_unread_in_different_thread, article);
}
void
header_pane_read_next_unread_thread (void)
{
      header_pane_first_selected_or_null (read_unread_after_this_thread, NULL);
}

static gboolean
test_article_for_new_in_different_thread (const Article * article, gpointer other_article)
{
      return test_article_for_different_thread(article,other_article) && article_is_new(article);
}
static void
read_new_after_this_thread (Article * article, gpointer unused)
{
      header_pane_read_next_if (test_article_for_new_in_different_thread, article);
}
void
header_pane_read_next_new_thread (void)
{
      header_pane_first_selected_or_null (read_new_after_this_thread, NULL);
}

void
header_pane_read_prev (void)
{
      header_pane_read_prev_if (test_article_for_ok, NULL);
}

static void
read_thread_before_this_article (Article * article, gpointer unused)
{
      header_pane_read_prev_if (test_article_for_different_thread, article);
}
void
header_pane_read_prev_thread (void)
{
      header_pane_first_selected_or_null (read_thread_before_this_article, NULL);
}

static gboolean
test_article_for_identity (const Article * article, gpointer compare)
{
      return article == compare;
}

static void
read_parent_of_this_article (Article * article, gpointer unused)
{
      if (article->parent != NULL)
            header_pane_read_prev_if (test_article_for_identity, article->parent);
}
void
header_pane_read_parent (void)
{
      header_pane_first_selected (read_parent_of_this_article, NULL);
}

/**
***
**/

gboolean
articlelist_has_prev_read (void)
{
      return pstring_is_set (&prev_message_id);
}

void
header_pane_read_prev_read (void)
{
      header_pane_read_prev_if (test_article_for_ok, NULL);
      if (articlelist_has_prev_read ())
      {
            GtkCTreeNode * node = articlelist_get_node_from_message_id (&prev_message_id);

            if (node != NULL)
                  activate_node_nolock (node);
      }
}

/**
***
**/

static const int SECONDS_IN_DAY = 60 * 60 * 24;

static gboolean
test_article_for_set (const Article * article, gpointer reference_article_gpointer)
{
      const Article * reference_article = (const Article *) reference_article_gpointer;

      /* same author, posted within a day and a half of article, with a similar subject */
      return test_article_for_ok(article,NULL)
            && abs(difftime (article->date, reference_article->date)) < SECONDS_IN_DAY
            && pstring_equal (&article->author_addr, &reference_article->author_addr)
            && article_subjects_are_similar (article, reference_article);
}

static void
add_set_to_hash (Article * reference_article, gpointer hash_gpointer)
{
      GHashTable * hash = (GHashTable*) hash_gpointer;
      GtkCTreeNode * first = gtk_ctree_node_nth (_article_ctree, 0);
      GtkCTreeNode * march;

      for (march=tree_node_next(first); march!=first; march=tree_node_next(march))
      {
            Article * test_article = articlelist_get_article_from_node (march);

            if (test_article_for_set (test_article, reference_article))
                  g_hash_table_insert (hash, march, march);
      }
}

void
articlelist_add_set_to_selection_nolock (void)
{
      GPtrArray * a = g_ptr_array_new ();
      GHashTable * h = g_hash_table_new (g_direct_hash, g_direct_equal);

      /* get a list of the selected nodes and their sets, and select them. */
      header_pane_foreach_selected (add_set_to_hash, h);
      pan_hash_to_ptr_array  (h, a);
      articlelist_set_selected_nodes_nolock ((GtkCTreeNode**)a->pdata, a->len);

      /* cleanup */
      g_ptr_array_free (a, TRUE);
      g_hash_table_destroy (h);
}

/***
****
****
****   SORTING
****
****
***/

static int
get_column_number_from_column_type (int type)
{
      int i;
      for (i=0; i<articlelist_column_qty; ++i)
            if (articlelist_columns[i] == type)
                  return i;

      /* column is not visible */
      return -1;
}

static const char*
column_to_title (int col)
{
      switch (articlelist_columns[col]) {
            case COLUMN_ACTION_STATE: return " ";
            case COLUMN_ARTICLE_STATE: return " ";
            case COLUMN_SCORE: return _("Score");
            case COLUMN_SUBJECT: return _("Subject");
            case COLUMN_LINES: return _("Lines");
            case COLUMN_AUTHOR: return _("Author");
            case COLUMN_DATE: return _("Date");
            default: break;
      }

      pan_warn_if_reached();
      return "BUG!!";
}

static int
column_type_to_sort_type (int column_type)
{
      switch (abs(column_type)) {
            case COLUMN_ARTICLE_STATE:  return ARTICLE_SORT_READ_STATE;
            case COLUMN_ACTION_STATE:   return ARTICLE_SORT_ACTION_STATE;
            case COLUMN_SCORE:          return ARTICLE_SORT_SCORE;
            case COLUMN_SUBJECT:        return ARTICLE_SORT_SUBJECT;
            case COLUMN_LINES:          return ARTICLE_SORT_LINES;
            case COLUMN_AUTHOR:         return ARTICLE_SORT_AUTHOR;
            case COLUMN_DATE:           return ARTICLE_SORT_DATE;
            default:                    pan_warn_if_reached(); return -1;
      }
}

static int
sort_type_to_column_type (int sort_type)
{
      switch (abs(sort_type)) {
            case ARTICLE_SORT_READ_STATE:   return COLUMN_ARTICLE_STATE;
            case ARTICLE_SORT_ACTION_STATE: return COLUMN_ACTION_STATE;
            case ARTICLE_SORT_SCORE:        return COLUMN_SCORE;
            case ARTICLE_SORT_SUBJECT:      return COLUMN_SUBJECT;
            case ARTICLE_SORT_LINES:        return COLUMN_LINES;
            case ARTICLE_SORT_AUTHOR:       return COLUMN_AUTHOR;
            case ARTICLE_SORT_DATE:         return COLUMN_DATE;
            default:                        pan_warn_if_reached(); return -1;
      }
}

static int
sort_type_to_column (int sort_type)
{
      int column_type = sort_type_to_column_type (sort_type);
      return get_column_number_from_column_type (column_type);
}

static void
articlelist_set_sort_bits_nolock (int new_type)
{
      char buf[64];
      const int old_col = sort_type_to_column (sort_type);
      const int new_col = sort_type_to_column (new_type);

      /* update old column */
      if (old_col != new_col)
            gtk_clist_set_column_title (_article_clist, old_col, column_to_title(old_col));

      /* update new column */
      sort_type = new_type;
      g_snprintf (buf, sizeof(buf), "%c%s", (sort_type>0?'+':'-'), column_to_title(new_col));
      gtk_clist_set_column_title (_article_clist, new_col, buf);
}

static void
articlelist_set_sort_type (int sort)
{
      articlelist_set_sort_bits_nolock (sort);

      if (my_group != NULL)
            group_set_sort_style (my_group, sort);

      header_pane_refresh (REFRESH_TREE);
}

static void
column_clicked_cb (GtkCList* clist, int column_number)
{
      const int column_type = articlelist_columns[column_number];
      int new_sort_type = column_type_to_sort_type (column_type);

      if (new_sort_type == sort_type)
            new_sort_type = -new_sort_type;

      articlelist_set_sort_type (new_sort_type);
}

/***
****
****
****
***/

Group*
articlelist_get_group (void)
{
      return my_group;
}


/***
****
****  ARTICLES ADDED
****
***/

static void
group_articles_added_cb (gpointer call_obj, gpointer call_arg, gpointer client_arg)
{
      Group * group = GROUP(call_obj);
      GPtrArray * added = (GPtrArray*) call_arg;
      debug_enter ("group_articles_added_cb");

      /* sanity checks */
      g_return_if_fail (group_is_valid(group));
      g_return_if_fail (added!=NULL);
      g_return_if_fail (added->len>0u);

      /* maybe refresh the articles */
      if (group == my_group)
            header_pane_refresh (REFRESH_ALL);

      debug_exit ("group_articles_added_cb");
}

/***
****
****  ARTICLES REMOVED
****
***/

typedef struct
{
      Article * a;
      int depth;
}
SortDepthStruct;

static int
compare_pSDS_to_pSDS_by_depth (const void * a, const void * b)
{
      return ((SortDepthStruct*)a)->depth -
             ((SortDepthStruct*)b)->depth;
}

static void
sort_articles_by_depth (Article ** articles, guint article_qty, gboolean parents_first)
{
      guint i;
      SortDepthStruct * buf;
      debug_enter ("sort_articles_by_depth");

      /* make an array of article,depth pairs */
      buf = g_newa (SortDepthStruct, article_qty);
      for (i=0; i!=article_qty; ++i)
      {
            SortDepthStruct * item = buf + i;
            const Article * tmp;
            item->a = articles[i];
            item->depth = 0;
            for (tmp=item->a; tmp->parent!=NULL; tmp=tmp->parent)
                  ++item->depth;
      }

      /* sort the array */
      qsort (buf,
             article_qty,
             sizeof(SortDepthStruct),
             compare_pSDS_to_pSDS_by_depth);


      /* put back into the original GPtrArray */
      for (i=0; i!=article_qty; ++i)
            articles[i] = buf[parents_first ? i : article_qty-1-i].a;

      /* cleanup */
      debug_exit ("sort_articles_by_depth");
}

static int
compare_ppArticle_to_ppArticle_by_message_id (const void * a, const void * b)
{
      return pstring_compare (&(*(const Article **)a)->message_id,
                              &(*(const Article **)b)->message_id);
}

static gboolean
test_article_for_absence_from_ptr_array (const Article * article, gpointer ptr_array_of_articles_sorted_by_msgid)
{
      GPtrArray * articles = (GPtrArray*) ptr_array_of_articles_sorted_by_msgid;
      gboolean match = FALSE;
      lower_bound (&article, articles->pdata, articles->len, sizeof(gpointer), compare_ppArticle_to_ppArticle_by_message_id, &match);
      return match == 0;
}

static void
remove_article_nolock (const Article * article)
{
      GtkCTreeNode * node = NULL;
      GtkCTree * ctree = _article_ctree;

      /* sanity checks */
      g_return_if_fail (article_is_valid(article));

      /* if this article currently has a node, remove the node */
      node = articlelist_get_node_from_message_id (&article->message_id);
      if (node!=NULL)
      {
            GSList *l=NULL, *l2;
            GtkCTreeNode * child;

            /* make a list of children. */
            for (child = GTK_CTREE_ROW(node)->children;
                 child != NULL;
                 child = GTK_CTREE_ROW(child)->sibling)
                  l = g_slist_prepend (l, child);
            l = g_slist_reverse (l);

            /* for each child, reparent to the grandparent */
            for (l2=l; l2!=NULL; l2=l2->next) {
                  GtkCTreeNode * child = (GtkCTreeNode*) l2->data;
                  gtk_ctree_move (ctree, child, GTK_CTREE_ROW(node)->parent, NULL);
            }

            /* cleanup */
            g_slist_free (l);
      }

      /* if the article was unread, update the parents' unread children count */
      if (!article_is_read (article))
            messageidset_add_articles_and_ancestors   (messages_needing_refresh, &article, 1);

      if (node != NULL)
      {
            /* remove from hashtable */
            g_static_mutex_lock (&hash_mutex);
            g_hash_table_remove (message_id_to_node, &article->message_id);
            g_static_mutex_unlock (&hash_mutex);

            /* remove from ctree */
            gtk_ctree_remove_node (ctree, node);
      }
}

static void
copy_node_pointer (GtkCTreeNode* node, gpointer user_data)
{
      *((GtkCTreeNode**)user_data) = node;
}

static gboolean _pane_is_about_to_be_rebuilt = FALSE;

static void
header_pane_remove_articles (Group * group, GPtrArray * articles)
{
      if (group==my_group&& !_pane_is_about_to_be_rebuilt)
      {
            guint i;
            const gboolean has_selection = _article_clist->selection != NULL;
            GtkCTreeNode * select_me = NULL;

            /* lock tree */
            g_static_mutex_lock (&article_ctree_lock);
            pan_lock ();
            gtk_clist_freeze (_article_clist);

            /* if the selected article is being deleted, move the selection
             * to the next article _not_ being deleted. */
            if (has_selection) {
                  sort_articles ((Article**)articles->pdata, articles->len, ARTICLE_SORT_MSG_ID, TRUE);
                  header_pane_find_next_iterated_node (tree_node_next,
                                                       test_article_for_absence_from_ptr_array, articles,
                                                       (NodeFunc)copy_node_pointer, &select_me, FALSE);

                  if (select_me == NULL) /* we're at the end of the header pane... go up instead. */
                        header_pane_find_next_iterated_node (tree_node_back,
                                                     test_article_for_absence_from_ptr_array, articles,
                                                     (NodeFunc)copy_node_pointer, &select_me, FALSE);
            }

            /* sort the articles so that we remove the children first --
               this reduces the number of GtkCTreeNodes we have to reparent */
            sort_articles_by_depth ((Article**)articles->pdata, articles->len, FALSE);

            /* remove the articles from the ctree */
            for (i=0; i!=articles->len; ++i)
                  remove_article_nolock (ARTICLE(g_ptr_array_index(articles,i)));

            /* try to select the next article that wasn't deleted */
            if (select_me != NULL)
                  select_node_nolock (select_me);

            /* unlock tree */
            gtk_clist_thaw (_article_clist);
            pan_unlock ();
            g_static_mutex_unlock (&article_ctree_lock);
      }
}

static int
articles_removed_mainthread (gpointer p)
{
      ArgSet * argset;
      Group * group;
      GPtrArray * articles;
      debug_enter ("articles_removed_mainthread");

      /* pump out the arguments */
      argset = (ArgSet*) p;
      group = (Group*) argset_get (argset, 0);
      articles = (GPtrArray*) argset_get (argset, 1);

      /* remove the articles */
      header_pane_remove_articles (group, articles);


      /* cleanup */
      g_ptr_array_free (articles, TRUE);
      group_unref_articles (group, NULL);
      argset_free (argset);

      debug_exit ("articles_removed_mainthread");
      return 0;
}

static void
clear_hash_table (void)
{
      g_static_mutex_lock (&hash_mutex);
      {
            if (message_id_to_node != NULL)
                  g_hash_table_destroy (message_id_to_node);
            message_id_to_node = g_hash_table_new (pstring_hash, pstring_equal);
      }
      g_static_mutex_unlock (&hash_mutex);
}

static int
header_pane_clear_nodes_mainthread (gpointer unused)
{
      pan_lock ();

      clear_hash_table ();
      gtk_clist_clear (_article_clist);

      pan_unlock ();
      return 0;
}

static void
group_articles_removed_cb (gpointer call_obj, gpointer call_arg, gpointer client_data)
{
      Group * group = GROUP(call_obj);
      GPtrArray * removed_articles_sorted_by_message_id = (GPtrArray*) call_arg;

      if (removed_articles_sorted_by_message_id->len == group->article_qty)
      {
            /* they're all being deleted, so we can save a lot of steps
             * by wiping clean the entire header pane. */
            gui_queue_add (header_pane_clear_nodes_mainthread, NULL);
      }
      else
      {
            /* ref the articles for safekeeping */
            group_ref_articles (group, NULL);

            /* push the rest of the work to the main thread */
            gui_queue_add (articles_removed_mainthread, 
                           argset_new2 (group, pan_g_ptr_array_dup(removed_articles_sorted_by_message_id)));
      }
}


/****
*****
*****  MARKING ARTICLES READ/UNREAD
*****
****/

static void
header_pane_unflag_articles (Article ** articles, guint article_qty, gpointer user_data)
{
      flagset_remove_articles ((const Article**)articles, article_qty);
      messageidset_add_articles (messages_needing_refresh, (const Article**)articles, article_qty);
}

void
articlelist_selected_unflag_for_dl_nolock (void)
{
      header_pane_forall_active (header_pane_unflag_articles, NULL);
}

static void
header_pane_flag_articles (Article ** articles, guint article_qty, gpointer user_data)
{
      flagset_add_articles ((const Article**)articles, article_qty);
      messageidset_add_articles (messages_needing_refresh, (const Article**)articles, article_qty);
}

void
articlelist_selected_flag_for_dl_nolock (void)
{
      header_pane_forall_active (header_pane_flag_articles, NULL);
}

/***
****
****
****  POPULATING THE TREE:  UTILITY FUNCTIONS
****
****
***/


/* siblings get matched up to, as per the user's 'show' settings,
 * with one caveat: "don't show ignored articles" outweighs 
 * "show relatives". This way ignored articles must be explicitly
 * turned on in `match ignored articles' before they're shown. */
static void
pass_filter_if_not_ignored (Article ** articles, guint article_qty, gpointer unused)
{
      guint i;
      for (i=0; i!=article_qty; ++i)
            articles[i]->passes_filter = filter_score_get_score_mode(articles[i]->score) != SCORE_IGNORED;
}

static void
apply_filter_tests (Filter * filter, FilterShow show, Article ** articles, guint article_qty)
{
      register guint i;
      GPtrArray * tmp;

      g_return_if_fail (articles!=NULL);
      g_return_if_fail (article_qty > 0);

      /* clear the filter state */
      for (i=0; i!=article_qty; ++i)
            articles[i]->passes_filter = FALSE;

      /* make a working copy of the headers */
      tmp = g_ptr_array_sized_new (article_qty);
      pan_g_ptr_array_append (tmp, (gpointer*)articles, article_qty);

      /* remove the articles that don't pass */
      if (filter != NULL)
            filter_remove_failures (filter, tmp);

      /* process matching article's families */
      for (i=0; i<tmp->len; ++i)
      {
            register Article * a = (Article*) g_ptr_array_index (tmp, i);

            if (show == FILTER_SHOW_SUBTHREADS)
                  article_forall_in_subthread (a, pass_filter_if_not_ignored, NULL);
            else if (show == FILTER_SHOW_THREAD)
                  article_forall_in_thread (a, pass_filter_if_not_ignored, NULL);
      }

      /* process matching articles.
         we do this after doing the families because pass_filter_if_not_ignored
         will fail ignored articles even if we've got `show ignored' turned on. */
      for (i=0; i<tmp->len; ++i)
            ARTICLE (g_ptr_array_index (tmp, i))->passes_filter = TRUE;

      /* cleanup */
      g_ptr_array_free (tmp, TRUE);
}

/***
****
****
****  POPULATING THE TREE:  THE DIRTY WORK
****
****
***/

static gboolean
article_should_be_seen (const Article * a)
{
      register gboolean retval = a->passes_filter;

      if (hide_mpart_child_nodes && a->parent!=NULL && a->part>1 && a->parent->multipart_state==MULTIPART_STATE_ALL)
            retval = FALSE;

      return retval;
}

static gboolean
article_is_toplevel (const Article * a)
{
      register gboolean retval = TRUE;
      register const Article * p;

      if (header_pane_is_threaded)
            for (p=a->parent; retval && p!=NULL; p=p->parent)
                  if (article_should_be_seen (p))
                        retval = FALSE;

      return retval;
}

static void
add_articles_as_children_of_node (GNode      * parent,
                                  GSList     * articles,
                                  GNode     ** gnode_pool)
{
      GSList * l;

      for (l=articles; l!=NULL; l=l->next)
      {
            GNode * parent_node_of_children = parent;
            Article * article = ARTICLE (l->data);

            if (article_should_be_seen (article))
            {
                  GNode * node = *gnode_pool;
                  ++*gnode_pool;
                  node->data = article;
                  g_node_prepend (parent, node);
                  parent_node_of_children = node;
            }

            if (article->threads != NULL)
                  add_articles_as_children_of_node (parent_node_of_children, article->threads, gnode_pool);
      }
}

static GNode*
build_gnode_hierarchy (Group        * group,
                       Article     ** articles,
                       int            article_qty,
                       GNode       ** gnode_pool)
{
      int i;
      GNode * root;

      /* buid the group node */
      root = *gnode_pool;
      ++*gnode_pool;
      root->data = group;

      /* add the articles */
      for (i=0; i<article_qty; ++i)
      {
            Article * article = articles[i];

            if (article_is_toplevel(article) && article_should_be_seen(article))
            {
                  GNode * node = *gnode_pool;
                  ++*gnode_pool;
                  node->data = article;
                  g_node_prepend (root, node);

                  if (header_pane_is_threaded && article->threads!=NULL)
                        add_articles_as_children_of_node (node, article->threads, gnode_pool);
            }
      }

      return root;
}

static void
sort_hierarchy (const Group * group, GNode * node)
{
      if (node!=NULL && node->children!=NULL)
      {
            int i;
            const int child_qty = g_node_n_children (node);
            GNode * walk;

            /* sort the children */
            if (child_qty > 1)
            {
                  GNode ** children = g_newa (GNode*, child_qty);
                  Article ** articles = g_newa (Article*, child_qty);

                  /* format nicely */
                  for (i=0, walk=node->children; walk!=NULL; walk=walk->next, ++i) {
                        children[i] = walk;
                        articles[i] = ARTICLE (walk->data);
                        articles[i]->header_pane_extra = walk;
                  }

                  /* unparent the children */
                  node->children = NULL;
                  for (i=0; i<child_qty; ++i)
                        children[i]->parent = children[i]->next = children[i]->prev = NULL;

                  /* sort the articles */
                  if (group->old_sort_style && (abs(group->old_sort_style) != abs(group->new_sort_style)))
                        sort_articles (articles, child_qty, abs(group->old_sort_style), group->old_sort_style>0);
                  sort_articles (articles, child_qty, abs(group->new_sort_style), group->new_sort_style>0);

                  /* re-insert the nodes, now sorted */
                  for (i=child_qty-1; i>=0; --i)
                        g_node_prepend (node, (GNode*) articles[i]->header_pane_extra);
            }

            /* recurse */
            for (walk=node->children; walk!=NULL; walk=walk->next)
                  sort_hierarchy (group, walk);
      }
}


static gboolean
calculate_article_header_pane_fields (GNode * node, gpointer unused)
{
      if (node->parent != NULL)
      {
            GNode * ancestor;
            Article * article = ARTICLE (node->data);
            const int score = article->score;

            if (article_is_new (article))
                  for (ancestor=node->parent; ancestor!=NULL && ancestor->parent!=NULL; ancestor=ancestor->parent)
                        ++ARTICLE(ancestor->data)->new_children;

            if (!article_is_read (article))
                  for (ancestor=node->parent; ancestor!=NULL && ancestor->parent!=NULL; ancestor=ancestor->parent)
                        ++ARTICLE(ancestor->data)->unread_children;


            for (ancestor=node; ancestor!=NULL && ancestor->parent!=NULL; ancestor=ancestor->parent)
            {
                  if (ARTICLE(ancestor->data)->subthread_score >= score) /* a sibling subthread is stronger; no need to continue */
                        break;
                  ARTICLE(ancestor->data)->subthread_score = score; /* propagate the score upward */
            }
      }

      return FALSE;
}

static gboolean
articlelist_renderer_gnode_func_wrapper (GtkCTree        * ctree,
                                         guint             depth,
                                         GNode           * gnode,
                                         GtkCTreeNode    * cnode,
                                         gpointer          data)
{
      /* let the renderer do its work */
      const gboolean retval = header_pane_renderer_populate_node (ctree, depth, gnode, cnode, data);

      /* update the mid-to-node hash.
       * the depth>1 check skips the Group* in the root gnode's data field. */
      if (depth>1)
      {
            const Article * article = (const Article*) gnode->data;

            g_static_mutex_lock (&hash_mutex);
            {
                  g_hash_table_insert (message_id_to_node, (gpointer)&article->message_id, cnode);
            }
            g_static_mutex_unlock (&hash_mutex);
      }

      return retval;
}

static void
articlelist_repopulate_nolock (Group          * group,
                               Article       ** article_buf,
                               int              article_qty,
                               GPtrArray      * sel_articles)
{
      int i;
      GNode * groot;
      GNode * gnode_pool;
      GNode * gnode_pool_anchor;
      GtkCTreeNode * croot;
      debug_enter ("articlelist_repopulate_nolock");

      /* Make sure we don't trigger any * 'tree-select-row' signals
       * FIXME: better way of doing this ? */
      _articlelist_repopulating = TRUE;

      /* ensure the sort mode is set */
      if (!group->new_sort_style)
            group->new_sort_style = -ARTICLE_SORT_DATE;
      articlelist_set_sort_bits_nolock (group->new_sort_style);

      /* clear out old */
      gtk_clist_freeze (_article_clist);
      gtk_clist_clear (_article_clist);
      clear_hash_table ();

      /* build the gnode tree.  the +1 is for the group node */
      gnode_pool_anchor = gnode_pool = g_new0 (GNode, article_qty + 1);
      groot = build_gnode_hierarchy (group, article_buf, article_qty, &gnode_pool);

      /* calculate out the subthread scores */
      for (i=0; i<article_qty; ++i) {
            article_buf[i]->unread_children = 0;
            article_buf[i]->new_children = 0;
            article_buf[i]->subthread_score = -9999;
      }
      g_node_traverse (groot, G_IN_ORDER, G_TRAVERSE_ALL, -1, calculate_article_header_pane_fields, NULL);

      /* sort the hierarchy */
      sort_hierarchy (group, groot);

      /* build the header pane */
      croot = gtk_ctree_insert_gnode (_article_ctree, NULL, NULL, groot, articlelist_renderer_gnode_func_wrapper, _renderer);

      /* update selection */
      if (sel_articles!=NULL && sel_articles->len)
      {
            int i;
            int node_qty = 0;
            GtkCTreeNode ** nodes = g_newa (GtkCTreeNode*, sel_articles->len);

            for (i=0; i<sel_articles->len; ++i) {
                  const Article * a = ARTICLE(g_ptr_array_index(sel_articles,i));
                  GtkCTreeNode * node = articlelist_get_node_from_message_id (&a->message_id);
                  if (node != NULL)
                        nodes[node_qty++] = node;
            }

            if (node_qty)
                  articlelist_set_selected_nodes_nolock (nodes, node_qty);
      }

      /* finished; display */
      if (expand_all_threads_by_default)
            gtk_ctree_expand_recursive (_article_ctree, croot);
      else
            gtk_ctree_expand (_article_ctree, croot);
      gtk_clist_thaw (_article_clist);
      gtk_widget_queue_resize (GTK_WIDGET(_article_ctree));

      /* OK to follow up on tree-select-row now */
      _articlelist_repopulating = FALSE;

      /* cleanup */
      g_free (gnode_pool_anchor);

      debug_exit ("articlelist_repopulate_nolock");
}

/***
****
****
****  UPDATING EXISTING NODES: THE DIRTY WORK
****
****
***/

static void 
articlelist_update_node_nolock (GtkCTreeNode   * node,
                                const Article  * article)
{
      header_pane_renderer_refresh_node  (_renderer, node, article);
}

/**
 * This is called a few times a second to refresh the rendering of changed articles.
 * These changes are batched up inside `messages_needing_refresh', rather than being
 * processed as they arive, because of the high cost of refreshing on each change
 * event -- for example, just reading an article posts individual events for queueing,
 * then adding the body to the cache, then the article being marked read, then unqueueing.
 */
static int
refresh_dirty_messages (gpointer user_data)
{
      GPtrArray * messageids = messageidset_get_ids (messages_needing_refresh);

      if (messageids->len)
      {
            guint i;
            GPtrArray * nodes;

            /* lock the gui */
            pan_lock ();

            /* see if any of these articles have nodes in the header pane */
            nodes = g_ptr_array_sized_new (messageids->len);
            g_static_mutex_lock (&hash_mutex);
            for (i=0; i<messageids->len; ++i)
            {
                  const PString * mid = (const PString*) g_ptr_array_index (messageids, i);
                  gpointer node = g_hash_table_lookup (message_id_to_node, mid);
                  if (node != NULL)
                        g_ptr_array_add (nodes, node);
            }
            g_static_mutex_unlock (&hash_mutex);

            /* refresh any nodes in our tree */
            if (nodes->len)
            {
                  const gboolean do_freeze = nodes->len > FREEZE_THRESHOLD;

                  if (do_freeze)
                        gtk_clist_freeze (_article_clist);

                  /* update each of the article nodes */
                  for (i=0; i<nodes->len; ++i) {
                        GtkCTreeNode * node = (GtkCTreeNode*) g_ptr_array_index (nodes, i);
                        const Article * article = articlelist_get_article_from_node (node);
                        if (article != NULL)
                              articlelist_update_node_nolock (node, article);
                  }

                  if (do_freeze)
                        gtk_clist_thaw (_article_clist);
            }

            /* unlock the gui */
            pan_unlock ();

            /* cleanup */
            g_ptr_array_free (nodes, TRUE);
            pan_g_ptr_array_foreach (messageids, (GFunc)pstring_free, NULL);
            messageidset_clear (messages_needing_refresh);
      }

      g_ptr_array_free (messageids, TRUE);
      return 1;
}

static GtkCTreeNode*
articlelist_get_node_from_message_id (const PString * mid)
{
      GtkCTreeNode * retval = NULL;

      g_return_val_if_fail (pstring_is_set (mid), NULL);

      g_static_mutex_lock (&hash_mutex);
      retval = g_hash_table_lookup (message_id_to_node, mid);
      g_static_mutex_unlock (&hash_mutex);

      return retval;
}

static void
refresh_articles_from_message_id_array_cb (gpointer call_obj,
                                           gpointer call_arg,
                                           gpointer user_data)
{
      const int qty = GPOINTER_TO_INT (call_arg);
      const PString ** message_ids = (const PString **) call_obj;
      messageidset_add_message_ids (messages_needing_refresh, message_ids, qty);
}

static void
articlelist_articles_changed_cb (gpointer event, gpointer foo, gpointer bar)
{
      const ArticleChangeEvent * e = (ArticleChangeEvent*) event;
      if (e->group == my_group)
            messageidset_add_articles_and_ancestors   (messages_needing_refresh, (const Article**)e->articles, e->article_qty);
}

static void
refilter_threads (Article ** articles, guint article_qty, gpointer unused)
{
      guint i;
      gboolean * old_filter;
      Article ** changed;
      guint changed_qty;

      if (article_qty == 0u)
            return;

      /* refilter the articles, and in doing so,
       * make a list of articles whose `passes' state has changed */
      old_filter = g_new (gboolean, article_qty);
      for (i=0; i!=article_qty; ++i)
            old_filter[i] = articles[i]->passes_filter;
      if (1) {
            Filter * filter = NULL;
            FilterShow show = 0;
            article_toolbar_get_filter (&filter, &show);
            apply_filter_tests (filter, show, articles, article_qty);
            pan_object_unref (PAN_OBJECT(filter));
      }
      changed_qty = 0;
      changed = g_new (Article*, article_qty);
      for (i=0; i<article_qty; ++i)
            if (old_filter[i] != articles[i]->passes_filter)
                  changed[changed_qty++] = articles[i];

      /* remove articles which no longer match */
      if (1) {
            GPtrArray * remove = g_ptr_array_new ();
            for (i=0; i!=changed_qty; ++i)
                  if (!changed[i]->passes_filter)
                        g_ptr_array_add (remove, changed[i]);
            if (remove->len != 0u)
                  header_pane_remove_articles (ARTICLE(g_ptr_array_index(remove,0))->group, remove);
            g_ptr_array_free (remove, TRUE);
      }

      /* update the subthread_scores */
      if (1) {
            /* FIXME: this code is identical to some in repopulate_nolock() and should be shared */
            /* build the gnode tree.  the +1 is for the group node */
            GNode * gnode_pool;
            GNode * gnode_pool_anchor = gnode_pool = g_new0 (GNode, article_qty + 1);
            GNode * groot = build_gnode_hierarchy (articles[0]->group, articles, article_qty, &gnode_pool);

            /* calculate out the subthread scores */
            for (i=0; i<article_qty; ++i) {
                  articles[i]->unread_children = 0;
                  articles[i]->new_children = 0;
                  articles[i]->subthread_score = -9999;
            }
            g_node_traverse (groot, G_IN_ORDER, G_TRAVERSE_ALL, -1, calculate_article_header_pane_fields, NULL);

            g_free (gnode_pool_anchor);
      }

      /* FIXME: add in articles which now match but didn't before
         this will probably involve some sort of progressive repopulate() function */

      /* cleanup */
      g_free (changed);
      g_free (old_filter);
}

static void
rescore_articles (Group * group, Article ** articles, guint article_qty, gpointer unused)
{
      GPtrArray * changed;

      /* rescore the articles. */
            changed = g_ptr_array_new ();
      ensure_articles_scored (group, articles, article_qty, changed);
      article_forall_in_threads ((Article**)changed->pdata, changed->len, GET_WHOLE_THREAD, refilter_threads, NULL);
      g_ptr_array_free (changed, TRUE);
}

static void
scorefile_invalidated_cb (gpointer call_obj_unused, gpointer do_rescore, gpointer user_data_unused)
{
      if (my_group!=NULL && do_rescore!=NULL)
            group_article_forall (my_group, rescore_articles, NULL);
}

/***
****
****  SELECTION / KEYPRESS / BUTTONPRESS HANDLING
****
***/

/**
 * The following buttonpress, keypress, and selection callbacks are all here
 * to interact with each other to do one thing: force this damn GtkCTree to
 * have the UI behavior that we want, which is described in 
 * articlelist_selection_changed()
 */

static void
selection_changed_upkeep (Article ** articles, guint article_qty, gpointer activate_gpointer)
{
      const gboolean activate = activate_gpointer != NULL;

      if (activate && article_qty==1)
            articlelist_activate_article (articles[0]);
      else if (article_qty == 0)
            articlelist_read_article (NULL);
}

static void
articlelist_selection_changed_cb (gpointer call_object,
                                  gpointer call_arg,
                                  gpointer user_data)
{
      gboolean activate = !_modifiers && _mb==1 && _button_click_count >= (single_click_selects_headers ? 2 : 1);

      /* handle the selection change */
      header_pane_forall_selected (selection_changed_upkeep, GINT_TO_POINTER(activate), TRUE);

      /* reset everything for the next time around */
      _button_click_count = -1;
      _modifiers = FALSE;
      _mb = -1;
}

/**
 * FIME: workaround for #82901; remove when fixed & gtk version bumped
 */
static int
resync (gpointer p) {
      GtkCList * clist = _article_clist;
      GTK_CLIST_GET_CLASS (clist)->resync_selection (clist, NULL);
      return 0;
}
static gboolean
articlelist_key_release (GtkWidget * w, GdkEventKey * e, gpointer data) {
      gui_queue_add (resync, NULL);
      return FALSE;
}

static gboolean
articlelist_key_press (GtkWidget      * widget,
                       GdkEventKey    * event,
                       gpointer         data)
{
      GtkCTreeNode * node;
      gboolean retval = FALSE;
      static const gulong modmask = GDK_SHIFT_MASK|GDK_CONTROL_MASK|GDK_MOD1_MASK|GDK_MOD2_MASK|GDK_MOD3_MASK|GDK_MOD4_MASK|GDK_MOD5_MASK;
      const gulong mod = event->state & modmask;

      switch (event->keyval)
      {
            case GDK_leftarrow:
            case GDK_Left:
                  articlelist_collapse_selected_threads ();
                  retval = TRUE;
                  break;

            case GDK_rightarrow:
            case GDK_Right:
                  articlelist_expand_selected_threads ();
                  retval = TRUE;
                  break;

            case GDK_Home:
                  if (mod == GDK_SHIFT_MASK) {
                        /* left shift-home behave as shift-control-home */
                        event->state |= GDK_CONTROL_MASK;
                  }
                  else if (!mod) {
                        /* go to the first thread */
                        node = gtk_ctree_node_nth (_article_ctree, 1);
                        if (node != NULL)
                              select_node_nolock (node);
                        break;
                  }

            case GDK_End:
                  if (mod == GDK_SHIFT_MASK) {
                        /* left shift-end behave as shift-control-end */
                        event->state |= GDK_CONTROL_MASK;
                  }
                  else if (!mod) {
                        /* go to the last thread */
                        node = gtk_ctree_node_nth (_article_ctree, 0);
                        if (node != NULL)
                              node = gtk_ctree_last (_article_ctree, node);
                        if (node != NULL)
                              select_node_nolock (node);
                        break;
                  }
      }

      return retval;
}

static gboolean
articlelist_button_press (GtkWidget* widget, GdkEventButton* bevent)
{
      gboolean retval = FALSE;

      GdkModifierType modifiers = 0;
      gdk_event_get_state ((GdkEvent*)bevent, &modifiers);
      _modifiers = (modifiers & (GDK_SHIFT_MASK || GDK_CONTROL_MASK || GDK_MOD1_MASK || GDK_MOD2_MASK || GDK_MOD3_MASK || GDK_MOD4_MASK || GDK_MOD5_MASK)) != (GdkModifierType)0;

      switch (bevent->button)
      {
            case 1:
            case 2:
                  _mb = bevent->button;
                  _button_click_count = bevent->type==GDK_2BUTTON_PRESS ? 2 : 1;
                  retval = FALSE;
                  break;
            case 3:
                  articlelist_menu_popup_nolock (bevent);
                  retval = TRUE;
                  break;
      }

      return retval;
}

static gboolean select_callback_pending = FALSE;
 
static int
tree_select_row_idle (gpointer data)
{
      Group * group;
      debug_enter ("tree_select_row_idle");

      group = GROUP (data);

      /* let everyone know that the selection has changed */
      pan_callback_call (articlelist_get_selection_changed_callback(),
                     Pan.article_ctree,
                     NULL);

      /* matching unref is in tree_select_row_cb */
      if (group != NULL)
            group_unref_articles (group, NULL);

      /* cleanup */
      select_callback_pending = FALSE;
      debug_exit ("tree_select_row_idle");
      return 0;
}
static void
tree_select_row_cb (GtkCTree     *tree,
                GtkCTreeNode *node,
                int           column,
                gpointer      data)
{
      if (!select_callback_pending && !_articlelist_repopulating)
      {
            select_callback_pending = TRUE;

            /* matching unref is in tree_select_row_idle */
            if (my_group != NULL)
                  group_ref_articles (my_group, NULL);

            gui_queue_add (tree_select_row_idle, my_group);
      }
}

/***
****
****   EXPAND / COLLAPSE THREADS
****
***/

void
articlelist_expand_selected_threads (void)
{
      guint i;
      GPtrArray * a;
      gboolean do_freeze;
      debug_enter ("articlelist_expand_selected_threads");

      /* expand */
      pan_lock ();
      a = articlelist_get_selected_nodes_nolock ();
      do_freeze = a->len > FREEZE_THRESHOLD;
      if (do_freeze)
            gtk_clist_freeze (_article_clist);
      for (i=0; i<a->len; ++i)
            gtk_ctree_expand (_article_ctree, (GtkCTreeNode*)g_ptr_array_index(a,i));
      if (do_freeze)
            gtk_clist_thaw (_article_clist);
      pan_unlock ();

      /* cleanup */
      g_ptr_array_free (a, TRUE);
      debug_exit ("articlelist_expand_selected_threads");
}

void
articlelist_collapse_selected_threads (void)
{
      guint i;
      GPtrArray * a;
      gboolean do_freeze;
      debug_enter ("articlelist_collapse_selected_threads");

      /* collapse */
      pan_lock ();
      a = articlelist_get_selected_nodes_nolock ();
      do_freeze = a->len > FREEZE_THRESHOLD;
      if (do_freeze)
            gtk_clist_freeze (_article_clist);
      for (i=0; i<a->len; ++i)
            gtk_ctree_collapse (_article_ctree, (GtkCTreeNode*)g_ptr_array_index(a,i));
      if (do_freeze)
            gtk_clist_thaw (_article_clist);
      pan_unlock ();

      /* cleanup */
      g_ptr_array_free (a, TRUE);
      debug_exit ("articlelist_collapse_selected_threads");
}

static void
tree_expand_cb (GtkCTree * ctree, GList * list, gpointer user_data)
{
      GtkCTreeNode * node = NULL;
      const Article * article = NULL;
      const PString * message_id = NULL;
      debug_enter ("tree_expand_cb");

      if (list != NULL)
            node = GTK_CTREE_NODE(list);
      if (node != NULL)
            article = articlelist_get_article_from_node (node);
      if (article != NULL)
            message_id = &article->message_id;
      if (message_id != NULL)
            messageidset_add_message_ids (messages_needing_refresh, &message_id, 1);

      debug_exit ("tree_expand_cb");
}

static void
tree_collapse_cb (GtkCTree * tree, GList * list, gpointer user_data)
{
      tree_expand_cb (tree, list, user_data);
}

/***
****  Callbacks
***/

static void
articlelist_groups_removed_cb (gpointer server_data,
                               gpointer groups_array,
                               gpointer unused)
{
      guint i;
      gboolean my_group_removed;
      const GPtrArray * groups;

      /* setup */ 
      groups = (const GPtrArray*) groups_array;

      /* was our group one of the ones removed? */
      my_group_removed = FALSE;
      for (i=0; !my_group_removed && i<groups->len; ++i)
            if (GROUP(g_ptr_array_index(groups,i)) == my_group)
                  my_group_removed = TRUE;

      /* if so, clear */
      if (my_group_removed)
            gui_queue_add ((GSourceFunc)articlelist_set_group, NULL);
}

static void
article_toolbar_user_changed_filter_cb (gpointer event, gpointer foo, gpointer bar)
{
      header_pane_refresh (REFRESH_FILTER);
}

/***
****
***/

static void
article_ctree_destroy_cb (void)
{
      /* stop listening to events */
      pan_callback_remove (acache_get_bodies_added_callback(),
                           refresh_articles_from_message_id_array_cb, NULL);
      pan_callback_remove (acache_get_bodies_removed_callback(),
                           refresh_articles_from_message_id_array_cb, NULL);
      pan_callback_remove (queue_get_message_id_status_changed(),
                           refresh_articles_from_message_id_array_cb, NULL);
      pan_callback_remove (article_toolbar_get_user_changed_filter_callback(),
                           article_toolbar_user_changed_filter_cb, NULL);
      pan_callback_remove (article_get_articles_changed_callback(),
                           articlelist_articles_changed_cb, NULL);
      pan_callback_remove (server_get_groups_removed_callback(),
                           articlelist_groups_removed_cb, NULL);
      pan_callback_remove (articlelist_get_selection_changed_callback(),
                           articlelist_selection_changed_cb, NULL);

      gtk_timeout_remove (refresh_dirty_messages_timeout_id);
}

void
articlelist_reset_style_nolock (void)
{
      header_pane_renderer_reset_style (_renderer);
      header_pane_refresh (REFRESH_TREE);
}

static void
style_set_cb (GtkWidget * w, GtkStyle * style, gpointer user_data)
{
      static gboolean nested = FALSE;

      /* _ The `nested' check is to prevent recursion from
       *   articlelist_reset_style_nolock().
       * _ The GTK_WIDGET_REALIZED() check is to prevent us from
       *   refreshing the header pane every time the user toggles
       *   tabbed pane layout on/off -- reparenting the header pane
       *   widget triggers a style_set signal. (#112812)
       */
      if (GTK_WIDGET_REALIZED(w) && !nested)
      {
            nested = TRUE;
            articlelist_reset_style_nolock ();
            nested = FALSE;
      }
}

/*--------------------------------------------------------------------
 * generate the listing of articles, for the "Articles" tab
 *--------------------------------------------------------------------*/
gpointer
create_articlelist_ctree (void)
{
      int i;
      GtkWidget * w;
      GtkCList * list;
      GtkCTree * tree;
      GtkWidget * s_window;
      GtkWidget * vbox;
      char ** titles = g_newa (char*, articlelist_column_qty);

      messages_needing_refresh = messageidset_new ();
      cur_message_id  = PSTRING_INIT;
      prev_message_id = PSTRING_INIT;

      /* callbacks */
      pan_callback_add (acache_get_bodies_added_callback(),
                        refresh_articles_from_message_id_array_cb, NULL);
      pan_callback_add (acache_get_bodies_removed_callback(),
                        refresh_articles_from_message_id_array_cb, NULL);
      pan_callback_add (queue_get_message_id_status_changed(),
                        refresh_articles_from_message_id_array_cb, NULL);
      pan_callback_add (article_toolbar_get_user_changed_filter_callback(),
                        article_toolbar_user_changed_filter_cb, NULL);
      pan_callback_add (article_get_articles_changed_callback(),
                        articlelist_articles_changed_cb, NULL);
      pan_callback_add (server_get_groups_removed_callback(),
                        articlelist_groups_removed_cb, NULL);
      pan_callback_add (articlelist_get_selection_changed_callback(),
                        articlelist_selection_changed_cb, NULL);
      pan_callback_add (score_get_scorefile_invalidated_callback(),
                        scorefile_invalidated_cb, NULL);

      vbox = gtk_vbox_new (FALSE, GUI_PAD_SMALL);
      gtk_container_set_border_width (GTK_CONTAINER(vbox), GUI_PAD_SMALL);

      /* filter */
      gtk_box_pack_start (GTK_BOX(vbox), article_toolbar_new(), FALSE, FALSE, 0);

      /* get the titles */
      for (i=0; i<articlelist_column_qty; ++i)
            titles[i] = (char*) column_to_title (i);

      clear_hash_table ();

      /* create the widget */
      i = get_column_number_from_column_type (COLUMN_SUBJECT);
      Pan.article_ctree = w = gtk_ctree_new_with_titles (articlelist_column_qty, i, titles);
      _article_ctree = tree = GTK_CTREE(w);
      _article_clist = list = GTK_CLIST(w);
      gtk_clist_set_selection_mode (_article_clist, GTK_SELECTION_MULTIPLE);
      list->button_actions[1] = list->button_actions[0];
      _renderer = header_pane_renderer_new (Pan.window, _article_ctree);

      /* wrap it in a scrolled window */
      s_window = gtk_scrolled_window_new(NULL, NULL);
      gtk_scrolled_window_set_policy (
            GTK_SCROLLED_WINDOW(s_window),
            GTK_POLICY_AUTOMATIC,
            GTK_POLICY_AUTOMATIC);
      gtk_container_add (GTK_CONTAINER(s_window), Pan.article_ctree);

      /* create the right click popup menu */
      threads_popup_menu = menu_create_items (threads_popup_entries,
                                              threads_popup_entries_qty,
                                              "<ThreadView>",
                                              &threads_popup_factory,
                                              NULL);

      /* connect signals */
      g_signal_connect (tree, "tree_expand", G_CALLBACK(tree_expand_cb), NULL);
      g_signal_connect (tree, "tree_collapse", G_CALLBACK(tree_collapse_cb), NULL);
      g_signal_connect (tree, "tree-select-row", G_CALLBACK(tree_select_row_cb), NULL);
      g_signal_connect (tree, "tree-unselect-row", G_CALLBACK(tree_select_row_cb), NULL);
      g_signal_connect (tree, "button_press_event", G_CALLBACK(articlelist_button_press), NULL);
      g_signal_connect (tree, "key_press_event", G_CALLBACK(articlelist_key_press), NULL);
      g_signal_connect (tree, "key_release_event", G_CALLBACK(articlelist_key_release), NULL);
      g_signal_connect (tree, "click_column", G_CALLBACK(column_clicked_cb), NULL);
      g_signal_connect (tree, "destroy", G_CALLBACK(article_ctree_destroy_cb), NULL);
      g_signal_connect (tree, "style_set", G_CALLBACK(style_set_cb), NULL);

      pan_callback_add (group_get_articles_removed_callback(), group_articles_removed_cb, NULL);
      pan_callback_add (group_get_articles_added_callback(), group_articles_added_cb, NULL);

      gtk_box_pack_start (GTK_BOX(vbox), s_window, TRUE, TRUE, 0);

      refresh_dirty_messages_timeout_id = pan_timeout_add (800, refresh_dirty_messages, NULL);

      return vbox;
}

void
articlelist_update_columns (void)
{
      int i;

      g_return_if_fail (_article_clist!=NULL);

      /* FIXME: this could/should also update the column widths ? */

      gtk_clist_freeze (_article_clist);
      for (i=0; i<articlelist_column_qty; ++i)
            gtk_clist_set_column_title (_article_clist, i, column_to_title (i));
      gtk_clist_thaw (_article_clist);
}

/**
***
***  ARTICLE THREADING
***
**/


void
articlelist_set_threaded (gboolean threaded_on)
{
      if (threaded_on != header_pane_is_threaded)
      {
            header_pane_is_threaded = threaded_on;
            header_pane_refresh (REFRESH_TREE);
      }
}

/****
*****
*****  COPY TO FOLDER
*****
****/

static void
header_pane_copy_articles_to_folder (Article ** articles, guint article_qty, gpointer folder_gpointer)
{
      Group * folder = GROUP (folder_gpointer);

      article_copy_articles_to_folder (folder, (const Article**)articles, article_qty);
}

static void
copy_to_folder_cb (gpointer user_data, int index, GtkWidget * w)
{
      Server * server;
      GPtrArray * folders;
      Group * folder;

      /* get the target folder */
      server = serverlist_get_named_server (&INTERNAL_SERVER_NAME);
      folders = server_get_groups (server, SERVER_GROUPS_ALL);
      folder = GROUP (g_ptr_array_index (folders, index));

      header_pane_forall_active (header_pane_copy_articles_to_folder, folder);

      /* cleanup */
      g_ptr_array_free (folders, TRUE);
}

/****
*****
*****   POPUP MENU
*****
****/

static void
refresh_folder_popup_menu (void)
{
      int i;
      Server * server;
      GPtrArray * folders;
      static GtkItemFactoryEntry * entries = NULL;
      static int qty = 0;
      debug_enter ("refresh_folder_popup_menu");

      /* remove any old server buttons */
      if (entries != NULL) {
            gtk_item_factory_delete_entries (threads_popup_factory, qty, entries);
            for (i=0; i<qty; ++i)
                  g_free (entries[i].path);
            g_free (entries);
            entries = NULL;
            qty = 0;
      }

      /* get a list of folders */
      server = serverlist_get_named_server (&INTERNAL_SERVER_NAME);
      folders = server_get_groups (server, SERVER_GROUPS_ALL);

      /* build the ItemFactoryEntries */
      entries = g_new0 (GtkItemFactoryEntry, folders->len);
      for (i=qty=0; i<folders->len; ++i)
      {
            const Group * folder = GROUP(g_ptr_array_index(folders,i));
            const PString name = folder->name;

            /* build an entry: the callback arg is the index into the folders array */
            entries[qty].path = g_strdup_printf ("/_Copy to Folder/%*.*s", name.len, name.len, name.str);
            entries[qty].item_type = NULL;
            entries[qty].callback = copy_to_folder_cb;
            entries[qty].callback_action = i;

            ++qty;
      }

      /* add the servers */
      gtk_item_factory_create_items (threads_popup_factory, qty, entries, NULL);

      /* cleanup */
      g_ptr_array_free (folders, TRUE);

      debug_exit ("refresh_folder_popup_menu");
}

static void
articlelist_menu_popup_nolock (GdkEventButton* bevent)
{
      GtkItemFactory * gif = threads_popup_factory;
      const gboolean have_article = header_pane_has_selection ();

      refresh_folder_popup_menu ();

      menu_set_sensitive (gif, "/Read Article", have_article);
      menu_set_sensitive (gif, "/Save Attachments", have_article);
      menu_set_sensitive (gif, "/Save Attachments As...", have_article);
      menu_set_sensitive (gif, "/Manual Decode...", have_article);
      menu_set_sensitive (gif, "/Download Flagged", have_article);
      menu_set_sensitive (gif, "/Flag", have_article);
      menu_set_sensitive (gif, "/Unflag", have_article);
      menu_set_sensitive (gif, "/Watch Thread", have_article);
      menu_set_sensitive (gif, "/Ignore Thread", have_article);
      menu_set_sensitive (gif, "/View Article's Scores", have_article);
      menu_set_sensitive (gif, "/Create Score", have_article);
      menu_set_sensitive (gif, "/Add Set to Selection", have_article);
      menu_set_sensitive (gif, "/Copy to Folder", have_article);
      menu_set_sensitive (gif, "/Delete", have_article);

      gtk_menu_popup (GTK_MENU (threads_popup_menu),
                  NULL, NULL, NULL, NULL,
                  bevent->button, bevent->time);
}

static void
menu_cb (gpointer user_data, int iaction, GtkWidget * w)
{
        const PanAction action = (PanAction) iaction;
        pan_action_do (action);
}
static int threads_popup_entries_qty = 20;
static GtkItemFactoryEntry threads_popup_entries[] =
{
      /* r */ {N_("/_Read Article"), NULL, articlelist_read_selected, 0, NULL},
      /*   */ {N_("/---"), NULL, NULL, 0, "<Separator>"},
      /* t */ {N_("/Save A_ttachments"), NULL, menu_cb, ACTION_SAVE, NULL},
      /* s */ {N_("/_Save Attachments As..."), NULL, menu_cb, ACTION_SAVE_AS, NULL},
      /* m */ {N_("/_Manual Decode..."), NULL, menu_cb, ACTION_MANUAL_DECODE, NULL},
      /*   */ {N_("/---"), NULL, NULL, 0, "<Separator>"},
      /* o */ {N_("/D_ownload Flagged"), NULL, flagset_flush, 0, NULL},
      /* f */ {N_("/_Flag"), NULL, articlelist_selected_flag_for_dl_nolock, 0, NULL},
      /* u */ {N_("/_Unflag"), NULL, articlelist_selected_unflag_for_dl_nolock, 0, NULL},
      /*   */ {N_("/---"), NULL, NULL, 0, "<Separator>"},
        /* w */ {N_("/_Watch Thread"), NULL, menu_cb, ACTION_SCORE_WATCH_THREAD, NULL},
        /* i */ {N_("/_Ignore Thread"), NULL, menu_cb, ACTION_SCORE_IGNORE_THREAD, NULL},
        /* v */ {N_("/_View Article's Scores"), NULL, menu_cb, ACTION_SCORE_VIEW, NULL},
        /*   */ {N_("/_Create Score"), NULL, menu_cb, ACTION_ADD_TO_SCOREFILE, NULL},
      /*   */ {N_("/---"), NULL, NULL, 0, "<Separator>"},
      /* e */ {N_("/_Add S_et to Selection"), NULL, articlelist_add_set_to_selection_nolock, 0, NULL},
      /*   */ {N_("/---"), NULL, NULL, 0, "<Separator>"},
      /* c */ {N_("/_Copy to Folder"), NULL, NULL, 0, "<Branch>"},
      /*   */ {N_("/---"), NULL, NULL, 0, "<Separator>"},
      /* d */ {N_("/_Delete"), NULL, menu_cb, ACTION_ARTICLE_DELETE, NULL}
};

/***
****  Events
***/

PanCallback*
articlelist_get_selection_changed_callback (void)
{
      static PanCallback * cb = NULL;
      if (cb==NULL) cb = pan_callback_new ();
      return cb;
}

PanCallback*
articlelist_get_group_changed_callback (void)
{
      static PanCallback * cb = NULL;
      if (cb==NULL) cb = pan_callback_new ();
      return cb;
}

static int
fire_group_changed (gpointer group_gp)
{
      Group * group = GROUP(group_gp);

      pan_callback_call (articlelist_get_group_changed_callback(),
                         Pan.article_ctree,
                         group);
      return 0;
}

/***
****
****  REFRESH
****
***/

static int
refresh_mainthread_end (gpointer p)
{
      ArgSet * argset;
      Group * group;
      StatusItem * status;
      GPtrArray * articles;
        int actions;
      debug_enter ("refresh_mainthread_end");

      /* pump out the arguments */
      argset = (ArgSet*) p;
      group = (Group*) argset_get (argset, 0);
      articles = (GPtrArray*) argset_get (argset, 1);
      status = (StatusItem*) argset_get (argset, 2);
      actions = GPOINTER_TO_INT (argset_get (argset, 3));

      /* if we've changed anything at all, we need to update the GUI */
      if (actions & REFRESH_ALL)
      {
            GPtrArray * tmp;
            pan_lock ();
            tmp = _articlelist_get_selected_articles_nolock ();
            articlelist_repopulate_nolock (group, (Article**)articles->pdata, articles->len, tmp);
            g_ptr_array_free (tmp, TRUE);
            pan_unlock ();
      }

      /* cleanup - unref the safe-keeping refs */
      group_unref_articles (group, NULL);

      /* cleanup - done with status-item */
      status_item_set_active (status, FALSE);
      pan_object_unref (PAN_OBJECT(status));
      
      /* cleanup - free the tmp stuff */
      g_ptr_array_free (articles, TRUE);
      argset_free (argset);

      debug_exit ("refresh_mainthread_end");
      return 0;
}

static void*
refresh_worker (void * p)
{
      ArgSet         * argset;
      Group          * group;
      StatusItem     * status;
      GPtrArray      * articles;
        int              actions;
      debug_enter ("refresh_worker");

      /* pump out the arguments */
      argset = (ArgSet*) p;
      group = (Group*) argset_get (argset, 0);
      articles = (GPtrArray*) argset_get (argset, 1);
      status = (StatusItem*) argset_get (argset, 2);
      actions = GPOINTER_TO_INT (argset_get (argset, 3));

      /* make sure the scoring is up-to-date */
      if (actions & REFRESH_SCORE) {
            status_item_emit_status (status, _("Scoring Articles"));
            ensure_articles_scored (group, (Article**)articles->pdata, articles->len, NULL);
      }

      /* filter the articles */
      if (actions & (REFRESH_FILTER | REFRESH_SCORE)) {
            Filter * filter = NULL;
            FilterShow show = 0;
            article_toolbar_get_filter (&filter, &show);
            status_item_emit_status (status, _("Filtering Articles"));
            if (articles->len)
                  apply_filter_tests (filter, show, (Article**)articles->pdata, articles->len);
            pan_object_unref (PAN_OBJECT(filter));
            article_toolbar_set_group_filter (group);
      }

      /* hard work done; back to the main thread */
      status_item_emit_status (status, _("Updating Header Pane..."));
      g_usleep (75000);
      gui_queue_add (refresh_mainthread_end, argset);

      debug_exit ("refresh_worker");
      return NULL;
}

static void
refresh_mainthread_articles (Group * group, Article ** articles, guint article_qty, gpointer actions_gpointer)
{
      char buf[512];
      StatusItem * status;
      GPtrArray * articles_gpa;

      /* create the status-item */
      g_snprintf (buf, sizeof(buf), _("Refreshing Group \"%s\""), group_get_name(group));
      status = status_item_new_with_description (buf);
      status_item_set_active (status, TRUE);

      /* pass the hard work to another thread */
      articles_gpa = g_ptr_array_sized_new (article_qty);
      pan_g_ptr_array_append (articles_gpa, (gpointer*)articles, article_qty);
      run_in_worker_thread (refresh_worker, argset_new4 (group, articles_gpa, status, actions_gpointer));
}

static int
refresh_mainthread_begin (gpointer actions_gpointer)
{
      Group * group;
      debug_enter ("refresh_mainthread_begin");

      _pane_is_about_to_be_rebuilt = FALSE;

      group = my_group;
      if (group != NULL)
      {
            group_ref_articles (group, NULL);
            group_article_forall (group, refresh_mainthread_articles, actions_gpointer);
      }

      debug_exit ("refresh_mainthread_begin");
      return 0;
}

static void
header_pane_refresh (int actions)
{
      if (!_pane_is_about_to_be_rebuilt)
      {
            _pane_is_about_to_be_rebuilt = TRUE;

            gui_queue_add (refresh_mainthread_begin, GINT_TO_POINTER(actions));
      }
}


/***
****
****  SET THE GROUP
****
***/

static void
set_group_field (Group * group)
{
      if (group != my_group)
      {
            Group * old_group = my_group;

            my_group = group;

            /* clear the model */
            if (old_group != NULL)
            {
                  if (mark_read_on_group_exit)
                        group_mark_all_read (old_group, TRUE);

                  file_headers_save (old_group, NULL);
                  server_save_grouplist_if_dirty (old_group->server, NULL);
                  group_unref_articles (old_group, NULL);
            }

            pstring_clear (&prev_message_id);
            pstring_clear (&cur_message_id);

            gui_queue_add (fire_group_changed, my_group);
      }

}

static int
clear_group (gpointer unused)
{
      /* clear the UI */
      gtk_clist_clear (_article_clist);
      clear_hash_table ();

      /* clear the model */
      set_group_field (NULL);

      return 0;
}

static int
set_group_mainthread_end (gpointer p)
{
      ArgSet * argset;
      Group * group;
      StatusItem * status;
      GPtrArray * articles;
      GPtrArray * tmp;
      debug_enter ("set_group_mainthread_end");

      /* pump out the arguments */
      argset = (ArgSet*) p;
      group = (Group*) argset_get (argset, 0);
      status = (StatusItem*) argset_get (argset, 1);
      articles = (GPtrArray*) argset_get (argset, 2);

      /* update the UI */
      pan_lock ();
      tmp = _articlelist_get_selected_articles_nolock ();
      articlelist_repopulate_nolock (group, (Article**)articles->pdata, articles->len, tmp);
      g_ptr_array_free (tmp, TRUE);
      pan_unlock ();

      /* update the model */
      set_group_field (group);
      my_group->loaded_since_last_fetch = TRUE;

      /* no longer using the status */
      status_item_set_active (status, FALSE);
      pan_object_unref (PAN_OBJECT(status));

      /* cleanup memory */
      g_ptr_array_free (articles, TRUE);
      argset_free (argset);

      /* Now that the articlelist is fully loaded,
         get new headers, if required. */
      if (fetch_new_on_group_enter && !group_is_folder(group))
            queue_insert_tasks (g_slist_append(NULL,task_headers_new(group,HEADERS_NEW)), 0);

      debug_exit ("set_group_mainthread_end");
      return 0;
}

static void
set_group_worker_articles (Group * group, Article ** articles, guint article_qty, gpointer user_data)
{
      GPtrArray * articles_gpa;
      ArgSet * argset = (ArgSet*) user_data;
      StatusItem * status = (StatusItem*) argset_get (argset, 1);
      debug_enter ("set_group_worker");

      /* we're switching. all other switch requests: please hold */
      g_static_mutex_lock (&switch_mutex);

      /* massage them into shape */
      status_item_emit_status (status, _("Scoring Articles"));
      ensure_articles_scored (group, articles, article_qty, NULL);

      /* test them against the header pane's filter */
      if (1) {
            Filter * filter = NULL;
            FilterShow show = 0;
            status_item_emit_status (status, _("Filtering Articles"));
            article_toolbar_set_group (group);
            article_toolbar_get_filter (&filter, &show);
            if (article_qty)
                  apply_filter_tests (filter, show, articles, article_qty);
            pan_object_unref (PAN_OBJECT(filter));
      }

      /* add the articles to the argset so that we can pass them around */
      articles_gpa = g_ptr_array_sized_new (article_qty);
      if (article_qty != 0u)
            pan_g_ptr_array_append (articles_gpa, (gpointer*)articles, article_qty);

      /* hard work over, pass back to main thread */
      argset_add (argset, articles_gpa);
      status_item_emit_status (status, _("Updating Header Pane..."));
      g_usleep (75000);
      gui_queue_add (set_group_mainthread_end, argset);

      /* safe to do another switch */
      g_static_mutex_unlock (&switch_mutex);
      debug_exit ("set_group_worker");
}

static void*
set_group_worker (void * p)
{
      ArgSet * argset = (ArgSet*) p;
      Group * group = (Group*) argset_get (argset, 0);
      StatusItem * status = (StatusItem*) argset_get (argset, 1);

      group_ref_articles (group, status);
      group_article_forall (group, set_group_worker_articles, argset);
      return NULL;
}

static int
set_group_mainthread_begin (gpointer p)
{
      char buf[512];
      ArgSet * argset;
      Group * group;
      StatusItem * status;
      debug_enter ("set_group_mainthread_begin");

      /* pump out the arguments */
      argset = (ArgSet*) p;
      group = (Group*) argset_get (argset, 0);

      /* create the status-item */
      g_snprintf (buf, sizeof(buf), _("Loading group \"%s\""), group_get_name(group));
      status = status_item_new_with_description (buf);
      status_item_set_active (status, TRUE);
      argset_add (argset, status);

      /* pass the hard work to another thread */
      run_in_worker_thread (set_group_worker, argset);
      debug_exit ("set_group_mainthread_begin");
      return 0;
}

static int
open_download_dialog (gpointer data)
{
      /* This is a bit ugly */
      group_action_selected_download_dialog ();
      return 0;
}

void
articlelist_set_group (Group * group)
{
      if (group == NULL)
      {
            gui_queue_add (clear_group, NULL);
      }
      else if (!group_is_folder(group)
               && !group->article_qty
               && !group->article_high)
      {
            gui_queue_add (open_download_dialog, NULL);
      }
      else
      {
            gui_queue_add (set_group_mainthread_begin, argset_new1(group));
      }
}

Generated by  Doxygen 1.6.0   Back to index