/** -*- Mode: C++; tab-width: 4 -*-
 * vim: sw=4 ts=4:
 *
 * Gnome Apt package tree display
 *
 * 	(C) 1998 Havoc Pennington <hp@pobox.com>
 * 	    2002-2004 Filip Van Raemdonck <mechanix@debian.org>
 *
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 * 	$Id$
 *
 **/

using namespace std;

////// Ideas for fixing this mess: 
// - Don't have invisible columns; pkgtree.cc should just not 
//   insert them.

#include <algorithm>
#include <math.h>
#include <gdk/gdkkeysyms.h>
#include <libgnome/gnome-config.h>

#include "drawtree.h"
#include "gaptdrawingarea.h"
#include "gaptpixbuf.h"
#include "gnome-apt.h"
#include "pkgtree.h"
#include "pkgutil.h"	/* FIXME: the needed methods belong in GAptCache really */

/* FIXME: remove this */
#define PM_SIZE 8
#define PM_INSET 3
#define EXPANDER_WIDTH (PM_SIZE+PM_INSET*2)

#define DT_COL_GAP 1
#define DT_LABELS 22
#define DT_INDENT 20
#define DT_MARGIN 1
#define DT_ROW_GAP 1
#define DT_ROW_HEIGHT 20

#define toprow_y() (DT_MARGIN * 2 + DT_LABELS)
#define row_offset(row) (toprow_y() + ((row - row_) * (DT_ROW_HEIGHT + DT_ROW_GAP)))

#define DT_COL_MINSIZE 4

static GdkPixbuf* pixbufs[GAPT_PIXBUF_UNKNOWN];
static GAptPixbufType columntypes[GAptPkgTree::Item::InvalidItem] = {
	GAPT_PIXBUF_SECTION, GAPT_PIXBUF_PACKAGE,
	GAPT_PIXBUF_DEPENDS, GAPT_PIXBUF_DEPENDS, GAPT_PIXBUF_RECOMMENDS, GAPT_PIXBUF_SUGGESTS,
	GAPT_PIXBUF_CONFLICTS, GAPT_PIXBUF_REPLACES
};

static GdkColor gcol_broken, gcol_orphan;
#define GCOL_BROKEN "red"
#define GCOL_ORPHAN "#CCFF33"

enum { DT_SELECTION_CHANGED, DT_LAST_SIGNAL };
static guint drawtree_signals[DT_LAST_SIGNAL] = { 0 };

/* Needed for loading and saving settings */

static const char* ColumnNames[] = {
	"Delete", "Install",
	"PackageName",
	"InstalledVersion", "AvailableVersion",
	"Status",
	"Section", "Priority",
	"Description"
};

static int NCols = sizeof (ColumnNames) / sizeof (ColumnNames[0]);

//////// Callback clutter

gint DrawTree::configure_event(GtkWidget* w, GdkEventConfigure* event, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);

  g_return_val_if_fail(data, FALSE);

  return dt->configure(event);
}


gint DrawTree::expose_event(GtkWidget* w, GdkEventExpose* event, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);

  g_return_val_if_fail(data, FALSE);

  return dt->expose(event);
}

gint
DrawTree::scroll_event_cb (GtkWidget* wdg, GdkEventScroll* ev, gpointer data) {
	g_return_val_if_fail (data, FALSE);
	DrawTree* dt = static_cast<DrawTree*> (data);

	return dt->scroll_event (ev);
}

gint DrawTree::button_event(GtkWidget* w, GdkEventButton* event, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);

  g_return_val_if_fail(data, FALSE);

  return dt->button(event);
}


gint DrawTree::motion_event(GtkWidget* w, GdkEventMotion* event, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);

  g_return_val_if_fail(data, FALSE);

  return dt->motion(event);
}

gint DrawTree::leave_event(GtkWidget* w, GdkEventCrossing* event, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);

  g_return_val_if_fail(data, FALSE);

  return dt->leave(event);
}

void DrawTree::realize_cb(GtkWidget* w, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);

  g_return_if_fail(data);

  dt->realize();
}


//////////// End static callbacks


////////// Begin method callbacks

static gint
generic_event (GtkWidget* wdg, GdkEvent* event, gpointer data) {
	ga_debug ("Got unknown event: %d", event->type);
	return FALSE;
}

gint
DrawTree::scroll_event (GdkEventScroll* ev) {
	if (ev->type == GDK_SCROLL) {
		gint step = 0;
		if (ev->direction == GDK_SCROLL_UP) {
			if (visible_nodes_.size() > 1) {
				step = - (visible_nodes_.size() - 2);
			}
		} else if (ev->direction == GDK_SCROLL_DOWN) {
			if (visible_nodes_.size() > 1) {
				step = visible_nodes_.size() - 2;
			}
		}
		scroll_by (step);
		recalc_rows();
		return TRUE;
	}
	return FALSE;
}

void 
DrawTree::realize()
{
  g_return_if_fail(da_->window != 0);
  g_return_if_fail(da_->style != 0);

  // Immediately set background, this just improves cosmetics -
  // otherwise you can see memory garbage for an instant while we
  // complete the pixmap draw

  gdk_window_set_background (da_->window, 
                             &da_->style->base[GTK_STATE_NORMAL]);

}

gint 
DrawTree::configure(GdkEventConfigure* event)
{
  if (da_->allocation.width == pixwidth_ && 
      da_->allocation.height == pixheight_) return FALSE; // nothing to do

	if (pixmap_) {
		g_object_unref (G_OBJECT (pixmap_));
	}

  pixwidth_ = da_->allocation.width;
  pixheight_ = da_->allocation.height;

  pixmap_ = gdk_pixmap_new(da_->window,
                           pixwidth_,
                           pixheight_,
                           -1);
		   
  recalc_widths(); 
  recalc_rows();

  setup_adjustments(); // page size will have changed

  queue_display_update();

  return TRUE;
}

gint
DrawTree::expose (GdkEventExpose* event) {
	gdk_draw_drawable (da_->window, da_->style->fg_gc[GTK_WIDGET_STATE (da_)], pixmap_,
	      event->area.x, event->area.y, event->area.x, event->area.y,
	      event->area.width, event->area.height);

	return FALSE;
}

