/* 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)tqlayout 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, tqlayout 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->tqcolorGroup().background(); if( active ) bg = TQBrush(tabbar->tqcolorGroup().base()); painter.setBrush( bg ); painter.setPen( TQPen( TQt::NoPen ) ); painter.drawPolygon( polygon ); // draw the lines painter.setPen( tabbar->tqcolorGroup().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->tqcolorGroup().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( tqcolorGroup().background() ); painter.tqbegin( TQT_TQPAINTDEVICE(&pm), this ); painter.setPen( tqcolorGroup().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::tqsizeHint() const { return TQSize( 40, tqstyle().tqpixelMetric( 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"