/* This file is part of the KDE project
   Copyright (C) 2003 Ariya Hidayat <ariya@kde.org>
   Copyright (C) 2003 Norbert Andres <nandres@web.de>
   Copyright (C) 2002 Laurent Montel <montel@kde.org>
   Copyright (C) 1999 David Faure <faure@kde.org>
   Copyright (C) 1999 Boris Wedl <boris.wedl@kfunigraz.ac.at>
   Copyright (C) 1998-2000 Torben Weis <weis@kde.org>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
*/

#include "KoTabBar.h"

#include <tqdrawutil.h>
#include <tqpainter.h>
#include <tqstring.h>
#include <tqstringlist.h>
#include <tqstyle.h>
#include <tqtimer.h>
#include <tqtoolbutton.h>
#include <tqvaluevector.h>
#include <tqwidget.h>

// TODO
// improvement possibilities
// - use offscreen buffer to reduce flicker even more
// - keep track of tabs, only (re)layout when necessary
// - paint all tabs to buffer, show only by shifting
// - customizable button pixmaps
// - use TQStyle to paint the tabs & buttons (is it good/possible?)


class KoTabBarPrivate
{
public:
    KoTabBar* tabbar;

    // scroll buttons
    TQToolButton* scrollFirstButton;
    TQToolButton* scrollLastButton;
    TQToolButton* scrollBackButton;
    TQToolButton* scrollForwardButton;

    // read-only: no mouse drag, double-click, right-click
    bool readOnly;
    
    // if true, layout is from right to left
    bool reverseLayout;

    // list of all tabs, in order of appearance
    TQStringList tabs;

    // array of TQRect for each visible tabs
    TQValueVector<TQRect> tabRects;

    // leftmost tab (or rightmost if reverseLayout)
    int firstTab;
    
    // rightmost tab (or leftmost if reverseLayout)
    int lastTab;

    // the active tab in the range form 1..n.
    // if this value is 0, that means that no tab is active.
    int activeTab;

    // unusable space on the left, taken by the scroll buttons
    int offset;

    // when the user drag the tab (in order to move it)
    // this is the target position, it's 0 if no tab is dragged
    int targetTab;

    // wheel movement since selected tab was last changed by the
    // mouse wheel
    int wheelDelta;

    // true if autoscroll is active
    bool autoScroll;

    // calculate the bounding rectangle for each visible tab
    void layoutTabs();

    // reposition scroll buttons
    void layoutButtons();

    // find a tab whose bounding rectangle contains the pos
    // return -1 if no such tab is found
    int tabAt( const TQPoint& pos );

    // draw a single tab
    void drawTab( TQPainter& painter, TQRect& rect, const TQString& text, bool active );

    // draw a marker to indicate tab moving
    void drawMoveMarker( TQPainter& painter, int x, int y );

    // update the enable/disable status of scroll buttons
    void updateButtons();

};

// built-in pixmap for scroll-first button
static const char * arrow_leftmost_xpm[] = {
"10 10 2 1",
" 	c None",
".	c #000000",
"          ",
"  .    .  ",
"  .   ..  ",
"  .  ...  ",
"  . ....  ",
"  .  ...  ",
"  .   ..  ",
"  .    .  ",
"          ",
"          "};

// built-in pixmap for scroll-last button
static const char * arrow_rightmost_xpm[] = {
"10 10 2 1",
" 	c None",
".	c #000000",
"          ",
"  .    .  ",
"  ..   .  ",
"  ...  .  ",
"  .... .  ",
"  ...  .  ",
"  ..   .  ",
"  .    .  ",
"          ",
"          "};

// built-in pixmap for scroll-left button
static const char * arrow_left_xpm[] = {
"10 10 2 1",
" 	c None",
".	c #000000",
"          ",
"      .   ",
"     ..   ",
"    ...   ",
"   ....   ",
"    ...   ",
"     ..   ",
"      .   ",
"          ",
"          "};

// built-in pixmap for scroll-right button
static const char * arrow_right_xpm[] = {
"10 10 2 1",
" 	c None",
".	c #000000",
"          ",
"   .      ",
"   ..     ",
"   ...    ",
"   ....   ",
"   ...    ",
"   ..     ",
"   .      ",
"          ",
"          "};