gint 
DrawTree::button(GdkEventButton* event)
{
  gint r;

  switch (event->type) {
  case GDK_BUTTON_PRESS:
    g_return_val_if_fail (resize_ == 0, FALSE);
    switch (event->button) {
    case 1:
      {
        //    scroll_by(-1);
        r = row_from_coords(static_cast<gint>(event->x), 
                            static_cast<gint>(event->y));
        
        if (r >= 0) 
          {
            gint vrn = r-row_;
            g_assert(static_cast<size_t>(vrn) < visible_nodes_.size());

            gint col = column_from_x(static_cast<gint>(event->x));
            if (col >= 0) 
              {
                if (columns_[col]->style == BoolCol) 
                  {
						if (display_bool (visible_nodes_[vrn], col)) {
							set_bool (visible_nodes_[vrn], col, !get_bool (visible_nodes_[vrn], col));
						}
					} else {
						change_selection (visible_nodes_[vrn]);

						if (columns_[col]->style == TreeCol) {
                    // see if we are on the expander - yes these magic 
                    //  numbers should not be cut-and-pasted all over this
                    //  file. bleh.
							gint start = columns_[col]->x + depths_[vrn] * DT_INDENT;
							gint end = start + EXPANDER_WIDTH;
                    
                    if (static_cast<gint>(event->x) >= start && 
                        static_cast<gint>(event->x) <= end) 
                      {
                        return openclose_node(visible_nodes_[vrn]);
                      }
                  }
					}
              }
          }
        else
          {
            // Perhaps we clicked a column
            // First assemble vector of visible column indices
            vector<Column*>::iterator ci = columns_.begin();
            gushort col = 0;
            vector<gushort> visible;
            while (ci != columns_.end()) 
              {
                if ((*ci)->visible) 
                  {
                    visible.push_back(col);
                  }
                ++ci;
                ++col;
              }

            vector<gushort>::const_iterator vi = visible.begin();
            vector<gushort>::const_iterator prev_vi = visible.end();
            while (vi != visible.end())
              {
                Column* c = columns_[*vi];
                col = *vi;         // variable recycling, evil
                

                GdkRectangle crect;
					crect.x = c->x;
					crect.y = DT_MARGIN;
					crect.width = c->width + c->extra;
					crect.height = DT_LABELS;

                if (col != 0 && 
                    abs(static_cast<gint>(event->x) - crect.x) <= 4)
                  {
                    g_return_val_if_fail(resize_ == 0, true);
                    
                    gint prev = -1;
                    
                    // Find a visible column to the left
                    if (prev_vi != visible.end())
                      prev = *prev_vi;
                    
                    // If none, we can't resize this
                    // Also check that the next column isn't a 
                    //  better match
						if (prev >= 0 &&
						      !((event->x > (gint) crect.x) &&
						        (crect.width < DT_COL_MINSIZE) &&
						        (vi + 1 != visible.end()))) {
                        resize_ = new Resize(prev, col);
                        
                        resize_->xor_gc_ = gdk_gc_new(da_->window);
                        gdk_gc_copy(resize_->xor_gc_, da_->style->white_gc);
                        
                        gdk_gc_set_function(resize_->xor_gc_, GDK_XOR);
                        
                        GdkEventMask emask = 
                          static_cast<GdkEventMask>(GDK_POINTER_MOTION_MASK | GDK_BUTTON_RELEASE_MASK);
                        
                        gdk_pointer_grab (da_->window,
                                          FALSE,
                                          emask,
                                          NULL,
                                          NULL,
                                          event->time);

							GdkCursor* cursor = gdk_cursor_new (GDK_SB_H_DOUBLE_ARROW);
							gdk_window_set_cursor (da_->window, cursor);
							gdk_cursor_unref (cursor);
 
                        draw_resize_line();
                          
#ifdef GNOME_ENABLE_DEBUG
                        ga_debug("Starting resize between col %u and %u\n",
                                resize_->left_, resize_->right_);
#endif
                        return true;
                      }  // If there's a previous
                  } // if it's on the inter-column line
                prev_vi = vi;
                ++vi;
              } // while iterating over visible columns
          } // else if we're resizing

      } // end of switch
      return true;
      break;
    case 2:
      break;
    default:
			ga_debug ("Got GDK_BUTTON_PRESS: %d", event->button);
      break;
    }
    break;
    
  case GDK_2BUTTON_PRESS:
    g_return_val_if_fail (resize_ == 0, FALSE);
    switch (event->button) {
      
    case 1:
      r = row_from_coords(static_cast<gint>(event->x), 
                          static_cast<gint>(event->y));

      if (r >= 0) {
        gint vrn = r-row_;

        return openclose_node(visible_nodes_[vrn]);
      }
      break;

    default:
			ga_debug ("Got GDK_2BUTTON_PRESS: %d", event->button);
      break;
    }
    break;
    
  case GDK_BUTTON_RELEASE:
    {
		ga_debug ("Got GDK_BUTTON_RELEASE: %d", event->button);
      if (resize_ == 0) return false;
      if (event->button != 1) return false;
    
      // We are resizing and we just stopped
      draw_resize_line(); // erase
      gdk_pointer_ungrab(event->time);
      gdk_window_set_cursor(da_->window,
                            NULL);
                      
      delete resize_;
      resize_ = 0;
      queue_display_update();
    }
    break;

  default:
		ga_debug ("Got GDK button event: %d", event->type);
    break;
  }
    
  return false;
}

gint 
DrawTree::consider_key_event(GdkEventKey* event)
{
  return key(event);
}

gint 
DrawTree::key (GdkEventKey* event) {
	gboolean ret = FALSE;

	if (event->keyval == GDK_Up) {
//#error if (event->state & GDK_SHIFT_MASK) { ...
    move_selection(-1);
    return true;
	} else if (event->keyval == GDK_Down) {
    move_selection(1);
    return true;
	} else if (event->keyval == GDK_Return) {
		if (dt_highlight) {
			openclose_node (dt_highlight);
		}
		return true;
	} else if (event->keyval == GDK_Page_Down && visible_nodes_.size() > 1) {
		move_selection ((int) visible_nodes_.size() - 2);
		return true;
	} else if (event->keyval == GDK_Page_Up && visible_nodes_.size() > 1) {
		move_selection (-((int) visible_nodes_.size() - 2));
		return true;
	} else if (!dt_selection.empty()) {
		/* Figure out whether a shortcut key was pressed */
      guint col = 0;
      while (col < columns_.size()) 
        {
			if (columns_[col]->style == BoolCol && event->keyval == columns_[col]->shortcut) {
				/* If it was, walk the tree of selected items ... */
				Nodes::iterator i = dt_selection.begin();
				while (i != dt_selection.end()) {
					/* ... and act upon all interested ones. */
					if (display_bool (*i, col)) {
						set_bool (*i, col, !get_bool (*i, col));
						ret = TRUE;
					}
					++i;
                }
            }
          ++col;
        }
    }

	return ret;
}

static void save_column_widths (const vector<DrawTree::Column*>);

static gboolean
in_collection (Nodes& coll, TreeNode* node) {
	Nodes::iterator i = coll.begin();
	while (i != coll.end()) {
		if (*i == node) {
			return TRUE;
		} else {
			++i;
		}
	}

	return FALSE;
}

gint
DrawTree::motion(GdkEventMotion* event)
{
#if 0
  // This should be in enter_notify, I'm just lazy for now
  if (!GTK_WIDGET_HAS_FOCUS(da_)) 
    gtk_widget_grab_focus(da_);
#endif

  gint x, y;
  GdkModifierType state;
  
  if (event->is_hint)
    gdk_window_get_pointer (event->window, &x, &y, &state);
  else {
    x = static_cast<gint>(event->x);
    y = static_cast<gint>(event->y);
    state = static_cast<GdkModifierType>(event->state);
  }

  if (resize_ == 0)
    {
      gint candidate = row_from_coords(x,y);
      
      if (candidate == prelight_) return false; // already done.
      
      gint oldprelight = prelight_;
      
		if (candidate >= 0) {
        prelight_ = candidate;
        
        update_row(candidate-row_, GTK_STATE_PRELIGHT);
      }
      else {
        prelight_ = -1;
      }
      
      if ((prelight_ != oldprelight) && 
          (oldprelight >= 0) && 
          (oldprelight >= static_cast<gint>(row_))) {
			gint row_index = oldprelight - row_;
			GtkStateType state = GTK_STATE_NORMAL;

			if (in_collection (dt_selection, visible_nodes_[row_index])) {
				state = GTK_STATE_SELECTED;
			}
			update_row (row_index, state);
		}
    }
  else
    {
      g_return_val_if_fail(resize_->left_ < resize_->right_, FALSE);
      g_return_val_if_fail(resize_->right_ < columns_.size(), FALSE);

      Column* left = columns_[resize_->left_];
      Column* right = columns_[resize_->right_];

      draw_resize_line(); // erase

      gint newwidth = x - left->x;
		if (newwidth < DT_COL_MINSIZE) {
			newwidth = DT_COL_MINSIZE;
		}

      left->width = newwidth;

		save_column_widths (columns_);
      recalc_widths();
      draw_resize_line();
      return true;
    }

  return false;
}

gint 
DrawTree::leave (GdkEventCrossing* event) {
	if ((prelight_ >= 0) && (prelight_ >= static_cast<gint> (row_))) {
    update_row(prelight_ - row_, GTK_STATE_NORMAL);
    prelight_ = -1;
  }
  return false;
}

void
DrawTree::draw_resize_line()
{
  g_return_if_fail(resize_ != 0);

  Column* left = columns_[resize_->left_];

  // We draw at the real width, not the "padded with extra" 
  //  width, because the padded width creates a weird 
  //  user interaction experience (line not aligned with pointer)
  gdk_draw_line(da_->window,
                resize_->xor_gc_,
                left->x + left->width,
                0,
                left->x + left->width,
                da_->allocation.height);

  
#ifdef GNOME_ENABLE_DEBUG
  ga_debug("Resize line widths: ");

  vector<Column*>::const_iterator q = columns_.begin();
  while (q != columns_.end()) 
    {

      ga_debug("%d ", (*q)->width);

      ++q;
    }

  ga_debug("\n");
#endif
}

