/****************************************************************************
**
** Copyright (C) 2000-2006 Frank Hemer <frank@hemer.org>,
**                         Tilo Riemer <riemer@crossvc.com>,
**                         Wim Delvaux <wim.delvaux@chello.be>,
**                         Jose Hernandez <joseh@tesco.net>
**
**
**----------------------------------------------------------------------------
**
**----------------------------------------------------------------------------
**
** CrossVC is available under two different licenses:
**
** If CrossVC is linked against the GPLed version of Qt 
** CrossVC is released under the terms of GPL also.
**
** If CrossVC is linked against a nonGPLed version of Qt 
** CrossVC is released under the terms of the 
** CrossVC License for non-Unix platforms (CLNU)
**
**
** CrossVC License for non-Unix platforms (CLNU):
**
** Redistribution and use in binary form, without modification, 
** are permitted provided that the following conditions are met:
**
** 1. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 2. It is not permitted to distribute the binary package under a name
**    different than CrossVC.
** 3. The name of the authors may not be used to endorse or promote
**    products derived from this software without specific prior written
**    permission.
** 4. The source code is the creative property of the authors.
**    Extensions and development under the terms of the Gnu Public License
**    are limited to the Unix platform. Any distribution or compilation of 
**    the source code against libraries licensed other than gpl requires 
**    the written permission of the authors.
**
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR 
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
** ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 
** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
** DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
** GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
** INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
** WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
**
**
** CrossVC License for Unix platforms:
**
** 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 version 2 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 "logtree.h"

#include "config.h"

#include <iostream>
#include <qpainter.h>
#include <qapplication.h>
#include <qtooltip.h>
#include <qregexp.h>
#include <qscrollbar.h>
#include <qtimer.h>

#include "globals.h"

static const int BORDER = 8;
static const int INSPACE = 3;
static const int BASEBLOCK = 10;

//needed as static for sizeHint() const
static int  static_base_width;
static int  static_base_height;

QColor LogTreeView::SelColor [2];


// -------------------- Tooltip --------------------------------------------------------------------------

class DynamicTip : public QToolTip
{
public:
   DynamicTip(LogTreeView *parent) : QToolTip(parent) {}

protected:
   virtual void maybeTip(const QPoint &pos);
};

// -------------------- LogTreeItem --------------------------------------------------------------------------

LogTreeView::LogTreeItem::LogTreeItem()
   : firstonbranch(false), followed(false), branched(false),
     row(0), col(0), selected(false), selection(0)
{
}

LogTreeView::LogTreeItem::LogTreeItem(QString _file,
      QString _rev,
      QString _authorLocked,
      QString _date,
      QString _author,
      QString _state,
      QString _lines,
      QString _kopt,
      QString _commitid,
      QString _mergepoint,
      QString _filename,
      QString _comment,
      QString _branch,
      QString _tagcomment,
      QString _BranchTagList,
      QString _RegularTagList)
   : file(_file), rev(_rev), locked(QString::null),authorLocked(_authorLocked),
     date(_date), author(_author), state(_state), lines(_lines), kopt(_kopt),
     commitid(_commitid), mergepoint(_mergepoint), filename(_filename), comment(_comment),
     branch(_branch), tagcomment(_tagcomment), BranchTagList(_BranchTagList),
     RegularTagList(_RegularTagList), branchpoint(QString::null),
     firstonbranch(false), followed(false), branched(false),
     row(0), col(0), selected(false), selection(0)
{
}

LogTreeView::LogTreeItem::~LogTreeItem()
{
}

// -------------------- ItemList --------------------------------------------------------------------------

LogTreeView::ItemList::ItemList()  : QPtrList<LogTreeView::LogTreeItem>() {
   setAutoDelete(false);
}

LogTreeView::ItemList::~ItemList() {
}

int LogTreeView::ItemList::compareItems ( QPtrCollection::Item item1, QPtrCollection::Item item2 ) {
   QString r1 = static_cast<LogTreeView::LogTreeItem*>(item1)->rev;
   QString r2 = static_cast<LogTreeView::LogTreeItem*>(item2)->rev;
   int c1 = r1.contains('.');
   int c2 = r2.contains('.');
   if (c1 != c2) {
      return (c1-c2);
   } else if (c1 == 1) {
      QString s1 = r1.section('.',0,0);
      QString s2 = r2.section('.',0,0);
      if (s1 != s2) {
	 return (s2.toInt()-s1.toInt());
      }
      s1 = r1.section('.',1,1);
      s2 = r2.section('.',1,1);
      return (s2.toInt()-s1.toInt());
   } else {
      int i = 0;
      QString s1 = r1.section('.',i,i);
      QString s2 = r2.section('.',i,i);
      do {
	 if (s1 != s2) {
	    return (s1.toInt()-s2.toInt());
	 }
	 ++i;
	 s1 = r1.section('.',i,i);
	 s2 = r2.section('.',i,i);
      } while (i < c1);
      return (s2.toInt()-s1.toInt());
   }
}

// -------------------- LogTreeView --------------------------------------------------------------------------

void LogTreeView::setSelectionColor (const unsigned char selection, const QColor &color)
{
   SelColor [selection] = color;
}

	
QColor LogTreeView::getSelectionColor (const unsigned char selection) const
{
   return SelColor [selection];
}


LogTreeView::LogTreeView(QWidget *parent, const char *name, WFlags f)
   : QtTableView(parent, name, f), bEnableBranchTags(false),
     bEnableRegularTags(false) {

   m_Font = font();
   m_pFM = new QFontMetrics( m_Font);
   static_base_width = m_pFM->width("123456789") + 2*BORDER + 2*INSPACE;
   static_base_height = 2*m_pFM->height() + 2*BORDER + 3*INSPACE;
  
   // Set default selection color to black
   for (int i=0; i<2; i++) {
      setSelectionColor (i, QColor("black"));
   }

   setNumCols(0);
   setNumRows(0);
   setAutoUpdate(false);
   setTableFlags( Tbl_autoVScrollBar | Tbl_autoHScrollBar |
	 Tbl_smoothVScrolling | Tbl_smoothHScrolling );
   setFrameStyle( QFrame::WinPanel | QFrame::Sunken );
   setBackgroundMode(PaletteBase);
  
   setMouseTracking(true);
   setFocusPolicy(ClickFocus);
   focus.col = -1;
   focus.row = -1;
  
   setCellWidth(0);
   setCellHeight(0);
  
   currentRow = -1;
   currentCol = -1;
   currentTree = 0;
  
   insertRow = 0;
   startCol = 0;

   newFile = TRUE;

   itemFileList.setAutoDelete(true);
   connectionFileList.setAutoDelete(true);
  
   currentItems = new QPtrList<LogTreeItem>();
   currentItems->setAutoDelete(true);
   itemFileList.append(currentItems);

   currentConnections = new QPtrList<LogTreeConnection>();
   currentConnections->setAutoDelete(true);
   connectionFileList.append( currentConnections );

   (void) new DynamicTip(this);

   connect(verticalScrollBar(),SIGNAL(valueChanged(int)),this,SLOT(scrollValueChanged(int)));
   connect(horizontalScrollBar(),SIGNAL(valueChanged(int)),this,SLOT(scrollValueChanged(int)));
   connect(verticalScrollBar(),SIGNAL(sliderMoved(int)),this,SLOT(scrollValueChanged(int)));
   connect(horizontalScrollBar(),SIGNAL(sliderMoved(int)),this,SLOT(scrollValueChanged(int)));
}


LogTreeView::~LogTreeView () {
}

void LogTreeView::zoomIn() {
   zoom(1);
}

void LogTreeView::zoomOut() {
   zoom(-1);
}

void LogTreeView::zoom(int val) {

   int size = m_Font.pointSize();
   int oldSize = m_pFM->width("123456789");

   size += val;
   while (size>0 && size<40) {

      m_Font.setPointSize(size);
      delete m_pFM;
      m_pFM = new QFontMetrics(m_Font);

      if (m_pFM->width("123456789") != oldSize) {// not supported ? -> ignore it

	 static_base_width = m_pFM->width("123456789") + 2*BORDER + 2*INSPACE;
	 static_base_height = 2*m_pFM->height() + 2*BORDER + 3*INSPACE;
	 setFont(m_Font);
	 recomputeCellSizes();
	 repaint (true);
	 break;

      }
      size += val;
   } 
}

void LogTreeView::wheelEvent( QWheelEvent *e) {
   if (viewRect().contains(e->pos())) {
   int d = e->delta();
   int v = verticalScrollBar()->value();
   int l = verticalScrollBar()->lineStep();
   l *= QApplication::wheelScrollLines();
   if (d > 0) {
      d = v - l;
   } else {
      d = v + l;
   }
   verticalScrollBar()->setValue(d);
   e->accept();
   } else {
      e->ignore();
   }
}


void LogTreeView::keyPressEvent ( QKeyEvent * e) {

   switch (e->key()) {
      case Qt::Key_Up: {
	 moveFocus(0,-1);
	 e->accept();
	 break;
      }
      case Qt::Key_Down: {
	 moveFocus(0,1);
	 e->accept();
	 break;
      }
      case Qt::Key_Left: {
	 moveFocus(-1,0);
	 e->accept();
	 break;
      }
      case Qt::Key_Right: {
	 moveFocus(1,0);
	 e->accept();
	 break;
      }
      case Qt::Key_Prior: {
	 if (focus.col == -1) focus.col = leftCell();
	 if (focus.row == -1) focus.row = lastRowVisible();
	 moveFocus(0,-focus.row);
	 e->accept();
	 break;
      }
      case Qt::Key_Next: {
	 if (focus.col == -1) focus.col = leftCell();
	 if (focus.row == -1) focus.row = lastRowVisible();
	 moveFocus(0,numRows() - 1 - focus.row);
	 e->accept();
	 break;
      }
      case Qt::Key_Home: {
	 if (focus.col == -1) focus.col = leftCell();
	 if (focus.row == -1) focus.row = lastRowVisible();
	 moveFocus(-focus.col,0);
	 e->accept();
	 break;
      }
      case Qt::Key_End: {
	 if (focus.col == -1) focus.col = leftCell();
	 if (focus.row == -1) focus.row = lastRowVisible();
	 moveFocus(numCols() - 1 - focus.col,0);
	 e->accept();
	 break;
      }
      case Qt::Key_Space: {
	 if (focus.col > -1 && focus.row > -1) {
	    if (e->state() == Qt::NoButton || e->state() == Qt::ShiftButton) {
	       QPtrList<LogTreeItem> * listItem = itemFileList.at(treeForCol(focus.col));
	       QPtrListIterator<LogTreeItem> it(*listItem);
	       for(; it.current(); ++it) {
		  if ((it.current()->row == focus.row) &&
			(it.current()->col == focus.col) &&
			(it.current()->state != "dead")) {
		     emit revisionClicked (it.current(), (e->state() == Qt::ShiftButton));
		     break;
		  }
	       }
	    }
	 }
	 break;
      }
      default: {
	 break;
      }
   }
}

void LogTreeView::moveFocus( int dx, int dy) {

   if (focus.col == -1 || focus.row == -1 || !rowIsVisible(focus.row) || !colIsVisible(focus.col) ) {
      focus.col = leftCell();
      focus.row = lastRowVisible();
   } else if (focus.col+dx >= 0 && focus.col+dx < numCols() && focus.row+dy >= 0 && focus.row+dy < numRows()) {
      focus.col += dx;
      focus.row += dy;
   } else return;

   int xoff=0;
   int yoff=0;
   int i;
   for (i=0; i<focus.col-1; ++i) {
      xoff += cellWidth(i);
   }
   for (i=0; i<=focus.row; ++i) {
      yoff += cellHeight(i);
   }
   if (focus.row+1 < numRows()) yoff += cellHeight(focus.row+1);

   setOffset(xoff,yoff-maxViewY());
   repaint(true);
}

void LogTreeView::scrollDown() {
   setOffset(0,totalHeight()-maxViewY());
}

QString LogTreeView::findCollision (int row, int col) const {

   QPtrListIterator<LogTreeItem> it(*currentItems);
   QString branch ("");

   for (it.toFirst(); it.current(); ++it) {
      if ((it.current()->row == row) && (it.current()->col == col)) {
	 branch = it.current()->rev;
	 branch.replace (QRegExp ("\\.[0-9]+$"), "");
      }
   }
   return (branch);
}


void LogTreeView::shiftBranch (QString branch) {
   // Move branches to the right
   QPtrListIterator<LogTreeItem> it(*currentItems);
  
   if (!branch.isEmpty()) {
      for (it.toFirst(); it.current(); ++it)
	 if (it.current()->rev.startsWith(branch)) {
	    shiftBranch (findCollision (it.current()->row, it.current()->col + 1));
	    if (++it.current()->col >= numCols())
	       setNumCols (it.current()->col + 1);
	
	    if (it.current()->rev.contains (QRegExp("\\.1$"))) //FIXME revision number might not end with '.1' because an indiv. number might be added
	       shiftBranch (findCollision (it.current()->row + 1, it.current()->col));
	 }
   }
}


void LogTreeView::shiftAll(int shift) {
   // Shift all items by 'shift'
   QPtrList<LogTreeItem> * listItem;
   for (listItem = itemFileList.first(); listItem; listItem = itemFileList.next()) {
      QPtrListIterator<LogTreeItem> it(*listItem);
      for (; it.current(); ++it) {
	 it.current()->row += shift;
      }
   }
}


void LogTreeView::shiftCurrent(int shift) {
   // Shift current items by 'shift'
   QPtrListIterator<LogTreeItem> it(*currentItems);
   for (; it.current(); ++it) {
      it.current()->row += shift;
   }
}

void LogTreeView::addRevisionList(ItemList & list) {
   list.sort();
   LogTreeItem * lte;
   for ( lte = list.first(); lte; lte = list.next() ) {
      addRevision(lte);
   }
}

void LogTreeView::addRevision( LogTreeItem * item) {

   QString revision, branch;
   int pos;

   revision = item->rev;
   branch = "";

   //analyze the revision
   if ((pos = revision.findRev('.')) != -1) {

      revision.truncate(pos);

      if ((pos = revision.findRev('.')) != -1) {// we're on a branch

	 // e. g. for rev = 1.1.2.3 we have
	 // branch = 1.1.2, revision = 1.1
	 branch = revision;
	 revision.truncate(pos);

      } else {// we're on the main trunk

	 int revNum = item->rev.mid(item->rev.findRev(".")+1).toInt();
	 if (newFile) {
	    trunkRev = revision.toInt();
	    rowOffset = revNum;
	    if (trunkRev > 1) ++rowOffset;// there might be a rev *.0
	    insertRow = numRows() - rowOffset;

	    if (insertRow < 0) {

	       /* Has more revisions than all prev. files
		* so shift all other revisions up
		* and adjust table size in advance
		*/
	       shiftAll(-insertRow);
	       setNumRows(numRows() - insertRow);
	       insertRow = 0;
	    }

	    setNumCols (startCol+1);
	    newFile = FALSE;

	 } else if (revision.toInt() != trunkRev) {
	    /* There's a jump in the master rev numbers
	     * e.g. 3.0 followed by 2.7
	     */
	    // 	qDebug("master trunk rev jump detected: "+file+
	    // 	       ", oldtrunk: "+QString::number(trunkRev)+
	    // 	       ", newtrunk: "+revision+
	    // 	       ", oldrev: "+QString::number(lastRevNum)+
	    // 	       ", newrev: "+QString::number(revNum));
	    trunkRev = revision.toInt();
	    int rows = revNum;
	    if (trunkRev > 1) ++rows;// we assume there's a rev *.0

	    /* Insert space for the new rows
	     * and shift current rev's up if 
	     * there's a jump in the rev numbers
	     * e.g. 3.2 followed by 2.7
	     */
	    shiftCurrent(lastRevNum-rows);
	    rowOffset += rows-lastRevNum;
	    insertRow += lastRevNum-rows;

	    /* And resize the table
	     * if necessary
	     */
	    int insert = rowOffset - numRows();
	    if (insert > 0) {
	       shiftAll(insert);
	       setNumRows(rowOffset);
	       insertRow += insert;
	    }

	 } else if (revNum < lastRevNum-1) {
	    /* There's a jump in the rev numbers
	     * e.g. 2.7 followed by 2.3
	     */
	    // 	qDebug("rev jump detected: "+file+
	    // 	       ", oldrev: "+QString::number(lastRevNum)+
	    // 	       ", newrev: "+QString::number(revNum));
	    int jump = lastRevNum - 1 - revNum;
	    shiftCurrent(jump);
	    insertRow += jump;
	    rowOffset -= jump;
	 }

	 lastRevNum = revNum;
      }
   }

   item->tree = currentTree;
   if (!item->authorLocked.isEmpty()) {
      item->locked = " locked";
   }
   item->branchpoint = revision;
   item->selected = false;

   if (branch.isEmpty()) {

      // We are on the main trunk
      item->firstonbranch = false;
      item->row = insertRow++;
      item->col = startCol;
      currentItems->append(item);
    
      return;
   }
    
   // look whether we have revisions on this branch
   // shift them up
   int row = -1, col = -1;
   QPtrListIterator<LogTreeItem> it(*currentItems);
   for (; it.current(); ++it) {

      if (it.current()->rev.startsWith(branch)) {
      
	 it.current()->firstonbranch = false;
	 row = it.current()->row;
	 col = it.current()->col;
	 it.current()->row--;

	 /* Are we inserting at the top of the widget?
	  * If so, we need to shift all the rows down.
	  */
	 if (row == 0) {

	    shiftAll(1);
	    setNumRows(numRows()+1);
	    row = 1;
	 }
      }
   }

   if (row == -1) {

      /* Ok, so we must open a new branch
       * Let's find the branch point
       */
      QPtrListIterator<LogTreeItem> it3(*currentItems);
      for (it3.toLast(); it3.current(); --it3) {

	 if (revision == it3.current()->rev) {

	    row = it3.current()->row-1;
	    col = it3.current()->col+1;

	    /* Display a compacted log tree so we can get
	     * more stuff on less screen.  This mode is
	     * basically a slightly smarted layout algorithm
	     * that attempts to position branches in a
	     * space saving way.
	     */
	    shiftBranch (findCollision (row, col));
	    shiftBranch (findCollision (row + 1, col));
	
	    if (col >= numCols()) {
	       setNumCols (col + 1);
	    }

	    /* Are we inserting above the top of the widget?
	     * If so, we need to shift all the rows down.
	     */
	    if (row == -1) {

	       shiftAll(1);
	       setNumRows(numRows()+1);
	       row = 0;
	    }
	    break;
	 }
      }
   }

   item->firstonbranch = true;
   item->row = row;
   item->col = col;
   currentItems->append(item);
}

