/* This file is part of the Konsole Terminal. Copyright 2006-2008 Robert Knight This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ //krazy:excludeall=qclasses // Own #include "ViewContainer.h" // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include #include #include #include #include // Konsole #include "IncrementalSearchBar.h" #include "ViewProperties.h" // TODO Perhaps move everything which is Konsole-specific into different files using namespace Konsole; ViewContainer::ViewContainer(NavigationPosition position , QObject* parent) : QObject(parent) , _navigationDisplayMode(AlwaysShowNavigation) , _navigationPosition(position) , _searchBar(0) { } ViewContainer::~ViewContainer() { foreach( QWidget* view , _views ) { disconnect(view,SIGNAL(destroyed(QObject*)),this,SLOT(viewDestroyed(QObject*))); } if (_searchBar) { _searchBar->deleteLater(); } emit destroyed(this); } void ViewContainer::moveViewWidget( int , int ) {} void ViewContainer::setFeatures(Features features) { _features = features; } ViewContainer::Features ViewContainer::features() const { return _features; } void ViewContainer::moveActiveView( MoveDirection direction ) { const int currentIndex = _views.indexOf( activeView() ) ; int newIndex = -1; switch ( direction ) { case MoveViewLeft: newIndex = qMax( currentIndex-1 , 0 ); break; case MoveViewRight: newIndex = qMin( currentIndex+1 , _views.count() -1 ); break; } Q_ASSERT( newIndex != -1 ); moveViewWidget( currentIndex , newIndex ); _views.swap(currentIndex,newIndex); setActiveView( _views[newIndex] ); } void ViewContainer::setNavigationDisplayMode(NavigationDisplayMode mode) { _navigationDisplayMode = mode; navigationDisplayModeChanged(mode); } ViewContainer::NavigationPosition ViewContainer::navigationPosition() const { return _navigationPosition; } void ViewContainer::setNavigationPosition(NavigationPosition position) { // assert that this position is supported Q_ASSERT( supportedNavigationPositions().contains(position) ); _navigationPosition = position; navigationPositionChanged(position); } QList ViewContainer::supportedNavigationPositions() const { return QList() << NavigationPositionTop; } ViewContainer::NavigationDisplayMode ViewContainer::navigationDisplayMode() const { return _navigationDisplayMode; } void ViewContainer::setNavigationTextMode(bool mode) { navigationTextModeChanged(mode); } void ViewContainer::addView(QWidget* view , ViewProperties* item, int index) { if (index == -1) _views.append(view); else _views.insert(index,view); _navigation[view] = item; connect( view , SIGNAL(destroyed(QObject*)) , this , SLOT(viewDestroyed(QObject*)) ); addViewWidget(view,index); emit viewAdded(view,item); } void ViewContainer::viewDestroyed(QObject* object) { QWidget* widget = static_cast(object); _views.removeAll(widget); _navigation.remove(widget); // FIXME This can result in ViewContainerSubClass::removeViewWidget() being // called after the widget's parent has been deleted or partially deleted // in the ViewContainerSubClass instance's destructor. // // Currently deleteLater() is used to remove child widgets in the subclass // constructors to get around the problem, but this is a hack and needs // to be fixed. removeViewWidget(widget); emit viewRemoved(widget); if (_views.count() == 0) emit empty(this); } void ViewContainer::removeView(QWidget* view) { _views.removeAll(view); _navigation.remove(view); disconnect( view , SIGNAL(destroyed(QObject*)) , this , SLOT(viewDestroyed(QObject*)) ); removeViewWidget(view); emit viewRemoved(view); if (_views.count() == 0) emit empty(this); } const QList ViewContainer::views() { return _views; } IncrementalSearchBar* ViewContainer::searchBar() { if (!_searchBar) { _searchBar = new IncrementalSearchBar(0); _searchBar->setVisible(false); connect(_searchBar, SIGNAL(destroyed(QObject*)), this, SLOT(searchBarDestroyed())); } return _searchBar; } void ViewContainer::searchBarDestroyed() { _searchBar = 0; } void ViewContainer::activateNextView() { QWidget* active = activeView(); int index = _views.indexOf(active); if ( index == -1 ) return; if ( index == _views.count() - 1 ) index = 0; else index++; setActiveView( _views.at(index) ); } void ViewContainer::activatePreviousView() { QWidget* active = activeView(); int index = _views.indexOf(active); if ( index == -1 ) return; if ( index == 0 ) index = _views.count() - 1; else index--; setActiveView( _views.at(index) ); } ViewProperties* ViewContainer::viewProperties( QWidget* widget ) { Q_ASSERT( _navigation.contains(widget) ); return _navigation[widget]; } QList ViewContainer::widgetsForItem(ViewProperties* item) const { return _navigation.keys(item); } ViewContainerTabBar::ViewContainerTabBar(QWidget* parent,TabbedViewContainer* container) : KTabBar(parent) , _container(container) , _dropIndicator(0) , _dropIndicatorIndex(-1) , _drawIndicatorDisabled(false) { setStyleSheet("QTabBar::tab { min-width: 2em; max-width: 25em }"); setElideMode(Qt::ElideLeft); } void ViewContainerTabBar::setDropIndicator(int index, bool drawDisabled) { if (!parentWidget() || _dropIndicatorIndex == index) return; _dropIndicatorIndex = index; const int ARROW_SIZE = 32; bool north = shape() == QTabBar::RoundedNorth || shape() == QTabBar::TriangularNorth; if (!_dropIndicator || _drawIndicatorDisabled != drawDisabled) { if (!_dropIndicator) { _dropIndicator = new QLabel(parentWidget()); _dropIndicator->resize(ARROW_SIZE,ARROW_SIZE); } QIcon::Mode drawMode = drawDisabled ? QIcon::Disabled : QIcon::Normal; const QString iconName = north ? "arrow-up" : "arrow-down"; _dropIndicator->setPixmap(KIcon(iconName).pixmap(ARROW_SIZE,ARROW_SIZE,drawMode)); _drawIndicatorDisabled = drawDisabled; } if (index < 0) { _dropIndicator->hide(); return; } const QRect rect = tabRect(index < count() ? index : index-1); QPoint pos; if (index < count()) pos = rect.topLeft(); else pos = rect.topRight(); if (north) pos.ry() += ARROW_SIZE; else pos.ry() -= ARROW_SIZE; pos.rx() -= ARROW_SIZE/2; _dropIndicator->move(mapTo(parentWidget(),pos)); _dropIndicator->show(); } void ViewContainerTabBar::dragLeaveEvent(QDragLeaveEvent*) { setDropIndicator(-1); } void ViewContainerTabBar::dragEnterEvent(QDragEnterEvent* event) { if (event->mimeData()->hasFormat(ViewProperties::mimeType()) && event->source() != 0) event->acceptProposedAction(); } void ViewContainerTabBar::dragMoveEvent(QDragMoveEvent* event) { if (event->mimeData()->hasFormat(ViewProperties::mimeType()) && event->source() != 0) { int index = dropIndex(event->pos()); if (index == -1) index = count(); setDropIndicator(index,proposedDropIsSameTab(event)); event->acceptProposedAction(); } } int ViewContainerTabBar::dropIndex(const QPoint& pos) const { int tab = tabAt(pos); if (tab < 0) return tab; // pick the closest tab boundary QRect rect = tabRect(tab); if ( (pos.x()-rect.left()) > (rect.width()/2) ) tab++; if (tab == count()) return -1; return tab; } bool ViewContainerTabBar::proposedDropIsSameTab(const QDropEvent* event) const { int index = dropIndex(event->pos()); int droppedId = ViewProperties::decodeMimeData(event->mimeData()); bool sameTabBar = event->source() == this; if (!sameTabBar) return false; const QList viewList = _container->views(); int sourceIndex = -1; for (int i=0;iviewProperties(viewList[i])->identifier(); if (idAtIndex == droppedId) sourceIndex = i; } bool sourceAndDropAreLast = sourceIndex == count()-1 && index == -1; if (sourceIndex == index || sourceIndex == index-1 || sourceAndDropAreLast) return true; else return false; } void ViewContainerTabBar::dropEvent(QDropEvent* event) { setDropIndicator(-1); if ( !event->mimeData()->hasFormat(ViewProperties::mimeType()) || proposedDropIsSameTab(event) ) { event->ignore(); return; } int index = dropIndex(event->pos()); int droppedId = ViewProperties::decodeMimeData(event->mimeData()); bool result = false; emit _container->moveViewRequest(index,droppedId,result); if (result) event->accept(); else event->ignore(); } QPixmap ViewContainerTabBar::dragDropPixmap(int tab) { Q_ASSERT(tab >= 0 && tab < count()); // TODO - grabWidget() works except that it includes part // of the tab bar outside the tab itself if the tab has // curved corners const QRect rect = tabRect(tab); const int borderWidth = 1; QPixmap tabPixmap(rect.width()+borderWidth, rect.height()+borderWidth); QPainter painter(&tabPixmap); painter.drawPixmap(0,0,QPixmap::grabWidget(this,rect)); QPen borderPen; borderPen.setBrush(palette().dark()); borderPen.setWidth(borderWidth); painter.setPen(borderPen); painter.drawRect(0,0,rect.width(),rect.height()); painter.end(); return tabPixmap; } TabbedViewContainer::TabbedViewContainer(NavigationPosition position , QObject* parent) : ViewContainer(position,parent) , _contextMenuTabIndex(0) { _containerWidget = new QWidget; _stackWidget = new QStackedWidget(); _tabBar = new ViewContainerTabBar(_containerWidget,this); _tabBar->setDrawBase(true); _tabBar->setDocumentMode(true); _tabBar->setFocusPolicy(Qt::NoFocus); _tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab); _newTabButton = new QToolButton(_containerWidget); _newTabButton->setIcon(KIcon("tab-new")); _newTabButton->adjustSize(); // new tab button is initially hidden, it will be shown when setFeatures() is called // with the QuickNewView flag enabled _newTabButton->setHidden(true); _closeTabButton = new QToolButton(_containerWidget); _closeTabButton->setIcon(KIcon("tab-close")); _closeTabButton->adjustSize(); _closeTabButton->setHidden(true); connect( _tabBar , SIGNAL(currentChanged(int)) , this , SLOT(currentTabChanged(int)) ); connect( _tabBar , SIGNAL(tabDoubleClicked(int)) , this , SLOT(tabDoubleClicked(int)) ); connect( _tabBar , SIGNAL(newTabRequest()) , this , SIGNAL(newViewRequest()) ); connect( _tabBar , SIGNAL(wheelDelta(int)) , this , SLOT(wheelScrolled(int)) ); connect( _tabBar , SIGNAL(initiateDrag(int)) , this , SLOT(startTabDrag(int)) ); connect( _tabBar, SIGNAL(contextMenu(int,QPoint)), this, SLOT(openTabContextMenu(int,QPoint)) ); connect( _newTabButton , SIGNAL(clicked()) , this , SIGNAL(newViewRequest()) ); connect( _closeTabButton , SIGNAL(clicked()) , this , SLOT(closeCurrentTab()) ); _layout = new TabbedViewContainerLayout; _layout->setSpacing(0); _layout->setContentsMargins(0, 0, 0, 0); _tabBarLayout = new QHBoxLayout; _tabBarLayout->setSpacing(0); _tabBarLayout->setContentsMargins(0, 0, 0, 0); _tabBarLayout->addWidget(_newTabButton); _tabBarLayout->addWidget(_tabBar); _tabBarLayout->addWidget(_closeTabButton); _layout->addWidget(_stackWidget); searchBar()->setParent(_containerWidget); if ( position == NavigationPositionTop ) { _layout->insertLayout(0,_tabBarLayout); _layout->insertWidget(-1,searchBar()); _tabBar->setShape(QTabBar::RoundedNorth); } else if ( position == NavigationPositionBottom ) { _layout->insertWidget(-1,searchBar()); _layout->insertLayout(-1,_tabBarLayout); _tabBar->setShape(QTabBar::RoundedSouth); } else Q_ASSERT(false); // position not supported _containerWidget->setLayout(_layout); _contextPopupMenu = new KMenu(_tabBar); _contextPopupMenu->addAction(KIcon("tab-detach"), i18nc("@action:inmenu", "&Detach Tab"), this, SLOT(tabContextMenuDetachTab())); _contextPopupMenu->addAction(KIcon(), i18nc("@action:inmenu", "&Rename Tab..."), this, SLOT(tabContextMenuRenameTab())); _contextPopupMenu->addAction(KIcon("tab-close"), i18nc("@action:inmenu", "&Close Tab"), this, SLOT(tabContextMenuCloseTab())); } void TabbedViewContainer::setNewViewMenu(QMenu* menu) { _newTabButton->setMenu(menu); } ViewContainer::Features TabbedViewContainer::supportedFeatures() const { return QuickNewView|QuickCloseView; } void TabbedViewContainer::setFeatures(Features features) { ViewContainer::setFeatures(features); const bool tabBarHidden = _tabBar->isHidden(); _newTabButton->setVisible(!tabBarHidden && (features & QuickNewView)); _closeTabButton->setVisible(!tabBarHidden && (features & QuickCloseView)); } void TabbedViewContainer::closeCurrentTab() { if (_stackWidget->currentIndex() != -1) { emit closeTab(this, _stackWidget->widget(_stackWidget->currentIndex())); } } void TabbedViewContainer::setTabBarVisible(bool visible) { _tabBar->setVisible(visible); _newTabButton->setVisible(visible && (features() & QuickNewView)); _closeTabButton->setVisible(visible && (features() & QuickCloseView)); } QList TabbedViewContainer::supportedNavigationPositions() const { return QList() << NavigationPositionTop << NavigationPositionBottom; } void TabbedViewContainer::navigationPositionChanged(NavigationPosition position) { // this method assumes that there are only two items // in the layout Q_ASSERT( _layout->count() == 3 ); // index of stack widget in the layout when tab bar is at the bottom const int StackIndexWithTabBottom = 0; if ( position == NavigationPositionTop && _layout->indexOf(_stackWidget) == StackIndexWithTabBottom ) { _layout->removeItem(_tabBarLayout); _layout->removeWidget(searchBar()); _layout->insertLayout(0,_tabBarLayout); _layout->insertWidget(-1,searchBar()); _tabBar->setShape(QTabBar::RoundedNorth); } else if ( position == NavigationPositionBottom && _layout->indexOf(_stackWidget) != StackIndexWithTabBottom ) { _layout->removeItem(_tabBarLayout); _layout->removeWidget(searchBar()); _layout->insertWidget(-1,searchBar()); _layout->insertLayout(-1,_tabBarLayout); _tabBar->setShape(QTabBar::RoundedSouth); } } void TabbedViewContainer::navigationDisplayModeChanged(NavigationDisplayMode mode) { if ( mode == AlwaysShowNavigation && _tabBar->isHidden() ) setTabBarVisible(true); if ( mode == AlwaysHideNavigation && !_tabBar->isHidden() ) setTabBarVisible(false); if ( mode == ShowNavigationAsNeeded ) dynamicTabBarVisibility(); } void TabbedViewContainer::dynamicTabBarVisibility() { if ( _tabBar->count() > 1 && _tabBar->isHidden() ) setTabBarVisible(true); if ( _tabBar->count() < 2 && !_tabBar->isHidden() ) setTabBarVisible(false); } void TabbedViewContainer::navigationTextModeChanged(bool useTextWidth) { if (useTextWidth) { _tabBar->setStyleSheet("QTabBar::tab { }"); _tabBar->setExpanding(false); _tabBar->setElideMode(Qt::ElideNone); } else { _tabBar->setStyleSheet("QTabBar::tab { min-width: 2em; max-width: 25em }"); _tabBar->setExpanding(true); _tabBar->setElideMode(Qt::ElideLeft); } } TabbedViewContainer::~TabbedViewContainer() { if (!_containerWidget.isNull()) _containerWidget->deleteLater(); } void TabbedViewContainer::startTabDrag(int tab) { QDrag* drag = new QDrag(_tabBar); const QRect tabRect = _tabBar->tabRect(tab); QPixmap tabPixmap = _tabBar->dragDropPixmap(tab); drag->setPixmap(tabPixmap); // offset the tab position so the tab will follow the cursor exactly // where it was clicked (as opposed to centering on the origin of the pixmap) QPoint mappedPos = _tabBar->mapFromGlobal(QCursor::pos()); mappedPos.rx() -= tabRect.x(); drag->setHotSpot(mappedPos); int id = viewProperties(views()[tab])->identifier(); QWidget* view = views()[tab]; drag->setMimeData(ViewProperties::createMimeData(id)); // start drag, if drag-and-drop is successful the view at 'tab' will be // deleted // // if the tab was dragged onto another application // which blindly accepted the drop then ignore it if (drag->exec() == Qt::MoveAction && drag->target() != 0) { // Deleting the view may cause the view container to be deleted, which // will also delete the QDrag object. // This can cause a crash if Qt's internal drag-and-drop handling // tries to delete it later. // // For now set the QDrag's parent to 0 so that it won't be deleted if // this view container is destroyed. // // FIXME: Resolve this properly drag->setParent(0); removeView(view); } } void TabbedViewContainer::tabDoubleClicked(int index) { renameTab(index); } void TabbedViewContainer::renameTab(int index) { viewProperties(views()[index])->rename(); } void TabbedViewContainer::openTabContextMenu(int index, const QPoint& pos) { _contextMenuTabIndex = index; // Enable 'Detach Tab' menu item only if there is more than 1 tab QList menuActions = _contextPopupMenu->actions(); if (_tabBar->count() == 1) menuActions.first()->setEnabled(false); else menuActions.first()->setEnabled(true); _contextPopupMenu->exec(pos); } void TabbedViewContainer::tabContextMenuCloseTab() { _tabBar->setCurrentIndex(_contextMenuTabIndex);// Required for this to work emit closeTab(this, _stackWidget->widget(_contextMenuTabIndex)); } void TabbedViewContainer::tabContextMenuDetachTab() { emit detachTab(this, _stackWidget->widget(_contextMenuTabIndex)); } void TabbedViewContainer::tabContextMenuRenameTab() { renameTab(_contextMenuTabIndex); } void TabbedViewContainer::moveViewWidget( int fromIndex , int toIndex ) { QString text = _tabBar->tabText(fromIndex); QIcon icon = _tabBar->tabIcon(fromIndex); // FIXME - This will lose properties of the tab other than // their text and icon when moving them _tabBar->removeTab(fromIndex); _tabBar->insertTab(toIndex,icon,text); QWidget* widget = _stackWidget->widget(fromIndex); _stackWidget->removeWidget(widget); _stackWidget->insertWidget(toIndex,widget); } void TabbedViewContainer::currentTabChanged(int index) { _stackWidget->setCurrentIndex(index); if (_stackWidget->widget(index)) emit activeViewChanged(_stackWidget->widget(index)); // clear activity indicators setTabActivity(index,false); } void TabbedViewContainer::wheelScrolled(int delta) { if ( delta < 0 ) activateNextView(); else activatePreviousView(); } QWidget* TabbedViewContainer::containerWidget() const { return _containerWidget; } QWidget* TabbedViewContainer::activeView() const { return _stackWidget->currentWidget(); } void TabbedViewContainer::setActiveView(QWidget* view) { const int index = _stackWidget->indexOf(view); Q_ASSERT( index != -1 ); _stackWidget->setCurrentWidget(view); _tabBar->setCurrentIndex(index); } void TabbedViewContainer::addViewWidget( QWidget* view , int index) { _stackWidget->insertWidget(index,view); _stackWidget->updateGeometry(); ViewProperties* item = viewProperties(view); connect( item , SIGNAL(titleChanged(ViewProperties*)) , this , SLOT(updateTitle(ViewProperties*))); connect( item , SIGNAL(iconChanged(ViewProperties*)) , this , SLOT(updateIcon(ViewProperties*))); connect( item , SIGNAL(activity(ViewProperties*)) , this , SLOT(updateActivity(ViewProperties*))); _tabBar->insertTab( index , item->icon() , item->title() ); if ( navigationDisplayMode() == ShowNavigationAsNeeded ) dynamicTabBarVisibility(); } void TabbedViewContainer::removeViewWidget( QWidget* view ) { if (!_stackWidget) return; const int index = _stackWidget->indexOf(view); Q_ASSERT( index != -1 ); _stackWidget->removeWidget(view); _tabBar->removeTab(index); if ( navigationDisplayMode() == ShowNavigationAsNeeded ) dynamicTabBarVisibility(); } void TabbedViewContainer::setTabActivity(int index , bool activity) { const QPalette& palette = _tabBar->palette(); KColorScheme colorScheme(palette.currentColorGroup()); const QColor colorSchemeActive = colorScheme.foreground(KColorScheme::ActiveText).color(); const QColor normalColor = palette.text().color(); const QColor activityColor = KColorUtils::mix(normalColor,colorSchemeActive); QColor color = activity ? activityColor : QColor(); if ( color != _tabBar->tabTextColor(index) ) _tabBar->setTabTextColor(index,color); } void TabbedViewContainer::updateActivity(ViewProperties* item) { QListIterator iter(widgetsForItem(item)); while ( iter.hasNext() ) { const int index = _stackWidget->indexOf(iter.next()); if ( index != _stackWidget->currentIndex() ) { setTabActivity(index,true); } } } void TabbedViewContainer::updateTitle(ViewProperties* item) { QListIterator iter(widgetsForItem(item)); while ( iter.hasNext() ) { const int index = _stackWidget->indexOf( iter.next() ); QString tabText = item->title(); _tabBar->setTabToolTip( index , tabText ); // To avoid having & replaced with _ (shortcut indicator) tabText.replace('&', "&&"); _tabBar->setTabText( index , tabText ); } } void TabbedViewContainer::updateIcon(ViewProperties* item) { QListIterator iter(widgetsForItem(item)); while ( iter.hasNext() ) { const int index = _stackWidget->indexOf( iter.next() ); _tabBar->setTabIcon( index , item->icon() ); } } StackedViewContainer::StackedViewContainer(QObject* parent) : ViewContainer(NavigationPositionTop,parent) { _containerWidget = new QWidget; QVBoxLayout *layout = new QVBoxLayout(_containerWidget); _stackWidget = new QStackedWidget(_containerWidget); searchBar()->setParent(_containerWidget); layout->addWidget(searchBar()); layout->addWidget(_stackWidget); layout->setContentsMargins(0, 0, 0, 0); } StackedViewContainer::~StackedViewContainer() { if (!_containerWidget.isNull()) _containerWidget->deleteLater(); } QWidget* StackedViewContainer::containerWidget() const { return _containerWidget; } QWidget* StackedViewContainer::activeView() const { return _stackWidget->currentWidget(); } void StackedViewContainer::setActiveView(QWidget* view) { _stackWidget->setCurrentWidget(view); } void StackedViewContainer::addViewWidget( QWidget* view , int ) { _stackWidget->addWidget(view); } void StackedViewContainer::removeViewWidget( QWidget* view ) { if (!_stackWidget) return; const int index = _stackWidget->indexOf(view); Q_ASSERT( index != -1); Q_UNUSED(index); _stackWidget->removeWidget(view); } #include "ViewContainer.moc"