static GHashTable* gc_hash = NULL;

static GdkGC*
drawtree_get_gc (DrawTree* tree, GdkColor* color, gchar* colstr) {
	if (!gc_hash) {
		gc_hash = g_hash_table_new (NULL, NULL);
	}

	GdkGC* hashgc = (GdkGC*) g_hash_table_lookup (gc_hash, color);
	if (hashgc) return hashgc;

	if (!GTK_WIDGET_REALIZED (GTK_WIDGET (tree->widget()))) return NULL;

	hashgc = gdk_gc_new (GTK_WIDGET (tree->widget())->window);
	g_hash_table_insert (gc_hash, color, hashgc);

	gdk_color_parse (colstr, color);
	gdk_colormap_alloc_color (gtk_widget_get_colormap (GTK_WIDGET (tree->widget())), color, FALSE, TRUE);
	gdk_gc_set_background (hashgc, color);

	return hashgc;
}

// FIXME it would be best to eliminate the 'state' argument,
//  and just check prelight_ and selected_node_

void 
DrawTree::update_row(guint vrn, GtkStateType state, bool copy_to_window)
{
  // This can happen if we get an event between cache swaps
  if (vrn >= visible_nodes_.size()) return; 
	Node* node = visible_nodes_[vrn];

  if (update_queued_) return; // we are going to redraw everything anyway

  GdkRectangle rect;
	rect.x = DT_MARGIN;
	rect.y = row_offset (row_ + vrn);
	rect.width = total_width_ - DT_MARGIN * 2;
	rect.height = DT_ROW_HEIGHT;

	GdkGC* bg_gc = da_->style->base_gc[state];
	GdkColor* bgc = &da_->style->base[state];
	ga_debug ("%s state %d", ((GAptPkgTree::Item*) node)->name(), state);

/* FIXME */
	if (((GAptPkgTree::Item*) node)->relation() != GAptPkgTree::Item::CategoryItem &&
	      state != GTK_STATE_PRELIGHT) {
		GAptPkgTree::Pkg* pkgitem = (GAptPkgTree::Pkg*) node;
		pkgCache::PkgIterator i = pkgitem->package (*(pkgitem->tree()->cache()));
		if (MarkRows[0] && pkgitem->tree()->cache()->pkgStatus (i) == GAptCache::StatusNowBroken) {
			bg_gc = drawtree_get_gc (this, &gcol_broken, GCOL_BROKEN);
			bgc = &gcol_broken;
		} else if (MarkRows[1] && pkgitem->Orphaned()) {
			bg_gc = drawtree_get_gc (this, &gcol_orphan, GCOL_ORPHAN);
			bgc = &gcol_orphan;
		}
	}

	gdk_gc_set_foreground (bg_gc, bgc);
	gdk_draw_rectangle (pixmap_, bg_gc, TRUE, rect.x, rect.y, rect.width, rect.height);
	gdk_gc_set_foreground (bg_gc, &da_->style->base[state]);

  /* Draw each label on the row */
  vector<Column*>::iterator ci = columns_.begin();
  gushort remaining = columns_.size();
  gushort col = 0;
  while (ci != columns_.end()) {
    --remaining;
    
    gint inset = 0;
		if ((*ci)->style == TreeCol) {
			inset = depths_[vrn] * DT_INDENT;
		}

    if ((*ci)->visible) 
      {
        
        GdkRectangle crect;
			crect.x = (*ci)->x + inset;
			crect.y = row_offset (vrn + row_) + DT_MARGIN;
			crect.width = (*ci)->width + (*ci)->extra;
			crect.height = DT_ROW_HEIGHT - DT_MARGIN * 2;

        if ((*ci)->style == BoolCol) 
          {
				if (display_bool (node, col)) {
					GtkStateType checkstate = state;
              if (checkstate == GTK_STATE_SELECTED) {
                // you can't select the buttons (you should
                //  be able to prelight them though, this 
                //  is kind of broken.)
                checkstate = GTK_STATE_NORMAL;
              }
              
              static const gint checkbutton_size = 10;
              
              // we really want "whatever this theme uses for 
              //  check items" but hey
              GtkStyle* default_style = gtk_widget_get_default_style();
              
              crect.x += (MAX(crect.width - checkbutton_size, 0))/2;
              crect.y += (MAX(crect.height - checkbutton_size, 0))/2; 
              crect.width = MIN(crect.width, checkbutton_size);
              crect.height = MIN(crect.height, checkbutton_size);
              
              GtkShadowType shadow_type;

				if (get_bool (node, col)) {
					shadow_type = GTK_SHADOW_IN;
				} else {
                shadow_type = GTK_SHADOW_OUT;
				}

					gtk_paint_option (da_->style, pixmap_,
					      checkstate, shadow_type, 0, da_, "radiobutton",
					      crect.x + 1, crect.y + 1,
					      crect.width - 1, crect.height - 1);
            }
          }
        else {        
          if ((*ci)->style == TreeCol)
            {
				/* Leave space for expander even if it's not drawn. */
				gint exp_w;
				GValue* size;

				size = g_new0 (GValue, 1);
				size = g_value_init (size, G_TYPE_INT);

				gtk_widget_style_get_property (da_, "expander_size", size);
				g_assert (G_VALUE_HOLDS_INT(size));

				exp_w = g_value_get_int (size);

				g_value_unset (size);
				g_free (size);

				if (node->expandable()) {
					GtkExpanderStyle exp_style;

					exp_style = GTK_EXPANDER_COLLAPSED;
					if (node->expanded()) {
						exp_style = GTK_EXPANDER_EXPANDED;
					}
					gtk_paint_expander (da_->style, pixmap_, state, 0, da_, "expander",
					      crect.x + PM_INSET, crect.y + crect.height / 2, exp_style);
                }

				crect.x += exp_w;
				crect.width -= exp_w + PM_INSET;
            }

			draw_column (node, col, remaining, state, &crect);
        }
      }
    
    ++ci;
    ++col;
  }

	if (copy_to_window) {
		gdk_draw_drawable (da_->window, da_->style->fg_gc[GTK_WIDGET_STATE (da_)], pixmap_,
		      rect.x, rect.y, rect.x, rect.y, rect.width, rect.height);
	}
}

gint
DrawTree::row_from_coords (gint x, gint y) {
	gint onerow = DT_ROW_HEIGHT + DT_ROW_GAP;

  if (toprow_y() > y) return -1; 

  guint n_above = (y - toprow_y()) / onerow;

  if (visible_nodes_.size() <= n_above) return -1; // we're off the bottom
  
  guint candidate = row_ + n_above;

  // compute row rectangle
  GdkRectangle rect;
	rect.x = DT_MARGIN;
	rect.y = row_offset (candidate);
	rect.width = total_width_ - DT_MARGIN * 2;
	rect.height = DT_ROW_HEIGHT;

  if ( (y > rect.y) &&
       (y < rect.y + rect.height) &&
       (x > rect.x) &&
       (x < rect.x + rect.width) ) {
    return candidate;
  }
  else return -1;
}

gint 
DrawTree::column_from_x(gint x)
{
  vector<Column*>::iterator j = columns_.begin();

  gint last_col = -1;

  while (j != columns_.end()) {
    if ( ! (*j)->visible) 
      ++j;
    else if ((*j)->x > x) 
      break;
    else 
      {
        ++last_col;
        ++j;
      }
  }
  
  return last_col;
}

// -1 for not visible
gint 
DrawTree::row_index(Node* n)
{
  // optimize some common cases
  if (n == 0) return -1;
  if (n->hidden()) return -1;

  // This is ridiculously inefficient O(n) crap, 
  //  but works for now. never more than 50 or so items
  //  in the vector.
	size_t i = 0;
	while (i < visible_nodes_.size()) {
		if (visible_nodes_[i] == n) return i;
    ++i;
  }
  return -1;
}

