Task #1170 Progress bar for meeting details pop-up
[qtmeetings] / src / UserInterface / Components / ScheduleWidget.cpp
1 #include "ScheduleWidget.h"
2
3 #include <QTableWidget>
4 #include <QHeaderView>
5 #include <QVBoxLayout>
6 #include <QTime>
7 #include <QtDebug>
8 #include <QResizeEvent>
9 #include <QPainter>
10 #include "Meeting.h"
11
12 const QColor ScheduleWidget::sFreeBackground = QColor( 192, 238, 189 );
13 const QColor ScheduleWidget::sBusyBackground = QColor( 238, 147, 17 );
14 const QColor ScheduleWidget::sHeaderBackground = QColor( Qt::white );
15 const QColor ScheduleWidget::sDayHighlightColor = QColor( 255, 235, 160 );
16 const QColor ScheduleWidget::sTimeHighlightColor = QColor( Qt::blue );
17 const QColor ScheduleWidget::sMainGridColor = QColor( 140, 140, 140 );
18 const QColor ScheduleWidget::sHalfGridColor = QColor( 195, 195, 195 );
19 const QColor ScheduleWidget::sFrameColor = QColor( Qt::black );
20
21 ScheduleTableWidget::ScheduleTableWidget( int aRows, int aColumns, QWidget *aParent ) :
22                 QTableWidget( aRows, aColumns, aParent )
23 {
24         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
25
26         iMeetingsByDay = new QList<MeetingContainer>[schedule->weekLengthAsDays()];
27         iTabletBlocked = false;
28         iTime.start();
29
30         setFocusPolicy( Qt::NoFocus );
31         setFrameStyle( QFrame::NoFrame );
32 }
33
34 ScheduleTableWidget::~ScheduleTableWidget()
35 {
36         delete[] iMeetingsByDay;
37 }
38
39 void ScheduleTableWidget::paintEvent( QPaintEvent* aEvent )
40 {
41         QTableWidget::paintEvent( aEvent );
42
43         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
44         QPainter painter( viewport() );
45         int rowHeight = rowViewportPosition( 2 ) - rowViewportPosition( 1 ) - 1;
46         int columnWidth = columnViewportPosition( 2 ) - columnViewportPosition( 1 ) - 1;
47
48         // draw frame around the table
49         QRect viewportRect = viewport()->rect();
50         viewportRect.adjust( 0, 0, -1, -1 );
51         painter.setPen( ScheduleWidget::sFrameColor );
52         painter.drawRect( viewportRect );
53
54         // draw horizontal half grid
55         for ( int i = 1; i < rowCount(); ++i )
56         {
57                 int x = columnViewportPosition( 1 );
58                 int y = rowViewportPosition( i ) + ( rowHeight / 2 ) - 1;
59                 painter.fillRect( QRect( x, y, width() - x - 1, 1 ), ScheduleWidget::sHalfGridColor );
60         }
61
62         // draw horizontal main grid
63         for ( int i = 1; i < rowCount(); ++i )
64         {
65                 int y = rowViewportPosition( i ) - 1;
66                 painter.fillRect( QRect( 1, y, width() - 2, 1 ), ScheduleWidget::sMainGridColor );
67         }
68
69         // draw vertical main grid
70         for ( int i = 1; i < columnCount(); ++i )
71         {
72                 int x = columnViewportPosition( i ) - 1;
73                 painter.fillRect( QRect( x, 1, 1, height() - 2 ), ScheduleWidget::sMainGridColor );
74         }
75
76         // draw current day highlight
77         QPen pen( ScheduleWidget::sDayHighlightColor );
78         pen.setWidth( 3 );
79         painter.setPen( pen );
80         painter.setBrush( Qt::NoBrush );
81
82         for ( int i = 1; i < columnCount(); ++i )
83         {
84                 if ( schedule->iCurrentDateTime.date() == schedule->iShownDate.addDays( i - 1 ) )
85                 {
86                         int x = columnViewportPosition( i ) + 1;
87                         int y = 2;
88                         int w = columnWidth - 3;
89                         int h = height() - 5;
90                         painter.drawRect( x, y, w, h );
91                         break;
92                 }
93         }
94
95         // draw meetings
96         QBrush brush( ScheduleWidget::sBusyBackground );
97         painter.setBrush( brush );
98         painter.setRenderHint( QPainter::Antialiasing );
99         painter.setPen( ScheduleWidget::sFrameColor );
100         populateMeetingList();
101
102         for ( int day = 0; day < schedule->weekLengthAsDays(); ++day )
103         {
104                 for ( int i = 0; i < iMeetingsByDay[day].size(); ++i )
105                 {
106                         painter.drawRoundRect( iMeetingsByDay[day][i].rect, 20, 20 );
107                 }
108         }
109
110         // draw current time highlight
111         painter.setBrush( Qt::NoBrush );
112         painter.setRenderHint( QPainter::Antialiasing, false );
113
114         for ( int i = 1; i < columnCount(); ++i )
115         {
116                 if ( schedule->iCurrentDateTime.date() == schedule->iShownDate.addDays( i - 1 ) )
117                 {
118                         int x = columnViewportPosition( i ) - 1;
119                         int y = computeViewportY( schedule->iCurrentDateTime.time() );
120                         int w = columnWidth + 2;
121                         int h = 4;
122                         painter.fillRect( x, y, w, h, ScheduleWidget::sTimeHighlightColor );
123                         break;
124                 }
125         }
126 }
127
128 void ScheduleTableWidget::tabletEvent( QTabletEvent* aEvent )
129 {
130         int ms = iTime.restart();
131
132         if ( iTabletBlocked && ms > 1000 )
133         {
134                 iTabletBlocked = false;
135                 qDebug() << "Tablet block released";
136         }
137
138         if ( iTabletBlocked == false )
139         {
140                 qDebug() << "Tablet blocked released";
141                 activateMeeting( aEvent->x(), aEvent->y() );
142         }
143 }
144
145 void ScheduleTableWidget::mouseMoveEvent( QMouseEvent* /* aEvent */ )
146 {
147         // this is overridden as empty method because otherwise
148         // unwanted behaviour would occur due to QTableWidget
149 }
150
151 void ScheduleTableWidget::mousePressEvent( QMouseEvent* aEvent )
152 {
153         activateMeeting( aEvent->x(), aEvent->y() );
154         iTabletBlocked = false;
155 }
156
157 void ScheduleTableWidget::populateMeetingList()
158 {
159         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
160
161         for ( int i = 0; i < schedule->weekLengthAsDays(); ++i )
162                 iMeetingsByDay[i].clear();
163
164         // insert suitable meetings to list
165         for ( int i = 0; i < schedule->iMeetings.count(); ++i )
166         {
167                 Meeting* meeting = schedule->iMeetings[i];
168                 int day = meeting->startsAt().date().dayOfWeek() - 1;
169                 if (( meeting->startsAt().date().weekNumber() == schedule->iShownDate.weekNumber() ) &&
170                           ( day < schedule->weekLengthAsDays() ) &&
171                           ( meeting->endsAt().time() > QTime( schedule->iStartHour, 0 ) ) &&
172                           ( meeting->startsAt().time() <= QTime( schedule->iStartHour + schedule->iNumberOfHours - 1,  59 ) ) )
173                 {
174                         MeetingContainer container;
175                         container.meeting = meeting;
176                         container.rect = QRect( 0, 0, 0, 0 );
177                         container.rectComputed = false;
178                         iMeetingsByDay[day].append( container );
179                 }
180         }
181
182         // compute meeting rectangles
183         for ( int day = 0; day < schedule->weekLengthAsDays(); ++day )
184         {
185                 for ( int i = 0; i < iMeetingsByDay[day].size(); ++i )
186                 {
187                         if ( iMeetingsByDay[day][i].rectComputed )
188                                 continue;
189
190                         QList<int> meetingIndices;
191                         findOverlappingMeetings( day, iMeetingsByDay[day][i].meeting, meetingIndices );
192                         meetingIndices.append( i );
193
194                         for ( int j = 0; j < meetingIndices.size(); ++j )
195                         {
196                                 if ( iMeetingsByDay[day][meetingIndices[j]].rectComputed )
197                                         continue;
198
199                                 int columnWidth = columnViewportPosition( 2 ) - columnViewportPosition( 1 ) - 1;
200
201                                 Meeting* meeting = iMeetingsByDay[day][meetingIndices[j]].meeting;
202                                 int x = columnViewportPosition( day + 1 ) + ( int )(( columnWidth / ( float )meetingIndices.size() ) * j );
203                                 int y = computeViewportY( meeting->startsAt().time() );
204                                 int width = ( int )( columnWidth / ( float )meetingIndices.size() + 0.5f );
205                                 int height = computeViewportY( meeting->endsAt().time() ) - y;
206
207                                 iMeetingsByDay[day][meetingIndices[j]].rect = QRect( x, y, width, height );
208                                 iMeetingsByDay[day][meetingIndices[j]].rectComputed = true;
209                         }
210                 }
211         }
212 }
213
214 bool ScheduleTableWidget::findOverlappingMeetings( int aDay, Meeting* aMeeting, QList<int>& aResult )
215 {
216         QSet<int> overlapSet;
217
218         // first find meetings that overlap with aMeeting
219         for ( int i = 0; i < iMeetingsByDay[aDay].size(); ++i )
220         {
221                 Meeting* other = iMeetingsByDay[aDay][i].meeting;
222                 if ( aMeeting != other && aMeeting->overlaps( *(other) ) )
223                         overlapSet.insert( i );
224         }
225
226         // then compare overlappiong ones against every meeting to make sure that
227         // the returned set can be used to compute rectangles for all cases at once
228         foreach( int index, overlapSet )
229         {
230                 Meeting* meetingInSet = iMeetingsByDay[aDay][index].meeting;
231                 for ( int i = 0; i < iMeetingsByDay[aDay].size(); ++i )
232                 {
233                         Meeting* other = iMeetingsByDay[aDay][i].meeting;
234                         if ( meetingInSet != other && aMeeting != other && meetingInSet->overlaps( *(other) ) )
235                                 overlapSet.insert( i );
236                 }
237         }
238
239         aResult.clear();
240         foreach( int index, overlapSet )
241         {
242                 aResult.append( index );
243         }
244
245         return !aResult.empty();
246 }
247
248 void ScheduleTableWidget::activateMeeting( int x, int y )
249 {
250         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
251
252         for ( int day = 0; day < schedule->weekLengthAsDays(); ++day )
253         {
254                 for ( int i = 0; i < iMeetingsByDay[day].size(); ++i )
255                 {
256                         if ( iMeetingsByDay[day][i].rect.contains( x, y ) && !iTabletBlocked )
257                         {
258                                 iTabletBlocked = true;
259                                 qDebug() << "Activated meeting at x" << x << "y" << y << ":" << iMeetingsByDay[day][i].meeting->toString();
260                                 emit schedule->meetingActivated( iMeetingsByDay[day][i].meeting );
261                         }
262                 }
263         }
264 }
265
266 int ScheduleTableWidget::computeViewportY( QTime aTime )
267 {
268         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
269         int secondsInDisplayDay = schedule->iNumberOfHours * 60 * 60;
270         int mainY = rowViewportPosition( 1 ) + 1;
271         int mainHeight = height() - mainY - 1;
272
273         return mainY + ( int )(( QTime( schedule->iStartHour, 0 ).secsTo( aTime ) / ( float )secondsInDisplayDay ) * mainHeight );
274 }
275
276 ScheduleWidget::ScheduleWidget( QDateTime aCurrentDateTime, DisplaySettings *aSettings, QWidget *aParent ) :
277                 ObservedWidget( aParent ),
278                 iCurrentDateTime( aCurrentDateTime ),
279                 iStartHour( aSettings->dayStartsAt().hour() ),
280                 iNumberOfHours( aSettings->dayEndsAt().hour() - aSettings->dayStartsAt().hour() + 1 ),
281                 iDaysInSchedule( aSettings->daysInSchedule() ),
282                 iLastRefresh( aCurrentDateTime.time() )
283 {
284         iStartHour = qBound( 0, iStartHour, 23 );
285         iNumberOfHours = qBound( 1, iNumberOfHours, 24 - iStartHour );
286
287         iScheduleTable = new ScheduleTableWidget(( iNumberOfHours + 1 ) * 1, weekLengthAsDays() + 1, this );
288         iScheduleTable->horizontalHeader()->hide();
289         iScheduleTable->verticalHeader()->hide();
290         iScheduleTable->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
291         iScheduleTable->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
292         iScheduleTable->setShowGrid( false );
293
294         QFont font;
295         font.setPointSize( 10 );
296
297         // add empty item to top-left corner, this will be updated in refresh()
298         QTableWidgetItem *weekItem = new QTableWidgetItem();
299         weekItem->setTextAlignment( Qt::AlignCenter );
300         weekItem->setFlags( Qt::ItemIsEnabled );
301         weekItem->setBackgroundColor( sHeaderBackground );
302         weekItem->setFont( font );
303         iScheduleTable->setItem( 0, 0, weekItem );
304
305         // add empty item to main cell
306         QTableWidgetItem *mainItem = new QTableWidgetItem();
307         mainItem->setFlags( Qt::ItemIsEnabled );
308         mainItem->setBackgroundColor( sFreeBackground );
309         iScheduleTable->setItem( 1, 1, mainItem );
310
311         // set row header items
312         QTime time( iStartHour, 0 );
313         for ( int i = 1; i < iScheduleTable->rowCount(); ++i )
314         {
315                 QTableWidgetItem *item = new QTableWidgetItem( time.toString( "HH:mm" ) );
316                 item->setTextAlignment( Qt::AlignHCenter );
317                 item->setFlags( Qt::ItemIsEnabled );
318                 item->setBackgroundColor( sHeaderBackground );
319                 item->setFont( font );
320                 iScheduleTable->setItem( i, 0, item );
321                 time = time.addSecs( 60 * 60 );
322         }
323
324         // set empty column header items, these will be updated in refresh()
325         for ( int i = 1; i < iScheduleTable->columnCount(); ++i )
326         {
327                 QTableWidgetItem *item = new QTableWidgetItem();
328                 item->setTextAlignment( Qt::AlignCenter );
329                 item->setFlags( Qt::ItemIsEnabled );
330                 item->setBackgroundColor( sHeaderBackground );
331                 item->setFont( font );
332                 iScheduleTable->setItem( 0, i, item );
333         }
334
335         QVBoxLayout *layout = new QVBoxLayout;
336         layout->addWidget( iScheduleTable );
337         layout->setAlignment( Qt::AlignCenter );
338         layout->setMargin( 0 );
339         setLayout( layout );
340
341         showCurrentWeek();
342 }
343
344 ScheduleWidget::~ScheduleWidget()
345 {
346         clear();
347         delete iScheduleTable;
348 }
349
350 int     ScheduleWidget::shownWeek()
351 {
352         return iShownDate.weekNumber();
353 }
354
355 QDate ScheduleWidget::beginningOfShownWeek()
356 {
357         return iShownDate.addDays( -1 * iShownDate.dayOfWeek() + 1 );
358 }
359
360 Meeting* ScheduleWidget::currentMeeting()
361 {
362         return meeting( iCurrentDateTime );
363 }
364
365 Meeting* ScheduleWidget::meeting( QDateTime aAt )
366 {
367         for ( int i = 0; i < iMeetings.count(); ++i )
368         {
369                 if ( iMeetings[i]->startsAt() <= aAt && iMeetings[i]->endsAt() >= aAt )
370                 {
371                         return iMeetings[i];
372                 }
373         }
374
375         return 0;
376 }
377
378 void ScheduleWidget::clear()
379 {
380         qDebug() << "ScheduleWidget::clear";
381         int i = 0;
382         while ( !iMeetings.isEmpty() )
383         {
384                 i++;
385                 iMeetings.removeFirst();
386         }
387         qDebug() << "Deleted " << i << " items";
388 }
389
390 void ScheduleWidget::clear( QDateTime aFrom, QDateTime aUntil )
391 {
392         for ( int i = 0; i < iMeetings.count(); ++i )
393         {
394                 if (( iMeetings[i]->startsAt() >= aFrom && iMeetings[i]->startsAt() <= aUntil ) ||
395                           ( iMeetings[i]->startsAt() <= aFrom && iMeetings[i]->endsAt() >= aFrom ) )
396                 {
397                         iMeetings.removeAt( i );
398                         --i;
399                 }
400         }
401 }
402
403 void ScheduleWidget::refresh()
404 {
405         for ( int i = 0; i < iScheduleTable->columnCount(); ++i )
406         {
407                 QTableWidgetItem* item = iScheduleTable->item( 0, i );
408                 QFont font = item->font();
409                 if ( i == 0 ) {
410                         item->setText( tr( "Wk %1" ).arg( iShownDate.weekNumber() ) );
411                         continue;
412                 }
413                 item->setText( iShownDate.addDays( i - 1 ).toString( tr( "ddd d MMM" ) ) );
414
415                 if ( iCurrentDateTime.date() == iShownDate.addDays( i - 1 ) )
416                 {
417                         // mark current day
418                         item->setBackgroundColor( sDayHighlightColor );
419                         font.setItalic( true );
420                         item->setFont( font );
421                 }
422                 else
423                 {
424                         item->setBackgroundColor( sHeaderBackground );
425                         font.setItalic( false );
426                         item->setFont( font );
427                 }
428         }
429
430         // force repaint of the main area
431         iScheduleTable->setSpan( 1, 1, iNumberOfHours, weekLengthAsDays() );
432
433         iLastRefresh = iCurrentDateTime.time();
434 }
435
436 void ScheduleWidget::setCurrentDateTime( QDateTime aCurrentDateTime )
437 {
438         Meeting* previous = meeting( iCurrentDateTime );
439         Meeting* current = meeting( aCurrentDateTime );
440         iCurrentDateTime = aCurrentDateTime;
441
442         if ( iLastRefresh.secsTo( iCurrentDateTime.time() ) > sRefreshIntervalInSeconds )
443         {
444                 // enough time has elapsed since last refresh
445                 refresh();
446         }
447
448         if ( previous != current )
449         {
450                 emit currentMeetingChanged( current );
451         }
452 }
453
454 void ScheduleWidget::insertMeeting( Meeting *aMeeting )
455 {
456         Meeting* previous = meeting( iCurrentDateTime );
457         iMeetings.append( aMeeting );
458         Meeting* current = meeting( iCurrentDateTime );
459
460         qDebug() << "Inserted" << aMeeting->toString();
461
462         refresh();
463
464         if ( previous != current )
465         {
466                 emit currentMeetingChanged( current );
467         }
468 }
469
470 void ScheduleWidget::removeMeeting( Meeting *aMeeting )
471 {
472         Meeting* previous = meeting( iCurrentDateTime );
473
474         qDebug() << "Delete" << aMeeting->toString();
475         for ( int i = 0; i < iMeetings.count(); ++i )
476         {
477                 if ( iMeetings[i]->equals( *(aMeeting) ) )
478                 {
479                         iMeetings.removeAt( i );
480
481                         refresh();
482
483                         Meeting* current = meeting( iCurrentDateTime );
484                         if ( previous != current )
485                                 emit currentMeetingChanged( current );
486
487                         return;
488                 }
489         }
490 }
491
492 //void ScheduleWidget::updateMeeting( Meeting *aMeeting )
493 //{
494 //
495 //}
496
497 void ScheduleWidget::showPreviousWeek()
498 {
499         iShownDate = iShownDate.addDays( -7 );
500         refresh();
501         emit shownWeekChanged( iShownDate );
502 }
503
504 void ScheduleWidget::showCurrentWeek()
505 {
506         iShownDate = iCurrentDateTime.date();
507
508         // set weekday to monday
509         iShownDate = iShownDate.addDays( -( iShownDate.dayOfWeek() - 1 ) );
510
511         refresh();
512         emit shownWeekChanged( iShownDate );
513 }
514
515 void ScheduleWidget::showNextWeek()
516 {
517         iShownDate = iShownDate.addDays( 7 );
518         refresh();
519         emit shownWeekChanged( iShownDate );
520 }
521
522 int ScheduleWidget::computeHeaderRow( QTime aTime )
523 {
524         // map given time to correct header row in the schedule table
525         return aTime.hour() - ( iStartHour - 1 );
526 }
527
528 int ScheduleWidget::weekLengthAsDays()
529 {
530         return ( iDaysInSchedule == DisplaySettings::WholeWeekInSchedule ) ? 7 : 5;
531 }
532
533 void ScheduleWidget::resizeEvent( QResizeEvent* /* aEvent */ )
534 {
535         QRect rect = iScheduleTable->contentsRect();
536         int rowHeight = ( int )( rect.height() / ( float )iScheduleTable->rowCount() );
537         int headerRowHeight = rowHeight;
538         int columnWidth = ( int )( rect.width() / ( iScheduleTable->columnCount() - 0.5f ) );
539         int headerColumnWidth = columnWidth / 2;
540
541         iScheduleTable->setRowHeight( 0, headerRowHeight );
542         for ( int i = 1; i < iScheduleTable->rowCount(); ++i )
543         {
544                 iScheduleTable->setRowHeight( i, rowHeight );
545         }
546
547         iScheduleTable->setColumnWidth( 0, headerColumnWidth );
548         for ( int i = 1; i < iScheduleTable->columnCount(); ++i )
549         {
550                 iScheduleTable->setColumnWidth( i, columnWidth );
551         }
552
553         // resize table so that frame size matches exactly
554         int leftMargin = 0, topMargin = 0, rightMargin = 0, bottomMargin = 0;
555         iScheduleTable->getContentsMargins( &leftMargin, &topMargin, &rightMargin, &bottomMargin );
556         iScheduleTable->resize( columnWidth * iScheduleTable->columnCount() - headerColumnWidth + leftMargin + rightMargin,
557                                         rowHeight * iScheduleTable->rowCount() + topMargin + bottomMargin );
558 }