void LogTreeView::nextFile() {

   collectConnections();
   treeList.append(numCols()-1);
   startCol = numCols()+1;
   ++currentTree;
   insertRow = 0;
   newFile = TRUE;

   currentItems = new QPtrList<LogTreeItem>();
   currentItems->setAutoDelete(true);
   itemFileList.append(currentItems);

   currentConnections = new QPtrList<LogTreeConnection>();
   currentConnections->setAutoDelete(true);
   connectionFileList.append( currentConnections );

}

unsigned int LogTreeView::treeForCol(int col) {
   unsigned int tree = 0;
   QValueList<int>::iterator it;
   for ( it = treeList.begin(); it != treeList.end(); ++it,++tree ) {
      if (col <= *it) return tree;
   }
   qDebug("tree not found");
   return 0;
}

void LogTreeView::collectConnections() {

   QPtrListIterator<LogTreeItem> it(*currentItems);
   for (; it.current(); ++it) {

      QString rev = it.current()->rev;
      QPtrListIterator<LogTreeItem> it2(*currentItems);
      for (it2=it,++it2; it2.current(); ++it2) {

	 if (it2.current()->branchpoint == rev &&
	       it2.current()->firstonbranch)	{

	    LogTreeConnection *conn = new LogTreeConnection;
	    conn->start = it.current();
	    conn->end = it2.current();
	    currentConnections->append(conn);
	 }
      }

   }
}