void
DrawTree::change_selection (Node* newnode, bool shutdown) {
  if (newnode == selected_node_) return;

	if (selected_node_ != 0 && !shutdown) {
		gint oldrow = row_index (selected_node_);
    if (oldrow >= 0) {
      update_row(oldrow, 
                 (oldrow == prelight_) ? GTK_STATE_PRELIGHT : GTK_STATE_NORMAL);
    }
    selected_node_ = 0;
  }

	selected_node_ = newnode;
	dt_highlight = newnode;

  if (selected_node_ != 0) {
#warning FIXME temporary
		dt_selection.clear();
		dt_selection.push_back (selected_node_);
    gint newrow = row_index(selected_node_);
    if (newrow >= 0) {
      update_row(newrow,GTK_STATE_SELECTED);      
    }
  }

	g_signal_emit (G_OBJECT (widget()), drawtree_signals[DT_SELECTION_CHANGED], 0);
}

/* FIXME: ugly hack */
int
DrawTree::focus_node (Node* node, bool scroll) {
	focus_node_ = node;
	recalc_rows();
	focus_node_ = 0;

	int scrollby = (int) focus_index_ - (int) row_;
	focus_index_ = 0;
	if (!scroll) {
		return scrollby + (int) row_;
	}

	if (scrollby < 0 || scrollby >= max_rows_onscreen()) {
		scroll_by (scrollby);
	}
	return -1;
}

bool
DrawTree::openclose_node(Node* node)
{
  g_return_val_if_fail(node != 0, false);

  bool changed = false;

  if (node->expandable()) {
    if (node->expanded()) {
			node->collapse();
			changed = true;
    }
    else {
      node->expand();
      changed = true;
    }
  }

	if (changed) {
		if (dt_highlight == node) {
			focus_node (dt_highlight);
		}
		queue_recalc_rows();
	}

  return changed;
}

void
DrawTree::move_selection (gint steps, SelectionMode mode) {
  g_return_if_fail(steps != 0);

	int vsz = visible_nodes_.size();

  if (selected_node_ == 0)
    {
		/* FIXME:
		 * This path should disappear; keep it for now as temporarily the
		 * highlighted node can remain the same as what the selected node
		 * was; incidentally this code does The Right Thing if there's no
		 * selection.
		 */
      if (visible_nodes_.empty()) 
        return;
      else 
        {
			/* Select something, to make the next move do a more sensible action */
			guint middle = vsz / 2;
			change_selection (visible_nodes_[middle]);
          return;
        }      
      g_assert_not_reached();
	} else {
		gint hlindex = focus_node (dt_highlight, false);
		gint newindex = hlindex + steps;

		if ((newindex - (int) row_) < 0 || (newindex - (int) row_) >= (vsz - 1)) {
			ga_debug ("offscreen, hlindex %d steps %d row_ %d newindex %d", hlindex, steps, row_, newindex);
			/* Target is outside visible range */
			if (newindex < 0) {
				steps -= newindex;
			} else if (newindex >= (int) nrows_) {
				steps -= (newindex + 1 - nrows_);
			} else if (hlindex < (int) row_ && newindex < (int) row_) {
				scroll_by (newindex - row_ - steps);
			} else if (newindex > (int) row_ + vsz) {
				scroll_by (newindex - (row_ + vsz - 2) - steps);
			}

			if (steps != 0) {
				scroll_by (steps);
				hlindex = focus_node (selected_node_, false);
				newindex = hlindex + steps;

				if (!visible_nodes_.empty() && mode != SelKeep) {
					int nnew = hlindex + steps - row_;
					change_selection (visible_nodes_[nnew]);
				}
			}
			return;
		}

          // if we didn't return, we're moving to an on-screen
          //  location.
		g_return_if_fail (newindex >= 0);
		g_return_if_fail (newindex < vsz);

		int nnew = newindex - row_;
		ga_debug ("onscreen, visible %d goto %d", visible_nodes_.size(), nnew);
		change_selection (visible_nodes_[nnew]);
		return;
	}
}

DrawTree::Node* 
DrawTree::selected_node()
{
  return selected_node_;
}

const char*
DrawTree::get_column_name (ColumnType ct, bool small = true) {
	if (ct == ColumnDelete) {
		if (small) return _("D");
		else return _("Delete");
	} else if (ct == ColumnInstall) {
		if (small) return _("I");
		else return _("Install");
	} else if (ct == ColumnName) {
		return _("Package Name");
	} else if (ct == ColumnCurrent) {
		if (small) return _("Inst. Vers.");
		else return _("Installed Version");
	} else if (ct == ColumnAvailable) {
		if (small) return _("Avail. Vers.");
		else return _("Available Version");
	} else if (ct == ColumnStatus) {
		return _("Status");
	} else if (ct == ColumnSection) {
		return _("Section");
	} else if (ct == ColumnPriority) {
		return _("Priority");
	} else if (ct == ColumnDescription) {
		return _("Description");
	} else {
		return "";
	}
}

static DrawTree::ColumnType
string_to_column (const char* s) {
	int i = 0;

	while (i < NCols) {
		if (strcmp (s, ColumnNames[i]) == 0) {
			return static_cast<DrawTree::ColumnType> (i);
		}

		++i;
	}

	return DrawTree::ColumnTypeEnd;
}

static DrawTree::Column*
find_column (vector<DrawTree::Column*> cols, DrawTree::ColumnType t) {
	guint i = 0;

	while (i < cols.size()) {
		if (cols[i]->getType() == t) {
			ga_debug ("found column: %s", DrawTree::get_column_name (t));
			return cols[i];
		}

		++i;
	}
	ga_debug ("didn't find column: %s", DrawTree::get_column_name (t));

	return 0;
}

static void
load_column_widths (vector<DrawTree::Column*>& cols) {
	gpointer config_iterator;
	guint loaded = 0;

	config_iterator = gnome_config_init_iterator ("/gnome-apt/ColumnWidths");

	if (config_iterator) {
		gchar *col, *width;

		loaded = 0;
		while ((config_iterator = gnome_config_iterator_next (config_iterator, &col, &width))) {
			if (!width || !col) {
				g_free (width);
				g_free (col);
				continue;
			}

			DrawTree::Column* found = find_column (cols, string_to_column (col));
			if (found) {
				gint wid = atoi (width);
				found->setWidth (wid);
			}
			g_free (width); width = 0;
			g_free (col); col = 0;

			++loaded;
		}
	}

	if (loaded != static_cast<guint> (DrawTree::ColumnTypeEnd)) {
		/* Maybe the user just changed gnome-apt versions, or we are otherwise
		 * weirded up.
		 * Clean the section - otherwise an old entry could remain forever and
		 * keep screwing us up in the future.
		 */
		gnome_config_clean_section ("/gnome-apt/ColumnWidths");
		gnome_config_sync();

		int i = 0;
		while (i < DrawTree::ColumnTypeEnd) {
			DrawTree::Column* found = find_column (cols, (DrawTree::ColumnType) i);

			/* Fill in defaults for empty columns */
			if (!found || found->width == DT_COL_MINSIZE) {
				gint def = 50;

				if (i == DrawTree::ColumnDelete || i == DrawTree::ColumnInstall) {
					def = 18;
				} else if (i == DrawTree::ColumnName) {
					def = 140;
				}

				if (found) {
					found->setWidth (def);
				}
			}
			++i;
		}
	}
}

/* FIXME: collapse load_column_* functions */
static void
load_column_visibility (vector<DrawTree::Column*>& cols) {
	gpointer config_iterator;
	guint loaded = 0;

	config_iterator = gnome_config_init_iterator ("/gnome-apt/ColumnVisibility");

	if (config_iterator) {
		gchar *col, *visibility;

		loaded = 0;
		while ((config_iterator = gnome_config_iterator_next (config_iterator, &col, &visibility))) {
			if (!visibility || !col) {
				g_free (visibility);
				g_free (col);
				continue;
			}

			DrawTree::Column* found = find_column (cols, string_to_column (col));
			if (found) {
				if (!g_ascii_strcasecmp (g_strstrip (visibility), "false")) {
					found->setVisible (false);
				}
			}
			g_free (visibility); visibility = 0;
			g_free (col); col = 0;

			++loaded;
		}
	}

	if (loaded != static_cast<guint> (DrawTree::ColumnTypeEnd)) {
		/* Maybe the user just changed gnome-apt versions, or we are otherwise
		 * weirded up.
		 * Clean the section - otherwise an old entry could remain forever and
		 * keep screwing us up in the future.
		 */
		gnome_config_clean_section ("/gnome-apt/ColumnVisibility");
		gnome_config_sync();

		/* No need to set defaults, the Column constructor does so */
	}
}