void KoTabBarPrivate::layoutTabs()
{
    tabRects.clear();

    TQPainter painter( tabbar );

    TQFont f = painter.font();
    f.setBold( true );
    painter.setFont( f );
    TQFontMetrics fm = painter.fontMetrics();
    
    if( !reverseLayout )
    {
        // left to right
        int x = 0;
        for( unsigned c = 0; c < tabs.count(); c++ )
        {
            TQRect rect;
            if( (int)c >= firstTab-1 )
            {
                TQString text = tabs[ c ];
                int tw = fm.width( text ) + 4;
                rect = TQRect( x, 0, tw + 20, tabbar->height() );
                x = x + tw + 20;
            }
            tabRects.append( rect );
        }

        lastTab = tabRects.count();
        for( unsigned i = 0; i < tabRects.count(); i++ )
            if( tabRects[i].right()-10+offset > tabbar->width() )
            {
                lastTab = i;
                break;
            }
    }
    else
    {
        // right to left
        int x = tabbar->width() - offset;
        for( unsigned c = 0; c < tabs.count(); c++ )
        {
            TQRect rect;
            if( (int)c >= firstTab-1 )
            {
                TQString text = tabs[ c ];
                int tw = fm.width( text ) + 4;
                rect = TQRect( x - tw - 20, 0, tw + 20, tabbar->height() );
                x = x - tw - 20;
            }
            tabRects.append( rect );
        }

        lastTab = tabRects.count();
        for( unsigned i = tabRects.count()-1; i>0; i-- )
            if( tabRects[i].left() > 0 )
            {
                lastTab = i+1;
                break;
            }
    }    
}

int KoTabBarPrivate::tabAt( const TQPoint& pos )
{
    for( unsigned i = 0; i < tabRects.count(); i++ )
    {
      TQRect rect = tabRects[ i ];
      if( rect.isNull() ) continue;
      if( rect.contains( pos ) ) return i;
    }

    return -1; // not found
}

void KoTabBarPrivate::drawTab( TQPainter& painter, TQRect& rect, const TQString& text, bool active )
{
    TQPointArray polygon;
    
    if( !reverseLayout )
        polygon.setPoints( 6, rect.x(), rect.y(),
            rect.x(), rect.bottom()-3,
            rect.x()+2, rect.bottom(),
            rect.right()-4, rect.bottom(),
            rect.right()-2, rect.bottom()-2,
            rect.right()+5, rect.top() );
    else      
        polygon.setPoints( 6, rect.right(), rect.top(),
            rect.right(), rect.bottom()-3,
            rect.right()-2, rect.bottom(),
            rect.x()+4, rect.bottom(),
            rect.x()+2, rect.bottom()-2,
            rect.x()-5, rect.top() );

    painter.save();

    // fill it first  
    TQBrush bg = tabbar->colorGroup().background();
    if( active ) bg = TQBrush(tabbar->colorGroup().base());
    painter.setBrush( bg );
    painter.setPen( TQPen( TQt::NoPen ) );
    painter.drawPolygon( polygon );

    // draw the lines
    painter.setPen( tabbar->colorGroup().dark() );
    if( !active )
      painter.drawLine( rect.x()-25, rect.y(), rect.right()+25, rect.top() );
    // TQt4: painter.setRenderHint( TQPainter::Antialiasing );
    painter.drawPolyline( polygon );

    painter.setPen( tabbar->colorGroup().buttonText() );
    TQFont f = painter.font();
    if( active ) f.setBold( true );
    painter.setFont( f );
    TQFontMetrics fm = painter.fontMetrics();
    int tx =  rect.x() + ( rect.width() - fm.width( text ) ) / 2;
    int ty =  rect.y() + ( rect.height() - fm.height() ) / 2 + fm.ascent();
    painter.drawText( tx, ty, text );

    painter.restore();
}

