]> git.sesse.net Git - pkanalytics/blob - main.cpp
Fix some -Wsign-compare warnings.
[pkanalytics] / main.cpp
1 #include <QMediaPlayer>
2 #include <QMainWindow>
3 #include <QApplication>
4 #include <QGridLayout>
5 #include <QVideoWidget>
6 #include <QShortcut>
7 #include <QInputDialog>
8 #include <QTimer>
9 #include <algorithm>
10 #include <string>
11 #include <map>
12 #include <vector>
13 #include <optional>
14 #include <sqlite3.h>
15 #include "mainwindow.h"
16 #include "ui_mainwindow.h"
17 #include "events.h"
18 #include "players.h"
19 #include "formations.h"
20 #include "json.h"
21
22 using namespace std;
23
24 string format_timestamp(uint64_t pos)
25 {
26         int ms = pos % 1000;
27         pos /= 1000;
28         int sec = pos % 60;
29         pos /= 60;
30         int min = pos % 60;
31         int hour = pos / 60;
32
33         char buf[256];
34         snprintf(buf, sizeof(buf), "%d:%02d:%02d.%03d", hour, min, sec, ms);
35         return buf;
36 }
37
38 MainWindow::MainWindow(EventsModel *events, PlayersModel *players,
39                        FormationsModel *offensive_formations, FormationsModel *defensive_formations)
40         : events(events), players(players), offensive_formations(offensive_formations), defensive_formations(defensive_formations)
41 {
42         video = new QMediaPlayer;
43         //video->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate.mkv"));
44         video->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate-prores.mkv"));
45         video->play();
46
47         ui = new Ui::MainWindow;
48         ui->setupUi(this);
49
50         ui->event_view->setModel(events);
51         ui->event_view->setColumnWidth(1, 150);
52         ui->event_view->setColumnWidth(2, 150);
53         connect(ui->event_view->selectionModel(), &QItemSelectionModel::currentRowChanged,
54                 [this, events](const QModelIndex &current, const QModelIndex &previous) {
55                         int64_t t = events->get_time(current.row());
56                         if (t != video->position()) {
57                                 video->setPosition(events->get_time(current.row()));
58                         } else {
59                                 // Selection could have changed, so we still need to update.
60                                 // (Just calling setPosition() would not give us the signal
61                                 // in this case.)
62                                 update_ui_from_time(t);
63                         }
64                 });
65
66         ui->player_view->setModel(players);
67         ui->player_view->setColumnWidth(0, 30);
68         ui->player_view->setColumnWidth(1, 20);
69         ui->player_view->horizontalHeader()->setStretchLastSection(true);
70
71         auto formation_changed = [this](const QModelIndex &current, const QModelIndex &previous) {
72                 QTimer::singleShot(1, [=]{  // The selection is wrong until the callback actually returns.
73                         update_action_buttons(video->position());
74                 });
75         };
76         ui->offensive_formation_view->setModel(offensive_formations);
77         ui->defensive_formation_view->setModel(defensive_formations);
78         connect(ui->offensive_formation_view->selectionModel(), &QItemSelectionModel::currentRowChanged, formation_changed);
79         connect(ui->defensive_formation_view->selectionModel(), &QItemSelectionModel::currentRowChanged, formation_changed);
80         connect(ui->offensive_formation_view, &QListView::doubleClicked, [this](const QModelIndex &index) {
81                 formation_double_clicked(true, index.row());
82         });
83         connect(ui->defensive_formation_view, &QListView::doubleClicked, [this](const QModelIndex &index) {
84                 formation_double_clicked(false, index.row());
85         });
86
87         connect(video, &QMediaPlayer::positionChanged, [this](uint64_t pos) {
88                 position_changed(pos);
89         });
90
91         video->setVideoOutput(ui->video);
92
93         // It's not really clear whether PgUp should be forwards or backwards,
94         // but mpv does at least up = forwards, so that's probably standard.
95         QShortcut *pgdown = new QShortcut(QKeySequence(Qt::Key_PageDown), this);
96         connect(pgdown, &QShortcut::activated, [this]() { seek(-120000); });
97         QShortcut *pgup = new QShortcut(QKeySequence(Qt::Key_PageUp), this);
98         connect(pgup, &QShortcut::activated, [this]() { seek(120000); });
99
100         // Ugh. Used when Qt messes up and hangs the video.
101         QShortcut *f5 = new QShortcut(QKeySequence(Qt::Key_F5), this);
102         connect(f5, &QShortcut::activated, [this]() {
103                 QVideoWidget *nvw = new QVideoWidget(ui->video->parentWidget());
104                 nvw->setObjectName("video");
105                 nvw->setMinimumSize(QSize(320, 240));
106                 video->setVideoOutput(nvw);
107                 ui->main_grid->replaceWidget(ui->video, nvw);
108                 ui->video = nvw;
109         });
110
111         connect(ui->minus10s, &QPushButton::clicked, [this]() { seek(-10000); });
112         connect(ui->plus10s, &QPushButton::clicked, [this]() { seek(10000); });
113
114         connect(ui->minus2s, &QPushButton::clicked, [this]() { seek(-2000); });
115         connect(ui->plus2s, &QPushButton::clicked, [this]() { seek(2000); });
116
117         // TODO: Would be nice to actually have a frame...
118         connect(ui->minus1f, &QPushButton::clicked, [this]() { seek(-20); });
119         connect(ui->plus1f, &QPushButton::clicked, [this]() { seek(20); });
120
121         connect(ui->play_pause, &QPushButton::clicked, [this]() {
122                 if (playing) {
123                         video->pause();
124                         ui->play_pause->setText("Play (space)");
125                 } else {
126                         video->setPlaybackRate(1.0);
127                         video->play();
128                         ui->play_pause->setText("Pause (space)");
129                 }
130                 playing = !playing;
131
132                 // Needs to be set anew when we modify setText(), evidently.
133                 ui->play_pause->setShortcut(QCoreApplication::translate("MainWindow", "Space", nullptr));
134         });
135
136         connect(ui->player_1, &QPushButton::clicked, [this]() { insert_player_event(0); });
137         connect(ui->player_2, &QPushButton::clicked, [this]() { insert_player_event(1); });
138         connect(ui->player_3, &QPushButton::clicked, [this]() { insert_player_event(2); });
139         connect(ui->player_4, &QPushButton::clicked, [this]() { insert_player_event(3); });
140         connect(ui->player_5, &QPushButton::clicked, [this]() { insert_player_event(4); });
141         connect(ui->player_6, &QPushButton::clicked, [this]() { insert_player_event(5); });
142         connect(ui->player_7, &QPushButton::clicked, [this]() { insert_player_event(6); });
143
144         // Offensive events
145         connect(ui->offense_label, &ClickableLabel::clicked, [this]() { insert_noplayer_event("set_offense"); });
146         connect(ui->catch_, &QPushButton::clicked, [this]() { set_current_event_type("catch"); });
147         connect(ui->throwaway, &QPushButton::clicked, [this, events]() {
148                 EventsModel::Status s = events->get_status_at(video->position());
149                 if (s.attack_state == EventsModel::Status::DEFENSE && s.pull_state == EventsModel::Status::PULL_IN_AIR) {
150                         insert_noplayer_event("pull_oob");
151                 } else {
152                         set_current_event_type("throwaway");
153                 }
154         });
155         connect(ui->drop, &QPushButton::clicked, [this]() { set_current_event_type("drop"); });
156         connect(ui->goal, &QPushButton::clicked, [this]() { set_current_event_type("goal"); });
157         connect(ui->offensive_soft_plus, &QPushButton::clicked, [this]() { set_current_event_type("offensive_soft_plus"); });
158         connect(ui->offensive_soft_minus, &QPushButton::clicked, [this]() { set_current_event_type("offensive_soft_minus"); });
159         connect(ui->pull, &QPushButton::clicked, [this, events]() {
160                 EventsModel::Status s = events->get_status_at(video->position());
161                 if (s.pull_state == EventsModel::Status::SHOULD_PULL) {
162                         set_current_event_type("pull");
163                 } else if (s.pull_state == EventsModel::Status::PULL_IN_AIR) {
164                         insert_noplayer_event("pull_landed");
165                 }
166         });
167
168         // Defensive events (TODO add more)
169         connect(ui->interception, &QPushButton::clicked, [this]() { set_current_event_type("interception"); });
170         connect(ui->defense_label, &ClickableLabel::clicked, [this]() { insert_noplayer_event("set_defense"); });
171         connect(ui->their_throwaway, &QPushButton::clicked, [this]() { insert_noplayer_event("their_throwaway"); });
172         connect(ui->their_goal, &QPushButton::clicked, [this]() { insert_noplayer_event("their_goal"); });
173         connect(ui->their_pull, &QPushButton::clicked, [this, events]() {
174                 EventsModel::Status s = events->get_status_at(video->position());
175                 if (s.pull_state == EventsModel::Status::SHOULD_PULL) {
176                         insert_noplayer_event("their_pull");
177                 }
178         });
179         connect(ui->our_defense, &QPushButton::clicked, [this]() { set_current_event_type("defense"); });
180         connect(ui->defensive_soft_plus, &QPushButton::clicked, [this]() { set_current_event_type("defensive_soft_plus"); });
181         connect(ui->defensive_soft_minus, &QPushButton::clicked, [this]() { set_current_event_type("defensive_soft_minus"); });
182
183         connect(ui->offensive_formation, &QPushButton::clicked, [this]() { insert_or_change_formation(/*offense=*/true); });
184         connect(ui->defensive_formation, &QPushButton::clicked, [this]() { insert_or_change_formation(/*offense=*/false); });
185
186         // Misc. events
187         connect(ui->substitution, &QPushButton::clicked, [this]() { make_substitution(); });
188         connect(ui->stoppage, &QPushButton::clicked, [this, events]() {
189                 EventsModel::Status s = events->get_status_at(video->position());
190                 if (s.stoppage) {
191                         insert_noplayer_event("restart");
192                 } else {
193                         insert_noplayer_event("stoppage");
194                 }
195         });
196         connect(ui->unknown, &QPushButton::clicked, [this]() { insert_noplayer_event("unknown"); });
197
198         QShortcut *key_delete = new QShortcut(QKeySequence(Qt::Key_Delete), this);
199         connect(key_delete, &QShortcut::activated, [this]() { ui->delete_->animateClick(); });
200         connect(ui->delete_, &QPushButton::clicked, [this]() { delete_current_event(); });
201 }
202
203 void MainWindow::position_changed(uint64_t pos)
204 {
205         ui->timestamp->setText(QString::fromUtf8(format_timestamp(pos)));
206         if (buffered_seek) {
207                 video->setPosition(*buffered_seek);
208                 buffered_seek.reset();
209         }
210         if (!playing) {
211                 video->pause();  // We only played to get a picture.
212         }
213         if (playing) {
214                 QModelIndex row = events->get_last_event_qt(video->position());
215                 ui->event_view->scrollTo(row, QAbstractItemView::PositionAtCenter);
216         }
217         update_ui_from_time(pos);
218 }
219
220 void MainWindow::seek(int64_t delta_ms)
221 {
222         int64_t current_pos = buffered_seek ? *buffered_seek : video->position();
223         uint64_t pos = max<int64_t>(current_pos + delta_ms, 0);
224         buffered_seek = pos;
225         if (!playing) {
226                 video->setPlaybackRate(0.01);
227                 video->play();  // Or Qt won't show the seek.
228         }
229 }
230
231 void MainWindow::insert_player_event(int button_id)
232 {
233         uint64_t t = video->position();
234         vector<int> team = events->sort_team(events->get_team_at(t));
235         if (unsigned(button_id) >= team.size()) {
236                 return;
237         }
238         int player_id = team[button_id];
239
240         EventsModel::Status s = events->get_status_at(t);
241
242         ui->event_view->selectionModel()->blockSignals(true);
243         if (s.attack_state == EventsModel::Status::OFFENSE) {
244                 // TODO: Perhaps not if that player already did the last catch?
245                 ui->event_view->selectRow(events->insert_event(t, player_id, nullopt, "catch"));
246         } else {
247                 ui->event_view->selectRow(events->insert_event(t, player_id, nullopt));
248         }
249         ui->event_view->selectionModel()->blockSignals(false);
250
251         update_ui_from_time(t);
252 }
253
254 void MainWindow::insert_noplayer_event(const string &type)
255 {
256         uint64_t t = video->position();
257
258         ui->event_view->selectionModel()->blockSignals(true);
259         ui->event_view->selectRow(events->insert_event(t, nullopt, nullopt, type));
260         ui->event_view->selectionModel()->blockSignals(false);
261
262         update_ui_from_time(t);
263 }
264
265 void MainWindow::set_current_event_type(const string &type)
266 {
267         QItemSelectionModel *select = ui->event_view->selectionModel();
268         if (!select->hasSelection()) {
269                 return;
270         }
271         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
272         events->set_event_type(row, type);
273         update_ui_from_time(video->position());
274 }
275
276 // Formation buttons either modify the existing formation (if we've selected
277 // a formation change event), or insert a new one (if not).
278 void MainWindow::insert_or_change_formation(bool offense)
279 {
280         FormationsModel *formations = offense ? offensive_formations : defensive_formations;
281         QListView *formation_view = offense ? ui->offensive_formation_view : ui->defensive_formation_view;
282         if (!formation_view->selectionModel()->hasSelection()) {
283                 // This shouldn't happen; the button should not have been enabled.
284                 return;
285         }
286         int formation_row = formation_view->selectionModel()->selectedRows().front().row();  // Should only be one, due to our selection behavior.
287         int formation_id = formations->get_formation_id(formation_row);
288         if (formation_id == -1) {
289                 // This also shouldn't happen (“Add new…” selected).
290                 return;
291         }
292
293         QItemSelectionModel *select = ui->event_view->selectionModel();
294         if (select->hasSelection()) {
295                 int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
296                 string expected_type = offense ? "formation_offense" : "formation_defense";
297                 if (events->get_event_type(row) == expected_type) {
298                         events->set_event_formation(row, formation_id);
299                         update_ui_from_time(video->position());
300                         return;
301                 }
302         }
303
304         // Insert a new formation event instead (same as double-click on the selected one).
305         events->set_formation_at(video->position(), offense, formation_id);
306         update_ui_from_time(video->position());
307 }
308
309 void MainWindow::delete_current_event()
310 {
311         QItemSelectionModel *select = ui->event_view->selectionModel();
312         if (!select->hasSelection()) {
313                 return;
314         }
315         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
316         ui->event_view->selectionModel()->blockSignals(true);
317         events->delete_event(row);
318         ui->event_view->selectionModel()->blockSignals(false);
319         update_ui_from_time(video->position());
320 }
321
322 void MainWindow::make_substitution()
323 {
324         QItemSelectionModel *select = ui->player_view->selectionModel();
325         set<int> new_team;
326         for (QModelIndex row : select->selectedRows()) {
327                 new_team.insert(players->get_player_id(row.row()));
328         }
329         events->set_team_at(video->position(), new_team);
330 }
331
332 void MainWindow::update_ui_from_time(uint64_t t)
333 {
334         update_status(t);
335         update_player_buttons(t);
336         update_action_buttons(t);
337 }
338
339 void MainWindow::update_status(uint64_t t)
340 {
341         EventsModel::Status s = events->get_status_at(t);
342         char buf[256];
343         std::string formation = "Not started";
344         if (s.attack_state == EventsModel::Status::OFFENSE) {
345                 if (s.offensive_formation != 0) {
346                         formation = offensive_formations->get_formation_name_by_id(s.offensive_formation);
347                 } else {
348                         formation = "Offense";
349                 }
350         } else if (s.attack_state == EventsModel::Status::DEFENSE) {
351                 if (s.defensive_formation != 0) {
352                         formation = defensive_formations->get_formation_name_by_id(s.defensive_formation);
353                 } else {
354                         formation = "Defense";
355                 }
356         }
357
358         snprintf(buf, sizeof(buf), "%d–%d | %s | %d passes, %d sec possession",
359                 s.our_score, s.their_score, formation.c_str(), s.num_passes, s.possession_sec);
360         if (s.stoppage_sec > 0) {
361                 char buf2[256];
362                 snprintf(buf2, sizeof(buf2), "%s (plus %d sec stoppage)", buf, s.stoppage_sec);
363                 ui->status->setText(buf2);
364         } else {
365                 ui->status->setText(buf);
366         }
367 }
368
369 void MainWindow::update_player_buttons(uint64_t t)
370 {
371         QPushButton *buttons[] = {
372                 ui->player_1,
373                 ui->player_2,
374                 ui->player_3,
375                 ui->player_4,
376                 ui->player_5,
377                 ui->player_6,
378                 ui->player_7
379         };
380         const char shortcuts[] = "qweasdf";
381         int num_players = 0;
382         for (int player_id : events->sort_team(events->get_team_at(t))) {
383                 QPushButton *btn = buttons[num_players];
384                 string label = players->get_player_name_by_id(player_id) + " (&" + shortcuts[num_players] + ")";
385                 char shortcut[2] = "";
386                 shortcut[0] = toupper(shortcuts[num_players]);
387                 btn->setText(QString::fromUtf8(label));
388                 btn->setShortcut(QCoreApplication::translate("MainWindow", shortcut, nullptr));
389                 btn->setEnabled(true);
390                 if (++num_players == 7) {
391                         break;
392                 }
393         }
394         for (int i = num_players; i < 7; ++i) {
395                 QPushButton *btn = buttons[i];
396                 btn->setText("No player");
397                 btn->setEnabled(false);
398         }
399 }
400
401 void MainWindow::update_action_buttons(uint64_t t)
402 {
403         {
404                 QItemSelectionModel *select = ui->offensive_formation_view->selectionModel();
405                 if (select->hasSelection()) {
406                         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
407                         ui->offensive_formation->setEnabled(offensive_formations->get_formation_id(row) != -1);
408                 } else {
409                         ui->offensive_formation->setEnabled(false);
410                 }
411         }
412         {
413                 QItemSelectionModel *select = ui->defensive_formation_view->selectionModel();
414                 if (select->hasSelection()) {
415                         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
416                         ui->defensive_formation->setEnabled(defensive_formations->get_formation_id(row) != -1);
417                 } else {
418                         ui->defensive_formation->setEnabled(false);
419                 }
420         }
421
422         EventsModel::Status s = events->get_status_at(t);
423
424         bool has_selection = false;
425         bool has_selection_with_player = false;
426
427         QItemSelectionModel *select = ui->event_view->selectionModel();
428         if (select->hasSelection()) {
429                 has_selection = true;
430                 int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
431                 has_selection_with_player = events->get_player_id(row).has_value();
432         }
433         ui->delete_->setEnabled(has_selection);
434
435         if (s.stoppage) {
436                 ui->stoppage->setText("Restart (&v)");
437                 ui->stoppage->setShortcut(QCoreApplication::translate("MainWindow", "V", nullptr));
438                 ui->catch_->setEnabled(false);
439                 ui->throwaway->setEnabled(false);
440                 ui->drop->setEnabled(false);
441                 ui->goal->setEnabled(false);
442                 ui->offensive_soft_plus->setEnabled(false);
443                 ui->offensive_soft_minus->setEnabled(false);
444                 ui->pull->setEnabled(false);
445                 ui->interception->setEnabled(false);
446                 ui->their_throwaway->setEnabled(false);
447                 ui->our_defense->setEnabled(false);
448                 ui->their_goal->setEnabled(false);
449                 ui->defensive_soft_plus->setEnabled(false);
450                 ui->defensive_soft_minus->setEnabled(false);
451                 ui->their_pull->setEnabled(false);
452                 return;
453         } else {
454                 ui->stoppage->setText("Stoppage (&v)");
455                 ui->stoppage->setShortcut(QCoreApplication::translate("MainWindow", "V", nullptr));
456         }
457
458         // Defaults for pull-related buttons.
459         ui->pull->setText("Pull (&p)");
460         ui->their_pull->setText("Their pull (&p)");
461         ui->pull->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr));
462         ui->their_pull->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr));
463         ui->throwaway->setText("Throwaway (&t)");
464         ui->throwaway->setShortcut(QCoreApplication::translate("MainWindow", "T", nullptr));
465
466         if (s.pull_state == EventsModel::Status::SHOULD_PULL) {
467                 ui->pull->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
468                 ui->their_pull->setEnabled(s.attack_state == EventsModel::Status::OFFENSE);
469
470                 ui->catch_->setEnabled(false);
471                 ui->throwaway->setEnabled(false);
472                 ui->drop->setEnabled(false);
473                 ui->goal->setEnabled(false);
474                 ui->offensive_soft_plus->setEnabled(false);
475                 ui->offensive_soft_minus->setEnabled(false);
476                 ui->interception->setEnabled(false);
477                 ui->their_throwaway->setEnabled(false);
478                 ui->our_defense->setEnabled(false);
479                 ui->their_goal->setEnabled(false);
480                 ui->defensive_soft_plus->setEnabled(false);
481                 ui->defensive_soft_minus->setEnabled(false);
482                 return;
483         }
484         if (s.pull_state == EventsModel::Status::PULL_IN_AIR) {
485                 if (s.attack_state == EventsModel::Status::DEFENSE) {
486                         ui->pull->setText("Pull landed (&p)");
487                         ui->pull->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr));
488                         ui->pull->setEnabled(true);
489
490                         ui->throwaway->setText("Pull OOB (&t)");
491                         ui->throwaway->setShortcut(QCoreApplication::translate("MainWindow", "T", nullptr));
492                         ui->throwaway->setEnabled(true);
493                 } else {
494                         ui->pull->setEnabled(false);
495                         ui->throwaway->setEnabled(false);
496                 }
497                 ui->their_pull->setEnabled(false);  // We don't track their pull landings; only by means of catch etc.
498
499                 ui->catch_->setEnabled(false);
500                 ui->drop->setEnabled(false);
501                 ui->goal->setEnabled(false);
502                 ui->offensive_soft_plus->setEnabled(false);
503                 ui->offensive_soft_minus->setEnabled(false);
504                 ui->interception->setEnabled(false);
505                 ui->their_throwaway->setEnabled(false);
506                 ui->our_defense->setEnabled(false);
507                 ui->their_goal->setEnabled(false);
508                 ui->defensive_soft_plus->setEnabled(false);
509                 ui->defensive_soft_minus->setEnabled(false);
510                 return;
511         }
512
513         ui->catch_->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
514         ui->throwaway->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
515         ui->drop->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
516         ui->goal->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
517         ui->offensive_soft_plus->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
518         ui->offensive_soft_minus->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
519         ui->pull->setEnabled(false);
520
521         ui->interception->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
522         ui->their_throwaway->setEnabled(s.attack_state == EventsModel::Status::DEFENSE);
523         ui->our_defense->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
524         ui->their_goal->setEnabled(s.attack_state == EventsModel::Status::DEFENSE);
525         ui->defensive_soft_plus->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
526         ui->defensive_soft_minus->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
527         ui->their_pull->setEnabled(false);
528 }
529
530 void MainWindow::formation_double_clicked(bool offense, unsigned row)
531 {
532         FormationsModel *formations = offense ? offensive_formations : defensive_formations;
533         int id = formations->get_formation_id(row);
534         if (id == -1) {  // “Add new” clicked.
535                 bool ok;
536                 QString new_formation_str = QInputDialog::getText(this, "New formation", "Choose name for new formation:", QLineEdit::Normal, "", &ok);
537                 if (!ok || new_formation_str.isEmpty()) {
538                         return;
539                 }
540
541                 id = formations->insert_new(new_formation_str.toStdString());
542                 QListView *view = offense ? ui->offensive_formation_view : ui->defensive_formation_view;
543                 view->selectionModel()->select(formations->index(formations->get_row_from_id(id), 0), QItemSelectionModel::ClearAndSelect);
544                 events->inserted_new_formation(id, new_formation_str.toStdString());
545         } else {
546                 events->set_formation_at(video->position(), offense, id);
547         }
548         update_ui_from_time(video->position());
549 }
550
551 sqlite3 *open_db(const char *filename)
552 {
553         sqlite3 *db;
554         int ret = sqlite3_open(filename, &db);
555         if (ret != SQLITE_OK) {
556                 fprintf(stderr, "%s: %s\n", filename, sqlite3_errmsg(db));
557                 exit(1);
558         }
559
560         sqlite3_exec(db, R"(
561                 CREATE TABLE IF NOT EXISTS player (player INTEGER PRIMARY KEY, number VARCHAR, name VARCHAR, gender VARCHAR(1));
562         )", nullptr, nullptr, nullptr);  // Ignore errors.
563
564         sqlite3_exec(db, R"(
565                 CREATE TABLE IF NOT EXISTS match (match INTEGER PRIMARY KEY, description VARCHAR);
566         )", nullptr, nullptr, nullptr);  // Ignore errors.
567
568         sqlite3_exec(db, R"(
569                 CREATE TABLE IF NOT EXISTS formation (formation INTEGER PRIMARY KEY, name VARCHAR, offense BOOLEAN NOT NULL);
570         )", nullptr, nullptr, nullptr);  // Ignore errors.
571
572         sqlite3_exec(db, R"(
573                 CREATE TABLE IF NOT EXISTS event (event INTEGER PRIMARY KEY, match INTEGER, t INTEGER, player INTEGER, type VARCHAR, formation INTEGER, FOREIGN KEY (player) REFERENCES player(player), FOREIGN KEY (match) REFERENCES match (match), FOREIGN KEY (formation) REFERENCES formation (formation));
574         )", nullptr, nullptr, nullptr);  // Ignore errors.
575
576         sqlite3_exec(db, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr);  // Ignore errors.
577         sqlite3_exec(db, "PRAGMA synchronous=NORMAL", nullptr, nullptr, nullptr);  // Ignore errors.
578         sqlite3_exec(db, "PRAGMA foreign_keys=ON", nullptr, nullptr, nullptr);  // Ignore errors.
579         return db;
580 }
581
582 int get_match_id(sqlite3 *db, QWidget *parent, int requested_match)
583 {
584         QStringList items;
585         vector<int> ids;
586         bool requested_match_ok = false;
587
588         // Read the list of matches already in the database.
589         sqlite3_stmt *stmt;
590         int ret = sqlite3_prepare_v2(db, "SELECT match, description FROM match ORDER BY match", -1, &stmt, 0);
591         if (ret != SQLITE_OK) {
592                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
593                 abort();
594         }
595         for ( ;; ) {
596                 ret = sqlite3_step(stmt);
597                 if (ret == SQLITE_ROW) {
598                         char buf[256];
599                         snprintf(buf, sizeof(buf), "%s (%d)", sqlite3_column_text(stmt, 1), sqlite3_column_int(stmt, 0));
600                         ids.push_back(sqlite3_column_int(stmt, 0));
601                         if (ids.back() == requested_match) {
602                                 requested_match_ok = true;
603                         }
604                         items.push_back(buf);
605                 } else if (ret == SQLITE_DONE) {
606                         break;
607                 } else {
608                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
609                         abort();
610                 }
611         }
612         ret = sqlite3_finalize(stmt);
613         if (ret != SQLITE_OK) {
614                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
615                 abort();
616         }
617         items.push_back("Add new…");
618
619         if (requested_match_ok) {
620                 return requested_match;
621         }
622
623         QString chosen_str;
624         {
625                 QInputDialog dialog(parent, Qt::WindowFlags());
626                 dialog.setWindowTitle("Open game");
627                 dialog.setLabelText("Choose game to analyze:");
628                 dialog.setComboBoxItems(items);
629                 dialog.setTextValue(items[items.size() - 2]);
630                 dialog.setOption(QInputDialog::UseListViewForComboBoxItems, true);
631                 if (!dialog.exec()) {
632                         return -1;
633                 }
634                 chosen_str = dialog.textValue();
635         }
636
637         for (unsigned i = 0; i < ids.size(); ++i) {
638                 if (chosen_str == items[i]) {
639                         return ids[i];
640                 }
641         }
642
643         // Must be a new game. Get its name and insert it into the database.
644         bool ok;
645         QString new_game_str = QInputDialog::getText(parent, "New game", "Choose name for new game:", QLineEdit::Normal, "", &ok);
646         if (!ok || new_game_str.isEmpty()) {
647                 return -1;
648         }
649
650         // Insert the new row into the database.
651         ret = sqlite3_prepare_v2(db, "INSERT INTO match (description) VALUES (?)", -1, &stmt, 0);
652         if (ret != SQLITE_OK) {
653                 fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
654                 abort();
655         }
656
657         QByteArray new_game_utf8 = new_game_str.toUtf8();
658         sqlite3_bind_text(stmt, 1, (const char *)new_game_utf8.data(), new_game_utf8.size(), SQLITE_STATIC);
659
660         ret = sqlite3_step(stmt);
661         if (ret == SQLITE_ROW) {
662                 fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db));
663                 abort();
664         }
665
666         ret = sqlite3_finalize(stmt);
667         if (ret != SQLITE_OK) {
668                 fprintf(stderr, "INSERT finalize: %s\n", sqlite3_errmsg(db));
669                 abort();
670         }
671
672         return sqlite3_last_insert_rowid(db);
673 }
674
675 int main(int argc, char *argv[])
676 {
677         QApplication app(argc, argv);
678         sqlite3 *db = open_db("ultimate.db");
679
680         // TODO: do this on-demand instead, from a menu
681         export_to_json(db, "ultimate.json");
682
683         int requested_match = -1;
684         if (argc >= 2) {
685                 requested_match = atoi(argv[1]);
686         }
687
688         int match_id = get_match_id(db, nullptr, requested_match);
689         if (match_id <= 0) {  // Cancel.
690                 return 0;
691         }
692
693         MainWindow mainWindow(new EventsModel(db, match_id), new PlayersModel(db),
694                               new FormationsModel(db, true), new FormationsModel(db, false));
695         mainWindow.resize(QSize(1280, 720));
696         mainWindow.show();
697
698         return app.exec();
699
700 }