static void
load_column_order (vector<DrawTree::ColumnType>& columns) {
	gpointer config_iterator;
	guint loaded = 0;

	config_iterator = gnome_config_init_iterator ("/gnome-apt/ColumnOrder");

	if (config_iterator) {
		gchar *col, *pos;
		columns.reserve (DrawTree::ColumnTypeEnd);

		loaded = 0;
		while ((config_iterator = gnome_config_iterator_next (config_iterator, &col, &pos))) {
			if (!pos || !col) {
				g_free (pos);
				g_free (col);
				continue;
			}

			DrawTree::ColumnType ct = string_to_column (col);
			gint index = atoi (pos);

			g_free (pos); pos = 0;
			g_free (col); col = 0;

			/* This can happen if the user mangles the config file */
			if (static_cast<guint> (index) >= columns.size()) continue;

			columns[index] = ct;
			++loaded;
		}
	}

	if (loaded != static_cast<guint> (DrawTree::ColumnTypeEnd)) {
		/* Either there was no saved order, or something is busted - use
		 * default order.
		 * Clean the section - otherwise an old entry could remain forever and
		 * keep screwing us up in the future.
		 */
		gnome_config_clean_section ("/gnome-apt/ColumnOrder");
		gnome_config_sync();

		ga_debug ("clearing order, got only %d (wanted %d)", loaded, DrawTree::ColumnTypeEnd);
		columns.clear();
		int i = 0;
		while (i < DrawTree::ColumnTypeEnd) {
			columns.push_back (static_cast<DrawTree::ColumnType> (i));
			++i;
		}
	}
	g_return_if_fail (columns.size() == static_cast<guint> (DrawTree::ColumnTypeEnd));
}

static const char*
column_to_string(DrawTree::ColumnType ct) {
	gint i = static_cast<int> (ct);

	g_return_val_if_fail (i < static_cast<int> (DrawTree::ColumnTypeEnd), 0);
	g_return_val_if_fail (i < NCols, 0);

	return ColumnNames[i];
}

static void
save_column_visibility (const vector<DrawTree::Column*> cols) {
	vector<DrawTree::Column*>::const_iterator i = cols.begin();
	while (i != cols.end()) {
		const gchar* s = column_to_string ((*i)->getType());

		if (!s) {
			g_warning ("Failed to get column label for type %d - this shouldn't happen!", (*i)->getType());
			continue;
		}

		gchar key[256];
		g_snprintf (key, 255, "/gnome-apt/ColumnVisibility/%s", s);
		gnome_config_set_bool (key, (*i)->isVisible());

		++i;
	}

	gnome_config_sync();
}

static void
save_column_widths (const vector<DrawTree::Column*> cols) {
	vector<DrawTree::Column*>::const_iterator i = cols.begin();
	while (i != cols.end()) {
		const gchar* s = column_to_string ((*i)->getType());

		if (!s) {
			g_warning ("Failed to get column label for type %d - this shouldn't happen!", (*i)->getType());
			continue;
		}

#ifdef GNOME_ENABLE_DEBUG
		ga_debug ("Saving column width %d for %s", (*i)->getWidth());
#endif
		gchar key[256];
		g_snprintf (key, 255, "/gnome-apt/ColumnWidths/%s", s);
		gnome_config_set_int (key, (*i)->getWidth());
#ifdef GNOME_ENABLE_DEBUG_ANNOYING
		ga_debug ("Saved %s - %d\n", key, (*i)->getWidth());
#endif

		++i;
	}

	gnome_config_sync();
}

static void
model_changed_cb (GObject* obj, DrawTree* tree) {
	tree->queue_recalc_rows (false);
}

static void
state_changed_cb (GObject* obj, DrawTree* tree) {
	tree->queue_recalc_rows (true);
}

void
selection_lost_cb (GObject* obj, DrawTree* tree) {
	if (tree) {
		tree->change_selection ((TreeNode*) 0, true);
	}
}

DrawTree::DrawTree (GAptPkgTree* tree) :
      dt_model (tree), da_ (0), pixmap_ (0),
      update_queued_ (false), dt_coltypes (ColumnTypeEnd), focus_node_ (0),
      focus_index_ (0), total_width_ (0), recalc_rows_queued_ (false), row_ (0),
      nrows_ (0), selected_node_ (0), dt_highlight (0), prelight_ (-1), gc_ (0),
    hadjustment_(0),
    vadjustment_(0),
    pixwidth_(0),
    pixheight_(0),
    resize_(0),
    update_idle_id_(0),
    recalc_idle_id_(0)
{
	g_assert (tree);

	MarkRows[0] = true, MarkRows[1] = false;

	/* FIXME: should not keep a separate cachecontrol object */
	dt_cachectrl = GAPT_CACHE_CONTROL (gapt_cache_control_new (gnome_apt_cache_file()));

	/* Needed for proper initialization */
	dt_selection.clear();

	da_ = gapt_drawing_area_new(); 
	GTK_WIDGET_SET_FLAGS (da_, GTK_CAN_FOCUS);
	gtk_widget_set_events (da_, GDK_EXPOSURE_MASK | GDK_BUTTON_PRESS_MASK |
	      GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK |
	      GDK_POINTER_MOTION_HINT_MASK | GDK_LEAVE_NOTIFY_MASK);
	g_signal_connect (G_OBJECT (da_), "realize", G_CALLBACK (realize_cb), this);
	g_signal_connect (G_OBJECT (da_), "configure_event", G_CALLBACK (configure_event), this);
	g_signal_connect (G_OBJECT (da_), "expose_event", G_CALLBACK (expose_event), this);
	g_signal_connect (G_OBJECT (da_), "button_press_event", G_CALLBACK (button_event), this);
	g_signal_connect (G_OBJECT (da_), "button_release_event", G_CALLBACK (button_event), this);
	g_signal_connect (G_OBJECT (da_), "motion_notify_event", G_CALLBACK (motion_event), this);
	g_signal_connect (G_OBJECT (da_), "leave_notify_event", G_CALLBACK (leave_event), this);
	g_signal_connect (G_OBJECT (da_), "scroll_event", G_CALLBACK (scroll_event_cb), this);
#ifdef GNOME_ENABLE_DEBUG
	g_signal_connect (G_OBJECT (da_), "event", G_CALLBACK (generic_event), this);
#endif

	drawtree_signals[DT_SELECTION_CHANGED] = g_signal_lookup ("selection-changed", GAPT_DRAWING_AREA_TYPE);

	load_column_order (dt_coltypes);
	load_column_attrs();

	g_signal_connect (G_OBJECT (tree->gaptObject()), "model-changed", G_CALLBACK (model_changed_cb), this);
	g_signal_connect (G_OBJECT (tree->gaptObject()), "state-changed", G_CALLBACK (state_changed_cb), this);
	g_signal_connect (G_OBJECT (tree->gaptObject()), "selection-lost", G_CALLBACK (selection_lost_cb), this);
}

void
DrawTree::load_column_attrs (void) {
	init_columns (dt_coltypes);
#if GNOME_ENABLE_DEBUG
	for (guint i = 0; i < columns_.size(); ++i) {
		ga_debug ("have column %ud: %s", i, columns_[i]->getName());
	}
#endif
	load_column_widths (columns_);
	load_column_visibility (columns_);
	recalc_widths();
	queue_display_update();
}

DrawTree::~DrawTree (void) {
	if (pixmap_) {
		g_object_unref (G_OBJECT (pixmap_));
	}

	for (int i = 0; i < GAPT_PIXBUF_UNKNOWN; ++i) {
		if (pixbufs[i]) {
			g_object_unref (G_OBJECT (pixbufs[i]));
		}
	}

  vector<Column*>::iterator i = columns_.begin();
  while (i != columns_.end()) {
    delete *i;
    ++i;
  }

  if (recalc_idle_id_ != 0)
    gtk_idle_remove(recalc_idle_id_);

	if (update_idle_id_) {
		gtk_idle_remove (update_idle_id_);
	}
}

