]> git.sesse.net Git - pkanalytics/blob - mainwindow.cpp
Propagate errors on video opening back to the UI.
[pkanalytics] / mainwindow.cpp
1 #include <QMediaPlayer>
2 #include <QMainWindow>
3 #include <QApplication>
4 #include <QGridLayout>
5 #include <QShortcut>
6 #include <QInputDialog>
7 #include <QTimer>
8 #include <algorithm>
9 #include <string>
10 #include <map>
11 #include <vector>
12 #include <optional>
13 #include <sqlite3.h>
14 #include "mainwindow.h"
15 #include "ui_mainwindow.h"
16 #include "events.h"
17 #include "players.h"
18 #include "formations.h"
19 #include "json.h"
20 #include "video_widget.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         ui = new Ui::MainWindow;
43         ui->setupUi(this);
44
45         if (!ui->video->open("/home/sesse/dev/stats/ultimate-prores.mkv")) {
46                 // TODO: Pop up a dialog box here instead
47                 fprintf(stderr, "WARNING: Video opening failed\n");
48         }
49         ui->video->play();
50
51         ui->event_view->setModel(events);
52         ui->event_view->setColumnWidth(1, 150);
53         ui->event_view->setColumnWidth(2, 150);
54         connect(ui->event_view->selectionModel(), &QItemSelectionModel::currentRowChanged,
55                 [this, events](const QModelIndex &current, const QModelIndex &previous) {
56                         uint64_t t = events->get_time(current.row());
57                         if (t != ui->video->get_position()) {
58                                 ui->video->seek_absolute(events->get_time(current.row()));
59                         } else {
60                                 // Selection could have changed, so we still need to update.
61                                 // (Just calling setPosition() would not give us the signal
62                                 // in this case.)
63                                 update_ui_from_time(t);
64                         }
65                 });
66
67         ui->player_view->setModel(players);
68         ui->player_view->setColumnWidth(0, 30);
69         ui->player_view->setColumnWidth(1, 20);
70         ui->player_view->horizontalHeader()->setStretchLastSection(true);
71
72         auto formation_changed = [this](const QModelIndex &current, const QModelIndex &previous) {
73                 QTimer::singleShot(1, [=]{  // The selection is wrong until the callback actually returns.
74                         update_action_buttons(ui->video->get_position());
75                 });
76         };
77         ui->offensive_formation_view->setModel(offensive_formations);
78         ui->defensive_formation_view->setModel(defensive_formations);
79         connect(ui->offensive_formation_view->selectionModel(), &QItemSelectionModel::currentRowChanged, formation_changed);
80         connect(ui->defensive_formation_view->selectionModel(), &QItemSelectionModel::currentRowChanged, formation_changed);
81         connect(ui->offensive_formation_view, &QListView::doubleClicked, [this](const QModelIndex &index) {
82                 formation_double_clicked(true, index.row());
83         });
84         connect(ui->defensive_formation_view, &QListView::doubleClicked, [this](const QModelIndex &index) {
85                 formation_double_clicked(false, index.row());
86         });
87
88         connect(ui->video, &VideoWidget::position_changed, [this](uint64_t pos) {
89                 position_changed(pos);
90         });
91
92         // It's not really clear whether PgUp should be forwards or backwards,
93         // but mpv does at least up = forwards, so that's probably standard.
94         QShortcut *pgdown = new QShortcut(QKeySequence(Qt::Key_PageDown), this);
95         connect(pgdown, &QShortcut::activated, [this]() { ui->video->seek(-120000); });
96         QShortcut *pgup = new QShortcut(QKeySequence(Qt::Key_PageUp), this);
97         connect(pgup, &QShortcut::activated, [this]() { ui->video->seek(120000); });
98
99         connect(ui->minus10s, &QPushButton::clicked, [this]() { ui->video->seek(-10000); });
100         connect(ui->plus10s, &QPushButton::clicked, [this]() { ui->video->seek(10000); });
101
102         connect(ui->minus2s, &QPushButton::clicked, [this]() { ui->video->seek(-2000); });
103         connect(ui->plus2s, &QPushButton::clicked, [this]() { ui->video->seek(2000); });
104         connect(ui->video, &VideoWidget::mouse_back_clicked, [this]() { ui->video->seek(-2000); });
105         connect(ui->video, &VideoWidget::mouse_forward_clicked, [this]() { ui->video->seek(2000); });
106
107         connect(ui->minus1f, &QPushButton::clicked, [this]() { ui->video->seek_frames(-1); });
108         connect(ui->plus1f, &QPushButton::clicked, [this]() { ui->video->seek_frames(1); });
109
110         connect(ui->play_pause, &QPushButton::clicked, [this]() {
111                 if (playing) {
112                         ui->video->pause();
113                         ui->play_pause->setText("Play (space)");
114                 } else {
115                         ui->video->play();
116                         ui->play_pause->setText("Pause (space)");
117                 }
118                 playing = !playing;
119
120                 // Needs to be set anew when we modify setText(), evidently.
121                 ui->play_pause->setShortcut(QCoreApplication::translate("MainWindow", "Space", nullptr));
122         });
123
124         connect(ui->player_1, &QPushButton::clicked, [this]() { insert_player_event(0); });
125         connect(ui->player_2, &QPushButton::clicked, [this]() { insert_player_event(1); });
126         connect(ui->player_3, &QPushButton::clicked, [this]() { insert_player_event(2); });
127         connect(ui->player_4, &QPushButton::clicked, [this]() { insert_player_event(3); });
128         connect(ui->player_5, &QPushButton::clicked, [this]() { insert_player_event(4); });
129         connect(ui->player_6, &QPushButton::clicked, [this]() { insert_player_event(5); });
130         connect(ui->player_7, &QPushButton::clicked, [this]() { insert_player_event(6); });
131
132         // Offensive events
133         connect(ui->offense_label, &ClickableLabel::clicked, [this]() { insert_noplayer_event("set_offense"); });
134         connect(ui->catch_, &QPushButton::clicked, [this]() { set_current_event_type("catch"); });
135         connect(ui->throwaway, &QPushButton::clicked, [this, events]() {
136                 EventsModel::Status s = events->get_status_at(ui->video->get_position());
137                 if (s.attack_state == EventsModel::Status::DEFENSE && s.pull_state == EventsModel::Status::PULL_IN_AIR) {
138                         insert_noplayer_event("pull_oob");
139                 } else {
140                         set_current_event_type("throwaway");
141                 }
142         });
143         connect(ui->drop, &QPushButton::clicked, [this]() { set_current_event_type("drop"); });
144         connect(ui->goal, &QPushButton::clicked, [this]() { set_current_event_type("goal"); });
145         connect(ui->offensive_soft_plus, &QPushButton::clicked, [this]() { set_current_event_type("offensive_soft_plus"); });
146         connect(ui->offensive_soft_minus, &QPushButton::clicked, [this]() { set_current_event_type("offensive_soft_minus"); });
147         connect(ui->pull_or_was_d, &QPushButton::clicked, [this, events]() {
148                 EventsModel::Status s = events->get_status_at(ui->video->get_position());
149                 if (s.pull_state == EventsModel::Status::SHOULD_PULL) {
150                         set_current_event_type("pull");
151                 } else if (s.pull_state == EventsModel::Status::PULL_IN_AIR) {
152                         insert_noplayer_event("pull_landed");
153                 } else if (s.pull_state == EventsModel::Status::NOT_PULLING) {
154                         set_current_event_type("was_d");
155                 }
156         });
157
158         // Defensive events (TODO add more)
159         connect(ui->interception, &QPushButton::clicked, [this]() { set_current_event_type("interception"); });
160         connect(ui->defense_label, &ClickableLabel::clicked, [this]() { insert_noplayer_event("set_defense"); });
161         connect(ui->their_throwaway, &QPushButton::clicked, [this]() { insert_noplayer_event("their_throwaway"); });
162         connect(ui->their_goal, &QPushButton::clicked, [this]() { insert_noplayer_event("their_goal"); });
163         connect(ui->their_pull, &QPushButton::clicked, [this, events]() {
164                 EventsModel::Status s = events->get_status_at(ui->video->get_position());
165                 if (s.pull_state == EventsModel::Status::SHOULD_PULL) {
166                         insert_noplayer_event("their_pull");
167                 }
168         });
169         connect(ui->our_defense, &QPushButton::clicked, [this]() { set_current_event_type("defense"); });
170         connect(ui->defensive_soft_plus, &QPushButton::clicked, [this]() { set_current_event_type("defensive_soft_plus"); });
171         connect(ui->defensive_soft_minus, &QPushButton::clicked, [this]() { set_current_event_type("defensive_soft_minus"); });
172
173         connect(ui->offensive_formation, &QPushButton::clicked, [this]() { insert_or_change_formation(/*offense=*/true); });
174         connect(ui->defensive_formation, &QPushButton::clicked, [this]() { insert_or_change_formation(/*offense=*/false); });
175
176         // Misc. events
177         connect(ui->substitution, &QPushButton::clicked, [this]() { make_substitution(); });
178         connect(ui->stoppage, &QPushButton::clicked, [this, events]() {
179                 EventsModel::Status s = events->get_status_at(ui->video->get_position());
180                 if (s.stoppage) {
181                         insert_noplayer_event("restart");
182                 } else {
183                         insert_noplayer_event("stoppage");
184                 }
185         });
186         connect(ui->unknown, &QPushButton::clicked, [this]() { insert_noplayer_event("unknown"); });
187
188         QShortcut *key_delete = new QShortcut(QKeySequence(Qt::Key_Delete), this);
189         connect(key_delete, &QShortcut::activated, [this]() { ui->delete_->animateClick(); });
190         connect(ui->delete_, &QPushButton::clicked, [this]() { delete_current_event(); });
191 }
192
193 void MainWindow::position_changed(uint64_t pos)
194 {
195         ui->timestamp->setText(QString::fromUtf8(format_timestamp(pos)));
196         if (!playing) {
197                 ui->video->pause();  // We only played to get a picture.
198         }
199         if (playing) {
200                 QModelIndex row = events->get_last_event_qt(ui->video->get_position());
201                 ui->event_view->scrollTo(row, QAbstractItemView::PositionAtCenter);
202         }
203         update_ui_from_time(pos);
204 }
205
206 void MainWindow::insert_player_event(int button_id)
207 {
208         uint64_t t = ui->video->get_position();
209         vector<int> team = events->sort_team(events->get_team_at(t));
210         if (unsigned(button_id) >= team.size()) {
211                 return;
212         }
213         int player_id = team[button_id];
214
215         EventsModel::Status s = events->get_status_at(t);
216
217         ui->event_view->selectionModel()->blockSignals(true);
218         if (s.attack_state == EventsModel::Status::OFFENSE) {
219                 // TODO: Perhaps not if that player already did the last catch?
220                 ui->event_view->selectRow(events->insert_event(t, player_id, nullopt, "catch"));
221         } else {
222                 ui->event_view->selectRow(events->insert_event(t, player_id, nullopt));
223         }
224         ui->event_view->selectionModel()->blockSignals(false);
225
226         update_ui_from_time(t);
227 }
228
229 void MainWindow::insert_noplayer_event(const string &type)
230 {
231         uint64_t t = ui->video->get_position();
232
233         ui->event_view->selectionModel()->blockSignals(true);
234         ui->event_view->selectRow(events->insert_event(t, nullopt, nullopt, type));
235         ui->event_view->selectionModel()->blockSignals(false);
236
237         update_ui_from_time(t);
238 }
239
240 void MainWindow::set_current_event_type(const string &type)
241 {
242         QItemSelectionModel *select = ui->event_view->selectionModel();
243         if (!select->hasSelection()) {
244                 return;
245         }
246         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
247         events->set_event_type(row, type);
248         update_ui_from_time(ui->video->get_position());
249 }
250
251 // Formation buttons either modify the existing formation (if we've selected
252 // a formation change event), or insert a new one (if not).
253 void MainWindow::insert_or_change_formation(bool offense)
254 {
255         FormationsModel *formations = offense ? offensive_formations : defensive_formations;
256         QListView *formation_view = offense ? ui->offensive_formation_view : ui->defensive_formation_view;
257         if (!formation_view->selectionModel()->hasSelection()) {
258                 // This shouldn't happen; the button should not have been enabled.
259                 return;
260         }
261         int formation_row = formation_view->selectionModel()->selectedRows().front().row();  // Should only be one, due to our selection behavior.
262         int formation_id = formations->get_formation_id(formation_row);
263         if (formation_id == -1) {
264                 // This also shouldn't happen (“Add new…” selected).
265                 return;
266         }
267
268         QItemSelectionModel *select = ui->event_view->selectionModel();
269         if (select->hasSelection()) {
270                 int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
271                 string expected_type = offense ? "formation_offense" : "formation_defense";
272                 if (events->get_event_type(row) == expected_type) {
273                         events->set_event_formation(row, formation_id);
274                         update_ui_from_time(ui->video->get_position());
275                         return;
276                 }
277         }
278
279         // Insert a new formation event instead (same as double-click on the selected one).
280         events->set_formation_at(ui->video->get_position(), offense, formation_id);
281         update_ui_from_time(ui->video->get_position());
282 }
283
284 void MainWindow::delete_current_event()
285 {
286         QItemSelectionModel *select = ui->event_view->selectionModel();
287         if (!select->hasSelection()) {
288                 return;
289         }
290         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
291         ui->event_view->selectionModel()->blockSignals(true);
292         events->delete_event(row);
293         ui->event_view->selectionModel()->blockSignals(false);
294         update_ui_from_time(ui->video->get_position());
295 }
296
297 void MainWindow::make_substitution()
298 {
299         QItemSelectionModel *select = ui->player_view->selectionModel();
300         set<int> new_team;
301         for (QModelIndex row : select->selectedRows()) {
302                 new_team.insert(players->get_player_id(row.row()));
303         }
304         events->set_team_at(ui->video->get_position(), new_team);
305         update_player_buttons(ui->video->get_position());
306 }
307
308 void MainWindow::update_ui_from_time(uint64_t t)
309 {
310         update_status(t);
311         update_player_buttons(t);
312         update_action_buttons(t);
313 }
314
315 void MainWindow::update_status(uint64_t t)
316 {
317         EventsModel::Status s = events->get_status_at(t);
318         char buf[256];
319         std::string formation = "Not started";
320         if (s.attack_state == EventsModel::Status::OFFENSE) {
321                 if (s.offensive_formation != 0) {
322                         formation = offensive_formations->get_formation_name_by_id(s.offensive_formation);
323                 } else {
324                         formation = "Offense";
325                 }
326         } else if (s.attack_state == EventsModel::Status::DEFENSE) {
327                 if (s.defensive_formation != 0) {
328                         formation = defensive_formations->get_formation_name_by_id(s.defensive_formation);
329                 } else {
330                         formation = "Defense";
331                 }
332         }
333
334         snprintf(buf, sizeof(buf), "%d–%d | %s | %d passes, %d sec possession",
335                 s.our_score, s.their_score, formation.c_str(), s.num_passes, s.possession_sec);
336         if (s.stoppage_sec > 0) {
337                 char buf2[256];
338                 snprintf(buf2, sizeof(buf2), "%s (plus %d sec stoppage)", buf, s.stoppage_sec);
339                 ui->status->setText(buf2);
340         } else {
341                 ui->status->setText(buf);
342         }
343 }
344
345 void MainWindow::update_player_buttons(uint64_t t)
346 {
347         QPushButton *buttons[] = {
348                 ui->player_1,
349                 ui->player_2,
350                 ui->player_3,
351                 ui->player_4,
352                 ui->player_5,
353                 ui->player_6,
354                 ui->player_7
355         };
356         const char shortcuts[] = "qweasdf";
357         int num_players = 0;
358         for (int player_id : events->sort_team(events->get_team_at(t))) {
359                 QPushButton *btn = buttons[num_players];
360                 string label = players->get_player_name_by_id(player_id) + " (&" + shortcuts[num_players] + ")";
361                 char shortcut[2] = "";
362                 shortcut[0] = toupper(shortcuts[num_players]);
363                 btn->setText(QString::fromUtf8(label));
364                 btn->setShortcut(QCoreApplication::translate("MainWindow", shortcut, nullptr));
365                 btn->setEnabled(true);
366                 if (++num_players == 7) {
367                         break;
368                 }
369         }
370         for (int i = num_players; i < 7; ++i) {
371                 QPushButton *btn = buttons[i];
372                 btn->setText("No player");
373                 btn->setEnabled(false);
374         }
375 }
376
377 void MainWindow::update_action_buttons(uint64_t t)
378 {
379         {
380                 QItemSelectionModel *select = ui->offensive_formation_view->selectionModel();
381                 if (select->hasSelection()) {
382                         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
383                         ui->offensive_formation->setEnabled(offensive_formations->get_formation_id(row) != -1);
384                 } else {
385                         ui->offensive_formation->setEnabled(false);
386                 }
387         }
388         {
389                 QItemSelectionModel *select = ui->defensive_formation_view->selectionModel();
390                 if (select->hasSelection()) {
391                         int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
392                         ui->defensive_formation->setEnabled(defensive_formations->get_formation_id(row) != -1);
393                 } else {
394                         ui->defensive_formation->setEnabled(false);
395                 }
396         }
397
398         EventsModel::Status s = events->get_status_at(t);
399
400         bool has_selection = false;
401         bool has_selection_with_player = false;
402
403         QItemSelectionModel *select = ui->event_view->selectionModel();
404         if (select->hasSelection()) {
405                 has_selection = true;
406                 int row = select->selectedRows().front().row();  // Should only be one, due to our selection behavior.
407                 has_selection_with_player = events->get_player_id(row).has_value();
408         }
409         ui->delete_->setEnabled(has_selection);
410
411         if (s.stoppage) {
412                 ui->stoppage->setText("Restart (&v)");
413                 ui->stoppage->setShortcut(QCoreApplication::translate("MainWindow", "V", nullptr));
414                 ui->catch_->setEnabled(false);
415                 ui->throwaway->setEnabled(false);
416                 ui->drop->setEnabled(false);
417                 ui->goal->setEnabled(false);
418                 ui->offensive_soft_plus->setEnabled(false);
419                 ui->offensive_soft_minus->setEnabled(false);
420                 ui->pull_or_was_d->setEnabled(false);
421                 ui->interception->setEnabled(false);
422                 ui->their_throwaway->setEnabled(false);
423                 ui->our_defense->setEnabled(false);
424                 ui->their_goal->setEnabled(false);
425                 ui->defensive_soft_plus->setEnabled(false);
426                 ui->defensive_soft_minus->setEnabled(false);
427                 ui->their_pull->setEnabled(false);
428                 return;
429         } else {
430                 ui->stoppage->setText("Stoppage (&v)");
431                 ui->stoppage->setShortcut(QCoreApplication::translate("MainWindow", "V", nullptr));
432         }
433
434         // Defaults for pull-related buttons.
435         ui->pull_or_was_d->setText("Pull (&p)");
436         ui->their_pull->setText("Their pull (&p)");
437         ui->pull_or_was_d->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr));
438         ui->their_pull->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr));
439         ui->throwaway->setText("Throwaway (&t)");
440         ui->throwaway->setShortcut(QCoreApplication::translate("MainWindow", "T", nullptr));
441
442         if (s.pull_state == EventsModel::Status::SHOULD_PULL) {
443                 ui->pull_or_was_d->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
444                 ui->their_pull->setEnabled(s.attack_state == EventsModel::Status::OFFENSE);
445
446                 ui->catch_->setEnabled(false);
447                 ui->throwaway->setEnabled(false);
448                 ui->drop->setEnabled(false);
449                 ui->goal->setEnabled(false);
450                 ui->offensive_soft_plus->setEnabled(false);
451                 ui->offensive_soft_minus->setEnabled(false);
452                 ui->interception->setEnabled(false);
453                 ui->their_throwaway->setEnabled(false);
454                 ui->our_defense->setEnabled(false);
455                 ui->their_goal->setEnabled(false);
456                 ui->defensive_soft_plus->setEnabled(false);
457                 ui->defensive_soft_minus->setEnabled(false);
458                 return;
459         }
460         if (s.pull_state == EventsModel::Status::PULL_IN_AIR) {
461                 if (s.attack_state == EventsModel::Status::DEFENSE) {
462                         ui->pull_or_was_d->setText("Pull landed (&p)");
463                         ui->pull_or_was_d->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr));
464                         ui->pull_or_was_d->setEnabled(true);
465
466                         ui->throwaway->setText("Pull OOB (&t)");
467                         ui->throwaway->setShortcut(QCoreApplication::translate("MainWindow", "T", nullptr));
468                         ui->throwaway->setEnabled(true);
469                 } else {
470                         ui->pull_or_was_d->setEnabled(false);
471                         ui->throwaway->setEnabled(false);
472                 }
473                 ui->their_pull->setEnabled(false);  // We don't track their pull landings; only by means of catch etc.
474
475                 ui->catch_->setEnabled(false);
476                 ui->drop->setEnabled(false);
477                 ui->goal->setEnabled(false);
478                 ui->offensive_soft_plus->setEnabled(false);
479                 ui->offensive_soft_minus->setEnabled(false);
480                 ui->interception->setEnabled(false);
481                 ui->their_throwaway->setEnabled(false);
482                 ui->our_defense->setEnabled(false);
483                 ui->their_goal->setEnabled(false);
484                 ui->defensive_soft_plus->setEnabled(false);
485                 ui->defensive_soft_minus->setEnabled(false);
486                 return;
487         }
488
489         // Not pulling, so reuse the pull button for got d-ed.
490         ui->pull_or_was_d->setText("Was d-ed (&z)");
491         ui->pull_or_was_d->setShortcut(QCoreApplication::translate("MainWindow", "Z", nullptr));
492         ui->pull_or_was_d->setEnabled(true);
493
494         ui->catch_->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
495         ui->throwaway->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
496         ui->drop->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
497         ui->goal->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
498         ui->offensive_soft_plus->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
499         ui->offensive_soft_minus->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);
500         ui->pull_or_was_d->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player);  // Was d-ed.
501
502         ui->interception->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
503         ui->their_throwaway->setEnabled(s.attack_state == EventsModel::Status::DEFENSE);
504         ui->our_defense->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
505         ui->their_goal->setEnabled(s.attack_state == EventsModel::Status::DEFENSE);
506         ui->defensive_soft_plus->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
507         ui->defensive_soft_minus->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player);
508         ui->their_pull->setEnabled(false);
509 }
510
511 void MainWindow::formation_double_clicked(bool offense, unsigned row)
512 {
513         FormationsModel *formations = offense ? offensive_formations : defensive_formations;
514         int id = formations->get_formation_id(row);
515         if (id == -1) {  // “Add new” clicked.
516                 bool ok;
517                 QString new_formation_str = QInputDialog::getText(this, "New formation", "Choose name for new formation:", QLineEdit::Normal, "", &ok);
518                 if (!ok || new_formation_str.isEmpty()) {
519                         return;
520                 }
521
522                 id = formations->insert_new(new_formation_str.toStdString());
523                 QListView *view = offense ? ui->offensive_formation_view : ui->defensive_formation_view;
524                 view->selectionModel()->select(formations->index(formations->get_row_from_id(id), 0), QItemSelectionModel::ClearAndSelect);
525                 events->inserted_new_formation(id, new_formation_str.toStdString());
526         } else {
527                 events->set_formation_at(ui->video->get_position(), offense, id);
528         }
529         update_ui_from_time(ui->video->get_position());
530 }
531