X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=mainwindow.cpp;h=eb82c0256fc3711d4cad86a4855164f7690f5105;hb=f4be2bd92a1e4b1cebd0d44e8fc46ffeb9eb5588;hp=c2beb7f183ba7c8dc55d09b66bbf2f7063129713;hpb=2d661311faeecfbcc8bddf8e5b17500ebe1d4b66;p=pkanalytics diff --git a/mainwindow.cpp b/mainwindow.cpp index c2beb7f..eb82c02 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -219,22 +219,22 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, // 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); }); + 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(pgup, &QShortcut::activated, [this] { ui->video->seek(120000); }); - connect(ui->minus10s, &QPushButton::clicked, [this]() { ui->video->seek(-10000); }); - connect(ui->plus10s, &QPushButton::clicked, [this]() { ui->video->seek(10000); }); + connect(ui->minus10s, &QPushButton::clicked, [this] { ui->video->seek(-10000); }); + connect(ui->plus10s, &QPushButton::clicked, [this] { ui->video->seek(10000); }); - connect(ui->minus2s, &QPushButton::clicked, [this]() { ui->video->seek(-2000); }); - connect(ui->plus2s, &QPushButton::clicked, [this]() { ui->video->seek(2000); }); - connect(ui->video, &VideoWidget::mouse_back_clicked, [this]() { ui->video->seek(-2000); }); - connect(ui->video, &VideoWidget::mouse_forward_clicked, [this]() { ui->video->seek(2000); }); + connect(ui->minus2s, &QPushButton::clicked, [this] { ui->video->seek(-2000); }); + connect(ui->plus2s, &QPushButton::clicked, [this] { ui->video->seek(2000); }); + connect(ui->video, &VideoWidget::mouse_back_clicked, [this] { ui->video->seek(-2000); }); + connect(ui->video, &VideoWidget::mouse_forward_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->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]() { + connect(ui->play_pause, &QPushButton::clicked, [this] { if (playing) { ui->video->pause(); ui->play_pause->setText("Play (space)"); @@ -248,18 +248,18 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, ui->play_pause->setShortcut(QCoreApplication::translate("MainWindow", "Space", nullptr)); }); - 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); }); + 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, 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, 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"); @@ -267,10 +267,10 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, 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->stallout, &QPushButton::clicked, [this]() { set_current_event_type("stallout"); }); - connect(ui->soft_plus, &QPushButton::clicked, [this, events]() { + connect(ui->drop, &QPushButton::clicked, [this] { set_current_event_type("drop"); }); + connect(ui->goal, &QPushButton::clicked, [this] { set_current_event_type("goal"); }); + connect(ui->stallout, &QPushButton::clicked, [this] { set_current_event_type("stallout"); }); + connect(ui->soft_plus, &QPushButton::clicked, [this, events] { EventsModel::Status s = events->get_status_at(ui->video->get_position()); if (s.attack_state == EventsModel::Status::OFFENSE) { set_current_event_type("offensive_soft_plus"); @@ -278,7 +278,7 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, set_current_event_type("defensive_soft_plus"); } }); - connect(ui->soft_minus, &QPushButton::clicked, [this, events]() { + connect(ui->soft_minus, &QPushButton::clicked, [this, events] { EventsModel::Status s = events->get_status_at(ui->video->get_position()); if (s.attack_state == EventsModel::Status::OFFENSE) { set_current_event_type("offensive_soft_minus"); @@ -286,7 +286,7 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, set_current_event_type("defensive_soft_minus"); } }); - connect(ui->pull_or_was_d, &QPushButton::clicked, [this, events]() { + connect(ui->pull_or_was_d, &QPushButton::clicked, [this, events] { EventsModel::Status s = events->get_status_at(ui->video->get_position()); if (s.pull_state == EventsModel::Status::SHOULD_PULL || events->get_status_at(ui->video->get_position() - 1).pull_state == EventsModel::Status::SHOULD_PULL) { @@ -299,24 +299,24 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, }); // Defensive events. - 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, events]() { + 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, 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->our_defense, &QPushButton::clicked, [this] { set_current_event_type("defense"); }); - 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); }); + 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]() { + connect(ui->substitution, &QPushButton::clicked, [this] { make_substitution(); }); + connect(ui->stoppage, &QPushButton::clicked, [this, events] { EventsModel::Status s = events->get_status_at(ui->video->get_position()); if (s.stoppage) { insert_noplayer_event("restart"); @@ -324,11 +324,34 @@ MainWindow::MainWindow(EventsModel *events, PlayersModel *players, insert_noplayer_event("stoppage"); } }); - connect(ui->unknown, &QPushButton::clicked, [this]() { insert_noplayer_event("unknown"); }); + connect(ui->unknown, &QPushButton::clicked, [this] { insert_noplayer_event("unknown"); }); QShortcut *key_delete = new QShortcut(QKeySequence(Qt::Key_Delete), this); - connect(key_delete, &QShortcut::activated, [this]() { ui->delete_->animateClick(); }); - connect(ui->delete_, &QPushButton::clicked, [this]() { delete_current_event(); }); + 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(); }); @@ -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;