/* This file is part of the KDE project Copyright (C) 2003 Ariya Hidayat Copyright (C) 2003 Norbert Andres Copyright (C) 2002 Laurent Montel Copyright (C) 1999 David Faure Copyright (C) 1999 Boris Wedl Copyright (C) 1998-2000 Torben Weis 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 #include #include #include #include #include #include #include #include // 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 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() ); 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( &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() == TQt::RightButton ) if( !d->readOnly ) emit contextMenu( ev->globalPos() ); } void KoTabBar::mouseReleaseEvent( TQMouseEvent* ev ) { if ( d->readOnly ) return; d->autoScroll = false; if ( ev->button() == TQt::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"