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