gint 
DrawTree::update_idle(gpointer data)
{
  DrawTree* tree = static_cast<DrawTree*>(data);
  g_return_val_if_fail(tree != 0, FALSE);

  tree->do_display_update();

  tree->update_idle_id_ = 0;
	ga_debug ("update_idle done");
  return FALSE; // remove idle
}

void 
DrawTree::queue_display_update()
{
  if (!update_queued_) { 
    update_idle_id_ = gtk_idle_add(update_idle, this); 
    update_queued_ = true;
  }
}


gint 
DrawTree::recalc_rows_idle(gpointer data)
{
  DrawTree* tree = static_cast<DrawTree*>(data);
  g_return_val_if_fail(tree != 0, FALSE);

  // If recalc_rows got called in between adding the idle
  //  and calling the idle, just return. There's a race
  //  condition with display update that can cause this.
  if (!tree->recalc_rows_queued_) return FALSE; 

  tree->recalc_rows();

  tree->recalc_idle_id_ = 0;

  return FALSE; // remove idle
}

void
DrawTree::queue_recalc_rows (bool statechanged) {
	if (statechanged) {
		size_t i = 0;
		while (i < visible_nodes_.size()) {
			if (((GAptPkgTree::Item*) visible_nodes_[i])->relation() == GAptPkgTree::Item::PackageItem) {
				bool expstate = visible_nodes_[i]->expanded();
				visible_nodes_[i]->collapse();
				if (expstate) {
					visible_nodes_[i]->expand();
				}
			}
			++i;
		}
	}

	if (!recalc_rows_queued_) {
    recalc_idle_id_ = gtk_idle_add(recalc_rows_idle, this); 
    recalc_rows_queued_ = true;
  }

	queue_display_update();
}

void 
DrawTree::do_display_update()
{
  update_queued_ = false;

  if (recalc_rows_queued_) {
    // This saves us from a race condition where we get here without
    // recalcing the rows.
    recalc_rows();
  }

  gc_ = da_->style->black_gc;

  GdkRectangle rect;
  rect.x = 0;
  rect.y = 0;
  rect.width = da_->allocation.width;
  rect.height = da_->allocation.height;

  // draw background
  gdk_draw_rectangle (pixmap_,
                      da_->style->base_gc[GTK_STATE_NORMAL],
                      TRUE,
                      rect.x, rect.y, rect.width, rect.height);

  // draw column titles

  vector<Column*>::iterator ci = columns_.begin();
  while (ci != columns_.end()) {    
    if ((*ci)->visible) {

      GdkRectangle crect;
      crect.x = (*ci)->x;
			crect.y = 0;
			crect.width = (*ci)->width + (*ci)->extra + 2 * DT_MARGIN;
			crect.height = toprow_y();

			gtk_paint_box (da_->style, pixmap_, GTK_STATE_INSENSITIVE, GTK_SHADOW_OUT, 0,
			      da_, "apt_titles", crect.x, crect.y, crect.width, crect.height);

      // leave an edge around the string
			crect.x += 3 + DT_MARGIN;
			crect.y += 2 + DT_MARGIN;
			crect.width -= (6 + 2 * DT_MARGIN);
			crect.height -= (4 + 2 * DT_MARGIN);

			gdk_gc_set_clip_rectangle (da_->style->black_gc, &crect);
			PangoLayout* pl = gtk_widget_create_pango_layout (da_, (*ci)->getName());
			gdk_draw_layout (pixmap_, da_->style->black_gc, crect.x, crect.y, pl);
			g_object_unref (pl);
			gdk_gc_set_clip_rectangle (da_->style->black_gc, 0);
    }
    
    ++ci;
  }

  // Draw rows 
  
  g_assert(visible_nodes_.size() == depths_.size());

  size_t ni = 0;
  while (ni < visible_nodes_.size()) {
		GtkStateType nstate = GTK_STATE_NORMAL;

		if (in_collection (dt_selection, visible_nodes_[ni])) {
			nstate = GTK_STATE_SELECTED;
		} else if (prelight_ == (gint) (ni + row_)) {
      nstate = GTK_STATE_PRELIGHT;
    }
    update_row(ni,
               nstate,
               false);
    ++ni;
  }

	/* Copy to window */
	gdk_draw_drawable (da_->window, da_->style->fg_gc[GTK_WIDGET_STATE (da_)], pixmap_,
	      rect.x, rect.y, rect.x, rect.y, rect.width, rect.height);
}

void
DrawTree::set_visible (ColumnType ct, bool visibility) {
	gushort col = type_column (ct);
	g_return_if_fail (col < columns_.size());
	columns_[col]->setVisible (visibility);

	save_column_visibility (columns_);
	recalc_widths();
	queue_display_update();
}

bool
DrawTree::is_visible (ColumnType ct) const {
	Column* found = find_column (columns_, ct);
	g_return_val_if_fail (found, true);

	return found->isVisible();
}

DrawTree::Column::Column (ColumnType t, ColumnStyle s, string n, char sc) :
      name (n), type (t), style (s), width (DT_COL_MINSIZE), extra (0), x (0), visible (true), shortcut (sc) {}

void
DrawTree::init_columns (const vector<ColumnType>& types) {
	vector<Column*>::iterator j = columns_.begin();
	while (j != columns_.end()) {
		delete *j;
		++j;
	}
	columns_.clear();

	columns_.reserve (types.size());

	vector<ColumnType>::const_iterator i = types.begin();
	while (i != types.end()) {
		char shortcut = 0;
		ColumnStyle style;

		if ((*i) == ColumnDelete) {
			style = BoolCol;
			shortcut = '-';
		} else if ((*i) == ColumnInstall) {
			style = BoolCol;
			shortcut = '+';
		} else {
			if ((*i) == ColumnName) {
				style = TreeCol;
			} else {
				style = DrawCol;
			}
			shortcut = 0;	/* for all but bool */
		}

		Column* c = new Column ((*i), style, get_column_name (*i), shortcut);

		columns_.push_back (c);
		++i;
	}
}

void
DrawTree::set_column(gushort col, ColumnStyle style)
{
  g_return_if_fail(col < columns_.size());
  columns_[col]->style = style;
  
  recalc_widths();

  queue_display_update();
}

// This function is so fucked up. 
// Need to fix the whole columns thing.
void
DrawTree::recalc_widths (void) {
	int avail = da_->allocation.width;
	int total = 0;
  int nvisible = 0;

  vector<Column*>::iterator j = columns_.begin();
  while (j != columns_.end()) {
    if ((*j)->visible) {
      ++nvisible;
			total += (*j)->width;
		}
		(*j)->extra = 0;
		++j;
  }

	total += DT_COL_GAP * nvisible;
	total_width_ = total;

	ga_debug ("total %d avail %d extra %d", total, avail, avail - total);
	if (total < avail) {
		const int extra = (avail - total);

    Column* rightmost = 0;
    vector<Column*>::iterator i = columns_.begin();
    while (i != columns_.end()) 
      {
        if ((*i)->visible) 
          {
            // This is just too weird when resizing columns
#if 0
            double ratio = double((*i)->width)/total;
            (*i)->extra = int(extra*ratio+0.5); // .5 to round
#else
            rightmost = *i;
#endif            
          }
        ++i;
      }
    if (rightmost != 0) rightmost->extra = extra;

		total_width_ = avail;
	} else if (total > avail) {
		/* FIXME: do scrollbar stuff */
		g_warning ("Hscrollbar");
	}

	int x = 0;

  vector<Column*>::iterator i = columns_.begin();
  while (i != columns_.end()) {
    if ((*i)->visible) {
			(*i)->x = x;
			x += (*i)->width + (*i)->extra + DT_COL_GAP;
    }
    else ((*i)->x = 0); // so we don't get weirdo garbage
    ++i;
  }
}