void LogTreeView::setSelectedPair (int treeA, int treeB, QString &selectionA, QString &selectionB) {

   bool selected, refresh = false;
   unsigned char selection;

   QPtrList<LogTreeItem> * listItem;
   for (listItem = itemFileList.first(); listItem; listItem = itemFileList.next()) {

      for (QPtrListIterator<LogTreeItem> it(*listItem); it.current(); ++it) {

	 if ( (it.current()->tree == treeA) && (selectionA == it.current()->rev) ) {

	    selected = true;
	    selection = 0;
	
	 } else if ( (it.current()->tree == treeB) && (selectionB == it.current()->rev) ) {
	
	    selected = true;
	    selection = 1;
	
	 } else {
	
	    selected = false;
	    selection = 0;
	 }
      
	 if ((selected != it.current()->selected) ||
	       (selection != it.current()->selection))	{
	
	    it.current()->selected = selected;
	    it.current()->selection = selection;
	
	    // mark for refresh if there are any changes
	    refresh = true;
	 }
      }
   }

   repaint (refresh);
}


QSize LogTreeView::sizeHint() const {
   return QSize (2 * static_base_width, 3 * static_base_height);
}


void LogTreeView::setupPainter(QPainter *p) {
   p->setBackgroundColor(colorGroup().base());
}

