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