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