void LogTreeView::scrollValueChanged(int) {
   //need to update to remove the prev focus frame
   if (hasFocus()) {
      repaint();
   }
}

void LogTreeView::paintEvent(QPaintEvent * e) {
   QtTableView::paintEvent(e);
   if (hasFocus()) {
      QRect r = viewRect();
      int x, y, w, h;
      r.rect(&x, &y, &w, &h);
      int mass = 2;

      QPainter p(this);
      do {
	 p.drawRect(x,y,w,h);
	 x += 1;
	 y += 1;
	 w -= 2;
	 h -= 2;
      } while (--mass > 0);
   }
}


void LogTreeView::paintCell(QPainter *p, int row, int col) {

   bool followed, branched;
   LogTreeItem *item;
  
   branched = false;
   followed = false;
   item = 0;
  
   unsigned int tree = treeForCol(col);

   QPtrList<LogTreeItem> * listItem = itemFileList.at(tree);
   QPtrListIterator<LogTreeItem> it(*listItem);
   for(; it.current(); ++it) {

      int itcol = it.current()->col;
      int itrow = it.current()->row;
      if (itrow == row-1 && itcol == col) {
	 followed = true;
      }
      if (itrow == row && itcol == col) {
	 item = it.current();
      }
   }

   QPtrList<LogTreeConnection> * listConnection = connectionFileList.at(tree);
   QPtrListIterator<LogTreeConnection> it2(*listConnection);
   for (; it2.current(); ++it2) {
    
      int itcol1 = it2.current()->start->col;
      int itcol2 = it2.current()->end->col;
      int itrow = it2.current()->start->row;
      if (itrow == row && itcol1 <= col && itcol2 > col) {
	 branched = true;
      }
   }

   p->fillRect (0, 0, cellWidth(col), cellHeight(row), colorGroup().base());

   //draw focus
   if (focus.col == col && focus.row == row) {
      int w = 1;
      p->setPen(QPen(black,w,DotLine));
      p->drawLine(0,0,cellWidth(col)-w,0);
      p->drawLine(0,0,0,cellHeight(row)-w);
      p->drawLine(0,cellHeight(row)-w,cellWidth(col)-w,cellHeight(row)-w);
      p->drawLine(cellWidth(col)-w,0,cellWidth(col)-w,cellHeight(row)-w);
   }

   p->setPen (colorGroup().text());

   if (item) {
      paintRevisionCell (p, item, followed, branched);
   } else if (followed || branched) {
      paintConnector(p, row, col, followed, branched);
   }
}