void KoTabBarPrivate::drawMoveMarker( TQPainter& painter, int x, int y )
{
    TQPointArray movmark;
    movmark.setPoints( 3, x, y, x + 7, y, x + 4, y + 6);
    TQBrush oldBrush = painter.brush();
    painter.setBrush( TQt::black );
    painter.drawPolygon(movmark);
    painter.setBrush( oldBrush );
}

void KoTabBarPrivate::layoutButtons()
{
    int bw = tabbar->height();
    int w = tabbar->width();
    offset = bw * 4;
    
    if( !reverseLayout )
    {
        scrollFirstButton->setGeometry( 0, 0, bw, bw );
        scrollFirstButton->setPixmap( arrow_leftmost_xpm );
        scrollBackButton->setGeometry( bw, 0, bw, bw );
        scrollBackButton->setPixmap( arrow_left_xpm );
        scrollForwardButton->setGeometry( bw*2, 0, bw, bw );
        scrollForwardButton->setPixmap( arrow_right_xpm );
        scrollLastButton->setGeometry( bw*3, 0, bw, bw );
        scrollLastButton->setPixmap( arrow_rightmost_xpm );
    }
    else
    {
        scrollFirstButton->setGeometry( w-bw, 0, bw, bw );
        scrollFirstButton->setPixmap( arrow_rightmost_xpm );
        scrollBackButton->setGeometry( w-2*bw, 0, bw, bw );
        scrollBackButton->setPixmap( arrow_right_xpm );
        scrollForwardButton->setGeometry( w-3*bw, 0, bw, bw );
        scrollForwardButton->setPixmap( arrow_left_xpm );
        scrollLastButton->setGeometry( w-4*bw, 0, bw, bw );
        scrollLastButton->setPixmap( arrow_leftmost_xpm );
    }
 }

void KoTabBarPrivate::updateButtons()
{
    scrollFirstButton->setEnabled( tabbar->canScrollBack() );
    scrollBackButton->setEnabled( tabbar->canScrollBack() );
    scrollForwardButton->setEnabled( tabbar->canScrollForward() );
    scrollLastButton->setEnabled( tabbar->canScrollForward() );
}

// creates a new tabbar
KoTabBar::KoTabBar( TQWidget* parent, const char* name )
    : TQWidget( parent, name, TQt::WResizeNoErase | TQt::WRepaintNoErase )
{
    d = new KoTabBarPrivate;
    d->tabbar = this;
    d->readOnly = false;
    d->reverseLayout = false;
    d->firstTab = 1;
    d->lastTab = 0;
    d->activeTab = 0;
    d->targetTab = 0;
    d->wheelDelta = 0;
    d->autoScroll = false;
    d->offset = 64;

    // initialize the scroll buttons
    d->scrollFirstButton = new TQToolButton( this );
    connect( d->scrollFirstButton, TQT_SIGNAL( clicked() ),
      this, TQT_SLOT( scrollFirst() ) );
    d->scrollLastButton = new TQToolButton( this );
    connect( d->scrollLastButton, TQT_SIGNAL( clicked() ),
      this, TQT_SLOT( scrollLast() ) );
    d->scrollBackButton = new TQToolButton( this );
    connect( d->scrollBackButton, TQT_SIGNAL( clicked() ),
      this, TQT_SLOT( scrollBack() ) );
    d->scrollForwardButton = new TQToolButton( this );
    connect( d->scrollForwardButton, TQT_SIGNAL( clicked() ),
      this, TQT_SLOT( scrollForward() ) );
    d->layoutButtons();
    d->updateButtons();
}

// destroys the tabbar
KoTabBar::~KoTabBar()
{
    delete d;
}

// adds a new visible tab
void KoTabBar::addTab( const TQString& text )
{
    d->tabs.append( text );

    update();
}

// removes a tab
void KoTabBar::removeTab( const TQString& text )
{
    int i = d->tabs.findIndex( text );
    if ( i == -1 ) return;

    if ( d->activeTab == i + 1 )
        d->activeTab = 0;

    d->tabs.remove( text );

    update();
}

// removes all tabs
void KoTabBar::clear()
{
    d->tabs.clear();
    d->activeTab = 0;
    d->firstTab = 1;

    update();
}

bool KoTabBar::readOnly() const
{
    return d->readOnly;
}

