TODO list update. Initial screen for pro mode
[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/MyDocs/.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.userMode = settings.value(settingsUserMode);
279     conf.defaultCourses = settings.value(settingsDefaultCourses);
280     settings.endGroup();
281
282     // Use default courses if no settings for that
283     if (!conf.defaultCourses.isValid())
284         conf.defaultCourses = "Yes";
285
286     // Use date sort order if no settings for that
287     if (!conf.sortOrder.isValid())
288         conf.sortOrder = "Date";
289
290     // Use basic mode if no settings for that
291     if (!conf.userMode.isValid())
292         conf.userMode = "Basic";
293
294     qDebug() << "Settings: " << conf.hcp << conf.homeClub << conf.sortOrder << conf.userMode << conf.defaultCourses;
295 }
296
297 void MainWindow::saveSettings(void)
298 {
299     TRACE;
300     settings.beginGroup(settingsGroup);
301     if (conf.hcp.isValid())
302         settings.setValue(settingsHcp, conf.hcp);
303     if (conf.homeClub.isValid())
304         settings.setValue(settingsHomeClub, conf.homeClub);
305     if (conf.sortOrder.isValid())
306         settings.setValue(settingsSortOrder, conf.sortOrder);
307     if (conf.userMode.isValid())
308         settings.setValue(settingsUserMode, conf.userMode);
309     if (conf.defaultCourses.isValid())
310         settings.setValue(settingsDefaultCourses, conf.defaultCourses);
311     settings.endGroup();
312 }
313
314 void MainWindow::createLayoutList(QWidget *parent)
315 {
316     TRACE;
317     QVBoxLayout * tableLayout = new QVBoxLayout;
318     tableLayout->addWidget(list);
319
320     QHBoxLayout *mainLayout = new QHBoxLayout(parent);
321     mainLayout->addLayout(tableLayout);
322     parent->setLayout(mainLayout);
323 }
324
325 void MainWindow::createListView(QList<Score *> &scoreList, 
326                                 QList <Club *> &clubList)
327 {
328     TRACE;
329     list = new QListView(this);
330
331     scoreListModel = new ScoreListModel(scoreList, clubList);
332     courseListModel = new CourseListModel(clubList);
333
334     list->setStyleSheet(defaultStyleSheet);
335
336     list->setSelectionMode(QAbstractItemView::SingleSelection);
337     list->setProperty("FingerScrolling", true);
338
339     // Initial view
340     listScores();
341
342     connect(list, SIGNAL(clicked(QModelIndex)),
343             this, SLOT(clickedList(QModelIndex)));
344 }
345
346 void MainWindow::listScores()
347 {
348     TRACE;
349     list->setModel(scoreListModel);
350     selectionModel = list->selectionModel();
351     updateTitleBar(titleScores);
352 }
353
354 void MainWindow::listCourses()
355 {
356     TRACE;
357     list->setModel(courseListModel);
358     selectionModel = list->selectionModel();
359     updateTitleBar(titleCourses);
360 }
361
362 void MainWindow::createActions()
363 {
364     TRACE;
365     newScoreAction = new QAction(tr("New Score"), this);
366     connect(newScoreAction, SIGNAL(triggered()), this, SLOT(newScore()));
367
368     newCourseAction = new QAction(tr("New Course"), this);
369     connect(newCourseAction, SIGNAL(triggered()), this, SLOT(newCourse()));
370
371     statAction = new QAction(tr("Statistics"), this);
372     connect(statAction, SIGNAL(triggered()), this, SLOT(viewStatistics()));
373
374     settingsAction = new QAction(tr("Settings"), this);
375     connect(settingsAction, SIGNAL(triggered()), this, SLOT(viewSettings()));
376
377     // Maemo5 style menu filters
378     filterGroup = new QActionGroup(this);
379     filterGroup->setExclusive(true);
380
381     listScoreAction = new QAction(tr("Scores"), filterGroup);
382     listScoreAction->setCheckable(true);
383     listScoreAction->setChecked(true);
384     connect(listScoreAction, SIGNAL(triggered()), this, SLOT(listScores()));
385
386     listCourseAction = new QAction(tr("Courses"), filterGroup);
387     listCourseAction->setCheckable(true);
388     connect(listCourseAction, SIGNAL(triggered()), this, SLOT(listCourses()));
389 }
390
391 void MainWindow::createMenus()
392 {
393     TRACE;
394 #ifdef Q_WS_MAEMO_5
395     menu = menuBar()->addMenu("");
396 #else
397     menu = menuBar()->addMenu("Menu");
398 #endif
399
400     menu->addAction(newScoreAction);
401     menu->addAction(newCourseAction);
402     menu->addAction(statAction);
403     menu->addAction(settingsAction);
404     menu->addActions(filterGroup->actions());
405 }
406
407 void MainWindow::updateTitleBar(QString & msg)
408 {
409     TRACE;
410     setWindowTitle(msg);
411 }
412
413 void MainWindow::showNote(QString msg)
414 {
415 #ifdef Q_WS_MAEMO_5
416     QMaemo5InformationBox::information(this, 
417                                        msg,
418                                        QMaemo5InformationBox::DefaultTimeout);
419 #endif
420 }
421
422 void MainWindow::clickedList(const QModelIndex &index)
423 {
424     TRACE;
425     int row = index.row();
426
427     const QAbstractItemModel *m = index.model();
428     if (m == scoreListModel) {
429         if (row < scoreList.count()) {
430             Score * score = scoreList.at(row);
431             Course * course = findCourse(score->getClubName(), score->getCourseName());
432             viewScore(score, course);
433         }
434     }
435     else if (m == courseListModel) {
436         QString str = courseListModel->data(index, Qt::DisplayRole).toString();
437         QStringList strList = str.split(", ");
438
439         if (strList.count() != 2) {
440             showNote(QString("Invalid course selection"));
441             return;
442         }
443         Course * course = findCourse(strList.at(0), strList.at(1));
444         viewCourse(course);
445     }
446 }
447
448 void MainWindow::viewScore(Score * score, Course * course)
449 {
450     TRACE;
451     scoreWindow->setup(score, course);
452     scoreWindow->show();
453 }
454
455 void MainWindow::newScore()
456 {
457     TRACE;
458     SelectDialog *selectDialog = new SelectDialog(this);
459
460     selectDialog->init(clubList);
461
462     int result = selectDialog->exec();
463     if (result) {
464         QString clubName;
465         QString courseName;
466         QString date;
467
468         selectDialog->results(clubName, courseName, date);
469
470         ScoreDialog *scoreDialog;
471         if (conf.userMode == "Basic")
472             scoreDialog = (ScoreDialog *)new ScoreDialog18(this);
473         else
474             scoreDialog = (ScoreDialog *)new ScoreDialogSingle(this);
475
476         QString title = "New Score: " + courseName + ", " + date;
477         scoreDialog->setWindowTitle(title);
478
479         qDebug() << clubName << courseName;
480
481         Club *club = findClub(clubName);
482         if (!club) {
483             showNote(tr("No club"));
484             return;
485         }
486         Course *course = club->getCourse(courseName);
487         if (!course) {
488             showNote(tr("Error: no such course:"));
489             return;
490         }
491         
492         scoreDialog->init(course);
493         result = scoreDialog->exec();
494         if (result) {
495             QVector<QString> scores(18);
496
497             scoreDialog->results(scores);
498             Score *score = new Score(scores, clubName, courseName, date);
499             scoreList << score;
500
501             // Sort the scores based on settings
502             sortScoreList();
503             // Save it
504             saveScoreFile(scoreFile, scoreList);
505             scoreListModel->update(scoreList);
506             list->update();
507         }
508     }
509 }
510
511 void MainWindow::editScore()
512 {
513     TRACE;
514     Course * course = 0;
515     Score *score = currentScore();
516
517     if (score) 
518         course = findCourse(score->getClubName(), score->getCourseName());
519
520     if (!course || !score) {
521         showNote(tr("No score or course to edit"));
522         return;
523     }
524
525     QString date = score->getDate();
526
527     ScoreDialog18 *scoreDialog = new ScoreDialog18(this);
528     scoreDialog->init(course, score);
529   
530     QString title = "Edit Score: " + course->getName() + ", " + date;
531     scoreDialog->setWindowTitle(title);
532
533     int result = scoreDialog->exec();
534     if (result) {
535         QVector<QString> scores(18);
536
537         scoreDialog->results(scores);
538     
539         score->update(scores);
540
541         // Sort the scores based on dates
542         qSort(scoreList.begin(), scoreList.end(), dateMoreThan); 
543         // Save it
544         saveScoreFile(scoreFile, scoreList);
545     }
546     if (scoreDialog)
547         delete scoreDialog;
548 }
549
550 void MainWindow::deleteScore()
551 {
552     TRACE;
553     if (scoreWindow)
554         scoreWindow->close();
555
556     QModelIndex index = selectionModel->currentIndex();
557     if (!index.isValid()) {
558         qDebug() << "Invalid index";
559         return;
560     }
561     
562     scoreList.removeAt(index.row());
563     // Save it
564     saveScoreFile(scoreFile, scoreList);
565     scoreListModel->update(scoreList);
566     list->update();
567 }
568
569 void MainWindow::viewCourse(Course * course)
570 {
571     TRACE;
572     courseWindow->setup(course);
573     courseWindow->show();
574 }
575
576 void MainWindow::newCourse()
577 {
578     TRACE;
579     CourseSelectDialog *selectDialog = new CourseSelectDialog(this);
580
581     int result = selectDialog->exec();
582     if (result) {
583         QString clubName;
584         QString courseName;
585         QString date;
586
587         selectDialog->results(clubName, courseName);
588
589         CourseDialog *courseDialog = new CourseDialog(this);
590         courseDialog->init();
591         QString title = "New Course: " + clubName + "," + courseName;
592         courseDialog->setWindowTitle(title);
593
594         int result = courseDialog->exec();
595         if (result) {
596             QVector<QString> par(18);
597             QVector<QString> hcp(18);
598             QVector<QString> len(18);
599
600             courseDialog->results(par, hcp, len);
601
602             Course *course = 0;
603             Club *club = findClub(clubName);
604             if (club) {
605                 course = club->getCourse(courseName);
606                 if (course) {
607                     showNote(tr("Club/course already in the database"));
608                     return;
609                 }
610                 else {
611                     course = new Course(courseName, par, hcp);
612                     club->addCourse(course);
613                 }
614             }
615             else {
616                 // New club and course
617                 club = new Club(clubName);
618                 course = new Course(courseName, par, hcp);
619                 club->addCourse(course);
620                 clubList << club;
621             }
622             // Save it
623             saveClubFile(clubFile, clubList);
624             courseListModel->update(clubList);
625             list->update();
626         }
627     }
628 }
629
630 void MainWindow::editCourse()
631 {
632     TRACE;
633     Course *course = currentCourse();
634
635     if (!course) {
636         showNote(tr("No course on edit"));
637         return;
638     }
639
640     CourseDialog *courseDialog = new CourseDialog(this);
641     courseDialog->init(course);
642
643     QString title = "Edit Course: " + course->getName();
644     courseDialog->setWindowTitle(title);
645   
646     int result = courseDialog->exec();
647     if (result) {
648         QVector<QString> par(18);
649         QVector<QString> hcp(18);
650         QVector<QString> len(18);
651     
652         courseDialog->results(par, hcp, len);
653     
654         course->update(par, hcp, len);
655         saveClubFile(clubFile, clubList);
656     }
657     if (courseDialog)
658         delete courseDialog;
659 }
660
661 void MainWindow::deleteCourse()
662 {
663     TRACE;
664     Club *club = 0;
665     Course * course = currentCourse();
666
667     if (!course) {
668         qDebug() << "Invalid course for deletion";
669         return;
670     }
671     club = course->parent();
672
673     // Can not delete course if it has scores -- check
674     if (findScore(club->getName(), course->getName()) != 0) {
675         showNote(tr("Can not delete course, delete scores on the course first"));
676         return;
677     }
678     // Close the window
679     if (courseWindow)
680         courseWindow->close();
681
682     club->delCourse(course);
683
684     if (club->isEmpty()) {
685         int index = clubList.indexOf(club);
686         if (index != -1)
687             clubList.removeAt(index);
688     }
689
690     // Save it
691     saveClubFile(clubFile, clubList);
692     courseListModel->update(clubList);
693     list->update();
694 }
695
696 void MainWindow::viewStatistics()
697 {
698     TRACE;
699     QMainWindow *win = new QMainWindow(this);
700     QString title = "Statistics";
701     win->setWindowTitle(title);
702 #ifdef Q_WS_MAEMO_5
703     win->setAttribute(Qt::WA_Maemo5StackedWindow);
704 #endif
705
706     StatModel *model = new StatModel(clubList, scoreList);
707
708     QTableView *table = new QTableView;
709     table->showGrid();
710     table->setSelectionMode(QAbstractItemView::NoSelection);
711     table->setStyleSheet(statStyleSheet);
712     table->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
713     table->verticalHeader()->setResizeMode(QHeaderView::Stretch);
714     table->verticalHeader()->setAutoFillBackground(true);
715     table->setModel(model);
716
717     QWidget *central = new QWidget(win);
718     win->setCentralWidget(central);
719
720     QTextEdit *textEdit = new QTextEdit;
721
722     textEdit->setReadOnly(true);
723
724     QVBoxLayout *infoLayout = new QVBoxLayout;
725     infoLayout->addWidget(table);
726
727     QHBoxLayout *mainLayout = new QHBoxLayout(central);
728     mainLayout->addLayout(infoLayout);
729     central->setLayout(mainLayout);
730
731     win->show();
732 }
733
734 void MainWindow::viewSettings()
735 {
736     TRACE;
737     SettingsDialog *dlg = new SettingsDialog(this);
738
739     dlg->init(conf, clubList);
740
741     int result = dlg->exec();
742     if (result) {
743         QString oldValue = conf.defaultCourses.toString();
744         dlg->results(conf);
745         QString newValue = conf.defaultCourses.toString();
746         saveSettings();
747
748         // Reload club list, or drop r/o courses from list
749         if (oldValue == "Yes" && newValue == "No") {
750             flushReadOnlyItems();
751             courseListModel->update(clubList);
752             list->update();
753         }
754         else if ((oldValue == "No" || oldValue == "") && newValue == "Yes") {
755             loadClubFile(masterFile, clubList, true);
756             courseListModel->update(clubList);
757             list->update();
758         }
759         // TODO: do these only if needed
760         markHomeClub();
761         sortScoreList();
762         scoreListModel->update(scoreList);
763         list->update();
764     }
765 }
766
767 void MainWindow::loadScoreFile(QString &fileName, QList<Score *> &list)
768 {
769     ScoreXmlHandler handler(list);
770
771     if (handler.parse(fileName))
772         qDebug() << "File loaded:" << fileName << " entries:" << list.size();
773 }
774
775 void MainWindow::saveScoreFile(QString &fileName, QList<Score *> &list)
776 {
777     ScoreXmlHandler handler(list);
778
779     if (handler.save(fileName))
780         // TODO: banner
781         qDebug() << "File saved:" << fileName << " entries:" << list.size();
782     else
783         qWarning() << "Unable to save:" << fileName;
784 }
785
786 void MainWindow::loadClubFile(QString &fileName, QList<Club *> &list, bool readOnly)
787 {
788     ClubXmlHandler handler(list);
789
790     if (handler.parse(fileName, readOnly))
791         qDebug() << "File loaded:" << fileName << " entries:" << list.size();
792 }
793
794 void MainWindow::saveClubFile(QString &fileName, QList<Club *> &list)
795 {
796     ClubXmlHandler handler(list);
797
798     if (handler.save(fileName))
799         // TODO: banner
800         qDebug() << "File saved:" << fileName << " entries:" << list.size();
801     else
802         qWarning() << "Unable to save:" << fileName;
803 }