void LogTreeView::paintConnector (QPainter *p, int row, int col,
      bool followed, bool branched) {

   int midx = colWidths[col] / 2;
   int midy = rowHeights[row] / 2;
  
   p->drawLine(0, midy, branched ? colWidths[col] : midx, midy);
   if (followed) {
      p->drawLine(midx, midy, midx, 0);
   }
}


void LogTreeView::paintRevisionCell (QPainter *p, LogTreeView::LogTreeItem *item,
      bool followed, bool branched) {

   int alignment = AlignVCenter;
   bool baseRev = FALSE;

   QSize r1 = m_pFM->size(alignment, item->author);
   QSize r2 = m_pFM->size(alignment, item->rev+item->locked);
   QSize r3 = m_pFM->size(alignment, item->BranchTagList);
   QSize r4 = m_pFM->size(alignment, item->RegularTagList);

   int boxwidth, boxheight;
   boxwidth = QMAX(static_base_width-2*BORDER, QMAX(r1.width(), r2.width()));
   boxheight = r1.height() + r2.height() + 3*INSPACE;
  
   if ((bEnableBranchTags) && (!item->BranchTagList.isEmpty())) {
      boxwidth = QMAX(boxwidth, r3.width());
      boxheight += r3.height() + INSPACE;
   }
  
   if ((bEnableRegularTags) && (!item->RegularTagList.isEmpty())) {
      boxwidth = QMAX(boxwidth, r4.width());
      boxheight += r4.height() + INSPACE;
   }

   if (item->row+1 == numRows()) {// rev. 1.1
      baseRev = TRUE;
      QSize r5 = m_pFM->size(alignment, item->file);
      boxwidth = QMAX(boxwidth, r5.width());
      boxheight += r5.height() + BASEBLOCK + INSPACE;
   }
  
   boxwidth += 2*INSPACE;
  
   int x = (colWidths[item->col] - boxwidth) / 2;
   int midx = colWidths[item->col] / 2;
   int y = (rowHeights[item->row] - boxheight ) / 2;
   int midy = rowHeights[item->row] / 2;

   // Connectors
   if (followed) {
      p->drawLine(midx, 0, midx, y);
   }
  
   if (branched) {
      p->drawLine(midx + boxwidth / 2, midy, colWidths[item->col], midy);
   }

   if (!baseRev) p->drawLine(midx, y + boxheight, midx, rowHeights[item->row]);
    
   /* Draw the box itself.  Revisions marked as dead by cvs are
    * a special case because we can't run normal CVS operations
    * on them.
    */
   if (item->state == "dead") {

      p->setBrush(QColor(221,221,221));
      p->setPen (QPen(black, 0, DotLine));
      p->drawRoundRect(x, y, boxwidth, boxheight, 10, 10);

   } else {
    
      if (item->selected) {

	 int h, s, v;
	 QColor FillColor (getSelectionColor(item->selection));
	 FillColor.hsv (&h, &s, &v);
	 p->setBrush(FillColor);
	 p->setPen ((v > 128) ? QColor("black") : QColor("white"));

      } else {
	 p->setBrush(QColor(255,240,220));
      }

      p->drawRoundRect(x, y, boxwidth, boxheight, 10, 10);
   }

   if (baseRev) {
      p->setBrush(QColor("black"));
      p->drawRoundRect(x, y + boxheight - BASEBLOCK, boxwidth, BASEBLOCK, 25, 25);
      p->fillRect(x, y + boxheight - BASEBLOCK, boxwidth, BASEBLOCK / 2, QColor("black") );
   }

   x += INSPACE;
   y += INSPACE;
   boxwidth -= 2*INSPACE;
  
   p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->author);
   y += r1.height() + INSPACE;
  
   if ((bEnableRegularTags) && (!item->RegularTagList.isEmpty())) {

      QFont font(p->font());
      QFont underline(font);
    
      underline.setUnderline(TRUE);
      p->setFont(underline);
      if (!item->selected) {
	 QColor col = p->pen().color();
	 p->setPen (QColor("blue"));
	 p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->RegularTagList);
	 p->setPen (col);
      } else {
	 p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->RegularTagList);
      }
      p->setFont(font);
      y += r4.height() + INSPACE;
   }

   if ((bEnableBranchTags) && (!item->BranchTagList.isEmpty())) {

      QFont font(p->font());
      QFont underline(font);
    
      underline.setUnderline(TRUE);
      p->setFont(underline);
      if (!item->selected) {
	 QColor col = p->pen().color();
	 p->setPen (QColor("red"));
	 p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->BranchTagList);
	 p->setPen (col);
      } else {
	 p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->BranchTagList);
      }
      p->setFont(font);
      y += r3.height() + INSPACE;
   }
  
   p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->rev+item->locked);
   y += r2.height() + INSPACE;

   if (baseRev) {// rev. 1.1

      QFont font(p->font());
      QFont italic(font);
    
      italic.setItalic(TRUE);
      p->setFont(italic);
      if (!item->selected) {
	 QColor col = p->pen().color();
	 p->setPen (QColor("magenta"));
	 p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->file);
	 p->setPen (col);
      } else {
	 p->drawText(x, y, boxwidth, boxheight, AlignHCenter, item->file);
      }
      p->setFont(font);
   }

}