void KoTabBar::setReadOnly( bool ro )
{
    d->readOnly = ro;
}

bool KoTabBar::reverseLayout() const
{
    return d->reverseLayout;
}

void KoTabBar::setReverseLayout( bool reverse )
{
    if( reverse != d->reverseLayout )
    {
        d->reverseLayout = reverse;
        d->layoutTabs();
        d->layoutButtons();
        d->updateButtons();
        update();
    }
}

void KoTabBar::setTabs( const TQStringList& list )
{
    TQString left, active;

    if( d->activeTab > 0 )
        active = d->tabs[ d->activeTab-1 ];
    if( d->firstTab > 0 )
        left = d->tabs[ d->firstTab-1 ];

    d->tabs = list;

    if( !left.isNull() )
    {
        d->firstTab = d->tabs.findIndex( left ) + 1;
        if( d->firstTab > (int)d->tabs.count() )
            d->firstTab = 1;
        if( d->firstTab <= 0 )
            d->firstTab = 1;
    }

    d->activeTab = 0;
    if( !active.isNull() )
        setActiveTab( active );

    update();
}

TQStringList KoTabBar::tabs() const
{
    return d->tabs;
}

unsigned KoTabBar::count() const
{
    return d->tabs.count();
}

bool KoTabBar::canScrollBack() const
{
    if ( d->tabs.count() == 0 )
        return false;

    return d->firstTab > 1;
}

bool KoTabBar::canScrollForward() const
{
    if ( d->tabs.count() == 0 )
        return false;
        
    return d->lastTab < (int)d->tabs.count();
}

void KoTabBar::scrollBack()
{
    if ( !canScrollBack() )
        return;

    d->firstTab--;
    if( d->firstTab < 1 ) d->firstTab = 1;

    d->layoutTabs();
    d->updateButtons();
    update();
}

void KoTabBar::scrollForward()
{
    if ( !canScrollForward() )
        return;

    d->firstTab ++;
    if( d->firstTab > (int)d->tabs.count() )
        d->firstTab = d->tabs.count();

    d->layoutTabs();
    d->updateButtons();
    update();
}

void KoTabBar::scrollFirst()
{
    if ( !canScrollBack() )
        return;

    d->firstTab = 1;
    d->layoutTabs();
    d->updateButtons();
    update();
}

void KoTabBar::scrollLast()
{
    if ( !canScrollForward() )
        return;

    d->layoutTabs();

    if( !d->reverseLayout )
    {
        int fullWidth = d->tabRects[ d->tabRects.count()-1 ].right();
        int delta = fullWidth - width() + d->offset;
        for( unsigned i = 0; i < d->tabRects.count(); i++ )
            if( d->tabRects[i].x() > delta )
            {
                d->firstTab = i+1;
                break;
            }
    }
    else
    {
        // FIXME optimize this, perhaps without loop
        for( ; d->firstTab <= (int)d->tabRects.count();)
        {
            int x = d->tabRects[ d->tabRects.count()-1 ].x();
            if( x > 0 ) break;
            d->firstTab++;
            d->layoutTabs();
        }
    }

    d->layoutTabs();
    d->updateButtons();
    update();
}

void KoTabBar::ensureVisible( const TQString& tab )
{
    int i = d->tabs.findIndex( tab );
    if ( i == -1 )
        return;
    i++;

    // already visible, then do nothing
    if( ( i >= d->firstTab ) && ( i <= d->lastTab ) )
      return;

    if( i < d->firstTab )
        while( i < d->firstTab )
            scrollBack();

    if( i > d->lastTab )
        while( i > d->lastTab )
            scrollForward();
}

void KoTabBar::moveTab( unsigned tab, unsigned target )
{
    TQString tabName = d->tabs[ tab ];
    TQStringList::Iterator it;

    it = d->tabs.at( tab );
    d->tabs.remove( it );

    if( target > tab ) target--;
    it = d->tabs.at( target );
    if( target >= d->tabs.count() )
      it = d->tabs.end();
    d->tabs.insert( it, tabName );

    if( d->activeTab == (int)tab+1 )
        d->activeTab = target+1;

    update();
}

