X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=main.cpp;h=42d42cc0899300aefd254b5f3682e3856f308388;hb=51f84ab86d5bceb0de58f5d87b0af20ec31cce1c;hp=d9e605894ddee0f5d3a91934ff6df5a308b2cfce;hpb=e1b3fc3c35cc286bb2c1d7d215c0185fb431b4f7;p=pkanalytics diff --git a/main.cpp b/main.cpp index d9e6058..42d42cc 100644 --- a/main.cpp +++ b/main.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,8 @@ #include "ui_mainwindow.h" #include "events.h" #include "players.h" +#include "formations.h" +#include "json.h" using namespace std; @@ -31,7 +34,9 @@ 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")); @@ -42,6 +47,8 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players) : events(even ui->setupUi(this); 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) { uint64_t t = events->get_time(current.row()); @@ -60,12 +67,44 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players) : events(even ui->player_view->setColumnWidth(1, 20); ui->player_view->horizontalHeader()->setStretchLastSection(true); + auto formation_changed = [this](const QModelIndex ¤t, const QModelIndex &previous) { + update_action_buttons(video->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(video, &QMediaPlayer::positionChanged, [this](uint64_t pos) { position_changed(pos); }); video->setVideoOutput(ui->video); + // 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]() { seek(-120000); }); + QShortcut *pgup = new QShortcut(QKeySequence(Qt::Key_PageUp), this); + connect(pgup, &QShortcut::activated, [this]() { seek(120000); }); + + // Ugh. Used when Qt messes up and hangs the video. + QShortcut *f5 = new QShortcut(QKeySequence(Qt::Key_F5), this); + connect(f5, &QShortcut::activated, [this]() { + QVideoWidget *nvw = new QVideoWidget(ui->video->parentWidget()); + nvw->setObjectName("video"); + nvw->setMinimumSize(QSize(320, 240)); + video->setVideoOutput(nvw); + ui->main_grid->replaceWidget(ui->video, nvw); + ui->video = nvw; + }); + connect(ui->minus10s, &QPushButton::clicked, [this]() { seek(-10000); }); connect(ui->plus10s, &QPushButton::clicked, [this]() { seek(10000); }); @@ -91,13 +130,13 @@ 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"); }); @@ -124,6 +163,7 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players) : events(even }); // 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"); }); @@ -182,23 +222,23 @@ void MainWindow::seek(int64_t delta_ms) } } -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()) { + vector team = events->sort_team(events->get_team_at(t)); + if (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.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); @@ -210,7 +250,7 @@ void MainWindow::insert_noplayer_event(const string &type) uint64_t t = video->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); @@ -281,7 +321,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, @@ -293,7 +332,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] = ""; @@ -314,6 +353,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; @@ -422,6 +480,26 @@ void MainWindow::update_action_buttons(uint64_t t) 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(video->position(), offense, id); + } +} + sqlite3 *open_db(const char *filename) { sqlite3 *db; @@ -436,20 +514,136 @@ sqlite3 *open_db(const char *filename) )", 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();