/*!!***************************************************************************
 *!! FUNCTION NAME
 *!!    LogTreeView::mouseDoubleClickEvent
 *!!
 *!! PARAMETERS
 *!!    QMouseEvent *e
 *!!
 *!! RETURN VALUE
 *!!    none
 *!!
 *!! GLOBALS ACCESSED
 *!!    ?
 *!!
 *!! DESCRIPTION
 *!!    Double clicking on a revision has the effect of bringing up a file
 *!!    view window.  The exception to this are dead revisions.  
 *!!**************************************************************************/

void LogTreeView::mouseDoubleClickEvent (QMouseEvent *e) {
   if (e->button() == LeftButton) {
      int row = findRow (e->pos().y());
      int col = findCol (e->pos().x());

      QPtrList<LogTreeItem> * listItem = itemFileList.at(treeForCol(col));
      QPtrListIterator<LogTreeItem> it(*listItem);
      for (; it.current(); ++it) {
	 if ((it.current()->row == row) &&
	       (it.current()->col == col) &&
	       (it.current()->state != "dead")) {
	    emit revisionDblClicked (it.current());
	    break;
	 }
      }

   }
}


void LogTreeView::mousePressEvent(QMouseEvent *e) {
   if ( (e->button() == LeftButton) ||
	 (e->button() == RightButton)) {

      int row = findRow( e->pos().y() );
      int col = findCol( e->pos().x() );
    
      QPtrList<LogTreeItem> * listItem = itemFileList.at(treeForCol(col));
      QPtrListIterator<LogTreeItem> it(*listItem);
      for(; it.current(); ++it) {
      
	 if ((it.current()->row == row) &&
	       (it.current()->col == col) &&
	       (it.current()->state != "dead")) {
	
	    emit revisionClicked (it.current(), (e->button() == RightButton));
	    break;
	 }

      }

   }
}