void KoTabBar::setActiveTab( const TQString& text )
{
    int i = d->tabs.findIndex( text );
    if ( i == -1 )
        return;

    if ( i + 1 == d->activeTab )
        return;

    d->activeTab = i + 1;
    d->updateButtons();
    update();

    emit tabChanged( text );
}

void KoTabBar::autoScrollBack()
{
    if( !d->autoScroll ) return;

    scrollBack();

    if( !canScrollBack() )
        d->autoScroll = false;
    else
        TQTimer::singleShot( 400, this, TQT_SLOT( autoScrollBack() ) );
}

void KoTabBar::autoScrollForward()
{
    if( !d->autoScroll ) return;

    scrollForward();

    if( !canScrollForward() )
        d->autoScroll = false;
    else
        TQTimer::singleShot( 400, this, TQT_SLOT( autoScrollForward() ) );
}

void KoTabBar::paintEvent( TQPaintEvent* )
{
    if ( d->tabs.count() == 0 )
    {
        erase();
        return;
    }

    TQPainter painter;
    TQPixmap pm( size() );
    pm.fill( colorGroup().background() );
    painter.begin( TQT_TQPAINTDEVICE(&pm), this );

    painter.setPen( colorGroup().dark() );
    painter.drawLine( 0, 0, width(), 0 );

    if( !d->reverseLayout )
        painter.translate( 5, 0 );

    d->layoutTabs();
    d->updateButtons();
    
    // draw first all non-active, visible tabs
    for( int c = d->tabRects.count()-1; c>=0; c-- )
    {
        TQRect rect = d->tabRects[ c ];
        if( rect.isNull() ) continue;
        TQString text = d->tabs[ c ];
        d->drawTab( painter, rect, text, false );
    }

    // draw the active tab
    if( d->activeTab > 0 )
    {
        TQRect rect = d->tabRects[ d->activeTab-1 ];
        if( !rect.isNull() )
        {
            TQString text = d->tabs[ d->activeTab-1 ];
            d->drawTab( painter, rect, text, true );
        }
    }

    // draw the move marker
    if( d->targetTab > 0 )
    {
        int p = TQMIN( d->targetTab, (int)d->tabRects.count() );
        TQRect rect = d->tabRects[ p-1 ];
        if( !rect.isNull() )
        {
            int x = !d->reverseLayout ? rect.x() : rect.right()-7;
            if( d->targetTab > (int)d->tabRects.count() )
              x = !d->reverseLayout ? rect.right()-7 : rect.x()-3;
            d->drawMoveMarker( painter, x, rect.y() );
        }
    }

    painter.end();
    
    if( !d->reverseLayout )
         bitBlt( this, d->offset, 0, &pm );
    else
         bitBlt( this, 0, 0, &pm );

}

void KoTabBar::resizeEvent( TQResizeEvent* )
{
    d->layoutButtons();
    d->updateButtons();
    update();
}

TQSize KoTabBar::sizeHint() const
{
    return TQSize( 40, style().pixelMetric( TQStyle::PM_ScrollBarExtent, this ) );
}

void KoTabBar::renameTab( const TQString& old_name, const TQString& new_name )
{
    TQStringList::Iterator it = d->tabs.find( old_name );
    (*it) = new_name;

    update();
}

TQString KoTabBar::activeTab() const
{
    if( d->activeTab == 0 )
        return TQString();
    else
        return d->tabs[ d->activeTab ];
}

void KoTabBar::mousePressEvent( TQMouseEvent* ev )
{
    if ( d->tabs.count() == 0 )
    {
        erase();
        return;
    }

    d->layoutTabs();

    TQPoint pos = ev->pos();
    if( !d->reverseLayout ) pos = pos - TQPoint( d->offset,0 );

    int tab = d->tabAt( pos ) + 1;
    if( ( tab > 0 ) && ( tab != d->activeTab ) )
    {
        d->activeTab = tab;
        update();

        emit tabChanged( d->tabs[ d->activeTab-1] );

        // scroll if partially visible
        if( d->tabRects[ tab-1 ].right() > width() - d->offset )
            scrollForward();
    }

    if( ev->button() == Qt::RightButton )
    if( !d->readOnly )
        emit contextMenu( ev->globalPos() );
}