gint
DrawTree::max_rows_onscreen (void) {
	guint each_row = DT_ROW_HEIGHT + DT_ROW_GAP;
	guint space = da_->allocation.height - toprow_y();

  if (space < 0) space = 0; // negative space means no space
  
  guint max = space/each_row;
	if (space%each_row) {
  max += 1; // partial row on the bottom
	}

  return max;
}

float
DrawTree::exact_rows_onscreen (void) {
	float each_row = DT_ROW_HEIGHT + DT_ROW_GAP;
	float space = da_->allocation.height - toprow_y();

  if (space < 0) space = 0; // negative space means no space
  
  return space/each_row;
}

void
DrawTree::recalc_rows()
{
  recalc_rows_queued_ = false;

  visible_nodes_.clear();
  depths_.clear();

  guint max = max_rows_onscreen();

  // Unfortunately we are now going to iterate over the whole freaking
  //  tree, but it saves us ever doing it again.
	nrows_ = 0;
	vector<TreeNode*>::iterator i = dt_model->begin(), e = dt_model->end();
	bool keep_hl = recalc_level (i, e, 0);

  // you can go at most 1 past the end
  if (row_ >= nrows_) row_ = nrows_;
/*	if (nrows_ <= max && row_ != 0) {
		scroll_by (0 - nrows_);
	}*/

	if (!keep_hl) {
		dt_highlight = 0;
	} else if (dt_highlight->hidden()) {
		/* If the focused node got hidden, focus it's parent. */
		/* FIXME */
		change_selection (dt_highlight->parent());
	}

	Nodes::iterator j = dt_selection.begin();
	while (j != dt_selection.end()) {
		if ((*j)->hidden()) {
			dt_selection.erase (j);
		} else {
			++j;
		}
	}

  setup_adjustments();

  g_assert(depths_.size() == visible_nodes_.size());
  g_assert(visible_nodes_.size() <= max);

#ifdef GNOME_ENABLE_DEBUG
  ga_debug(" ** recalc_rows: top: %u, visible: %u  total: %u\n", 
          row_, visible_nodes_.size(), nrows_);
#endif
}

bool
DrawTree::recalc_level (Nodes::iterator& i, Nodes::iterator& e, gint depth) {
	bool keep_hl = false;
	guint max = max_rows_onscreen();

	while (i != e) {
		Node* n = *i;
#ifdef GNOME_ENABLE_DEBUG
		if (root == 0) g_warning ("Null node got in the tree!");
#endif

		if (n == dt_highlight) keep_hl = true;
		if (n == focus_node_) {
			focus_index_ = nrows_;
		}

		if (!n->hidden()) {
			++nrows_;

			if (nrows_ > row_ && visible_nodes_.size() < max) {
				/* This one is visible, add it */
				visible_nodes_.push_back (n);
				depths_.push_back (depth);
			}

			if (n->expanded()) {
				vector<TreeNode*>::iterator i = n->begin(), e = n->end();
				keep_hl |= recalc_level (i, e, depth + 1);
			}
		}
		++i;
	}

	return keep_hl;
}

void
DrawTree::scroll_by (gint rows) {
	if (rows) {
		int endlimit, introw;

		introw = row_;
		/* +1 is for the possible partial bottom row */
		endlimit = nrows_ - max_rows_onscreen() + 1;

		introw += rows;
		if (introw > endlimit) {
			introw = endlimit;
		}
		if (introw < 0) {
			introw = 0;
		}

		if (static_cast<gint> (row_) != introw) {
			row_ = introw;
			prelight_ = -1;

			queue_recalc_rows();
		}
	}
}

void 
DrawTree::set_hadjustment(GtkAdjustment* a)
{
  hadjustment_ = a;

  // FIXME need to save id and disconnect
	g_signal_connect (G_OBJECT (hadjustment_), "value_changed", G_CALLBACK (hadjustment_changed_signal), this);
	ga_debug ("installed hadjustment");

  if (GTK_WIDGET_REALIZED(da_)) {
    setup_adjustments();
  }
}

void 
DrawTree::set_vadjustment(GtkAdjustment* a)
{
  vadjustment_ = a;

  // FIXME need to save id and disconnect
	g_signal_connect (G_OBJECT (vadjustment_), "value_changed", G_CALLBACK (vadjustment_changed_signal), this);
	ga_debug ("installed vadjustment");

  if (GTK_WIDGET_REALIZED(da_)) {
    setup_adjustments();
  }
}

void 
DrawTree::setup_adjustments()
{
  if (hadjustment_) {

  }

  if (vadjustment_) {
#ifdef GNOME_ENABLE_DEBUG
		ga_debug ("setting up vadjustment");
#endif

    // It's important to emit signals only if there's an actual
    // change, otherwise we get weird scrollbar flicker.
    bool value_changed = false;
    bool other_changed = false;

    if (vadjustment_->value != row_)
      {
#ifdef GNOME_ENABLE_DEBUG
			ga_debug (" - setting value %d", row_);
#endif
        vadjustment_->value          = row_;
        value_changed = true;
      }

    float onscreen = exact_rows_onscreen();
    // can't scroll below this
    float newupper = nrows_; //nrows_ - onscreen;
    //if (newupper < 0.0) newupper = 0.0;

    if (vadjustment_->upper != newupper || vadjustment_->page_size != onscreen)
      {
#ifdef GNOME_ENABLE_DEBUG
			ga_debug (" - setting other (upper %f incr %d size %f)",
			      newupper, MAX (1, (visible_nodes_.size() - 1)), onscreen);
#endif
        vadjustment_->upper          = newupper;
			vadjustment_->page_increment = MAX (1, (visible_nodes_.size() - 1));
        vadjustment_->page_size      = onscreen;
        other_changed = true;
      }

    vadjustment_->step_increment = 1;
    vadjustment_->lower          = 0;

    //    other_changed = value_changed = true;

    // Note that these trigger our own callbacks,
    //  but we properly handle the scroll == 0 case

    if (other_changed)
      gtk_adjustment_changed      (vadjustment_);
    if (value_changed) 
      gtk_adjustment_value_changed(vadjustment_);
  }
}

void  
DrawTree::hadjustment_changed_signal(GtkAdjustment* a, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);
  
  g_return_if_fail(data);

  dt->hadjustment_changed();
}

void 
DrawTree::vadjustment_changed_signal(GtkAdjustment* a, gpointer data)
{
  DrawTree* dt = static_cast<DrawTree*>(data);
  
  g_return_if_fail(data);

  dt->vadjustment_changed();
}

void 
DrawTree::hadjustment_changed()
{

}

void 
DrawTree::vadjustment_changed()
{
/* FIXME: on partial bottom row, vadj->val is .xx more than previous row */
  gint nvalue = CLAMP(static_cast<gint>(vadjustment_->value), 
                      0, ((gint)nrows_));
  gint introw = static_cast<gint>(row_);

#ifdef GNOME_ENABLE_DEBUG
  ga_debug("vadjustment value changed: %f upper: %f pagesize: %f\n",
           vadjustment_->value, vadjustment_->upper, vadjustment_->page_size);
#endif

  scroll_by(nvalue - introw);
}

static void
save_column_order (const vector<DrawTree::ColumnType>& columns) {
	g_return_if_fail (columns.size() == static_cast<guint> (DrawTree::ColumnTypeEnd));

	int position = 0;
	vector<DrawTree::ColumnType>::const_iterator i = columns.begin();
	while (i != columns.end()) {
		gchar key[256];
		g_snprintf (key, 255, "/gnome-apt/ColumnOrder/%s", column_to_string (*i));
		gchar val[30];
		g_snprintf (val, 29, "%d", position);
		gnome_config_set_string (key, val);

		++position;
		++i;
	}

	gnome_config_sync();
}

gushort
DrawTree::type_column (ColumnType col) const {
	gushort i = 0;
	while (i < dt_coltypes.size()) {
		if (dt_coltypes[i] == col) return i;
		++i;
	}
	g_return_val_if_fail (0,0); /* Warn and return 0 */
	return 0;
}

void
DrawTree::set_column_order (const vector<ColumnType>& cts) {
	g_return_if_fail (cts.size() == dt_coltypes.size());

	dt_coltypes = cts;
	save_column_order (dt_coltypes);

	load_column_attrs();
}

