ViewBase added and major changes to use the new architecture
[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 }
155
156 void ScheduleTableWidget::populateMeetingList()
157 {
158         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
159
160         for ( int i = 0; i < schedule->weekLengthAsDays(); ++i )
161                 iMeetingsByDay[i].clear();
162
163         // insert suitable meetings to list
164         for ( int i = 0; i < schedule->iMeetings.count(); ++i )
165         {
166                 Meeting* meeting = schedule->iMeetings[i];
167                 int day = meeting->startsAt().date().dayOfWeek() - 1;
168                 if (( meeting->startsAt().date().weekNumber() == schedule->iShownDate.weekNumber() ) &&
169                           ( day < schedule->weekLengthAsDays() ) &&
170                           ( meeting->endsAt().time() > QTime( schedule->iStartHour, 0 ) ) &&
171                           ( meeting->startsAt().time() <= QTime( schedule->iStartHour + schedule->iNumberOfHours - 1,  59 ) ) )
172                 {
173                         MeetingContainer container;
174                         container.meeting = meeting;
175                         container.rect = QRect( 0, 0, 0, 0 );
176                         container.rectComputed = false;
177                         iMeetingsByDay[day].append( container );
178                 }
179         }
180
181         // compute meeting rectangles
182         for ( int day = 0; day < schedule->weekLengthAsDays(); ++day )
183         {
184                 for ( int i = 0; i < iMeetingsByDay[day].size(); ++i )
185                 {
186                         if ( iMeetingsByDay[day][i].rectComputed )
187                                 continue;
188
189                         QList<int> meetingIndices;
190                         findOverlappingMeetings( day, iMeetingsByDay[day][i].meeting, meetingIndices );
191                         meetingIndices.append( i );
192
193                         for ( int j = 0; j < meetingIndices.size(); ++j )
194                         {
195                                 if ( iMeetingsByDay[day][meetingIndices[j]].rectComputed )
196                                         continue;
197
198                                 int columnWidth = columnViewportPosition( 2 ) - columnViewportPosition( 1 ) - 1;
199
200                                 Meeting* meeting = iMeetingsByDay[day][meetingIndices[j]].meeting;
201                                 int x = columnViewportPosition( day + 1 ) + ( int )(( columnWidth / ( float )meetingIndices.size() ) * j );
202                                 int y = computeViewportY( meeting->startsAt().time() );
203                                 int width = ( int )( columnWidth / ( float )meetingIndices.size() + 0.5f );
204                                 int height = computeViewportY( meeting->endsAt().time() ) - y;
205
206                                 iMeetingsByDay[day][meetingIndices[j]].rect = QRect( x, y, width, height );
207                                 iMeetingsByDay[day][meetingIndices[j]].rectComputed = true;
208                         }
209                 }
210         }
211 }
212
213 bool ScheduleTableWidget::findOverlappingMeetings( int aDay, Meeting* aMeeting, QList<int>& aResult )
214 {
215         QSet<int> overlapSet;
216
217         // first find meetings that overlap with aMeeting
218         for ( int i = 0; i < iMeetingsByDay[aDay].size(); ++i )
219         {
220                 Meeting* other = iMeetingsByDay[aDay][i].meeting;
221                 if ( aMeeting != other && aMeeting->overlaps( *(other) ) )
222                         overlapSet.insert( i );
223         }
224
225         // then compare overlappiong ones against every meeting to make sure that
226         // the returned set can be used to compute rectangles for all cases at once
227         foreach( int index, overlapSet )
228         {
229                 Meeting* meetingInSet = iMeetingsByDay[aDay][index].meeting;
230                 for ( int i = 0; i < iMeetingsByDay[aDay].size(); ++i )
231                 {
232                         Meeting* other = iMeetingsByDay[aDay][i].meeting;
233                         if ( meetingInSet != other && aMeeting != other && meetingInSet->overlaps( *(other) ) )
234                                 overlapSet.insert( i );
235                 }
236         }
237
238         aResult.clear();
239         foreach( int index, overlapSet )
240         {
241                 aResult.append( index );
242         }
243
244         return !aResult.empty();
245 }
246
247 void ScheduleTableWidget::activateMeeting( int x, int y )
248 {
249         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
250
251         for ( int day = 0; day < schedule->weekLengthAsDays(); ++day )
252         {
253                 for ( int i = 0; i < iMeetingsByDay[day].size(); ++i )
254                 {
255                         if ( iMeetingsByDay[day][i].rect.contains( x, y ) && !iTabletBlocked )
256                         {
257                                 iTabletBlocked = true;
258                                 qDebug() << "Activated meeting at x" << x << "y" << y << ":" << iMeetingsByDay[day][i].meeting->toString();
259                                 emit schedule->meetingActivated( iMeetingsByDay[day][i].meeting );
260                         }
261                 }
262         }
263 }
264
265 int ScheduleTableWidget::computeViewportY( QTime aTime )
266 {
267         ScheduleWidget* schedule = static_cast<ScheduleWidget*>( parent() );
268         int secondsInDisplayDay = schedule->iNumberOfHours * 60 * 60;
269         int mainY = rowViewportPosition( 1 ) + 1;
270         int mainHeight = height() - mainY - 1;
271
272         return mainY + ( int )(( QTime( schedule->iStartHour, 0 ).secsTo( aTime ) / ( float )secondsInDisplayDay ) * mainHeight );
273 }
274
275 ScheduleWidget::ScheduleWidget( QDateTime aCurrentDateTime, DisplaySettings *aSettings, QWidget *aParent ) :
276                 ObservedWidget( aParent ),
277                 iCurrentDateTime( aCurrentDateTime ),
278                 iStartHour( aSettings->dayStartsAt().hour() ),
279                 iNumberOfHours( aSettings->dayEndsAt().hour() - aSettings->dayStartsAt().hour() + 1 ),
280                 iDaysInSchedule( aSettings->daysInSchedule() ),
281                 iLastRefresh( aCurrentDateTime.time() )
282 {
283         iStartHour = qBound( 0, iStartHour, 23 );
284         iNumberOfHours = qBound( 1, iNumberOfHours, 24 - iStartHour );
285
286         iScheduleTable = new ScheduleTableWidget(( iNumberOfHours + 1 ) * 1, weekLengthAsDays() + 1, this );
287         iScheduleTable->horizontalHeader()->hide();
288         iScheduleTable->verticalHeader()->hide();
289         iScheduleTable->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
290         iScheduleTable->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
291         iScheduleTable->setShowGrid( false );
292
293         QFont font;
294         font.setPointSize( 10 );
295
296         // add empty item to top-left corner, this will be updated in refresh()
297         QTableWidgetItem *weekItem = new QTableWidgetItem();
298         weekItem->setTextAlignment( Qt::AlignCenter );
299         weekItem->setFlags( Qt::ItemIsEnabled );
300         weekItem->setBackgroundColor( sHeaderBackground );
301         weekItem->setFont( font );
302         iScheduleTable->setItem( 0, 0, weekItem );
303
304         // add empty item to main cell
305         QTableWidgetItem *mainItem = new QTableWidgetItem();
306         mainItem->setFlags( Qt::ItemIsEnabled );
307         mainItem->setBackgroundColor( sFreeBackground );
308         iScheduleTable->setItem( 1, 1, mainItem );
309
310         // set row header items
311         QTime time( iStartHour, 0 );
312         for ( int i = 1; i < iScheduleTable->rowCount(); ++i )
313         {
314                 QTableWidgetItem *item = new QTableWidgetItem( time.toString( "HH:mm" ) );
315                 item->setTextAlignment( Qt::AlignHCenter );
316                 item->setFlags( Qt::ItemIsEnabled );
317                 item->setBackgroundColor( sHeaderBackground );
318                 item->setFont( font );
319                 iScheduleTable->setItem( i, 0, item );
320                 time = time.addSecs( 60 * 60 );
321         }
322
323         // set empty column header items, these will be updated in refresh()
324         for ( int i = 1; i < iScheduleTable->columnCount(); ++i )
325         {
326                 QTableWidgetItem *item = new QTableWidgetItem();
327                 item->setTextAlignment( Qt::AlignCenter );
328                 item->setFlags( Qt::ItemIsEnabled );
329                 item->setBackgroundColor( sHeaderBackground );
330                 item->setFont( font );
331                 iScheduleTable->setItem( 0, i, item );
332         }
333
334         QVBoxLayout *layout = new QVBoxLayout;
335         layout->addWidget( iScheduleTable );
336         layout->setAlignment( Qt::AlignCenter );
337         layout->setMargin( 0 );
338         setLayout( layout );
339
340         showCurrentWeek();
341 }
342
343 ScheduleWidget::~ScheduleWidget()
344 {
345         clear();
346         delete iScheduleTable;
347 }
348
349 int     ScheduleWidget::shownWeek()
350 {
351         return iShownDate.weekNumber();
352 }
353
354 QDate ScheduleWidget::beginningOfShownWeek()
355 {
356         return iShownDate.addDays( -1 * iShownDate.dayOfWeek() + 1 );
357 }
358
359 Meeting* ScheduleWidget::currentMeeting()
360 {
361         return meeting( iCurrentDateTime );
362 }
363
364 Meeting* ScheduleWidget::meeting( QDateTime aAt )
365 {
366         for ( int i = 0; i < iMeetings.count(); ++i )
367         {
368                 if ( iMeetings[i]->startsAt() <= aAt && iMeetings[i]->endsAt() >= aAt )
369                 {
370                         return iMeetings[i];
371                 }
372         }
373
374         return 0;
375 }
376
377 void ScheduleWidget::clear()
378 {
379         qDebug() << "ScheduleWidget::clear";
380         int i = 0;
381         while ( !iMeetings.isEmpty() )
382         {
383                 i++;
384                 iMeetings.removeFirst();
385         }
386         qDebug() << "Deleted " << i << " items";
387 }
388
389 void ScheduleWidget::clear( QDateTime aFrom, QDateTime aUntil )
390 {
391         for ( int i = 0; i < iMeetings.count(); ++i )
392         {
393                 if (( iMeetings[i]->startsAt() >= aFrom && iMeetings[i]->startsAt() <= aUntil ) ||
394                           ( iMeetings[i]->startsAt() <= aFrom && iMeetings[i]->endsAt() >= aFrom ) )
395                 {
396                         iMeetings.removeAt( i );
397                         --i;
398                 }
399         }
400 }
401
402 void ScheduleWidget::refresh()
403 {
404         for ( int i = 0; i < iScheduleTable->columnCount(); ++i )
405         {
406                 QTableWidgetItem* item = iScheduleTable->item( 0, i );
407                 QFont font = item->font();
408                 if ( i == 0 ) {
409                         item->setText( tr( "Wk %1" ).arg( iShownDate.weekNumber() ) );
410                         continue;
411                 }
412                 item->setText( iShownDate.addDays( i - 1 ).toString( tr( "ddd d MMM" ) ) );
413
414                 if ( iCurrentDateTime.date() == iShownDate.addDays( i - 1 ) )
415                 {
416                         // mark current day
417                         item->setBackgroundColor( sDayHighlightColor );
418                         font.setItalic( true );
419                         item->setFont( font );
420                 }
421                 else
422                 {
423                         item->setBackgroundColor( sHeaderBackground );
424                         font.setItalic( false );
425                         item->setFont( font );
426                 }
427         }
428
429         // force repaint of the main area
430         iScheduleTable->setSpan( 1, 1, iNumberOfHours, weekLengthAsDays() );
431
432         iLastRefresh = iCurrentDateTime.time();
433 }
434
435 void ScheduleWidget::setCurrentDateTime( QDateTime aCurrentDateTime )
436 {
437         Meeting* previous = meeting( iCurrentDateTime );
438         Meeting* current = meeting( aCurrentDateTime );
439         iCurrentDateTime = aCurrentDateTime;
440
441         if ( iLastRefresh.secsTo( iCurrentDateTime.time() ) > sRefreshIntervalInSeconds )
442         {
443                 // enough time has elapsed since last refresh
444                 refresh();
445         }
446
447         if ( previous != current )
448         {
449                 emit currentMeetingChanged( current );
450         }
451 }
452
453 void ScheduleWidget::insertMeeting( Meeting *aMeeting )
454 {
455         Meeting* previous = meeting( iCurrentDateTime );
456         iMeetings.append( aMeeting );
457         Meeting* current = meeting( iCurrentDateTime );
458
459         qDebug() << "Inserted" << aMeeting->toString();
460
461         refresh();
462
463         if ( previous != current )
464         {
465                 emit currentMeetingChanged( current );
466         }
467 }
468
469 void ScheduleWidget::removeMeeting( Meeting *aMeeting )
470 {
471         Meeting* previous = meeting( iCurrentDateTime );
472
473         qDebug() << "Delete" << aMeeting->toString();
474         for ( int i = 0; i < iMeetings.count(); ++i )
475         {
476                 if ( iMeetings[i]->equals( *(aMeeting) ) )
477                 {
478                         iMeetings.removeAt( i );
479
480                         refresh();
481
482                         Meeting* current = meeting( iCurrentDateTime );
483                         if ( previous != current )
484                                 emit currentMeetingChanged( current );
485
486                         return;
487                 }
488         }
489 }
490
491 //void ScheduleWidget::updateMeeting( Meeting *aMeeting )
492 //{
493 //
494 //}
495
496 void ScheduleWidget::showPreviousWeek()
497 {
498         iShownDate = iShownDate.addDays( -7 );
499         refresh();
500         emit shownWeekChanged( iShownDate );
501 }
502
503 void ScheduleWidget::showCurrentWeek()
504 {
505         iShownDate = iCurrentDateTime.date();
506
507         // set weekday to monday
508         iShownDate = iShownDate.addDays( -( iShownDate.dayOfWeek() - 1 ) );
509
510         refresh();
511         emit shownWeekChanged( iShownDate );
512 }
513
514 void ScheduleWidget::showNextWeek()
515 {
516         iShownDate = iShownDate.addDays( 7 );
517         refresh();
518         emit shownWeekChanged( iShownDate );
519 }
520
521 int ScheduleWidget::computeHeaderRow( QTime aTime )
522 {
523         // map given time to correct header row in the schedule table
524         return aTime.hour() - ( iStartHour - 1 );
525 }
526
527 int ScheduleWidget::weekLengthAsDays()
528 {
529         return ( iDaysInSchedule == DisplaySettings::WholeWeekInSchedule ) ? 7 : 5;
530 }
531
532 void ScheduleWidget::resizeEvent( QResizeEvent* /* aEvent */ )
533 {
534         QRect rect = iScheduleTable->contentsRect();
535         int rowHeight = ( int )( rect.height() / ( float )iScheduleTable->rowCount() );
536         int headerRowHeight = rowHeight;
537         int columnWidth = ( int )( rect.width() / ( iScheduleTable->columnCount() - 0.5f ) );
538         int headerColumnWidth = columnWidth / 2;
539
540         iScheduleTable->setRowHeight( 0, headerRowHeight );
541         for ( int i = 1; i < iScheduleTable->rowCount(); ++i )
542         {
543                 iScheduleTable->setRowHeight( i, rowHeight );
544         }
545
546         iScheduleTable->setColumnWidth( 0, headerColumnWidth );
547         for ( int i = 1; i < iScheduleTable->columnCount(); ++i )
548         {
549                 iScheduleTable->setColumnWidth( i, columnWidth );
550         }
551
552         // resize table so that frame size matches exactly
553         int leftMargin = 0, topMargin = 0, rightMargin = 0, bottomMargin = 0;
554         iScheduleTable->getContentsMargins( &leftMargin, &topMargin, &rightMargin, &bottomMargin );
555         iScheduleTable->resize( columnWidth * iScheduleTable->columnCount() - headerColumnWidth + leftMargin + rightMargin,
556                                         rowHeight * iScheduleTable->rowCount() + topMargin + bottomMargin );
557 }