]> git.sesse.net Git - pkanalytics/commitdiff
Add the beginnings of formation support.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 29 May 2023 12:44:12 +0000 (14:44 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Mon, 29 May 2023 12:44:12 +0000 (14:44 +0200)
events.cpp
events.h
formations.cpp [new file with mode: 0644]
formations.h [new file with mode: 0644]
main.cpp
mainwindow.h
mainwindow.ui
meson.build

index e6ed5796340ff3521ab051512f25920d1d04c022..bb915b273b072ef4c4e5ec5eb9c348ac4d562f6b 100644 (file)
@@ -24,7 +24,7 @@ QVariant EventsModel::headerData(int section, Qt::Orientation orientation, int r
                if (section == 0) {
                        return "Time";
                } else if (section == 1) {
-                       return "Player";
+                       return "Who/what";
                } else {
                        return "Type";
                }
@@ -42,10 +42,15 @@ QVariant EventsModel::data(const QModelIndex &index, int role) const
                return QString::fromUtf8(format_timestamp(events[index.row()].t));
        } else if (index.column() == 1) {
                optional<int> player_id = events[index.row()].player_id;
+               optional<int> formation_id = events[index.row()].formation_id;
                if (player_id) {
                        auto p_it = players.find(*player_id);
                        const Player &p = p_it->second;
                        return QString::fromUtf8(p.name + " (" + p.number + ")");
+               } else if (formation_id) {
+                       auto f_it = formations.find(*formation_id);
+                       const Formation &p = f_it->second;
+                       return QString::fromUtf8(p.name);
                } else {
                        return QVariant();
                }
@@ -90,8 +95,34 @@ void EventsModel::load_data()
                abort();
        }
 
+       // Read the formations.
+       ret = sqlite3_prepare_v2(db, "SELECT formation, name FROM formation", -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) {
+                       Formation f;
+                       f.formation_id = sqlite3_column_int(stmt, 0);
+                       f.name = (const char *) sqlite3_column_text(stmt, 1);
+                       formations[f.formation_id] = std::move(f);
+               } 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();
+       }
+
        // Read the events.
-       ret = sqlite3_prepare_v2(db, "SELECT event, t, player, type FROM event WHERE match=? ORDER BY t", -1, &stmt, 0);
+       ret = sqlite3_prepare_v2(db, "SELECT event, t, player, formation, type FROM event WHERE match=? ORDER BY t", -1, &stmt, 0);
        if (ret != SQLITE_OK) {
                fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
                abort();
@@ -106,7 +137,10 @@ void EventsModel::load_data()
                        if (sqlite3_column_type(stmt, 2) == SQLITE_INTEGER) {  // Non-NULL.
                                e.player_id = sqlite3_column_int(stmt, 2);
                        }
-                       e.type = (const char *)sqlite3_column_text(stmt, 3);
+                       if (sqlite3_column_type(stmt, 3) == SQLITE_INTEGER) {  // Non-NULL.
+                               e.formation_id = sqlite3_column_int(stmt, 3);
+                       }
+                       e.type = (const char *)sqlite3_column_text(stmt, 4);
                        events.push_back(std::move(e));
                } else if (ret == SQLITE_DONE) {
                        break;
@@ -122,7 +156,7 @@ void EventsModel::load_data()
        }
 }
 
