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