static void
draw_icon (GdkDrawable* drawable, GdkPixbuf* pixbuf, gint* x, GdkRectangle* rect, GtkWidget* wdg) {
	if (pixbuf) {
		/* Hack: all icons are 16 high */
		const gint pixw = gdk_pixbuf_get_width (pixbuf), pixh = 16;
		GdkGC* gc = wdg->style->white_gc;
		gint y = rect->y + 2;

		gdk_draw_pixbuf (drawable, gc, pixbuf, 0, 0, *x, y, pixw, pixh, GDK_RGB_DITHER_NONE, 0, 0);

/* FIXME: keeping track of all offset/size changes is a PITA */
		*x += pixw + 2;
		rect->width -= (pixw + 2);
	}
}

/* FIXME: we shouldn't pas dt, probably... */
static void
draw_string (GdkDrawable* drawable, DrawTree* dt, const gchar* s, gint* x, GdkRectangle* rect,
      GtkStateType state, GAptPkgTree::Item* item) {
	if (s) {
		GdkColor* bg_col = &dt->widget()->style->base[state];
		GdkGC* usegc = dt->widget()->style->base_gc[state];

		if (state != GTK_STATE_PRELIGHT) {
			GAptCache::PkgStatusType status = GAptCache::StatusTypeEnd;
			if (item->relation() != GAptPkgTree::Item::CategoryItem) {
				pkgCache::PkgIterator i = ((GAptPkgTree::Pkg*) item)->package (*(dt->getModel()->cache()));
				status = dt->getModel()->cache()->pkgStatus (i);
			}

			if (dt->MarkRows[0] && status == GAptCache::StatusNowBroken) {
				usegc = drawtree_get_gc (dt, &gcol_broken, GCOL_BROKEN);
				bg_col = &gcol_broken;
			} else if (dt->MarkRows[1] && ((GAptPkgTree::Pkg*) item)->Orphaned()) {
				usegc = drawtree_get_gc (dt, &gcol_orphan, GCOL_ORPHAN);
				bg_col = &gcol_orphan;
			}
		}

		rect->width -= 2;
		gdk_gc_set_clip_rectangle (usegc, rect);

		PangoLayout* pl = gtk_widget_create_pango_layout (dt->widget(), s);
		gdk_draw_layout_with_colors (drawable, usegc, *x + 1, rect->y, pl,
		      &dt->widget()->style->text[state], bg_col);
		g_object_unref (pl);
		gdk_gc_set_clip_rectangle (usegc, 0);
	}
}

void
DrawTree::draw_column (Node* n, gushort column, gushort remaining,
      GtkStateType state, GdkRectangle* rect) {
	GdkDrawable* drawable = pixmap_;
	GAptPkgTree::Item* node = (GAptPkgTree::Item*) n;
	GAptPkgTree* tree = node->tree();

	gint ourx = rect->x, oldw = rect->width;

	const gchar* s = 0; gchar* alloced_s = 0;

	GAptCache::PkgStatusType status = GAptCache::StatusTypeEnd;

	ColumnType ct = column_type (column);
	if (ct == ColumnDelete || ct == ColumnInstall) {
		g_warning ("Boolean column being drawn!");
	} else if (ct == ColumnName) {
		s = node->name();

		GdkPixbuf* pixbuf;
		if (!pixbufs[columntypes[node->relation()]]) {
			pixbufs[columntypes[node->relation()]] = gapt_pixbuf_new (columntypes[node->relation()]);
		}
		pixbuf = pixbufs[columntypes[node->relation()]];

		draw_icon (drawable, pixbuf, &ourx, rect, widget());
	} else if (node->relation() != GAptPkgTree::Item::CategoryItem) {

		pkgCache::PkgIterator i = ((GAptPkgTree::Pkg*) node)->package (*(tree->cache()));
		bool virtual_package = (*(tree->cache()))[i].CandidateVer == 0;
		status = tree->cache()->pkgStatus (i);

	if (ct == ColumnCurrent) {
		if (!virtual_package) {
			pkgCache::VerIterator vi = i.CurrentVer();
			if (!vi.end()) s = vi.VerStr();
			else s = _("None");
		}
	} else if (ct == ColumnAvailable) {
		if (!virtual_package) {
			pkgCache::VerIterator vi = (*tree->cache())[i].CandidateVerIter (*tree->cache());	/* i.TargetVer(); */
			if (!vi.end()) s = vi.VerStr();
			else s = _("None");
		}
	} else if (ct == ColumnSection && !virtual_package) {
		s = i.Section();
	} else if (ct == ColumnPriority && !virtual_package) {
		s = tree->cache()->priorityString (i, 0);
	} else if (ct == ColumnStatus) {
			if (virtual_package) {
				s = _("Virtual package");
			} else {
				s = GAptCache::statusText (status);
			}
		} else if (ct == ColumnDescription) {
			pkgRecords::Parser* p = tree->cache()->pkgParser (i);
			if (p) {
				string tmp = p->ShortDesc();
				if (tmp.empty()) s = 0;
				else s = alloced_s = g_strdup (tmp.c_str());
			}
	} else {
		g_warning ("Unknown column type");
		return;
	}

	} /* !CategoryItem */

	draw_string (this->pixmap_, this, s, &ourx, rect, state, (GAptPkgTree::Item*) node);
	rect->width = oldw;
	if (alloced_s) g_free (alloced_s);
}

Nodes*
DrawTree::get_selection (void) {
	g_warning (__FUNCTION__);
	return 0;
}

gboolean
DrawTree::get_bool (gpointer n, gushort column) {
	GAptPkgTree::Item* node = (GAptPkgTree::Item*) n;
	pkgCache::Package* pkg = node->package();
	if (!pkg) {
	   return FALSE;
	}

/* FIXME: should use this->tree(),
   which also removes the need for node to be a GAptPkgTree::Item */
	pkgCache::PkgIterator i (*(node->tree()->cache()), pkg);


	ColumnType ct = column_type (column);
	if (ct == ColumnDelete) {
		return Util::removed (i, node->tree());
	} else if (ct == ColumnInstall) {
		return Util::installed (i, node->tree());
	}

	g_warning ("Getting bool for non-bool column!");
	return FALSE;
}

void
DrawTree::set_bool (gpointer n, gushort column, bool state) {
	GAptPkgTree::Item* node = (GAptPkgTree::Item*) n;
	pkgCache::Package* pkg = node->package();
	if (!pkg) {
	   return;
	}

	ColumnType ct = column_type (column);
	if (ct == ColumnDelete) {
		if (state) {
			/* FIXME */
			gapt_cache_control_remove_package (dt_cachectrl, pkg);
			queue_recalc_rows (true);
			node->tree()->update_status();
		} else {
			pkgCache::PkgIterator i (*(node->tree()->cache()), pkg);
			node->tree()->cache()->MarkKeep (i);
			queue_recalc_rows (true);
			node->tree()->update_status();
		}
	} else if (ct == ColumnInstall) {
		if (state) {
			if (gapt_cache_control_install_package (dt_cachectrl, pkg)) {
				queue_recalc_rows (true);
				node->tree()->update_status();
			}
		} else {
			pkgCache::PkgIterator i (*(node->tree()->cache()), pkg);
			node->tree()->cache()->MarkKeep (i);
			queue_recalc_rows (true);
			node->tree()->update_status();
		}
    } else {
		g_warning ("Setting bool on non-bool column!");
	}
}

gboolean
DrawTree::display_bool (gpointer n, gushort column) {
	GAptPkgTree::Item* node = (GAptPkgTree::Item*) n;
	pkgCache::Package* pkg = node->package();
	if (!pkg) {
	   return FALSE;
	}

	pkgCache::PkgIterator i (*(node->tree()->cache()), pkg);

	ColumnType ct = column_type (column);
	if (ct == ColumnDelete) {
		return Util::can_change_remove (i, node->tree());
	} else if (ct == ColumnInstall) {
		return Util::can_change_install (i, node->tree());
	}

	g_warning ("Asking whether to display bool on non-bool column!");
	return FALSE;
}
