You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2325 lines
70 KiB
2325 lines
70 KiB
/****************************************************************************** |
|
* |
|
* Copyright 2008 Szymon Tomasz Stefanek <pragma@kvirc.net> |
|
* |
|
* 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. |
|
* |
|
*******************************************************************************/ |
|
|
|
#include "messagelistview/core/view.h" |
|
|
|
#include "messagelistview/core/aggregation.h" |
|
#include "messagelistview/core/delegate.h" |
|
#include "messagelistview/core/groupheaderitem.h" |
|
#include "messagelistview/core/item.h" |
|
#include "messagelistview/core/manager.h" |
|
#include "messagelistview/core/messageitem.h" |
|
#include "messagelistview/core/model.h" |
|
#include "messagelistview/core/theme.h" |
|
#include "messagelistview/core/storagemodelbase.h" |
|
#include "messagelistview/core/widgetbase.h" |
|
|
|
#include <kmime/kmime_dateformatter.h> // kdepimlibs |
|
#include <libkdepim/maillistdrag.h> |
|
|
|
#include <QHelpEvent> |
|
#include <QToolTip> |
|
#include <QHeaderView> |
|
#include <QTimer> |
|
|
|
#include <KMenu> |
|
#include <KLocale> |
|
#include <KDebug> |
|
#include <KGlobalSettings> |
|
|
|
namespace KMail |
|
{ |
|
|
|
namespace MessageListView |
|
{ |
|
|
|
namespace Core |
|
{ |
|
|
|
View::View( Widget *pParent ) |
|
: QTreeView( pParent ), mWidget( pParent ) |
|
{ |
|
mTheme = 0; |
|
mAggregation = 0; |
|
mDelegate = new Delegate( this ); |
|
mLastCurrentItem = 0; |
|
mFirstShow = true; |
|
|
|
mSaveThemeColumnStateTimer = new QTimer(); |
|
connect( mSaveThemeColumnStateTimer, SIGNAL( timeout() ), this, SLOT( saveThemeColumnState() ) ); |
|
|
|
mApplyThemeColumnsTimer = new QTimer(); |
|
connect( mApplyThemeColumnsTimer, SIGNAL( timeout() ), this, SLOT( applyThemeColumns() ) ); |
|
|
|
setItemDelegate( mDelegate ); |
|
setVerticalScrollMode( QAbstractItemView::ScrollPerPixel ); |
|
setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOn ); |
|
setAlternatingRowColors( true ); |
|
setAllColumnsShowFocus( true ); |
|
setSelectionMode( QAbstractItemView::ExtendedSelection ); |
|
viewport()->setAcceptDrops( true ); |
|
|
|
//setUniformRowHeights( true ); |
|
|
|
header()->setContextMenuPolicy( Qt::CustomContextMenu ); |
|
connect( header(), SIGNAL( customContextMenuRequested( const QPoint& ) ), |
|
SLOT( slotHeaderContextMenuRequested( const QPoint& ) ) ); |
|
connect( header(), SIGNAL( sectionResized( int, int, int ) ), |
|
SLOT( slotHeaderSectionResized( int, int ,int ) ) ); |
|
|
|
mSaveThemeColumnStateOnSectionResize = true; |
|
|
|
header()->setClickable( true ); |
|
header()->setResizeMode( QHeaderView::Interactive ); |
|
header()->setMinimumSectionSize( 2 ); // QTreeView overrides our sections sizes if we set them smaller than this value |
|
header()->setDefaultSectionSize( 2 ); // QTreeView overrides our sections sizes if we set them smaller than this value |
|
|
|
mModel = new Model( this ); |
|
setModel( mModel ); |
|
|
|
//connect( selectionModel(), SIGNAL( currentChanged( const QModelIndex &, const QModelIndex & ) ), |
|
// this, SLOT( slotCurrentIndexChanged( const QModelIndex &, const QModelIndex & ) ) ); |
|
connect( selectionModel(), SIGNAL( selectionChanged( const QItemSelection &, const QItemSelection & ) ), |
|
this, SLOT( slotSelectionChanged( const QItemSelection &, const QItemSelection & ) ) ); |
|
|
|
|
|
} |
|
|
|
View::~View() |
|
{ |
|
if ( mSaveThemeColumnStateTimer->isActive() ) |
|
mSaveThemeColumnStateTimer->stop(); |
|
delete mSaveThemeColumnStateTimer; |
|
} |
|
|
|
void View::ignoreCurrentChanges( bool ignore ) |
|
{ |
|
if ( ignore ) |
|
{ |
|
disconnect( selectionModel(), SIGNAL( selectionChanged( const QItemSelection &, const QItemSelection & ) ), |
|
this, SLOT( slotSelectionChanged( const QItemSelection &, const QItemSelection & ) ) ); |
|
viewport()->setUpdatesEnabled( false ); |
|
} else { |
|
connect( selectionModel(), SIGNAL( selectionChanged( const QItemSelection &, const QItemSelection & ) ), |
|
this, SLOT( slotSelectionChanged( const QItemSelection &, const QItemSelection & ) ) ); |
|
viewport()->setUpdatesEnabled( true ); |
|
} |
|
} |
|
|
|
|
|
const StorageModel * View::storageModel() const |
|
{ |
|
return mModel->storageModel(); |
|
} |
|
|
|
void View::setAggregation( const Aggregation * aggregation ) |
|
{ |
|
mAggregation = aggregation; |
|
mModel->setAggregation( aggregation ); |
|
|
|
if ( !mAggregation ) |
|
return; |
|
} |
|
|
|
void View::setTheme( Theme * theme ) |
|
{ |
|
mNeedToApplyThemeColumns = true; |
|
mTheme = theme; |
|
mDelegate->setTheme( theme ); |
|
mModel->setTheme( theme ); |
|
} |
|
|
|
void View::reload() |
|
{ |
|
setStorageModel( storageModel() ); |
|
} |
|
|
|
void View::setStorageModel( const StorageModel * storageModel, PreSelectionMode preSelectionMode ) |
|
{ |
|
// This will cause the model to be reset. |
|
mSaveThemeColumnStateOnSectionResize = false; |
|
mModel->setStorageModel( storageModel, preSelectionMode ); |
|
mSaveThemeColumnStateOnSectionResize = true; |
|
} |
|
|
|
void View::modelJobBatchStarted() |
|
{ |
|
// This is called by the model when the first job of a batch starts |
|
mWidget->viewJobBatchStarted(); |
|
} |
|
|
|
void View::modelJobBatchTerminated() |
|
{ |
|
// This is called by the model when all the pending jobs have been processed |
|
mWidget->viewJobBatchTerminated(); |
|
} |
|
|
|
void View::modelHasBeenReset() |
|
{ |
|
// This is called by Model when it has been reset. |
|
if ( mNeedToApplyThemeColumns ) |
|
applyThemeColumns(); |
|
} |
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////// |
|
// Theme column state machinery |
|
// |
|
// This is yet another beast to beat. The QHeaderView behaviour, at the time of writing, |
|
// is quite unpredictable. This is due to the complex interaction with the model, with the QTreeView |
|
// and due to its attempts to delay the layout jobs. The delayed layouts, especially, may |
|
// cause the widths of the columns to quickly change in an unexpected manner in a place |
|
// where previously they have been always settled to the values you set... |
|
// |
|
// So here we have the tools to: |
|
// |
|
// - Apply the saved state of the theme columns (applyThemeColumns()). |
|
// This function computes the "best fit" state of the visible columns and tries |
|
// to apply it to QHeaderView. It also saves the new computed state to the Theme object. |
|
// |
|
// - Explicitly save the column state, used when the user changes the widths or visibility manually. |
|
// This is called through a delayed timer after a column has been resized or used directly |
|
// when the visibility state of a column has been changed by toggling a popup menu entry. |
|
// |
|
// - Display the column state context popup menu and handle its actions |
|
// |
|
// - Apply the theme columns when the theme changes, when the model changes or in certain |
|
// ugly corner cases when the widget is resized or shown. |
|
// |
|
// - Avoid saving a corrupted column state in that QHeaderView can be found *very* frequently. |
|
// |
|
|
|
void View::applyThemeColumns() |
|
{ |
|
if ( mApplyThemeColumnsTimer->isActive() ) |
|
mApplyThemeColumnsTimer->stop(); |
|
|
|
kDebug() << "Apply theme columns (VISIBLE:" << isVisible() << ")"; |
|
|
|
if ( !mTheme ) |
|
return; |
|
|
|
const QList< Theme::Column * > & columns = mTheme->columns(); |
|
|
|
if ( columns.count() < 1 ) |
|
return; // bad theme |
|
|
|
if ( !viewport()->isVisible() ) |
|
return; // invisible |
|
|
|
if ( viewport()->width() < 1 ) |
|
return; // insane width |
|
|
|
// Now we want to distribute the available width on all the visible columns. |
|
// |
|
// The rules: |
|
// - The visible columns will span the width of the view, if possible. |
|
// - The columns with a saved width should take that width. |
|
// - The columns on the left should take more space, if possible. |
|
// - The columns with no text take just slightly more than their size hint. |
|
// while the columns with text take possibly a lot more. |
|
// |
|
|
|
// Note that the first column is always shown (it can't be hidden at all) |
|
|
|
// The algorithm below is a sort of compromise between: |
|
// - Saving the user preferences for widths |
|
// - Using exactly the available view space |
|
// |
|
// It "tends to work" in all cases: |
|
// - When there are no user preferences saved and the column widths must be |
|
// automatically computed to make best use of available space |
|
// - When there are user preferences for only some of the columns |
|
// and that should be somewhat preserved while still using all the |
|
// available space. |
|
// - When all the columns have well defined saved widths |
|
|
|
QList< Theme::Column * >::ConstIterator it; |
|
int idx = 0; |
|
|
|
kDebug() << "Going to really apply columns"; |
|
|
|
|
|
// Gather total size "hint" for visible sections: if the widths of the columns wers |
|
// all saved then the total hint is equal to the total saved width. |
|
|
|
int totalVisibleWidthHint = 0; |
|
QList< int > lColumnSizeHints; |
|
|
|
for ( it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
if ( ( *it )->currentlyVisible() || ( idx == 0 ) ) |
|
{ |
|
// Column visible |
|
int savedWidth = ( *it )->currentWidth(); |
|
int hintWidth = mDelegate->sizeHintForItemTypeAndColumn( Item::Message, idx ).width(); |
|
totalVisibleWidthHint += savedWidth > 0 ? savedWidth : hintWidth; |
|
lColumnSizeHints.append( hintWidth ); |
|
} else { |
|
// The column is not visible |
|
lColumnSizeHints.append( -1 ); // dummy |
|
} |
|
idx++; |
|
} |
|
|
|
if ( totalVisibleWidthHint < 16 ) |
|
totalVisibleWidthHint = 16; // be reasonable |
|
|
|
// Now compute somewhat "proportional" widths. |
|
idx = 0; |
|
|
|
QList< int > lColumnWidths; |
|
int totalVisibleWidth = 0; |
|
|
|
for ( it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
int savedWidth = ( *it )->currentWidth(); |
|
int hintWidth = savedWidth > 0 ? savedWidth : lColumnSizeHints[ idx ]; |
|
int realWidth; |
|
|
|
if ( ( *it )->currentlyVisible() || ( idx == 0 ) ) |
|
{ |
|
if ( ( *it )->containsTextItems() ) |
|
{ |
|
// the column contains text items, it should get more space (if possible) |
|
realWidth = ( ( hintWidth * viewport()->width() ) / totalVisibleWidthHint ); |
|
} else { |
|
// the column contains no text items, it should get exactly its hint/saved width. |
|
realWidth = hintWidth; |
|
} |
|
|
|
if ( realWidth < 2 ) |
|
realWidth = 2; // don't allow very insane values |
|
|
|
kDebug() << "Column " << idx << " saved " << savedWidth << " hint " << hintWidth << " chosen " << realWidth; |
|
totalVisibleWidth += realWidth; |
|
} else { |
|
// Column not visible |
|
realWidth = -1; |
|
} |
|
|
|
lColumnWidths.append( realWidth ); |
|
|
|
idx++; |
|
} |
|
|
|
kDebug() << "Total visible width before fixing is " << totalVisibleWidth << ", viewport width is " << viewport()->width(); |
|
|
|
// Now the algorithm above may be wrong for several reasons... |
|
// - We're using fixed widths for certain columns and proportional |
|
// for others... |
|
// - The user might have changed the width of the view from the |
|
// time in that the widths have been saved |
|
// - There are some (not well identified) issues with the QTreeView |
|
// scrollbar that make our view appear larger or shorter by 2-3 pixels |
|
// sometimes. |
|
// - ... |
|
// So we correct the previous estimates by trying to use exactly |
|
// the available space. |
|
|
|
idx = 0; |
|
|
|
if ( totalVisibleWidth != viewport()->width() ) |
|
{ |
|
// The estimated widths were not using exactly the available space. |
|
if ( totalVisibleWidth < viewport()->width() ) |
|
{ |
|
// We were using less space than available. |
|
|
|
// Give the additional space to the text columns |
|
// also give more space to the first ones and less space to the last ones |
|
int available = viewport()->width() - totalVisibleWidth; |
|
|
|
for ( it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
if ( ( ( *it )->currentlyVisible() || ( idx == 0 ) ) && ( *it )->containsTextItems() ) |
|
{ |
|
// give more space to this column |
|
available >>= 1; // eat half of the available space |
|
lColumnWidths[ idx ] += available; // and give it to this column |
|
if ( available < 1 ) |
|
break; // no more space to give away |
|
} |
|
|
|
idx++; |
|
} |
|
|
|
// if any space is still available, give it to the first column |
|
if ( available ) |
|
lColumnWidths[ 0 ] += available; |
|
} else { |
|
// We were using more space than available |
|
|
|
// If the columns span just a little bit more than the view then |
|
// try to squeeze them in order to make them fit |
|
if ( totalVisibleWidth < ( viewport()->width() + 100 ) ) |
|
{ |
|
int missing = totalVisibleWidth - viewport()->width(); |
|
int count = lColumnWidths.count(); |
|
|
|
if ( missing > 0 ) |
|
{ |
|
idx = count - 1; |
|
|
|
while ( idx >= 0 ) |
|
{ |
|
if ( columns.at( idx )->currentlyVisible() || ( idx == 0 ) ) |
|
{ |
|
int chop = lColumnWidths[ idx ] - lColumnSizeHints[ idx ]; |
|
if ( chop > 0 ) |
|
{ |
|
if ( chop > missing ) |
|
chop = missing; |
|
lColumnWidths[ idx ] -= chop; |
|
missing -= chop; |
|
if ( missing < 1 ) |
|
break; // no more space to recover |
|
} |
|
} // else it's invisible |
|
idx--; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
// We're ready to assign widths. |
|
|
|
bool oldSave = mSaveThemeColumnStateOnSectionResize; |
|
mSaveThemeColumnStateOnSectionResize = false; |
|
|
|
// A huge problem here is that QHeaderView goes quite nuts if we show or hide sections |
|
// while resizing them. This is because it has several machineries aimed to delay |
|
// the layout to the last possible moment. So if we show a column, it will tend to |
|
// screw up the layout of other ones. |
|
|
|
// We first loop showing/hiding columns then. |
|
|
|
idx = 0; |
|
|
|
for ( it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
bool visible = ( idx == 0 ) || ( *it )->currentlyVisible(); |
|
( *it )->setCurrentlyVisible( visible ); |
|
header()->setSectionHidden( idx, !visible ); |
|
idx++; |
|
} |
|
|
|
// Then we loop assigning widths. This is still complicated since QHeaderView tries |
|
// very badly to stretch the last section and thus will resize it in the meantime. |
|
// But seems to work most of the times... |
|
|
|
idx = 0; |
|
totalVisibleWidth = 0; |
|
|
|
for ( it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
if ( ( *it )->currentlyVisible() ) |
|
{ |
|
// kDebug() << "Resize section " << idx << " to " << lColumnWidths[ idx ]; |
|
( *it )->setCurrentWidth( lColumnWidths[ idx ] ); |
|
header()->resizeSection( idx, lColumnWidths[ idx ] ); |
|
totalVisibleWidth += lColumnWidths[ idx ]; |
|
} else { |
|
( *it )->setCurrentWidth( -1 ); |
|
} |
|
idx++; |
|
} |
|
|
|
kDebug() << "Total visible width after fixing is " << totalVisibleWidth << ", viewport width is " << viewport()->width(); |
|
|
|
totalVisibleWidth = 0; |
|
idx = 0; |
|
|
|
bool bTriggeredQtBug = false; |
|
|
|
for ( QList< Theme::Column * >::ConstIterator it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
if ( !header()->isSectionHidden( idx ) ) |
|
{ |
|
kDebug() << "!! Final size for column " << idx << " is " << header()->sectionSize( idx ); |
|
if ( !( *it )->currentlyVisible() ) |
|
{ |
|
bTriggeredQtBug = true; |
|
kDebug() << "!! ERROR: Qt screwed up: I've set column " << idx << " to hidden but it's shown"; |
|
} |
|
totalVisibleWidth += header()->sectionSize( idx ); |
|
} |
|
idx++; |
|
} |
|
|
|
kDebug() << "Total real visible width after fixing is " << totalVisibleWidth << ", viewport width is " << viewport()->width(); |
|
|
|
setHeaderHidden( mTheme->viewHeaderPolicy() == Theme::NeverShowHeader ); |
|
|
|
mSaveThemeColumnStateOnSectionResize = oldSave; |
|
mNeedToApplyThemeColumns = false; |
|
|
|
static bool bAllowRecursion = true; |
|
|
|
if (bTriggeredQtBug && bAllowRecursion) |
|
{ |
|
bAllowRecursion = false; |
|
kDebug() << "I've triggered the QHeaderView bug: trying to fix by calling myself again"; |
|
applyThemeColumns(); |
|
bAllowRecursion = true; |
|
} |
|
} |
|
|
|
void View::triggerDelayedApplyThemeColumns() |
|
{ |
|
if ( mApplyThemeColumnsTimer->isActive() ) |
|
mApplyThemeColumnsTimer->stop(); |
|
mApplyThemeColumnsTimer->setSingleShot( true ); |
|
mApplyThemeColumnsTimer->start( 100 ); |
|
} |
|
|
|
void View::saveThemeColumnState() |
|
{ |
|
kDebug() << "Save theme column state"; |
|
|
|
if ( mSaveThemeColumnStateTimer->isActive() ) |
|
mSaveThemeColumnStateTimer->stop(); |
|
|
|
if ( !mTheme ) |
|
return; |
|
|
|
const QList< Theme::Column * > & columns = mTheme->columns(); |
|
|
|
if ( columns.count() < 1 ) |
|
return; // bad theme |
|
|
|
int idx = 0; |
|
|
|
|
|
for ( QList< Theme::Column * >::ConstIterator it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
if ( header()->isSectionHidden( idx ) ) |
|
{ |
|
( *it )->setCurrentlyVisible( false ); |
|
( *it )->setCurrentWidth( -1 ); // reset (hmmm... we could use the "don't touch" policy here too...) |
|
} else { |
|
( *it )->setCurrentlyVisible( true ); |
|
( *it )->setCurrentWidth( header()->sectionSize( idx ) ); |
|
} |
|
idx++; |
|
} |
|
} |
|
|
|
void View::triggerDelayedSaveThemeColumnState() |
|
{ |
|
if ( mSaveThemeColumnStateTimer->isActive() ) |
|
mSaveThemeColumnStateTimer->stop(); |
|
mSaveThemeColumnStateTimer->setSingleShot( true ); |
|
mSaveThemeColumnStateTimer->start( 200 ); |
|
} |
|
|
|
void View::resizeEvent( QResizeEvent * e ) |
|
{ |
|
kDebug() << "Resize event enter (viewport width is " << viewport()->width() << ")"; |
|
|
|
QTreeView::resizeEvent( e ); |
|
|
|
if ( !isVisible() ) |
|
return; // don't play with |
|
|
|
if ( (!mFirstShow) && mNeedToApplyThemeColumns ) |
|
triggerDelayedApplyThemeColumns(); |
|
|
|
if ( header()->isVisible() ) |
|
return; |
|
|
|
// header invisible |
|
|
|
bool oldSave = mSaveThemeColumnStateOnSectionResize; |
|
mSaveThemeColumnStateOnSectionResize = false; |
|
|
|
if ( ( header()->count() - header()->hiddenSectionCount() ) < 2 ) |
|
{ |
|
// a single column visible: resize it |
|
int visibleIndex; |
|
int count = header()->count(); |
|
for ( visibleIndex = 0; visibleIndex < count; visibleIndex++ ) |
|
{ |
|
if ( !header()->isSectionHidden( visibleIndex ) ) |
|
break; |
|
} |
|
if ( visibleIndex < count ) |
|
header()->resizeSection( visibleIndex, viewport()->width() - 4 ); |
|
} |
|
|
|
mSaveThemeColumnStateOnSectionResize = oldSave; |
|
|
|
triggerDelayedSaveThemeColumnState(); |
|
} |
|
|
|
void View::modelAboutToEmitLayoutChanged() |
|
{ |
|
// QHeaderView goes totally NUTS with a layoutChanged() call |
|
mSaveThemeColumnStateOnSectionResize = false; |
|
} |
|
|
|
void View::modelEmittedLayoutChanged() |
|
{ |
|
// This is after a first chunk of work has been done by the model: do apply column states |
|
mSaveThemeColumnStateOnSectionResize = true; |
|
applyThemeColumns(); |
|
} |
|
|
|
void View::slotHeaderSectionResized( int logicalIndex, int oldWidth, int newWidth ) |
|
{ |
|
Q_UNUSED( logicalIndex ); |
|
Q_UNUSED( oldWidth ); |
|
Q_UNUSED( newWidth ); |
|
|
|
if ( mSaveThemeColumnStateOnSectionResize ) |
|
triggerDelayedSaveThemeColumnState(); |
|
} |
|
|
|
int View::sizeHintForColumn( int logicalColumnIndex ) const |
|
{ |
|
// QTreeView: please don't touch my column widths... |
|
int w = header()->sectionSize( logicalColumnIndex ); |
|
if ( w > 0 ) |
|
return w; |
|
if ( !mDelegate ) |
|
return 32; // dummy |
|
w = mDelegate->sizeHintForItemTypeAndColumn( Item::Message, logicalColumnIndex ).width(); |
|
return w; |
|
} |
|
|
|
void View::showEvent( QShowEvent *e ) |
|
{ |
|
kDebug() << "Show event enter"; |
|
|
|
QTreeView::showEvent( e ); |
|
if ( mFirstShow ) |
|
{ |
|
kDebug() << "First show"; |
|
// |
|
// If we're shown for the first time and the theme has been already set |
|
// then we need to reapply the theme column widths since the previous |
|
// application probably used invalid widths. |
|
// |
|
if ( mTheme ) |
|
triggerDelayedApplyThemeColumns(); |
|
mFirstShow = false; |
|
} |
|
kDebug() << "Show event exit"; |
|
} |
|
|
|
const int gHeaderContextMenuAdjustColumnSizesId = -1; |
|
const int gHeaderContextMenuShowDefaultColumnsId = -2; |
|
|
|
void View::slotHeaderContextMenuRequested( const QPoint &pnt ) |
|
{ |
|
if ( !mTheme ) |
|
return; |
|
|
|
const QList< Theme::Column * > & columns = mTheme->columns(); |
|
|
|
if ( columns.count() < 1 ) |
|
return; // bad theme |
|
|
|
// the menu for the columns |
|
KMenu menu; |
|
|
|
menu.addTitle( i18n( "Show Columns" ) ); |
|
|
|
int idx = 0; |
|
QAction * act; |
|
|
|
for ( QList< Theme::Column * >::ConstIterator it = columns.begin(); it != columns.end(); ++it ) |
|
{ |
|
act = menu.addAction( ( *it )->label() ); |
|
act->setCheckable( true ); |
|
act->setChecked( !header()->isSectionHidden( idx ) ); |
|
act->setData( QVariant( idx ) ); |
|
if ( idx == 0) |
|
act->setEnabled( false ); |
|
|
|
idx++; |
|
} |
|
|
|
menu.addSeparator(); |
|
act = menu.addAction( i18n( "Adjust Column Sizes" ) ); |
|
act->setData( QVariant( static_cast< int >( gHeaderContextMenuAdjustColumnSizesId ) ) ); |
|
|
|
act = menu.addAction( i18n( "Show Default Columns" ) ); |
|
act->setData( QVariant( static_cast< int >( gHeaderContextMenuShowDefaultColumnsId ) ) ); |
|
|
|
QObject::connect( |
|
&menu, SIGNAL( triggered( QAction * ) ), |
|
this, SLOT( slotHeaderContextMenuTriggered( QAction * ) ) |
|
); |
|
|
|
menu.exec( header()->mapToGlobal( pnt ) ); |
|
} |
|
|
|
void View::slotHeaderContextMenuTriggered( QAction * act ) |
|
{ |
|
if ( !mTheme ) |
|
return; // oops |
|
|
|
if ( !act ) |
|
return; |
|
|
|
bool ok; |
|
int columnIdx = act->data().toInt( &ok ); |
|
|
|
if ( !ok ) |
|
return; |
|
|
|
if ( columnIdx < 0 ) |
|
{ |
|
if ( columnIdx == gHeaderContextMenuAdjustColumnSizesId ) |
|
{ |
|
// "Adjust Column Sizes" |
|
mTheme->resetColumnSizes(); |
|
applyThemeColumns(); |
|
} else if ( columnIdx == gHeaderContextMenuShowDefaultColumnsId ) |
|
{ |
|
// "Show Default Columns" |
|
mTheme->resetColumnState(); |
|
applyThemeColumns(); |
|
} |
|
return; |
|
} |
|
|
|
// Single column show or hide action |
|
if ( columnIdx == 0 ) |
|
return; // can never be hidden |
|
|
|
if ( columnIdx >= mTheme->columns().count() ) |
|
return; |
|
|
|
bool showIt = header()->isSectionHidden( columnIdx ); |
|
|
|
Theme::Column * column = mTheme->columns().at( columnIdx ); |
|
Q_ASSERT( column ); |
|
|
|
// first save column state (as it is, with the column still in previous state) |
|
saveThemeColumnState(); |
|
|
|
// If a section has just been shown, invalidate its width in the skin |
|
// since QTreeView assigned it a (possibly insane) default width. |
|
// If a section has been hidden, then invalidate its width anyway... |
|
// so finally invalidate width always, here. |
|
column->setCurrentlyVisible( showIt ); |
|
column->setCurrentWidth( -1 ); |
|
|
|
// then apply theme columns to re-compute proportional widths (so we hopefully stay in the view) |
|
applyThemeColumns(); |
|
} |
|
|
|
MessageItem * View::currentMessageItem( bool selectIfNeeded ) const |
|
{ |
|
QModelIndex idx = currentIndex(); |
|
if ( !idx.isValid() ) |
|
return 0; |
|
Item * it = static_cast< Item * >( idx.internalPointer() ); |
|
Q_ASSERT( it ); |
|
if ( it->type() != Item::Message ) |
|
return 0; |
|
|
|
if ( selectIfNeeded ) |
|
{ |
|
// Keep things coherent, if the user didn't select it, but acted on it via |
|
// a shortcut, do select it now. |
|
if ( !selectionModel()->isSelected( idx ) ) |
|
selectionModel()->select( idx, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows ); |
|
} |
|
|
|
return static_cast< MessageItem * >( it ); |
|
} |
|
|
|
void View::setCurrentMessageItem( MessageItem * it ) |
|
{ |
|
if ( it ) |
|
selectionModel()->setCurrentIndex( mModel->index( it, 0 ), QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows ); |
|
else |
|
selectionModel()->setCurrentIndex( QModelIndex(), QItemSelectionModel::Current | QItemSelectionModel::Clear ); |
|
} |
|
|
|
bool View::selectionEmpty() const |
|
{ |
|
return selectionModel()->selectedRows().isEmpty(); |
|
} |
|
|
|
QList< MessageItem * > View::selectionAsMessageItemList( bool includeCollapsedChildren ) const |
|
{ |
|
QList< MessageItem * > selectedMessages; |
|
|
|
QModelIndexList lSelected = selectionModel()->selectedRows(); |
|
if ( lSelected.isEmpty() ) |
|
return selectedMessages; |
|
|
|
for ( QModelIndexList::Iterator it = lSelected.begin(); it != lSelected.end(); ++it ) |
|
{ |
|
// The asserts below are theoretically valid but at the time |
|
// of writing they fail because of a bug in QItemSelectionModel::selectedRows() |
|
// which returns also non-selectable items. |
|
|
|
//Q_ASSERT( selectedItem->type() == Item::Message ); |
|
//Q_ASSERT( ( *it ).isValid() ); |
|
|
|
if ( !( *it ).isValid() ) |
|
continue; |
|
|
|
Item * selectedItem = static_cast< Item * >( ( *it ).internalPointer() ); |
|
Q_ASSERT( selectedItem ); |
|
|
|
if ( selectedItem->type() != Item::Message ) |
|
continue; |
|
|
|
if ( !static_cast< MessageItem * >( selectedItem )->isValid() ) |
|
continue; |
|
|
|
Q_ASSERT( !selectedMessages.contains( static_cast< MessageItem * >( selectedItem ) ) ); |
|
|
|
if ( includeCollapsedChildren && ( selectedItem->childItemCount() > 0 ) && ( !isExpanded( *it ) ) ) |
|
{ |
|
static_cast< MessageItem * >( selectedItem )->subTreeToList( selectedMessages ); |
|
} else { |
|
selectedMessages.append( static_cast< MessageItem * >( selectedItem ) ); |
|
} |
|
} |
|
|
|
return selectedMessages; |
|
} |
|
|
|
QList< MessageItem * > View::currentThreadAsMessageItemList() const |
|
{ |
|
QList< MessageItem * > currentThread; |
|
|
|
MessageItem * msg = currentMessageItem(); |
|
if ( !msg ) |
|
return currentThread; |
|
|
|
while ( msg->parent() ) |
|
{ |
|
if ( msg->parent()->type() != Item::Message ) |
|
break; |
|
msg = static_cast< MessageItem * >( msg->parent() ); |
|
} |
|
|
|
msg->subTreeToList( currentThread ); |
|
|
|
return currentThread; |
|
} |
|
|
|
void View::setChildrenExpanded( const Item * root, bool expand ) |
|
{ |
|
Q_ASSERT( root ); |
|
QList< Item * > * childList = root->childItems(); |
|
if ( !childList ) |
|
return; |
|
for ( QList< Item * >::Iterator it = childList->begin(); it != childList->end(); ++it ) |
|
{ |
|
QModelIndex idx = mModel->index( *it, 0 ); |
|
Q_ASSERT( idx.isValid() ); |
|
Q_ASSERT( static_cast< Item * >( idx.internalPointer() ) == ( *it ) ); |
|
|
|
if ( expand ) |
|
{ |
|
setExpanded( idx, true ); |
|
|
|
if ( ( *it )->childItemCount() > 0 ) |
|
setChildrenExpanded( *it, true ); |
|
} else { |
|
if ( ( *it )->childItemCount() > 0 ) |
|
setChildrenExpanded( *it, false ); |
|
|
|
setExpanded( idx, false ); |
|
} |
|
} |
|
} |
|
|
|
void View::setCurrentThreadExpanded( bool expand ) |
|
{ |
|
MessageItem * message = currentMessageItem(); |
|
if ( !message ) |
|
return; |
|
|
|
while ( message->parent() ) |
|
{ |
|
if ( message->parent()->type() != Item::Message ) |
|
break; |
|
message = static_cast< MessageItem * >( message->parent() ); |
|
} |
|
|
|
if ( expand ) |
|
{ |
|
setExpanded( mModel->index( message, 0 ), true ); |
|
setChildrenExpanded( message, true ); |
|
} else { |
|
setChildrenExpanded( message, false ); |
|
setExpanded( mModel->index( message, 0 ), false ); |
|
} |
|
} |
|
|
|
void View::setAllThreadsExpanded( bool expand ) |
|
{ |
|
setChildrenExpanded( mModel->rootItem(), expand ); |
|
} |
|
|
|
void View::selectMessageItems( const QList< MessageItem * > &list ) |
|
{ |
|
QItemSelection selection; |
|
for ( QList< MessageItem * >::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it ) |
|
{ |
|
Q_ASSERT( *it ); |
|
QModelIndex idx = mModel->index( *it, 0 ); |
|
Q_ASSERT( idx.isValid() ); |
|
Q_ASSERT( static_cast< MessageItem * >( idx.internalPointer() ) == ( *it ) ); |
|
if ( !selectionModel()->isSelected( idx ) ) |
|
selection.append( QItemSelectionRange( idx ) ); |
|
ensureCurrentlyViewable( *it ); |
|
} |
|
if ( !selection.isEmpty() ) |
|
selectionModel()->select( selection, QItemSelectionModel::Select | QItemSelectionModel::Rows ); |
|
} |
|
|
|
static inline bool message_type_matches( Item * item, MessageTypeFilter messageTypeFilter ) |
|
{ |
|
switch( messageTypeFilter ) |
|
{ |
|
case MessageTypeAny: |
|
return true; |
|
break; |
|
case MessageTypeNewOnly: |
|
return item->status().isNew(); |
|
break; |
|
case MessageTypeUnreadOnly: |
|
return item->status().isUnread(); |
|
break; |
|
case MessageTypeNewOrUnreadOnly: |
|
return item->status().isNew() || item->status().isUnread(); |
|
break; |
|
default: |
|
// nuthin here |
|
break; |
|
} |
|
|
|
// never reached |
|
Q_ASSERT( false ); |
|
return false; |
|
} |
|
|
|
Item * View::messageItemAfter( Item * referenceItem, MessageTypeFilter messageTypeFilter, bool loop ) |
|
{ |
|
if ( !storageModel() ) |
|
return 0; // no folder |
|
|
|
// find the item to start with |
|
Item * below; |
|
|
|
if ( referenceItem ) |
|
{ |
|
// there was a current item: we start just below it |
|
if ( |
|
( referenceItem->childItemCount() > 0 ) |
|
&& |
|
( |
|
( messageTypeFilter != MessageTypeAny ) |
|
|| |
|
isExpanded( mModel->index( referenceItem, 0 ) ) |
|
) |
|
) |
|
{ |
|
// the current item had children: either expanded or we want unread/new messages (and so we'll expand it if it isn't) |
|
below = referenceItem->itemBelow(); |
|
} else { |
|
// the current item had no children: ask the parent to find the item below |
|
Q_ASSERT( referenceItem->parent() ); |
|
below = referenceItem->parent()->itemBelowChild( referenceItem ); |
|
} |
|
|
|
if ( !below ) |
|
{ |
|
// reached the end |
|
if ( loop ) |
|
{ |
|
// try re-starting from top |
|
below = mModel->rootItem()->itemBelow(); |
|
Q_ASSERT( below ); // must exist (we had a current item) |
|
|
|
if ( below == referenceItem ) |
|
return 0; // only one item in folder: loop complete |
|
} else { |
|
// looping not requested |
|
return 0; |
|
} |
|
} |
|
|
|
} else { |
|
// there was no current item, start from beginning |
|
below = mModel->rootItem()->itemBelow(); |
|
|
|
if ( !below ) |
|
return 0; // folder empty |
|
} |
|
|
|
// ok.. now below points to the next message. |
|
// While it doesn't satisfy our requirements, go further down |
|
|
|
while ( |
|
// is not a message (we want messages, don't we ?) |
|
( below->type() != Item::Message ) || |
|
// message filter doesn't match |
|
( !message_type_matches( below, messageTypeFilter ) ) || |
|
// is hidden (and we don't want hidden items as they arent "officially" in the view) |
|
isRowHidden( below->parent()->indexOfChildItem( below ), mModel->index( below->parent(), 0 ) ) |
|
) |
|
{ |
|
// find the next one |
|
if ( ( below->childItemCount() > 0 ) && ( ( messageTypeFilter != MessageTypeAny ) || isExpanded( mModel->index( below, 0 ) ) ) ) |
|
{ |
|
// the current item had children: either expanded or we want unread messages (and so we'll expand it if it isn't) |
|
below = below->itemBelow(); |
|
} else { |
|
// the current item had no children: ask the parent to find the item below |
|
Q_ASSERT( below->parent() ); |
|
below = below->parent()->itemBelowChild( below ); |
|
} |
|
|
|
if ( !below ) |
|
{ |
|
// we reached the end of the folder |
|
if ( loop ) |
|
{ |
|
// looping requested |
|
if ( referenceItem ) // <-- this means "we have started from something that is not the top: looping makes sense" |
|
below = mModel->rootItem()->itemBelow(); |
|
// else mi == 0 and below == 0: we have started from the beginning and reached the end (it will fail the test below and exit) |
|
} else { |
|
// looping not requested: nothing more to do |
|
return 0; |
|
} |
|
} |
|
|
|
if( below == referenceItem ) |
|
{ |
|
Q_ASSERT( loop ); |
|
return 0; // looped and returned back to the first message |
|
} |
|
} |
|
|
|
return below; |
|
} |
|
|
|
Item * View::nextMessageItem( MessageTypeFilter messageTypeFilter, bool loop ) |
|
{ |
|
return messageItemAfter( currentMessageItem( false ), messageTypeFilter, loop ); |
|
} |
|
|
|
Item * View::messageItemBefore( Item * referenceItem, MessageTypeFilter messageTypeFilter, bool loop ) |
|
{ |
|
if ( !storageModel() ) |
|
return 0; // no folder |
|
|
|
Item * above; |
|
|
|
if ( referenceItem ) |
|
{ |
|
// There was a current item, we start just above it |
|
above = referenceItem->itemAbove(); |
|
|
|
if ( ( !above ) || ( above == mModel->rootItem() ) ) |
|
{ |
|
// reached the beginning |
|
if ( loop ) |
|
{ |
|
// try re-starting from bottom |
|
above = mModel->rootItem()->deepestItem(); |
|
Q_ASSERT( above ); // must exist (we had a current item) |
|
Q_ASSERT( above != mModel->rootItem() ); |
|
|
|
if ( above == referenceItem ) |
|
return 0; // only one item in folder: loop complete |
|
} else { |
|
// looping not requested |
|
return 0; |
|
} |
|
|
|
} |
|
} else { |
|
// there was no current item, start from end |
|
above = mModel->rootItem()->deepestItem(); |
|
|
|
if ( !above || ( above == mModel->rootItem() ) ) |
|
return 0; // folder empty |
|
} |
|
|
|
// ok.. now below points to the previous message. |
|
// While it doesn't satisfy our requirements, go further up |
|
|
|
while ( |
|
// is not a message (we want messages, don't we ?) |
|
( above->type() != Item::Message ) || |
|
// message filter doesn't match |
|
( !message_type_matches( above, messageTypeFilter ) ) || |
|
// we don't expand items but the item has parents unexpanded (so should be skipped) |
|
( |
|
// !expand items |
|
( messageTypeFilter == MessageTypeAny ) && |
|
// has unexpanded parents or is itself hidden |
|
( ! isCurrentlyViewable( above ) ) |
|
) || |
|
// is hidden |
|
isRowHidden( above->parent()->indexOfChildItem( above ), mModel->index( above->parent(), 0 ) ) |
|
) |
|
{ |
|
|
|
above = above->itemAbove(); |
|
|
|
if ( ( !above ) || ( above == mModel->rootItem() ) ) |
|
{ |
|
// reached the beginning |
|
if ( loop ) |
|
{ |
|
// looping requested |
|
if ( referenceItem ) // <-- this means "we have started from something that is not the beginning: looping makes sense" |
|
above = mModel->rootItem()->deepestItem(); |
|
// else mi == 0 and above == 0: we have started from the end and reached the beginning (it will fail the test below and exit) |
|
} else { |
|
// looping not requested: nothing more to do |
|
return 0; |
|
} |
|
} |
|
|
|
if( above == referenceItem ) |
|
{ |
|
Q_ASSERT( loop ); |
|
return 0; // looped and returned back to the first message |
|
} |
|
} |
|
|
|
return above; |
|
} |
|
|
|
Item * View::previousMessageItem( MessageTypeFilter messageTypeFilter, bool loop ) |
|
{ |
|
return messageItemBefore( currentMessageItem( false ), messageTypeFilter, loop ); |
|
} |
|
|
|
void View::growOrShrinkExistingSelection( const QModelIndex &newSelectedIndex, bool movingUp ) |
|
{ |
|
// Qt: why visualIndex() is private? ...I'd really need it here... |
|
|
|
int selectedVisualCoordinate = visualRect( newSelectedIndex ).top(); |
|
|
|
int topVisualCoordinate = 0xfffffff; // huuuuuge number |
|
int bottomVisualCoordinate = -(0xfffffff); |
|
|
|
int candidate; |
|
|
|
QModelIndex bottomIndex; |
|
QModelIndex topIndex; |
|
|
|
// find out the actual selection range |
|
const QItemSelection selection = selectionModel()->selection(); |
|
|
|
foreach ( QItemSelectionRange range, selection ) |
|
{ |
|
// We're asking the model for the index as range.topLeft() and range.bottomRight() |
|
// can return indexes in invisible columns which have a null visualRect(). |
|
// Column 0, instead, is always visible. |
|
|
|
QModelIndex top = mModel->index( range.top(), 0, range.parent() ); |
|
QModelIndex bottom = mModel->index( range.bottom(), 0, range.parent() ); |
|
|
|
if ( top.isValid() ) |
|
{ |
|
if ( !bottom.isValid() ) |
|
bottom = top; |
|
} else { |
|
if ( !top.isValid() ) |
|
top = bottom; |
|
} |
|
candidate = visualRect( bottom ).bottom(); |
|
if ( candidate > bottomVisualCoordinate ) |
|
{ |
|
bottomVisualCoordinate = candidate; |
|
bottomIndex = range.bottomRight(); |
|
} |
|
|
|
candidate = visualRect( top ).top(); |
|
if ( candidate < topVisualCoordinate ) |
|
{ |
|
topVisualCoordinate = candidate; |
|
topIndex = range.topLeft(); |
|
} |
|
} |
|
|
|
|
|
if ( topIndex.isValid() && bottomIndex.isValid() ) |
|
{ |
|
if ( movingUp ) |
|
{ |
|
if ( selectedVisualCoordinate < topVisualCoordinate ) |
|
{ |
|
// selecting something above the top: grow selection |
|
selectionModel()->select( newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select ); |
|
} else { |
|
// selecting something below the top: shrink selection |
|
QModelIndexList selectedIndexes = selection.indexes(); |
|
foreach ( QModelIndex idx, selectedIndexes ) |
|
{ |
|
if ( ( idx.column() == 0 ) && ( visualRect( idx ).top() > selectedVisualCoordinate ) ) |
|
selectionModel()->select( idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect ); |
|
} |
|
} |
|
} else { |
|
if ( selectedVisualCoordinate > bottomVisualCoordinate ) |
|
{ |
|
// selecting something below bottom: grow selection |
|
selectionModel()->select( newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select ); |
|
} else { |
|
// selecting something above bottom: shrink selection |
|
QModelIndexList selectedIndexes = selection.indexes(); |
|
foreach ( QModelIndex idx, selectedIndexes ) |
|
{ |
|
if ( ( idx.column() == 0 ) && ( visualRect( idx ).top() < selectedVisualCoordinate ) ) |
|
selectionModel()->select( idx, QItemSelectionModel::Rows | QItemSelectionModel::Deselect ); |
|
} |
|
} |
|
} |
|
} else { |
|
// no existing selection, just grow |
|
selectionModel()->select( newSelectedIndex, QItemSelectionModel::Rows | QItemSelectionModel::Select ); |
|
} |
|
} |
|
|
|
bool View::selectNextMessageItem( |
|
MessageTypeFilter messageTypeFilter, |
|
ExistingSelectionBehaviour existingSelectionBehaviour, |
|
bool centerItem, |
|
bool loop |
|
) |
|
{ |
|
Item * it = nextMessageItem( messageTypeFilter, loop ); |
|
if ( !it ) |
|
return false; |
|
|
|
setFocus(); |
|
|
|
if ( it->parent() != mModel->rootItem() ) |
|
ensureCurrentlyViewable( it ); |
|
|
|
QModelIndex idx = mModel->index( it, 0 ); |
|
|
|
Q_ASSERT( idx.isValid() ); |
|
|
|
switch ( existingSelectionBehaviour ) |
|
{ |
|
case ExpandExistingSelection: |
|
selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate ); |
|
selectionModel()->select( idx, QItemSelectionModel::Rows | QItemSelectionModel::Select ); |
|
break; |
|
case GrowOrShrinkExistingSelection: |
|
selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate ); |
|
growOrShrinkExistingSelection( idx, false ); |
|
break; |
|
default: |
|
//case ClearExistingSelection: |
|
setCurrentIndex( idx ); |
|
break; |
|
} |
|
|
|
if ( centerItem ) |
|
scrollTo( idx, QAbstractItemView::PositionAtCenter ); |
|
|
|
return true; |
|
} |
|
|
|
bool View::selectPreviousMessageItem( |
|
MessageTypeFilter messageTypeFilter, |
|
ExistingSelectionBehaviour existingSelectionBehaviour, |
|
bool centerItem, |
|
bool loop |
|
) |
|
{ |
|
Item * it = previousMessageItem( messageTypeFilter, loop ); |
|
if ( !it ) |
|
return false; |
|
|
|
setFocus(); |
|
|
|
if ( it->parent() != mModel->rootItem() ) |
|
ensureCurrentlyViewable( it ); |
|
|
|
QModelIndex idx = mModel->index( it, 0 ); |
|
|
|
Q_ASSERT( idx.isValid() ); |
|
|
|
switch ( existingSelectionBehaviour ) |
|
{ |
|
case ExpandExistingSelection: |
|
selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate ); |
|
selectionModel()->select( idx, QItemSelectionModel::Rows | QItemSelectionModel::Select ); |
|
break; |
|
case GrowOrShrinkExistingSelection: |
|
selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate ); |
|
growOrShrinkExistingSelection( idx, true ); |
|
break; |
|
default: |
|
//case ClearExistingSelection: |
|
setCurrentIndex( idx ); |
|
break; |
|
} |
|
|
|
if ( centerItem ) |
|
scrollTo( idx, QAbstractItemView::PositionAtCenter ); |
|
|
|
return true; |
|
} |
|
|
|
bool View::focusNextMessageItem( MessageTypeFilter messageTypeFilter, bool centerItem, bool loop ) |
|
{ |
|
Item * it = nextMessageItem( messageTypeFilter, loop ); |
|
if ( !it ) |
|
return false; |
|
|
|
setFocus(); |
|
|
|
if ( it->parent() != mModel->rootItem() ) |
|
ensureCurrentlyViewable( it ); |
|
|
|
QModelIndex idx = mModel->index( it, 0 ); |
|
|
|
Q_ASSERT( idx.isValid() ); |
|
|
|
selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate ); |
|
|
|
if ( centerItem ) |
|
scrollTo( idx, QAbstractItemView::PositionAtCenter ); |
|
|
|
return true; |
|
} |
|
|
|
bool View::focusPreviousMessageItem( MessageTypeFilter messageTypeFilter, bool centerItem, bool loop ) |
|
{ |
|
Item * it = previousMessageItem( messageTypeFilter, loop ); |
|
if ( !it ) |
|
return false; |
|
|
|
setFocus(); |
|
|
|
if ( it->parent() != mModel->rootItem() ) |
|
ensureCurrentlyViewable( it ); |
|
|
|
QModelIndex idx = mModel->index( it, 0 ); |
|
|
|
Q_ASSERT( idx.isValid() ); |
|
|
|
selectionModel()->setCurrentIndex( idx, QItemSelectionModel::NoUpdate ); |
|
|
|
if ( centerItem ) |
|
scrollTo( idx, QAbstractItemView::PositionAtCenter ); |
|
|
|
return true; |
|
} |
|
|
|
void View::selectFocusedMessageItem( bool centerItem ) |
|
{ |
|
QModelIndex idx = currentIndex(); |
|
if ( !idx.isValid() ) |
|
return; |
|
|
|
setFocus(); |
|
|
|
if ( selectionModel()->isSelected( idx ) ) |
|
return; |
|
|
|
selectionModel()->select( idx, QItemSelectionModel::Select | QItemSelectionModel::Current | QItemSelectionModel::Rows ); |
|
|
|
if ( centerItem ) |
|
scrollTo( idx, QAbstractItemView::PositionAtCenter ); |
|
} |
|
|
|
void View::applyMessagePreSelection( PreSelectionMode preSelectionMode ) |
|
{ |
|
mModel->applyMessagePreSelection( preSelectionMode ); |
|
} |
|
|
|
|
|
bool View::selectFirstMessageItem( MessageTypeFilter messageTypeFilter, bool centerItem ) |
|
{ |
|
if ( !storageModel() ) |
|
return false; // nothing to do |
|
|
|
Item * it = firstMessageItem( messageTypeFilter ); |
|
if ( !it ) |
|
return false; |
|
|
|
Q_ASSERT( it != mModel->rootItem() ); // must never happen (obviously) |
|
|
|
setFocus(); |
|
ensureCurrentlyViewable( it ); |
|
|
|
QModelIndex idx = mModel->index( it, 0 ); |
|
|
|
Q_ASSERT( idx.isValid() ); |
|
|
|
setCurrentIndex( idx ); |
|
|
|
if ( centerItem ) |
|
scrollTo( idx, QAbstractItemView::PositionAtCenter ); |
|
|
|
return true; |
|
} |
|
|
|
void View::modelFinishedLoading() |
|
{ |
|
Q_ASSERT( storageModel() ); |
|
Q_ASSERT( !mModel->isLoading() ); |
|
|
|
// nothing here for now :) |
|
} |
|
|
|
MessageItemSetReference View::createPersistentSet( const QList< MessageItem * > &items ) |
|
{ |
|
return mModel->createPersistentSet( items ); |
|
} |
|
|
|
QList< MessageItem * > View::persistentSetCurrentMessageItemList( MessageItemSetReference ref ) |
|
{ |
|
return mModel->persistentSetCurrentMessageItemList( ref ); |
|
} |
|
|
|
void View::deletePersistentSet( MessageItemSetReference ref ) |
|
{ |
|
mModel->deletePersistentSet( ref ); |
|
} |
|
|
|
void View::markMessageItemsAsAboutToBeRemoved( QList< MessageItem * > &items, bool bMark ) |
|
{ |
|
if ( bMark ) |
|
{ |
|
for ( QList< MessageItem * >::Iterator it = items.begin(); it != items.end(); ++it ) |
|
{ |
|
( *it )->setAboutToBeRemoved( true ); |
|
QModelIndex idx = mModel->index( *it, 0 ); |
|
Q_ASSERT( idx.isValid() ); |
|
Q_ASSERT( static_cast< MessageItem * >( idx.internalPointer() ) == *it ); |
|
if ( selectionModel()->isSelected( idx ) ) |
|
selectionModel()->select( idx, QItemSelectionModel::Deselect | QItemSelectionModel::Rows ); |
|
} |
|
} else { |
|
for ( QList< MessageItem * >::Iterator it = items.begin(); it != items.end(); ++it ) |
|
{ |
|
if ( ( *it )->isValid() ) // hasn't been removed in the meantime |
|
( *it )->setAboutToBeRemoved( false ); |
|
} |
|
} |
|
|
|
viewport()->update(); |
|
} |
|
|
|
void View::ensureCurrentlyViewable( Item * it ) |
|
{ |
|
Q_ASSERT( it ); |
|
Q_ASSERT( it->parent() ); |
|
Q_ASSERT( it->isViewable() ); // must be attached to the viewable root |
|
|
|
if ( isRowHidden( it->parent()->indexOfChildItem( it ), mModel->index( it->parent(), 0 ) ) ) |
|
setRowHidden( it->parent()->indexOfChildItem( it ), mModel->index( it->parent(), 0 ), false ); |
|
|
|
it = it->parent(); |
|
|
|
while ( it->parent() ) |
|
{ |
|
if ( isRowHidden( it->parent()->indexOfChildItem( it ), mModel->index( it->parent(), 0 ) ) ) |
|
setRowHidden( it->parent()->indexOfChildItem( it ), mModel->index( it->parent(), 0 ), false ); |
|
|
|
QModelIndex idx = mModel->index( it, 0 ); |
|
|
|
Q_ASSERT( idx.isValid() ); |
|
Q_ASSERT( static_cast< Item * >( idx.internalPointer() ) == it ); |
|
|
|
if ( !isExpanded( idx ) ) |
|
setExpanded( idx, true ); |
|
|
|
it = it->parent(); |
|
} |
|
} |
|
|
|
bool View::isCurrentlyViewable( Item * it ) const |
|
{ |
|
Q_ASSERT( it ); |
|
Q_ASSERT( it->parent() ); |
|
|
|
if ( !it->isViewable() ) |
|
return false; |
|
if ( isRowHidden( it->parent()->indexOfChildItem( it ), mModel->index( it->parent(), 0 ) ) ) |
|
return false; |
|
|
|
it = it->parent(); |
|
|
|
if ( it == mModel->rootItem() ) |
|
return true; |
|
|
|
while ( it ) |
|
{ |
|
if ( it->parent() == mModel->rootItem() ) |
|
return true; |
|
if ( !isExpanded( mModel->index( it, 0 ) ) ) |
|
return false; |
|
it = it->parent(); |
|
} |
|
return false; |
|
} |
|
|
|
bool View::isThreaded() const |
|
{ |
|
if ( !mAggregation ) |
|
return false; |
|
return mAggregation->threading() != Aggregation::NoThreading; |
|
} |
|
|
|
void View::slotSelectionChanged( const QItemSelection &, const QItemSelection & ) |
|
{ |
|
// We assume that when selection changes, current item also changes. |
|
QModelIndex current = currentIndex(); |
|
|
|
// Abort any pending message pre-selection as the user is probably |
|
// already navigating the view (so pre-selection would make his view jump |
|
// to an unexpected place). |
|
mModel->abortMessagePreSelection(); |
|
|
|
if ( !current.isValid() ) |
|
{ |
|
if ( mLastCurrentItem ) |
|
{ |
|
mWidget->viewMessageSelected( 0 ); |
|
mLastCurrentItem = 0; |
|
} |
|
mWidget->viewMessageSelected( 0 ); |
|
mWidget->viewSelectionChanged(); |
|
return; |
|
} |
|
|
|
if ( !selectionModel()->isSelected( current ) ) |
|
{ |
|
if ( selectedIndexes().count() < 1 ) |
|
{ |
|
// It may happen after row removals: Model calls this slot on currentIndex() |
|
// that actually might have changed "silently", without being selected. |
|
QItemSelection selection; |
|
selection.append( QItemSelectionRange( current ) ); |
|
selectionModel()->select( selection, QItemSelectionModel::Select | QItemSelectionModel::Rows ); |
|
} else { |
|
// something is still selected anyway |
|
// This is probably a result of CTRL+Click which unselected current: leave it as it is. |
|
return; |
|
} |
|
} |
|
|
|
Item * it = static_cast< Item * >( current.internalPointer() ); |
|
Q_ASSERT( it ); |
|
|
|
switch ( it->type() ) |
|
{ |
|
case Item::Message: |
|
{ |
|
if ( mLastCurrentItem != it ) |
|
{ |
|
kDebug() << "Message selected [" << it->subject().toUtf8().data() << "]" << endl; |
|
mWidget->viewMessageSelected( static_cast< MessageItem * >( it ) ); |
|
mLastCurrentItem = 0; |
|
} |
|
} |
|
break; |
|
case Item::GroupHeader: |
|
if ( mLastCurrentItem ) |
|
{ |
|
mWidget->viewMessageSelected( 0 ); |
|
mLastCurrentItem = 0; |
|
} |
|
break; |
|
default: |
|
// should never happen |
|
Q_ASSERT( false ); |
|
break; |
|
} |
|
|
|
mWidget->viewSelectionChanged(); |
|
} |
|
|
|
void View::mouseDoubleClickEvent( QMouseEvent * e ) |
|
{ |
|
// Perform a hit test |
|
if ( !mDelegate->hitTest( e->pos(), true ) ) |
|
return; |
|
|
|
// Something was hit :) |
|
|
|
Item * it = static_cast< Item * >( mDelegate->hitItem() ); |
|
if ( !it ) |
|
return; // should never happen |
|
|
|
switch ( it->type() ) |
|
{ |
|
case Item::Message: |
|
{ |
|
// Let QTreeView handle the expansion |
|
QTreeView::mousePressEvent( e ); |
|
|
|
switch ( e->button() ) |
|
{ |
|
case Qt::LeftButton: |
|
|
|
if ( mDelegate->hitContentItem() ) |
|
{ |
|
// Double clikcking on clickable icons does NOT activate the message |
|
if ( mDelegate->hitContentItem()->isIcon() && mDelegate->hitContentItem()->isClickable() ) |
|
return; |
|
} |
|
|
|
mWidget->viewMessageActivated( static_cast< MessageItem * >( it ) ); |
|
break; |
|
default: |
|
// make gcc happy |
|
break; |
|
} |
|
} |
|
break; |
|
case Item::GroupHeader: |
|
{ |
|
// Don't let QTreeView handle the selection (as it deselects the curent messages) |
|
switch ( e->button() ) |
|
{ |
|
case Qt::LeftButton: |
|
if ( it->childItemCount() > 0 ) |
|
{ |
|
// toggle expanded state |
|
setExpanded( mDelegate->hitIndex(), !isExpanded( mDelegate->hitIndex() ) ); |
|
} |
|
break; |
|
default: |
|
// make gcc happy |
|
break; |
|
} |
|
} |
|
break; |
|
default: |
|
// should never happen |
|
Q_ASSERT( false ); |
|
break; |
|
} |
|
} |
|
|
|
void View::changeMessageStatus( MessageItem * it, const KPIM::MessageStatus &set, const KPIM::MessageStatus &unset ) |
|
{ |
|
// We first change the status of MessageItem itself. This will make the change |
|
// visible to the user even if the Model is actually in the middle of a long job (maybe it's loading) |
|
// and can't process the status change request immediately. |
|
// Here we actually desynchronize the cache and trust that the later call to |
|
// mWidget->viewMessageStatusChangeRequest() will really perform the status change on the storage. |
|
// Well... in KMail it will unless something is really screwed. Anyway, if it will not, at the next |
|
// load the status will be just unchanged: no animals will be harmed. |
|
|
|
qint32 stat = it->status().toQInt32(); |
|
stat |= set.toQInt32(); |
|
stat &= ~( unset.toQInt32() ); |
|
KPIM::MessageStatus status; |
|
status.fromQInt32( stat ); |
|
it->setStatus( status ); |
|
|
|
// Trigger an update so the immediate change will be shown to the user |
|
|
|
viewport()->update(); |
|
|
|
// This will actually request the widget to perform a status change on the storage. |
|
// The request will be then processed by the Model and the message will be updated again. |
|
|
|
mWidget->viewMessageStatusChangeRequest( it, set, unset ); |
|
} |
|
|
|
void View::mousePressEvent( QMouseEvent * e ) |
|
{ |
|
mMousePressPosition = QPoint(); |
|
|
|
// Perform a hit test |
|
if ( !mDelegate->hitTest( e->pos(), true ) ) |
|
return; |
|
|
|
// Something was hit :) |
|
|
|
Item * it = static_cast< Item * >( mDelegate->hitItem() ); |
|
if ( !it ) |
|
return; // should never happen |
|
|
|
switch ( it->type() ) |
|
{ |
|
case Item::Message: |
|
{ |
|
mMousePressPosition = e->pos(); |
|
|
|
switch ( e->button() ) |
|
{ |
|
case Qt::LeftButton: |
|
// if we have multi selection then the meaning of hitting |
|
// the content item is quite unclear. |
|
if ( mDelegate->hitContentItem() && ( selectedIndexes().count() > 1 ) ) |
|
{ |
|
kDebug() << "Left hit with selectedIndexes().count() == " << selectedIndexes().count(); |
|
|
|
switch ( mDelegate->hitContentItem()->type() ) |
|
{ |
|
case Theme::ContentItem::ActionItemStateIcon: |
|
changeMessageStatus( |
|
static_cast< MessageItem * >( it ), |
|
it->status().isToAct() ? KPIM::MessageStatus() : KPIM::MessageStatus::statusToAct(), |
|
it->status().isToAct() ? KPIM::MessageStatus::statusToAct() : KPIM::MessageStatus() |
|
); |
|
return; // don't select the item |
|
break; |
|
case Theme::ContentItem::ImportantStateIcon: |
|
changeMessageStatus( |
|
static_cast< MessageItem * >( it ), |
|
it->status().isImportant() ? KPIM::MessageStatus() : KPIM::MessageStatus::statusImportant(), |
|
it->status().isImportant() ? KPIM::MessageStatus::statusImportant() : KPIM::MessageStatus() |
|
); |
|
return; // don't select the item |
|
break; |
|
case Theme::ContentItem::SpamHamStateIcon: |
|
changeMessageStatus( |
|
static_cast< MessageItem * >( it ), |
|
it->status().isSpam() ? KPIM::MessageStatus() : ( it->status().isHam() ? KPIM::MessageStatus::statusSpam() : KPIM::MessageStatus::statusHam() ), |
|
it->status().isSpam() ? KPIM::MessageStatus::statusSpam() : ( it->status().isHam() ? KPIM::MessageStatus::statusHam() : KPIM::MessageStatus() ) |
|
); |
|
return; // don't select the item |
|
break; |
|
case Theme::ContentItem::WatchedIgnoredStateIcon: |
|
changeMessageStatus( |
|
static_cast< MessageItem * >( it ), |
|
it->status().isIgnored() ? KPIM::MessageStatus() : ( it->status().isWatched() ? KPIM::MessageStatus::statusIgnored() : KPIM::MessageStatus::statusWatched() ), |
|
it->status().isIgnored() ? KPIM::MessageStatus::statusIgnored() : ( it->status().isWatched() ? KPIM::MessageStatus::statusWatched() : KPIM::MessageStatus() ) |
|
); |
|
return; // don't select the item |
|
break; |
|
default: |
|
// make gcc happy |
|
break; |
|
} |
|
} |
|
|
|
// Let QTreeView handle the selection and emit the appropriate signals (slotSelectionChanged() may be called) |
|
QTreeView::mousePressEvent( e ); |
|
|
|
break; |
|
case Qt::RightButton: |
|
// Let QTreeView handle the selection and emit the appropriate signals (slotSelectionChanged() may be called) |
|
QTreeView::mousePressEvent( e ); |
|
|
|
mWidget->viewMessageListContextPopupRequest( selectionAsMessageItemList(), viewport()->mapToGlobal( e->pos() ) ); |
|
break; |
|
default: |
|
// make gcc happy |
|
break; |
|
} |
|
} |
|
break; |
|
case Item::GroupHeader: |
|
{ |
|
// Don't let QTreeView handle the selection (as it deselects the curent messages) |
|
GroupHeaderItem *groupHeaderItem = static_cast< GroupHeaderItem * >( it ); |
|
|
|
switch ( e->button() ) |
|
{ |
|
case Qt::LeftButton: |
|
if ( !mDelegate->hitContentItem() ) |
|
return; |
|
|
|
if ( mDelegate->hitContentItem()->type() == Theme::ContentItem::ExpandedStateIcon ) |
|
{ |
|
if ( groupHeaderItem->childItemCount() > 0 ) |
|
{ |
|
// toggle expanded state |
|
setExpanded( mDelegate->hitIndex(), !isExpanded( mDelegate->hitIndex() ) ); |
|
} |
|
} |
|
break; |
|
case Qt::RightButton: |
|
clearSelection(); // make sure it's true, so it's clear that the eventual popup belongs to the group header |
|
mWidget->viewGroupHeaderContextPopupRequest( groupHeaderItem, viewport()->mapToGlobal( e->pos() ) ); |
|
break; |
|
default: |
|
// make gcc happy |
|
break; |
|
} |
|
} |
|
break; |
|
default: |
|
// should never happen |
|
Q_ASSERT( false ); |
|
break; |
|
} |
|
} |
|
|
|
void View::mouseMoveEvent( QMouseEvent * e ) |
|
{ |
|
if ( !e->buttons() & Qt::LeftButton ) |
|
{ |
|
QTreeView::mouseMoveEvent( e ); |
|
return; |
|
} |
|
|
|
if ( mMousePressPosition.isNull() ) |
|
return; |
|
|
|
if ( ( e->pos() - mMousePressPosition ).manhattanLength() <= KGlobalSettings::dndEventDelay() ) |
|
return; |
|
|
|
mWidget->viewStartDragRequest(); |
|
} |
|
|
|
void View::dragEnterEvent( QDragEnterEvent * e ) |
|
{ |
|
mWidget->viewDragEnterEvent( e ); |
|
} |
|
|
|
void View::dragMoveEvent( QDragMoveEvent * e ) |
|
{ |
|
mWidget->viewDragMoveEvent( e ); |
|
} |
|
|
|
void View::dropEvent( QDropEvent * e ) |
|
{ |
|
mWidget->viewDropEvent( e ); |
|
} |
|
|
|
void View::changeEvent( QEvent *e ) |
|
{ |
|
switch ( e->type() ) |
|
{ |
|
case QEvent::PaletteChange: |
|
case QEvent::FontChange: |
|
case QEvent::StyleChange: |
|
case QEvent::LayoutDirectionChange: |
|
case QEvent::LocaleChange: |
|
case QEvent::LanguageChange: |
|
// All of these affect the theme's internal cache. |
|
setTheme( mTheme ); |
|
// A layoutChanged() event will screw up the view state a bit. |
|
// Since this is a rare event we just reload the view. |
|
reload(); |
|
break; |
|
default: |
|
// make gcc happy by default |
|
break; |
|
} |
|
|
|
QTreeView::changeEvent( e ); |
|
} |
|
|
|
bool View::event( QEvent *e ) |
|
{ |
|
// We catch ToolTip events and pass everything else |
|
|
|
if( e->type() != QEvent::ToolTip ) |
|
return QTreeView::event( e ); |
|
|
|
QHelpEvent * he = dynamic_cast< QHelpEvent * >( e ); |
|
if ( !he ) |
|
return true; // eh ? |
|
|
|
if ( !Manager::instance()->displayMessageToolTips() ) |
|
return true; // don't display tooltips |
|
|
|
QPoint pnt = viewport()->mapFromGlobal( mapToGlobal( he->pos() ) ); |
|
|
|
if ( pnt.y() < 0 ) |
|
return true; // don't display the tooltip for items hidden under the header |
|
|
|
QModelIndex idx = indexAt( pnt ); |
|
if ( !idx.isValid() ) |
|
return true; // may be |
|
|
|
Item * it = static_cast< Item * >( idx.internalPointer() ); |
|
if ( !it ) |
|
return true; // hum |
|
|
|
Q_ASSERT( storageModel() ); |
|
|
|
QColor bckColor = palette().color( QPalette::ToolTipBase ); |
|
QColor txtColor = palette().color( QPalette::ToolTipText ); |
|
QColor darkerColor( |
|
( ( bckColor.red() * 8 ) + ( txtColor.red() * 2 ) ) / 10, |
|
( ( bckColor.green() * 8 ) + ( txtColor.green() * 2 ) ) / 10, |
|
( ( bckColor.blue() * 8 ) + ( txtColor.blue() * 2 ) ) / 10 |
|
); |
|
|
|
QString bckColorName = bckColor.name(); |
|
QString txtColorName = txtColor.name(); |
|
QString darkerColorName = darkerColor.name(); |
|
|
|
|
|
QString tip = QString::fromLatin1( |
|
"<table width=\"100%\" border=\"0\" cellpadding=\"2\" cellspacing=\"0\">" |
|
); |
|
|
|
switch ( it->type() ) |
|
{ |
|
case Item::Message: |
|
{ |
|
MessageItem *mi = static_cast< MessageItem * >( it ); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td bgcolor=\"%1\" align=\"left\" valign=\"middle\">" \ |
|
"<div style=\"color: %2; font-weight: bold;\">" \ |
|
"%3" \ |
|
"</div>" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( txtColorName ).arg( bckColorName ).arg( mi->subject() ); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td align=\"center\" valign=\"middle\">" \ |
|
"<table width=\"100%\" border=\"0\" cellpadding=\"2\" cellspacing=\"0\">" |
|
); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td align=\"right\" valign=\"top\" width=\"45\">" \ |
|
"<div style=\"font-weight: bold;\"><nobr>" \ |
|
"%1:" \ |
|
"</nobr></div>" \ |
|
"</td>" \ |
|
"<td align=\"left\" valign=\"top\">" \ |
|
"%2" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( i18n( "From" ) ).arg( mi->sender() ); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td align=\"right\" valign=\"top\" width=\"45\">" \ |
|
"<div style=\"font-weight: bold;\"><nobr>" \ |
|
"%1:" \ |
|
"</nobr></div>" \ |
|
"</td>" \ |
|
"<td align=\"left\" valign=\"top\">" \ |
|
"%2" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( i18nc( "Receiver of the emial", "To" ) ).arg( mi->receiver() ); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td align=\"right\" valign=\"top\" width=\"45\">" \ |
|
"<div style=\"font-weight: bold;\"><nobr>" \ |
|
"%1:" \ |
|
"</nobr></div>" \ |
|
"</td>" \ |
|
"<td align=\"left\" valign=\"top\">" \ |
|
"%2" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( i18n( "Date" ) ).arg( mi->formattedDate() ); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td align=\"right\" valign=\"top\" width=\"45\">" \ |
|
"<div style=\"font-weight: bold;\"><nobr>" \ |
|
"%1:" \ |
|
"</nobr></div>" \ |
|
"</td>" \ |
|
"<td align=\"left\" valign=\"top\">" \ |
|
"%2" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( i18n( "Status" ) ).arg( mi->statusDescription() ); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td align=\"right\" valign=\"top\" width=\"45\">" \ |
|
"<div style=\"font-weight: bold;\"><nobr>" \ |
|
"%1:" \ |
|
"</nobr></div>" \ |
|
"</td>" \ |
|
"<td align=\"left\" valign=\"top\">" \ |
|
"%2" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( i18n( "Size" ) ).arg( mi->formattedSize() ); |
|
|
|
|
|
tip += QString::fromLatin1( |
|
"</table" \ |
|
"</td>" \ |
|
"</tr>" |
|
); |
|
|
|
// FIXME: Find a way to show also CC and other header fields ? |
|
// Text-mail 2 line preview would be also nice.. but that's kinda hard... |
|
|
|
if ( mi->hasChildren() ) |
|
{ |
|
Item::ChildItemStats stats; |
|
mi->childItemStats( stats ); |
|
|
|
QString statsText; |
|
|
|
statsText = i18np( "<b>%1</b> reply", "<b>%1</b> replies", mi->childItemCount() ); |
|
statsText += QLatin1String( ", " ); |
|
|
|
statsText += i18np( |
|
"<b>%1</b> message in subtree (<b>%2</b> new + <b>%3</b> unread)", |
|
"<b>%1</b> messages in subtree (<b>%2</b> new + <b>%3</b> unread)", |
|
stats.mTotalChildCount, |
|
stats.mNewChildCount, |
|
stats.mUnreadChildCount |
|
); |
|
|
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td bgcolor=\"%1\" align=\"left\" valign=\"middle\">" \ |
|
"<nobr>%2</nobr>" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( darkerColorName ).arg( statsText ); |
|
} |
|
|
|
} |
|
break; |
|
case Item::GroupHeader: |
|
{ |
|
GroupHeaderItem *ghi = static_cast< GroupHeaderItem * >( it ); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td bgcolor=\"%1\" align=\"left\" valign=\"middle\">" \ |
|
"<div style=\"color: %2; font-weight: bold;\">" \ |
|
"%3" \ |
|
"</div>" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( txtColorName ).arg( bckColorName ).arg( ghi->label() ); |
|
|
|
QString description; |
|
|
|
switch( mAggregation->grouping() ) |
|
{ |
|
case Aggregation::GroupByDate: |
|
if ( mAggregation->threading() != Aggregation::NoThreading ) |
|
{ |
|
switch ( mAggregation->threadLeader() ) |
|
{ |
|
case Aggregation::TopmostMessage: |
|
if ( ghi->label().contains( QRegExp( "[0-9]" ) ) ) |
|
description = i18nc( |
|
"@info:tooltip Formats to something like 'Threads started on 2008-12-21'", |
|
"Threads started on %1", |
|
ghi->label() |
|
); |
|
else |
|
description = i18nc( |
|
"@info:tooltip Formats to something like 'Threads started Yesterday'", |
|
"Threads started %1", |
|
ghi->label() |
|
); |
|
break; |
|
case Aggregation::MostRecentMessage: |
|
description = i18n( "Threads with messages dated %1", ghi->label() ); |
|
break; |
|
default: |
|
// nuthin, make gcc happy |
|
break; |
|
} |
|
} else { |
|
if ( ghi->label().contains( QRegExp( "[0-9]" ) ) ) |
|
{ |
|
if ( storageModel()->containsOutboundMessages() ) |
|
description = i18nc( |
|
"@info:tooltip Formats to something like 'Messages sent on 2008-12-21'", |
|
"Messages sent on %1", |
|
ghi->label() |
|
); |
|
else |
|
description = i18nc( |
|
"@info:tooltip Formats to something like 'Messages received on 2008-12-21'", |
|
"Messages received on %1", |
|
ghi->label() |
|
); |
|
} else { |
|
if ( storageModel()->containsOutboundMessages() ) |
|
description = i18nc( |
|
"@info:tooltip Formats to something like 'Messages sent Yesterday'", |
|
"Messages sent %1", |
|
ghi->label() |
|
); |
|
else |
|
description = i18nc( |
|
"@info:tooltip Formats to something like 'Messages received Yesterday'", |
|
"Messages received %1", |
|
ghi->label() |
|
); |
|
} |
|
} |
|
break; |
|
case Aggregation::GroupByDateRange: |
|
if ( mAggregation->threading() != Aggregation::NoThreading ) |
|
{ |
|
switch ( mAggregation->threadLeader() ) |
|
{ |
|
case Aggregation::TopmostMessage: |
|
description = i18n( "Threads started within %1", ghi->label() ); |
|
break; |
|
case Aggregation::MostRecentMessage: |
|
description = i18n( "Threads containing messages with dates within %1", ghi->label() ); |
|
break; |
|
default: |
|
// nuthin, make gcc happy |
|
break; |
|
} |
|
} else { |
|
if ( storageModel()->containsOutboundMessages() ) |
|
description = i18n( "Messages sent within %1", ghi->label() ); |
|
else |
|
description = i18n( "Messages received within %1", ghi->label() ); |
|
} |
|
break; |
|
case Aggregation::GroupBySenderOrReceiver: |
|
case Aggregation::GroupBySender: |
|
if ( mAggregation->threading() != Aggregation::NoThreading ) |
|
{ |
|
switch ( mAggregation->threadLeader() ) |
|
{ |
|
case Aggregation::TopmostMessage: |
|
description = i18n( "Threads started by %1", ghi->label() ); |
|
break; |
|
case Aggregation::MostRecentMessage: |
|
description = i18n( "Threads with most recent message by %1", ghi->label() ); |
|
break; |
|
default: |
|
// nuthin, make gcc happy |
|
break; |
|
} |
|
} else { |
|
if ( storageModel()->containsOutboundMessages() ) |
|
{ |
|
if ( mAggregation->grouping() == Aggregation::GroupBySenderOrReceiver ) |
|
description = i18n( "Messages sent to %1", ghi->label() ); |
|
else |
|
description = i18n( "Messages sent by %1", ghi->label() ); |
|
} else { |
|
description = i18n( "Messages received from %1", ghi->label() ); |
|
} |
|
} |
|
break; |
|
case Aggregation::GroupByReceiver: |
|
if ( mAggregation->threading() != Aggregation::NoThreading ) |
|
{ |
|
switch ( mAggregation->threadLeader() ) |
|
{ |
|
case Aggregation::TopmostMessage: |
|
description = i18n( "Threads directed to %1", ghi->label() ); |
|
break; |
|
case Aggregation::MostRecentMessage: |
|
description = i18n( "Threads with most recent message directed to %1", ghi->label() ); |
|
break; |
|
default: |
|
// nuthin, make gcc happy |
|
break; |
|
} |
|
} else { |
|
if ( storageModel()->containsOutboundMessages() ) |
|
{ |
|
description = i18n( "Messages sent to %1", ghi->label() ); |
|
} else { |
|
description = i18n( "Messages received by %1", ghi->label() ); |
|
} |
|
} |
|
break; |
|
default: |
|
// nuthin, make gcc happy |
|
break; |
|
} |
|
|
|
if ( !description.isEmpty() ) |
|
{ |
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td align=\"left\" valign=\"middle\">" \ |
|
"%1" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( description ); |
|
} |
|
|
|
if ( ghi->hasChildren() ) |
|
{ |
|
Item::ChildItemStats stats; |
|
ghi->childItemStats( stats ); |
|
|
|
QString statsText; |
|
|
|
if ( mAggregation->threading() != Aggregation::NoThreading ) |
|
{ |
|
statsText = i18np( "<b>%1</b> thread", "<b>%1</b> threads", ghi->childItemCount() ); |
|
statsText += QLatin1String( ", " ); |
|
} |
|
|
|
statsText += i18np( |
|
"<b>%1</b> message (<b>%2</b> new + <b>%3</b> unread)", |
|
"<b>%1</b> messages (<b>%2</b> new + <b>%3</b> unread)", |
|
stats.mTotalChildCount, |
|
stats.mNewChildCount, |
|
stats.mUnreadChildCount |
|
); |
|
|
|
tip += QString::fromLatin1( |
|
"<tr>" \ |
|
"<td bgcolor=\"%1\" align=\"left\" valign=\"middle\">" \ |
|
"<nobr>%2</nobr>" \ |
|
"</td>" \ |
|
"</tr>" |
|
).arg( darkerColorName ).arg( statsText ); |
|
} |
|
|
|
} |
|
break; |
|
default: |
|
// nuthin (just make gcc happy for now) |
|
break; |
|
} |
|
|
|
|
|
tip += QString::fromLatin1( |
|
"</table>" |
|
); |
|
|
|
QToolTip::showText( he->globalPos(), tip, viewport(), visualRect( idx ) ); |
|
|
|
return true; |
|
} |
|
|
|
void View::slotCollapseAllGroups() |
|
{ |
|
if ( mAggregation->grouping() == Aggregation::NoGrouping ) |
|
return; |
|
|
|
Item * item = mModel->rootItem(); |
|
|
|
QList< Item * > * childList = item->childItems(); |
|
if ( !childList ) |
|
return; |
|
|
|
for ( QList< Item * >::Iterator it = childList->begin(); it != childList->end(); ++it ) |
|
{ |
|
Q_ASSERT( ( *it )->type() == Item::GroupHeader ); |
|
QModelIndex idx = mModel->index( *it, 0 ); |
|
Q_ASSERT( idx.isValid() ); |
|
Q_ASSERT( static_cast< Item * >( idx.internalPointer() ) == ( *it ) ); |
|
if ( isExpanded( idx ) ) |
|
setExpanded( idx, false ); |
|
} |
|
} |
|
|
|
void View::slotExpandAllGroups() |
|
{ |
|
if ( mAggregation->grouping() == Aggregation::NoGrouping ) |
|
return; |
|
|
|
Item * item = mModel->rootItem(); |
|
|
|
QList< Item * > * childList = item->childItems(); |
|
if ( !childList ) |
|
return; |
|
|
|
for ( QList< Item * >::Iterator it = childList->begin(); it != childList->end(); ++it ) |
|
{ |
|
Q_ASSERT( ( *it )->type() == Item::GroupHeader ); |
|
QModelIndex idx = mModel->index( *it, 0 ); |
|
Q_ASSERT( idx.isValid() ); |
|
Q_ASSERT( static_cast< Item * >( idx.internalPointer() ) == ( *it ) ); |
|
if ( !isExpanded( idx ) ) |
|
setExpanded( idx, true ); |
|
} |
|
} |
|
|
|
} // namespace Core |
|
|
|
} // namespace MessageListView |
|
|
|
} // namespace KMail |
|
|
|
|
|
#if 0 |
|
|
|
void KMHeaders::setFolder( KMFolder *aFolder, bool forceJumpToUnread ) |
|
{ |
|
if (mFolder) { |
|
disconnect(mFolder, SIGNAL(numUnreadMsgsChanged(KMFolder*)), |
|
this, SLOT(setFolderInfoStatus())); |
|
disconnect(mFolder, SIGNAL(changed()), |
|
this, SLOT(msgChanged())); |
|
disconnect( mFolder, SIGNAL( statusMsg( const QString& ) ), |
|
BroadcastStatus::instance(), SLOT( setStatusMsg( const QString& ) ) ); |
|
disconnect(mFolder, SIGNAL(viewConfigChanged()), this, SLOT(reset())); |
|
} |
|
|
|
mOwner->useAction()->setEnabled( mFolder ? |
|
( kmkernel->folderIsTemplates( mFolder ) ) : false ); |
|
mOwner->messageActions()->replyListAction()->setEnabled( mFolder ? |
|
mFolder->isMailingListEnabled() : false ); |
|
if (mFolder) |
|
{ |
|
connect(mFolder, SIGNAL(changed()), |
|
this, SLOT(msgChanged())); |
|
connect(mFolder, SIGNAL(statusMsg(const QString&)), |
|
BroadcastStatus::instance(), SLOT( setStatusMsg( const QString& ) ) ); |
|
connect(mFolder, SIGNAL(numUnreadMsgsChanged(KMFolder*)), |
|
this, SLOT(setFolderInfoStatus())); |
|
connect(mFolder, SIGNAL(viewConfigChanged()), this, SLOT(reset())); |
|
} |
|
|
|
colText = i18n( "Date" ); |
|
if (mPaintInfo.orderOfArrival) |
|
colText = i18n( "Date (Order of Arrival)" ); |
|
setColumnText( mPaintInfo.dateCol, colText); |
|
} |
|
} |
|
|
|
void KMHeaders::setFolderInfoStatus () |
|
{ |
|
if ( !mFolder ) return; |
|
QString str; |
|
const int unread = mFolder->countUnread(); |
|
if ( static_cast<KMFolder*>(mFolder) == kmkernel->outboxFolder() ) |
|
str = unread ? i18ncp( "Number of unsent messages", "1 unsent", "%1 unsent", unread ) : i18n( "0 unsent" ); |
|
else |
|
str = unread ? i18ncp( "Number of unread messages", "1 unread", "%1 unread", unread ) |
|
: i18nc( "No unread messages", "0 unread" ); |
|
const int count = mFolder->count(); |
|
str = count ? i18ncp( "Number of unread messages", "1 message, %2.", "%1 messages, %2.", count, str ) |
|
: i18nc( "No unread messages", "0 messages" ); // no need for "0 unread" to be added here |
|
if ( mFolder->isReadOnly() ) |
|
str = i18nc("%1 = n messages, m unread.", "%1 Folder is read-only.", str ); |
|
BroadcastStatus::instance()->setStatusMsg(str); |
|
} |
|
|
|
#endif |
|
|
|
|