-unsigned EventsModel::insert_event(uint64_t t, optional<int> player_id, const string &type)
+unsigned EventsModel::insert_event(uint64_t t, optional<int> player_id, optional<int> formation_id, const string &type)
 {
        auto it = lower_bound(events.begin(), events.end(), t,
                [](const Event &e, uint64_t t) { return e.t < t; });
@@ -139,7 +173,7 @@ unsigned EventsModel::insert_event(uint64_t t, optional<int> player_id, const st
 
        // Insert the new row into the database.
        sqlite3_stmt *stmt;
-       int ret = sqlite3_prepare_v2(db, "INSERT INTO event (match, t, player, type) VALUES (?, ?, ?, ?)", -1, &stmt, 0);
+       int ret = sqlite3_prepare_v2(db, "INSERT INTO event (match, t, player, formation, type) VALUES (?, ?, ?, ?, ?)", -1, &stmt, 0);
        if (ret != SQLITE_OK) {
                fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
                abort();
@@ -152,7 +186,12 @@ unsigned EventsModel::insert_event(uint64_t t, optional<int> player_id, const st
        } else {
                sqlite3_bind_null(stmt, 3);
        }
-       sqlite3_bind_text(stmt, 4, type.data(), type.size(), SQLITE_STATIC);
+       if (formation_id) {
+               sqlite3_bind_int64(stmt, 4, *formation_id);
+       } else {
+               sqlite3_bind_null(stmt, 4);
+       }
+       sqlite3_bind_text(stmt, 5, type.data(), type.size(), SQLITE_STATIC);
 
        ret = sqlite3_step(stmt);
        if (ret == SQLITE_ROW) {
@@ -385,16 +424,44 @@ void EventsModel::set_team_at(uint64_t t, const set<int> &new_team)
        set<int> old_team = get_team_at(backdate_point);
        for (int player_id : old_team) {
                if (!new_team.count(player_id)) {
-                       insert_event(backdate_point, player_id, "out");
+                       insert_event(backdate_point, player_id, nullopt, "out");
                }
        }
        for (int player_id : new_team) {
                if (!old_team.count(player_id)) {
-                       insert_event(backdate_point, player_id, "in");
+                       insert_event(backdate_point, player_id, nullopt, "in");
                }
        }
 }
 
+void EventsModel::set_formation_at(uint64_t t, bool offense, unsigned formation)
+{
+       // If there's another goal/stoppage no more than 20 seconds ago,
+       // we assume that the formation started at that point (it just took
+       // the operator a bit of time to see it). If not, we assume we
+       // changed in the middle of a point.
+       uint64_t backdate_point = 0;
+       for (const Event &e : events) {
+               if (e.t > t) {
+                       break;
+               }
+               if (e.type == "goal" || e.type == "their_goal" || e.type == "stoppage" || e.type == "reset" || e.type == "set_offense" || e.type == "set_defense" || e.type == "in" || e.type == "out") {
+                       backdate_point = e.t + 1;
+               }
+               if (e.type == "formation_offense" || e.type == "formation_defense") {
+                       backdate_point = 0;
+               }
+       }
+       if (backdate_point != 0 && t - backdate_point < 20000) {
+               t = backdate_point;
+       }
+       if (offense) {
+               insert_event(t, nullopt, formation, "formation_offense");
+       } else {
+               insert_event(t, nullopt, formation, "formation_defense");
+       }
+}
+
 vector<int> EventsModel::sort_team(const set<int> &team) const
 {
        vector<int> ret(team.begin(), team.end());
index caf832ac26ca0180364d8e899dba453beb909c00..545437b16d9f181c998cf707e99307b7c849b87f 100644 (file)
--- a/events.h
+++ b/events.h
@@ -25,7 +25,7 @@ public:
        QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
        QVariant data(const QModelIndex &index, int role) const override;
 
-       unsigned insert_event(uint64_t t, std::optional<int> player_id, const std::string &type = "unknown");  // Returns the row.
+       unsigned insert_event(uint64_t t, std::optional<int> player_id, std::optional<int> formation_id, const std::string &type = "unknown");  // Returns the row.
        void delete_event(unsigned row);
        void set_event_type(unsigned row, const std::string &type);
        uint64_t get_time(unsigned row) { return events[row].t; }
@@ -48,6 +48,7 @@ public:
        std::set<int> get_team_at(uint64_t t);
        void set_team_at(uint64_t, const std::set<int> &new_team);
        std::vector<int> sort_team(const std::set<int> &team) const;  // Ordered first by gender, then by number.
+       void set_formation_at(uint64_t t, bool offense, unsigned formation);
 
 private:
        struct Player {
@@ -58,10 +59,17 @@ private:
        std::map<int, Player> players;
        std::map<int, int> player_ordering;  // From id to position.
 
+       struct Formation {
+               int formation_id;
+               std::string name;
+       };
+       std::map<int, Formation> formations;
+
        struct Event {
                int event_id;
                uint64_t t;
                std::optional<int> player_id;
+               std::optional<int> formation_id;
                std::string type;
        };
        std::vector<Event> events;
diff --git a/formations.cpp b/formations.cpp
new file mode 100644 (file)
index 0000000..fa2337b
--- /dev/null
@@ -0,0 +1,80 @@
+#include <string>
+#include <vector>
+#include <sqlite3.h>
+#include "formations.h"
+
+using namespace std;
+
+FormationsModel::FormationsModel(sqlite3 *db, bool offense) : db(db), offense(offense)
+{
+       load_data();
+}
+
+QVariant FormationsModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+       if (role != Qt::DisplayRole) {
+               return QVariant();
+       }
+       if (orientation == Qt::Horizontal) {
+               if (section == 0) {
+                       return "Name";
+               } else {
+                       return QVariant();
+               }
+       } else {
+               return "";
+       }
+}
+
+QVariant FormationsModel::data(const QModelIndex &index, int role) const
+{
+       if (role == Qt::TextAlignmentRole) {
+               return (Qt::AlignLeft | Qt::AlignVCenter).toInt();
+       }
+       if (role != Qt::DisplayRole) {
+               return QVariant();
+       }
+       if (index.column() == 0) {
+               if (index.row() == 0) {
+                       return QString::fromUtf8("(None/unknown)");
+               } else if (index.row() == formations.size() + 1) {
+                       return QString::fromUtf8("Add new…");
+               } else {
+                       return QString::fromUtf8(formations[index.row() - 1].name);
+               }
+       }
+       return QVariant();
+}
+
+void FormationsModel::load_data()
+{
+       formations.clear();
+
+       // Read the formations.
+       sqlite3_stmt *stmt;
+       int ret = sqlite3_prepare_v2(db, "SELECT formation, name FROM formation WHERE offense=? ORDER BY name", -1, &stmt, 0);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
+               abort();
+       }
+       sqlite3_bind_int(stmt, 1, offense);
+       for ( ;; ) {
+               ret = sqlite3_step(stmt);
+               if (ret == SQLITE_ROW) {
+                       Formation f;
+                       f.formation_id = sqlite3_column_int(stmt, 0);
+                       f.name = (const char *)sqlite3_column_text(stmt, 1);
+                       formations.push_back(f);
+               } 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();
+       }
+}
diff --git a/formations.h b/formations.h
new file mode 100644 (file)
index 0000000..5eda723
--- /dev/null
@@ -0,0 +1,45 @@
+#ifndef _FORMATIONS_H
+#define _FORMATIONS_H 1
+
+#include <sqlite3.h>
+#include <QAbstractListModel>
+#include <string>
+#include <vector>
+
+class FormationsModel : public QAbstractListModel
+{
+public:
+       FormationsModel(sqlite3 *db, bool offense);
+
+       int rowCount(const QModelIndex &parent) const override
+       {
+               return formations.size() + 2;
+       }
+       QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+       QVariant data(const QModelIndex &index, int role) const override;
+
+       int get_formation_id(unsigned row) const {
+               if (row == 0) {
+                       return 0;
+               }
+               if (row == formations.size() + 1) {
+                       return -1;
+               }
+               return formations[row - 1].formation_id;
+       }
+       std::string get_formation_name_by_id(unsigned formation_id);
+
+private:
+       struct Formation {
+               int formation_id;
+               std::string name;
+       };
+       std::vector<Formation> formations;
+
+       sqlite3 *db;
+       bool offense;
+
+       void load_data();
+};
+
+#endif  // !defined(_FORMATIONS_H)
index 4383f2e84958bc1dbfc06c6c72f58e9a959cba36..d23c63e8c51d3e15be86ac36e78c433d2e1689c1 100644 (file)
--- a/main.cpp
+++ b/main.cpp
@@ -15,6 +15,7 @@
 #include "ui_mainwindow.h"
 #include "events.h"
 #include "players.h"
+#include "formations.h"
 #include "json.h"
 
 using namespace std;
@@ -33,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"));
@@ -62,6 +65,20 @@ 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 &current, 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);
        });
