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