okular/ui/sidebar.cpp
Eike Hein 796958b1a7 Fix the mouse interaction model of the sidebar icon list
to allow changing sidebar pages by keeping the left mouse
button pressed and moving the cursor over the icon list,
similar to a popup menu or the treelist version of the
tab bar in Konversation.

A remaining bug in the sidebar is that switching iconlist
items using the keyboard arrow keys won't switch sidebar
pages. I will fix that next time, unless someone else is
faster.

svn path=/trunk/KDE/kdegraphics/okular/; revision=1034540
2009-10-13 00:03:54 +00:00

708 lines
21 KiB
C++

/***************************************************************************
* Copyright (C) 2007 by Pino Toscano <pino@kde.org> *
* Copyright (C) 2009 by Eike Hein <hein@kde.org> *
* *
* 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 <qabstractitemdelegate.h>
#include <qaction.h>
#include <qapplication.h>
#include <qevent.h>
#include <qfont.h>
#include <qfontmetrics.h>
#include <qlabel.h>
#include <qlayout.h>
#include <qlist.h>
#include <qlistwidget.h>
#include <qpainter.h>
#include <qscrollbar.h>
#include <qsplitter.h>
#include <qstackedwidget.h>
#include <kglobalsettings.h>
#include <kiconloader.h>
#include <klocale.h>
#include <kmenu.h>
#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
{
public:
SidebarDelegate( QObject *parent = 0 );
~SidebarDelegate();
void setShowText( bool show );
bool isTextShown() const;
// from QAbstractItemDelegate
void paint( QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index ) const;
QSize sizeHint( const QStyleOptionViewItem &option, const QModelIndex &index ) const;
private:
bool m_showText;
};
SidebarDelegate::SidebarDelegate( QObject *parent )
: QAbstractItemDelegate( parent ), m_showText( true )
{
}
SidebarDelegate::~SidebarDelegate()
{
}
void SidebarDelegate::setShowText( bool show )
{
m_showText = show;
}
bool SidebarDelegate::isTextShown() const
{
return m_showText;
}
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 = option.palette.brush( QPalette::Disabled, QPalette::Base );
foreColor = option.palette.color( QPalette::Disabled, QPalette::Text );
disabled = true;
}
else if ( option.state & ( QStyle::State_HasFocus | QStyle::State_Selected ) )
{
backBrush = option.palette.brush( QPalette::Highlight );
foreColor = option.palette.color( QPalette::HighlightedText );
}
else if ( option.state & QStyle::State_MouseOver )
{
backBrush = option.palette.color( QPalette::Highlight ).light( 115 );
foreColor = option.palette.color( QPalette::HighlightedText );
hover = true;
}
else /*if ( option.state & QStyle::State_Enabled )*/
{
backBrush = option.palette.brush( QPalette::Base );
foreColor = option.palette.color( QPalette::Text );
}
QStyle *style = QApplication::style();
QStyleOptionViewItemV4 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 = 0 );
~SidebarListWidget();
protected:
// from QListWidget
void mouseDoubleClickEvent( QMouseEvent *event );
void mouseMoveEvent( QMouseEvent *event );
void mousePressEvent( QMouseEvent *event );
void mouseReleaseEvent( QMouseEvent *event );
QModelIndex moveCursor( QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers );
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 )
{
}
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 )
{
QRect bottomElemRect(
QPoint( 0, 0 ),
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();
}
bottomElemRect.setWidth( w );
}
bottomElemRect.translate( 0, bottomElemRect.height() * ( list->count() - 1 ) );
itemsHeight = bottomElemRect.height() * list->count();
list->setMinimumHeight( itemsHeight + list->frameWidth() * 2 );
int curWidth = list->minimumWidth();
int newWidth = expand
? qMax( bottomElemRect.width() + list->frameWidth() * 2, curWidth )
: qMin( bottomElemRect.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 );
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->list->setFont( KGlobalSettings::toolBarFont() );
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 );
d->sideContainer->hide();
connect( d->list, SIGNAL( customContextMenuRequested( const QPoint & ) ),
this, SLOT( listContextMenu( const QPoint & ) ) );
connect( d->splitter, SIGNAL( splitterMoved( int, int ) ), this, SLOT( splitterMoved( int, int ) ) );
}
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<int> 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( 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 == currentIndex() )
// 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( int index ) const
{
if ( index < 0 || index >= d->pages.count() )
return false;
Qt::ItemFlags f = d->pages.at( index )->flags();
return ( f & Qt::ItemIsEnabled ) == Qt::ItemIsEnabled;
}
void Sidebar::setCurrentIndex( int index )
{
if ( index < 0 || index >= d->pages.count() || !isItemEnabled( index ) )
return;
itemClicked( d->pages.at( index ) );
QModelIndex modelindex = d->list->model()->index( index, 0 );
d->list->setCurrentIndex( modelindex );
d->list->selectionModel()->select( modelindex, QItemSelectionModel::ClearAndSelect );
}
int Sidebar::currentIndex() const
{
return d->list->currentRow();
}
void Sidebar::setSidebarVisibility( bool visible )
{
if ( visible != d->list->isHidden() )
return;
static bool sideWasVisible = !d->sideContainer->isHidden();
d->list->setHidden( !visible );
if ( visible )
{
d->sideContainer->setHidden( !sideWasVisible );
sideWasVisible = true;
}
else
{
sideWasVisible = !d->sideContainer->isHidden();
d->sideContainer->setHidden( true );
}
}
bool Sidebar::isSidebarVisible() const
{
return !d->sideContainer->isHidden();
}
void Sidebar::itemClicked( QListWidgetItem *item )
{
if ( !item )
return;
SidebarItem* sbItem = dynamic_cast< SidebarItem* >( item );
if ( !sbItem )
return;
if ( sbItem->widget() == d->stack->currentWidget() )
{
if ( d->sideContainer->isVisible() )
{
d->list->selectionModel()->clear();
d->sideContainer->hide();
}
else
{
d->sideContainer->show();
}
}
else
{
if ( d->sideContainer->isHidden() )
d->sideContainer->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()->writeConfig();
}
void Sidebar::listContextMenu( const QPoint &pos )
{
KMenu menu( this );
menu.addTitle( i18n( "Okular" ) );
QAction *showTextAct = menu.addAction( i18n( "Show Text" ) );
showTextAct->setCheckable( true );
showTextAct->setChecked( d->sideDelegate->isTextShown() );
connect( showTextAct, SIGNAL( toggled( bool ) ), this, SLOT( showTextToggled( bool ) ) );
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, SIGNAL( triggered( QAction* ) ), this, SLOT( iconSizeChanged( QAction* ) ) );
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()->writeConfig();
}
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()->writeConfig();
}
#include "sidebar.moc"