/* Copyright (c) 2008 Bruno Virlet 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 #include #include #include #include #include #include #include #include #include #include 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 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( 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 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( 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( 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 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 ); }