Initial new score dialogs
[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.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         Club *club = findClub(clubName);
480         if (!club) {
481             showNote(tr("No club"));
482             return;
483         }
484         Course *course = club->getCourse(courseName);
485         if (!course) {
486             showNote(tr("Error: no such course:"));
487             return;
488         }
489         scoreDialog->init(course);
490         result = scoreDialog->exec();
491         if (result) {
492             QVector<QString> scores(18);
493
494             scoreDialog->results(scores);
495             Score *score = new Score(scores, clubName, courseName, date);
496             scoreList << score;
497
498             // Sort the scores based on settings
499             sortScoreList();
500             // Save it
501             saveScoreFile(scoreFile, scoreList);
502             scoreListModel->update(scoreList);
503             list->update();
504         }
505     }
506 }
507
508 void MainWindow::editScore()
509 {
510     TRACE;
511     Course * course = 0;
512     Score *score = currentScore();
513
514     if (score) 
515         course = findCourse(score->getClubName(), score->getCourseName());
516
517     if (!course || !score) {
518         showNote(tr("No score or course to edit"));
519         return;
520     }
521
522     QString date = score->getDate();
523
524     ScoreDialog18 *scoreDialog = new ScoreDialog18(this);
525     scoreDialog->init(course, score);
526   
527     QString title = "Edit Score: " + course->getName() + ", " + date;
528     scoreDialog->setWindowTitle(title);
529
530     int result = scoreDialog->exec();
531     if (result) {
532         QVector<QString> scores(18);
533
534         scoreDialog->results(scores);
535     
536         score->update(scores);
537
538         // Sort the scores based on dates
539         qSort(scoreList.begin(), scoreList.end(), dateMoreThan); 
540         // Save it
541         saveScoreFile(scoreFile, scoreList);
542     }
543     if (scoreDialog)
544         delete scoreDialog;
545 }
546
547 void MainWindow::deleteScore()
548 {
549     TRACE;
550     if (scoreWindow)
551         scoreWindow->close();
552
553     QModelIndex index = selectionModel->currentIndex();
554     if (!index.isValid()) {
555         qDebug() << "Invalid index";
556         return;
557     }
558     
559     scoreList.removeAt(index.row());
560     // Save it
561     saveScoreFile(scoreFile, scoreList);
562     scoreListModel->update(scoreList);
563     list->update();
564 }
565
566 void MainWindow::viewCourse(Course * course)
567 {
568     TRACE;
569     courseWindow->setup(course);
570     courseWindow->show();
571 }
572
573 void MainWindow::newCourse()
574 {
575     TRACE;
576     CourseSelectDialog *selectDialog = new CourseSelectDialog(this);
577
578     int result = selectDialog->exec();
579     if (result) {
580         QString clubName;
581         QString courseName;
582         QString date;
583
584         selectDialog->results(clubName, courseName);
585
586         CourseDialog *courseDialog = new CourseDialog(this);
587         courseDialog->init();
588         QString title = "New Course: " + clubName + "," + courseName;
589         courseDialog->setWindowTitle(title);
590
591         int result = courseDialog->exec();
592         if (result) {
593             QVector<QString> par(18);
594             QVector<QString> hcp(18);
595             QVector<QString> len(18);
596
597             courseDialog->results(par, hcp, len);
598
599             Course *course = 0;
600             Club *club = findClub(clubName);
601             if (club) {
602                 course = club->getCourse(courseName);
603                 if (course) {
604                     showNote(tr("Club/course already in the database"));
605                     return;
606                 }
607                 else {
608                     course = new Course(courseName, par, hcp);
609                     club->addCourse(course);
610                 }
611             }
612             else {
613                 // New club and course
614                 club = new Club(clubName);
615                 course = new Course(courseName, par, hcp);
616                 club->addCourse(course);
617                 clubList << club;
618             }
619             // Save it
620             saveClubFile(clubFile, clubList);
621             courseListModel->update(clubList);
622             list->update();
623         }
624     }
625 }
626
627 void MainWindow::editCourse()
628 {
629     TRACE;
630     Course *course = currentCourse();
631
632     if (!course) {
633         showNote(tr("No course on edit"));
634         return;
635     }
636
637     CourseDialog *courseDialog = new CourseDialog(this);
638     courseDialog->init(course);
639
640     QString title = "Edit Course: " + course->getName();
641     courseDialog->setWindowTitle(title);
642   
643     int result = courseDialog->exec();
644     if (result) {
645         QVector<QString> par(18);
646         QVector<QString> hcp(18);
647         QVector<QString> len(18);
648     
649         courseDialog->results(par, hcp, len);
650     
651         course->update(par, hcp, len);
652         saveClubFile(clubFile, clubList);
653     }
654     if (courseDialog)
655         delete courseDialog;
656 }
657
658 void MainWindow::deleteCourse()
659 {
660     TRACE;
661     Club *club = 0;
662     Course * course = currentCourse();
663
664     if (!course) {
665         qDebug() << "Invalid course for deletion";
666         return;
667     }
668     club = course->parent();
669
670     // Can not delete course if it has scores -- check
671     if (findScore(club->getName(), course->getName()) != 0) {
672         showNote(tr("Can not delete course, delete scores on the course first"));
673         return;
674     }
675     // Close the window
676     if (courseWindow)
677         courseWindow->close();
678
679     club->delCourse(course);
680
681     if (club->isEmpty()) {
682         int index = clubList.indexOf(club);
683         if (index != -1)
684             clubList.removeAt(index);
685     }
686
687     // Save it
688     saveClubFile(clubFile, clubList);
689     courseListModel->update(clubList);
690     list->update();
691 }
692
693 void MainWindow::viewStatistics()
694 {
695     TRACE;
696     QMainWindow *win = new QMainWindow(this);
697     QString title = "Statistics";
698     win->setWindowTitle(title);
699 #ifdef Q_WS_MAEMO_5
700     win->setAttribute(Qt::WA_Maemo5StackedWindow);
701 #endif
702
703     StatModel *model = new StatModel(clubList, scoreList);
704
705     QTableView *table = new QTableView;
706     table->showGrid();
707     table->setSelectionMode(QAbstractItemView::NoSelection);
708     table->setStyleSheet(statStyleSheet);
709     table->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
710     table->verticalHeader()->setResizeMode(QHeaderView::Stretch);
711     table->verticalHeader()->setAutoFillBackground(true);
712     table->setModel(model);
713
714     QWidget *central = new QWidget(win);
715     win->setCentralWidget(central);
716
717     QTextEdit *textEdit = new QTextEdit;
718
719     textEdit->setReadOnly(true);
720
721     QVBoxLayout *infoLayout = new QVBoxLayout;
722     infoLayout->addWidget(table);
723
724     QHBoxLayout *mainLayout = new QHBoxLayout(central);
725     mainLayout->addLayout(infoLayout);
726     central->setLayout(mainLayout);
727
728     win->show();
729 }
730
731 void MainWindow::viewSettings()
732 {
733     TRACE;
734     SettingsDialog *dlg = new SettingsDialog(this);
735
736     dlg->init(conf, clubList);
737
738     int result = dlg->exec();
739     if (result) {
740         QString oldValue = conf.defaultCourses.toString();
741         dlg->results(conf);
742         QString newValue = conf.defaultCourses.toString();
743         saveSettings();
744
745         // Reload club list, or drop r/o courses from list
746         if (oldValue == "Yes" && newValue == "No") {
747             flushReadOnlyItems();
748             courseListModel->update(clubList);
749             list->update();
750         }
751         else if ((oldValue == "No" || oldValue == "") && newValue == "Yes") {
752             loadClubFile(masterFile, clubList, true);
753             courseListModel->update(clubList);
754             list->update();
755         }
756         // TODO: do these only if needed
757         markHomeClub();
758         sortScoreList();
759         scoreListModel->update(scoreList);
760         list->update();
761     }
762 }
763
764 void MainWindow::loadScoreFile(QString &fileName, QList<Score *> &list)
765 {
766     ScoreXmlHandler handler(list);
767
768     if (handler.parse(fileName))
769         qDebug() << "File loaded:" << fileName << " entries:" << list.size();
770 }
771
772 void MainWindow::saveScoreFile(QString &fileName, QList<Score *> &list)
773 {
774     ScoreXmlHandler handler(list);
775
776     if (handler.save(fileName))
777         // TODO: banner
778         qDebug() << "File saved:" << fileName << " entries:" << list.size();
779     else
780         qWarning() << "Unable to save:" << fileName;
781 }
782
783 void MainWindow::loadClubFile(QString &fileName, QList<Club *> &list, bool readOnly)
784 {
785     ClubXmlHandler handler(list);
786
787     if (handler.parse(fileName, readOnly))
788         qDebug() << "File loaded:" << fileName << " entries:" << list.size();
789 }
790
791 void MainWindow::saveClubFile(QString &fileName, QList<Club *> &list)
792 {
793     ClubXmlHandler handler(list);
794
795     if (handler.save(fileName))
796         // TODO: banner
797         qDebug() << "File saved:" << fileName << " entries:" << list.size();
798     else
799         qWarning() << "Unable to save:" << fileName;
800 }