kde-playground/kdepim/calendarviews/month/monthview.cpp
2015-04-14 21:49:29 +00:00

638 lines
19 KiB
C++

/*
Copyright (c) 2008 Bruno Virlet <bruno.virlet@gmail.com>
Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net
Author: Bertjan Broeksema, broeksema@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.
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.
As a special exception, permission is given to link this program
with any edition of Qt, and distribute the resulting executable,
without including the source code for Qt in the source distribution.
*/
#include "monthview.h"
#include "monthgraphicsitems.h"
#include "monthitem.h"
#include "monthscene.h"
#include "prefs.h"
#include <calendarsupport/collectionselection.h>
#include <Akonadi/Calendar/ETMCalendar>
#include <calendarsupport/kcalprefs.h>
#include <calendarsupport/utils.h>
#include <KCalCore/OccurrenceIterator>
#include <KCheckableProxyModel>
#include <KIcon>
#include <QHBoxLayout>
#include <QTimer>
#include <QToolButton>
#include <QWheelEvent>
using namespace EventViews;
namespace EventViews {
class MonthViewPrivate : public Akonadi::ETMCalendar::CalendarObserver
{
MonthView *q;
public: /// Methods
explicit MonthViewPrivate( MonthView *qq );
void addIncidence( const Akonadi::Item &incidence );
void moveStartDate( int weeks, int months );
// void setUpModels();
void triggerDelayedReload( EventView::Change reason );
public: /// Members
QTimer reloadTimer;
MonthScene *scene;
QDate selectedItemDate;
Akonadi::Item::Id selectedItemId;
MonthGraphicsView *view;
QToolButton *fullView;
// List of uids for QDate
QMap<QDate, QStringList > mBusyDays;
protected:
/* reimplemented from KCalCore::Calendar::CalendarObserver */
void calendarIncidenceAdded( const KCalCore::Incidence::Ptr &incidence );
void calendarIncidenceChanged( const KCalCore::Incidence::Ptr &incidence );
void calendarIncidenceDeleted( const KCalCore::Incidence::Ptr &incidence );
};
}
MonthViewPrivate::MonthViewPrivate( MonthView *qq )
: q( qq ),
scene( new MonthScene( qq ) ),
selectedItemId( -1 ),
view( new MonthGraphicsView( qq ) ),
fullView( 0 )
{
reloadTimer.setSingleShot( true );
view->setScene( scene );
}
void MonthViewPrivate::addIncidence( const Akonadi::Item &incidence )
{
Q_UNUSED( incidence );
//TODO: add some more intelligence here...
q->setChanges( q->changes() | EventView::IncidencesAdded );
reloadTimer.start( 50 );
}
void MonthViewPrivate::moveStartDate( int weeks, int months )
{
KDateTime start = q->startDateTime();
KDateTime end = q->endDateTime();
start = start.addDays( weeks * 7 );
end = end.addDays( weeks * 7 );
start = start.addMonths( months );
end = end.addMonths( months );
#ifndef KDEPIM_MOBILE_UI
KCalCore::DateList dateList;
QDate d = start.date();
while ( d <= end.date() ) {
dateList.append( d );
d = d.addDays( 1 );
}
/**
* If we call q->setDateRange( start, end ); directly,
* it will change the selected dates in month view,
* but the application won't know about it.
* The correct way is to emit datesSelected()
* #250256
* */
emit q->datesSelected( dateList );
#else
// korg-mobile doesn't use korg's date navigator.
// Before creating a solution with no #ifndef, we must first extract the remaining views from
// korg, and review the API.
q->setDateRange( start, end );
#endif
}
void MonthViewPrivate::triggerDelayedReload( EventView::Change reason )
{
q->setChanges( q->changes() | reason );
if ( !reloadTimer.isActive() ) {
reloadTimer.start( 50 );
}
}
void MonthViewPrivate::calendarIncidenceAdded( const KCalCore::Incidence::Ptr & )
{
triggerDelayedReload( MonthView::IncidencesAdded );
}
void MonthViewPrivate::calendarIncidenceChanged( const KCalCore::Incidence::Ptr & )
{
triggerDelayedReload( MonthView::IncidencesEdited );
}
void MonthViewPrivate::calendarIncidenceDeleted( const KCalCore::Incidence::Ptr &incidence )
{
Q_ASSERT( !incidence->uid().isEmpty() );
scene->removeIncidence( incidence->uid() );
}
/// MonthView
MonthView::MonthView( NavButtonsVisibility visibility, QWidget *parent )
: EventView( parent ), d( new MonthViewPrivate( this ) )
{
QHBoxLayout *topLayout = new QHBoxLayout( this );
topLayout->addWidget( d->view );
topLayout->setMargin( 0 );
if ( visibility == Visible ) {
QVBoxLayout *rightLayout = new QVBoxLayout( );
rightLayout->setSpacing( 0 );
rightLayout->setMargin( 0 );
// push buttons to the bottom
rightLayout->addStretch( 1 );
d->fullView = new QToolButton( this );
d->fullView->setIcon( KIcon( QLatin1String("view-fullscreen") ) );
d->fullView->setAutoRaise( true );
d->fullView->setCheckable( true );
d->fullView->setChecked( preferences()->fullViewMonth() );
d->fullView->isChecked() ?
d->fullView->setToolTip( i18nc( "@info:tooltip",
"Display calendar in a normal size" ) ) :
d->fullView->setToolTip( i18nc( "@info:tooltip",
"Display calendar in a full window" ) );
d->fullView->setWhatsThis(
i18nc( "@info:whatsthis",
"Click this button and the month view will be enlarged to fill the "
"maximum available window space / or shrunk back to its normal size." ) );
connect( d->fullView, SIGNAL(clicked()),
this, SLOT(changeFullView()) );
QToolButton *minusMonth = new QToolButton( this );
minusMonth->setIcon( KIcon( QLatin1String("arrow-up-double") ) );
minusMonth->setAutoRaise( true );
minusMonth->setToolTip( i18nc( "@info:tooltip", "Go back one month" ) );
minusMonth->setWhatsThis(
i18nc( "@info:whatsthis",
"Click this button and the view will be scrolled back in time by 1 month." ) );
connect( minusMonth, SIGNAL(clicked()),
this, SLOT(moveBackMonth()) );
QToolButton *minusWeek = new QToolButton( this );
minusWeek->setIcon( KIcon( QLatin1String("arrow-up") ) );
minusWeek->setAutoRaise( true );
minusWeek->setToolTip( i18nc( "@info:tooltip", "Go back one week" ) );
minusWeek->setWhatsThis(
i18nc( "@info:whatsthis",
"Click this button and the view will be scrolled back in time by 1 week." ) );
connect( minusWeek, SIGNAL(clicked()),
this, SLOT(moveBackWeek()) );
QToolButton *plusWeek = new QToolButton( this );
plusWeek->setIcon( KIcon( QLatin1String("arrow-down") ) );
plusWeek->setAutoRaise( true );
plusWeek->setToolTip( i18nc( "@info:tooltip", "Go forward one week" ) );
plusWeek->setWhatsThis(
i18nc( "@info:whatsthis",
"Click this button and the view will be scrolled forward in time by 1 week." ) );
connect( plusWeek, SIGNAL(clicked()),
this, SLOT(moveFwdWeek()) );
QToolButton *plusMonth = new QToolButton( this );
plusMonth->setIcon( KIcon( QLatin1String("arrow-down-double") ) );
plusMonth->setAutoRaise( true );
plusMonth->setToolTip( i18nc( "@info:tooltip", "Go forward one month" ) );
plusMonth->setWhatsThis(
i18nc( "@info:whatsthis",
"Click this button and the view will be scrolled forward in time by 1 month." ) );
connect( plusMonth, SIGNAL(clicked()),
this, SLOT(moveFwdMonth()) );
rightLayout->addWidget( d->fullView );
rightLayout->addWidget( minusMonth );
rightLayout->addWidget( minusWeek );
rightLayout->addWidget( plusWeek );
rightLayout->addWidget( plusMonth );
topLayout->addLayout( rightLayout );
} else {
d->view->setFrameStyle( QFrame::NoFrame );
}
connect( d->scene, SIGNAL(showIncidencePopupSignal(Akonadi::Item,QDate)),
SIGNAL(showIncidencePopupSignal(Akonadi::Item,QDate)) );
connect( d->scene, SIGNAL(incidenceSelected(Akonadi::Item,QDate)),
SIGNAL(incidenceSelected(Akonadi::Item,QDate)) );
connect( d->scene, SIGNAL(newEventSignal()),
SIGNAL(newEventSignal()) );
connect( d->scene, SIGNAL(showNewEventPopupSignal()),
SIGNAL(showNewEventPopupSignal()) );
connect( &d->reloadTimer, SIGNAL(timeout()), this, SLOT(reloadIncidences()) );
updateConfig();
// d->setUpModels();
d->reloadTimer.start( 50 );
}
MonthView::~MonthView()
{
if ( calendar() ) {
calendar()->unregisterObserver( d );
}
delete d;
}
void MonthView::updateConfig()
{
d->scene->update();
setChanges( changes() | ConfigChanged );
d->reloadTimer.start( 50 );
}
int MonthView::currentDateCount() const
{
return actualStartDateTime().date().daysTo( actualEndDateTime().date() );
}
KCalCore::DateList MonthView::selectedIncidenceDates() const
{
KCalCore::DateList list;
if ( d->scene->selectedItem() ) {
IncidenceMonthItem *tmp = qobject_cast<IncidenceMonthItem *>( d->scene->selectedItem() );
if ( tmp ) {
QDate selectedItemDate = tmp->realStartDate();
if ( selectedItemDate.isValid() ) {
list << selectedItemDate;
}
}
} else if ( d->scene->selectedCell() ) {
list << d->scene->selectedCell()->date();
}
return list;
}
QDateTime MonthView::selectionStart() const
{
if ( d->scene->selectedCell() ) {
return QDateTime( d->scene->selectedCell()->date() );
} else {
return QDateTime();
}
}
QDateTime MonthView::selectionEnd() const
{
// Only one cell can be selected (for now)
return selectionStart();
}
void MonthView::setDateRange( const KDateTime &start, const KDateTime &end,
const QDate &preferredMonth )
{
EventView::setDateRange( start, end, preferredMonth );
setChanges( changes() | DatesChanged );
d->reloadTimer.start( 50 );
}
bool MonthView::eventDurationHint( QDateTime &startDt, QDateTime &endDt, bool &allDay ) const
{
if ( d->scene->selectedCell() ) {
startDt.setDate( d->scene->selectedCell()->date() );
endDt.setDate( d->scene->selectedCell()->date() );
allDay = true;
return true;
}
return false;
}
void MonthView::showIncidences( const Akonadi::Item::List &incidenceList, const QDate &date )
{
Q_UNUSED( incidenceList );
Q_UNUSED( date );
}
void MonthView::changeIncidenceDisplay( const Akonadi::Item &incidence, int action )
{
Q_UNUSED( incidence );
Q_UNUSED( action );
//TODO: add some more intelligence here...
// don't call reloadIncidences() directly. It would delete all
// MonthItems, but this changeIncidenceDisplay()-method was probably
// called by one of the MonthItem objects. So only schedule a reload
// as event
setChanges( changes() | IncidencesEdited );
d->reloadTimer.start( 50 );
}
void MonthView::updateView()
{
d->view->update();
}
#ifndef QT_NO_WHEELEVENT
void MonthView::wheelEvent( QWheelEvent *event )
{
// invert direction to get scroll-like behaviour
if ( event->delta() > 0 ) {
d->moveStartDate( -1, 0 );
} else if ( event->delta() < 0 ) {
d->moveStartDate( 1, 0 );
}
// call accept in every case, we do not want anybody else to react
event->accept();
}
#endif
void MonthView::keyPressEvent( QKeyEvent *event )
{
if ( event->key() == Qt::Key_PageUp ) {
d->moveStartDate( 0, -1 );
event->accept();
} else if ( event->key() == Qt::Key_PageDown ) {
d->moveStartDate( 0, 1 );
event->accept();
} else if ( processKeyEvent( event ) ) {
event->accept();
} else {
event->ignore();
}
}
void MonthView::keyReleaseEvent( QKeyEvent *event )
{
if ( processKeyEvent( event ) ) {
event->accept();
} else {
event->ignore();
}
}
void MonthView::changeFullView()
{
bool fullView = d->fullView->isChecked();
if( fullView ) {
d->fullView->setIcon( KIcon( QLatin1String("view-restore") ) );
d->fullView->setToolTip( i18nc( "@info:tooltip",
"Display calendar in a normal size" ) );
} else {
d->fullView->setIcon( KIcon( QLatin1String("view-fullscreen") ) );
d->fullView->setToolTip( i18nc( "@info:tooltip",
"Display calendar in a full window" ) );
}
preferences()->setFullViewMonth( fullView );
preferences()->writeConfig();
emit fullViewChanged( fullView );
}
void MonthView::moveBackMonth()
{
d->moveStartDate( 0, -1 );
}
void MonthView::moveBackWeek()
{
d->moveStartDate( -1, 0 );
}
void MonthView::moveFwdWeek()
{
d->moveStartDate( 1, 0 );
}
void MonthView::moveFwdMonth()
{
d->moveStartDate( 0, 1 );
}
void MonthView::showDates( const QDate &start, const QDate &end, const QDate &preferedMonth )
{
Q_UNUSED( start );
Q_UNUSED( end );
Q_UNUSED( preferedMonth );
d->triggerDelayedReload( DatesChanged );
}
QPair<KDateTime,KDateTime> MonthView::actualDateRange( const KDateTime &start,
const KDateTime &,
const QDate &preferredMonth ) const
{
KDateTime dayOne = preferredMonth.isValid() ? KDateTime( preferredMonth ) : start;
dayOne.setDate( QDate( dayOne.date().year(), dayOne.date().month(), 1 ) );
const int weekdayCol = ( dayOne.date().dayOfWeek() + 7 - KGlobal::locale()->weekStartDay() ) % 7;
KDateTime actualStart = dayOne.addDays( -weekdayCol );
actualStart.setTime( QTime( 0, 0, 0, 0 ) );
KDateTime actualEnd = actualStart.addDays( 6 * 7 - 1 );
actualEnd.setTime( QTime( 23, 59, 59, 99 ) );
return qMakePair( actualStart, actualEnd );
}
Akonadi::Item::List MonthView::selectedIncidences() const
{
Akonadi::Item::List selected;
if ( d->scene->selectedItem() ) {
IncidenceMonthItem *tmp = qobject_cast<IncidenceMonthItem *>( d->scene->selectedItem() );
if ( tmp ) {
Akonadi::Item incidenceSelected = tmp->akonadiItem();
if ( incidenceSelected.isValid() ) {
selected.append( incidenceSelected );
}
}
}
return selected;
}
void MonthView::reloadIncidences()
{
if ( changes() == NothingChanged ) {
return;
}
// keep selection if it exists
Akonadi::Item incidenceSelected;
MonthItem *itemToReselect = 0;
if ( IncidenceMonthItem *tmp = qobject_cast<IncidenceMonthItem *>( d->scene->selectedItem() ) ) {
d->selectedItemId = tmp->akonadiItem().id();
d->selectedItemDate = tmp->realStartDate();
if ( !d->selectedItemDate.isValid() ) {
return;
}
}
d->scene->resetAll();
d->mBusyDays.clear();
// build monthcells hash
int i = 0;
for ( QDate date = actualStartDateTime().date();
date <= actualEndDateTime().date(); date = date.addDays( 1 ) ) {
d->scene->mMonthCellMap[ date ] = new MonthCell( i, date, d->scene );
i ++;
}
// build global event list
KDateTime::Spec timeSpec = CalendarSupport::KCalPrefs::instance()->timeSpec();
const bool colorMonthBusyDays = preferences()->colorMonthBusyDays();
KCalCore::OccurrenceIterator occurIter( *calendar(), actualStartDateTime(), actualEndDateTime() );
while ( occurIter.hasNext() ) {
occurIter.next();
// Remove the two checks when filtering is done through a proxyModel, when using calendar search
if ( !preferences()->showTodosMonthView() &&
occurIter.incidence()->type() == KCalCore::Incidence::TypeTodo ) {
continue;
}
if ( !preferences()->showJournalsMonthView() &&
occurIter.incidence()->type() == KCalCore::Incidence::TypeJournal ) {
continue;
}
const bool busyDay = colorMonthBusyDays && makesWholeDayBusy( occurIter.incidence() );
if ( busyDay ) {
QStringList &list = d->mBusyDays[occurIter.occurrenceStartDate().date()];
list.append( occurIter.incidence()->uid() );
}
const Akonadi::Item item = calendar()->item( occurIter.incidence() );
if ( !item.isValid() ) {
continue;
}
Q_ASSERT(item.isValid());
Q_ASSERT(item.hasPayload());
MonthItem *manager = new IncidenceMonthItem( d->scene,
calendar(),
item,
occurIter.incidence(),
occurIter.occurrenceStartDate().toTimeSpec( timeSpec ).date() );
d->scene->mManagerList << manager;
if ( d->selectedItemId == item.id() &&
manager->realStartDate() == d->selectedItemDate ) {
// only select it outside the loop because we are still creating items
itemToReselect = manager;
}
}
if ( itemToReselect ) {
d->scene->selectItem( itemToReselect );
}
// add holidays
const QList<QDate> workDays = CalendarSupport::workDays( actualStartDateTime().date(),
actualEndDateTime().date() );
for ( QDate date = actualStartDateTime().date();
date <= actualEndDateTime().date(); date = date.addDays( 1 ) ) {
// Only call CalendarSupport::holiday() if it's not a workDay, saves come cpu cicles.
if ( !workDays.contains( date ) ) {
QStringList holidays( CalendarSupport::holiday( date ) );
if ( !holidays.isEmpty() ) {
MonthItem *holidayItem =
new HolidayMonthItem(
d->scene, date,
holidays.join( i18nc( "@item:intext delimiter for joining holiday names", "," ) ) );
d->scene->mManagerList << holidayItem;
}
}
}
// sort it
qSort( d->scene->mManagerList.begin(),
d->scene->mManagerList.end(),
MonthItem::greaterThan );
// build each month's cell event list
foreach ( MonthItem *manager, d->scene->mManagerList ) {
for ( QDate date = manager->startDate();
date <= manager->endDate(); date = date.addDays( 1 ) ) {
MonthCell *cell = d->scene->mMonthCellMap.value( date );
if ( cell ) {
cell->mMonthItemList << manager;
}
}
}
foreach ( MonthItem *manager, d->scene->mManagerList ) {
manager->updateMonthGraphicsItems();
manager->updatePosition();
}
foreach ( MonthItem *manager, d->scene->mManagerList ) {
manager->updateGeometry();
}
d->scene->setInitialized( true );
d->view->update();
d->scene->update();
}
void MonthView::calendarReset()
{
kDebug();
d->triggerDelayedReload( ResourcesChanged );
}
QDate MonthView::averageDate() const
{
return actualStartDateTime().date().addDays(
actualStartDateTime().date().daysTo( actualEndDateTime().date() ) / 2 );
}
int MonthView::currentMonth() const
{
return averageDate().month();
}
bool MonthView::usesFullWindow()
{
return preferences()->fullViewMonth();
}
bool MonthView::isBusyDay( const QDate &day ) const
{
return !d->mBusyDays[day].isEmpty();
}
void MonthView::setCalendar( const Akonadi::ETMCalendar::Ptr &cal )
{
Q_ASSERT( cal );
if ( calendar() ) {
calendar()->unregisterObserver( d );
}
EventView::setCalendar( cal );
calendar()->registerObserver( d );
}