fix start/stop/pause
[Sak.git] / sakhits.cpp
blobcdcb3d9cced514c1c8658b262a703d3302167e05
1 /*************************-**************************************************
2 * Copyright (C) 2007 by Arrigo Zanette *
3 * zanettea@gmail.com *
4 ***************************************************************************/
6 #include <QtGui>
7 #include <QSettings>
9 #include "sak.h"
10 #include "backupper.h"
11 #include "sakwidget.h"
12 #include "saksubwidget.h"
13 #include "sakmessageitem.h"
14 #include "pixmapviewer.h"
15 #include "timeline.h"
16 #include "backupper.h"
17 #include "piechart.h"
20 //BEGIN Hits >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
22 //BEGIN MyDateItemDelegate
25 MyDateItemDelegate::MyDateItemDelegate(QObject *parent)
26 : QItemDelegate(parent)
30 QWidget *MyDateItemDelegate::createEditor(QWidget *parent,
31 const QStyleOptionViewItem &/* option */,
32 const QModelIndex & index ) const
34 QDateTimeEdit *editor = new QDateTimeEdit(parent);
35 editor->setCalendarPopup(true);
36 editor->setDisplayFormat(DATETIMEFORMAT);
37 editor->setDateTime(QDateTime::fromString(index.data().toString(), DATETIMEFORMAT));
38 return editor;
41 void MyDateItemDelegate::setEditorData(QWidget *editor,
42 const QModelIndex &index) const
44 const QDateTime& value = index.model()->data(index, Qt::DisplayRole).toDateTime();
46 QDateTimeEdit *e = static_cast<QDateTimeEdit*>(editor);
47 e->setDateTime(value);
50 void MyDateItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
51 const QModelIndex &index) const
53 QDateTimeEdit *e = static_cast<QDateTimeEdit*>(editor);
54 e->interpretText();
55 const QDateTime& value = e->dateTime();
57 model->setData(index, value.toString(DATETIMEFORMAT), Qt::EditRole);
60 void MyDateItemDelegate::updateEditorGeometry(QWidget *editor,
61 const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
63 editor->setGeometry(option.rect);
66 //END MyDateItemDelegate
68 //BEGIN TaskItemDelegate
70 TaskItemDelegate::TaskItemDelegate(Sak* sak, QObject *parent)
71 : QItemDelegate(parent), m_sak(sak)
75 QWidget *TaskItemDelegate::createEditor(QWidget *parent,
76 const QStyleOptionViewItem &/* option */,
77 const QModelIndex & index ) const
79 QComboBox *editor = new QComboBox(parent);
80 int j = -1;
81 int i=0;
82 QString current = index.data().toString();
83 foreach(const Task& t, *m_sak->tasks()) {
84 editor->addItem(t.icon, t.title);
85 if (t.title == current)
86 j = i;
88 Q_ASSERT(j = -1);
89 editor->setCurrentIndex(j);
90 return editor;
93 void TaskItemDelegate::setEditorData(QWidget *editor,
94 const QModelIndex &index) const
96 const QString& value ( index.data().toString() );
98 QComboBox *e = static_cast<QComboBox*>(editor);
99 for(int i=0; i<e->count(); i++) {
100 if (e->itemText(i) == value) {
101 e->setCurrentIndex(i);
102 break;
107 void TaskItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
108 const QModelIndex &index) const
110 QComboBox *e = static_cast<QComboBox*>(editor);
111 model->setData(index, e->currentText(), Qt::EditRole);
112 model->setData(index, e->itemIcon(e->currentIndex()), Qt::DecorationRole);
115 void TaskItemDelegate::updateEditorGeometry(QWidget *editor,
116 const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
118 editor->setGeometry(option.rect);
121 //END TaskItemDelegate
124 //BEGIN SubTaskItemDelegate
126 SubTaskItemDelegate::SubTaskItemDelegate(Sak* s, QObject *parent)
127 : QItemDelegate(parent), m_sak(s)
131 QWidget *SubTaskItemDelegate::createEditor(QWidget *parent,
132 const QStyleOptionViewItem &/* option */,
133 const QModelIndex & index ) const
135 QComboBox *editor = new QComboBox(parent);
136 editor->setEditable(true);
138 int j = -1;
139 QString taskTitle = index.model()->index(index.row(), index.column()-1, index.parent()).data().toString();
141 QHash<QString, Task>::iterator titr = m_sak->tasks()->find(taskTitle);
142 if (titr == m_sak->tasks()->end()) {
143 return editor;
145 const Task& task(titr.value());
147 QString current = index.data().toString();
148 QHash< QString, Task::SubTask >::const_iterator itr = task.subTasks.begin(), end = task.subTasks.end();
149 for(int i=0; itr != end; itr++, i++) {
150 editor->addItem(itr->title);
151 if (itr->title == current)
152 j = i;
155 editor->setCurrentIndex(j);
156 return editor;
159 void SubTaskItemDelegate::setEditorData(QWidget *editor,
160 const QModelIndex &index) const
162 const QString& value ( index.data().toString() );
164 QComboBox *e = static_cast<QComboBox*>(editor);
165 for(int i=0; i<e->count(); i++) {
166 if (e->itemText(i) == value) {
167 e->setCurrentIndex(i);
168 break;
173 void SubTaskItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,
174 const QModelIndex &index) const
176 QComboBox *e = static_cast<QComboBox*>(editor);
177 model->setData(index, e->currentText(), Qt::EditRole);
178 model->setData(index, e->itemIcon(e->currentIndex()), Qt::DecorationRole);
181 void SubTaskItemDelegate::updateEditorGeometry(QWidget *editor,
182 const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
184 editor->setGeometry(option.rect);
187 //END TaskItemDelegate
191 void Sak::addDefaultHit()
193 QTreeWidgetItem* i = new QTreeWidgetItem;
194 Task::Hit p(QDateTime::currentDateTime(), 0);
195 i->setText(0, p.timestamp.toString(DATETIMEFORMAT));
196 i->setText(1, m_editedTasks.begin().key());
197 i->setIcon(1, m_editedTasks.begin().value().icon);
198 i->setSizeHint(0, QSize(32, 32));
199 i->setText(3, QString("%1").arg(p.duration));
200 i->setFlags(i->flags() | Qt::ItemIsEditable);
201 hitsList->addTopLevelItem( i );
202 m_changedHit=true;
203 m_editedTasks[i->text(1)].hits[i->text(2)] << p;
204 i->setData(1, Qt::UserRole, qVariantFromValue(HitElement(&m_editedTasks[i->text(1)], i->text(2), p.timestamp, p.duration)));
207 void Sak::exportHits()
209 QString fileName = QFileDialog::getSaveFileName();
210 QFile file(fileName);
211 if (!file.open(QIODevice::Truncate|QIODevice::ReadWrite)) {
212 QMessageBox::warning(0, QString("Error saving"), QString("Error opening file %1").arg(fileName));
213 return;
215 QTextStream stream(&file);
216 for (int i=0; i<hitsList->topLevelItemCount(); i++) {
217 QTreeWidgetItem* w = hitsList->topLevelItem ( i );
218 QString name(w->text(1));
219 QDateTime timestamp(QDateTime::fromString(w->text(0), DATETIMEFORMAT));
220 QHash<QString, Task>::iterator titr = m_tasks.find(name);
221 stream << w->text(1) << ";" << w->text(0) << ";" << w->text(2) << ";" << w->text(3) << ";\n";
223 file.flush();
224 file.close();
228 QList<HitElement> Sak::createHitsList(const QDateTime& from , const QDateTime& to )
230 QMap<QDateTime, HitElement> hits;
231 foreach(const Task& t, m_editedTasks) {
232 QHash<QString, QList< Task::Hit> > ::const_iterator hitr = t.hits.begin(), hend = t.hits.end();
233 while(hitr != hend) {
234 const QList< Task::Hit > & l(hitr.value());
235 for (int i=0; i<l.count(); i++) {
236 const QDateTime& d = l[i].timestamp;
237 if ( (!from.isValid() || d >= from) && ( !to.isValid() || d <= to) ) {
238 hits.insertMulti(d, HitElement((Task*)&t, hitr.key(), d, l[i].duration));
241 hitr++;
244 return hits.values();
248 QMap<double,QPair<Task*, QString> > Sak::createSummaryList(const QList<HitElement>& hits)
250 QHash<QPair<Task*, QString>, double> summaryMap;
251 foreach(const HitElement& hit, hits) {
252 summaryMap[QPair<Task*, QString>(hit.task, hit.subtask)] += Task::hours(hit.duration);
253 summaryMap[QPair<Task*, QString>(hit.task, "")] += Task::hours(hit.duration);
255 QMap<double, QPair<Task*, QString> > summaryOrderedMap;
256 QHash<QPair<Task*, QString>, double>::const_iterator itr = summaryMap.begin();
257 while(itr != summaryMap.end()) {
258 summaryOrderedMap.insertMulti(itr.value(), itr.key());
259 itr++;
261 return summaryOrderedMap;
266 void Sak::saveHitChanges()
268 if (m_changedHit) {
269 if ( QMessageBox::question ( 0, "Hit list changed", "Hit list has changed: do you want to save changes?", QMessageBox::Save | QMessageBox::Discard, QMessageBox::Discard) == QMessageBox::Save ) {
270 m_tasks = m_editedTasks;
271 } else m_editedTasks = m_tasks; // undo changes
272 m_changedHit=false;
273 selectedStartDate(QDate());
274 populateTasks();
278 bool Sak::hitsListEventFilter(QEvent* e)
280 if (e->type() == QEvent::ContextMenu) {
281 QContextMenuEvent* me = dynamic_cast<QContextMenuEvent*>(e);
282 if (!me) return false;
283 m_addHitMenu->popup(me->globalPos());
284 return true;
285 } else if (e->type() == QEvent::KeyRelease) {
286 QKeyEvent* ke = dynamic_cast<QKeyEvent*>(e);
287 if (!ke) return false;
288 if ( !hitsList->hasFocus() || (ke->key() != Qt::Key_Delete && ke->key() != Qt::Key_Backspace) ) return false;
289 QList<QTreeWidgetItem*> selected = hitsList->selectedItems();
290 if (selected.isEmpty()) return false;
291 foreach(QTreeWidgetItem* ii, selected) {
292 hitsListItemChanged(hitsList->takeTopLevelItem(hitsList->indexOfTopLevelItem (ii)), -1);
294 return true;
295 } else if (e->type() == QEvent::Hide) {
296 saveHitChanges();
297 }/* else if (e->type() == QEvent::Show) {
298 m_editedTasks = m_tasks;
299 selectedStartDate(QDate());
301 return false;
305 void Sak::selectedStartDate(const QDate& _date)
307 QDate date = _date;
308 if (!date.isValid()) {
309 date = cal1->selectedDate();
312 if (sender() != cal1) {
313 cal1->setSelectedDate(date);
315 if (sender() != cal3) {
316 cal3->setSelectedDate(date);
318 if (cal2->selectedDate() < cal1->selectedDate()) {
319 cal2->setSelectedDate(date.addDays(1));
320 cal4->setSelectedDate(date.addDays(1));
322 cal2->setMinimumDate(date);
323 cal4->setMinimumDate(date);
324 populateHitsList(createHitsList(QDateTime(cal1->selectedDate()), QDateTime(cal2->selectedDate())));
325 if (hitsTimeline)
326 hitsTimeline->setPeriod(QDateTime(cal1->selectedDate()), QDateTime(cal2->selectedDate()));
329 void Sak::selectedEndDate(const QDate& _date)
331 QDate date = _date;
332 if (!date.isValid())
333 date = cal1->selectedDate();
335 if (sender() != cal2) {
336 cal2->setSelectedDate(date);
338 if (sender() != cal4) {
339 cal4->setSelectedDate(date);
341 if (cal1->selectedDate() > date) {
342 cal2->setSelectedDate(cal1->selectedDate().addDays(1));
343 cal4->setSelectedDate(cal1->selectedDate().addDays(1));
345 populateHitsList(createHitsList(QDateTime(cal1->selectedDate()), QDateTime(cal2->selectedDate())));
346 hitsTimeline->setPeriod(QDateTime(cal1->selectedDate()), QDateTime(cal2->selectedDate()));
350 void Sak::hitsListItemChanged(QTreeWidgetItem* i, int column)
352 hitsList->blockSignals(true);
353 m_changedHit = true;
354 // save change in structure m_editedTasks
355 while(1) {
356 const HitElement& origHit = i->data(1, Qt::UserRole).value<HitElement>();
357 // find hit in m_editedTasks
358 Q_ASSERT(origHit.task);
359 Task& t(*origHit.task);
360 Q_ASSERT(m_editedTasks.contains(t.title));
361 Task& et(m_editedTasks[t.title]);
363 QList< Task::Hit >& origList(et.hits[origHit.subtask]);
365 int hitPosition = origList.indexOf(Task::Hit(origHit.timestamp, origHit.duration));
366 if (hitPosition==-1) {
367 qWarning() << "CANNOT FIND IN TASK LIST "<< t.title << origHit.subtask << origHit.timestamp << origHit.duration;
368 break;
370 if (!m_editedTasks.contains(i->text(1))) {
371 i->setText(1, t.title);
372 qDebug() << "Task " << i->text(1) << " does not exists -> undo change";
373 } else if (column == -1) {
374 qDebug() << "remove hit from task " << t.title;
375 origList.takeAt(hitPosition);
376 } else {
377 qDebug() << "remove hit from task " << t.title;
378 origList.takeAt(hitPosition);
379 Task& nt(m_editedTasks[i->text(1)]);
380 Task::Hit p(QDateTime::fromString(i->text(0), DATETIMEFORMAT), i->text(3).toUInt());
381 i->setData(1, Qt::UserRole, qVariantFromValue(HitElement(&nt, i->text(2), p.timestamp, p.duration)));
382 qDebug() << "insert hit into task " << i->text(1) << p.timestamp;
383 nt.hits[i->text(2)] << p;
385 break;
387 if (column == 1) {
388 if (!i) {}
389 else if (m_editedTasks.contains(i->text(column))) {
390 i->setIcon(column, m_editedTasks[i->text(column)].icon);
391 } else {
392 i->setIcon(column, QIcon());
395 hitsList->blockSignals(false);
398 void Sak::hitsSelectedInList(QTreeWidgetItem* current, QTreeWidgetItem* /*prev*/)
400 if (current) {
401 // clear current selection
402 QList<QGraphicsItem*> sitems = hitsTimeline->scene()->selectedItems();
403 for(int j=0; j<sitems.count(); j++) {
404 HitItem* tmp = dynamic_cast<HitItem*>(sitems[j]);
405 if (tmp) {
406 tmp->setZValue(tmp->timestamp().toTime_t());
407 tmp->setSelected(false);
412 QPointF center (QDateTime::fromString(current->text(0), DATETIMEFORMAT).toTime_t() / 60.0, 0);
413 int duration = current->text(3).toUInt();
414 QList<QGraphicsItem*> items = hitsTimeline->scene()->items();
415 for(int i=0; i<items.count(); i++) {
416 HitItem* hitem = dynamic_cast<HitItem*>(items[i]);
417 if (hitem && hitem->timestamp() == QDateTime::fromString(current->text(0), DATETIMEFORMAT) && hitem->task()->title == current->text(1) && hitem->subtask() == current->text(2) && hitem->duration() == duration) {
418 hitem->setZValue(1e20);
419 hitem->setSelected(true);
420 break;
423 hitsTimeline->centerOn(center);
427 void Sak::hitsSelectedInTimeline(HitItem* hitem)
429 const Task* t = hitem->task();
430 QList<QTreeWidgetItem *> items = hitsList->findItems(hitem->timestamp().toString(DATETIMEFORMAT), Qt::MatchExactly, 0);
431 qDebug() << "look for " << hitem->timestamp().toString(DATETIMEFORMAT);
433 QString tmp(QString ("%1").arg(hitem->duration()));
434 qDebug() << "duration " << tmp;
435 foreach(QTreeWidgetItem* item, items) {
436 if (item->text(1) == t->title && item->text(2) == hitem->subtask() && item->text(3) == tmp) {
437 hitsList->clearSelection();
438 item->setSelected(true);
439 if (hitem->timestamp() != hitem->newTimestamp() || hitem->duration() != hitem->newDuration()) {
440 item->setText(0, hitem->newTimestamp().toString(DATETIMEFORMAT));
441 item->setText(3, QString("%1").arg(hitem->newDuration()));
442 hitem->commitChanges();
444 hitsList->scrollToItem(item);
451 void Sak::populateHitsList(const QList<HitElement>& hits, QTreeWidget* theHitsList )
453 if (theHitsList == 0)
454 theHitsList = hitsList;
455 Q_ASSERT(theHitsList);
456 saveHitChanges();
457 theHitsList->clear();
459 QVector<double> o;
460 double totOverestimation;
461 HitElement::overestimations(hits, o, totOverestimation);
462 QList<QTreeWidgetItem*> widgets;
463 int i=0;
464 foreach(const HitElement& hit, hits) {
465 QTreeWidgetItem* w = new QTreeWidgetItem;
466 w->setText(0, hit.timestamp.toString(DATETIMEFORMAT));
467 w->setText(1, hit.task->title);
468 w->setSizeHint(0, QSize(24, 24));
469 w->setData(1, Qt::UserRole, qVariantFromValue(hit));
470 w->setText(2, hit.subtask);
471 w->setText(3, QString("%1").arg(hit.duration));
472 if (hit.task->title == "<away>") {
473 for(int i=0; i<4; i++) {
474 w->setForeground(i,Qt::gray);
476 } else {
477 for(int i=0; i<4; i++) {
478 w->setBackground(i,hit.task->bgColor);
479 w->setForeground(i,hit.task->fgColor);
481 #if MARK_OVERESTIMATIONS
482 if (o[i] != 0) {
483 w->setBackground(0,Qt::red);
484 w->setBackground(1,Qt::red);
485 w->setBackground(2,Qt::red);
486 w->setBackground(3,Qt::red);
487 w->setBackground(4,Qt::red);
488 w->setText(4,QString("%1").arg(o[i]));
490 w->setText(4,QString("%1").arg(o[i]));
491 #endif
493 w->setIcon(1, hit.task->icon);
494 if (hit.editable)
495 w->setFlags(w->flags() | Qt::ItemIsEditable);
496 else w->setDisabled(true);
497 widgets << w;
498 i++;
500 theHitsList->addTopLevelItems(widgets);
502 widgets.clear();
504 if (summaryList) {
505 theHitsList = summaryList;
506 theHitsList->clear();
507 const QMap<double, QPair<Task*, QString> >& map(createSummaryList(hits));
508 QMap<double, QPair<Task*, QString> >::const_iterator itr = map.begin();
509 QHash<Task*, QTreeWidgetItem*> topLevels;
510 for(; itr != map.end(); itr++) { // first be sure to insert top levels
511 QTreeWidgetItem* topLevel;
512 if (!topLevels.contains(itr.value().first)) {
513 QTreeWidgetItem* w = new QTreeWidgetItem(QTreeWidgetItem::UserType);
514 w->setText(0, itr.value().first->title);
515 w->setIcon(0, itr.value().first->icon);
516 topLevels[itr.value().first] = w;
518 topLevel = topLevels[itr.value().first];
519 if (itr.value().second == "")
520 topLevel->setText(1, QString("%1").arg(topLevel->text(1).toInt() + itr.key(), 4, 'f', 2, ' '));
521 if (!itr.value().second.isEmpty()) {
522 QTreeWidgetItem* w = new QTreeWidgetItem(QTreeWidgetItem::UserType);
523 w->setText(0, itr.value().second);
524 w->setText(1, QString("%1").arg(itr.key(), 4, 'f', 2, ' '));
525 topLevel->addChild(w);
529 theHitsList->addTopLevelItems(topLevels.values());
532 populateHitsTimeline(hits, hitsTimeline);
535 void Sak::populateHitsTimeline(const QList<HitElement>& hits, Timeline* timeline )
537 if (!timeline) return;
538 QGraphicsScene* scene = timeline->scene();
539 if (scene) {
540 QList<QGraphicsItem*> items = scene->items();
541 foreach(QGraphicsItem* item, items) {
542 if (dynamic_cast<HitItem*>(item)) {
543 scene->removeItem(item);
544 delete item;
547 foreach(HitElement e, hits) {
548 HitItem* item = new HitItem(e.task, e.timestamp, e.duration, e.subtask);
549 connect(item, SIGNAL(changed()), timeline, SLOT(selectionChanged()));
550 scene->addItem(item);
556 void Sak::interactiveMergeHits()
558 if (!m_incremental->foundPieces.count()) return;
559 QDialog mergeDialog;
560 mergeDialog.setWindowTitle("Merge sparse hits");
561 mergeDialog.setModal(true);
562 QTreeWidget* theHitsList = newHitsList();
563 QMap<QDateTime, HitElement> hits;
564 QMap<QDateTime, Incremental::Hit >::iterator itr = m_incremental->foundPieces.begin();
565 while(itr != m_incremental->foundPieces.end()) {
566 QHash<QString, Task>::iterator titr = m_tasks.find(itr.value().task);
567 if (titr == m_tasks.end()) {
568 qDebug() << "Discard piece for unknown task " << itr.value().task;
569 } else {
570 hits.insertMulti(itr.key(), HitElement(&titr.value(), itr.value().subtask, itr.key(), itr.value().duration));
572 itr++;
574 if (hits.count() == 0) return;
576 qDebug() << "hits: " << hits.count() << hits.begin().key() << (--hits.end()).key();
577 QList<HitElement> okHits (createHitsList(hits.begin().key(), (--hits.end()).key()));
578 QMap<QDateTime, HitElement> tmpHits;
579 for(int i=0; i<okHits.count(); i++) {
580 HitElement& cmp(okHits[i]);
581 QMap<QDateTime, HitElement>::iterator itr = hits.find(cmp.timestamp);
582 bool skip=false;
583 while(itr != hits.end() && itr.key() == cmp.timestamp) {
584 const HitElement& cur ( itr.value() );
585 if (cmp.task->title == cur.task->title && cmp.subtask == cur.subtask && cmp.duration == cur.duration) {
586 itr = hits.erase(itr);
587 skip = true;
588 } else itr++;
590 if (skip) continue;
591 okHits[i].editable=false;
592 tmpHits.insertMulti(okHits[i].timestamp, okHits[i]);
595 if (!hits.count()) return;
596 else hits.unite(tmpHits);
598 populateHitsList(hits.values(), theHitsList);
599 mergeDialog.setMinimumWidth(750);
600 mergeDialog.setMinimumHeight(600);
601 QVBoxLayout mainLayout(&mergeDialog);
602 mainLayout.addWidget(theHitsList);
603 QPushButton* ok = new QPushButton("Do merge!");
604 QPushButton* cancel = new QPushButton("Cancel");
605 QHBoxLayout* buttons = new QHBoxLayout;
606 buttons->addWidget(ok);
607 buttons->addWidget(cancel);
608 mainLayout.addLayout(buttons);
609 connect(ok, SIGNAL(clicked()), &mergeDialog, SLOT(accept()));
610 connect(cancel, SIGNAL(clicked()), &mergeDialog, SLOT(reject()));
611 // do auto merging
612 if ( mergeDialog.exec() ) {
613 for (int i=0; i<theHitsList->topLevelItemCount(); i++) {
614 QTreeWidgetItem* w = theHitsList->topLevelItem ( i );
615 if (!w->isDisabled()) {
616 QString name(w->text(1));
617 QDateTime timestamp(QDateTime::fromString(w->text(0), DATETIMEFORMAT));
618 int duration (w->text(3).toUInt());
619 QHash<QString, Task>::iterator titr = m_tasks.find(name);
620 Q_ASSERT(titr != m_tasks.end());
621 (*titr).hits[w->text(2)] << Task::Hit(timestamp, duration);
624 flush();
625 m_incremental->clearMergedPieces();