void LogTreeView::recomputeCellSizes () {

   // Store current scrollbar focus
   int xCenteredCell = (leftCell()+lastColVisible()) / 2;
   int yCenteredCell = (topCell()+lastRowVisible()) / 2;
   int xScrollVal = horizontalScrollBar()->value();
   int yScrollVal = verticalScrollBar()->value();
   QRect centeredCellRect = cellPos(xCenteredCell,yCenteredCell);
   int oldXCellCenter = (centeredCellRect.left()+centeredCellRect.right()) / 2;
   int oldYCellCenter = (centeredCellRect.top()+centeredCellRect.bottom()) / 2;

   // Fill with default
   int v = static_base_width;
   colWidths.fill(v, numCols());
   v = static_base_height;
   rowHeights.fill(v, numRows());
  
   // block view
   setAutoUpdate(false);

   // For all files (trees) ...
   QPtrList<LogTreeItem> * listItem;
   for (listItem = itemFileList.first(); listItem; listItem = itemFileList.next()) {

      // Compute maximum for each column and row
      QPtrListIterator<LogTreeItem> it(*listItem);
      for (; it.current(); ++it) {
    
	 LogTreeItem *item = it.current();
    
	 QSize r1 = m_pFM->size(AlignVCenter, item->rev+item->locked);
	 QSize r2 = m_pFM->size(AlignVCenter, item->author);
      
	 int boxwidth = QMAX(r1.width(), r2.width());
	 int boxheight = r1.height() + r2.height() + 3*INSPACE;
      
	 if ((bEnableBranchTags) && (!item->BranchTagList.isEmpty())) {
	
	    QSize r3 = m_pFM->size(AlignVCenter, item->BranchTagList);
	    boxwidth = QMAX(boxwidth, r3.width());
	    boxheight += r3.height() + INSPACE;
	 }
      
	 if ((bEnableRegularTags) && (!item->RegularTagList.isEmpty())) {
	
	    QSize r4 = m_pFM->size(AlignVCenter, item->RegularTagList);
	    boxwidth = QMAX(boxwidth, r4.width());
	    boxheight += r4.height() + INSPACE;
	 }
      
	 if (item->row+1 == numRows()) {// rev. 1.1
	
	    QSize r5 = m_pFM->size(AlignVCenter, item->file);
	    boxwidth = QMAX(boxwidth, r5.width());
	    boxheight += r5.height() + BASEBLOCK + INSPACE;
	 }
      
	 boxwidth += 2*INSPACE;
      
	 colWidths[item->col] = QMAX(colWidths[item->col], boxwidth + 2*BORDER);
	 rowHeights[item->row] = QMAX(rowHeights[item->row], boxheight + 2*BORDER);
      }
   }

   setAutoUpdate(true);
   updateTableSize();
   update();

   // restore scrollbar focus
   centeredCellRect = cellPos(xCenteredCell,yCenteredCell);
   int newXCellCenter = (centeredCellRect.left()+centeredCellRect.right()) / 2;
   int newYCellCenter = (centeredCellRect.top()+centeredCellRect.bottom()) / 2;
   if (horizontalScrollBar()->isVisible()) horizontalScrollBar()->setValue(xScrollVal+newXCellCenter-oldXCellCenter);
   if (verticalScrollBar()->isVisible()) verticalScrollBar()->setValue(yScrollVal+newYCellCenter-oldYCellCenter);
   updateScrollBars();

}