@@ -217,9 +234,9 @@ void MainWindow::insert_player_event(int button_id)
        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);
 
@@ -231,7 +248,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);
@@ -334,6 +351,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;
@@ -442,6 +478,23 @@ 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;
+               }
+
+               // FIXME insert
+       } else {
+               events->set_formation_at(video->position(), offense, id);
+       }
+}
+
 sqlite3 *open_db(const char *filename)
 {
        sqlite3 *db;
@@ -460,7 +513,11 @@ 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, match INTEGER, t INTEGER, player INTEGER, type VARCHAR, FOREIGN KEY (player) REFERENCES player(player), FOREIGN KEY (match) REFERENCES match (match));
+               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.
@@ -580,7 +637,8 @@ int main(int argc, char *argv[])
                return 0;
        }
 
-       MainWindow mainWindow(new EventsModel(db, match_id), new PlayersModel(db));
+       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();
 
index 2d702c75a4d8544b2d861ec25368e71547d6b607..83ed90957118262b443acb0eabbfb318926acb3b 100644 (file)
@@ -7,13 +7,15 @@
 
 class EventsModel;
 class PlayersModel;
+class FormationsModel;
 
 class MainWindow : public QMainWindow
 {
        Q_OBJECT
 
 public:
-       MainWindow(EventsModel *events, PlayersModel *players);
+       MainWindow(EventsModel *events, PlayersModel *players,
+                  FormationsModel *offensive_formations, FormationsModel *defensive_formations);
 
 private:
        void position_changed(uint64_t pos);
@@ -23,6 +25,7 @@ private:
        void set_current_event_type(const std::string &type);
        void delete_current_event();
        void make_substitution();
+       void formation_double_clicked(bool offense, unsigned row);
 
        void update_ui_from_time(uint64_t t);
        void update_status(uint64_t t);
@@ -32,6 +35,8 @@ private:
        Ui::MainWindow *ui;
        EventsModel *events;
        PlayersModel *players;
+       FormationsModel *offensive_formations;
+       FormationsModel *defensive_formations;
        bool seeking = false;
        bool playing = true;
        std::optional<uint64_t> buffered_seek;
index c672fd5be66bc75c0263a29f113cda028a7dca09..0c9226571fa3542b4a7297d0f596ed77ec2b1978 100644 (file)
   <widget class="QWidget" name="centralwidget">
    <layout class="QGridLayout" name="gridLayout_2">
     <item row="0" column="0">
-     <layout class="QGridLayout" name="main_grid" rowstretch="1,0,0" columnstretch="1,0">
-      <item row="0" column="0">
-       <widget class="QVideoWidget" name="video" native="true">
-        <property name="minimumSize">
-         <size>
-          <width>320</width>
-          <height>240</height>
-         </size>
-        </property>
-       </widget>
-      </item>
-      <item row="2" column="0">
-       <widget class="QTableView" name="event_view">
-        <property name="selectionMode">
-         <enum>QAbstractItemView::SingleSelection</enum>
-        </property>
-        <property name="selectionBehavior">
-         <enum>QAbstractItemView::SelectRows</enum>
-        </property>
-       </widget>
-      </item>
-      <item row="1" column="0">
-       <layout class="QHBoxLayout" name="nav_buttons" stretch="1,0,0,0,0,0,0,0,2">
-        <item>
-         <widget class="QLabel" name="timestamp">
-          <property name="text">
-           <string>0:00:00.000</string>
-          </property>
-          <property name="alignment">
-           <set>Qt::AlignCenter</set>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="minus10s">
-          <property name="text">
-           <string>-10s (&amp;K)</string>
-          </property>
-          <property name="shortcut">
-           <string>K</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="plus10s">
-          <property name="text">
-           <string>+10s (&amp;L)</string>
-          </property>
-          <property name="shortcut">
-           <string>L</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="minus2s">
-          <property name="text">
-           <string>-2s (&amp;←)</string>
-          </property>
-          <property name="shortcut">
-           <string>Left</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="plus2s">
-          <property name="text">
-           <string>+2s (&amp;→)</string>
-          </property>
-          <property name="shortcut">
-           <string>Right</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="minus1f">
-          <property name="text">
-           <string>-1f (&amp;,)</string>
-          </property>
-          <property name="shortcut">
-           <string>,</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="plus1f">
-          <property name="text">
-           <string>+1f (&amp;.)</string>
-          </property>
-          <property name="shortcut">
-           <string>.</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QPushButton" name="play_pause">
-          <property name="minimumSize">
-           <size>
-            <width>110</width>
-            <height>0</height>
-           </size>
-          </property>
-          <property name="text">
-           <string>Pause (space)</string>
-          </property>
-          <property name="shortcut">
-           <string>Space</string>
-          </property>
-         </widget>
-        </item>
-        <item>
-         <widget class="QLabel" name="status">
-          <property name="text">
-           <string>0–0 | offense | 0 passes, 0 sec possession</string>
-          </property>
-          <property name="alignment">
-           <set>Qt::AlignCenter</set>
-          </property>
-         </widget>
-        </item>
-       </layout>
-      </item>
-      <item row="0" column="1" rowspan="3">
+     <layout class="QGridLayout" name="main_grid" rowstretch="1,0,0,0" columnstretch="1,0">
+      <item row="0" column="1" rowspan="4">
        <layout class="QVBoxLayout" name="buttons" stretch="0,0,0,0,0,0,0,1">
         <item>
          <layout class="QGridLayout" name="player_grid">
            </widget>
           </item>
           <item row="4" column="1">
-           <widget class="QPushButton" name="unused_button">
+           <widget class="QPushButton" name="offensive_formation">
             <property name="enabled">
-             <bool>false</bool>
+             <bool>true</bool>
             </property>
             <property name="text">
-             <string/>
+             <string>Formation (&amp;o)</string>
             </property>
            </widget>
           </item>
            </widget>
           </item>
           <item row="3" column="1">
-           <widget class="QPushButton" name="unused_button2">
+           <widget class="QPushButton" name="defensive_formation">
             <property name="enabled">
-             <bool>false</bool>
+             <bool>true</bool>
             </property>
             <property name="text">
-             <string/>
+             <string>Formation (&amp;o)</string>
             </property>
             <property name="shortcut">
              <string>O</string>
         </item>
        </layout>
       </item>
+      <item row="0" column="0">
+       <widget class="QVideoWidget" name="video" native="true">
+        <property name="minimumSize">
+         <size>
+          <width>320</width>
+          <height>240</height>
+         </size>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <layout class="QHBoxLayout" name="nav_buttons" stretch="1,0,0,0,0,0,0,0,2">
+        <item>
+         <widget class="QLabel" name="timestamp">
+          <property name="text">
+           <string>0:00:00.000</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignCenter</set>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="minus10s">
+          <property name="text">
+           <string>-10s (&amp;K)</string>
+          </property>
+          <property name="shortcut">
+           <string>K</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="plus10s">
+          <property name="text">
+           <string>+10s (&amp;L)</string>
+          </property>
+          <property name="shortcut">
+           <string>L</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="minus2s">
+          <property name="text">
+           <string>-2s (&amp;←)</string>
+          </property>
+          <property name="shortcut">
+           <string>Left</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="plus2s">
+          <property name="text">
+           <string>+2s (&amp;→)</string>
+          </property>
+          <property name="shortcut">
+           <string>Right</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="minus1f">
+          <property name="text">
+           <string>-1f (&amp;,)</string>
+          </property>
+          <property name="shortcut">
+           <string>,</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="plus1f">
+          <property name="text">
+           <string>+1f (&amp;.)</string>
+          </property>
+          <property name="shortcut">
+           <string>.</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QPushButton" name="play_pause">
+          <property name="minimumSize">
+           <size>
+            <width>110</width>
+            <height>0</height>
+           </size>
+          </property>
+          <property name="text">
+           <string>Pause (space)</string>
+          </property>
+          <property name="shortcut">
+           <string>Space</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="status">
+          <property name="text">
+           <string>0–0 | offense | 0 passes, 0 sec possession</string>
+          </property>
+          <property name="alignment">
+           <set>Qt::AlignCenter</set>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item row="2" column="0">
+       <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,0">
+        <item>
+         <widget class="QTableView" name="event_view">
+          <property name="selectionMode">
+           <enum>QAbstractItemView::SingleSelection</enum>
+          </property>
+          <property name="selectionBehavior">
+           <enum>QAbstractItemView::SelectRows</enum>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="offensive_formation_layout">
+          <item>
+           <widget class="QLabel" name="label">
+            <property name="text">
+             <string>Offensive formation</string>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignCenter</set>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QListView" name="offensive_formation_view"/>
+          </item>
+         </layout>
+        </item>
+        <item>
+         <layout class="QVBoxLayout" name="defensive_formation_layout">
+          <item>
+           <widget class="QLabel" name="label_2">
+            <property name="text">
+             <string>Defensive formation</string>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignCenter</set>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QListView" name="defensive_formation_view"/>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </item>
      </layout>
     </item>
    </layout>
index ae33315d663e7dc9e2230392e2f759c320a5b317..02cb58639541407cb232888ceab96c59a7a7793f 100644 (file)
@@ -9,4 +9,4 @@ qt_files = qt6.preprocess(
        ui_files: ['mainwindow.ui'],
         dependencies: qt6deps)
 
-executable('stats', ['main.cpp', 'events.cpp', 'players.cpp', 'json.cpp'], qt_files, dependencies: [qt6deps, sqlite3dep])
+executable('stats', ['main.cpp', 'events.cpp', 'players.cpp', 'formations.cpp', 'json.cpp'], qt_files, dependencies: [qt6deps, sqlite3dep])