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