/*************************************************************************** * Copyright (C) 2007 by Pino Toscano * * Copyright (C) 2009 by Eike Hein * * * * 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. * ***************************************************************************/ #include "sidebar.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "settings.h" static const int SidebarItemType = QListWidgetItem::UserType + 1; /* List item representing a sidebar entry. */ class SidebarItem : public QListWidgetItem { public: SidebarItem( QWidget* w, const QIcon &icon, const QString &text ) : QListWidgetItem( 0, SidebarItemType ), m_widget( w ) { setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled ); setIcon( icon ); setText( text ); setToolTip( text ); } QWidget* widget() const { return m_widget; } private: QWidget *m_widget; }; /* A simple delegate to paint the icon of each item */ #define ITEM_MARGIN_LEFT 5 #define ITEM_MARGIN_TOP 5 #define ITEM_MARGIN_RIGHT 5 #define ITEM_MARGIN_BOTTOM 5 #define ITEM_PADDING 5 class SidebarDelegate : public QAbstractItemDelegate { Q_OBJECT public: SidebarDelegate( QObject *parent = Q_NULLPTR ); ~SidebarDelegate() override; void setShowText( bool show ); bool isTextShown() const; // from QAbstractItemDelegate void paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const override; QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const override; private slots: void updateBrushCache(); private: bool m_showText; QScopedPointer m_windowBackground; QScopedPointer m_windowForeground; QScopedPointer m_selectionBackground; QScopedPointer m_selectionForeground; }; SidebarDelegate::SidebarDelegate( QObject *parent ) : QAbstractItemDelegate( parent ), m_showText( true ), m_windowBackground( 0 ), m_windowForeground( 0 ), m_selectionBackground( 0 ), m_selectionForeground( 0 ) { updateBrushCache(); connect(qApp, &QGuiApplication::paletteChanged, this, &SidebarDelegate::updateBrushCache); } SidebarDelegate::~SidebarDelegate() { } void SidebarDelegate::setShowText( bool show ) { m_showText = show; } bool SidebarDelegate::isTextShown() const { return m_showText; } void SidebarDelegate::updateBrushCache() { m_windowBackground.reset(new KStatefulBrush(KColorScheme::Window, KColorScheme::NormalBackground)); m_windowForeground.reset(new KStatefulBrush(KColorScheme::Window, KColorScheme::NormalText)); m_selectionBackground.reset(new KStatefulBrush(KColorScheme::Selection, KColorScheme::NormalBackground)); m_selectionForeground.reset(new KStatefulBrush(KColorScheme::Selection, KColorScheme::NormalText)); } void SidebarDelegate::paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const { QBrush backBrush; QColor foreColor; bool disabled = false; bool hover = false; if ( !( option.state & QStyle::State_Enabled ) ) { backBrush = m_windowBackground->brush(QPalette::Disabled); foreColor = m_windowForeground->brush(QPalette::Disabled).color(); disabled = true; } else if ( option.state & ( QStyle::State_HasFocus | QStyle::State_Selected ) ) { backBrush = m_selectionBackground->brush(option.palette); foreColor = m_selectionForeground->brush(option.palette).color(); } else if ( option.state & QStyle::State_MouseOver ) { backBrush = m_selectionBackground->brush(option.palette).color().light( 115 ); foreColor = m_selectionForeground->brush(option.palette).color(); hover = true; } else /*if ( option.state & QStyle::State_Enabled )*/ { backBrush = m_windowBackground->brush(option.palette); foreColor = m_windowForeground->brush(option.palette).color(); } QStyle *style = QApplication::style(); QStyleOptionViewItem opt( option ); // KStyle provides an "hover highlight" effect for free; // but we want that for non-KStyle-based styles too if ( !style->inherits( "KStyle" ) && hover ) { Qt::BrushStyle bs = opt.backgroundBrush.style(); if ( bs > Qt::NoBrush && bs < Qt::TexturePattern ) opt.backgroundBrush = opt.backgroundBrush.color().light( 115 ); else opt.backgroundBrush = backBrush; } painter->save(); style->drawPrimitive( QStyle::PE_PanelItemViewItem, &opt, painter, 0 ); painter->restore(); QIcon icon = index.data( Qt::DecorationRole ).value< QIcon >(); if ( !icon.isNull() ) { QPoint iconpos( ( option.rect.width() - option.decorationSize.width() ) / 2, ITEM_MARGIN_TOP ); iconpos += option.rect.topLeft(); QIcon::Mode iconmode = disabled ? QIcon::Disabled : QIcon::Normal; painter->drawPixmap( iconpos, icon.pixmap( option.decorationSize, iconmode ) ); } if ( m_showText ) { QString text = index.data( Qt::DisplayRole ).toString(); QRect fontBoundaries = QFontMetrics( option.font ).boundingRect( text ); QPoint textPos( ITEM_MARGIN_LEFT + ( option.rect.width() - ITEM_MARGIN_LEFT - ITEM_MARGIN_RIGHT - fontBoundaries.width() ) / 2, ITEM_MARGIN_TOP + option.decorationSize.height() + ITEM_PADDING ); fontBoundaries.translate( -fontBoundaries.topLeft() ); fontBoundaries.translate( textPos ); fontBoundaries.translate( option.rect.topLeft() ); painter->setPen( foreColor ); painter->drawText( fontBoundaries, Qt::AlignCenter, text ); } } QSize SidebarDelegate::sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const { QSize baseSize( option.decorationSize.width(), option.decorationSize.height() ); if ( m_showText ) { QRect fontBoundaries = QFontMetrics( option.font ).boundingRect( index.data( Qt::DisplayRole ).toString() ); baseSize.setWidth( qMax( fontBoundaries.width(), baseSize.width() ) ); baseSize.setHeight( baseSize.height() + fontBoundaries.height() + ITEM_PADDING ); } return baseSize + QSize( ITEM_MARGIN_LEFT + ITEM_MARGIN_RIGHT, ITEM_MARGIN_TOP + ITEM_MARGIN_BOTTOM ); } /* A custom list widget that ignores the events for disabled items */ class SidebarListWidget : public QListWidget { public: SidebarListWidget( QWidget *parent = Q_NULLPTR ); ~SidebarListWidget() override; int countVisible() const { int ret = 0; for ( int i = 0, c = count(); i < c; ++i ) { ret += !item(i)->isHidden(); } return ret; } protected: // from QListWidget void mouseDoubleClickEvent( QMouseEvent *event ) override; void mouseMoveEvent( QMouseEvent *event ) override; void mousePressEvent( QMouseEvent *event ) override; void mouseReleaseEvent( QMouseEvent *event ) override; QModelIndex moveCursor( QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers ) override; private: // These two are used to keep track of the row an initial mousePress- // Event() occurs on and the row the cursor moves over while the left // mouse button is pressed, respectively, as well as for event compre- // ssion, to avoid calling SideBar::itemClicked() multiple times for // the same item in a row on mouseMoveEvent()'s. This code is written // under the assumption that the number and positions of items in the // list won't change while the user interacts with it using the mouse. // Future work here must see to that this assumption continues to hold // up, or achieve calling SideBar::itemClicked() differently. int mousePressedRow; int rowUnderMouse; }; SidebarListWidget::SidebarListWidget( QWidget *parent ) : QListWidget( parent ) { mousePressedRow = -1; rowUnderMouse = -1; } SidebarListWidget::~SidebarListWidget() { } void SidebarListWidget::mouseDoubleClickEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() && !( index.flags() & Qt::ItemIsSelectable ) ) return; QListWidget::mouseDoubleClickEvent( event ); } void SidebarListWidget::mouseMoveEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() ) { if ( index.flags() & Qt::ItemIsSelectable ) { if ( event->buttons() & Qt::LeftButton && index.row() != mousePressedRow && index.row() != rowUnderMouse ) { mousePressedRow = -1; rowUnderMouse = index.row(); QMetaObject::invokeMethod(parent(), "itemClicked", Qt::DirectConnection, Q_ARG(QListWidgetItem*, item(index.row()))); } } else return; } QListWidget::mouseMoveEvent( event ); } void SidebarListWidget::mousePressEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() ) { if ( index.flags() & Qt::ItemIsSelectable ) { if ( event->buttons() & Qt::LeftButton ) mousePressedRow = index.row(); } else return; } QListWidget::mousePressEvent( event ); } void SidebarListWidget::mouseReleaseEvent( QMouseEvent *event ) { QModelIndex index = indexAt( event->pos() ); if ( index.isValid() ) { if ( index.flags() & Qt::ItemIsSelectable ) { if ( event->button() == Qt::LeftButton && index.row() != rowUnderMouse ) { QMetaObject::invokeMethod(parent(), "itemClicked", Qt::DirectConnection, Q_ARG(QListWidgetItem*, item(index.row()))); } } else { mousePressedRow = -1; rowUnderMouse = -1; return; } } mousePressedRow = -1; rowUnderMouse = -1; QListWidget::mouseReleaseEvent( event ); } QModelIndex SidebarListWidget::moveCursor( QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers ) { Q_UNUSED( modifiers ) QModelIndex oldindex = currentIndex(); QModelIndex newindex = oldindex; switch ( cursorAction ) { case MoveUp: case MovePrevious: { int row = oldindex.row() - 1; while ( row > -1 && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) --row; if ( row > -1 ) newindex = model()->index( row, 0 ); break; } case MoveDown: case MoveNext: { int row = oldindex.row() + 1; int max = model()->rowCount(); while ( row < max && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) ++row; if ( row < max ) newindex = model()->index( row, 0 ); break; } case MoveHome: case MovePageUp: { int row = 0; while ( row < oldindex.row() && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) ++row; if ( row < oldindex.row() ) newindex = model()->index( row, 0 ); break; } case MoveEnd: case MovePageDown: { int row = model()->rowCount() - 1; while ( row > oldindex.row() && !( model()->index( row, 0 ).flags() & Qt::ItemIsSelectable ) ) --row; if ( row > oldindex.row() ) newindex = model()->index( row, 0 ); break; } // no navigation possible for these case MoveLeft: case MoveRight: break; } // dirty hack to change item when the key cursor changes item if ( oldindex != newindex ) { emit itemClicked( itemFromIndex( newindex ) ); } return newindex; } /* Private storage. */ class Sidebar::Private { public: Private() : sideWidget( 0 ), bottomWidget( 0 ), splitterSizesSet( false ), itemsHeight( 0 ) { } int indexOf(QWidget *w) const { for (int i = 0; i < pages.count(); ++i) { if (pages[i]->widget() == w) return i; } return -1; } void adjustListSize( bool recalc, bool expand = true ); SidebarListWidget *list; QSplitter *splitter; QStackedWidget *stack; QWidget *sideContainer; QLabel *sideTitle; QVBoxLayout *vlay; QWidget *sideWidget; QWidget *bottomWidget; QList< SidebarItem* > pages; bool splitterSizesSet; int itemsHeight; SidebarDelegate *sideDelegate; }; void Sidebar::Private::adjustListSize( bool recalc, bool expand ) { QSize bottomElemSize( list->sizeHintForIndex( list->model()->index( list->count() - 1, 0 ) ) ); if ( recalc ) { int w = 0; for ( int i = 0; i < list->count(); ++i ) { QSize s = list->sizeHintForIndex( list->model()->index( i, 0 ) ); if ( s.width() > w ) w = s.width(); } bottomElemSize.setWidth( w ); } itemsHeight = bottomElemSize.height() * list->countVisible(); list->setMinimumHeight( itemsHeight + list->frameWidth() * 2 ); int curWidth = list->minimumWidth(); int newWidth = expand ? qMax( bottomElemSize.width() + list->frameWidth() * 2, curWidth ) : qMin( bottomElemSize.width() + list->frameWidth() * 2, curWidth ); list->setFixedWidth( newWidth ); } Sidebar::Sidebar( QWidget *parent ) : QWidget( parent ), d( new Private ) { QHBoxLayout *mainlay = new QHBoxLayout( this ); mainlay->setMargin( 0 ); mainlay->setSpacing( 0 ); setAutoFillBackground( true ); setAcceptDrops( true ); d->list = new SidebarListWidget( this ); mainlay->addWidget( d->list ); d->list->setMouseTracking( true ); d->list->viewport()->setAttribute( Qt::WA_Hover ); d->sideDelegate = new SidebarDelegate( d->list ); d->sideDelegate->setShowText( Okular::Settings::sidebarShowText() ); d->list->setItemDelegate( d->sideDelegate ); d->list->setUniformItemSizes( true ); d->list->setSelectionMode( QAbstractItemView::SingleSelection ); int iconsize = Okular::Settings::sidebarIconSize(); d->list->setIconSize( QSize( iconsize, iconsize ) ); d->list->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); d->list->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); d->list->setContextMenuPolicy( Qt::CustomContextMenu ); d->list->viewport()->setAutoFillBackground( false ); d->splitter = new QSplitter( this ); mainlay->addWidget( d->splitter ); d->splitter->setOpaqueResize( true ); d->splitter->setChildrenCollapsible( false ); d->sideContainer = new QWidget( d->splitter ); d->sideContainer->setMinimumWidth( 90 ); d->sideContainer->setMaximumWidth( 600 ); d->vlay = new QVBoxLayout( d->sideContainer ); d->vlay->setMargin( 0 ); d->sideTitle = new QLabel( d->sideContainer ); d->vlay->addWidget( d->sideTitle ); QFont tf = d->sideTitle->font(); tf.setBold( true ); d->sideTitle->setFont( tf ); d->sideTitle->setMargin( 3 ); d->sideTitle->setIndent( 3 ); d->stack = new QStackedWidget( d->sideContainer ); d->vlay->addWidget( d->stack ); connect(d->list, &SidebarListWidget::customContextMenuRequested, this, &Sidebar::listContextMenu); connect(d->splitter, &QSplitter::splitterMoved, this, &Sidebar::splitterMoved); setCollapsed( true ); setFocusProxy( d->list ); } Sidebar::~Sidebar() { delete d; } int Sidebar::addItem( QWidget *widget, const QIcon &icon, const QString &text ) { if ( !widget ) return -1; SidebarItem *newitem = new SidebarItem( widget, icon, text ); d->list->addItem( newitem ); d->pages.append( newitem ); widget->setParent( d->stack ); d->stack->addWidget( widget ); // updating the minimum height of the icon view, so all are visible with no scrolling d->adjustListSize( false, true ); return d->pages.count() - 1; } void Sidebar::setMainWidget( QWidget *widget ) { delete d->sideWidget; d->sideWidget = widget; if ( d->sideWidget ) { // setting the splitter as parent for the widget automatically plugs it // into the splitter, neat! d->sideWidget->setParent( d->splitter ); if ( !d->splitterSizesSet ) { QList splitterSizes = Okular::Settings::splitterSizes(); if ( !splitterSizes.count() ) { // the first time use 1/10 for the panel and 9/10 for the pageView splitterSizes.push_back( 50 ); splitterSizes.push_back( 500 ); } d->splitter->setSizes( splitterSizes ); d->splitterSizesSet = true; } } } void Sidebar::setBottomWidget( QWidget *widget ) { delete d->bottomWidget; d->bottomWidget = widget; if ( d->bottomWidget ) { d->bottomWidget->setParent( this ); d->vlay->addWidget( d->bottomWidget ); } } void Sidebar::setItemEnabled( QWidget *widget, bool enabled ) { const int index = d->indexOf( widget ); setIndexEnabled( index, enabled ); } void Sidebar::setIndexEnabled( int index, bool enabled ) { if ( index < 0 || index >= d->pages.count() ) return; Qt::ItemFlags f = d->pages.at( index )->flags(); if ( enabled ) { f |= Qt::ItemIsEnabled; f |= Qt::ItemIsSelectable; } else { f &= ~Qt::ItemIsEnabled; f &= ~Qt::ItemIsSelectable; } d->pages.at( index )->setFlags( f ); if ( !enabled && index == d->list->currentRow() && isSidebarVisible() ) // find an enabled item, and select that one for ( int i = 0; i < d->pages.count(); ++i ) if ( d->pages.at(i)->flags() & Qt::ItemIsEnabled ) { setCurrentIndex( i ); break; } } bool Sidebar::isItemEnabled( QWidget *widget ) const { const int index = d->indexOf( widget ); return isIndexEnabled( index ); } bool Sidebar::isIndexEnabled( int index ) const { if ( index < 0 ) return false; Qt::ItemFlags f = d->pages.at( index )->flags(); return ( f & Qt::ItemIsEnabled ) == Qt::ItemIsEnabled; } void Sidebar::setCurrentItem( QWidget *widget, SetCurrentItemBehaviour b ) { const int index = d->indexOf( widget ); setCurrentIndex( index, b ); } void Sidebar::setCurrentIndex( int index, SetCurrentItemBehaviour b ) { if ( index < 0 || !isIndexEnabled( index ) ) return; itemClicked( d->pages.at( index ), b ); QModelIndex modelindex = d->list->model()->index( index, 0 ); d->list->setCurrentIndex( modelindex ); d->list->selectionModel()->select( modelindex, QItemSelectionModel::ClearAndSelect ); } QWidget *Sidebar::currentItem() const { const int row = d->list->currentRow(); if (row < 0 || row >= d->pages.count()) return 0; return d->pages[row]->widget(); } void Sidebar::setSidebarVisibility( bool visible ) { if ( visible != d->list->isHidden() ) return; static bool wasCollapsed = isCollapsed(); d->list->setHidden( !visible ); if ( visible ) { setCollapsed( wasCollapsed ); wasCollapsed = false; } else { wasCollapsed = isCollapsed(); setCollapsed( true ); } } bool Sidebar::isSidebarVisible() const { return !d->list->isHidden(); } void Sidebar::setCollapsed( bool collapsed ) { d->sideContainer->setHidden( collapsed ); } bool Sidebar::isCollapsed() const { return d->sideContainer->isHidden(); } void Sidebar::moveSplitter(int sideWidgetSize) { QList splitterSizeList = d->splitter->sizes(); const int total = splitterSizeList.at( 0 ) + splitterSizeList.at( 1 ); splitterSizeList.replace( 0, total - sideWidgetSize ); splitterSizeList.replace( 1, sideWidgetSize ); d->splitter->setSizes( splitterSizeList ); } void Sidebar::setItemVisible( QWidget *widget, bool visible ) { const int index = d->indexOf( widget ); if ( index < 0 ) return; d->list->setRowHidden( index, !visible ); setIndexEnabled( index, visible ); } void Sidebar::itemClicked( QListWidgetItem *item ) { itemClicked( item, UncollapseIfCollapsed ); } void Sidebar::itemClicked( QListWidgetItem *item, SetCurrentItemBehaviour b ) { if ( !item ) return; SidebarItem* sbItem = dynamic_cast< SidebarItem* >( item ); if ( !sbItem ) return; if ( sbItem->widget() == d->stack->currentWidget() ) { if ( !isCollapsed() ) { d->list->selectionModel()->clear(); setCollapsed( true ); } else { if ( b == UncollapseIfCollapsed ) { setCollapsed( false ); d->list->show(); } } } else { if ( isCollapsed() && b == UncollapseIfCollapsed ) { setCollapsed( false ); d->list->show(); } d->stack->setCurrentWidget( sbItem->widget() ); d->sideTitle->setText( sbItem->toolTip() ); } } void Sidebar::splitterMoved( int /*pos*/, int index ) { // if the side panel has been resized, save splitter sizes if ( index == 1 ) saveSplitterSize(); } void Sidebar::saveSplitterSize() const { Okular::Settings::setSplitterSizes( d->splitter->sizes() ); Okular::Settings::self()->save(); } void Sidebar::listContextMenu( const QPoint &pos ) { QMenu menu( this ); menu.setTitle( i18n( "Okular" ) ); QAction *showTextAct = menu.addAction( i18n( "Show Text" ) ); showTextAct->setCheckable( true ); showTextAct->setChecked( d->sideDelegate->isTextShown() ); connect(showTextAct, &QAction::toggled, this, &Sidebar::showTextToggled); menu.addSeparator(); QActionGroup *sizeGroup = new QActionGroup( &menu ); int curSize = d->list->iconSize().width(); #define ADD_SIZE_ACTION( text, _itssize ) \ { \ const int itssize = static_cast< int >( _itssize ); \ QAction *sizeAct = menu.addAction( text ); \ sizeAct->setCheckable( true ); \ sizeAct->setData( qVariantFromValue( itssize ) ); \ sizeAct->setChecked( itssize == curSize ); \ sizeGroup->addAction( sizeAct ); \ } ADD_SIZE_ACTION( i18n( "Small Icons" ), KIconLoader::SizeSmallMedium ) ADD_SIZE_ACTION( i18n( "Normal Icons" ), KIconLoader::SizeMedium ) ADD_SIZE_ACTION( i18n( "Large Icons" ), KIconLoader::SizeLarge ) #undef ADD_SIZE_ACTION connect(sizeGroup, &QActionGroup::triggered, this, &Sidebar::iconSizeChanged); menu.exec( mapToGlobal( pos ) ); } void Sidebar::showTextToggled( bool on ) { d->sideDelegate->setShowText( on ); d->adjustListSize( true, on ); d->list->reset(); d->list->update(); Okular::Settings::setSidebarShowText( on ); Okular::Settings::self()->save(); } void Sidebar::iconSizeChanged( QAction *action ) { int size = action->data().toInt(); int oldSize = d->list->iconSize().width(); d->list->setIconSize( QSize( size, size ) ); d->adjustListSize( true, size > oldSize ); d->list->reset(); d->list->update(); Okular::Settings::setSidebarIconSize( size ); Okular::Settings::self()->save(); } void Sidebar::dragEnterEvent( QDragEnterEvent* event ) { event->setAccepted( event->mimeData()->hasUrls() ); } void Sidebar::dropEvent( QDropEvent* event ) { const QList list = KUrlMimeData::urlsFromMimeData( event->mimeData() ); emit urlsDropped( list ); } #include "sidebar.moc"