diff --git a/calendarviews/agenda/agenda.cpp b/calendarviews/agenda/agenda.cpp index 03dec7d3ce..a9f8ae6a9c 100644 --- a/calendarviews/agenda/agenda.cpp +++ b/calendarviews/agenda/agenda.cpp @@ -1,2334 +1,2339 @@ /* Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com Author: Sergio Martins, sergio@kdab.com Marcus Bains line. Copyright (c) 2001 Ali Rahimi 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 "agenda.h" #include "agendaitem.h" #include "agendaview.h" #include "viewcalendar.h" #include "prefs.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // for fabs() using namespace EventViews; /////////////////////////////////////////////////////////////////////////////// class MarcusBains::Private { public: Private( EventView *eventView, Agenda *agenda ) : mEventView( eventView ), mAgenda( agenda ), mTimer( 0 ), mTimeBox( 0 ), mOldTodayCol( -1 ) { } int todayColumn() const; public: EventView *mEventView; Agenda *mAgenda; QTimer *mTimer; QLabel *mTimeBox; // Label showing the current time KDateTime mOldDateTime; int mOldTodayCol; }; int MarcusBains::Private::todayColumn() const { const QDate currentDate = QDate::currentDate(); int col = 0; const KCalCore::DateList dateList = mAgenda->dateList(); foreach ( const QDate &date, dateList ) { if ( date == currentDate ) { return QApplication::isRightToLeft() ? mAgenda->columns() - 1 - col : col; } ++col; } return -1; } MarcusBains::MarcusBains( EventView *eventView, Agenda *agenda ) : QFrame( agenda ), d( new Private( eventView, agenda ) ) { d->mTimeBox = new QLabel( d->mAgenda ); d->mTimeBox->setAlignment( Qt::AlignRight | Qt::AlignBottom ); d->mTimer = new QTimer( this ); d->mTimer->setSingleShot( true ); connect( d->mTimer, SIGNAL(timeout()), this, SLOT(updateLocation()) ); d->mTimer->start( 0 ); } MarcusBains::~MarcusBains() { delete d; } void MarcusBains::updateLocation() { updateLocationRecalc(); } void MarcusBains::updateLocationRecalc( bool recalculate ) { const bool showSeconds = d->mEventView->preferences()->marcusBainsShowSeconds(); const QColor color = d->mEventView->preferences()->agendaMarcusBainsLineLineColor(); const KDateTime now = KDateTime::currentLocalDateTime(); const QTime time = now.time(); if (now.date() != d->mOldDateTime.date()) { recalculate = true; // New day } const int todayCol = recalculate ? d->todayColumn() : d->mOldTodayCol; // Number of minutes since beginning of the day const int minutes = time.hour() * 60 + time.minute(); const int minutesPerCell = 24 * 60 / d->mAgenda->rows(); d->mOldDateTime = now; d->mOldTodayCol = todayCol; int y = int( minutes * d->mAgenda->gridSpacingY() / minutesPerCell ); int x = int( d->mAgenda->gridSpacingX() * todayCol ); bool hideIt = !( d->mEventView->preferences()->marcusBainsEnabled() ); if ( !isHidden() && ( hideIt || ( todayCol < 0 ) ) ) { hide(); d->mTimeBox->hide(); return; } if ( isHidden() && !hideIt ) { show(); d->mTimeBox->show(); } /* Line */ // It seems logical to adjust the line width with the label's font weight const int fw = d->mEventView->preferences()->agendaMarcusBainsLineFont().weight(); setLineWidth( 1 + abs( fw - QFont::Normal ) / QFont::Light ); setFrameStyle( QFrame::HLine | QFrame::Plain ); QPalette pal = palette(); pal.setColor( QPalette::Window, color ); // for Oxygen pal.setColor( QPalette::WindowText, color ); // for Plastique setPalette( pal ); if ( recalculate ) { setFixedSize( int( d->mAgenda->gridSpacingX() ), 1 ); } move( x, y ); raise(); /* Label */ d->mTimeBox->setFont( d->mEventView->preferences()->agendaMarcusBainsLineFont() ); QPalette pal1 = d->mTimeBox->palette(); pal1.setColor( QPalette::WindowText, color ); d->mTimeBox->setPalette( pal1 ); d->mTimeBox->setText( KGlobal::locale()->formatTime( time, showSeconds ) ); d->mTimeBox->adjustSize(); if ( y - d->mTimeBox->height() >= 0 ) { y -= d->mTimeBox->height(); } else { y++; } if ( x - d->mTimeBox->width() + d->mAgenda->gridSpacingX() > 0 ) { x += int( d->mAgenda->gridSpacingX() - d->mTimeBox->width() - 1 ); } else { x++; } d->mTimeBox->move( x, y ); d->mTimeBox->raise(); if ( showSeconds || recalculate ) { d->mTimer->start( 1000 ); } else { d->mTimer->start( 1000 * ( 60 - time.second() ) ); } } //////////////////////////////////////////////////////////////////////////// class Agenda::Private { public: Private( AgendaView *agendaView, QScrollArea *scrollArea, int columns, int rows, int rowSize, bool isInteractive ) : mAgendaView( agendaView ), mScrollArea( scrollArea ), mAllDayMode( false ), mColumns( columns ), mRows( rows ), mGridSpacingX( 0.0 ), mGridSpacingY( rowSize ), mDesiredGridSpacingY( rowSize ), mChanger( 0 ), mResizeBorderWidth( 0 ), mScrollBorderWidth( 0 ), mScrollDelay( 0 ), mScrollOffset( 0 ), mWorkingHoursEnable( false ), mHolidayMask( 0 ), mWorkingHoursYTop( 0 ), mWorkingHoursYBottom( 0 ), mHasSelection( 0 ), mSelectedId( -1 ), mMarcusBains( 0 ), mActionType( Agenda::NOP ), mItemMoved( false ), mOldLowerScrollValue( 0 ), mOldUpperScrollValue( 0 ), mReturnPressed( false ), mIsInteractive( isInteractive ) { if ( mGridSpacingY < 4 || mGridSpacingY > 30 ) { mGridSpacingY = 10; } } public: PrefsPtr preferences() const { return mAgendaView->preferences(); } bool isQueuedForDeletion( const QString &uid ) const { // if mAgendaItemsById contains it it means that a createAgendaItem() was called // before the previous agenda items were deleted. return mItemsQueuedForDeletion.contains( uid ) && !mAgendaItemsById.contains( uid ); } QMultiHash mAgendaItemsById; // It's a QMultiHash because recurring incidences might have many agenda items QSet mItemsQueuedForDeletion; AgendaView *mAgendaView; QScrollArea *mScrollArea; bool mAllDayMode; // Number of Columns/Rows of agenda grid int mColumns; int mRows; // Width and height of agenda cells. mDesiredGridSpacingY is the height // set in the config. The actual height might be larger since otherwise // more than 24 hours might be displayed. double mGridSpacingX; double mGridSpacingY; double mDesiredGridSpacingY; Akonadi::IncidenceChanger *mChanger; // size of border, where mouse action will resize the AgendaItem int mResizeBorderWidth; // size of border, where mouse mve will cause a scroll of the agenda int mScrollBorderWidth; int mScrollDelay; int mScrollOffset; QTimer mScrollUpTimer; QTimer mScrollDownTimer; // Cells to store Move and Resize coordiantes while performing the action QPoint mStartCell; QPoint mEndCell; // Working Hour coordiantes bool mWorkingHoursEnable; QVector *mHolidayMask; int mWorkingHoursYTop; int mWorkingHoursYBottom; // Selection bool mHasSelection; QPoint mSelectionStartPoint; QPoint mSelectionStartCell; QPoint mSelectionEndCell; // List of dates to be displayed KCalCore::DateList mSelectedDates; // The AgendaItem, which has been right-clicked last QPointer mClickedItem; // The AgendaItem, which is being moved/resized QPointer mActionItem; // Currently selected item QPointer mSelectedItem; // Uid of the last selected incidence. Used for reselecting in situations // where the selected item points to a no longer valid incidence, for // example during resource reload. QString mSelectedId; // The Marcus Bains Line widget. MarcusBains *mMarcusBains; MouseActionType mActionType; bool mItemMoved; // List of all Items contained in agenda QList mItems; QList mItemsToDelete; int mOldLowerScrollValue; int mOldUpperScrollValue; bool mReturnPressed; bool mIsInteractive; MultiViewCalendar::Ptr mCalendar; }; /* Create an agenda widget with rows rows and columns columns. */ Agenda::Agenda( AgendaView *agendaView, QScrollArea *scrollArea, int columns, int rows, int rowSize, bool isInteractive ) : QWidget( scrollArea ), d( new Private( agendaView, scrollArea, columns, rows, rowSize, isInteractive ) ) { setMouseTracking( true ); init(); } /* Create an agenda widget with columns columns and one row. This is used for all-day events. */ Agenda::Agenda( AgendaView *agendaView, QScrollArea *scrollArea, int columns, bool isInteractive ) : QWidget( scrollArea ), d( new Private( agendaView, scrollArea, columns, 1, 24, isInteractive ) ) { d->mAllDayMode = true; init(); } Agenda::~Agenda() { delete d->mMarcusBains; delete d; } KCalCore::Incidence::Ptr Agenda::selectedIncidence() const { return d->mSelectedItem ? d->mSelectedItem->incidence() : KCalCore::Incidence::Ptr(); } QDate Agenda::selectedIncidenceDate() const { return d->mSelectedItem ? d->mSelectedItem->occurrenceDate() : QDate(); } QString Agenda::lastSelectedItemId() const { return d->mSelectedId; } void Agenda::init() { setAttribute( Qt::WA_OpaquePaintEvent ); d->mGridSpacingX = static_cast( d->mScrollArea->width() ) / d->mColumns; d->mDesiredGridSpacingY = d->preferences()->hourSize(); if ( d->mDesiredGridSpacingY < 4 || d->mDesiredGridSpacingY > 30 ) { d->mDesiredGridSpacingY = 10; } // make sure that there are not more than 24 per day d->mGridSpacingY = static_cast( height() ) / d->mRows; if ( d->mGridSpacingY < d->mDesiredGridSpacingY ) { d->mGridSpacingY = d->mDesiredGridSpacingY; } d->mResizeBorderWidth = 12; d->mScrollBorderWidth = 12; d->mScrollDelay = 30; d->mScrollOffset = 10; // Grab key strokes for keyboard navigation of agenda. Seems to have no // effect. Has to be fixed. setFocusPolicy( Qt::WheelFocus ); connect( &d->mScrollUpTimer, SIGNAL(timeout()), SLOT(scrollUp()) ); connect( &d->mScrollDownTimer, SIGNAL(timeout()), SLOT(scrollDown()) ); d->mStartCell = QPoint( 0, 0 ); d->mEndCell = QPoint( 0, 0 ); d->mHasSelection = false; d->mSelectionStartPoint = QPoint( 0, 0 ); d->mSelectionStartCell = QPoint( 0, 0 ); d->mSelectionEndCell = QPoint( 0, 0 ); d->mOldLowerScrollValue = -1; d->mOldUpperScrollValue = -1; d->mClickedItem = 0; d->mActionItem = 0; d->mActionType = NOP; d->mItemMoved = false; d->mSelectedItem = 0; d->mSelectedId = -1; setAcceptDrops( true ); installEventFilter( this ); /* resizeContents( int( mGridSpacingX * mColumns ), int( mGridSpacingY * mRows ) ); */ d->mScrollArea->viewport()->update(); // mScrollArea->viewport()->setAttribute( Qt::WA_NoSystemBackground, true ); d->mScrollArea->viewport()->setFocusPolicy( Qt::WheelFocus ); calculateWorkingHours(); connect( verticalScrollBar(), SIGNAL(valueChanged(int)), SLOT(checkScrollBoundaries(int)) ); // Create the Marcus Bains line. if( d->mAllDayMode ) { d->mMarcusBains = 0; } else { d->mMarcusBains = new MarcusBains( d->mAgendaView, this ); } } void Agenda::clear() { qDeleteAll( d->mItems ); qDeleteAll( d->mItemsToDelete ); d->mItems.clear(); d->mItemsToDelete.clear(); d->mAgendaItemsById.clear(); d->mItemsQueuedForDeletion.clear(); d->mSelectedItem = 0; clearSelection(); } void Agenda::clearSelection() { d->mHasSelection = false; d->mActionType = NOP; update(); } void Agenda::marcus_bains() { if ( d->mMarcusBains ) { d->mMarcusBains->updateLocationRecalc( true ); } } void Agenda::changeColumns( int columns ) { if ( columns == 0 ) { kDebug() << "called with argument 0"; return; } clear(); d->mColumns = columns; // setMinimumSize( mColumns * 10, mGridSpacingY + 1 ); // init(); // update(); QResizeEvent event( size(), size() ); QApplication::sendEvent( this, &event ); } int Agenda::columns() const { return d->mColumns; } int Agenda::rows() const { return d->mRows; } double Agenda::gridSpacingX() const { return d->mGridSpacingX; } double Agenda::gridSpacingY() const { return d->mGridSpacingY; } /* This is the eventFilter function, which gets all events from the AgendaItems contained in the agenda. It has to handle moving and resizing for all items. */ bool Agenda::eventFilter ( QObject *object, QEvent *event ) { switch( event->type() ) { case QEvent::MouseButtonPress: case QEvent::MouseButtonDblClick: case QEvent::MouseButtonRelease: case QEvent::MouseMove: return eventFilter_mouse( object, static_cast( event ) ); #ifndef QT_NO_WHEELEVENT case QEvent::Wheel: return eventFilter_wheel( object, static_cast( event ) ); #endif case QEvent::KeyPress: case QEvent::KeyRelease: return eventFilter_key( object, static_cast( event ) ); case ( QEvent::Leave ): #ifndef QT_NO_CURSOR if ( !d->mActionItem ) { setCursor( Qt::ArrowCursor ); } #endif if ( object == this ) { // so timelabels hide the mouse cursor emit leaveAgenda(); } return true; case QEvent::Enter: emit enterAgenda(); return QWidget::eventFilter( object, event ); #ifndef QT_NO_DRAGANDDROP #ifndef KORG_NODND case QEvent::DragEnter: case QEvent::DragMove: case QEvent::DragLeave: case QEvent::Drop: // case QEvent::DragResponse: return eventFilter_drag( object, static_cast( event ) ); #endif #endif default: return QWidget::eventFilter( object, event ); } } bool Agenda::eventFilter_drag( QObject *obj, QDropEvent *de ) { #ifndef QT_NO_DRAGANDDROP const QMimeData *md = de->mimeData(); switch ( de->type() ) { case QEvent::DragEnter: case QEvent::DragMove: if ( !CalendarSupport::canDecode( md ) ) { return false; } if ( CalendarSupport::mimeDataHasIncidence( md ) ) { de->accept(); } else { de->ignore(); } return true; break; case QEvent::DragLeave: return false; break; case QEvent::Drop: { if ( !CalendarSupport::canDecode( md ) ) { return false; } const QList incidenceUrls = CalendarSupport::incidenceItemUrls( md ); const KCalCore::Incidence::List incidences = CalendarSupport::incidences( md, d->mCalendar->mETMCalendar->timeSpec() ); Q_ASSERT( !incidenceUrls.isEmpty() || !incidences.isEmpty() ); de->setDropAction( Qt::MoveAction ); QWidget *dropTarget = qobject_cast( obj ); QPoint dropPosition = de->pos(); if ( dropTarget && dropTarget != this ) { dropPosition = dropTarget->mapTo( this, dropPosition ); } const QPoint gridPosition = contentsToGrid( dropPosition ); if ( !incidenceUrls.isEmpty() ) { emit droppedIncidences( incidenceUrls, gridPosition, d->mAllDayMode ); } else { emit droppedIncidences( incidences, gridPosition, d->mAllDayMode ); } return true; } break; case QEvent::DragResponse: default: break; } #endif return false; } #ifndef QT_NO_WHEELEVENT bool Agenda::eventFilter_wheel ( QObject *object, QWheelEvent *e ) { QPoint viewportPos; bool accepted=false; if ( ( e->modifiers() & Qt::ShiftModifier ) == Qt::ShiftModifier ) { if ( object != this ) { viewportPos = ( (QWidget *) object )->mapToParent( e->pos() ); } else { viewportPos = e->pos(); } //kDebug() << type:" << e->type() << "delta:" << e->delta(); emit zoomView( -e->delta(), contentsToGrid( viewportPos ), Qt::Horizontal ); accepted = true; } if ( ( e->modifiers() & Qt::ControlModifier ) == Qt::ControlModifier ){ if ( object != this ) { viewportPos = ( (QWidget *)object )->mapToParent( e->pos() ); } else { viewportPos = e->pos(); } emit zoomView( -e->delta(), contentsToGrid( viewportPos ), Qt::Vertical ); emit mousePosSignal( gridToContents( contentsToGrid( viewportPos ) ) ); accepted = true; } if ( accepted ) { e->accept(); } return accepted; } #endif bool Agenda::eventFilter_key( QObject *, QKeyEvent *ke ) { return d->mAgendaView->processKeyEvent( ke ); } bool Agenda::eventFilter_mouse( QObject *object, QMouseEvent *me ) { QPoint viewportPos; if ( object != this ) { viewportPos = static_cast( object )->mapToParent( me->pos() ); } else { viewportPos = me->pos(); } switch ( me->type() ) { case QEvent::MouseButtonPress: if ( object != this ) { if ( me->button() == Qt::RightButton ) { d->mClickedItem = dynamic_cast( object ); if ( d->mClickedItem ) { selectItem( d->mClickedItem ); emit showIncidencePopupSignal( d->mClickedItem->incidence(), d->mClickedItem->occurrenceDate() ); } } else { AgendaItem::QPtr item = dynamic_cast(object); if (item) { KCalCore::Incidence::Ptr incidence = item->incidence(); if ( incidence->isReadOnly() ) { d->mActionItem = 0; } else { d->mActionItem = item; startItemAction( viewportPos ); } // Warning: do selectItem() as late as possible, since all // sorts of things happen during this call. Some can lead to // this filter being run again and mActionItem being set to // null. selectItem( item ); } } } else { if ( me->button() == Qt::RightButton ) { // if mouse pointer is not in selection, select the cell below the cursor QPoint gpos = contentsToGrid( viewportPos ); if ( !ptInSelection( gpos ) ) { d->mSelectionStartCell = gpos; d->mSelectionEndCell = gpos; d->mHasSelection = true; emit newStartSelectSignal(); emit newTimeSpanSignal( d->mSelectionStartCell, d->mSelectionEndCell ); // updateContents(); } showNewEventPopupSignal(); } else { selectItem( 0 ); d->mActionItem = 0; #ifndef QT_NO_CURSOR setCursor( Qt::ArrowCursor ); #endif startSelectAction( viewportPos ); update(); } } break; case QEvent::MouseButtonRelease: if ( d->mActionItem ) { endItemAction(); } else if ( d->mActionType == SELECT ) { endSelectAction( viewportPos ); } // This nasty gridToContents(contentsToGrid(..)) is needed to // avoid an offset of a few pixels. Don't ask me why... emit mousePosSignal( gridToContents( contentsToGrid( viewportPos ) ) ); break; case QEvent::MouseMove: { if ( !d->mIsInteractive ) { return true; } // This nasty gridToContents(contentsToGrid(..)) is needed todos // avoid an offset of a few pixels. Don't ask me why... QPoint indicatorPos = gridToContents( contentsToGrid( viewportPos ) ); if ( object != this ) { AgendaItem::QPtr moveItem = dynamic_cast( object ); KCalCore::Incidence::Ptr incidence = moveItem ? moveItem->incidence() : KCalCore::Incidence::Ptr(); if ( incidence && !incidence->isReadOnly() ) { if ( !d->mActionItem ) { setNoActionCursor( moveItem, viewportPos ); } else { performItemAction( viewportPos ); if ( d->mActionType == MOVE ) { // show cursor at the current begin of the item AgendaItem::QPtr firstItem = d->mActionItem->firstMultiItem(); if ( !firstItem ) { firstItem = d->mActionItem; } indicatorPos = gridToContents( QPoint( firstItem->cellXLeft(), firstItem->cellYTop() ) ); } else if ( d->mActionType == RESIZEBOTTOM ) { // RESIZETOP is handled correctly, only resizebottom works differently indicatorPos = gridToContents( QPoint( d->mActionItem->cellXLeft(), d->mActionItem->cellYBottom() + 1 ) ); } } // If we have an action item } // If move item && !read only } else { if ( d->mActionType == SELECT ) { performSelectAction( viewportPos ); // show cursor at end of timespan if ( ( ( d->mStartCell.y() < d->mEndCell.y() ) && ( d->mEndCell.x() >= d->mStartCell.x() ) ) || ( d->mEndCell.x() > d->mStartCell.x() ) ) { indicatorPos = gridToContents( QPoint( d->mEndCell.x(), d->mEndCell.y() + 1 ) ); } else { indicatorPos = gridToContents( d->mEndCell ); } } } emit mousePosSignal( indicatorPos ); break; } case QEvent::MouseButtonDblClick: if ( object == this ) { selectItem( 0 ); emit newEventSignal(); } else { AgendaItem::QPtr doubleClickedItem = dynamic_cast( object ); if ( doubleClickedItem ) { selectItem( doubleClickedItem ); emit editIncidenceSignal( doubleClickedItem->incidence() ); } } break; default: break; } return true; } bool Agenda::ptInSelection( const QPoint &gpos ) const { if ( !d->mHasSelection ) { return false; } else if ( gpos.x() < d->mSelectionStartCell.x() || gpos.x() > d->mSelectionEndCell.x() ) { return false; } else if ( ( gpos.x() == d->mSelectionStartCell.x() ) && ( gpos.y() < d->mSelectionStartCell.y() ) ) { return false; } else if ( ( gpos.x() == d->mSelectionEndCell.x() ) && ( gpos.y() > d->mSelectionEndCell.y() ) ) { return false; } return true; } void Agenda::startSelectAction( const QPoint &viewportPos ) { emit newStartSelectSignal(); d->mActionType = SELECT; d->mSelectionStartPoint = viewportPos; d->mHasSelection = true; QPoint pos = viewportPos ; QPoint gpos = contentsToGrid( pos ); // Store new selection d->mStartCell = gpos; d->mEndCell = gpos; d->mSelectionStartCell = gpos; d->mSelectionEndCell = gpos; // updateContents(); } void Agenda::performSelectAction( const QPoint &pos ) { const QPoint gpos = contentsToGrid( pos ); // Scroll if cursor was moved to upper or lower end of agenda. if ( pos.y() - contentsY() < d->mScrollBorderWidth && contentsY() > 0 ) { d->mScrollUpTimer.start( d->mScrollDelay ); } else if ( contentsY() + d->mScrollArea->viewport()->height() - d->mScrollBorderWidth < pos.y() ) { d->mScrollDownTimer.start( d->mScrollDelay ); } else { d->mScrollUpTimer.stop(); d->mScrollDownTimer.stop(); } if ( gpos != d->mEndCell ) { d->mEndCell = gpos; if ( d->mStartCell.x() > d->mEndCell.x() || ( d->mStartCell.x() == d->mEndCell.x() && d->mStartCell.y() > d->mEndCell.y() ) ) { // backward selection d->mSelectionStartCell = d->mEndCell; d->mSelectionEndCell = d->mStartCell; } else { d->mSelectionStartCell = d->mStartCell; d->mSelectionEndCell = d->mEndCell; } update(); } } void Agenda::endSelectAction( const QPoint ¤tPos ) { d->mScrollUpTimer.stop(); d->mScrollDownTimer.stop(); d->mActionType = NOP; emit newTimeSpanSignal( d->mSelectionStartCell, d->mSelectionEndCell ); if ( d->preferences()->selectionStartsEditor() ) { if ( ( d->mSelectionStartPoint - currentPos ).manhattanLength() > QApplication::startDragDistance() ) { emit newEventSignal(); } } } Agenda::MouseActionType Agenda::isInResizeArea( bool horizontal, const QPoint &pos, AgendaItem::QPtr item ) { if ( !item ) { return NOP; } QPoint gridpos = contentsToGrid( pos ); QPoint contpos = gridToContents( gridpos + QPoint( ( QApplication::isRightToLeft() ) ? 1 : 0, 0 ) ); if ( horizontal ) { int clXLeft = item->cellXLeft(); int clXRight = item->cellXRight(); if ( QApplication::isRightToLeft() ) { int tmp = clXLeft; clXLeft = clXRight; clXRight = tmp; } int gridDistanceX = int( pos.x() - contpos.x() ); if ( gridDistanceX < d->mResizeBorderWidth && clXLeft == gridpos.x() ) { if ( QApplication::isRightToLeft() ) { return RESIZERIGHT; } else { return RESIZELEFT; } } else if ( ( d->mGridSpacingX - gridDistanceX ) < d->mResizeBorderWidth && clXRight == gridpos.x() ) { if ( QApplication::isRightToLeft() ) { return RESIZELEFT; } else { return RESIZERIGHT; } } else { return MOVE; } } else { int gridDistanceY = int( pos.y() - contpos.y() ); if ( gridDistanceY < d->mResizeBorderWidth && item->cellYTop() == gridpos.y() && !item->firstMultiItem() ) { return RESIZETOP; } else if ( ( d->mGridSpacingY - gridDistanceY ) < d->mResizeBorderWidth && item->cellYBottom() == gridpos.y() && !item->lastMultiItem() ) { return RESIZEBOTTOM; } else { return MOVE; } } } void Agenda::startItemAction( const QPoint &pos ) { Q_ASSERT( d->mActionItem ); d->mStartCell = contentsToGrid( pos ); d->mEndCell = d->mStartCell; bool noResize = CalendarSupport::hasTodo( d->mActionItem->incidence() ); d->mActionType = MOVE; if ( !noResize ) { d->mActionType = isInResizeArea( d->mAllDayMode, pos, d->mActionItem ); } d->mActionItem->startMove(); setActionCursor( d->mActionType, true ); } void Agenda::performItemAction( const QPoint &pos ) { QPoint gpos = contentsToGrid( pos ); // Cursor left active agenda area. // This starts a drag. if ( pos.y() < 0 || pos.y() >= contentsY() + d->mScrollArea->viewport()->height() || pos.x() < 0 || pos.x() >= width() ) { if ( d->mActionType == MOVE ) { d->mScrollUpTimer.stop(); d->mScrollDownTimer.stop(); d->mActionItem->resetMove(); placeSubCells( d->mActionItem ); emit startDragSignal( d->mActionItem->incidence() ); #ifndef QT_NO_CURSOR setCursor( Qt::ArrowCursor ); #endif if ( d->mChanger ) { // d->mChanger->cancelChange( d->mActionItem->incidence() ); } d->mActionItem = 0; d->mActionType = NOP; d->mItemMoved = false; return; } } else { setActionCursor( d->mActionType, true ); } // Scroll if item was moved to upper or lower end of agenda. const int distanceToTop = pos.y() - contentsY(); if ( distanceToTop < d->mScrollBorderWidth && distanceToTop > -d->mScrollBorderWidth ) { d->mScrollUpTimer.start( d->mScrollDelay ); } else if ( contentsY() + d->mScrollArea->viewport()->height() - d->mScrollBorderWidth < pos.y() ) { d->mScrollDownTimer.start( d->mScrollDelay ); } else { d->mScrollUpTimer.stop(); d->mScrollDownTimer.stop(); } // Move or resize item if necessary if ( d->mEndCell != gpos ) { if ( !d->mItemMoved ) { if ( !d->mChanger ) { KMessageBox::information( this, i18n( "Unable to lock item for modification. " "You cannot make any changes." ), i18n( "Locking Failed" ), QLatin1String("AgendaLockingFailed") ); d->mScrollUpTimer.stop(); d->mScrollDownTimer.stop(); d->mActionItem->resetMove(); placeSubCells( d->mActionItem ); #ifndef QT_NO_CURSOR setCursor( Qt::ArrowCursor ); #endif d->mActionItem = 0; d->mActionType = NOP; d->mItemMoved = false; return; } d->mItemMoved = true; } d->mActionItem->raise(); if ( d->mActionType == MOVE ) { // Move all items belonging to a multi item AgendaItem::QPtr firstItem = d->mActionItem->firstMultiItem(); if ( !firstItem ) { firstItem = d->mActionItem; } AgendaItem::QPtr lastItem = d->mActionItem->lastMultiItem(); if ( !lastItem ) { lastItem = d->mActionItem; } QPoint deltapos = gpos - d->mEndCell; AgendaItem::QPtr moveItem = firstItem; while ( moveItem ) { bool changed = false; if ( deltapos.x() != 0 ) { moveItem->moveRelative( deltapos.x(), 0 ); changed = true; } // in all day view don't try to move multi items, since there are none if ( moveItem == firstItem && !d->mAllDayMode ) { // is the first item int newY = deltapos.y() + moveItem->cellYTop(); // If event start moved earlier than 0:00, it starts the previous day if ( newY < 0 && newY > d->mScrollBorderWidth ) { moveItem->expandTop( -moveItem->cellYTop() ); // prepend a new item at ( x-1, rows()+newY to rows() ) AgendaItem::QPtr newFirst = firstItem->prevMoveItem(); // cell's y values are first and last cell of the bar, // so if newY=-1, they need to be the same if ( newFirst ) { newFirst->setCellXY( moveItem->cellXLeft() - 1, rows() + newY, rows() - 1 ); d->mItems.append( newFirst ); moveItem->resize( int( d->mGridSpacingX * newFirst->cellWidth() ), int( d->mGridSpacingY * newFirst->cellHeight() ) ); QPoint cpos = gridToContents( QPoint( newFirst->cellXLeft(), newFirst->cellYTop() ) ); newFirst->setParent( this ); newFirst->move( cpos.x(), cpos.y() ); } else { newFirst = insertItem( moveItem->incidence(), moveItem->occurrenceDateTime(), moveItem->cellXLeft() - 1, rows() + newY, rows() - 1, moveItem->itemPos(), moveItem->itemCount(), false ) ; } if ( newFirst ) { newFirst->show(); } moveItem->prependMoveItem( newFirst ); firstItem = newFirst; } else if ( newY >= rows() ) { // If event start is moved past 24:00, it starts the next day // erase current item (i.e. remove it from the multiItem list) firstItem = moveItem->nextMultiItem(); moveItem->hide(); d->mItems.removeAll( moveItem ); // removeChild( moveItem ); d->mActionItem->removeMoveItem( moveItem ); moveItem=firstItem; // adjust next day's item if ( moveItem ) { moveItem->expandTop( rows() - newY ); } } else { moveItem->expandTop( deltapos.y(), true ); } changed=true; } if ( moveItem && !moveItem->lastMultiItem() && !d->mAllDayMode ) { // is the last item int newY = deltapos.y() + moveItem->cellYBottom(); if ( newY < 0 ) { // erase current item lastItem = moveItem->prevMultiItem(); moveItem->hide(); d->mItems.removeAll( moveItem ); // removeChild( moveItem ); moveItem->removeMoveItem( moveItem ); moveItem = lastItem; moveItem->expandBottom( newY + 1 ); } else if ( newY >= rows() ) { moveItem->expandBottom( rows()-moveItem->cellYBottom() - 1 ); // append item at ( x+1, 0 to newY-rows() ) AgendaItem::QPtr newLast = lastItem->nextMoveItem(); if ( newLast ) { newLast->setCellXY( moveItem->cellXLeft() + 1, 0, newY-rows() - 1 ); d->mItems.append( newLast ); moveItem->resize( int( d->mGridSpacingX * newLast->cellWidth() ), int( d->mGridSpacingY * newLast->cellHeight() ) ); QPoint cpos = gridToContents( QPoint( newLast->cellXLeft(), newLast->cellYTop() ) ) ; newLast->setParent( this ); newLast->move( cpos.x(), cpos.y() ); } else { newLast = insertItem( moveItem->incidence(), moveItem->occurrenceDateTime(), moveItem->cellXLeft() + 1, 0, newY - rows() - 1, moveItem->itemPos(), moveItem->itemCount(), false ) ; } moveItem->appendMoveItem( newLast ); newLast->show(); lastItem = newLast; } else { moveItem->expandBottom( deltapos.y() ); } changed = true; } if ( changed ) { adjustItemPosition( moveItem ); } if ( moveItem ) { moveItem = moveItem->nextMultiItem(); } } } else if ( d->mActionType == RESIZETOP ) { if ( d->mEndCell.y() <= d->mActionItem->cellYBottom() ) { d->mActionItem->expandTop( gpos.y() - d->mEndCell.y() ); adjustItemPosition( d->mActionItem ); } } else if ( d->mActionType == RESIZEBOTTOM ) { if ( d->mEndCell.y() >= d->mActionItem->cellYTop() ) { d->mActionItem->expandBottom( gpos.y() - d->mEndCell.y() ); adjustItemPosition( d->mActionItem ); } } else if ( d->mActionType == RESIZELEFT ) { if ( d->mEndCell.x() <= d->mActionItem->cellXRight() ) { d->mActionItem->expandLeft( gpos.x() - d->mEndCell.x() ); adjustItemPosition( d->mActionItem ); } } else if ( d->mActionType == RESIZERIGHT ) { if ( d->mEndCell.x() >= d->mActionItem->cellXLeft() ) { d->mActionItem->expandRight( gpos.x() - d->mEndCell.x() ); adjustItemPosition( d->mActionItem ); } } d->mEndCell = gpos; } } void Agenda::endItemAction() { //PENDING(AKONADI_PORT) review all this cloning and changer calls d->mActionType = NOP; d->mScrollUpTimer.stop(); d->mScrollDownTimer.stop(); #ifndef QT_NO_CURSOR setCursor( Qt::ArrowCursor ); #endif if ( !d->mChanger ) { kError() << "No IncidenceChanger set"; return; } bool multiModify = false; // FIXME: do the cloning here... KCalCore::Incidence::Ptr incidence = d->mActionItem->incidence(); const KDateTime recurrenceId = d->mActionItem->occurrenceDateTime(); d->mItemMoved = d->mItemMoved && !( d->mStartCell.x() == d->mEndCell.x() && d->mStartCell.y() == d->mEndCell.y() ); bool addIncidence = false; if ( d->mItemMoved ) { bool modify = false; //get the main event and not the exception if (incidence->hasRecurrenceId() && !incidence->recurs()) { KCalCore::Incidence::Ptr mainIncidence; KCalCore::Calendar::Ptr cal = d->mCalendar->findCalendar(incidence)->getCalendar(); if (CalendarSupport::hasEvent(incidence)) { mainIncidence = cal->event(incidence->uid()); } else if (CalendarSupport::hasTodo(incidence)) { mainIncidence = cal->todo(incidence->uid()); } incidence = mainIncidence; } Akonadi::Item item = d->mCalendar->item(incidence); if ( incidence->recurs()) { const int res = d->mAgendaView->showMoveRecurDialog( incidence, recurrenceId.date() ); switch ( res ) { case KCalUtils::RecurrenceActions::AllOccurrences: // All occurrences // Moving the whole sequene of events is handled by the itemModified below. modify = true; break; case KCalUtils::RecurrenceActions::SelectedOccurrence: case KCalUtils::RecurrenceActions::FutureOccurrences: { const bool thisAndFuture = (res == KCalUtils::RecurrenceActions::FutureOccurrences); modify = true; multiModify = true; d->mChanger->startAtomicOperation( i18n( "Dissociate event from recurrence" ) ); KCalCore::Incidence::Ptr newInc( KCalCore::Calendar::createException( incidence, recurrenceId, thisAndFuture ) ); if ( newInc ) { newInc->removeCustomProperty("VOLATILE", "AKONADI-ID"); Akonadi::Item newItem = d->mCalendar->item(newInc); if (newItem.isValid() && newItem != item ) { //it is not a new exception item = newItem; newInc->setCustomProperty("VOLATILE", "AKONADI-ID", QString::number(newItem.id())); addIncidence = false; } else { addIncidence = true; } // don't recreate items, they already have the correct position d->mAgendaView->enableAgendaUpdate( false ); d->mActionItem->setIncidence( newInc ); d->mActionItem->dissociateFromMultiItem(); d->mAgendaView->enableAgendaUpdate( true ); } else { KMessageBox::sorry( this, i18n( "Unable to add the exception item to the calendar. " "No change will be done." ), i18n( "Error Occurred" ) ); } break; } default: modify = false; d->mActionItem->resetMove(); placeSubCells( d->mActionItem ); //PENDING(AKONADI_PORT) should this be done after //the new item was asynchronously added? } } AgendaItem::QPtr placeItem = d->mActionItem->firstMultiItem(); if ( !placeItem ) { placeItem = d->mActionItem; } Akonadi::Collection::Id saveCollection = -1; if ( item.isValid()) { saveCollection = item.parentCollection().id(); // if parent collection is only a search collection for example if (!(item.parentCollection().rights() & Akonadi::Collection::CanCreateItem)) { saveCollection = item.storageCollectionId(); } } if ( modify ) { d->mActionItem->endMove(); AgendaItem::QPtr modif = placeItem; QList oldconflictItems = placeItem->conflictItems(); QList::iterator it; for ( it = oldconflictItems.begin(); it != oldconflictItems.end(); ++it ) { if ( *it ) { placeSubCells( *it ); } } while ( placeItem ) { placeSubCells( placeItem ); placeItem = placeItem->nextMultiItem(); } // Notify about change // The agenda view will apply the changes to the actual Incidence*! // Bug #228696 don't call endChanged now it's async in Akonadi so it can // be called before that modified item was done. And endChange is // calling when we move item. // Not perfect need to improve it! //mChanger->endChange( inc ); if (item.isValid()) { d->mAgendaView->updateEventDates( modif, addIncidence, saveCollection ); } if ( addIncidence ) { // delete the one we dragged, there's a new one being added async, due to dissociation. delete modif; } } else { // the item was moved, but not further modified, since it's not recurring // make sure the view updates anyhow, with the right item if (item.isValid()) { d->mAgendaView->updateEventDates( placeItem, addIncidence, saveCollection ); } } } d->mActionItem = 0; d->mItemMoved = false; if ( multiModify ) { d->mChanger->endAtomicOperation(); } } void Agenda::setActionCursor( int actionType, bool acting ) { #ifndef QT_NO_CURSOR switch ( actionType ) { case MOVE: if ( acting ) { setCursor( Qt::SizeAllCursor ); } else { setCursor( Qt::ArrowCursor ); } break; case RESIZETOP: case RESIZEBOTTOM: setCursor( Qt::SizeVerCursor ); break; case RESIZELEFT: case RESIZERIGHT: setCursor( Qt::SizeHorCursor ); break; default: setCursor( Qt::ArrowCursor ); } #endif } void Agenda::setNoActionCursor( AgendaItem::QPtr moveItem, const QPoint &pos ) { const KCalCore::Incidence::Ptr item = moveItem ? moveItem->incidence() : KCalCore::Incidence::Ptr(); const bool noResize = CalendarSupport::hasTodo( item ); Agenda::MouseActionType resizeType = MOVE; if ( !noResize ) { resizeType = isInResizeArea( d->mAllDayMode, pos, moveItem ); } setActionCursor( resizeType ); } /** calculate the width of the column subcells of the given item */ double Agenda::calcSubCellWidth( AgendaItem::QPtr item ) { QPoint pt, pt1; pt = gridToContents( QPoint( item->cellXLeft(), item->cellYTop() ) ); pt1 = gridToContents( QPoint( item->cellXLeft(), item->cellYTop() ) + QPoint( 1, 1 ) ); pt1 -= pt; int maxSubCells = item->subCells(); double newSubCellWidth; if ( d->mAllDayMode ) { newSubCellWidth = static_cast( pt1.y() ) / maxSubCells; } else { newSubCellWidth = static_cast( pt1.x() ) / maxSubCells; } return newSubCellWidth; } void Agenda::adjustItemPosition( AgendaItem::QPtr item ) { if ( !item ) { return; } item->resize( int( d->mGridSpacingX * item->cellWidth() ), int( d->mGridSpacingY * item->cellHeight() ) ); int clXLeft = item->cellXLeft(); if ( QApplication::isRightToLeft() ) { clXLeft = item->cellXRight() + 1; } QPoint cpos = gridToContents( QPoint( clXLeft, item->cellYTop() ) ); item->move( cpos.x(), cpos.y() ); } void Agenda::placeAgendaItem( AgendaItem::QPtr item, double subCellWidth ) { // "left" upper corner, no subcells yet, RTL layouts have right/left // switched, widths are negative then QPoint pt = gridToContents( QPoint( item->cellXLeft(), item->cellYTop() ) ); // right lower corner QPoint pt1 = gridToContents( QPoint( item->cellXLeft() + item->cellWidth(), item->cellYBottom() + 1 ) ); double subCellPos = item->subCell() * subCellWidth; // we need to add 0.01 to make sure we don't loose one pixed due to numerics // (i.e. if it would be x.9998, we want the integer, not rounded down. double delta = 0.01; if ( subCellWidth < 0 ) { delta = -delta; } int height, width, xpos, ypos; if ( d->mAllDayMode ) { width = pt1.x() - pt.x(); height = int( subCellPos + subCellWidth + delta ) - int( subCellPos ); xpos = pt.x(); ypos = pt.y() + int( subCellPos ); } else { width = int( subCellPos + subCellWidth + delta ) - int( subCellPos ); height = pt1.y() - pt.y(); xpos = pt.x() + int( subCellPos ); ypos = pt.y(); } if ( QApplication::isRightToLeft() ) { // RTL language/layout xpos += width; width = -width; } if ( height < 0 ) { // BTT (bottom-to-top) layout ?!? ypos += height; height = -height; } item->resize( width, height ); item->move( xpos, ypos ); } /* Place item in cell and take care that multiple items using the same cell do not overlap. This method is not yet optimal. It doesn't use the maximum space it can get in all cases. At the moment the method has a bug: When an item is placed only the sub cell widths of the items are changed, which are within the Y region the item to place spans. When the sub cell width change of one of this items affects a cell, where other items are, which do not overlap in Y with the item to place, the display gets corrupted, although the corruption looks quite nice. */ void Agenda::placeSubCells( AgendaItem::QPtr placeItem ) { #if 0 kDebug(); if ( placeItem ) { KCalCore::Incidence::Ptr event = placeItem->incidence(); if ( !event ) { kDebug() << " event is 0"; } else { kDebug() << " event:" << event->summary(); } } else { kDebug() << " placeItem is 0"; } kDebug() << "Agenda::placeSubCells()..."; #endif QList cells; foreach ( CalendarSupport::CellItem *item, d->mItems ) { if ( item ) { cells.append( item ); } } QList items = CalendarSupport::CellItem::placeItem( cells, placeItem ); placeItem->setConflictItems( QList() ); double newSubCellWidth = calcSubCellWidth( placeItem ); QList::iterator it; for ( it = items.begin(); it != items.end(); ++it ) { if ( *it ) { AgendaItem::QPtr item = static_cast( *it ); placeAgendaItem( item, newSubCellWidth ); item->addConflictItem( placeItem ); placeItem->addConflictItem( item ); } } if ( items.isEmpty() ) { placeAgendaItem( placeItem, newSubCellWidth ); } placeItem->update(); } int Agenda::columnWidth( int column ) const { int start = gridToContents( QPoint( column, 0 ) ).x(); if ( QApplication::isRightToLeft() ) { column--; } else { column++; } int end = gridToContents( QPoint( column, 0 ) ).x(); return end - start; } void Agenda::paintEvent( QPaintEvent * ) { QPainter p( this ); drawContents( &p, 0, -y(), d->mGridSpacingX * d->mColumns, d->mGridSpacingY * d->mRows + y() ); } /* Draw grid in the background of the agenda. */ void Agenda::drawContents( QPainter *p, int cx, int cy, int cw, int ch ) { QPixmap db( cw, ch ); db.fill(); // We don't want to see leftovers from previous paints QPainter dbp( &db ); // TODO: CHECK THIS // if ( ! d->preferences()->agendaGridBackgroundImage().isEmpty() ) { // QPixmap bgImage( d->preferences()->agendaGridBackgroundImage() ); // dbp.drawPixmap( 0, 0, cw, ch, bgImage ); FIXME // } dbp.fillRect( 0, 0, cw, ch, d->preferences()->agendaGridBackgroundColor() ); dbp.translate( -cx, -cy ); double lGridSpacingY = d->mGridSpacingY * 2; // If work day, use work color // If busy day, use busy color // if work and busy day, mix both, and busy color has alpha const QVector busyDayMask = d->mAgendaView->busyDayMask(); // Highlight working hours if ( d->mWorkingHoursEnable && d->mHolidayMask ) { const QColor workColor = d->preferences()->workingHoursColor(); QPoint pt1( cx, d->mWorkingHoursYTop ); QPoint pt2( cx + cw, d->mWorkingHoursYBottom ); if ( pt2.x() >= pt1.x() /*&& pt2.y() >= pt1.y()*/) { int gxStart = contentsToGrid( pt1 ).x(); int gxEnd = contentsToGrid( pt2 ).x(); // correct start/end for rtl layouts if ( gxStart > gxEnd ) { int tmp = gxStart; gxStart = gxEnd; gxEnd = tmp; } int xoffset = ( QApplication::isRightToLeft() ? 1 : 0 ); while ( gxStart <= gxEnd ) { int xStart = gridToContents( QPoint( gxStart + xoffset, 0 ) ).x(); int xWidth = columnWidth( gxStart ) + 1; if ( pt2.y() < pt1.y() ) { // overnight working hours if ( ( ( gxStart == 0 ) && !d->mHolidayMask->at( d->mHolidayMask->count() - 1 ) ) || ( ( gxStart > 0 ) && ( gxStart < int( d->mHolidayMask->count() ) ) && ( !d->mHolidayMask->at( gxStart - 1 ) ) ) ) { if ( pt2.y() > cy ) { dbp.fillRect( xStart, cy, xWidth, pt2.y() - cy + 1, workColor ); } } if ( ( gxStart < int( d->mHolidayMask->count() - 1 ) ) && ( !d->mHolidayMask->at( gxStart ) ) ) { if ( pt1.y() < cy + ch - 1 ) { dbp.fillRect( xStart, pt1.y(), xWidth, cy + ch - pt1.y() + 1, workColor ); } } } else { // last entry in holiday mask denotes the previous day not visible // (needed for overnight shifts) if ( gxStart < int( d->mHolidayMask->count() - 1 ) && !d->mHolidayMask->at( gxStart ) ) { dbp.fillRect( xStart, pt1.y(), xWidth, pt2.y() - pt1.y() + 1, workColor ); } } ++gxStart; } } } // busy days if ( d->preferences()->colorAgendaBusyDays() && !d->mAllDayMode ) { for ( int i = 0; i < busyDayMask.count(); ++i ) { if ( busyDayMask[i] ) { const QPoint pt1( cx + d->mGridSpacingX * i, 0 ); // const QPoint pt2( cx + mGridSpacingX * ( i+1 ), ch ); QColor busyColor = d->preferences()->viewBgBusyColor(); busyColor.setAlpha( EventViews::BUSY_BACKGROUND_ALPHA ); dbp.fillRect( pt1.x(), pt1.y(), d->mGridSpacingX, cy + ch, busyColor ); } } } // draw selection if ( d->mHasSelection && d->mAgendaView->dateRangeSelectionEnabled() ) { QPoint pt, pt1; if ( d->mSelectionEndCell.x() > d->mSelectionStartCell.x() ) { // multi day selection // draw start day pt = gridToContents( d->mSelectionStartCell ); pt1 = gridToContents( QPoint( d->mSelectionStartCell.x() + 1, d->mRows + 1 ) ); dbp.fillRect( QRect( pt, pt1 ), d->preferences()->agendaGridHighlightColor() ); // draw all other days between the start day and the day of the selection end for ( int c = d->mSelectionStartCell.x() + 1; c < d->mSelectionEndCell.x(); ++c ) { pt = gridToContents( QPoint( c, 0 ) ); pt1 = gridToContents( QPoint( c + 1, d->mRows + 1 ) ); dbp.fillRect( QRect( pt, pt1 ), d->preferences()->agendaGridHighlightColor() ); } // draw end day pt = gridToContents( QPoint( d->mSelectionEndCell.x(), 0 ) ); pt1 = gridToContents( d->mSelectionEndCell + QPoint( 1, 1 ) ); dbp.fillRect( QRect( pt, pt1 ), d->preferences()->agendaGridHighlightColor() ); } else { // single day selection pt = gridToContents( d->mSelectionStartCell ); pt1 = gridToContents( d->mSelectionEndCell + QPoint( 1, 1 ) ); dbp.fillRect( QRect( pt, pt1 ), d->preferences()->agendaGridHighlightColor() ); } } QPen hourPen( d->preferences()->agendaGridBackgroundColor().dark( 150 ) ); QPen halfHourPen( d->preferences()->agendaGridBackgroundColor().dark( 125 ) ); dbp.setPen( hourPen ); // Draw vertical lines of grid, start with the last line not yet visible double x = ( int( cx / d->mGridSpacingX ) ) * d->mGridSpacingX; while ( x < cx + cw ) { dbp.drawLine( int( x ), cy, int( x ), cy + ch ); x += d->mGridSpacingX; } // Draw horizontal lines of grid double y = ( int( cy / ( 2 * lGridSpacingY ) ) ) * 2 * lGridSpacingY; while ( y < cy + ch ) { dbp.drawLine( cx, int( y ), cx + cw, int( y ) ); y += 2 * lGridSpacingY; } y = ( 2 * int( cy / ( 2 * lGridSpacingY ) ) + 1 ) * lGridSpacingY; dbp.setPen( halfHourPen ); while ( y < cy + ch ) { dbp.drawLine( cx, int( y ), cx + cw, int( y ) ); y += 2 * lGridSpacingY; } p->drawPixmap( cx, cy, db ); } /* Convert srcollview contents coordinates to agenda grid coordinates. */ QPoint Agenda::contentsToGrid ( const QPoint &pos ) const { int gx = int( QApplication::isRightToLeft() ? d->mColumns - pos.x() / d->mGridSpacingX : pos.x() / d->mGridSpacingX ); int gy = int( pos.y() / d->mGridSpacingY ); return QPoint( gx, gy ); } /* Convert agenda grid coordinates to scrollview contents coordinates. */ QPoint Agenda::gridToContents( const QPoint &gpos ) const { int x = int( QApplication::isRightToLeft() ? ( d->mColumns - gpos.x() ) * d->mGridSpacingX : gpos.x() * d->mGridSpacingX ); int y = int( gpos.y() * d->mGridSpacingY ); return QPoint( x, y ); } /* Return Y coordinate corresponding to time. Coordinates are rounded to fit into the grid. */ int Agenda::timeToY( const QTime &time ) const { int minutesPerCell = 24 * 60 / d->mRows; int timeMinutes = time.hour() * 60 + time.minute(); int Y = ( timeMinutes + ( minutesPerCell / 2 ) ) / minutesPerCell; return Y; } /* Return time corresponding to cell y coordinate. Coordinates are rounded to fit into the grid. */ QTime Agenda::gyToTime( int gy ) const { int secondsPerCell = 24 * 60 * 60 / d->mRows; int timeSeconds = secondsPerCell * gy; QTime time( 0, 0, 0 ); if ( timeSeconds < 24 * 60 * 60 ) { time = time.addSecs(timeSeconds); } else { time.setHMS( 23, 59, 59 ); } return time; } QVector Agenda::minContentsY() const { QVector minArray; minArray.fill( timeToY( QTime( 23, 59 ) ), d->mSelectedDates.count() ); foreach ( AgendaItem::QPtr item, d->mItems ) { if ( item ) { int ymin = item->cellYTop(); int index = item->cellXLeft(); if ( index >= 0 && index < (int)( d->mSelectedDates.count() ) ) { if ( ymin < minArray[index] && !d->mItemsToDelete.contains( item ) ) { minArray[index] = ymin; } } } } return minArray; } QVector Agenda::maxContentsY() const { QVector maxArray; maxArray.fill( timeToY( QTime( 0, 0 ) ), d->mSelectedDates.count() ); foreach ( AgendaItem::QPtr item, d->mItems ) { if ( item ) { int ymax = item->cellYBottom(); int index = item->cellXLeft(); if ( index >= 0 && index < (int)( d->mSelectedDates.count() ) ) { if ( ymax > maxArray[index] && !d->mItemsToDelete.contains( item ) ) { maxArray[index] = ymax; } } } } return maxArray; } void Agenda::setStartTime( const QTime &startHour ) { const double startPos = ( startHour.hour() / 24. + startHour.minute() / 1440. + startHour.second() / 86400. ) * d->mRows * gridSpacingY(); verticalScrollBar()->setValue( startPos ); } /* Insert AgendaItem into agenda. */ AgendaItem::QPtr Agenda::insertItem( const KCalCore::Incidence::Ptr &incidence, const KDateTime &recurrenceId, int X, int YTop, int YBottom, int itemPos, int itemCount, bool isSelected ) { if ( d->mAllDayMode ) { kDebug() << "using this in all-day mode is illegal."; return 0; } d->mActionType = NOP; AgendaItem::QPtr agendaItem = createAgendaItem( incidence, itemPos, itemCount, recurrenceId, isSelected ); if ( !agendaItem ) { return AgendaItem::QPtr(); } if ( YBottom <= YTop ) { kDebug() << "Text:" << agendaItem->text() << " YSize<0"; YBottom = YTop; } agendaItem->resize( int( ( X + 1 ) * d->mGridSpacingX ) - int( X * d->mGridSpacingX ), int( YTop * d->mGridSpacingY ) - int( ( YBottom + 1 ) * d->mGridSpacingY ) ); agendaItem->setCellXY( X, YTop, YBottom ); agendaItem->setCellXRight( X ); agendaItem->setResourceColor(d->mCalendar->resourceColor(incidence)); agendaItem->installEventFilter( this ); agendaItem->move( int( X * d->mGridSpacingX ), int( YTop * d->mGridSpacingY ) ); d->mItems.append( agendaItem ); placeSubCells( agendaItem ); agendaItem->show(); marcus_bains(); return agendaItem; } /* Insert all-day AgendaItem into agenda. */ AgendaItem::QPtr Agenda::insertAllDayItem( const KCalCore::Incidence::Ptr &incidence, const KDateTime &recurrenceId, int XBegin, int XEnd, bool isSelected ) { if ( !d->mAllDayMode ) { kError() << "using this in non all-day mode is illegal."; return 0; } d->mActionType = NOP; AgendaItem::QPtr agendaItem = createAgendaItem( incidence, 1, 1, recurrenceId, isSelected ); if ( !agendaItem ) { return AgendaItem::QPtr(); } agendaItem->setCellXY( XBegin, 0, 0 ); agendaItem->setCellXRight( XEnd ); const double startIt = d->mGridSpacingX * ( agendaItem->cellXLeft() ); const double endIt = d->mGridSpacingX * ( agendaItem->cellWidth() + agendaItem->cellXLeft() ); agendaItem->resize( int( endIt ) - int( startIt ), int( d->mGridSpacingY ) ); agendaItem->installEventFilter( this ); agendaItem->setResourceColor(d->mCalendar->resourceColor(incidence)); agendaItem->move( int( XBegin * d->mGridSpacingX ), 0 ) ; d->mItems.append( agendaItem ); placeSubCells( agendaItem ); agendaItem->show(); return agendaItem; } AgendaItem::QPtr Agenda::createAgendaItem( const KCalCore::Incidence::Ptr &incidence, int itemPos, int itemCount, const KDateTime &recurrenceId, bool isSelected ) { if ( !incidence ) { kWarning() << "Agenda::createAgendaItem() item is invalid."; return AgendaItem::QPtr(); } AgendaItem::QPtr agendaItem = new AgendaItem( d->mAgendaView, d->mCalendar, incidence, itemPos, itemCount, recurrenceId, isSelected, this ); connect( agendaItem, SIGNAL(removeAgendaItem(AgendaItem::QPtr)), SLOT(removeAgendaItem(AgendaItem::QPtr)) ); connect( agendaItem, SIGNAL(showAgendaItem(AgendaItem::QPtr)), SLOT(showAgendaItem(AgendaItem::QPtr)) ); d->mAgendaItemsById.insert( incidence->uid(), agendaItem ); return agendaItem; } void Agenda::insertMultiItem( const KCalCore::Incidence::Ptr &event, const KDateTime &recurrenceId, int XBegin, int XEnd, int YTop, int YBottom, bool isSelected ) { KCalCore::Event::Ptr ev = CalendarSupport::event( event ); Q_ASSERT( ev ); if ( d->mAllDayMode ) { kDebug() << "using this in all-day mode is illegal."; return; } d->mActionType = NOP; int cellX, cellYTop, cellYBottom; QString newtext; int width = XEnd - XBegin + 1; int count = 0; AgendaItem::QPtr current = 0; QList multiItems; int visibleCount = d->mSelectedDates.first().daysTo( d->mSelectedDates.last() ); for ( cellX = XBegin; cellX <= XEnd; ++cellX ) { ++count; //Only add the items that are visible. if( cellX >=0 && cellX <= visibleCount ) { if ( cellX == XBegin ) { cellYTop = YTop; } else { cellYTop = 0; } if ( cellX == XEnd ) { cellYBottom = YBottom; } else { cellYBottom = rows() - 1; } newtext = QString::fromLatin1( "(%1/%2): " ).arg( count ).arg( width ); newtext.append( ev->summary() ); current = insertItem( event, recurrenceId, cellX, cellYTop, cellYBottom, count, width, isSelected ); Q_ASSERT( current ); current->setText( newtext ); multiItems.append( current ); } } QList::iterator it = multiItems.begin(); QList::iterator e = multiItems.end(); if ( it != e ) { // .first asserts if the list is empty AgendaItem::QPtr first = multiItems.first(); AgendaItem::QPtr last = multiItems.last(); AgendaItem::QPtr prev = 0, next = 0; while ( it != e ) { AgendaItem::QPtr item = *it; ++it; next = ( it == e ) ? 0 : (*it); if ( item ) { item->setMultiItem( ( item == first ) ? 0 : first, prev, next, ( item == last ) ? 0 : last ); } prev = item; } } marcus_bains(); } void Agenda::removeIncidence( const KCalCore::Incidence::Ptr &incidence ) { if ( !incidence ) { kWarning() << "Agenda::removeIncidence() incidence is invalid"; return; } /* if ( !d->mViewCalendar->item(incidence).isValid() ) { // Ok, we really don't know the ID, give up. kWarning() << "Agenda::removeIncidence() Item to remove is invalid. uid = " << incidence->instanceIdentifier(); return; } */ if ( d->isQueuedForDeletion( incidence->uid() ) ) { return; // It's already queued for deletion } AgendaItem::List agendaItems = d->mAgendaItemsById.values( incidence->uid() ); if ( agendaItems.isEmpty() ) { // We're not displaying such item // kDebug() << "Ignoring"; return; } foreach ( const AgendaItem::QPtr &agendaItem, agendaItems ) { - if ( agendaItem && !removeAgendaItem( agendaItem ) ) { - kWarning() << "Agenda::removeIncidence() Failed to remove " << incidence->uid(); + if (agendaItem) { + if (incidence->instanceIdentifier() != agendaItem->incidence()->instanceIdentifier() ) { + continue; + } + if ( !removeAgendaItem( agendaItem ) ) { + kWarning() << "Agenda::removeIncidence() Failed to remove " << incidence->uid(); + } } } } void Agenda::showAgendaItem( AgendaItem::QPtr agendaItem ) { if ( !agendaItem ) { kError() << "Show what?"; return; } agendaItem->hide(); agendaItem->setParent( this ); if ( !d->mItems.contains( agendaItem ) ) { d->mItems.append( agendaItem ); } placeSubCells( agendaItem ); agendaItem->show(); } bool Agenda::removeAgendaItem( AgendaItem::QPtr agendaItem ) { Q_ASSERT( agendaItem ); // we found the item. Let's remove it and update the conflicts bool taken = false; QList conflictItems = agendaItem->conflictItems(); // removeChild( thisItem ); taken = d->mItems.removeAll( agendaItem ) > 0; d->mAgendaItemsById.remove( agendaItem->incidence()->uid(), agendaItem ); QList::iterator it; for ( it = conflictItems.begin(); it != conflictItems.end(); ++it ) { if ( *it ) { (*it)->setSubCells( ( *it )->subCells()-1 ); } } for ( it = conflictItems.begin(); it != conflictItems.end(); ++it ) { // the item itself is also in its own conflictItems list! if ( *it && *it != agendaItem ) { placeSubCells( *it ); } } d->mItemsToDelete.append( agendaItem ); d->mItemsQueuedForDeletion.insert( agendaItem->incidence()->uid() ); agendaItem->setVisible( false ); QTimer::singleShot( 0, this, SLOT(deleteItemsToDelete()) ); return taken; } void Agenda::deleteItemsToDelete() { qDeleteAll( d->mItemsToDelete ); d->mItemsToDelete.clear(); d->mItemsQueuedForDeletion.clear(); } /*QSizePolicy Agenda::sizePolicy() const { // Thought this would make the all-day event agenda minimum size and the // normal agenda take the remaining space. But it doesn't work. The QSplitter // don't seem to think that an Expanding widget needs more space than a // Preferred one. // But it doesn't hurt, so it stays. if (mAllDayMode) { return QSizePolicy(QSizePolicy::Expanding,QSizePolicy::Preferred); } else { return QSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding); } }*/ /* Overridden from QScrollView to provide proper resizing of AgendaItems. */ void Agenda::resizeEvent ( QResizeEvent *ev ) { QSize newSize( ev->size() ); if ( d->mAllDayMode ) { d->mGridSpacingX = static_cast( newSize.width() ) / d->mColumns; d->mGridSpacingY = newSize.height(); } else { d->mGridSpacingX = static_cast( newSize.width() ) / d->mColumns; // make sure that there are not more than 24 per day d->mGridSpacingY = static_cast( newSize.height() ) / d->mRows; if ( d->mGridSpacingY < d->mDesiredGridSpacingY ) { d->mGridSpacingY = d->mDesiredGridSpacingY; } } calculateWorkingHours(); QTimer::singleShot( 0, this, SLOT(resizeAllContents()) ); emit gridSpacingYChanged( d->mGridSpacingY * 4 ); QWidget::resizeEvent( ev ); updateGeometry(); } void Agenda::resizeAllContents() { double subCellWidth; if ( d->mAllDayMode ) { foreach ( const AgendaItem::QPtr &item, d->mItems ) { if ( item ) { subCellWidth = calcSubCellWidth( item ); placeAgendaItem( item, subCellWidth ); } } } else { foreach ( const AgendaItem::QPtr &item, d->mItems ) { if ( item ) { subCellWidth = calcSubCellWidth( item ); placeAgendaItem( item, subCellWidth ); } } } checkScrollBoundaries(); marcus_bains(); update(); } void Agenda::scrollUp() { int currentValue = verticalScrollBar()->value(); verticalScrollBar()->setValue( currentValue - d->mScrollOffset ); } void Agenda::scrollDown() { int currentValue = verticalScrollBar()->value(); verticalScrollBar()->setValue( currentValue + d->mScrollOffset ); } QSize Agenda::minimumSize() const { return sizeHint(); } QSize Agenda::minimumSizeHint() const { return sizeHint(); } int Agenda::minimumHeight() const { // all day agenda never has scrollbars and the scrollarea will // resize it to fit exactly on the viewport. if ( d->mAllDayMode ) { return 0; } else { return d->mGridSpacingY * d->mRows; } } void Agenda::updateConfig() { const double oldGridSpacingY = d->mGridSpacingY; if ( !d->mAllDayMode ) { d->mDesiredGridSpacingY = d->preferences()->hourSize(); if ( d->mDesiredGridSpacingY < 4 || d->mDesiredGridSpacingY > 30 ) { d->mDesiredGridSpacingY = 10; } /* // make sure that there are not more than 24 per day d->mGridSpacingY = static_cast( height() ) / d->mRows; if ( d->mGridSpacingY < d->mDesiredGridSpacingY || true) { d->mGridSpacingY = d->mDesiredGridSpacingY; } */ //can be two doubles equal?, it's better to compare them with an epsilon if ( fabs( oldGridSpacingY - d->mDesiredGridSpacingY ) > 0.1 ) { d->mGridSpacingY = d->mDesiredGridSpacingY; updateGeometry(); } } calculateWorkingHours(); marcus_bains(); } void Agenda::checkScrollBoundaries() { // Invalidate old values to force update d->mOldLowerScrollValue = -1; d->mOldUpperScrollValue = -1; checkScrollBoundaries( verticalScrollBar()->value() ); } void Agenda::checkScrollBoundaries( int v ) { int yMin = int( (v) / d->mGridSpacingY ); int yMax = int( ( v + d->mScrollArea->height() ) / d->mGridSpacingY ); if ( yMin != d->mOldLowerScrollValue ) { d->mOldLowerScrollValue = yMin; emit lowerYChanged( yMin ); } if ( yMax != d->mOldUpperScrollValue ) { d->mOldUpperScrollValue = yMax; emit upperYChanged( yMax ); } } int Agenda::visibleContentsYMin() const { int v = verticalScrollBar()->value(); return int( v / d->mGridSpacingY ); } int Agenda::visibleContentsYMax() const { int v = verticalScrollBar()->value(); return int( ( v + d->mScrollArea->height() ) / d->mGridSpacingY ); } void Agenda::deselectItem() { if ( d->mSelectedItem.isNull() ) { return; } const KCalCore::Incidence::Ptr selectedItem = d->mSelectedItem->incidence(); foreach ( AgendaItem::QPtr item, d->mItems ) { if ( item ) { const KCalCore::Incidence::Ptr itemInc = item->incidence(); if( itemInc && selectedItem && itemInc->uid() == selectedItem->uid() ) { item->select( false ); } } } d->mSelectedItem = 0; } void Agenda::selectItem( AgendaItem::QPtr item ) { if ( ( AgendaItem::QPtr )d->mSelectedItem == item ) { return; } deselectItem(); if ( item == 0 ) { emit incidenceSelected( KCalCore::Incidence::Ptr(), QDate() ); return; } d->mSelectedItem = item; d->mSelectedItem->select(); Q_ASSERT( d->mSelectedItem->incidence() ); d->mSelectedId = d->mSelectedItem->incidence()->uid(); foreach ( AgendaItem::QPtr item, d->mItems ) { if( item && item->incidence()->uid() == d->mSelectedId ) { item->select(); } } emit incidenceSelected( d->mSelectedItem->incidence(), d->mSelectedItem->occurrenceDate() ); } void Agenda::selectIncidenceByUid( const QString &uid ) { foreach ( AgendaItem::QPtr item, d->mItems ) { if ( item && item->incidence()->uid() == uid ) { selectItem( item ); break; } } } void Agenda::selectItem( const Akonadi::Item &item ) { selectIncidenceByUid( CalendarSupport::incidence(item)->uid() ); } // This function seems never be called. void Agenda::keyPressEvent( QKeyEvent *kev ) { switch( kev->key() ) { case Qt::Key_PageDown: verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepAdd ); break; case Qt::Key_PageUp: verticalScrollBar()->triggerAction( QAbstractSlider::SliderPageStepSub ); break; case Qt::Key_Down: verticalScrollBar()->triggerAction( QAbstractSlider::SliderSingleStepAdd ); break; case Qt::Key_Up: verticalScrollBar()->triggerAction( QAbstractSlider::SliderSingleStepSub ); break; default: ; } } void Agenda::calculateWorkingHours() { d->mWorkingHoursEnable = !d->mAllDayMode; QTime tmp = d->preferences()->workingHoursStart().time(); d->mWorkingHoursYTop = int( 4 * d->mGridSpacingY * ( tmp.hour() + tmp.minute() / 60. + tmp.second() / 3600. ) ); tmp = d->preferences()->workingHoursEnd().time(); d->mWorkingHoursYBottom = int( 4 * d->mGridSpacingY * ( tmp.hour() + tmp.minute() / 60. + tmp.second() / 3600. ) - 1 ); } void Agenda::setDateList( const KCalCore::DateList &selectedDates ) { d->mSelectedDates = selectedDates; marcus_bains(); } KCalCore::DateList Agenda::dateList() const { return d->mSelectedDates; } void Agenda::setCalendar( const MultiViewCalendar::Ptr &cal ) { d->mCalendar = cal; } void Agenda::setIncidenceChanger( Akonadi::IncidenceChanger *changer ) { d->mChanger = changer; } void Agenda::setHolidayMask( QVector *mask ) { d->mHolidayMask = mask; } void Agenda::contentsMousePressEvent ( QMouseEvent *event ) { Q_UNUSED( event ); } QSize Agenda::sizeHint() const { if ( d->mAllDayMode ) { return QWidget::sizeHint(); } else { return QSize( parentWidget()->width(), d->mGridSpacingY * d->mRows ); } } QScrollBar * Agenda::verticalScrollBar() const { return d->mScrollArea->verticalScrollBar(); } QScrollArea *Agenda::scrollArea() const { return d->mScrollArea; } AgendaItem::List Agenda::agendaItems( const QString &uid ) const { return d->mAgendaItemsById.values( uid ); } AgendaScrollArea::AgendaScrollArea( bool isAllDay, AgendaView *agendaView, bool isInteractive, QWidget *parent ) : QScrollArea( parent ) { if ( isAllDay ) { mAgenda = new Agenda( agendaView, this, 1, isInteractive ); setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); } else { mAgenda = new Agenda( agendaView, this, 1, 96, agendaView->preferences()->hourSize(), isInteractive ); } #ifdef KDEPIM_MOBILE_UI setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); #endif setWidgetResizable( true ); setWidget( mAgenda ); setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); mAgenda->setStartTime( agendaView->preferences()->dayBegins().time() ); } AgendaScrollArea::~AgendaScrollArea() { } Agenda *AgendaScrollArea::agenda() const { return mAgenda; } // kate: space-indent on; indent-width 2; replace-tabs on; diff --git a/calendarviews/agenda/agendaview.cpp b/calendarviews/agenda/agendaview.cpp index 08d6c83f13..c26d883770 100644 --- a/calendarviews/agenda/agendaview.cpp +++ b/calendarviews/agenda/agendaview.cpp @@ -1,2314 +1,2320 @@ /* Copyright (c) 2001 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com Author: Sergio Martins, sergio.martins@kdab.com 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 "agendaview.h" #include "agenda.h" #include "agendaitem.h" #include "viewcalendar.h" #include "alternatelabel.h" #include "calendardecoration.h" #include "decorationlabel.h" #include "prefs.h" #include "timelabels.h" #include "timelabelszone.h" #include #include #include #include #include #include #include #include #include #include // for SmallIcon() #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace EventViews; enum { SPACING = 2 }; enum { SHRINKDOWN = 2 // points less for the timezone font }; class EventIndicator::Private { EventIndicator *const q; public: Private( EventIndicator *parent, EventIndicator::Location loc ) : q( parent ), mColumns( 1 ), mLocation( loc ) { mEnabled.resize( mColumns ); QChar ch; // Dashed up and down arrow characters ch = QChar( mLocation == Top ? 0x21e1 : 0x21e3 ); QFont font = q->font(); font.setPixelSize( KIconLoader::global()->currentSize( KIconLoader::Dialog ) ); QFontMetrics fm( font ); QRect rect = fm.boundingRect( ch ).adjusted( -2, -2, 2, 2 ); mPixmap = QPixmap( rect.size() ); mPixmap.fill( Qt::transparent ); QPainter p( &mPixmap ); p.setOpacity( 0.33 ); p.setFont( font ); p.setPen( q->palette().text().color() ); p.drawText( -rect.left(), -rect.top(), ch ); } void adjustGeometry() { QRect rect; rect.setWidth( q->parentWidget()->width() ); rect.setHeight( q->height() ); rect.setLeft( 0 ); rect.setTop( mLocation == EventIndicator::Top ? 0 : q->parentWidget()->height() - rect.height() ); q->setGeometry( rect ); } public: int mColumns; Location mLocation; QPixmap mPixmap; QVector mEnabled; }; EventIndicator::EventIndicator( Location loc, QWidget *parent ) : QFrame( parent ), d( new Private( this, loc ) ) { setAttribute( Qt::WA_TransparentForMouseEvents ); setFixedHeight( d->mPixmap.height() ); parent->installEventFilter( this ); } EventIndicator::~EventIndicator() { delete d; } void EventIndicator::paintEvent( QPaintEvent * ) { QPainter painter( this ); const double cellWidth = static_cast( width() ) / d->mColumns; const bool isRightToLeft = QApplication::isRightToLeft(); const uint pixmapOffset = isRightToLeft ? 0 : ( cellWidth - d->mPixmap.width() ); for ( int i = 0; i < d->mColumns; ++i ) { if ( d->mEnabled[ i ] ) { const int xOffset = ( isRightToLeft ? ( d->mColumns - 1 - i ) : i ) * cellWidth; painter.drawPixmap( xOffset + pixmapOffset, 0, d->mPixmap ); } } } bool EventIndicator::eventFilter( QObject *, QEvent * event ) { if ( event->type() == QEvent::Resize ) { d->adjustGeometry(); } return false; } void EventIndicator::changeColumns( int columns ) { d->mColumns = columns; d->mEnabled.resize( d->mColumns ); show(); raise(); update(); } void EventIndicator::enableColumn( int column, bool enable ) { Q_ASSERT( column < d->mEnabled.count() ); d->mEnabled[ column ] = enable; } //////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// class AgendaView::Private : public Akonadi::ETMCalendar::CalendarObserver { AgendaView *const q; public: explicit Private( AgendaView *parent, bool isInteractive, bool isSideBySide ) : q( parent ), mTopDayLabels( 0 ), mLayoutTopDayLabels( 0 ), mTopDayLabelsFrame( 0 ), mLayoutBottomDayLabels( 0 ), mBottomDayLabels( 0 ), mBottomDayLabelsFrame( 0 ), mTimeBarHeaderFrame( 0 ), mAllDayAgenda( 0 ), mAgenda( 0 ), mTimeLabelsZone( 0 ), mAllowAgendaUpdate( true ), mUpdateItem( 0 ), mIsSideBySide( isSideBySide ), mDummyAllDayLeft( 0 ), mUpdateAllDayAgenda( true ), mUpdateAgenda( true ), mIsInteractive( isInteractive ), mUpdateEventIndicatorsScheduled( false ), mViewCalendar(MultiViewCalendar::Ptr( new MultiViewCalendar())) { mViewCalendar->mAgendaView = q; mViewCalendar->setETMCalendar(q->calendar()); } public: // view widgets QGridLayout *mGridLayout; QFrame *mTopDayLabels; QBoxLayout *mLayoutTopDayLabels; KHBox *mTopDayLabelsFrame; QList mDateDayLabels; QBoxLayout *mLayoutBottomDayLabels; QFrame *mBottomDayLabels; KHBox *mBottomDayLabelsFrame; KHBox *mAllDayFrame; QWidget *mTimeBarHeaderFrame; QSplitter *mSplitterAgenda; QList mTimeBarHeaders; Agenda *mAllDayAgenda; Agenda *mAgenda; TimeLabelsZone *mTimeLabelsZone; KCalCore::DateList mSelectedDates; // List of dates to be displayed KCalCore::DateList mSaveSelectedDates; // Save the list of dates between updateViews int mViewType; EventIndicator *mEventIndicatorTop; EventIndicator *mEventIndicatorBottom; QVector mMinY; QVector mMaxY; QVector mHolidayMask; QDateTime mTimeSpanBegin; QDateTime mTimeSpanEnd; bool mTimeSpanInAllDay; bool mAllowAgendaUpdate; Akonadi::Item mUpdateItem; const bool mIsSideBySide; QWidget *mDummyAllDayLeft; bool mUpdateAllDayAgenda; bool mUpdateAgenda; bool mIsInteractive; bool mUpdateEventIndicatorsScheduled; // Contains days that have at least one all-day Event with TRANSP: OPAQUE ( busy ) // that has you as organizer or attendee so we can color background with a different // color QMap mBusyDays; EventViews::MultiViewCalendar::Ptr mViewCalendar; bool makesWholeDayBusy( const KCalCore::Incidence::Ptr &incidence ) const; CalendarDecoration::Decoration *loadCalendarDecoration( const QString &name ); void clearView(); void setChanges( EventView::Changes changes, const KCalCore::Incidence::Ptr &incidence = KCalCore::Incidence::Ptr() ); /** Returns a list of consecutive dates, starting with @p start and ending with @p end. If either start or end are invalid, a list with QDate::currentDate() is returned */ static QList generateDateList( const QDate &start, const QDate &end ); void changeColumns( int numColumns ); AgendaItem::List agendaItems( const QString &uid ) const; // insertAtDateTime is in the view's timezone void insertIncidence( const KCalCore::Incidence::Ptr &, const KDateTime &recurrenceId, const KDateTime &insertAtDateTime, bool createSelected ); void reevaluateIncidence( const KCalCore::Incidence::Ptr &incidence ); bool datesEqual( const KCalCore::Incidence::Ptr &one, const KCalCore::Incidence::Ptr &two ) const; /** * Returns false if the incidence is for sure outside of the visible timespan. * Returns true if it might be, meaning that to be sure, timezones must be * taken into account. * This is a very fast way of discarding incidences that are outside of the * timespan and only performing expensive timezone operations on the ones * that might be viisble */ bool mightBeVisible( const KCalCore::Incidence::Ptr &incidence ) const; 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 ); }; bool AgendaView::Private::datesEqual( const KCalCore::Incidence::Ptr &one, const KCalCore::Incidence::Ptr &two ) const { const KDateTime start1 = one->dtStart(); const KDateTime start2 = two->dtStart(); const KDateTime end1 = one->dateTime( KCalCore::Incidence::RoleDisplayEnd ); const KDateTime end2 = two->dateTime( KCalCore::Incidence::RoleDisplayEnd ); if ( start1.isValid() ^ start2.isValid() ) return false; if ( end1.isValid() ^ end2.isValid() ) return false; if ( start1.isValid() && start1 != start2 ) return false; if ( end1.isValid() && end1 != end2 ) return false; return true; } AgendaItem::List AgendaView::Private::agendaItems( const QString &uid ) const { AgendaItem::List allDayAgendaItems = mAllDayAgenda->agendaItems( uid ); return allDayAgendaItems.isEmpty() ? mAgenda->agendaItems( uid ) : allDayAgendaItems; } bool AgendaView::Private::mightBeVisible( const KCalCore::Incidence::Ptr &incidence ) const { KCalCore::Todo::Ptr todo = incidence.dynamicCast(); const KDateTime::Spec timeSpec = q->preferences()->timeSpec(); KDateTime firstVisibleDateTime( mSelectedDates.first(), timeSpec ); KDateTime lastVisibleDateTime( mSelectedDates.last(), timeSpec ); // KDateTime::toTimeSpec() is expensive, so lets first compare only the date, // to see if the incidence is visible. // If it's more than 48h of diff, then for sure it won't be visible, // independently of timezone. // The largest difference between two timezones is about 24 hours. if ( todo && todo->isOverdue() ) { // Don't optimize this case. Overdue to-dos have their own rules for displaying themselves return true; } if ( !incidence->recurs() ) { // If DTEND/DTDUE is before the 1st visible column if ( incidence->dateTime( KCalCore::Incidence::RoleEnd ).date().daysTo( firstVisibleDateTime.date() ) > 2 ) { return false; } // if DTSTART is after the last visible column if ( !todo && lastVisibleDateTime.date().daysTo( incidence->dtStart().date() ) > 2 ) { return false; } // if DTDUE is after the last visible column if ( todo && lastVisibleDateTime.date().daysTo( todo->dtDue().date() ) > 2 ) { return false; } } return true; } void AgendaView::Private::changeColumns( int numColumns ) { // mMinY, mMaxY and mEnabled must all have the same size. // Make sure you preserve this order because mEventIndicatorTop->changeColumns() // can trigger a lot of stuff, and code will be executed when mMinY wasn't resized yet. mMinY.resize( numColumns ); mMaxY.resize( numColumns ); mEventIndicatorTop->changeColumns( numColumns ); mEventIndicatorBottom->changeColumns( numColumns ); } /** static */ QList AgendaView::Private::generateDateList( const QDate &start, const QDate &end ) { QList list; if ( start.isValid() && end.isValid() && end >= start && start.daysTo( end ) < AgendaView::MAX_DAY_COUNT ) { QDate date = start; while ( date <= end ) { list.append( date ); date = date.addDays( 1 ); } } else { list.append( QDate::currentDate() ); } return list; } void AgendaView::Private::reevaluateIncidence( const KCalCore::Incidence::Ptr &incidence ) { if (!incidence || !mViewCalendar->isValid( incidence )) { kWarning() << "invalid or unknown incidence." << incidence; return; } q->removeIncidence( incidence ); q->displayIncidence( incidence, false ); mAgenda->checkScrollBoundaries(); q->updateEventIndicators(); } void AgendaView::Private::calendarIncidenceAdded( const KCalCore::Incidence::Ptr &incidence ) { if (!incidence || !mViewCalendar->isValid( incidence )) { kWarning() << "invalid or unknown incidence." << incidence; Q_ASSERT( false ); return; } if ( incidence->hasRecurrenceId() && mViewCalendar->isValid(incidence)) { // Reevaluate the main event instead, if it was inserted before this one KCalCore::Incidence::Ptr mainIncidence = q->calendar2(incidence)->incidence( incidence->uid() ); if ( mainIncidence ) { reevaluateIncidence( mainIncidence ); } } else if ( q->displayIncidence( incidence, false ) ) { mAgenda->checkScrollBoundaries(); q->scheduleUpdateEventIndicators(); } } void AgendaView::Private::calendarIncidenceChanged( const KCalCore::Incidence::Ptr &incidence ) { if ( !incidence || incidence->uid().isEmpty() ) { kError() << "AgendaView::calendarIncidenceChanged() Invalid incidence or empty UID. " << incidence; Q_ASSERT( false ); return; } AgendaItem::List agendaItems = this->agendaItems( incidence->uid() ); if ( agendaItems.isEmpty() ) { kWarning() << "AgendaView::calendarIncidenceChanged() Invalid agendaItem for incidence " << incidence->uid(); return; } // Optimization: If the dates didn't change, just repaint it. // This optimization for now because we need to process collisions between agenda items. if ( false && !incidence->recurs() && agendaItems.count() == 1 ) { KCalCore::Incidence::Ptr originalIncidence = agendaItems.first()->incidence(); if ( datesEqual( originalIncidence, incidence ) ) { foreach ( const AgendaItem::QPtr &agendaItem, agendaItems ) { agendaItem->setIncidence( KCalCore::Incidence::Ptr( incidence->clone() ) ); agendaItem->update(); } return; } } if ( incidence->hasRecurrenceId() && mViewCalendar->isValid(incidence) ) { // Reevaluate the main event instead, if it exists KCalCore::Incidence::Ptr mainIncidence = q->calendar2(incidence)->incidence( incidence->uid() ); reevaluateIncidence( mainIncidence ? mainIncidence : incidence ); } else { reevaluateIncidence( incidence ); } // No need to call setChanges(), that triggers a fillAgenda() // setChanges( q->changes() | IncidencesEdited, incidence ); } void AgendaView::Private::calendarIncidenceDeleted( const KCalCore::Incidence::Ptr &incidence ) { if ( !incidence || incidence->uid().isEmpty() ) { kWarning() << "invalid incidence or empty uid: " << incidence; Q_ASSERT( false ); return; } q->removeIncidence( incidence ); if ( incidence->hasRecurrenceId()) { // Reevaluate the main event, if it exists. The exception was removed so the main recurrent series // will no be bigger. - if ( mViewCalendar->isValid(incidence) ) { - KCalCore::Incidence::Ptr mainIncidence = q->calendar2(incidence)->incidence( incidence->uid() ); + if ( mViewCalendar->isValid(incidence->uid()) ) { + KCalCore::Incidence::Ptr mainIncidence = q->calendar2(incidence->uid())->incidence( incidence->uid() ); if ( mainIncidence ) { - reevaluateIncidence( mainIncidence ); + reevaluateIncidence( mainIncidence ); } } } else if ( mightBeVisible( incidence ) ) { // No need to call setChanges(), that triggers a fillAgenda() // setChanges( q->changes() | IncidencesDeleted, CalendarSupport::incidence( incidence ) ); mAgenda->checkScrollBoundaries(); q->scheduleUpdateEventIndicators(); } } void EventViews::AgendaView::Private::setChanges( EventView::Changes changes, const KCalCore::Incidence::Ptr &incidence ) { // We could just call EventView::setChanges(...) but we're going to do a little // optimization. If only an all day item was changed, only all day agenda // should be updated. // all bits = 1 const int ones = ~0; const int incidenceOperations = IncidencesAdded | IncidencesEdited | IncidencesDeleted; // If changes has a flag turned on, other than incidence operations, then update both agendas if ( ( ones ^ incidenceOperations ) & changes ) { mUpdateAllDayAgenda = true; mUpdateAgenda = true; } else if ( incidence ) { mUpdateAllDayAgenda = mUpdateAllDayAgenda | incidence->allDay(); mUpdateAgenda = mUpdateAgenda | !incidence->allDay(); } q->EventView::setChanges( changes ); } void AgendaView::Private::clearView() { if ( mUpdateAllDayAgenda ) { mAllDayAgenda->clear(); } if ( mUpdateAgenda ) { mAgenda->clear(); } mBusyDays.clear(); } void AgendaView::Private::insertIncidence( const KCalCore::Incidence::Ptr &incidence, const KDateTime &recurrenceId, const KDateTime &insertAtDateTime, bool createSelected ) { if ( !q->filterByCollectionSelection( incidence ) ) { return; } KCalCore::Event::Ptr event = CalendarSupport::event( incidence ); KCalCore::Todo::Ptr todo = CalendarSupport::todo( incidence ); const QDate insertAtDate = insertAtDateTime.date(); // In case incidence->dtStart() isn't visible (crosses bounderies) const int curCol = qMax( mSelectedDates.first().daysTo( insertAtDate ), 0 ); // The date for the event is not displayed, just ignore it if ( curCol >= mSelectedDates.count() ) { return; } if ( mMinY.count() <= curCol ) { mMinY.resize( mSelectedDates.count() ); } if ( mMaxY.count() <= curCol ) { mMaxY.resize( mSelectedDates.count() ); } // Default values, which can never be reached mMinY[curCol] = mAgenda->timeToY( QTime( 23, 59 ) ) + 1; mMaxY[curCol] = mAgenda->timeToY( QTime( 0, 0 ) ) - 1; int beginX; int endX; if ( event ) { const QDate firstVisibleDate = mSelectedDates.first(); // its crossing bounderies, lets calculate beginX and endX const int duration = event->dtStart().toTimeSpec( q->preferences()->timeSpec() ).daysTo( event->dtEnd() ); if ( insertAtDate < firstVisibleDate ) { beginX = curCol + firstVisibleDate.daysTo( insertAtDate ); endX = beginX + duration; } else { beginX = curCol; endX = beginX + duration; } } else if ( todo ) { if ( !todo->hasDueDate() ) { return; // todo shall not be displayed if it has no date } beginX = endX = curCol; } else { return; } const KDateTime::Spec timeSpec = q->preferences()->timeSpec(); const QDate today = KDateTime::currentDateTime( timeSpec ).date(); if ( todo && todo->isOverdue() && today >= insertAtDate ) { mAllDayAgenda->insertAllDayItem( incidence, recurrenceId, curCol, curCol, createSelected ); } else if ( incidence->allDay() ) { mAllDayAgenda->insertAllDayItem( incidence, recurrenceId, beginX, endX, createSelected ); } else if ( event && event->isMultiDay( timeSpec ) ) { // TODO: We need a better isMultiDay(), one that receives the occurrence. // In the single-day handling code there's a neat comment on why // we're calculating the start time this way const QTime startTime = insertAtDateTime.time(); // In the single-day handling code there's a neat comment on why we use the // duration instead of fetching the end time directly const int durationOfFirstOccurrence = event->dtStart().secsTo( event->dtEnd() ); QTime endTime = startTime.addSecs( durationOfFirstOccurrence ); const int startY = mAgenda->timeToY( startTime ); if ( endTime == QTime( 0, 0, 0 ) ) { endTime = QTime( 23, 59, 59 ); } const int endY = mAgenda->timeToY( endTime ) - 1; if ( ( beginX <= 0 && curCol == 0 ) || beginX == curCol ) { mAgenda->insertMultiItem( incidence, recurrenceId, beginX, endX, startY, endY, createSelected ); } if ( beginX == curCol ) { mMaxY[curCol] = mAgenda->timeToY( QTime( 23, 59 ) ); if ( startY < mMinY[curCol] ) { mMinY[curCol] = startY; } } else if ( endX == curCol ) { mMinY[curCol] = mAgenda->timeToY( QTime( 0, 0 ) ); if ( endY > mMaxY[curCol] ) { mMaxY[curCol] = endY; } } else { mMinY[curCol] = mAgenda->timeToY( QTime( 0, 0 ) ); mMaxY[curCol] = mAgenda->timeToY( QTime( 23, 59 ) ); } } else { int startY = 0, endY = 0; if ( event ) { // Single day events fall here // Don't use event->dtStart().toTimeSpec( timeSpec ).time(). // If it's a UTC recurring event it should have a different time when it crosses DST, // so we must use insertAtDate here, so we get the correct time. // // The nth occurrence doesn't always have the same time as the 1st occurrence. const QTime startTime = insertAtDateTime.time(); // We could just fetch the end time directly from dtEnd() instead of adding a duration to the // start time. This way is best because it preserves the duration of the event. There are some // corner cases where the duration would be messed up, for example a UTC event that when // converted to local has dtStart() in day light saving time, but dtEnd() outside DST. // It could create events with 0 duration. const int durationOfFirstOccurrence = event->dtStart().secsTo( event->dtEnd() ); QTime endTime = startTime.addSecs( durationOfFirstOccurrence ); startY = mAgenda->timeToY( startTime ); if ( endTime == QTime( 0, 0, 0 ) ) { endTime = QTime( 23, 59, 59 ); } endY = mAgenda->timeToY( endTime ) - 1; } if ( todo ) { QTime t; if ( todo->recurs() ) { // The time we get depends on the insertAtDate, because of daylight savings changes const KDateTime ocurrrenceDateTime = KDateTime( insertAtDate, todo->dtDue().time(), todo->dtDue().timeSpec() ); t = ocurrrenceDateTime.toTimeSpec( timeSpec ).time(); } else { t = todo->dtDue().toTimeSpec( timeSpec ).time(); } if ( t == QTime( 0, 0 ) && !todo->recurs() ) { // To-dos due at 00h00 are drawn at the previous day and ending at // 23h59. For recurring to-dos, that's not being done because it wasn't // implemented yet in ::fillAgenda(). t = QTime( 23, 59 ); } const int halfHour = 1800; if ( t.addSecs( -halfHour ) < t ) { startY = mAgenda->timeToY( t.addSecs( -halfHour ) ); endY = mAgenda->timeToY( t ) - 1; } else { startY = 0; endY = mAgenda->timeToY( t.addSecs( halfHour ) ) - 1; } } if ( endY < startY ) { endY = startY; } mAgenda->insertItem( incidence, recurrenceId, curCol, startY, endY, 1, 1, createSelected ); if ( startY < mMinY[curCol] ) { mMinY[curCol] = startY; } if ( endY > mMaxY[curCol] ) { mMaxY[curCol] = endY; } } } //////////////////////////////////////////////////////////////////////////// AgendaView::AgendaView( const QDate &start, const QDate &end, bool isInteractive, bool isSideBySide, QWidget *parent ) : EventView( parent ), d( new Private( this, isInteractive, isSideBySide ) ) { init( start, end ); } AgendaView::AgendaView( const PrefsPtr &prefs, const QDate &start, const QDate &end, bool isInteractive, bool isSideBySide, QWidget *parent ) : EventView( parent ), d( new Private( this, isInteractive, isSideBySide ) ) { setPreferences( prefs ); init( start, end ); } void AgendaView::init( const QDate &start, const QDate &end ) { d->mSelectedDates = Private::generateDateList( start, end ); d->mGridLayout = new QGridLayout( this ); d->mGridLayout->setMargin( 0 ); /* Create agenda splitter */ d->mSplitterAgenda = new QSplitter( Qt::Vertical, this ); d->mGridLayout->addWidget( d->mSplitterAgenda, 1, 0 ); d->mSplitterAgenda->setOpaqueResize( KGlobalSettings::opaqueResize() ); /* Create day name labels for agenda columns */ d->mTopDayLabelsFrame = new KHBox( d->mSplitterAgenda ); d->mTopDayLabelsFrame->setSpacing( SPACING ); /* Create all-day agenda widget */ d->mAllDayFrame = new KHBox( d->mSplitterAgenda ); d->mAllDayFrame->setSpacing( SPACING ); // Alignment and description widgets if ( !d->mIsSideBySide ) { d->mTimeBarHeaderFrame = new KHBox( d->mAllDayFrame ); } // The widget itself d->mDummyAllDayLeft = new QWidget( d->mAllDayFrame ); AgendaScrollArea *allDayScrollArea = new AgendaScrollArea( true, this, d->mIsInteractive, d->mAllDayFrame ); d->mAllDayAgenda = allDayScrollArea->agenda(); /* Create the main agenda widget and the related widgets */ QWidget *agendaFrame = new QWidget( d->mSplitterAgenda ); QHBoxLayout *agendaLayout = new QHBoxLayout( agendaFrame ); agendaLayout->setMargin( 0 ); agendaLayout->setSpacing( SPACING ); // Create agenda AgendaScrollArea *scrollArea = new AgendaScrollArea( false, this, d->mIsInteractive, agendaFrame ); d->mAgenda = scrollArea->agenda(); // Create event indicator bars d->mEventIndicatorTop = new EventIndicator( EventIndicator::Top, scrollArea->viewport() ); d->mEventIndicatorBottom = new EventIndicator( EventIndicator::Bottom, scrollArea->viewport() ); // Create time labels d->mTimeLabelsZone = new TimeLabelsZone( this, preferences(), d->mAgenda ); // This timeLabelsZoneLayout is for adding some spacing // to align timelabels, to agenda's grid QVBoxLayout *timeLabelsZoneLayout = new QVBoxLayout(); agendaLayout->addLayout( timeLabelsZoneLayout ); agendaLayout->addWidget( scrollArea ); timeLabelsZoneLayout->addSpacing( scrollArea->frameWidth() ); timeLabelsZoneLayout->addWidget( d->mTimeLabelsZone ); timeLabelsZoneLayout->addSpacing( scrollArea->frameWidth() ); // Scrolling connect( d->mAgenda, SIGNAL(zoomView(int,QPoint,Qt::Orientation)), SLOT(zoomView(int,QPoint,Qt::Orientation)) ); // Event indicator updates connect( d->mAgenda, SIGNAL(lowerYChanged(int)), SLOT(updateEventIndicatorTop(int)) ); connect( d->mAgenda, SIGNAL(upperYChanged(int)), SLOT(updateEventIndicatorBottom(int)) ); if ( d->mIsSideBySide ) { d->mTimeLabelsZone->hide(); } /* Create a frame at the bottom which may be used by decorations */ d->mBottomDayLabelsFrame = new KHBox( d->mSplitterAgenda ); d->mBottomDayLabelsFrame->setSpacing( SPACING ); if ( !d->mIsSideBySide ) { /* Make the all-day and normal agendas line up with each other */ int margin = style()->pixelMetric( QStyle::PM_ScrollBarExtent ); if ( style()->styleHint( QStyle::SH_ScrollView_FrameOnlyAroundContents ) ) { // Needed for some styles. Oxygen needs it, Plastique does not. margin -= scrollArea->frameWidth(); } d->mAllDayFrame->layout()->addItem( new QSpacerItem( margin, 0 ) ); } updateTimeBarWidth(); // Don't call it now, bottom agenda isn't fully up yet QMetaObject::invokeMethod( this, "alignAgendas", Qt::QueuedConnection ); // Whoever changes this code, remember to leave createDayLabels() // inside the ctor, so it's always called before readSettings(), so // readSettings() works on the splitter that has the right amount of // widgets ( createDayLabels() via placeDecorationFrame() removes widgets). createDayLabels( true ); /* Connect the agendas */ connect( d->mAllDayAgenda, SIGNAL(newTimeSpanSignal(QPoint,QPoint)), SLOT(newTimeSpanSelectedAllDay(QPoint,QPoint)) ); connect( d->mAgenda, SIGNAL(newTimeSpanSignal(QPoint,QPoint)), SLOT(newTimeSpanSelected(QPoint,QPoint)) ); connectAgenda( d->mAgenda, d->mAllDayAgenda ); connectAgenda( d->mAllDayAgenda, d->mAgenda ); } AgendaView::~AgendaView() { foreach(const ViewCalendar::Ptr &cal, d->mViewCalendar->mSubCalendars) { if (cal->getCalendar()) { cal->getCalendar()->unregisterObserver(d); } } delete d; } KCalCore::Calendar::Ptr AgendaView::calendar2(KCalCore::Incidence::Ptr incidence) const { return d->mViewCalendar->findCalendar(incidence)->getCalendar(); } +KCalCore::Calendar::Ptr AgendaView::calendar2(const QString &incidenceIdentifier) const +{ + return d->mViewCalendar->findCalendar(incidenceIdentifier)->getCalendar(); +} void AgendaView::setCalendar( const Akonadi::ETMCalendar::Ptr &cal ) { if ( calendar() ) { calendar()->unregisterObserver( d ); } Q_ASSERT( cal ); EventView::setCalendar( cal ); calendar()->registerObserver( d ); d->mViewCalendar->setETMCalendar(cal); d->mAgenda->setCalendar( d->mViewCalendar ); d->mAllDayAgenda->setCalendar( d->mViewCalendar ); } void AgendaView::addCalendar(const ViewCalendar::Ptr &cal) { d->mViewCalendar->addCalendar(cal); cal->getCalendar()->registerObserver(d); } void AgendaView::connectAgenda( Agenda *agenda, Agenda *otherAgenda ) { connect( agenda, SIGNAL(showNewEventPopupSignal()), SIGNAL(showNewEventPopupSignal()) ); connect( agenda, SIGNAL(showIncidencePopupSignal(KCalCore::Incidence::Ptr,QDate)), SLOT(slotShowIncidencePopup(KCalCore::Incidence::Ptr,QDate))); agenda->setCalendar( d->mViewCalendar ); connect( agenda, SIGNAL(newEventSignal()), SIGNAL(newEventSignal()) ); connect( agenda, SIGNAL(newStartSelectSignal()), otherAgenda, SLOT(clearSelection()) ); connect( agenda, SIGNAL(newStartSelectSignal()), SIGNAL(timeSpanSelectionChanged()) ); connect( agenda, SIGNAL(editIncidenceSignal(KCalCore::Incidence::Ptr)), SLOT(slotEditIncidence(KCalCore::Incidence::Ptr)) ); connect( agenda, SIGNAL(showIncidenceSignal(KCalCore::Incidence::Ptr)), SLOT(slotShowIncidence(KCalCore::Incidence::Ptr)) ); connect( agenda, SIGNAL(deleteIncidenceSignal(KCalCore::Incidence::Ptr)), SLOT(slotDeleteIncidence(KCalCore::Incidence::Ptr)) ); // drag signals connect( agenda, SIGNAL(startDragSignal(KCalCore::Incidence::Ptr)), SLOT(startDrag(KCalCore::Incidence::Ptr)) ); // synchronize selections connect( agenda, SIGNAL(incidenceSelected(KCalCore::Incidence::Ptr,QDate)), otherAgenda, SLOT(deselectItem()) ); connect( agenda, SIGNAL(incidenceSelected(KCalCore::Incidence::Ptr,QDate)), SLOT(slotIncidenceSelected(KCalCore::Incidence::Ptr,QDate)) ); // rescheduling of todos by d'n'd connect( agenda, SIGNAL(droppedIncidences(KCalCore::Incidence::List,QPoint,bool)), SLOT(slotIncidencesDropped(KCalCore::Incidence::List,QPoint,bool)) ); connect( agenda, SIGNAL(droppedIncidences(QList,QPoint,bool)), SLOT(slotIncidencesDropped(QList,QPoint,bool)) ); } void AgendaView::slotIncidenceSelected(const KCalCore::Incidence::Ptr &incidence,QDate date) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { emit incidenceSelected(item, date); } } void AgendaView::slotShowIncidencePopup(const KCalCore::Incidence::Ptr &incidence, QDate date) { Akonadi::Item item = d->mViewCalendar->item(incidence); kDebug() << "wanna see the popup for " << incidence->uid() << item.id(); if (item.isValid()) { emit showIncidencePopupSignal(item, date); } } void AgendaView::slotShowIncidence(const KCalCore::Incidence::Ptr &incidence) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { emit showIncidenceSignal(item); } } void AgendaView::slotEditIncidence(const KCalCore::Incidence::Ptr &incidence) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { emit editIncidenceSignal(item); } } void AgendaView::slotDeleteIncidence(const KCalCore::Incidence::Ptr &incidence) { Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { emit deleteIncidenceSignal(item); } } void AgendaView::zoomInVertically( ) { if ( !d->mIsSideBySide ) { preferences()->setHourSize( preferences()->hourSize() + 1 ); } d->mAgenda->updateConfig(); d->mAgenda->checkScrollBoundaries(); d->mTimeLabelsZone->updateAll(); setChanges( changes() | ZoomChanged ); updateView(); } void AgendaView::zoomOutVertically( ) { if ( preferences()->hourSize() > 4 || d->mIsSideBySide ) { if ( !d->mIsSideBySide ) { preferences()->setHourSize( preferences()->hourSize() - 1 ); } d->mAgenda->updateConfig(); d->mAgenda->checkScrollBoundaries(); d->mTimeLabelsZone->updateAll(); setChanges( changes() | ZoomChanged ); updateView(); } } void AgendaView::zoomInHorizontally( const QDate &date ) { QDate begin; QDate newBegin; QDate dateToZoom = date; int ndays, count; begin = d->mSelectedDates.first(); ndays = begin.daysTo( d->mSelectedDates.last() ); // zoom with Action and are there a selected Incidence?, Yes, I zoom in to it. if ( ! dateToZoom.isValid () ) { dateToZoom = d->mAgenda->selectedIncidenceDate(); } if ( !dateToZoom.isValid() ) { if ( ndays > 1 ) { newBegin = begin.addDays(1); count = ndays - 1; emit zoomViewHorizontally ( newBegin, count ); } } else { if ( ndays <= 2 ) { newBegin = dateToZoom; count = 1; } else { newBegin = dateToZoom.addDays( -ndays / 2 + 1 ); count = ndays -1 ; } emit zoomViewHorizontally ( newBegin, count ); } } void AgendaView::zoomOutHorizontally( const QDate &date ) { QDate begin; QDate newBegin; QDate dateToZoom = date; int ndays, count; begin = d->mSelectedDates.first(); ndays = begin.daysTo( d->mSelectedDates.last() ); // zoom with Action and are there a selected Incidence?, Yes, I zoom out to it. if ( ! dateToZoom.isValid () ) { dateToZoom = d->mAgenda->selectedIncidenceDate(); } if ( !dateToZoom.isValid() ) { newBegin = begin.addDays( -1 ); count = ndays + 3 ; } else { newBegin = dateToZoom.addDays( -ndays / 2 - 1 ); count = ndays + 3; } if ( abs( count ) >= 31 ) { kDebug() << "change to the month view?"; } else { //We want to center the date emit zoomViewHorizontally( newBegin, count ); } } void AgendaView::zoomView( const int delta, const QPoint &pos, const Qt::Orientation orient ) { // TODO find out why this is necessary. seems to be some kind of performance hack static QDate zoomDate; static QTimer *t = new QTimer( this ); //Zoom to the selected incidence, on the other way // zoom to the date on screen after the first mousewheel move. if ( orient == Qt::Horizontal ) { const QDate date = d->mAgenda->selectedIncidenceDate(); if ( date.isValid() ) { zoomDate=date; } else { if ( !t->isActive() ) { zoomDate= d->mSelectedDates[ pos.x() ]; } t->setSingleShot( true ); t->start ( 1000 ); } if ( delta > 0 ) { zoomOutHorizontally( zoomDate ); } else { zoomInHorizontally( zoomDate ); } } else { // Vertical zoom const QPoint posConstentsOld = d->mAgenda->gridToContents( pos ); if ( delta > 0 ) { zoomOutVertically(); } else { zoomInVertically(); } const QPoint posConstentsNew = d->mAgenda->gridToContents( pos ); d->mAgenda->verticalScrollBar()->scroll( 0, posConstentsNew.y() - posConstentsOld.y() ); } } #ifndef EVENTVIEWS_NODECOS bool AgendaView::loadDecorations( const QStringList &decorations, DecorationList &decoList ) { foreach ( const QString &decoName, decorations ) { if ( preferences()->selectedPlugins().contains( decoName ) ) { decoList << d->loadCalendarDecoration( decoName ); } } return decoList.count() > 0; } void AgendaView::placeDecorationsFrame( KHBox *frame, bool decorationsFound, bool isTop ) { if ( decorationsFound ) { if ( isTop ) { // inserts in the first position d->mSplitterAgenda->insertWidget( 0, frame ); } else { // inserts in the last position frame->setParent( d->mSplitterAgenda ); } } else { frame->setParent( this ); d->mGridLayout->addWidget( frame, 0, 0 ); } } void AgendaView::placeDecorations( DecorationList &decoList, const QDate &date, KHBox *labelBox, bool forWeek ) { foreach ( CalendarDecoration::Decoration *deco, decoList ) { CalendarDecoration::Element::List elements; elements = forWeek ? deco->weekElements( date ) : deco->dayElements( date ); if ( elements.count() > 0 ) { KHBox *decoHBox = new KHBox( labelBox ); decoHBox->setFrameShape( QFrame::StyledPanel ); decoHBox->setMinimumWidth( 1 ); foreach ( CalendarDecoration::Element *it, elements ) { DecorationLabel *label = new DecorationLabel( it, decoHBox ); label->setAlignment( Qt::AlignBottom ); label->setMinimumWidth( 1 ); } } } } #endif void AgendaView::createDayLabels( bool force ) { // Check if mSelectedDates has changed, if not just return // Removes some flickering and gains speed (since this is called by each updateView()) if ( !force && d->mSaveSelectedDates == d->mSelectedDates ) { return; } d->mSaveSelectedDates = d->mSelectedDates; delete d->mTopDayLabels; delete d->mBottomDayLabels; d->mDateDayLabels.clear(); QFontMetrics fm = fontMetrics(); d->mTopDayLabels = new QFrame ( d->mTopDayLabelsFrame ); d->mTopDayLabelsFrame->setStretchFactor( d->mTopDayLabels, 1 ); d->mLayoutTopDayLabels = new QHBoxLayout( d->mTopDayLabels ); d->mLayoutTopDayLabels->setMargin( 0 ); d->mLayoutTopDayLabels->setSpacing( 1 ); // this spacer moves the day labels over to line up with the day columns QSpacerItem *spacer = new QSpacerItem( ( !d->mIsSideBySide ? d->mTimeLabelsZone->width() : 0 ) + SPACING + d->mAllDayAgenda->scrollArea()->frameWidth(), 1, QSizePolicy::Fixed ); d->mLayoutTopDayLabels->addSpacerItem( spacer ); KVBox *topWeekLabelBox = new KVBox( d->mTopDayLabels ); d->mLayoutTopDayLabels->addWidget( topWeekLabelBox ); if ( d->mIsSideBySide ) { topWeekLabelBox->hide(); } d->mBottomDayLabels = new QFrame( d->mBottomDayLabelsFrame ); d->mBottomDayLabelsFrame->setStretchFactor( d->mBottomDayLabels, 1 ); d->mLayoutBottomDayLabels = new QHBoxLayout( d->mBottomDayLabels ); d->mLayoutBottomDayLabels->setMargin( 0 ); KVBox *bottomWeekLabelBox = new KVBox( d->mBottomDayLabels ); d->mLayoutBottomDayLabels->addWidget( bottomWeekLabelBox ); const KCalendarSystem *calsys = KGlobal::locale()->calendar(); #ifndef EVENTVIEWS_NODECOS QList topDecos; QStringList topStrDecos = preferences()->decorationsAtAgendaViewTop(); placeDecorationsFrame( d->mTopDayLabelsFrame, loadDecorations( topStrDecos, topDecos ), true ); QList botDecos; QStringList botStrDecos = preferences()->decorationsAtAgendaViewBottom(); placeDecorationsFrame( d->mBottomDayLabelsFrame, loadDecorations( botStrDecos, botDecos ), false ); #endif Q_FOREACH ( const QDate &date, d->mSelectedDates ) { KVBox *topDayLabelBox = new KVBox( d->mTopDayLabels ); d->mLayoutTopDayLabels->addWidget( topDayLabelBox ); KVBox *bottomDayLabelBox = new KVBox( d->mBottomDayLabels ); d->mLayoutBottomDayLabels->addWidget( bottomDayLabelBox ); int dW = calsys->dayOfWeek( date ); QString veryLongStr = KGlobal::locale()->formatDate( date ); QString longstr = i18nc( "short_weekday date (e.g. Mon 13)","%1 %2", calsys->weekDayName( dW, KCalendarSystem::ShortDayName ), calsys->day( date ) ); QString shortstr = QString::number( calsys->day( date ) ); AlternateLabel *dayLabel = new AlternateLabel( shortstr, longstr, veryLongStr, topDayLabelBox ); dayLabel->useShortText(); // will be recalculated in updateDayLabelSizes() anyway dayLabel->setMinimumWidth( 1 ); dayLabel->setAlignment( Qt::AlignHCenter ); if ( date == QDate::currentDate() ) { QFont font = dayLabel->font(); font.setBold( true ); dayLabel->setFont( font ); } d->mDateDayLabels.append( dayLabel ); // if a holiday region is selected, show the holiday name const QStringList texts = CalendarSupport::holiday( date ); Q_FOREACH ( const QString &text, texts ) { // Compute a small version of the holiday string for AlternateLabel const KWordWrap *ww = KWordWrap::formatText( fm, topDayLabelBox->rect(), 0, text, -1 ); AlternateLabel *label = new AlternateLabel( ww->truncatedString(), text, text, topDayLabelBox ); label->setMinimumWidth( 1 ); label->setAlignment( Qt::AlignCenter ); delete ww; } #ifndef EVENTVIEWS_NODECOS // Day decoration labels placeDecorations( topDecos, date, topDayLabelBox, false ); placeDecorations( botDecos, date, bottomDayLabelBox, false ); #endif } QSpacerItem *rightSpacer = new QSpacerItem( d->mAllDayAgenda->scrollArea()->frameWidth(), 1, QSizePolicy::Fixed ); d->mLayoutTopDayLabels->addSpacerItem( rightSpacer ); #ifndef EVENTVIEWS_NODECOS // Week decoration labels placeDecorations( topDecos, d->mSelectedDates.first(), topWeekLabelBox, true ); placeDecorations( botDecos, d->mSelectedDates.first(), bottomWeekLabelBox, true ); #endif if ( !d->mIsSideBySide ) { d->mLayoutTopDayLabels->addSpacing( d->mAgenda->verticalScrollBar()->width() ); d->mLayoutBottomDayLabels->addSpacing( d->mAgenda->verticalScrollBar()->width() ); } d->mTopDayLabels->show(); d->mBottomDayLabels->show(); // Update the labels now and after a single event loop run. Now to avoid flicker, and // delayed so that the delayed layouting size is taken into account. updateDayLabelSizes(); } void AgendaView::updateDayLabelSizes() { // First, calculate the maximum text type that fits for all labels AlternateLabel::TextType overallType = AlternateLabel::Extensive; foreach ( AlternateLabel *label, d->mDateDayLabels ) { AlternateLabel::TextType type = label->largestFittingTextType(); if ( type < overallType ) { overallType = type; } } // Then, set that maximum text type to all the labels foreach ( AlternateLabel *label, d->mDateDayLabels ) { label->setFixedType( overallType ); } } void AgendaView::resizeEvent( QResizeEvent *resizeEvent ) { updateDayLabelSizes(); EventView::resizeEvent( resizeEvent ); } void AgendaView::enableAgendaUpdate( bool enable ) { d->mAllowAgendaUpdate = enable; } int AgendaView::currentDateCount() const { return d->mSelectedDates.count(); } Akonadi::Item::List AgendaView::selectedIncidences() const { Akonadi::Item::List selected; KCalCore::Incidence::Ptr agendaitem = d->mAgenda->selectedIncidence(); if ( agendaitem ) { selected.append( d->mViewCalendar->item(agendaitem) ); } KCalCore::Incidence::Ptr dayitem = d->mAllDayAgenda->selectedIncidence(); if ( dayitem ) { selected.append( d->mViewCalendar->item(dayitem) ); } return selected; } KCalCore::DateList AgendaView::selectedIncidenceDates() const { KCalCore::DateList selected; QDate qd; qd = d->mAgenda->selectedIncidenceDate(); if ( qd.isValid() ) { selected.append( qd ); } qd = d->mAllDayAgenda->selectedIncidenceDate(); if ( qd.isValid() ) { selected.append( qd ); } return selected; } bool AgendaView::eventDurationHint( QDateTime &startDt, QDateTime &endDt, bool &allDay ) const { if ( selectionStart().isValid() ) { QDateTime start = selectionStart(); QDateTime end = selectionEnd(); if ( start.secsTo( end ) == 15 * 60 ) { // One cell in the agenda view selected, e.g. // because of a double-click, => Use the default duration QTime defaultDuration( CalendarSupport::KCalPrefs::instance()->defaultDuration().time() ); int addSecs = ( defaultDuration.hour() * 3600 ) + ( defaultDuration.minute() * 60 ); end = start.addSecs( addSecs ); } startDt = start; endDt = end; allDay = selectedIsAllDay(); return true; } return false; } /** returns if only a single cell is selected, or a range of cells */ bool AgendaView::selectedIsSingleCell() const { if ( !selectionStart().isValid() || !selectionEnd().isValid() ) { return false; } if ( selectedIsAllDay() ) { int days = selectionStart().daysTo( selectionEnd() ); return ( days < 1 ); } else { int secs = selectionStart().secsTo( selectionEnd() ); return ( secs <= 24 * 60 * 60 / d->mAgenda->rows() ); } } void AgendaView::updateView() { fillAgenda(); } /* Update configuration settings for the agenda view. This method is not complete. */ void AgendaView::updateConfig() { // Agenda can be null if setPreferences() is called inside the ctor // We don't need to update anything in this case. if ( d->mAgenda && d->mAllDayAgenda ) { d->mAgenda->updateConfig(); d->mAllDayAgenda->updateConfig(); d->mTimeLabelsZone->setPreferences( preferences() ); d->mTimeLabelsZone->updateAll(); updateTimeBarWidth(); setHolidayMasks(); createDayLabels( true ); setChanges( changes() | ConfigChanged ); updateView(); } } void AgendaView::createTimeBarHeaders() { qDeleteAll( d->mTimeBarHeaders ); d->mTimeBarHeaders.clear(); const QFont oldFont( font() ); QFont labelFont = d->mTimeLabelsZone->preferences()->agendaTimeLabelsFont(); labelFont.setPointSize( labelFont.pointSize() - SHRINKDOWN ); foreach ( QScrollArea *area, d->mTimeLabelsZone->timeLabels() ) { TimeLabels *timeLabel = static_cast( area->widget() ); QLabel *label = new QLabel( timeLabel->header().replace( QLatin1Char('/'), QLatin1String("/ ") ), d->mTimeBarHeaderFrame ); label->setFont( labelFont ); label->setAlignment( Qt::AlignBottom | Qt::AlignRight ); label->setMargin( 0 ); label->setWordWrap( true ); label->setToolTip( timeLabel->headerToolTip() ); d->mTimeBarHeaders.append( label ); } setFont( oldFont ); } void AgendaView::updateTimeBarWidth() { if ( d->mIsSideBySide ) { return; } createTimeBarHeaders(); const QFont oldFont( font() ); QFont labelFont = d->mTimeLabelsZone->preferences()->agendaTimeLabelsFont(); labelFont.setPointSize( labelFont.pointSize() - SHRINKDOWN ); QFontMetrics fm( labelFont ); int width = d->mTimeLabelsZone->preferedTimeLabelsWidth(); foreach ( QLabel *l, d->mTimeBarHeaders ) { foreach ( const QString &word, l->text().split( QLatin1Char(' ') ) ) { width = qMax( width, fm.width( word ) ); } } setFont( oldFont ); width = width + fm.width( QLatin1Char( '/' ) ); const int timeBarWidth = width * d->mTimeBarHeaders.count(); d->mTimeBarHeaderFrame->setFixedWidth( timeBarWidth - SPACING ); d->mTimeLabelsZone->setFixedWidth( timeBarWidth ); d->mDummyAllDayLeft->setFixedWidth( 0 ); } void AgendaView::updateEventDates( AgendaItem *item, bool addIncidence, Akonadi::Collection::Id collectionId ) { kDebug() << item->text() << "; item->cellXLeft(): " << item->cellXLeft() << "; item->cellYTop(): " << item->cellYTop() << "; item->lastMultiItem(): " << item->lastMultiItem() << "; item->itemPos(): " << item->itemPos() << "; item->itemCount(): " << item->itemCount() << endl; KDateTime startDt, endDt; // Start date of this incidence, calculate the offset from it // (so recurring and non-recurring items can be treated exactly the same, // we never need to check for recurs(), because we only move the start day // by the number of days the agenda item was really moved. Smart, isn't it?) QDate thisDate; if ( item->cellXLeft() < 0 ) { thisDate = ( d->mSelectedDates.first() ).addDays( item->cellXLeft() ); } else { thisDate = d->mSelectedDates[ item->cellXLeft() ]; } int daysOffset = 0; // daysOffset should only be calculated if item->cellXLeft() is positive which doesn't happen // if the event's start isn't visible. if ( item->cellXLeft() >= 0 ) { daysOffset = item->occurrenceDate().daysTo( thisDate ); } int daysLength = 0; // startDt.setDate( startDate ); KCalCore::Incidence::Ptr incidence = item->incidence(); Akonadi::Item aitem = d->mViewCalendar->item(incidence); if ( (!aitem.isValid() && !addIncidence) || !incidence || !changer() ) { kWarning() << "changer is " << changer() << " and incidence is " << incidence.data(); return; } QTime startTime( 0, 0, 0 ), endTime( 0, 0, 0 ); if ( incidence->allDay() ) { daysLength = item->cellWidth() - 1; } else { startTime = d->mAgenda->gyToTime( item->cellYTop() ); if ( item->lastMultiItem() ) { endTime = d->mAgenda->gyToTime( item->lastMultiItem()->cellYBottom() + 1 ); daysLength = item->lastMultiItem()->cellXLeft() - item->cellXLeft(); } else if ( item->itemPos() == item->itemCount() && item->itemCount() > 1 ) { /* multiitem handling in agenda assumes two things: - The start (first KOAgendaItem) is always visible. - The first KOAgendaItem of the incidence has a non-null item->lastMultiItem() pointing to the last KOagendaItem. But those aren't always met, for example when in day-view. kolab/issue4417 */ // Cornercase 1: - Resizing the end of the event but the start isn't visible endTime = d->mAgenda->gyToTime( item->cellYBottom() + 1 ); daysLength = item->itemCount() - 1; startTime = incidence->dtStart().time(); } else if ( item->itemPos() == 1 && item->itemCount() > 1 ) { // Cornercase 2: - Resizing the start of the event but the end isn't visible endTime = incidence->dateTime( KCalCore::Incidence::RoleEnd ).time(); daysLength = item->itemCount() - 1; } else { endTime = d-> mAgenda->gyToTime( item->cellYBottom() + 1 ); } } // FIXME: use a visitor here if ( const KCalCore::Event::Ptr ev = CalendarSupport::event( incidence ) ) { startDt = incidence->dtStart(); // convert to calendar timespec because we then manipulate it // with time coming from the calendar startDt = startDt.toTimeSpec( preferences()->timeSpec() ); startDt = startDt.addDays( daysOffset ); if ( !startDt.isDateOnly() ) { startDt.setTime( startTime ); } endDt = startDt.addDays( daysLength ); if ( !endDt.isDateOnly() ) { endDt.setTime( endTime ); } if ( incidence->dtStart().toTimeSpec( preferences()->timeSpec() ) == startDt && ev->dtEnd().toTimeSpec( preferences()->timeSpec() ) == endDt ) { // No change QTimer::singleShot( 0, this, SLOT(updateView()) ); return; } } else if ( const KCalCore::Todo::Ptr td = CalendarSupport::todo( incidence ) ) { startDt = td->hasStartDate() ? td->dtStart() : td->dtDue(); // convert to calendar timespec because we then manipulate it with time coming from // the calendar startDt = startDt.toTimeSpec( preferences()->timeSpec() ); startDt.setDate( thisDate.addDays( td->dtDue().daysTo( startDt ) ) ); if ( !startDt.isDateOnly() ) { startDt.setTime( startTime ); } endDt = startDt; endDt.setDate( thisDate ); if ( !endDt.isDateOnly() ) { endDt.setTime( endTime ); } if ( td->dtDue().toTimeSpec( preferences()->timeSpec() ) == endDt ) { // No change QMetaObject::invokeMethod( this, "updateView", Qt::QueuedConnection ); return; } } // A commented code block which had 150 lines to adjust recurrence was here. // I deleted it in rev 1180272 to make this function readable. if ( const KCalCore::Event::Ptr ev = CalendarSupport::event( incidence ) ) { /* setDtEnd() must be called before setDtStart(), otherwise, when moving * events, CalendarLocal::incidenceUpdated() will not remove the old hash * and that causes the event to be shown in the old date also (bug #179157). * * TODO: We need a better hashing mechanism for CalendarLocal. */ ev->setDtEnd( endDt.toTimeSpec( incidence->dateTime( KCalCore::Incidence::RoleEnd ).timeSpec() ) ); incidence->setDtStart( startDt.toTimeSpec( incidence->dtStart().timeSpec() ) ); } else if ( const KCalCore::Todo::Ptr td = CalendarSupport::todo( incidence ) ) { if ( td->hasStartDate() ) { td->setDtStart( startDt.toTimeSpec( incidence->dtStart().timeSpec() ) ); } td->setDtDue( endDt.toTimeSpec( td->dtDue().timeSpec() ) ); } if (!incidence->hasRecurrenceId()) { item->setOccurrenceDateTime( startDt ); } bool result; if ( addIncidence ) { Akonadi::Collection collection = calendar()->collection( collectionId ); result = changer()->createIncidence( incidence, collection, this ) != -1; } else { KCalCore::Incidence::Ptr oldIncidence( CalendarSupport::incidence(aitem) ); aitem.setPayload(incidence); result = changer()->modifyIncidence( aitem, oldIncidence, this ) != -1; } // Update the view correctly if an agenda item move was aborted by // cancelling one of the subsequent dialogs. if ( !result ) { setChanges( changes() | IncidencesEdited ); QMetaObject::invokeMethod( this, "updateView", Qt::QueuedConnection ); return; } // don't update the agenda as the item already has the correct coordinates. // an update would delete the current item and recreate it, but we are still // using a pointer to that item! => CRASH enableAgendaUpdate( false ); // We need to do this in a timer to make sure we are not deleting the item // we are currently working on, which would lead to crashes // Only the actually moved agenda item is already at the correct position and mustn't be // recreated. All others have to!!! if ( incidence->recurs() || incidence->hasRecurrenceId() ) { d->mUpdateItem = aitem; QMetaObject::invokeMethod( this, "updateView", Qt::QueuedConnection ); } enableAgendaUpdate( true ); } QDate AgendaView::startDate() const { if ( d->mSelectedDates.isEmpty() ) { return QDate(); } return d->mSelectedDates.first(); } QDate AgendaView::endDate() const { if ( d->mSelectedDates.isEmpty() ) { return QDate(); } return d->mSelectedDates.last(); } void AgendaView::showDates( const QDate &start, const QDate &end, const QDate &preferredMonth ) { Q_UNUSED( preferredMonth ); if ( !d->mSelectedDates.isEmpty() && d->mSelectedDates.first() == start && d->mSelectedDates.last() == end ) { return; } if ( !start.isValid() || !end.isValid() || start > end || start.daysTo( end ) > MAX_DAY_COUNT ) { kWarning() << "got bizare parameters: " << start << end << " - aborting here"; return; } d->mSelectedDates = d->generateDateList( start, end ); // and update the view setChanges( changes() | DatesChanged ); fillAgenda(); } void AgendaView::showIncidences( const Akonadi::Item::List &incidences, const QDate &date ) { Q_UNUSED( date ); if ( !calendar() ) { kError() << "No Calendar set"; return; } // we must check if they are not filtered; if they are, remove the filter KCalCore::CalFilter *filter = calendar()->filter(); bool wehaveall = true; if ( filter ) { Q_FOREACH ( const Akonadi::Item &aitem, incidences ) { if ( !( wehaveall = filter->filterIncidence( CalendarSupport::incidence( aitem ) ) ) ) { break; } } } if ( !wehaveall ) { calendar()->setFilter( 0 ); } const KDateTime::Spec timeSpec = preferences()->timeSpec(); KDateTime start = CalendarSupport::incidence( incidences.first() )->dtStart().toTimeSpec( timeSpec ); KDateTime end = CalendarSupport::incidence( incidences.first() )->dateTime( KCalCore::Incidence::RoleEnd ).toTimeSpec( timeSpec ); Akonadi::Item first = incidences.first(); Q_FOREACH ( const Akonadi::Item &aitem, incidences ) { if ( CalendarSupport::incidence( aitem )->dtStart().toTimeSpec( timeSpec ) < start ) { first = aitem; } start = qMin( start, CalendarSupport::incidence( aitem )->dtStart().toTimeSpec( timeSpec ) ); end = qMax( start, CalendarSupport::incidence( aitem )->dateTime( KCalCore::Incidence::RoleEnd ).toTimeSpec( timeSpec ) ); } end.toTimeSpec( start ); // allow direct comparison of dates if ( start.date().daysTo( end.date() ) + 1 <= currentDateCount() ) { showDates( start.date(), end.date() ); } else { showDates( start.date(), start.date().addDays( currentDateCount() - 1 ) ); } d->mAgenda->selectItem( first ); } void AgendaView::fillAgenda() { if ( changes() == NothingChanged ) { return; } if ( d->mViewCalendar->calendars() == 0) { kWarning() << "No calendar is set"; return; } /* kDebug() << "changes = " << changes() << "; mUpdateAgenda = " << d->mUpdateAgenda << "; mUpdateAllDayAgenda = " << d->mUpdateAllDayAgenda; */ /* Remember the item Ids of the selected items. In case one of the * items was deleted and re-added, we want to reselect it. */ const QString selectedAgendaId = d->mAgenda->lastSelectedItemId(); const QString selectedAllDayAgendaId = d->mAllDayAgenda->lastSelectedItemId(); enableAgendaUpdate( true ); d->clearView(); if ( changes().testFlag( DatesChanged ) ) { d->mAllDayAgenda->changeColumns( d->mSelectedDates.count() ); d->mAgenda->changeColumns( d->mSelectedDates.count() ); d->changeColumns( d->mSelectedDates.count() ); createDayLabels( false ); setHolidayMasks(); d->mAgenda->setDateList( d->mSelectedDates ); } setChanges( NothingChanged ); bool somethingReselected = false; const KCalCore::Incidence::List incidences = d->mViewCalendar->incidences(); foreach ( const KCalCore::Incidence::Ptr &incidence, incidences ) { Q_ASSERT( incidence ); const bool wasSelected = incidence->uid() == selectedAgendaId || incidence->uid() == selectedAllDayAgendaId; if ( ( incidence->allDay() && d->mUpdateAllDayAgenda ) || ( !incidence->allDay() && d->mUpdateAgenda ) ) { displayIncidence( incidence, wasSelected ); } if ( wasSelected ) { somethingReselected = true; } } d->mAgenda->checkScrollBoundaries(); updateEventIndicators(); // mAgenda->viewport()->update(); // mAllDayAgenda->viewport()->update(); // make invalid deleteSelectedDateTime(); d->mUpdateAgenda = false; d->mUpdateAllDayAgenda = false; if ( !somethingReselected ) { emit incidenceSelected( Akonadi::Item(), QDate() ); } } bool AgendaView::displayIncidence( const KCalCore::Incidence::Ptr &incidence, bool createSelected ) { if ( !incidence || incidence->hasRecurrenceId() ) { return false; } KCalCore::Todo::Ptr todo = CalendarSupport::todo( incidence ); if ( todo && ( !preferences()->showTodosAgendaView() || !todo->hasDueDate() ) ) { return false; } KCalCore::Event::Ptr event = CalendarSupport::event( incidence ); const QDate today = QDate::currentDate(); KCalCore::DateTimeList::iterator t; const KDateTime::Spec timeSpec = preferences()->timeSpec(); KDateTime firstVisibleDateTime( d->mSelectedDates.first(), timeSpec ); KDateTime lastVisibleDateTime( d->mSelectedDates.last(), timeSpec ); // Optimization, very cheap operation that discards incidences that aren't in the timespan if ( !d->mightBeVisible( incidence ) ) { return false; } lastVisibleDateTime.setTime( QTime( 23, 59, 59, 59 ) ); firstVisibleDateTime.setTime( QTime( 0, 0 ) ); KCalCore::DateTimeList dateTimeList; const KDateTime incDtStart = incidence->dtStart().toTimeSpec( timeSpec ); const KDateTime incDtEnd = incidence->dateTime( KCalCore::Incidence::RoleEnd ).toTimeSpec( timeSpec ); bool alreadyAddedToday = false; if ( incidence->recurs() ) { // timed incidences occur in [dtStart(), dtEnd()[ // all-day incidences occur in [dtStart(), dtEnd()] // so we subtract 1 second in the timed case const int secsToAdd = incidence->allDay() ? 0 : -1; const int eventDuration = event ? incDtStart.daysTo( incDtEnd.addSecs( secsToAdd ) ) : 0; // if there's a multiday event that starts before firstVisibleDateTime but ends after // lets include it. timesInInterval() ignores incidences that aren't totaly inside // the range const KDateTime startDateTimeWithOffset = firstVisibleDateTime.addDays( -eventDuration ); KCalCore::OccurrenceIterator rIt( *calendar(), incidence, startDateTimeWithOffset, lastVisibleDateTime ); while ( rIt.hasNext() ) { rIt.next(); const KDateTime occurrenceDate( rIt.occurrenceStartDate().toTimeSpec( timeSpec ) ); const bool makesDayBusy = preferences()->colorAgendaBusyDays() && makesWholeDayBusy( rIt.incidence() ); if ( makesDayBusy ) { KCalCore::Event::List &busyEvents = d->mBusyDays[occurrenceDate.date()]; busyEvents.append( event ); } if ( occurrenceDate.date() == today ) { alreadyAddedToday = true; } d->insertIncidence( rIt.incidence(), rIt.recurrenceId(), occurrenceDate, createSelected ); } } else { KDateTime dateToAdd; // date to add to our date list KDateTime incidenceStart; KDateTime incidenceEnd; if ( todo && todo->hasDueDate() && !todo->isOverdue() ) { // If it's not overdue it will be shown at the original date (not today) dateToAdd = todo->dtDue().toTimeSpec( timeSpec ); // To-dos are drawn with the bottom of the rectangle at dtDue // if dtDue is at 00:00, then it should be displayed in the previous day, at 23:59 if ( dateToAdd.time() == QTime( 0, 0 ) ) { dateToAdd = dateToAdd.addSecs( -1 ); } incidenceEnd = dateToAdd; } else if ( event ) { dateToAdd = incDtStart; incidenceEnd = incDtEnd; } if ( dateToAdd.isValid() && dateToAdd.isDateOnly() ) { // so comparisons with < > actually work dateToAdd.setTime( QTime( 0, 0 ) ); incidenceEnd.setTime( QTime( 23, 59, 59, 59 ) ); } if ( dateToAdd <= lastVisibleDateTime && incidenceEnd > firstVisibleDateTime ) { dateTimeList += dateToAdd; } } // ToDo items shall be displayed today if they are overdue const KDateTime dateTimeToday = KDateTime( today, timeSpec ); if ( todo && todo->isOverdue() && dateTimeToday >= firstVisibleDateTime && dateTimeToday <= lastVisibleDateTime ) { /* If there's a recurring instance showing up today don't add "today" again * we don't want the event to appear duplicated */ if ( !alreadyAddedToday ) { dateTimeList += dateTimeToday; } } const bool makesDayBusy = preferences()->colorAgendaBusyDays() && makesWholeDayBusy( incidence ); for ( t = dateTimeList.begin(); t != dateTimeList.end(); ++t ) { if ( makesDayBusy ) { KCalCore::Event::List &busyEvents = d->mBusyDays[(*t).date()]; busyEvents.append( event ); } d->insertIncidence( incidence, t->toTimeSpec( timeSpec ), t->toTimeSpec( timeSpec ), createSelected ); } // Can be multiday if ( event && makesDayBusy && event->isMultiDay() ) { const QDate lastVisibleDate = d->mSelectedDates.last(); for ( QDate date = event->dtStart().date(); date <= event->dtEnd().date() && date <= lastVisibleDate ; date = date.addDays( 1 ) ) { KCalCore::Event::List &busyEvents = d->mBusyDays[date]; busyEvents.append( event ); } } return !dateTimeList.isEmpty(); } void AgendaView::updateEventIndicatorTop( int newY ) { for ( int i = 0; i < d->mMinY.size(); ++i ) { d->mEventIndicatorTop->enableColumn( i, newY > d->mMinY[i] ); } d->mEventIndicatorTop->update(); } void AgendaView::updateEventIndicatorBottom( int newY ) { for ( int i = 0; i < d->mMaxY.size(); ++i ) { d->mEventIndicatorBottom->enableColumn( i, newY <= d->mMaxY[i] ); } d->mEventIndicatorBottom->update(); } void AgendaView::slotIncidencesDropped( const QList &items, const QPoint &gpos, bool allDay ) { Q_UNUSED( items ); Q_UNUSED( gpos ); Q_UNUSED( allDay ); #ifdef AKONADI_PORT_DISABLED // one item -> multiple items, Incidence* -> akonadi item url (we might have to fetch the items here first!) if ( gpos.x() < 0 || gpos.y() < 0 ) { return; } const QDate day = d->mSelectedDates[gpos.x()]; const QTime time = d->mAgenda->gyToTime( gpos.y() ); KDateTime newTime( day, time, preferences()->timeSpec() ); newTime.setDateOnly( allDay ); Todo::Ptr todo = CalendarSupport::todo( todoItem ); if ( todo && dynamic_cast( calendar() ) ) { const Akonadi::Item existingTodoItem = calendar()->itemForIncidence( calendar()->todo( todo->uid() ) ); if ( Todo::Ptr existingTodo = CalendarSupport::todo( existingTodoItem ) ) { kDebug() << "Drop existing Todo"; Todo::Ptr oldTodo( existingTodo->clone() ); if ( changer() ) { existingTodo->setDtDue( newTime ); existingTodo->setAllDay( allDay ); changer()->modifyIncidence( existingTodoItem, oldTodo, this ); } else { KMessageBox::sorry( this, i18n( "Unable to modify this to-do, " "because it cannot be locked." ) ); } } else { kDebug() << "Drop new Todo"; todo->setDtDue( newTime ); todo->setAllDay( allDay ); if ( !changer()->addIncidence( todo, this ) ) { KMessageBox::sorry( this, i18n( "Unable to save %1 \"%2\".", i18n( todo->type() ), todo->summary() ) ); } } } #else kDebug() << "AKONADI PORT: Disabled code in " << Q_FUNC_INFO; #endif } void AgendaView::slotIncidencesDropped( const KCalCore::Incidence::List &incidences, const QPoint &gpos, bool allDay ) { if ( gpos.x() < 0 || gpos.y() < 0 ) { return; } const QDate day = d->mSelectedDates[gpos.x()]; const QTime time = d->mAgenda->gyToTime( gpos.y() ); KDateTime newTime( day, time, preferences()->timeSpec() ); newTime.setDateOnly( allDay ); Q_FOREACH ( const KCalCore::Incidence::Ptr &incidence, incidences ) { const Akonadi::Item existingItem = calendar()->item( incidence ); const bool existsInSameCollection = existingItem.isValid() && ( existingItem.storageCollectionId() == collectionId() || collectionId() == -1 ); if ( existingItem.isValid() && existsInSameCollection ) { KCalCore::Incidence::Ptr newIncidence = existingItem.payload(); KCalCore::Incidence::Ptr oldIncidence( newIncidence->clone() ); if ( newIncidence->dtStart() == newTime && newIncidence->allDay() == allDay ) { // Nothing changed continue; } newIncidence->setAllDay( allDay ); newIncidence->setDateTime( newTime, KCalCore::Incidence::RoleDnD ); changer()->modifyIncidence( existingItem, oldIncidence, this ); } else { // Create a new one // The drop came from another application create a new incidence incidence->setDateTime( newTime, KCalCore::Incidence::RoleDnD ); incidence->setAllDay( allDay ); incidence->setUid( KCalCore::CalFormat::createUniqueId() ); Akonadi::Collection collection( collectionId() ); const bool added = -1 != changer()->createIncidence( incidence, collection, this ); if ( added ) { // TODO: make async if ( existingItem.isValid() ) { // Dragged from one agenda to another, delete origin changer()->deleteIncidence( existingItem ); } } } } } void AgendaView::startDrag(const KCalCore::Incidence::Ptr &incidence) { if ( !calendar() ) { kError() << "No Calendar set"; return; } const Akonadi::Item item = d->mViewCalendar->item(incidence); if (item.isValid()) { startDrag(item); } } void AgendaView::startDrag( const Akonadi::Item &incidence ) { if ( !calendar() ) { kError() << "No Calendar set"; return; } #ifndef KORG_NODND if ( QDrag *drag = CalendarSupport::createDrag( incidence, calendar()->timeSpec(), this ) ) { drag->exec(); } #else Q_UNUSED( incidence ); #endif } void AgendaView::readSettings() { readSettings( KGlobal::activeComponent().config().data() ); } void AgendaView::readSettings( const KConfig *config ) { const KConfigGroup group = config->group( "Views" ); const QList sizes = group.readEntry( "Separator AgendaView", QList() ); // the size depends on the number of plugins used // we don't want to read invalid/corrupted settings or else agenda becomes invisible if ( sizes.count() >= 2 && !sizes.contains( 0 ) ) { d->mSplitterAgenda->setSizes( sizes ); updateConfig(); } } void AgendaView::writeSettings( KConfig *config ) { KConfigGroup group = config->group( "Views" ); QList list = d->mSplitterAgenda->sizes(); group.writeEntry( "Separator AgendaView", list ); } QVector AgendaView::busyDayMask() const { if ( d->mSelectedDates.isEmpty() || !d->mSelectedDates[0].isValid() ) { return QVector(); } QVector busyDayMask; busyDayMask.resize( d->mSelectedDates.count() ); for ( int i = 0; i < d->mSelectedDates.count(); ++i ) { busyDayMask[i] = !d->mBusyDays[d->mSelectedDates[i]].isEmpty(); } return busyDayMask; } void AgendaView::setHolidayMasks() { if ( d->mSelectedDates.isEmpty() || !d->mSelectedDates[0].isValid() ) { return; } d->mHolidayMask.resize( d->mSelectedDates.count() + 1 ); const QList workDays = CalendarSupport::workDays( d->mSelectedDates.first().addDays( -1 ), d->mSelectedDates.last() ); for ( int i = 0; i < d->mSelectedDates.count(); ++i ) { d->mHolidayMask[i] = !workDays.contains( d->mSelectedDates[ i ] ); } // Store the information about the day before the visible area (needed for // overnight working hours) in the last bit of the mask: bool showDay = !workDays.contains( d->mSelectedDates[ 0 ].addDays( -1 ) ); d->mHolidayMask[ d->mSelectedDates.count() ] = showDay; d->mAgenda->setHolidayMask( &d->mHolidayMask ); d->mAllDayAgenda->setHolidayMask( &d->mHolidayMask ); } void AgendaView::clearSelection() { d->mAgenda->deselectItem(); d->mAllDayAgenda->deselectItem(); } void AgendaView::newTimeSpanSelectedAllDay( const QPoint &start, const QPoint &end ) { newTimeSpanSelected( start, end ); d->mTimeSpanInAllDay = true; } void AgendaView::newTimeSpanSelected( const QPoint &start, const QPoint &end ) { if ( !d->mSelectedDates.count() ) { return; } d->mTimeSpanInAllDay = false; const QDate dayStart = d-> mSelectedDates[ qBound( 0, start.x(), (int)d->mSelectedDates.size() - 1 ) ]; const QDate dayEnd = d->mSelectedDates[ qBound( 0, end.x(), (int)d->mSelectedDates.size() - 1 ) ]; const QTime timeStart = d->mAgenda->gyToTime( start.y() ); const QTime timeEnd = d->mAgenda->gyToTime( end.y() + 1 ); d->mTimeSpanBegin = QDateTime( dayStart, timeStart ); d->mTimeSpanEnd = QDateTime( dayEnd, timeEnd ); } QDateTime AgendaView::selectionStart() const { return d->mTimeSpanBegin; } QDateTime AgendaView::selectionEnd() const { return d->mTimeSpanEnd; } bool AgendaView::selectedIsAllDay() const { return d->mTimeSpanInAllDay; } void AgendaView::deleteSelectedDateTime() { d->mTimeSpanBegin.setDate( QDate() ); d->mTimeSpanEnd.setDate( QDate() ); d->mTimeSpanInAllDay = false; } void AgendaView::removeIncidence( const KCalCore::Incidence::Ptr &incidence ) { // Don't wrap this in a if ( incidence->isAllDay ) because whe all day // property might have changed d->mAllDayAgenda->removeIncidence( incidence ); d->mAgenda->removeIncidence( incidence ); - if ( !incidence->hasRecurrenceId() && d->mViewCalendar->isValid(incidence)) { - KCalCore::Incidence::List exceptions = calendar2(incidence)->instances( incidence ); + if (!incidence->hasRecurrenceId() && d->mViewCalendar->isValid(incidence->uid())) { + // Deleted incidence is an main incidence + // Delete all exceptions as well + KCalCore::Incidence::List exceptions = calendar2(incidence->uid())->instances( incidence ); foreach ( const KCalCore::Incidence::Ptr &exception, exceptions ) { if ( exception->allDay() ) { d->mAllDayAgenda->removeIncidence( exception ); } else { d->mAgenda->removeIncidence( exception ); } } } } void AgendaView::updateEventIndicators() { d->mUpdateEventIndicatorsScheduled = false; d->mMinY = d->mAgenda->minContentsY(); d->mMaxY = d->mAgenda->maxContentsY(); d->mAgenda->checkScrollBoundaries(); updateEventIndicatorTop( d->mAgenda->visibleContentsYMin() ); updateEventIndicatorBottom( d->mAgenda->visibleContentsYMax() ); } void AgendaView::setIncidenceChanger( Akonadi::IncidenceChanger *changer ) { EventView::setIncidenceChanger( changer ); d->mAgenda->setIncidenceChanger( changer ); d->mAllDayAgenda->setIncidenceChanger( changer ); } void AgendaView::clearTimeSpanSelection() { d->mAgenda->clearSelection(); d->mAllDayAgenda->clearSelection(); deleteSelectedDateTime(); } Agenda *AgendaView::agenda() const { return d->mAgenda; } Agenda *AgendaView::allDayAgenda() const { return d->mAllDayAgenda; } QSplitter *AgendaView::splitter() const { return d->mSplitterAgenda; } bool AgendaView::filterByCollectionSelection( const KCalCore::Incidence::Ptr &incidence ) { const Akonadi::Item item = d->mViewCalendar->item(incidence); if (!item.isValid()) { return true; } if ( customCollectionSelection() ) { return customCollectionSelection()->contains(item.parentCollection().id() ); } if ( collectionId() < 0 ) { return true; } else { return collectionId() == item.storageCollectionId(); } } void AgendaView::alignAgendas() { // resize dummy widget so the allday agenda lines up with the hourly agenda. d->mDummyAllDayLeft->setFixedWidth( -SPACING + d->mTimeLabelsZone->width() - ( d->mIsSideBySide ? 0 : d->mTimeBarHeaderFrame->width() ) ); // Must be async, so they are centered createDayLabels( true ); } CalendarDecoration::Decoration *AgendaView::Private::loadCalendarDecoration( const QString &name ) { const QString type = CalendarSupport::Plugin::serviceType(); const int version = CalendarSupport::Plugin::interfaceVersion(); QString constraint; if ( version >= 0 ) { constraint = QString::fromLatin1( "[X-KDE-PluginInterfaceVersion] == %1" ).arg( QString::number( version ) ); } KService::List list = KServiceTypeTrader::self()->query( type, constraint ); KService::List::ConstIterator it; for ( it = list.constBegin(); it != list.constEnd(); ++it ) { if ( (*it)->desktopEntryName() == name ) { KService::Ptr service = *it; KPluginLoader loader( *service ); KPluginFactory *factory = loader.factory(); if ( !factory ) { kDebug() << "Factory creation failed"; return 0; } CalendarDecoration::DecorationFactory *pluginFactory = static_cast( factory ); if ( !pluginFactory ) { kDebug() << "Cast failed"; return 0; } return pluginFactory->createPluginFactory(); } } return 0; } void AgendaView::setChanges( EventView::Changes changes ) { d->setChanges( changes ); } void AgendaView::scheduleUpdateEventIndicators() { if ( !d->mUpdateEventIndicatorsScheduled ) { d->mUpdateEventIndicatorsScheduled = true; QTimer::singleShot( 0, this, SLOT(updateEventIndicators()) ); } } // kate: space-indent on; indent-width 2; replace-tabs on; diff --git a/calendarviews/agenda/agendaview.h b/calendarviews/agenda/agendaview.h index 03b49f6254..c5b1a1e284 100644 --- a/calendarviews/agenda/agendaview.h +++ b/calendarviews/agenda/agendaview.h @@ -1,285 +1,286 @@ /* Copyright (c) 2000,2001,2003 Cornelius Schumacher Copyright (C) 2003-2004 Reinhold Kainhofer Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.net Author: Kevin Krammer, krake@kdab.com Author: Sergio Martins, sergio.martins@kdab.com 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. */ #ifndef EVENTVIEWS_AGENDAVIEW_H #define EVENTVIEWS_AGENDAVIEW_H #include "eventviews_export.h" #include "eventview.h" #include "viewcalendar.h" #include #include class KConfig; class KHBox; class QSplitter; namespace EventViews { #ifndef EVENTVIEWS_NODECOS namespace CalendarDecoration { class Decoration; } #endif class TimeLabels; class TimeLabelsZone; class Agenda; class AgendaItem; class AgendaView; class EVENTVIEWS_EXPORT EventIndicator : public QFrame { Q_OBJECT public: enum Location { Top, Bottom }; explicit EventIndicator( Location loc = Top, QWidget *parent = 0 ); virtual ~EventIndicator(); void changeColumns( int columns ); void enableColumn( int column, bool enable ); protected: void paintEvent( QPaintEvent *event ); bool eventFilter( QObject *, QEvent * ); private: class Private; Private *const d; }; /** AgendaView is the agenda-like view that displays events in a single or multi-day view. */ class EVENTVIEWS_EXPORT AgendaView : public EventView { Q_OBJECT public: explicit AgendaView( const PrefsPtr &preferences, const QDate &start, const QDate &end, bool isInteractive, bool isSideBySide = false, QWidget *parent = 0 ); explicit AgendaView( const QDate &start, const QDate &end, bool isInteractive, bool isSideBySide = false, QWidget *parent = 0 ); virtual ~AgendaView(); enum { MAX_DAY_COUNT = 42 // ( 6 * 7) }; /** Returns number of currently shown dates. */ virtual int currentDateCount() const; /** returns the currently selected events */ virtual Akonadi::Item::List selectedIncidences() const; /** returns the currently selected incidence's dates */ virtual KCalCore::DateList selectedIncidenceDates() const; /** return the default start/end date/time for new events */ virtual bool eventDurationHint( QDateTime &startDt, QDateTime &endDt, bool &allDay ) const; /** start-datetime of selection */ virtual QDateTime selectionStart() const; /** end-datetime of selection */ virtual QDateTime selectionEnd() const; /** returns true if selection is for whole day */ bool selectedIsAllDay() const; /** make selected start/end invalid */ void deleteSelectedDateTime(); /** returns if only a single cell is selected, or a range of cells */ bool selectedIsSingleCell() const; /* reimp from EventView */ virtual void setCalendar( const Akonadi::ETMCalendar::Ptr &cal ); virtual void addCalendar( const ViewCalendar::Ptr &cal); QSplitter *splitter() const; // FIXME: we already have startDateTime() and endDateTime() in the base class /** First shown day */ QDate startDate() const; /** Last shown day */ QDate endDate() const; /** Update event belonging to agenda item If the incidence is multi-day, item is the first one */ void updateEventDates( AgendaItem *item, bool addIncidence, Akonadi::Collection::Id collectionId ); QVector busyDayMask() const; /** * Return calendar object for an congrete incidence. * this function is able to use mutiple calenders * TODO: replace EventsView::calendar() */ virtual KCalCore::Calendar::Ptr calendar2(KCalCore::Incidence::Ptr incidence) const; + virtual KCalCore::Calendar::Ptr calendar2(const QString &incidenceIdentifier) const; public slots: virtual void updateView(); virtual void updateConfig(); virtual void showDates( const QDate &start, const QDate &end, const QDate &preferredMonth = QDate() ); virtual void showIncidences( const Akonadi::Item::List &incidenceList, const QDate &date ); void clearSelection(); void startDrag( const KCalCore::Incidence::Ptr & ); void startDrag( const Akonadi::Item & ); void readSettings(); void readSettings( const KConfig * ); void writeSettings( KConfig * ); /** reschedule the todo to the given x- and y- coordinates. Third parameter determines all-day (no time specified) */ void slotIncidencesDropped( const KCalCore::Incidence::List &incidences, const QPoint &, bool ); void slotIncidencesDropped( const QList& incidences, const QPoint &, bool ); void enableAgendaUpdate( bool enable ); void setIncidenceChanger( Akonadi::IncidenceChanger *changer ); void zoomInHorizontally( const QDate &date=QDate() ); void zoomOutHorizontally( const QDate &date=QDate() ); void zoomInVertically( ); void zoomOutVertically( ); void zoomView( const int delta, const QPoint &pos, const Qt::Orientation orient=Qt::Horizontal ); void clearTimeSpanSelection(); // Used by the timelabelszone void updateTimeBarWidth(); /** Create labels for the selected dates. */ void createDayLabels( bool force ); void createTimeBarHeaders(); void setChanges( EventView::Changes ); Q_SIGNALS: void showNewEventPopupSignal(); void showIncidencePopupSignal( Akonadi::Item, QDate ); void zoomViewHorizontally( const QDate &, int count ); void timeSpanSelectionChanged(); protected: /** Fill agenda using the current set value for the start date */ void fillAgenda(); void connectAgenda( Agenda *agenda, Agenda *otherAgenda ); /** Set the masks on the agenda widgets indicating, which days are holidays. */ void setHolidayMasks(); void removeIncidence( const KCalCore::Incidence::Ptr &inc ); virtual void resizeEvent( QResizeEvent *resizeEvent ); protected Q_SLOTS: void updateEventIndicatorTop( int newY ); void updateEventIndicatorBottom( int newY ); /** Updates data for selected timespan */ void newTimeSpanSelected( const QPoint &start, const QPoint &end ); /** Updates data for selected timespan for all day event*/ void newTimeSpanSelectedAllDay( const QPoint &start, const QPoint &end ); /** Updates the event indicators after a certain incidence was modified or removed. */ void updateEventIndicators(); void scheduleUpdateEventIndicators(); void updateDayLabelSizes(); void alignAgendas(); private slots: void slotIncidenceSelected(const KCalCore::Incidence::Ptr &incidence, QDate date); void slotShowIncidencePopup(const KCalCore::Incidence::Ptr &incidence, QDate date); void slotEditIncidence(const KCalCore::Incidence::Ptr &incidence); void slotShowIncidence(const KCalCore::Incidence::Ptr &incidence); void slotDeleteIncidence(const KCalCore::Incidence::Ptr &incidence); private: void init( const QDate &start, const QDate &end ); bool filterByCollectionSelection( const KCalCore::Incidence::Ptr &incidence ); void setupTimeLabel( TimeLabels *timeLabel ); bool displayIncidence( const KCalCore::Incidence::Ptr &incidence, bool createSelected ); #ifndef EVENTVIEWS_NODECOS typedef QList DecorationList; bool loadDecorations( const QStringList &decorations, DecorationList &decoList ); void placeDecorationsFrame( KHBox *frame, bool decorationsFound, bool isTop ); void placeDecorations( DecorationList &decoList, const QDate &date, KHBox *labelBox, bool forWeek ); #endif friend class TimeLabelsZone; friend class MultiAgendaView; Agenda *agenda() const; Agenda *allDayAgenda() const; private: class Private; Private *const d; }; } #endif // kate: space-indent on; indent-width 2; replace-tabs on;