void KoTabBar::mouseReleaseEvent( TQMouseEvent* ev )
{
    if ( d->readOnly ) return;

    d->autoScroll = false;

    if ( ev->button() == Qt::LeftButton && d->targetTab != 0 )
    {
        emit tabMoved( d->activeTab-1, d->targetTab-1 );
        d->targetTab = 0;
    }
}

void KoTabBar::mouseMoveEvent( TQMouseEvent* ev )
{
    if ( d->readOnly ) return;

    TQPoint pos = ev->pos();
    if( !d->reverseLayout) pos = pos - TQPoint( d->offset,0 );
    
    // check if user drags a tab to move it
    int i = d->tabAt( pos ) + 1;
    if( ( i > 0 ) && ( i != d->targetTab ) )
    {
        if( i == d->activeTab ) i = 0;
        if( i == d->activeTab+1 ) i = 0;

        if( i != d->targetTab )
        {
           d->targetTab = i;
           d->autoScroll = false;
           update();
        }
    }

    // drag past the very latest visible tab
    // e.g move a tab to the last ordering position
    TQRect r = d->tabRects[ d->tabRects.count()-1 ];
    bool moveToLast = false;
    if( r.isValid() )
    {
        if( !d->reverseLayout )
        if( pos.x() > r.right() )
        if( pos.x() < width() )
            moveToLast = true;
        if( d->reverseLayout )
        if( pos.x() < r.x() )
        if( pos.x() > 0 )
            moveToLast = true;
    }
    if( moveToLast )
    if( d->targetTab != (int)d->tabRects.count()+1 )
    {
        d->targetTab = d->tabRects.count()+1;
        d->autoScroll = false;
        update();
    }

    // outside far too left ? activate autoscroll...
    if ( pos.x() < 0 && !d->autoScroll  )
    {
        d->autoScroll = true;
        autoScrollBack();
    }

    // outside far too right ? activate autoscroll...
    int w = width() - d->offset;
    if ( pos.x() > w && !d->autoScroll )
    {
        d->autoScroll = true;
        autoScrollForward();
    }
}

void KoTabBar::mouseDoubleClickEvent( TQMouseEvent* ev )
{
    int offset = d->reverseLayout ? 0 : d->offset;
    if( ev->pos().x() > offset )
    if( !d->readOnly )
        emit doubleClicked();
}

void KoTabBar::wheelEvent( TQWheelEvent * e )
{
  if ( d->tabs.count() == 0 )
  {
    erase();
    return;
  }

  // Currently one wheel movement is a delta of 120.
  // The 'unused' delta is stored for devices that allow
  // a higher scrolling resolution.
  // The delta required to move one tab is one wheel movement:
  const int deltaRequired = 120;

  d->wheelDelta += e->delta();
  int tabDelta = - (d->wheelDelta / deltaRequired);
  d->wheelDelta = d->wheelDelta % deltaRequired;
  int numTabs = d->tabs.size();

  if(d->activeTab + tabDelta > numTabs)
  {
    // Would take us past the last tab
    d->activeTab = numTabs;
  }
  else if (d->activeTab + tabDelta < 1)
  {
    // Would take us before the first tab
    d->activeTab = 1;
  }
  else
  {
    d->activeTab = d->activeTab + tabDelta;
  }

  // Find the left and right edge of the new tab.  If we're
  // going forward, and the right of the new tab isn't visible
  // then scroll forward.  Likewise, if going back, and the 
  // left of the new tab isn't visible, then scroll back.
  int activeTabRight = d->tabRects[ d->activeTab-1 ].right();
  int activeTabLeft  = d->tabRects[ d->activeTab-1 ].left();
  if(tabDelta > 0 && activeTabRight > width() - d->offset )
  {
    scrollForward();
  }
  else if(tabDelta < 0 && activeTabLeft < width() - d->offset )
  {
    scrollBack();
  }

  update();
  emit tabChanged( d->tabs[ d->activeTab-1] );
}


#include "KoTabBar.moc"
