X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=main.cpp;h=560a9a658676b09723edab126b6230a5ac8c3190;hb=280e1391ec1d4ea4e498377726d7613874335943;hp=f6d289d201d97616bf7d2501eb176a29796210e7;hpb=4e0bfdd823a798c2397a2a16bf523f9db2985678;p=pkanalytics diff --git a/main.cpp b/main.cpp index f6d289d..560a9a6 100644 --- a/main.cpp +++ b/main.cpp @@ -2,8 +2,9 @@ #include #include #include -#include #include +#include +#include #include #include #include @@ -14,6 +15,9 @@ #include "ui_mainwindow.h" #include "events.h" #include "players.h" +#include "formations.h" +#include "json.h" +#include "video_widget.h" using namespace std; @@ -31,47 +35,80 @@ string format_timestamp(uint64_t pos) return buf; } -MainWindow::MainWindow(EventsModel *events, PlayersModel *players) : events(events), players(players) +MainWindow::MainWindow(EventsModel *events, PlayersModel *players, + FormationsModel *offensive_formations, FormationsModel *defensive_formations) + : events(events), players(players), offensive_formations(offensive_formations), defensive_formations(defensive_formations) { - video = new QMediaPlayer; - //video->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate.mkv")); - video->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate-prores.mkv")); - video->play(); - ui = new Ui::MainWindow; ui->setupUi(this); + ui->video->open("/home/sesse/dev/stats/ultimate-prores.mkv"); + ui->video->play(); + ui->event_view->setModel(events); + ui->event_view->setColumnWidth(1, 150); + ui->event_view->setColumnWidth(2, 150); connect(ui->event_view->selectionModel(), &QItemSelectionModel::currentRowChanged, [this, events](const QModelIndex ¤t, const QModelIndex &previous) { - video->setPosition(events->get_time(current.row())); + uint64_t t = events->get_time(current.row()); + if (t != ui->video->get_position()) { + ui->video->seek_absolute(events->get_time(current.row())); + } else { + // Selection could have changed, so we still need to update. + // (Just calling setPosition() would not give us the signal + // in this case.) + update_ui_from_time(t); + } }); ui->player_view->setModel(players); + ui->player_view->setColumnWidth(0, 30); + ui->player_view->setColumnWidth(1, 20); + ui->player_view->horizontalHeader()->setStretchLastSection(true); - connect(video, &QMediaPlayer::positionChanged, [this](uint64_t pos) { + auto formation_changed = [this](const QModelIndex ¤t, const QModelIndex &previous) { + QTimer::singleShot(1, [=]{ // The selection is wrong until the callback actually returns. + update_action_buttons(ui->video->get_position()); + }); + }; + ui->offensive_formation_view->setModel(offensive_formations); + ui->defensive_formation_view->setModel(defensive_formations); + connect(ui->offensive_formation_view->selectionModel(), &QItemSelectionModel::currentRowChanged, formation_changed); + connect(ui->defensive_formation_view->selectionModel(), &QItemSelectionModel::currentRowChanged, formation_changed); + connect(ui->offensive_formation_view, &QListView::doubleClicked, [this](const QModelIndex &index) { + formation_double_clicked(true, index.row()); + }); + connect(ui->defensive_formation_view, &QListView::doubleClicked, [this](const QModelIndex &index) { + formation_double_clicked(false, index.row()); + }); + + connect(ui->video, &VideoWidget::position_changed, [this](uint64_t pos) { position_changed(pos); }); - video->setVideoOutput(ui->video); - connect(ui->minus10s, &QPushButton::clicked, [this]() { seek(-10000); }); - connect(ui->plus10s, &QPushButton::clicked, [this]() { seek(10000); }); + // It's not really clear whether PgUp should be forwards or backwards, + // but mpv does at least up = forwards, so that's probably standard. + QShortcut *pgdown = new QShortcut(QKeySequence(Qt::Key_PageDown), this); + connect(pgdown, &QShortcut::activated, [this]() { ui->video->seek(-120000); }); + QShortcut *pgup = new QShortcut(QKeySequence(Qt::Key_PageUp), this); + connect(pgup, &QShortcut::activated, [this]() { ui->video->seek(120000); }); - connect(ui->minus2s, &QPushButton::clicked, [this]() { seek(-2000); }); - connect(ui->plus2s, &QPushButton::clicked, [this]() { seek(2000); }); + connect(ui->minus10s, &QPushButton::clicked, [this]() { ui->video->seek(-10000); }); + connect(ui->plus10s, &QPushButton::clicked, [this]() { ui->video->seek(10000); }); - // TODO: Would be nice to actually have a frame... - connect(ui->minus1f, &QPushButton::clicked, [this]() { seek(-20); }); - connect(ui->plus1f, &QPushButton::clicked, [this]() { seek(20); }); + connect(ui->minus2s, &QPushButton::clicked, [this]() { ui->video->seek(-2000); }); + connect(ui->plus2s, &QPushButton::clicked, [this]() { ui->video->seek(2000); }); + + connect(ui->minus1f, &QPushButton::clicked, [this]() { ui->video->seek_frames(-1); }); + connect(ui->plus1f, &QPushButton::clicked, [this]() { ui->video->seek_frames(1); }); connect(ui->play_pause, &QPushButton::clicked, [this]() { if (playing) { - video->pause(); + ui->video->pause(); ui->play_pause->setText("Play (space)"); } else { - video->setPlaybackRate(1.0); - video->play(); + ui->video->play(); ui->play_pause->setText("Pause (space)"); } playing = !playing; @@ -80,36 +117,60 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players) : events(even ui->play_pause->setShortcut(QCoreApplication::translate("MainWindow", "Space", nullptr)); }); - connect(ui->player_1, &QPushButton::clicked, [this]() { insert_event(1); }); - connect(ui->player_2, &QPushButton::clicked, [this]() { insert_event(2); }); - connect(ui->player_3, &QPushButton::clicked, [this]() { insert_event(3); }); - connect(ui->player_4, &QPushButton::clicked, [this]() { insert_event(4); }); - connect(ui->player_5, &QPushButton::clicked, [this]() { insert_event(5); }); - connect(ui->player_6, &QPushButton::clicked, [this]() { insert_event(6); }); - connect(ui->player_7, &QPushButton::clicked, [this]() { insert_event(7); }); + connect(ui->player_1, &QPushButton::clicked, [this]() { insert_player_event(0); }); + connect(ui->player_2, &QPushButton::clicked, [this]() { insert_player_event(1); }); + connect(ui->player_3, &QPushButton::clicked, [this]() { insert_player_event(2); }); + connect(ui->player_4, &QPushButton::clicked, [this]() { insert_player_event(3); }); + connect(ui->player_5, &QPushButton::clicked, [this]() { insert_player_event(4); }); + connect(ui->player_6, &QPushButton::clicked, [this]() { insert_player_event(5); }); + connect(ui->player_7, &QPushButton::clicked, [this]() { insert_player_event(6); }); // Offensive events + connect(ui->offense_label, &ClickableLabel::clicked, [this]() { insert_noplayer_event("set_offense"); }); connect(ui->catch_, &QPushButton::clicked, [this]() { set_current_event_type("catch"); }); - connect(ui->throwaway, &QPushButton::clicked, [this]() { set_current_event_type("throwaway"); }); + connect(ui->throwaway, &QPushButton::clicked, [this, events]() { + EventsModel::Status s = events->get_status_at(ui->video->get_position()); + if (s.attack_state == EventsModel::Status::DEFENSE && s.pull_state == EventsModel::Status::PULL_IN_AIR) { + insert_noplayer_event("pull_oob"); + } else { + set_current_event_type("throwaway"); + } + }); connect(ui->drop, &QPushButton::clicked, [this]() { set_current_event_type("drop"); }); connect(ui->goal, &QPushButton::clicked, [this]() { set_current_event_type("goal"); }); connect(ui->offensive_soft_plus, &QPushButton::clicked, [this]() { set_current_event_type("offensive_soft_plus"); }); connect(ui->offensive_soft_minus, &QPushButton::clicked, [this]() { set_current_event_type("offensive_soft_minus"); }); - connect(ui->pull, &QPushButton::clicked, [this]() { set_current_event_type("pull"); }); - connect(ui->pull_landed, &QPushButton::clicked, [this]() { insert_noplayer_event("pull_landed"); }); + connect(ui->pull, &QPushButton::clicked, [this, events]() { + EventsModel::Status s = events->get_status_at(ui->video->get_position()); + if (s.pull_state == EventsModel::Status::SHOULD_PULL) { + set_current_event_type("pull"); + } else if (s.pull_state == EventsModel::Status::PULL_IN_AIR) { + insert_noplayer_event("pull_landed"); + } + }); // Defensive events (TODO add more) + connect(ui->interception, &QPushButton::clicked, [this]() { set_current_event_type("interception"); }); + connect(ui->defense_label, &ClickableLabel::clicked, [this]() { insert_noplayer_event("set_defense"); }); connect(ui->their_throwaway, &QPushButton::clicked, [this]() { insert_noplayer_event("their_throwaway"); }); connect(ui->their_goal, &QPushButton::clicked, [this]() { insert_noplayer_event("their_goal"); }); - connect(ui->their_pull, &QPushButton::clicked, [this]() { insert_noplayer_event("their_pull"); }); + connect(ui->their_pull, &QPushButton::clicked, [this, events]() { + EventsModel::Status s = events->get_status_at(ui->video->get_position()); + if (s.pull_state == EventsModel::Status::SHOULD_PULL) { + insert_noplayer_event("their_pull"); + } + }); connect(ui->our_defense, &QPushButton::clicked, [this]() { set_current_event_type("defense"); }); connect(ui->defensive_soft_plus, &QPushButton::clicked, [this]() { set_current_event_type("defensive_soft_plus"); }); connect(ui->defensive_soft_minus, &QPushButton::clicked, [this]() { set_current_event_type("defensive_soft_minus"); }); + connect(ui->offensive_formation, &QPushButton::clicked, [this]() { insert_or_change_formation(/*offense=*/true); }); + connect(ui->defensive_formation, &QPushButton::clicked, [this]() { insert_or_change_formation(/*offense=*/false); }); + // Misc. events connect(ui->substitution, &QPushButton::clicked, [this]() { make_substitution(); }); connect(ui->stoppage, &QPushButton::clicked, [this, events]() { - EventsModel::Status s = events->get_status_at(video->position()); + EventsModel::Status s = events->get_status_at(ui->video->get_position()); if (s.stoppage) { insert_noplayer_event("restart"); } else { @@ -126,44 +187,33 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players) : events(even void MainWindow::position_changed(uint64_t pos) { ui->timestamp->setText(QString::fromUtf8(format_timestamp(pos))); - if (buffered_seek) { - video->setPosition(*buffered_seek); - buffered_seek.reset(); - } if (!playing) { - video->pause(); // We only played to get a picture. + ui->video->pause(); // We only played to get a picture. } - update_ui_from_time(pos); -} - -void MainWindow::seek(int64_t delta_ms) -{ - int64_t current_pos = buffered_seek ? *buffered_seek : video->position(); - uint64_t pos = max(current_pos + delta_ms, 0); - buffered_seek = pos; - if (!playing) { - video->setPlaybackRate(0.01); - video->play(); // Or Qt won't show the seek. + if (playing) { + QModelIndex row = events->get_last_event_qt(ui->video->get_position()); + ui->event_view->scrollTo(row, QAbstractItemView::PositionAtCenter); } + update_ui_from_time(pos); } -void MainWindow::insert_event(int button_id) +void MainWindow::insert_player_event(int button_id) { - uint64_t t = video->position(); - set team = events->get_team_at(t); - if (button_id > team.size()) { + uint64_t t = ui->video->get_position(); + vector team = events->sort_team(events->get_team_at(t)); + if (unsigned(button_id) >= team.size()) { return; } - int player_id = *next(team.begin(), button_id - 1); + int player_id = team[button_id]; EventsModel::Status s = events->get_status_at(t); ui->event_view->selectionModel()->blockSignals(true); - if (s.offense) { + if (s.attack_state == EventsModel::Status::OFFENSE) { // TODO: Perhaps not if that player already did the last catch? - ui->event_view->selectRow(events->insert_event(t, player_id, "catch")); + ui->event_view->selectRow(events->insert_event(t, player_id, nullopt, "catch")); } else { - ui->event_view->selectRow(events->insert_event(t, player_id)); + ui->event_view->selectRow(events->insert_event(t, player_id, nullopt)); } ui->event_view->selectionModel()->blockSignals(false); @@ -172,10 +222,10 @@ void MainWindow::insert_event(int button_id) void MainWindow::insert_noplayer_event(const string &type) { - uint64_t t = video->position(); + uint64_t t = ui->video->get_position(); ui->event_view->selectionModel()->blockSignals(true); - ui->event_view->selectRow(events->insert_event(t, nullopt, type)); + ui->event_view->selectRow(events->insert_event(t, nullopt, nullopt, type)); ui->event_view->selectionModel()->blockSignals(false); update_ui_from_time(t); @@ -189,7 +239,40 @@ void MainWindow::set_current_event_type(const string &type) } int row = select->selectedRows().front().row(); // Should only be one, due to our selection behavior. events->set_event_type(row, type); - update_ui_from_time(video->position()); + update_ui_from_time(ui->video->get_position()); +} + +// Formation buttons either modify the existing formation (if we've selected +// a formation change event), or insert a new one (if not). +void MainWindow::insert_or_change_formation(bool offense) +{ + FormationsModel *formations = offense ? offensive_formations : defensive_formations; + QListView *formation_view = offense ? ui->offensive_formation_view : ui->defensive_formation_view; + if (!formation_view->selectionModel()->hasSelection()) { + // This shouldn't happen; the button should not have been enabled. + return; + } + int formation_row = formation_view->selectionModel()->selectedRows().front().row(); // Should only be one, due to our selection behavior. + int formation_id = formations->get_formation_id(formation_row); + if (formation_id == -1) { + // This also shouldn't happen (“Add new…” selected). + return; + } + + QItemSelectionModel *select = ui->event_view->selectionModel(); + if (select->hasSelection()) { + int row = select->selectedRows().front().row(); // Should only be one, due to our selection behavior. + string expected_type = offense ? "formation_offense" : "formation_defense"; + if (events->get_event_type(row) == expected_type) { + events->set_event_formation(row, formation_id); + update_ui_from_time(ui->video->get_position()); + return; + } + } + + // Insert a new formation event instead (same as double-click on the selected one). + events->set_formation_at(ui->video->get_position(), offense, formation_id); + update_ui_from_time(ui->video->get_position()); } void MainWindow::delete_current_event() @@ -202,38 +285,17 @@ void MainWindow::delete_current_event() ui->event_view->selectionModel()->blockSignals(true); events->delete_event(row); ui->event_view->selectionModel()->blockSignals(false); - update_ui_from_time(video->position()); + update_ui_from_time(ui->video->get_position()); } void MainWindow::make_substitution() { QItemSelectionModel *select = ui->player_view->selectionModel(); - - // FIXME: we should backdate t to start of point (last goal, or 0) if: - // - no players we're removing have had actions yet - // - there have been no other in/out events - // - // ...but if so, we might need to modify in/out events that are already there - // (perhaps just overwrite them all?) - uint64_t t = video->position(); - - set old_team = events->get_team_at(t); set new_team; - for (QModelIndex row : select->selectedRows()) { new_team.insert(players->get_player_id(row.row())); } - - for (int player_id : old_team) { - if (!new_team.count(player_id)) { - events->insert_event(t, player_id, "out"); - } - } - for (int player_id : new_team) { - if (!old_team.count(player_id)) { - events->insert_event(t, player_id, "in"); - } - } + events->set_team_at(ui->video->get_position(), new_team); } void MainWindow::update_ui_from_time(uint64_t t) @@ -247,8 +309,23 @@ void MainWindow::update_status(uint64_t t) { EventsModel::Status s = events->get_status_at(t); char buf[256]; + std::string formation = "Not started"; + if (s.attack_state == EventsModel::Status::OFFENSE) { + if (s.offensive_formation != 0) { + formation = offensive_formations->get_formation_name_by_id(s.offensive_formation); + } else { + formation = "Offense"; + } + } else if (s.attack_state == EventsModel::Status::DEFENSE) { + if (s.defensive_formation != 0) { + formation = defensive_formations->get_formation_name_by_id(s.defensive_formation); + } else { + formation = "Defense"; + } + } + snprintf(buf, sizeof(buf), "%d–%d | %s | %d passes, %d sec possession", - s.our_score, s.their_score, s.offense ? "offense" : "defense", s.num_passes, s.possession_sec); + s.our_score, s.their_score, formation.c_str(), s.num_passes, s.possession_sec); if (s.stoppage_sec > 0) { char buf2[256]; snprintf(buf2, sizeof(buf2), "%s (plus %d sec stoppage)", buf, s.stoppage_sec); @@ -260,7 +337,6 @@ void MainWindow::update_status(uint64_t t) void MainWindow::update_player_buttons(uint64_t t) { - // FIXME: sort by number, instead of by internal ID QPushButton *buttons[] = { ui->player_1, ui->player_2, @@ -272,7 +348,7 @@ void MainWindow::update_player_buttons(uint64_t t) }; const char shortcuts[] = "qweasdf"; int num_players = 0; - for (int player_id : events->get_team_at(t)) { + for (int player_id : events->sort_team(events->get_team_at(t))) { QPushButton *btn = buttons[num_players]; string label = players->get_player_name_by_id(player_id) + " (&" + shortcuts[num_players] + ")"; char shortcut[2] = ""; @@ -293,6 +369,25 @@ void MainWindow::update_player_buttons(uint64_t t) void MainWindow::update_action_buttons(uint64_t t) { + { + QItemSelectionModel *select = ui->offensive_formation_view->selectionModel(); + if (select->hasSelection()) { + int row = select->selectedRows().front().row(); // Should only be one, due to our selection behavior. + ui->offensive_formation->setEnabled(offensive_formations->get_formation_id(row) != -1); + } else { + ui->offensive_formation->setEnabled(false); + } + } + { + QItemSelectionModel *select = ui->defensive_formation_view->selectionModel(); + if (select->hasSelection()) { + int row = select->selectedRows().front().row(); // Should only be one, due to our selection behavior. + ui->defensive_formation->setEnabled(defensive_formations->get_formation_id(row) != -1); + } else { + ui->defensive_formation->setEnabled(false); + } + } + EventsModel::Status s = events->get_status_at(t); bool has_selection = false; @@ -316,7 +411,6 @@ void MainWindow::update_action_buttons(uint64_t t) ui->offensive_soft_plus->setEnabled(false); ui->offensive_soft_minus->setEnabled(false); ui->pull->setEnabled(false); - ui->pull_landed->setEnabled(false); ui->interception->setEnabled(false); ui->their_throwaway->setEnabled(false); ui->our_defense->setEnabled(false); @@ -324,32 +418,103 @@ void MainWindow::update_action_buttons(uint64_t t) ui->defensive_soft_plus->setEnabled(false); ui->defensive_soft_minus->setEnabled(false); ui->their_pull->setEnabled(false); - ui->our_foul->setEnabled(false); return; } else { ui->stoppage->setText("Stoppage (&v)"); ui->stoppage->setShortcut(QCoreApplication::translate("MainWindow", "V", nullptr)); } - ui->catch_->setEnabled(s.offense && has_selection_with_player); - ui->throwaway->setEnabled(s.offense && has_selection_with_player); - ui->drop->setEnabled(s.offense && has_selection_with_player); - ui->goal->setEnabled(s.offense && has_selection_with_player); - ui->offensive_soft_plus->setEnabled(s.offense && has_selection_with_player); - ui->offensive_soft_minus->setEnabled(s.offense && has_selection_with_player); - - // TODO: be stricter - ui->pull->setEnabled(!s.offense && s.should_pull && has_selection_with_player); - ui->pull_landed->setEnabled(!s.offense && has_selection_with_player); - - ui->interception->setEnabled(!s.offense && has_selection_with_player); - ui->their_throwaway->setEnabled(!s.offense); - ui->our_defense->setEnabled(!s.offense && has_selection_with_player); - ui->their_goal->setEnabled(!s.offense); - ui->defensive_soft_plus->setEnabled(!s.offense && has_selection_with_player); - ui->defensive_soft_minus->setEnabled(!s.offense && has_selection_with_player); - ui->their_pull->setEnabled(s.offense && s.should_pull); - ui->our_foul->setEnabled(!s.offense && has_selection_with_player); + // Defaults for pull-related buttons. + ui->pull->setText("Pull (&p)"); + ui->their_pull->setText("Their pull (&p)"); + ui->pull->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr)); + ui->their_pull->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr)); + ui->throwaway->setText("Throwaway (&t)"); + ui->throwaway->setShortcut(QCoreApplication::translate("MainWindow", "T", nullptr)); + + if (s.pull_state == EventsModel::Status::SHOULD_PULL) { + ui->pull->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player); + ui->their_pull->setEnabled(s.attack_state == EventsModel::Status::OFFENSE); + + ui->catch_->setEnabled(false); + ui->throwaway->setEnabled(false); + ui->drop->setEnabled(false); + ui->goal->setEnabled(false); + ui->offensive_soft_plus->setEnabled(false); + ui->offensive_soft_minus->setEnabled(false); + ui->interception->setEnabled(false); + ui->their_throwaway->setEnabled(false); + ui->our_defense->setEnabled(false); + ui->their_goal->setEnabled(false); + ui->defensive_soft_plus->setEnabled(false); + ui->defensive_soft_minus->setEnabled(false); + return; + } + if (s.pull_state == EventsModel::Status::PULL_IN_AIR) { + if (s.attack_state == EventsModel::Status::DEFENSE) { + ui->pull->setText("Pull landed (&p)"); + ui->pull->setShortcut(QCoreApplication::translate("MainWindow", "P", nullptr)); + ui->pull->setEnabled(true); + + ui->throwaway->setText("Pull OOB (&t)"); + ui->throwaway->setShortcut(QCoreApplication::translate("MainWindow", "T", nullptr)); + ui->throwaway->setEnabled(true); + } else { + ui->pull->setEnabled(false); + ui->throwaway->setEnabled(false); + } + ui->their_pull->setEnabled(false); // We don't track their pull landings; only by means of catch etc. + + ui->catch_->setEnabled(false); + ui->drop->setEnabled(false); + ui->goal->setEnabled(false); + ui->offensive_soft_plus->setEnabled(false); + ui->offensive_soft_minus->setEnabled(false); + ui->interception->setEnabled(false); + ui->their_throwaway->setEnabled(false); + ui->our_defense->setEnabled(false); + ui->their_goal->setEnabled(false); + ui->defensive_soft_plus->setEnabled(false); + ui->defensive_soft_minus->setEnabled(false); + return; + } + + ui->catch_->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player); + ui->throwaway->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player); + ui->drop->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player); + ui->goal->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player); + ui->offensive_soft_plus->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player); + ui->offensive_soft_minus->setEnabled(s.attack_state == EventsModel::Status::OFFENSE && has_selection_with_player); + ui->pull->setEnabled(false); + + ui->interception->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player); + ui->their_throwaway->setEnabled(s.attack_state == EventsModel::Status::DEFENSE); + ui->our_defense->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player); + ui->their_goal->setEnabled(s.attack_state == EventsModel::Status::DEFENSE); + ui->defensive_soft_plus->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player); + ui->defensive_soft_minus->setEnabled(s.attack_state == EventsModel::Status::DEFENSE && has_selection_with_player); + ui->their_pull->setEnabled(false); +} + +void MainWindow::formation_double_clicked(bool offense, unsigned row) +{ + FormationsModel *formations = offense ? offensive_formations : defensive_formations; + int id = formations->get_formation_id(row); + if (id == -1) { // “Add new” clicked. + bool ok; + QString new_formation_str = QInputDialog::getText(this, "New formation", "Choose name for new formation:", QLineEdit::Normal, "", &ok); + if (!ok || new_formation_str.isEmpty()) { + return; + } + + id = formations->insert_new(new_formation_str.toStdString()); + QListView *view = offense ? ui->offensive_formation_view : ui->defensive_formation_view; + view->selectionModel()->select(formations->index(formations->get_row_from_id(id), 0), QItemSelectionModel::ClearAndSelect); + events->inserted_new_formation(id, new_formation_str.toStdString()); + } else { + events->set_formation_at(ui->video->get_position(), offense, id); + } + update_ui_from_time(ui->video->get_position()); } sqlite3 *open_db(const char *filename) @@ -362,24 +527,140 @@ sqlite3 *open_db(const char *filename) } sqlite3_exec(db, R"( - CREATE TABLE IF NOT EXISTS player (player INTEGER PRIMARY KEY, number VARCHAR, name VARCHAR); + CREATE TABLE IF NOT EXISTS player (player INTEGER PRIMARY KEY, number VARCHAR, name VARCHAR, gender VARCHAR(1)); )", nullptr, nullptr, nullptr); // Ignore errors. sqlite3_exec(db, R"( - CREATE TABLE IF NOT EXISTS event (event INTEGER PRIMARY KEY, t INTEGER, player INTEGER, type VARCHAR, FOREIGN KEY (player) REFERENCES player(player)); + CREATE TABLE IF NOT EXISTS match (match INTEGER PRIMARY KEY, description VARCHAR); + )", nullptr, nullptr, nullptr); // Ignore errors. + + sqlite3_exec(db, R"( + CREATE TABLE IF NOT EXISTS formation (formation INTEGER PRIMARY KEY, name VARCHAR, offense BOOLEAN NOT NULL); + )", nullptr, nullptr, nullptr); // Ignore errors. + + sqlite3_exec(db, R"( + 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)); )", nullptr, nullptr, nullptr); // Ignore errors. sqlite3_exec(db, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr); // Ignore errors. sqlite3_exec(db, "PRAGMA synchronous=NORMAL", nullptr, nullptr, nullptr); // Ignore errors. + sqlite3_exec(db, "PRAGMA foreign_keys=ON", nullptr, nullptr, nullptr); // Ignore errors. return db; } +int get_match_id(sqlite3 *db, QWidget *parent, int requested_match) +{ + QStringList items; + vector ids; + bool requested_match_ok = false; + + // Read the list of matches already in the database. + sqlite3_stmt *stmt; + int ret = sqlite3_prepare_v2(db, "SELECT match, description FROM match ORDER BY match", -1, &stmt, 0); + if (ret != SQLITE_OK) { + fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db)); + abort(); + } + for ( ;; ) { + ret = sqlite3_step(stmt); + if (ret == SQLITE_ROW) { + char buf[256]; + snprintf(buf, sizeof(buf), "%s (%d)", sqlite3_column_text(stmt, 1), sqlite3_column_int(stmt, 0)); + ids.push_back(sqlite3_column_int(stmt, 0)); + if (ids.back() == requested_match) { + requested_match_ok = true; + } + items.push_back(buf); + } else if (ret == SQLITE_DONE) { + break; + } else { + fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db)); + abort(); + } + } + ret = sqlite3_finalize(stmt); + if (ret != SQLITE_OK) { + fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db)); + abort(); + } + items.push_back("Add new…"); + + if (requested_match_ok) { + return requested_match; + } + + QString chosen_str; + { + QInputDialog dialog(parent, Qt::WindowFlags()); + dialog.setWindowTitle("Open game"); + dialog.setLabelText("Choose game to analyze:"); + dialog.setComboBoxItems(items); + dialog.setTextValue(items[items.size() - 2]); + dialog.setOption(QInputDialog::UseListViewForComboBoxItems, true); + if (!dialog.exec()) { + return -1; + } + chosen_str = dialog.textValue(); + } + + for (unsigned i = 0; i < ids.size(); ++i) { + if (chosen_str == items[i]) { + return ids[i]; + } + } + + // Must be a new game. Get its name and insert it into the database. + bool ok; + QString new_game_str = QInputDialog::getText(parent, "New game", "Choose name for new game:", QLineEdit::Normal, "", &ok); + if (!ok || new_game_str.isEmpty()) { + return -1; + } + + // Insert the new row into the database. + ret = sqlite3_prepare_v2(db, "INSERT INTO match (description) VALUES (?)", -1, &stmt, 0); + if (ret != SQLITE_OK) { + fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db)); + abort(); + } + + QByteArray new_game_utf8 = new_game_str.toUtf8(); + sqlite3_bind_text(stmt, 1, (const char *)new_game_utf8.data(), new_game_utf8.size(), SQLITE_STATIC); + + ret = sqlite3_step(stmt); + if (ret == SQLITE_ROW) { + fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db)); + abort(); + } + + ret = sqlite3_finalize(stmt); + if (ret != SQLITE_OK) { + fprintf(stderr, "INSERT finalize: %s\n", sqlite3_errmsg(db)); + abort(); + } + + return sqlite3_last_insert_rowid(db); +} + int main(int argc, char *argv[]) { QApplication app(argc, argv); sqlite3 *db = open_db("ultimate.db"); - MainWindow mainWindow(new EventsModel(db), new PlayersModel(db)); + // TODO: do this on-demand instead, from a menu + export_to_json(db, "ultimate.json"); + + int requested_match = -1; + if (argc >= 2) { + requested_match = atoi(argv[1]); + } + + int match_id = get_match_id(db, nullptr, requested_match); + if (match_id <= 0) { // Cancel. + return 0; + } + + MainWindow mainWindow(new EventsModel(db, match_id), new PlayersModel(db), + new FormationsModel(db, true), new FormationsModel(db, false)); mainWindow.resize(QSize(1280, 720)); mainWindow.show();