Fix a crash when viewing statistics including a score on a course which is not found.
[scorecard] / src / main-window.cpp
1 /*
2  * Copyright (C) 2009 Sakari Poussa
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, version 2.
7  */
8
9 #include <QtGui>
10 #ifdef Q_WS_MAEMO_5
11 #include <QMaemo5InformationBox>
12 #endif
13
14 #include "score-common.h"
15 #include "main-window.h"
16 #include "score-dialog.h"
17 #include "course-dialog.h"
18 #include "settings-dialog.h"
19 #include "stat-model.h"
20 #include "xml-dom-parser.h"
21
22 QString appName("scorecard");
23 QString topDir("/opt");
24 QString mmcDir("/media/mmc1");
25 QString dataDirName("data");
26 QString dataDir;
27 QString userDataDir;
28 QString imgDir(topDir + "/pixmaps");
29 QString scoreFileName("score.xml");
30 QString scoreFile;
31 QString clubFileName("club.xml");
32 QString clubFile;
33 QString masterFileName("club-master.xml");
34 QString masterFile;
35 QString logFile("/tmp/scorecard.log");
36 QString titleScores("ScoreCard - Scores");
37 QString titleCourses("ScoreCard - Courses");
38
39 bool dateLessThan(const Score *s1, const Score *s2)
40 {
41     return (*s1) < (*s2);
42 }
43
44 bool dateMoreThan(const Score *s1, const Score *s2)
45 {
46     return (*s1) > (*s2);
47 }
48
49 bool scoreMoreThan(const Score *s1, const Score *s2)
50 {
51     return s1->getTotal(Total).toInt() > s2->getTotal(Total).toInt();
52 }
53
54 bool scoreLessThan(const Score *s1, const Score *s2)
55 {
56     return s1->getTotal(Total).toInt() < s2->getTotal(Total).toInt();
57 }
58
59 // Find score based on club and course name
60 Score *MainWindow::findScore(QString & clubName, QString & courseName)
61 {
62     TRACE;
63     QListIterator<Score *> i(scoreList);
64     Score * s;
65
66     while (i.hasNext()) {
67         s = i.next();
68         if ((s->getClubName() == clubName) &&
69             (s->getCourseName() == courseName))
70             return s;
71     }
72     return 0;
73 }
74
75 // Find club based on name
76 Club *MainWindow::findClub(QString &name)
77 {
78     TRACE;
79     QListIterator<Club *> i(clubList);
80     Club *c;
81
82     while (i.hasNext()) {
83         c = i.next();
84         if (c->getName() == name)
85             return c;
86     }
87     return 0;
88 }
89
90 // Find course based on club & course name
91 Course *MainWindow::findCourse(const QString &clubName, 
92                                const QString &courseName)
93 {
94     TRACE;
95     QListIterator<Club *> i(clubList);
96     Club *c;
97
98     while (i.hasNext()) {
99         c = i.next();
100         if (c->getName() == clubName) {
101             return c->getCourse(courseName);
102         }
103     }
104     return 0;
105 }
106
107 // Find course based on current selection on the list
108 // TODO: make sure this is only called when course list is the model...
109 Course *MainWindow::currentCourse()
110 {
111     TRACE;
112     QModelIndex index = selectionModel->currentIndex();
113
114     if (!index.isValid())
115         return 0;
116
117     const QAbstractItemModel *model = selectionModel->model();
118     QString str = model->data(index, Qt::DisplayRole).toString();
119
120     QStringList strList = str.split(", ");
121     if (strList.count() != 2) {
122         showNote(tr("Invalid course selection"));
123         return 0;
124     }
125     return findCourse(strList[0], strList[1]);
126 }
127
128 // Find score based on current selection on the list
129 // TODO: make sure this is only called when score list is the model...
130 Score *MainWindow::currentScore()
131 {
132     TRACE;
133     QModelIndex index = selectionModel->currentIndex();
134
135     if (!index.isValid())
136         return 0;
137
138     return scoreList.at(index.row());
139 }
140
141 void MainWindow::flushReadOnlyItems()
142 {
143     TRACE;
144     QMutableListIterator<Club *> i(clubList);
145     Club *c;
146
147     while (i.hasNext()) {
148         c = i.next();
149         if (c->isReadOnly()) {
150             qDebug() << "Del:" << c->getName();
151             i.remove();
152         }
153     }
154 }
155
156 void MainWindow::markHomeClub()
157 {
158     TRACE;
159     QListIterator<Club *> i(clubList);
160     Club *c;
161
162     while (i.hasNext()) {
163         c = i.next();
164         if (c->getName() == conf.homeClub)
165             c->setHomeClub(true);
166         else
167             c->setHomeClub(false);
168     }
169 }
170
171 void MainWindow::sortScoreList()
172 {
173     if (conf.sortOrder == "Date")
174         qSort(scoreList.begin(), scoreList.end(), dateMoreThan); 
175     else if (conf.sortOrder == "Score")
176         qSort(scoreList.begin(), scoreList.end(), scoreLessThan); 
177 }
178
179 MainWindow::MainWindow(QMainWindow *parent): QMainWindow(parent)
180 {
181     resize(800, 480);
182
183 #ifdef Q_WS_MAEMO_5
184     setAttribute(Qt::WA_Maemo5StackedWindow);
185 #endif
186
187     loadSettings();
188
189     centralWidget = new QWidget(this);
190
191     setCentralWidget(centralWidget);
192
193     loadScoreFile(scoreFile, scoreList);
194     if (conf.defaultCourses == "Yes")
195         loadClubFile(masterFile, clubList, true);
196     loadClubFile(clubFile, clubList);
197     markHomeClub();
198
199     // Sort the scores based on settings
200     sortScoreList();
201
202     createActions();
203     createMenus();
204
205     createListView(scoreList, clubList);
206
207     createLayoutList(centralWidget);
208
209     scoreWindow = new ScoreWindow(this);
210     courseWindow = new CourseWindow(this);
211 }
212
213 void MainWindow::loadSettings(void)
214 {
215     TRACE;
216     bool external = false;
217
218     QDir mmc(mmcDir);
219     if (mmc.exists())
220         external = true;
221
222     // TODO: make via user option, automatic will never work
223     external = false;
224
225 #ifndef Q_WS_MAEMO_5
226     dataDir = "./" + dataDirName;
227 #else
228     if (external) {
229         dataDir = mmcDir + "/" + appName + "/" + dataDirName;
230     }
231     else {
232         dataDir = topDir + "/" + appName + "/" + dataDirName;
233     }
234 #endif
235
236     // Use MyDoc directory to get automatic backup/restore 
237     userDataDir = QDir::homePath() + "/MyDocs/." + appName;
238     QDir dir(userDataDir);
239     if (!dir.exists())
240         if (!dir.mkpath(userDataDir)) {
241             qWarning() << "Unable to create: " + userDataDir;
242             return;
243         }
244
245     masterFile = dataDir + "/" + masterFileName;
246
247     // Store user data files under $HOME so they are not lost in
248     // re-flash
249     scoreFile = userDataDir + "/" + scoreFileName;
250     clubFile = userDataDir + "/" + clubFileName;
251
252     // Start of 0.19 migration
253     // Copy existing user data to new location
254     // 0.18 and earlier: score.xml and club.xml are in /opt/scorecard/data
255     // 0.19 and later: score.xml and club.xml are in /home/user/.scorecard
256     QString scoreFileOld = dataDir + "/" + scoreFileName;
257     QString clubFileOld = dataDir + "/" + clubFileName;
258
259     QFile file1(scoreFileOld);
260     QFile file2(clubFileOld);
261     QDir move;
262     if (file1.exists()) {
263         move.rename(scoreFileOld, scoreFile);
264         qDebug() << "Moved: " << scoreFileOld << "->" << scoreFile;
265     }
266     if (file2.exists()) {
267         move.rename(clubFileOld, clubFile);
268         qDebug() << "Moved: " << clubFileOld << "->" << clubFile;
269     }
270     // End of 0.19 migration
271
272     qDebug() << "User data is at:" + userDataDir;
273
274     settings.beginGroup(settingsGroup);
275     conf.hcp = settings.value(settingsHcp);
276     conf.homeClub = settings.value(settingsHomeClub);
277     conf.sortOrder = settings.value(settingsSortOrder);
278     conf.defaultCourses = settings.value(settingsDefaultCourses);
279     settings.endGroup();
280
281     // Use default courses if no settings for that
282     if (!conf.defaultCourses.isValid())
283         conf.defaultCourses = "Yes";
284
285     // Use date sort order if no settings for that
286     if (!conf.sortOrder.isValid())
287         conf.sortOrder = "Date";
288
289     qDebug() << "Settings: " << conf.hcp << conf.homeClub << conf.sortOrder << conf.defaultCourses;
290 }
291
292 void MainWindow::saveSettings(void)
293 {
294     TRACE;
295     settings.beginGroup(settingsGroup);
296     if (conf.hcp.isValid())
297         settings.setValue(settingsHcp, conf.hcp);
298     if (conf.homeClub.isValid())
299         settings.setValue(settingsHomeClub, conf.homeClub);
300     if (conf.sortOrder.isValid())
301         settings.setValue(settingsSortOrder, conf.sortOrder);
302     if (conf.defaultCourses.isValid())
303         settings.setValue(settingsDefaultCourses, conf.defaultCourses);
304     settings.endGroup();
305 }
306
307 void MainWindow::createLayoutList(QWidget *parent)
308 {
309     TRACE;
310     QVBoxLayout * tableLayout = new QVBoxLayout;
311     tableLayout->addWidget(list);
312
313     QHBoxLayout *mainLayout = new QHBoxLayout(parent);
314     mainLayout->addLayout(tableLayout);
315     parent->setLayout(mainLayout);
316 }
317
318 void MainWindow::createListView(QList<Score *> &scoreList, 
319                                 QList <Club *> &clubList)
320 {
321     TRACE;
322     list = new QListView(this);
323
324     scoreListModel = new ScoreListModel(scoreList, clubList);
325     courseListModel = new CourseListModel(clubList);
326
327     list->setStyleSheet(defaultStyleSheet);
328
329     list->setSelectionMode(QAbstractItemView::SingleSelection);
330     list->setProperty("FingerScrolling", true);
331
332     // Initial view
333     listScores();
334
335     connect(list, SIGNAL(clicked(QModelIndex)),
336             this, SLOT(clickedList(QModelIndex)));
337 }
338
339 void MainWindow::listScores()
340 {
341     TRACE;
342     list->setModel(scoreListModel);
343     selectionModel = list->selectionModel();
344     updateTitleBar(titleScores);
345 }
346
347 void MainWindow::listCourses()
348 {
349     TRACE;
350     list->setModel(courseListModel);
351     selectionModel = list->selectionModel();
352     updateTitleBar(titleCourses);
353 }
354
355 void MainWindow::createActions()
356 {
357     TRACE;
358     newScoreAction = new QAction(tr("New Score"), this);
359     connect(newScoreAction, SIGNAL(triggered()), this, SLOT(newScore()));
360
361     newCourseAction = new QAction(tr("New Course"), this);
362     connect(newCourseAction, SIGNAL(triggered()), this, SLOT(newCourse()));
363
364     statAction = new QAction(tr("Statistics"), this);
365     connect(statAction, SIGNAL(triggered()), this, SLOT(viewStatistics()));
366
367     settingsAction = new QAction(tr("Settings"), this);
368     connect(settingsAction, SIGNAL(triggered()), this, SLOT(viewSettings()));
369
370     // Maemo5 style menu filters
371     filterGroup = new QActionGroup(this);
372     filterGroup->setExclusive(true);
373
374     listScoreAction = new QAction(tr("Scores"), filterGroup);
375     listScoreAction->setCheckable(true);
376     listScoreAction->setChecked(true);
377     connect(listScoreAction, SIGNAL(triggered()), this, SLOT(listScores()));
378
379     listCourseAction = new QAction(tr("Courses"), filterGroup);
380     listCourseAction->setCheckable(true);
381     connect(listCourseAction, SIGNAL(triggered()), this, SLOT(listCourses()));
382 }
383
384 void MainWindow::createMenus()
385 {
386     TRACE;
387 #ifdef Q_WS_MAEMO_5
388     menu = menuBar()->addMenu("");
389 #else
390     menu = menuBar()->addMenu("Menu");
391 #endif
392
393     menu->addAction(newScoreAction);
394     menu->addAction(newCourseAction);
395     menu->addAction(statAction);
396     menu->addAction(settingsAction);
397     menu->addActions(filterGroup->actions());
398 }
399
400 void MainWindow::updateTitleBar(QString & msg)
401 {
402     TRACE;
403     setWindowTitle(msg);
404 }
405
406 void MainWindow::showNote(QString msg)
407 {
408 #ifdef Q_WS_MAEMO_5
409     QMaemo5InformationBox::information(this, 
410                                        msg,
411                                        QMaemo5InformationBox::DefaultTimeout);
412 #endif
413 }
414
415 void MainWindow::clickedList(const QModelIndex &index)
416 {
417     TRACE;
418     int row = index.row();
419
420     const QAbstractItemModel *m = index.model();
421     if (m == scoreListModel) {
422         if (row < scoreList.count()) {
423             Score * score = scoreList.at(row);
424             Course * course = findCourse(score->getClubName(), score->getCourseName());
425             viewScore(score, course);
426         }
427     }
428     else if (m == courseListModel) {
429         QString str = courseListModel->data(index, Qt::DisplayRole).toString();
430         QStringList strList = str.split(", ");
431
432         if (strList.count() != 2) {
433             showNote(QString("Invalid course selection"));
434             return;
435         }
436         Course * course = findCourse(strList.at(0), strList.at(1));
437         viewCourse(course);
438     }
439 }
440
441 void MainWindow::viewScore(Score * score, Course * course)
442 {
443     TRACE;
444     scoreWindow->setup(score, course);
445     scoreWindow->show();
446 }
447
448 void MainWindow::newScore()
449 {
450     TRACE;
451     SelectDialog *selectDialog = new SelectDialog(this);
452
453     selectDialog->init(clubList);
454
455     int result = selectDialog->exec();
456     if (result) {
457         QString clubName;
458         QString courseName;
459         QString date;
460
461         selectDialog->results(clubName, courseName, date);
462
463         ScoreDialog *scoreDialog = new ScoreDialog(this);
464         QString title = "New Score: " + courseName + ", " + date;
465         scoreDialog->setWindowTitle(title);
466
467         Club *club = findClub(clubName);
468         if (!club) {
469             showNote(tr("No club"));
470             return;
471         }
472         Course *course = club->getCourse(courseName);
473         if (!course) {
474             showNote(tr("Error: no such course:"));
475             return;
476         }
477         scoreDialog->init(course);
478         result = scoreDialog->exec();
479         if (result) {
480             QVector<QString> scores(18);
481
482             scoreDialog->results(scores);
483             Score *score = new Score(scores, clubName, courseName, date);
484             scoreList << score;
485
486             // Sort the scores based on settings
487             sortScoreList();
488             // Save it
489             saveScoreFile(scoreFile, scoreList);
490             scoreListModel->update(scoreList);
491             list->update();
492         }
493     }
494 }
495
496 void MainWindow::editScore()
497 {
498     TRACE;
499     Course * course = 0;
500     Score *score = currentScore();
501
502     if (score) 
503         course = findCourse(score->getClubName(), score->getCourseName());
504
505     if (!course || !score) {
506         showNote(tr("No score or course to edit"));
507         return;
508     }
509
510     QString date = score->getDate();
511
512     ScoreDialog *scoreDialog = new ScoreDialog(this);
513     scoreDialog->init(course, score);
514   
515     QString title = "Edit Score: " + course->getName() + ", " + date;
516     scoreDialog->setWindowTitle(title);
517
518     int result = scoreDialog->exec();
519     if (result) {
520         QVector<QString> scores(18);
521
522         scoreDialog->results(scores);
523     
524         score->update(scores);
525
526         // Sort the scores based on dates
527         qSort(scoreList.begin(), scoreList.end(), dateMoreThan); 
528         // Save it
529         saveScoreFile(scoreFile, scoreList);
530     }
531     if (scoreDialog)
532         delete scoreDialog;
533 }
534
535 void MainWindow::deleteScore()
536 {
537     TRACE;
538     if (scoreWindow)
539         scoreWindow->close();
540
541     QModelIndex index = selectionModel->currentIndex();
542     if (!index.isValid()) {
543         qDebug() << "Invalid index";
544         return;
545     }
546     
547     scoreList.removeAt(index.row());
548     // Save it
549     saveScoreFile(scoreFile, scoreList);
550     scoreListModel->update(scoreList);
551     list->update();
552 }
553
554 void MainWindow::viewCourse(Course * course)
555 {
556     TRACE;
557     courseWindow->setup(course);
558     courseWindow->show();
559 }
560
561 void MainWindow::newCourse()
562 {
563     TRACE;
564     CourseSelectDialog *selectDialog = new CourseSelectDialog(this);
565
566     int result = selectDialog->exec();
567     if (result) {
568         QString clubName;
569         QString courseName;
570         QString date;
571
572         selectDialog->results(clubName, courseName);
573
574         CourseDialog *courseDialog = new CourseDialog(this);
575         courseDialog->init();
576         QString title = "New Course: " + clubName + "," + courseName;
577         courseDialog->setWindowTitle(title);
578
579         int result = courseDialog->exec();
580         if (result) {
581             QVector<QString> par(18);
582             QVector<QString> hcp(18);
583             QVector<QString> len(18);
584
585             courseDialog->results(par, hcp, len);
586
587             Course *course = 0;
588             Club *club = findClub(clubName);
589             if (club) {
590                 course = club->getCourse(courseName);
591                 if (course) {
592                     showNote(tr("Club/course already in the database"));
593                     return;
594                 }
595                 else {
596                     course = new Course(courseName, par, hcp);
597                     club->addCourse(course);
598                 }
599             }
600             else {
601                 // New club and course
602                 club = new Club(clubName);
603                 course = new Course(courseName, par, hcp);
604                 club->addCourse(course);
605                 clubList << club;
606             }
607             // Save it
608             saveClubFile(clubFile, clubList);
609             courseListModel->update(clubList);
610             list->update();
611         }
612     }
613 }
614
615 void MainWindow::editCourse()
616 {
617     TRACE;
618     Course *course = currentCourse();
619
620     if (!course) {
621         showNote(tr("No course on edit"));
622         return;
623     }
624
625     CourseDialog *courseDialog = new CourseDialog(this);
626     courseDialog->init(course);
627
628     QString title = "Edit Course: " + course->getName();
629     courseDialog->setWindowTitle(title);
630   
631     int result = courseDialog->exec();
632     if (result) {
633         QVector<QString> par(18);
634         QVector<QString> hcp(18);
635         QVector<QString> len(18);
636     
637         courseDialog->results(par, hcp, len);
638     
639         course->update(par, hcp, len);
640         saveClubFile(clubFile, clubList);
641     }
642     if (courseDialog)
643         delete courseDialog;
644 }
645
646 void MainWindow::deleteCourse()
647 {
648     TRACE;
649     Club *club = 0;
650     Course * course = currentCourse();
651
652     if (!course) {
653         qDebug() << "Invalid course for deletion";
654         return;
655     }
656     club = course->parent();
657
658     // Can not delete course if it has scores -- check
659     if (findScore(club->getName(), course->getName()) != 0) {
660         showNote(tr("Can not delete course, delete scores on the course first"));
661         return;
662     }
663     // Close the window
664     if (courseWindow)
665         courseWindow->close();
666
667     club->delCourse(course);
668
669     if (club->isEmpty()) {
670         int index = clubList.indexOf(club);
671         if (index != -1)
672             clubList.removeAt(index);
673     }
674
675     // Save it
676     saveClubFile(clubFile, clubList);
677     courseListModel->update(clubList);
678     list->update();
679 }
680
681 void MainWindow::viewStatistics()
682 {
683     TRACE;
684     QMainWindow *win = new QMainWindow(this);
685     QString title = "Statistics";
686     win->setWindowTitle(title);
687 #ifdef Q_WS_MAEMO_5
688     win->setAttribute(Qt::WA_Maemo5StackedWindow);
689 #endif
690
691     StatModel *model = new StatModel(clubList, scoreList);
692
693     QTableView *table = new QTableView;
694     table->showGrid();
695     table->setSelectionMode(QAbstractItemView::NoSelection);
696     table->setStyleSheet(statStyleSheet);
697     table->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
698     table->verticalHeader()->setResizeMode(QHeaderView::Stretch);
699     table->verticalHeader()->setAutoFillBackground(true);
700     table->setModel(model);
701
702     QWidget *central = new QWidget(win);
703     win->setCentralWidget(central);
704
705     QTextEdit *textEdit = new QTextEdit;
706
707     textEdit->setReadOnly(true);
708
709     QVBoxLayout *infoLayout = new QVBoxLayout;
710     infoLayout->addWidget(table);
711
712     QHBoxLayout *mainLayout = new QHBoxLayout(central);
713     mainLayout->addLayout(infoLayout);
714     central->setLayout(mainLayout);
715
716     win->show();
717 }
718
719 void MainWindow::viewSettings()
720 {
721     TRACE;
722     SettingsDialog *dlg = new SettingsDialog(this);
723
724     dlg->init(conf, clubList);
725
726     int result = dlg->exec();
727     if (result) {
728         QString oldValue = conf.defaultCourses.toString();
729         dlg->results(conf);
730         QString newValue = conf.defaultCourses.toString();
731         saveSettings();
732
733         // Reload club list, or drop r/o courses from list
734         if (oldValue == "Yes" && newValue == "No") {
735             flushReadOnlyItems();
736             courseListModel->update(clubList);
737             list->update();
738         }
739         else if ((oldValue == "No" || oldValue == "") && newValue == "Yes") {
740             loadClubFile(masterFile, clubList, true);
741             courseListModel->update(clubList);
742             list->update();
743         }
744         // TODO: do these only if needed
745         markHomeClub();
746         sortScoreList();
747         scoreListModel->update(scoreList);
748         list->update();
749     }
750 }
751
752 void MainWindow::loadScoreFile(QString &fileName, QList<Score *> &list)
753 {
754     ScoreXmlHandler handler(list);
755
756     if (handler.parse(fileName))
757         qDebug() << "File loaded:" << fileName << " entries:" << list.size();
758 }
759
760 void MainWindow::saveScoreFile(QString &fileName, QList<Score *> &list)
761 {
762     ScoreXmlHandler handler(list);
763
764     if (handler.save(fileName))
765         // TODO: banner
766         qDebug() << "File saved:" << fileName << " entries:" << list.size();
767     else
768         qWarning() << "Unable to save:" << fileName;
769 }
770
771 void MainWindow::loadClubFile(QString &fileName, QList<Club *> &list, bool readOnly)
772 {
773     ClubXmlHandler handler(list);
774
775     if (handler.parse(fileName, readOnly))
776         qDebug() << "File loaded:" << fileName << " entries:" << list.size();
777 }
778
779 void MainWindow::saveClubFile(QString &fileName, QList<Club *> &list)
780 {
781     ClubXmlHandler handler(list);
782
783     if (handler.save(fileName))
784         // TODO: banner
785         qDebug() << "File saved:" << fileName << " entries:" << list.size();
786     else
787         qWarning() << "Unable to save:" << fileName;
788 }