From e25f3e74ee30c48d4b60fa7cee303ea8fc5388b6 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Thu, 27 Jul 2023 12:03:17 +0200 Subject: [PATCH] Count selected number of subs in the UI. Includes support for gender counting. This was a long time coming, and saves a fair amount of pain for the operator, at the cost of some precious screen estate. --- mainwindow.cpp | 186 +++++++++++++++++++++++++++++++++++++++++++++++++ mainwindow.h | 1 + mainwindow.ui | 34 ++++++++- players.cpp | 6 ++ players.h | 10 +++ 5 files changed, 234 insertions(+), 3 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index 254e62d..eb82c02 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -330,6 +330,29 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, connect(key_delete, &QShortcut::activated, [this] { ui->delete_->animateClick(); }); connect(ui->delete_, &QPushButton::clicked, [this] { delete_current_event(); }); + // Player list shortcuts. + connect(ui->get_current_players, &QPushButton::clicked, [this, players, events] { + uint64_t t = ui->video->get_position(); + QItemSelection selection; + set team = events->get_team_at(t); + for (int row = 0; row < players->rowCount(QModelIndex()); ++row) { + if (team.count(players->get_player_id(row))) { + selection.select(players->get_row_start_qt(row), players->get_row_end_qt(row)); + } + } + ui->player_view->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); + }); + connect(ui->clear_player_list, &QPushButton::clicked, [this] { + ui->player_view->selectionModel()->clear(); + }); + connect(ui->player_view->selectionModel(), &QItemSelectionModel::selectionChanged, [this] { + update_gender_ratio(ui->video->get_position()); + }); + update_gender_ratio(0); + + // The shortcuts take up so much space that we really need, so we sacrifice the header. + ui->player_view->horizontalHeader()->hide(); + // Menus. connect(ui->action_exit, &QAction::triggered, [this] { close(); }); connect(ui->action_export_json, &QAction::triggered, [db] { export_to_json(db, "ultimate.json"); }); @@ -341,6 +364,7 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, }); connect(ui->action_gender_pull_rule, &QAction::toggled, [this, db, match_id] { save_match_property(db, match_id, "gender_pull_rule", ui->action_gender_pull_rule->isChecked()); + update_gender_ratio(ui->video->get_position()); }); } @@ -464,6 +488,7 @@ void MainWindow::update_ui_from_time(uint64_t t) update_status(t); update_player_buttons(t); update_action_buttons(t); + update_gender_ratio(t); } void MainWindow::update_status(uint64_t t) @@ -659,6 +684,167 @@ void MainWindow::update_action_buttons(uint64_t t) ui->their_pull->setEnabled(false); } +vector> sort_gender(const map &gender_count) +{ + vector> sorted_gender; + for (const auto &[gender, count] : gender_count) { + sorted_gender.emplace_back(gender, count); + } + sort(sorted_gender.begin(), sorted_gender.end(), [](const pair &a, const pair &b) { + if (a.second != b.second) { + return b.second < a.second; + } + return a.first < b.first; + }); + return sorted_gender; +} + +string format_gender_counts(const map &gender_count) +{ + vector> sorted_gender = sort_gender(gender_count); + + string str; + for (const auto &[gender, count] : sorted_gender) { + if (!str.empty()) { + str += ", "; + } + char buf[256]; + snprintf(buf, sizeof(buf), "%d ", count); + str += buf; + str += gender; + } + return str; +} + +string format_gender_counts(const map &gender_count, const map &reference_gender_count) +{ + vector> sorted_gender = sort_gender(reference_gender_count); // Less swapping around this way. + + string str; + for (const auto &[gender, count] : sorted_gender) { + if (!str.empty()) { + str += ", "; + } + char buf[256]; + snprintf(buf, sizeof(buf), "%d/%d ", gender_count.find(gender)->second, count); + str += buf; + str += gender; + } + return str; +} + +void MainWindow::update_gender_ratio(uint64_t t) +{ + string str; + bool ok = true; + + // Count the gender ratio in the current selection. + map gender_count; + for (int i = 0; i < players->rowCount(QModelIndex()); ++i) { + string gender = players->get_player_gender(i); + gender_count[gender] = 0; + } + int num_players = 0; + QItemSelectionModel *select = ui->player_view->selectionModel(); + for (QModelIndex row : select->selectedRows()) { + string gender = players->get_player_gender(row.row()); + ++gender_count[gender]; + ++num_players; + } + + const bool gender_rule_a = ui->action_gender_rule_a->isChecked(); + if (gender_rule_a) { + // This is tricky. We don't want to hard-code assumptions about gender, + // since there are so many possible variations (e.g. 5 players for indoors, + // or loose mixed). We can't get everything right, but our general + // strategy will be: + // + // - We assume ABBA pattern is followed throughout, ie., we switch + // on odd-numbered points. We use goals as reference for points. + // - We always use the two or three latest points as reference; + // this means an issue with the first point won't persist forever. + // - When we don't switch, we expect the identical number as last time. + // - When we _do_ switch, we copy the variation from two points ago + // if we have it; if not, we simply expect it to be different. + // - We should always have the same number of people as last point. + // + // The viewer's warnings also checks that things are correct across + // stoppages, which we don't yet. + map current_gender_count; + for (int i = 0; i < players->rowCount(QModelIndex()); ++i) { + string gender = players->get_player_gender(i); + current_gender_count[gender] = 0; + } + vector> historical_gender_counts; + for (int row = 0; row < events->rowCount(QModelIndex()); ++row) { + if (events->get_time(row) > t) { + break; + } + EventType type = events->get_event_type(row); + if (type == EventType::GOAL || type == EventType::THEIR_GOAL) { + historical_gender_counts.push_back(current_gender_count); + } else if (type == EventType::IN) { + string gender = players->get_player_gender_by_id(*events->get_player_id(row)); + ++current_gender_count[gender]; + } else if (type == EventType::OUT) { + string gender = players->get_player_gender_by_id(*events->get_player_id(row)); + --current_gender_count[gender]; + } + } + + ok = true; + if (historical_gender_counts.empty()) { + // We don't have any points yet. Just output the ratio. + str = format_gender_counts(gender_count); + } else if (historical_gender_counts.size() == 1) { + // We have one, so this one must be different, but we don't know what it must be. + // It must have the same number of players, though. + str = format_gender_counts(gender_count); + ok = (gender_count != historical_gender_counts.back()); + int old_sum = 0, new_sum = 0; + for (const auto &[gender, count] : historical_gender_counts.back()) { + old_sum += count; + } + for (const auto &[gender, count] : gender_count) { + new_sum += count; + } + if (old_sum != new_sum) { + ok = false; + } + } else if (historical_gender_counts.size() % 2 == 0) { + // Must be same as previous. + str = format_gender_counts(gender_count, historical_gender_counts.back()); + ok = (gender_count == historical_gender_counts.back()); + } else { + // Must be same as two points ago. + const auto &ref = historical_gender_counts[historical_gender_counts.size() - 3]; + str = format_gender_counts(gender_count, ref); + ok = (gender_count == ref); + } + } else if (gender_count.size() == 1) { + // Everybody is either of the same gender or nobody has gender noted, + // so just count the number of players. We don't make red here. + char buf[256]; + snprintf(buf, sizeof(buf), "%d selected", num_players); + str = buf; + } else { + // We don't have gender rule A, but we have gender counts, + // so show that. We don't make red here. + string str = format_gender_counts(gender_count); + } + + // Seemingly this setting this every frame is very costly, so we diff. + if (QString::fromUtf8(str) != ui->selected_gender_ratio->text()) { + ui->selected_gender_ratio->setText(QString::fromUtf8(str)); + } + bool current_ok = ui->selected_gender_ratio->styleSheet().isEmpty(); + if (ok && !current_ok) { + ui->selected_gender_ratio->setStyleSheet(""); + } else if (!ok && current_ok) { + ui->selected_gender_ratio->setStyleSheet("QLabel { color: red }"); + } +} + void MainWindow::formation_double_clicked(bool offense, unsigned row) { FormationsModel *formations = offense ? offensive_formations : defensive_formations; diff --git a/mainwindow.h b/mainwindow.h index ea4ba37..c47e9bf 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -39,6 +39,7 @@ private: void update_status(uint64_t t); void update_player_buttons(uint64_t t); void update_action_buttons(uint64_t t); + void update_gender_ratio(uint64_t t); Ui::MainWindow *ui; EventsModel *events; diff --git a/mainwindow.ui b/mainwindow.ui index 3806b60..14608d3 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 1251 + 1278 754 @@ -18,7 +18,7 @@ - + @@ -338,6 +338,34 @@ + + + + + + Current + + + + + + + Clear + + + + + + + 0/0 F, 0/0 M + + + Qt::AlignCenter + + + + + @@ -512,7 +540,7 @@ 0 0 - 1251 + 1278 23 diff --git a/players.cpp b/players.cpp index 0ed7d32..585956f 100644 --- a/players.cpp +++ b/players.cpp @@ -63,6 +63,12 @@ string PlayersModel::get_player_name_by_id(unsigned player_id) return it->name; } +string PlayersModel::get_player_gender_by_id(unsigned player_id) +{ + auto it = find_if(players.begin(), players.end(), [player_id](const Player &p) { return p.player_id == int(player_id); }); + return it->gender; +} + void PlayersModel::load_data() { players.clear(); diff --git a/players.h b/players.h index a8940ff..b5ba81a 100644 --- a/players.h +++ b/players.h @@ -25,7 +25,17 @@ public: int get_player_id(unsigned row) const { return players[row].player_id; } + std::string get_player_gender(unsigned row) const { + return players[row].gender; + } std::string get_player_name_by_id(unsigned player_id); + std::string get_player_gender_by_id(unsigned player_id); + QModelIndex get_row_start_qt(unsigned row) const { + return createIndex(row, 0); + } + QModelIndex get_row_end_qt(unsigned row) const { + return createIndex(row, 2); + } private: struct Player { -- 2.39.2