int LogTreeView::cellWidth(int col) {
   if (col < 0 || col >= (int)colWidths.size()) {
      return 0;
   }
   return colWidths[col];
}


int LogTreeView::cellHeight(int row) {
   if (row < 0 || row >= (int)rowHeights.size()) {
      return 0;
   }
   return rowHeights[row];
}

QRect LogTreeView::cellPos(int col,int row) {
   int left = 0;
   int top = 0;
   int width = 0;
   int height = 0;

   int i;
   for (i=0; i<col; ++i) {
      left += cellWidth(i);
   }
   width = cellWidth(i);

   for (i=0; i<row; ++i) {
      top += cellHeight(i);
   }
   height = cellHeight(i);

   return QRect(left,top,width,height);
}

void LogTreeView::toggleBranchTags (bool enable) {

   bEnableBranchTags = enable;
   recomputeCellSizes();
   repaint (true);
}


void LogTreeView::toggleRegularTags (bool enable) {

   bEnableRegularTags = enable;
   recomputeCellSizes();
   repaint (true);
}


void LogTreeView::lookupTip (const QPoint &p, QRect *r, QString *s) {
   int row = findRow (p.y());
   int col = findCol (p.x());
  
   *r = QRect(0, 0, -1, -1);
   *s = "";

   QPtrList<LogTreeItem> * listItem = itemFileList.at(treeForCol(col));
   for (QPtrListIterator<LogTreeItem> it(*listItem); it.current(); ++it) {

      if ((it.current()->row == row) && (it.current()->col == col)) {

	 QString text = "<qt><nobr><b>";
	 text += it.current()->rev;
	 text += "</b>&nbsp;&nbsp;";
	 text += it.current()->author;
	 text += "&nbsp;&nbsp;<b>";
	 text += it.current()->date;
	 text += "</b></nobr>";
	 if (!it.current()->locked.isEmpty()) {
	    text += "<br><nobr><b>";
	    text += it.current()->locked;
	    text += "</b>&nbsp;by:&nbsp;";
	    text += "&nbsp;&nbsp;<b>";
	    text += it.current()->authorLocked;
	    text += "</b></nobr>";
	 }
	 if (!it.current()->lines.isEmpty()) {
	    text += "<br><nobr>lines:&nbsp;";
	    text += "<b>";
	    text += it.current()->lines;
	    text += "</b></nobr>";
	 }
	 if (!it.current()->commitid.isEmpty()) {
	    text += "<br><nobr>commitid:&nbsp;";
	    text += "<b>";
	    text += it.current()->commitid;
	    text += "</b></nobr>";
	 }
	 if (!it.current()->mergepoint.isEmpty()) {
	    text += "<br><nobr>mergepoint:&nbsp;";
	    text += "<b>";
	    text += it.current()->mergepoint;
	    text += "</b></nobr>";
	 }
	 if (!it.current()->filename.isEmpty()) {
	    text += "<br><nobr>filename:&nbsp;";
	    text += "<b>";
	    text += it.current()->filename;
	    text += "</b></nobr>";
	 }
	 QStringList list2 = QStringList::split("\n", it.current()->comment);
	 QStringList::Iterator it2;
	 for (it2 = list2.begin(); it2 != list2.end(); ++it2) {

	    text += "<br>";
	    text += escapeHtmlTags(*it2);
	 }
	 if (!it.current()->tagcomment.isEmpty()) {

	    text += "<i>";
	    QStringList list3 = QStringList::split("\n", it.current()->tagcomment);
	    QStringList::Iterator it3;
	    for (it3 = list3.begin(); it3 != list3.end(); ++it3) {

	       text += "<br>";
	       text += (*it3);
	    }
	    text += "</i>";
	 }
	 text += "</qt>";

	 int left; colXPos(col, &left); //!!!left += cellWidth(col);
	 int top; rowYPos(row, &top);
	 int width = cellWidth(col);
	 int height = cellHeight(row);
      
	 r->setRect(left, top, width, height);
	 *s = text;
	 return;
      }
   }
}


QString LogTreeView::escapeHtmlTags(const QString & txt) {

   QString text = txt;
   text.replace(QRegExp("<"),"&lt;");
   text.replace(QRegExp(">"),"&gt;");
   return text;
}


void DynamicTip::maybeTip(const QPoint &pos) {
   if (!parentWidget()->inherits("LogTreeView")) {
      return;
   }
   LogTreeView *logtree = static_cast<LogTreeView*>(parentWidget());
  
   QRect r;
   QString s;
   logtree->lookupTip(pos, &r, &s);
   if (r.isValid()) {
      tip( r, s );
   }
}



syntax highlighted by Code2HTML, v. 0.9.1