From 13192a9c32b0b4d2b61b8753ee7c69a15c4a240d Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Wed, 10 Jul 2024 00:02:03 +0200 Subject: [PATCH 01/20] Improved distinction between individual and club rankings --- categoriesmodel.cpp | 4 ++-- cattypedelegate.cpp | 2 +- translations/LBChronoRace_en.ts | 24 ++++++++++++------------ translations/LBChronoRace_it.ts | 24 ++++++++++++------------ 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/categoriesmodel.cpp b/categoriesmodel.cpp index 8a0a5b3..281ed01 100644 --- a/categoriesmodel.cpp +++ b/categoriesmodel.cpp @@ -81,7 +81,7 @@ QVariant CategoriesModel::data(QModelIndex const &index, int role) const else if (role == Qt::ToolTipRole) switch (index.column()) { case static_cast(Category::Field::CTF_TEAM): - return QVariant(tr("Individual (I) or Team (T)")); + return QVariant(tr("Individual/Relay (I) or Club (T)")); case static_cast(Category::Field::CTF_SEX): return QVariant(tr("Men (M), Women (F), Mixed (X) or All (U)")); case static_cast(Category::Field::CTF_TO_YEAR): @@ -153,7 +153,7 @@ QVariant CategoriesModel::headerData(int section, Qt::Orientation orientation, i if (orientation == Qt::Horizontal) switch (section) { case static_cast(Category::Field::CTF_TEAM): - return QString("%1").arg(tr("Individual/Team")); + return QString("%1").arg(tr("Individual/Club")); case static_cast(Category::Field::CTF_SEX): return QString("%1").arg(tr("Sex")); case static_cast(Category::Field::CTF_TO_YEAR): diff --git a/cattypedelegate.cpp b/cattypedelegate.cpp index 7d69a28..41d6292 100644 --- a/cattypedelegate.cpp +++ b/cattypedelegate.cpp @@ -76,7 +76,7 @@ QString CategoryTypeDelegate::toCatTypeString(Category::Type type) { switch (type) { case Category::Type::INDIVIDUAL: - return tr("Individual"); + return tr("Individual/Relay"); case Category::Type::CLUB: return tr("Club"); default: diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index 90fa913..42f8f68 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -73,10 +73,6 @@ I I - - Individual (I) or Team (T) - Individual (I) or Team (T) - The category will include competitors born up to and including this year (i.e. 2000); 0 to disable The category will include competitors born up to and including this year (i.e. 2000); 0 to disable @@ -93,10 +89,6 @@ Short category name Short category name - - Individual/Team - Individual/Team - Sex Sex @@ -121,6 +113,14 @@ Men (M), Women (F), Mixed (X) or All (U) Men (M), Women (F), Mixed (X) or All (U) + + Individual/Relay (I) or Club (T) + Individual/Relay (I) or Club (T) + + + Individual/Club + Individual/Club + Category @@ -162,10 +162,6 @@ CategoryTypeDelegate - - Individual - Individual - Club Club @@ -174,6 +170,10 @@ Unexpected Type enum value '%1' Unexpected Type enum value '%1' + + Individual/Relay + Individual/Relay + ChronoRaceData diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 86c83be..6c2031f 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -73,10 +73,6 @@ I I - - Individual (I) or Team (T) - Individuale (I) o Società (T) - The category will include competitors born up to and including this year (i.e. 2000); 0 to disable La categoria include concorrenti nati fino a quest'anno incluso (es. 2000); 0 per disabilitare @@ -93,10 +89,6 @@ Short category name Abbreviazione categoria - - Individual/Team - Individuale/Società - Sex Sesso @@ -121,6 +113,14 @@ Men (M), Women (F), Mixed (X) or All (U) Maschile (M), Femminile (F), Mista (X) o Tutti (U) + + Individual/Relay (I) or Club (T) + Individuale/Staffetta (I) or Società (T) + + + Individual/Club + Individuale/Società + Category @@ -162,10 +162,6 @@ CategoryTypeDelegate - - Individual - Individuale - Club Società @@ -174,6 +170,10 @@ Unexpected Type enum value '%1' Valore enumerazione Type '%1' non valido + + Individual/Relay + Individuale/Staffetta + ChronoRaceData From f90e14eb841cfb5fffab9151e1850e546d685326 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Mon, 15 Jul 2024 17:25:25 +0200 Subject: [PATCH 02/20] Add Import/Export buttons for Timings in main window This pair of convenience buttons are directly connected to the Import/Export slots of the Timings table. --- chronorace.ui | 64 +++++++++++++++++++++++++++------ chronoracetable.hpp | 4 +-- lbchronorace.cpp | 4 +++ translations/LBChronoRace_en.ts | 16 +++++++++ translations/LBChronoRace_it.ts | 16 +++++++++ 5 files changed, 91 insertions(+), 13 deletions(-) diff --git a/chronorace.ui b/chronorace.ui index d12a3c8..1a53822 100644 --- a/chronorace.ui +++ b/chronorace.ui @@ -275,19 +275,34 @@ - + - - - Qt::Horizontal + + + Load the timings collected and exported in another PC + + + Import Timings from another PC + + + + :/material/icons/download.svg:/material/icons/download.svg + + + + + + + Copy the collected timings to a file to be imported on another PC + + + Export Timings to another PC - - - 40 - 20 - + + + :/material/icons/upload.svg:/material/icons/upload.svg - + @@ -374,7 +389,7 @@ 0 0 1024 - 22 + 19 @@ -410,6 +425,9 @@ Tools + + + @@ -552,6 +570,30 @@ Save current race data to a new file + + + + :/material/icons/download.svg:/material/icons/download.svg + + + Import Timings + + + Load the timings collected and exported in another PC + + + + + + :/material/icons/upload.svg:/material/icons/upload.svg + + + Export Timings + + + Copy the collected timings to a file to be imported on another PC + + diff --git a/chronoracetable.hpp b/chronoracetable.hpp index 4630356..96e3ddd 100644 --- a/chronoracetable.hpp +++ b/chronoracetable.hpp @@ -39,12 +39,12 @@ class ChronoRaceTable : public QDialog private slots: void rowAdd() const; void rowDel() const; - void modelImport(); - void modelExport(); void dialogQuit(); public slots: void show(); //NOSONAR + void modelImport(); + void modelExport(); signals: void newRowCount(int count); diff --git a/lbchronorace.cpp b/lbchronorace.cpp index 7684447..7351be3 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -101,6 +101,8 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : QObject::connect(ui->editClubsList, &QPushButton::clicked, &teamsTable, &ChronoRaceTable::show); QObject::connect(ui->editCategories, &QPushButton::clicked, &categoriesTable, &ChronoRaceTable::show); QObject::connect(ui->editTimings, &QPushButton::clicked, &timingsTable, &ChronoRaceTable::show); + QObject::connect(ui->importTimings, &QPushButton::clicked, &timingsTable, &ChronoRaceTable::modelImport); + QObject::connect(ui->exportTimings, &QPushButton::clicked, &timingsTable, &ChronoRaceTable::modelExport); QObject::connect(ui->makeStartList, &QPushButton::clicked, this, &LBChronoRace::makeStartList); QObject::connect(ui->collectTimings, &QPushButton::clicked, &timings, &ChronoRaceTimings::show); QObject::connect(ui->makeRankings, &QPushButton::clicked, this, &LBChronoRace::makeRankings); @@ -114,6 +116,8 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : QObject::connect(ui->actionEditTeams, &QAction::triggered, &teamsTable, &ChronoRaceTable::show); QObject::connect(ui->actionEditCategories, &QAction::triggered, &categoriesTable, &ChronoRaceTable::show); QObject::connect(ui->actionEditTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::show); + QObject::connect(ui->actionImportTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::modelImport); + QObject::connect(ui->actionExportTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::modelExport); QObject::connect(ui->actionMakeStartList, &QAction::triggered, this, &LBChronoRace::makeStartList); QObject::connect(ui->actionCollectTimings, &QAction::triggered, &timings, &ChronoRaceTimings::show); QObject::connect(ui->actionMakeRankings, &QAction::triggered, this, &LBChronoRace::makeRankings); diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index 42f8f68..7ce0ec4 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -976,6 +976,22 @@ Please uodate the application. Clubs Clubs + + Load the timings collected and exported in another PC + Load the timings collected and exported in another PC + + + Import Timings from another PC + Import Timings from another PC + + + Copy the collected timings to a file to be imported on another PC + Copy the collected timings to a file to be imported on another PC + + + Export Timings to another PC + Export Timings to another PC + PDFRankingPrinter diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 6c2031f..dca53a5 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -976,6 +976,22 @@ Aggiornare l'applicazione. Clubs Società + + Load the timings collected and exported in another PC + Carica i tempi registrati ed esportati in un altro PC + + + Import Timings from another PC + Importa Tempi da un altro PC + + + Copy the collected timings to a file to be imported on another PC + Copia i tempi registrati su un file da importare su un altro PC + + + Export Timings to another PC + Esporta Tempi per un altro PC + PDFRankingPrinter From 1717afff01008633c6ec79f734274c3f3866b9e3 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Mon, 15 Jul 2024 21:49:11 +0200 Subject: [PATCH 03/20] Set CSV/Plain Text Encoding using a dedicated dialog The encoding is used either when a CSV is imported or exported, and when a CSV or Plain Text rankings file is generated. --- chronorace.ui | 14 +++++++++ chronoracetable.cpp | 18 ++++++----- chronoracetable.hpp | 3 +- crloader.cpp | 2 +- icons/abc.svg | 1 + lbchronorace.cpp | 53 ++++++++++++++++++++++++++++----- lbchronorace.hpp | 8 +++-- materialicons.qrc | 1 + translations/LBChronoRace_en.ts | 46 +++++++++++++++++++++++++--- translations/LBChronoRace_it.ts | 46 +++++++++++++++++++++++++--- 10 files changed, 164 insertions(+), 28 deletions(-) create mode 100644 icons/abc.svg diff --git a/chronorace.ui b/chronorace.ui index 1a53822..691fcc9 100644 --- a/chronorace.ui +++ b/chronorace.ui @@ -431,6 +431,8 @@ + + @@ -630,6 +632,18 @@ Generate results + + + + :/material/icons/abc.svg:/material/icons/abc.svg + + + Set Encoding + + + Set encoding for CSV and Plain Text + + diff --git a/chronoracetable.cpp b/chronoracetable.cpp index a4470de..e5ca083 100644 --- a/chronoracetable.cpp +++ b/chronoracetable.cpp @@ -18,6 +18,7 @@ #include #include "chronoracetable.hpp" +#include "crloader.hpp" ChronoRaceTable::ChronoRaceTable(QWidget *parent) : QDialog(parent) { @@ -97,18 +98,21 @@ void ChronoRaceTable::rowDel() const void ChronoRaceTable::modelImport() { if (QMessageBox::question(this, tr("CSV Encoding"), - tr("Are the data you are importing ISO-8859-1 (Latin-1) encoded?\n" - "Choose No to use UTF-8 encoding. If in doubt, choose Yes."), - QMessageBox::No | QMessageBox::Yes, QMessageBox::Yes) == QMessageBox::Yes) { - emit modelImported(CRLoader::Encoding::LATIN1); - } else { - emit modelImported(CRLoader::Encoding::UTF8); + tr("The data being imported must be %1 encoded.\n" + "Continue?").arg(CRLoader::encodingToLabel(CRLoader::getEncoding())), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { + emit modelImported(); } } void ChronoRaceTable::modelExport() { - emit modelExported(); + if (QMessageBox::question(this, tr("CSV Encoding"), + tr("The data will be exported with %1 encoding.\n" + "Continue?").arg(CRLoader::encodingToLabel(CRLoader::getEncoding())), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { + emit modelExported(); + } } void ChronoRaceTable::dialogQuit() diff --git a/chronoracetable.hpp b/chronoracetable.hpp index 96e3ddd..781d12d 100644 --- a/chronoracetable.hpp +++ b/chronoracetable.hpp @@ -21,7 +21,6 @@ #include #include "ui_chronoracetable.h" -#include "crloader.hpp" #include "crtablemodel.hpp" class ChronoRaceTable : public QDialog @@ -48,7 +47,7 @@ public slots: signals: void newRowCount(int count); - void modelImported(CRLoader::Encoding encoding); + void modelImported(); void modelExported(); void countersRefresh(); diff --git a/crloader.cpp b/crloader.cpp index a58ecb1..777d7e1 100644 --- a/crloader.cpp +++ b/crloader.cpp @@ -28,7 +28,7 @@ TeamsListModel CRLoader::teamsListModel; TimingsModel CRLoader::timingsModel; CategoriesModel CRLoader::categoriesModel; QList CRLoader::standardItemList; -CRLoader::Encoding CRLoader::encoding = CRLoader::Encoding::UTF8; +CRLoader::Encoding CRLoader::encoding = CRLoader::Encoding::LATIN1; CRLoader::Format CRLoader::format = CRLoader::Format::PDF; CRTableModel *CRLoader::getStartListModel() diff --git a/icons/abc.svg b/icons/abc.svg new file mode 100644 index 0000000..1f3c09e --- /dev/null +++ b/icons/abc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lbchronorace.cpp b/lbchronorace.cpp index 7351be3..c85ebf6 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -118,6 +119,7 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : QObject::connect(ui->actionEditTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::show); QObject::connect(ui->actionImportTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::modelImport); QObject::connect(ui->actionExportTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::modelExport); + QObject::connect(ui->actionSetEncoding, &QAction::triggered, this, &LBChronoRace::setEncoding); QObject::connect(ui->actionMakeStartList, &QAction::triggered, this, &LBChronoRace::makeStartList); QObject::connect(ui->actionCollectTimings, &QAction::triggered, &timings, &ChronoRaceTimings::show); QObject::connect(ui->actionMakeRankings, &QAction::triggered, this, &LBChronoRace::makeRankings); @@ -165,7 +167,7 @@ void LBChronoRace::appendErrorMessage(QString const &message) const ui->errorDisplay->appendPlainText(message); } -void LBChronoRace::importStartList(CRLoader::Encoding encoding) +void LBChronoRace::importStartList() { startListFileName = QFileDialog::getOpenFileName(this, tr("Select Start List"), lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); @@ -174,7 +176,6 @@ void LBChronoRace::importStartList(CRLoader::Encoding encoding) QPair count(0, 0); appendInfoMessage(tr("Start List File: %1").arg(startListFileName)); try { - CRLoader::setEncoding(encoding); count = CRLoader::importStartList(startListFileName); appendInfoMessage(tr("Loaded: %n competitor(s)", "", count.first)); appendInfoMessage(tr("Loaded: %n team(s)", "", count.second)); @@ -187,7 +188,7 @@ void LBChronoRace::importStartList(CRLoader::Encoding encoding) } } -void LBChronoRace::importCategoriesList(CRLoader::Encoding encoding) +void LBChronoRace::importCategoriesList() { categoriesFileName = QFileDialog::getOpenFileName(this, tr("Select Categories File"), lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); @@ -196,7 +197,6 @@ void LBChronoRace::importCategoriesList(CRLoader::Encoding encoding) int count = 0; appendInfoMessage(tr("Categories File: %1").arg(categoriesFileName)); try { - CRLoader::setEncoding(encoding); count = CRLoader::importCategories(categoriesFileName); appendInfoMessage(tr("Loaded: %n category(s)", "", count)); lastSelectedPath = QFileInfo(categoriesFileName).absoluteDir(); @@ -207,7 +207,7 @@ void LBChronoRace::importCategoriesList(CRLoader::Encoding encoding) } } -void LBChronoRace::importTimingsList(CRLoader::Encoding encoding) +void LBChronoRace::importTimingsList() { timingsFileName = QFileDialog::getOpenFileName(this, tr("Select Timings File"), lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); @@ -216,7 +216,6 @@ void LBChronoRace::importTimingsList(CRLoader::Encoding encoding) int count = 0; appendInfoMessage(tr("Timings File: %1").arg(timingsFileName)); try { - CRLoader::setEncoding(encoding); count = CRLoader::importTimings(timingsFileName); appendInfoMessage(tr("Loaded: %n timing(s)", "", count)); lastSelectedPath = QFileInfo(timingsFileName).absoluteDir(); @@ -351,7 +350,7 @@ void LBChronoRace::encodingSelector(int idx) const CRLoader::setEncoding(CRLoader::Encoding::UTF8); break; default: - CRLoader::setEncoding(CRLoader::Encoding::UTF8); + CRLoader::setEncoding(CRLoader::Encoding::LATIN1); break; } @@ -508,6 +507,46 @@ void LBChronoRace::saveRaceAs() raceDataFileName = oldRaceDataFileName; } +void LBChronoRace::setEncoding() +{ + bool ok = false; + int current = 0; + + QStringList items = { + CRLoader::encodingToLabel(CRLoader::Encoding::LATIN1), + CRLoader::encodingToLabel(CRLoader::Encoding::UTF8) + }; + + switch (CRLoader::getEncoding()) { + case CRLoader::Encoding::LATIN1: + current = 0; + break; + case CRLoader::Encoding::UTF8: + current = 1; + break; + default: + appendErrorMessage(tr("Error: unexpected encoding value (fall back to the default)")); + break; + } + + QString item = QInputDialog::getItem(this, + tr("Settings"), + tr("CSV and Plain Text Encoding"), + items, + current, + false, + &ok); + + if (ok) { + if (item == items[0]) + CRLoader::setEncoding(CRLoader::Encoding::LATIN1); + else if (item == items[1]) + CRLoader::setEncoding(CRLoader::Encoding::UTF8); + else + appendErrorMessage(tr("Error: unexpected encoding value (encoding not changed)")); + } +} + void LBChronoRace::makeStartList() { //NOSONAR ui->errorDisplay->clear(); diff --git a/lbchronorace.hpp b/lbchronorace.hpp index 5c960b1..86928d4 100644 --- a/lbchronorace.hpp +++ b/lbchronorace.hpp @@ -107,14 +107,16 @@ private slots: void saveRace(); void saveRaceAs(); + void setEncoding(); + void encodingSelector(int idx) const; void formatSelector(int idx) const; void makeStartList(); void makeRankings(); - void importStartList(CRLoader::Encoding encoding); - void importCategoriesList(CRLoader::Encoding encoding); - void importTimingsList(CRLoader::Encoding encoding); + void importStartList(); + void importCategoriesList(); + void importTimingsList(); void exportStartList(); void exportTeamList(); diff --git a/materialicons.qrc b/materialicons.qrc index fb9c9c7..3195e6d 100644 --- a/materialicons.qrc +++ b/materialicons.qrc @@ -32,5 +32,6 @@ icons/unknown_med.svg icons/person.svg icons/group.svg + icons/abc.svg diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index 7ce0ec4..eea2435 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -501,10 +501,16 @@ Quit - Are the data you are importing ISO-8859-1 (Latin-1) encoded? -Choose No to use UTF-8 encoding. If in doubt, choose Yes. - Are the data you are importing ISO-8859-1 (Latin-1) encoded? -Choose No to use UTF-8 encoding. If in doubt, choose Yes. + The data being imported must be %1 encoded. +Continue? + The data being imported must be %1 encoded. +Continue? + + + The data will be exported with %1 encoding. +Continue? + The data will be exported with %1 encoding. +Continue? CSV Encoding @@ -992,6 +998,38 @@ Please uodate the application. Export Timings to another PC Export Timings to another PC + + Import Timings + Import Timings + + + Export Timings + Export Timings + + + Set Encoding + Set Encoding + + + Set encoding for CSV and Plain Text + Set encoding for CSV and Plain Text + + + Error: unexpected encoding value (fall back to the default) + Error: unexpected encoding value (fall back to the default) + + + Settings + Settings + + + CSV and Plain Text Encoding + CSV and Plain Text Encoding + + + Error: unexpected encoding value (encoding not changed) + Error: unexpected encoding value (encoding not changed) + PDFRankingPrinter diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index dca53a5..ea99fba 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -501,10 +501,16 @@ Esci - Are the data you are importing ISO-8859-1 (Latin-1) encoded? -Choose No to use UTF-8 encoding. If in doubt, choose Yes. - I dati che si stanno per importare sono codificati ISO-8859-1 (Latin-1)? -Sceglire No per usare la codifica UTF-8. Nel dubbio, scegliere Si. + The data being imported must be %1 encoded. +Continue? + I dati da importare devono avere codifica %1. +Procedere? + + + The data will be exported with %1 encoding. +Continue? + I dati saranno esportati con codifica %1. +Procedere? CSV Encoding @@ -992,6 +998,38 @@ Aggiornare l'applicazione. Export Timings to another PC Esporta Tempi per un altro PC + + Import Timings + Importa Tempi + + + Export Timings + Esporta Tempi + + + Set Encoding + Imposta Codifica + + + Set encoding for CSV and Plain Text + Imposta codifica per CSV e Testo + + + Error: unexpected encoding value (fall back to the default) + Errore: valore codifica inatteso (ripiego sulla codifica predefinita) + + + Settings + Impostazioni + + + CSV and Plain Text Encoding + Codifica CVS e Testo + + + Error: unexpected encoding value (encoding not changed) + Errore: valore codifica inatteso (codifica non modificata) + PDFRankingPrinter From fe72e71bf9e06aa880a4bfb6665576d389b55fdd Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Sat, 27 Jul 2024 14:50:12 +0200 Subject: [PATCH 04/20] Translate the "Created with" sentence on PDFs --- pdfrankingprinter.cpp | 10 ++++------ translations/LBChronoRace_en.ts | 4 ++++ translations/LBChronoRace_it.ts | 4 ++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pdfrankingprinter.cpp b/pdfrankingprinter.cpp index b3eec25..7e6bf42 100644 --- a/pdfrankingprinter.cpp +++ b/pdfrankingprinter.cpp @@ -905,9 +905,8 @@ void PDFRankingPrinter::drawTemplatePortrait(QString const &fullDescription, int QString editingTimestamp = QDateTime::currentDateTime().toString("dd/MM/yyyy hh:mm"); // Created with - QString createdWith = QString("Created with %1 %2") - .arg(QStringLiteral(LBCHRONORACE_NAME), - QStringLiteral(LBCHRONORACE_VERSION)); + QString createdWith = tr("Created with %1 %2").arg(QStringLiteral(LBCHRONORACE_NAME), + QStringLiteral(LBCHRONORACE_VERSION)); // Horizontal lines if (page == 1) { @@ -1040,9 +1039,8 @@ void PDFRankingPrinter::drawTemplatePortrait(QString const &fullDescription, int //NOSONAR QString editingTimestamp = QDateTime::currentDateTime().toString("dd/MM/yyyy hh:mm"); //NOSONAR // Created with -//NOSONAR QString createdWith = QString("Created with %1 %2") -//NOSONAR .arg(QStringLiteral(LBCHRONORACE_NAME), -//NOSONAR QStringLiteral(LBCHRONORACE_VERSION)); +//NOSONAR QString createdWith = tr("Created with %1 %2").arg(QStringLiteral(LBCHRONORACE_NAME), +//NOSONAR QStringLiteral(LBCHRONORACE_VERSION)); //NOSONAR // Horizontal lines //NOSONAR if (page == 1) { diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index eea2435..0583b1c 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -1180,6 +1180,10 @@ Please uodate the application. Error: cannot write to PDF Error: cannot write to PDF + + Created with %1 %2 + Created with %1 %2 + QMessageBox diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index ea99fba..3cf6c4b 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -1180,6 +1180,10 @@ Aggiornare l'applicazione. Error: cannot write to PDF Error: impossibile scrivere su PDF + + Created with %1 %2 + Creato con %1 %2 + QMessageBox From fd1199c7108db9a0093b50bc9d208d8f6cac3d85 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Mon, 29 Jul 2024 14:20:43 +0200 Subject: [PATCH 05/20] Do not drop recorded times or bibs In case a recorded bib misses the related time or vice-versa, the value now is not dropped anymore. --- chronoracetimings.cpp | 26 ++++++++++++++++++-------- translations/LBChronoRace_en.ts | 16 ++++++++-------- translations/LBChronoRace_it.ts | 16 ++++++++-------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/chronoracetimings.cpp b/chronoracetimings.cpp index 8f33cf8..6cbcbb2 100644 --- a/chronoracetimings.cpp +++ b/chronoracetimings.cpp @@ -262,17 +262,27 @@ void ChronoRaceTimings::saveTimings() int r; int c; + QTableWidgetItem const *bib; + QTableWidgetItem const *time; + CRLoader::clearTimings(); - for (r = 0, c = 0; r < ui->dataArea->rowCount(); r++) - if (ui->dataArea->item(r, 0) && ui->dataArea->item(r, 1)) { - CRLoader::addTiming(ui->dataArea->item(r, 0)->text(), ui->dataArea->item(r, 1)->text()); - c++; - } else if (ui->dataArea->item(r, 0)) { - emit error(tr("Droped bib %1 due to missing time").arg(ui->dataArea->item(r, 0)->text())); - } else if (ui->dataArea->item(r, 1)) { - emit error(tr("Dropped time %1 due to missing bib").arg(ui->dataArea->item(r, 1)->text())); + for (r = 0, c = 0; r < ui->dataArea->rowCount(); r++) { + + bib = ui->dataArea->item(r, 0); + time = ui->dataArea->item(r, 1); + + if (!bib && !time) { + continue; + } else if (!bib) { + emit error(tr("Missing bib for time %1").arg(time->text())); + } else if (!time) { + emit error(tr("Missing time for bib %1").arg(bib->text())); } + CRLoader::addTiming(bib ? bib->text() : "0", time ? time->text() : "0:00:00"); + c++; + } + emit newTimingsCount(c); } diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index 0583b1c..038a76d 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -599,14 +599,6 @@ Do you want to discard the recorded timings? The previously recorded timings list will be preserved. Do you want to discard the recorded timings? - - Droped bib %1 due to missing time - Droped bib %1 due to missing time - - - Dropped time %1 due to missing bib - Dropped time %1 due to missing bib - Reset Timings List Reset Timings List @@ -623,6 +615,14 @@ Continue? Timer controls are disabled when locked Timer controls are disabled when locked + + Missing time for bib %1 + Missing time for bib %1 + + + Missing bib for time %1 + Missing bib for time %1 + ClassEntry diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 3cf6c4b..30b1233 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -599,14 +599,6 @@ Do you want to discard the recorded timings? La lista tempi precedente non sarà sovrascritta. Eliminare i tempi registrati? - - Droped bib %1 due to missing time - Scartato pettorale %1 (tempo mancante) - - - Dropped time %1 due to missing bib - Scartato tempo %1 (pettorale mancante) - Reset Timings List Azzera Lista Tempi @@ -623,6 +615,14 @@ Continuare? Timer controls are disabled when locked Disabilita i pulsanti di controllo del cronometro + + Missing time for bib %1 + Tempo mancante per il pettorale %1 + + + Missing bib for time %1 + Pettorale mancante per il tempo %1 + ClassEntry From 532d1d9f46c33600471aa23fcedbf6c3e7a65939 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Tue, 16 Jul 2024 17:47:44 +0200 Subject: [PATCH 06/20] Support mixed club relays and distinguish ranking from category It is now possible to define categories for relays consisting of competitors from different clubs. In addition, the 'ranking' is now defined as a container of one or more categories. --- CMakeLists.txt | 18 +- categoriesmodel.cpp | 96 ++--- category.cpp | 174 +++++---- category.hpp | 36 +- cattypedelegate.cpp | 34 +- cattypedelegate.hpp | 6 +- chronorace.ui | 183 +++++---- chronoracetable.cpp | 5 +- classentry.cpp | 334 ++++++++++++++-- classentry.hpp | 50 ++- clubdelegate.cpp | 13 +- clubdelegate.hpp | 4 +- competitor.cpp | 117 ++---- competitor.hpp | 21 +- sexdelegate.cpp => compsexdelegate.cpp | 50 +-- sexdelegate.hpp => compsexdelegate.hpp | 18 +- crhelper.cpp | 246 ++++++++++++ crhelper.hpp | 61 +++ crloader.cpp | 190 ++++----- crloader.hpp | 36 +- csvrankingprinter.cpp | 33 +- csvrankingprinter.hpp | 6 +- icons/filter_1.svg | 1 + icons/filter_2.svg | 1 + icons/filter_3.svg | 1 + icons/filter_4.svg | 1 + icons/filter_5.svg | 1 + icons/filter_6.svg | 1 + icons/filter_7.svg | 1 + icons/filter_8.svg | 1 + icons/filter_9.svg | 1 + lbchronorace.cpp | 110 ++++-- lbchronorace.hpp | 32 +- materialicons.qrc | 9 + multiselectcombobox.cpp | 255 +++++++++++++ multiselectcombobox.hpp | 67 ++++ pdfrankingprinter.cpp | 142 ++++--- pdfrankingprinter.hpp | 12 +- ranking.cpp | 189 +++++++++ ranking.hpp | 104 +++++ rankingcatsdelegate.cpp | 77 ++++ rankingcatsdelegate.hpp | 48 +++ rankingprinter.hpp | 8 +- rankingsbuilder.cpp | 235 +++++++----- rankingsbuilder.hpp | 17 +- rankingsmodel.cpp | 230 +++++++++++ rankingsmodel.hpp | 66 ++++ rankingswizard.cpp | 76 ++-- rankingswizard.hpp | 4 + rankingswizardformat.cpp | 11 +- rankingswizardselection.cpp | 24 +- rankingswizardselection.hpp | 6 +- catsexdelegate.cpp => rankingtypedelegate.cpp | 52 +-- catsexdelegate.hpp => rankingtypedelegate.hpp | 18 +- samples/mass_start/latin1/categories.csv | 26 +- samples/mass_start/latin1/rankings.csv | 6 + samples/mass_start/latin1/startlist.csv | 2 +- samples/mass_start/mass_start.crd | Bin 904082 -> 904806 bytes samples/mass_start/utf8/categories.csv | 26 +- samples/mass_start/utf8/rankings.csv | 6 + samples/mass_start/utf8/startlist.csv | 2 +- samples/relay/latin1/categories.csv | 9 +- samples/relay/latin1/rankings.csv | 6 + samples/relay/relay.crd | Bin 897730 -> 897954 bytes startlistmodel.cpp | 137 ++++--- teamclassentry.cpp | 2 +- teamclassentry.hpp | 4 +- teamslistmodel.cpp | 24 +- timing.cpp | 28 +- timing.hpp | 3 - timingsmodel.cpp | 66 ++-- translations/LBChronoRace_en.ts | 357 +++++++++++------ translations/LBChronoRace_it.ts | 361 +++++++++++------- txtrankingprinter.cpp | 31 +- txtrankingprinter.hpp | 6 +- 75 files changed, 3343 insertions(+), 1291 deletions(-) rename sexdelegate.cpp => compsexdelegate.cpp (53%) rename sexdelegate.hpp => compsexdelegate.hpp (82%) create mode 100644 crhelper.cpp create mode 100644 crhelper.hpp create mode 100644 icons/filter_1.svg create mode 100644 icons/filter_2.svg create mode 100644 icons/filter_3.svg create mode 100644 icons/filter_4.svg create mode 100644 icons/filter_5.svg create mode 100644 icons/filter_6.svg create mode 100644 icons/filter_7.svg create mode 100644 icons/filter_8.svg create mode 100644 icons/filter_9.svg create mode 100644 multiselectcombobox.cpp create mode 100644 multiselectcombobox.hpp create mode 100644 ranking.cpp create mode 100644 ranking.hpp create mode 100644 rankingcatsdelegate.cpp create mode 100644 rankingcatsdelegate.hpp create mode 100644 rankingsmodel.cpp create mode 100644 rankingsmodel.hpp rename catsexdelegate.cpp => rankingtypedelegate.cpp (53%) rename catsexdelegate.hpp => rankingtypedelegate.hpp (81%) create mode 100644 samples/mass_start/latin1/rankings.csv create mode 100644 samples/mass_start/utf8/rankings.csv create mode 100644 samples/relay/latin1/rankings.csv diff --git a/CMakeLists.txt b/CMakeLists.txt index 9650fa9..6d5923a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,8 +53,6 @@ set(PROJECT_SOURCES categoriesmodel.hpp category.cpp category.hpp - catsexdelegate.cpp - catsexdelegate.hpp cattypedelegate.cpp cattypedelegate.hpp chronorace.ui @@ -75,6 +73,10 @@ set(PROJECT_SOURCES clubdelegate.hpp competitor.cpp competitor.hpp + compsexdelegate.cpp + compsexdelegate.hpp + crhelper.cpp + crhelper.hpp crloader.cpp crloader.hpp crtablemodel.hpp @@ -84,12 +86,20 @@ set(PROJECT_SOURCES lbchronorace.hpp lbcrexception.cpp lbcrexception.hpp + multiselectcombobox.cpp + multiselectcombobox.hpp pdfrankingprinter.cpp pdfrankingprinter.hpp + ranking.cpp + ranking.hpp + rankingcatsdelegate.cpp + rankingcatsdelegate.hpp rankingsbuilder.cpp rankingsbuilder.hpp rankingprinter.cpp rankingprinter.hpp + rankingsmodel.cpp + rankingsmodel.hpp rankingswizard.cpp rankingswizard.hpp rankingswizardformat.cpp @@ -98,8 +108,8 @@ set(PROJECT_SOURCES rankingswizardmode.hpp rankingswizardselection.cpp rankingswizardselection.hpp - sexdelegate.cpp - sexdelegate.hpp + rankingtypedelegate.cpp + rankingtypedelegate.hpp startlistmodel.cpp startlistmodel.hpp teamclassentry.cpp diff --git a/categoriesmodel.cpp b/categoriesmodel.cpp index 281ed01..05a2e22 100644 --- a/categoriesmodel.cpp +++ b/categoriesmodel.cpp @@ -15,8 +15,10 @@ * along with this program. If not, see . * *****************************************************************************/ +#include "lbchronorace.hpp" #include "categoriesmodel.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" QDataStream &operator<<(QDataStream &out, CategoriesModel const &data) { @@ -63,10 +65,8 @@ QVariant CategoriesModel::data(QModelIndex const &index, int role) const if ((role == Qt::DisplayRole) || (role == Qt::EditRole)) switch (index.column()) { - case static_cast(Category::Field::CTF_TEAM): - return QVariant(categories.at(index.row()).isTeam() ? tr("T") : tr("I")); - case static_cast(Category::Field::CTF_SEX): - return QVariant(Competitor::toSexString(categories.at(index.row()).getSex())); + case static_cast(Category::Field::CTF_TYPE): + return QVariant(CRHelper::toTypeString(categories.at(index.row()).getType())); case static_cast(Category::Field::CTF_TO_YEAR): return QVariant(categories.at(index.row()).getToYear()); case static_cast(Category::Field::CTF_FROM_YEAR): @@ -80,10 +80,8 @@ QVariant CategoriesModel::data(QModelIndex const &index, int role) const } else if (role == Qt::ToolTipRole) switch (index.column()) { - case static_cast(Category::Field::CTF_TEAM): - return QVariant(tr("Individual/Relay (I) or Club (T)")); - case static_cast(Category::Field::CTF_SEX): - return QVariant(tr("Men (M), Women (F), Mixed (X) or All (U)")); + case static_cast(Category::Field::CTF_TYPE): + return QVariant(tr("Male Individual/Relay (M), Female Individual/Relay (F), Mixed M/F Relay (X), Male Mixed Clubs Relay (Y), or Female Mixed Clubs Relay (Y)")); case static_cast(Category::Field::CTF_TO_YEAR): return QVariant(tr("The category will include competitors born up to and including this year (i.e. 2000); 0 to disable")); case static_cast(Category::Field::CTF_FROM_YEAR): @@ -101,47 +99,51 @@ QVariant CategoriesModel::data(QModelIndex const &index, int role) const bool CategoriesModel::setData(QModelIndex const &index, QVariant const &value, int role) { - bool retval = false; - if (index.isValid() && role == Qt::EditRole) { - uint uval; - switch (index.column()) { - case static_cast(Category::Field::CTF_TEAM): - categories[index.row()].setTeam(QString::compare(value.toString().trimmed(), "T", Qt::CaseInsensitive) == 0); - break; - case static_cast(Category::Field::CTF_SEX): - try { - auto sex = Competitor::toSex(value.toString().trimmed()); - categories[index.row()].setSex(sex); - retval = true; - } catch (ChronoRaceException &ex) { - emit error(ex.getMessage()); - retval = false; - } - break; - case static_cast(Category::Field::CTF_TO_YEAR): - uval = value.toUInt(&retval); - if (retval) categories[index.row()].setToYear(uval); - break; - case static_cast(Category::Field::CTF_FROM_YEAR): - uval = value.toUInt(&retval); - if (retval) categories[index.row()].setFromYear(uval); - break; - case static_cast(Category::Field::CTF_FULL_DESCR): - categories[index.row()].setFullDescription(value.toString().simplified()); - retval = true; - break; - case static_cast(Category::Field::CTF_SHORT_DESCR): - categories[index.row()].setShortDescription(value.toString().simplified()); + if (!index.isValid()) + return retval; + + if (role != Qt::EditRole) + return retval; + + if (value.toString().contains(LBChronoRace::csvFilter)) + return retval; + + uint uval; + switch (index.column()) { + case static_cast(Category::Field::CTF_TYPE): + try { + auto type = CRHelper::toCategoryType(value.toString().trimmed()); + categories[index.row()].setType(type); retval = true; - break; - default: - break; + } catch (ChronoRaceException &ex) { + emit error(ex.getMessage()); + retval = false; } - - if (retval) emit dataChanged(index, index); + break; + case static_cast(Category::Field::CTF_TO_YEAR): + uval = value.toUInt(&retval); + if (retval) categories[index.row()].setToYear(uval); + break; + case static_cast(Category::Field::CTF_FROM_YEAR): + uval = value.toUInt(&retval); + if (retval) categories[index.row()].setFromYear(uval); + break; + case static_cast(Category::Field::CTF_FULL_DESCR): + categories[index.row()].setFullDescription(value.toString().simplified()); + retval = true; + break; + case static_cast(Category::Field::CTF_SHORT_DESCR): + categories[index.row()].setShortDescription(value.toString().simplified()); + retval = true; + break; + default: + break; } + + if (retval) emit dataChanged(index, index); + return retval; } @@ -152,10 +154,8 @@ QVariant CategoriesModel::headerData(int section, Qt::Orientation orientation, i if (orientation == Qt::Horizontal) switch (section) { - case static_cast(Category::Field::CTF_TEAM): - return QString("%1").arg(tr("Individual/Club")); - case static_cast(Category::Field::CTF_SEX): - return QString("%1").arg(tr("Sex")); + case static_cast(Category::Field::CTF_TYPE): + return QString("%1").arg(tr("Type")); case static_cast(Category::Field::CTF_TO_YEAR): return QString("%1").arg(tr("Up to")); case static_cast(Category::Field::CTF_FROM_YEAR): diff --git a/category.cpp b/category.cpp index f2f4c90..5a3c13d 100644 --- a/category.cpp +++ b/category.cpp @@ -16,28 +16,20 @@ *****************************************************************************/ #include "category.hpp" +#include "lbchronorace.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" Category::Field CategorySorter::sortingField = Category::Field::CTF_FIRST; Qt::SortOrder CategorySorter::sortingOrder = Qt::AscendingOrder; -Category::Category(QString const &team) -{ - if (team.length() != 1) { - throw(ChronoRaceException(tr("Illegal category type - expected 'I' or 'T' - found %1").arg(team))); - } else { - this->team = (team.compare("T", Qt::CaseInsensitive) == 0); - } -} - QDataStream &operator<<(QDataStream &out, Category const &category) { - out << qint32(category.team) - << Competitor::toSexString(category.sex) + out << category.fullDescription + << category.shortDescription + << quint32(category.type) << quint32(category.toYear) - << quint32(category.fromYear) - << category.fullDescription - << category.shortDescription; + << quint32(category.fromYear); return out; } @@ -46,54 +38,65 @@ QDataStream &operator>>(QDataStream &in, Category &category) { quint32 toYear32; quint32 fromYear32; - qint32 team32; - QString sexStr; - - in >> team32 - >> sexStr - >> toYear32 - >> fromYear32 - >> category.fullDescription - >> category.shortDescription; - - category.team = (bool) team32; - category.sex = Competitor::toSex(sexStr); + + if (LBChronoRace::binFormat < LBCHRONORACE_BIN_FMT_v4) { + qint32 team32; + QString sexStr; + + in >> team32 + >> sexStr + >> toYear32 + >> fromYear32 + >> category.fullDescription + >> category.shortDescription; + + switch (CRHelper::toSex(sexStr)) { + case Competitor::Sex::MALE: + category.type = Category::Type::MALE; + break; + case Competitor::Sex::FEMALE: + category.type = Category::Type::FEMALE; + break; + case Competitor::Sex::UNDEFINED: + category.type = Category::Type::RELAY_MF; + break; + } + } else { + qint32 type32; + + in >> category.fullDescription + >> category.shortDescription + >> type32 + >> toYear32 + >> fromYear32; + + switch (type32) { + case static_cast(Category::Type::MALE): + [[fallthrough]]; + case static_cast(Category::Type::FEMALE): + [[fallthrough]]; + case static_cast(Category::Type::RELAY_MF): + [[fallthrough]]; + case static_cast(Category::Type::RELAY_Y): + [[fallthrough]]; + case static_cast(Category::Type::RELAY_X): + category.type = static_cast(type32); + break; + default: + category.illegalType(type32); + break; + } + } + category.toYear = toYear32; category.fromYear = fromYear32; return in; } -Category::Type Category::toType(QString const &type) +void Category::illegalType(quint32 value) const { - if (type.compare("I", Qt::CaseInsensitive) == 0) - return Type::INDIVIDUAL; - else if (type.compare("T", Qt::CaseInsensitive) == 0) - return Type::CLUB; - else - throw(ChronoRaceException(tr("Illegal type '%1'").arg(type))); -} - -QString Category::toTypeString(Type const type) -{ - switch (type) { - case Type::INDIVIDUAL: - return "I"; - case Type::CLUB: - return "T"; - default: - throw(ChronoRaceException(tr("Unexpected Type enum value '%1'").arg(static_cast(type)))); - } -} - -bool Category::isTeam() const -{ - return team; -} - -void Category::setTeam(bool newTeam) -{ - this->team = newTeam; + throw(ChronoRaceException(tr("Illegal category type '%1'").arg(value))); } uint Category::getFromYear() const @@ -116,14 +119,14 @@ void Category::setFullDescription(QString const &newFullDescription) this->fullDescription = newFullDescription; } -Competitor::Sex Category::getSex() const +Category::Type Category::getType() const { - return sex; + return type; } -void Category::setSex(Competitor::Sex const newSex) +void Category::setType(Category::Type const newType) { - this->sex = newSex; + this->type = newType; } QString const &Category::getShortDescription() const @@ -146,27 +149,50 @@ void Category::setToYear(uint newToYear) this->toYear = newToYear; } -bool Category::isValid() const +uint Category::getWeight() const { - return (!fullDescription.isEmpty() && !shortDescription.isEmpty()); + uint weight = 0u; + + if (this->fromYear) + weight++; + + if (this->toYear) + weight++; + + //NOSONAR switch (this->type) { + //NOSONAR case Category::Type::RELAY_Y: + //NOSONAR [[fallthrough]]; + //NOSONAR case Category::Type::RELAY_X: + //NOSONAR weight++; + //NOSONAR break; + //NOSONAR case Category::Type::MALE: + //NOSONAR [[fallthrough]]; + //NOSONAR case Category::Type::FEMALE: + //NOSONAR [[fallthrough]]; + //NOSONAR case Category::Type::RELAY_MF: + //NOSONAR // do nothing + //NOSONAR break; + //NOSONAR default: + //NOSONAR Q_UNREACHABLE(); + //NOSONAR break; + //NOSONAR } + + return weight; } -bool Category::includes(Competitor const *competitor) const +bool Category::isValid() const { - return (competitor && !this->isTeam() && - (competitor->getSex() == this->getSex()) && - (competitor->getYear() >= this->getFromYear()) && - (competitor->getYear() <= this->getToYear())); + return (!fullDescription.isEmpty() && !shortDescription.isEmpty()); } bool Category::operator< (Category const &rhs) const { - return (!this->isTeam() && rhs.isTeam()); + return (this->getFullDescription() < rhs.getFullDescription()); } bool Category::operator> (Category const &rhs) const { - return (this->isTeam() && !rhs.isTeam()); + return (this->getFullDescription() > rhs.getFullDescription()); } bool Category::operator<=(Category const &rhs) const @@ -179,11 +205,17 @@ bool Category::operator>=(Category const &rhs) const return !(*this < rhs); } +bool Category::operator== (Category const &rhs) const +{ + return ((this->getFullDescription() == rhs.getFullDescription()) + && (this->getShortDescription() == rhs.getShortDescription())); +} + bool CategorySorter::operator() (Category const &lhs, Category const &rhs) const { switch(sortingField) { - case Category::Field::CTF_SEX: - return (sortingOrder == Qt::DescendingOrder) ? (Competitor::toSexString(lhs.getSex()) > Competitor::toSexString(rhs.getSex())) : (Competitor::toSexString(lhs.getSex()) < Competitor::toSexString(rhs.getSex())); + case Category::Field::CTF_TYPE: + return (sortingOrder == Qt::DescendingOrder) ? (lhs.getType() > rhs.getType()) : (lhs.getType() < rhs.getType()); case Category::Field::CTF_TO_YEAR: return (sortingOrder == Qt::DescendingOrder) ? (lhs.getToYear() > rhs.getToYear()) : (lhs.getToYear() < rhs.getToYear()); case Category::Field::CTF_FROM_YEAR: @@ -192,8 +224,6 @@ bool CategorySorter::operator() (Category const &lhs, Category const &rhs) const return (sortingOrder == Qt::DescendingOrder) ? (lhs.getFullDescription() > rhs.getFullDescription()) : (lhs.getFullDescription() < rhs.getFullDescription()); case Category::Field::CTF_SHORT_DESCR: return (sortingOrder == Qt::DescendingOrder) ? (lhs.getShortDescription() > rhs.getShortDescription()) : (lhs.getShortDescription() < rhs.getShortDescription()); - case Category::Field::CTF_TEAM: - [[fallthrough]]; default: return (sortingOrder == Qt::DescendingOrder) ? (lhs > rhs) : (lhs < rhs); } diff --git a/category.hpp b/category.hpp index 9de5545..fbb43f8 100644 --- a/category.hpp +++ b/category.hpp @@ -18,10 +18,9 @@ #ifndef CATEGORY_H #define CATEGORY_H +#include #include -#include "competitor.hpp" - namespace category { class Category; class CategorySorter; @@ -33,8 +32,11 @@ class Category { public: enum class Type { - INDIVIDUAL, - CLUB, + MALE = 0, + FEMALE = 1, + RELAY_MF = 2, + RELAY_Y = 3, + RELAY_X = 4 }; enum class Field @@ -42,52 +44,46 @@ class Category { CTF_FIRST = 0, CTF_FULL_DESCR = 0, CTF_SHORT_DESCR = 1, - CTF_SEX = 2, + CTF_TYPE = 2, CTF_FROM_YEAR = 3, CTF_TO_YEAR = 4, - CTF_TEAM = 5, - CTF_LAST = 5, - CTF_COUNT = 6 + CTF_LAST = 4, + CTF_COUNT = 5 }; private: - bool team { false }; - Competitor::Sex sex { Competitor::Sex::UNDEFINED }; + Type type { Type::MALE }; uint toYear { 0u }; uint fromYear { 0u }; QString fullDescription { "" }; QString shortDescription { "" }; + void illegalType(quint32 value) const; + public: Category() = default; - explicit Category(QString const &team); friend QDataStream &operator<<(QDataStream &out, Category const &category); friend QDataStream &operator>>(QDataStream &in, Category &category); - static Type toType(QString const &type); - static QString toTypeString(Type const type); - - bool isTeam() const; - void setTeam(bool newTeam); uint getFromYear() const; void setFromYear(uint newFromYear); QString const &getFullDescription() const; void setFullDescription(QString const &newFullDescription); - Competitor::Sex getSex() const; - void setSex(Competitor::Sex const newSex); + Type getType() const; + void setType(Type const newType); QString const &getShortDescription() const; void setShortDescription(QString const &newShortDescription); uint getToYear() const; void setToYear(unsigned int newToYear); + uint getWeight() const; bool isValid() const; - bool includes(Competitor const *competitor) const; - bool operator< (Category const &rhs) const; bool operator> (Category const &rhs) const; bool operator<= (Category const &rhs) const; bool operator>= (Category const &rhs) const; + bool operator== (Category const &rhs) const; }; Category::Field &operator++(Category::Field &field); diff --git a/cattypedelegate.cpp b/cattypedelegate.cpp index 41d6292..a11a533 100644 --- a/cattypedelegate.cpp +++ b/cattypedelegate.cpp @@ -16,18 +16,21 @@ *****************************************************************************/ #include "cattypedelegate.hpp" -#include "lbcrexception.hpp" +#include "crhelper.hpp" CategoryTypeDelegate::CategoryTypeDelegate(QObject *parent) : QStyledItemDelegate(parent) { - auto *comboBox = box.data(); + auto *comboBox = categoryTypeBox.data(); comboBox->setEditable(false); comboBox->setInsertPolicy(QComboBox::NoInsert); comboBox->setDuplicatesEnabled(false); comboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); - comboBox->addItem(QIcon(":/material/icons/person.svg"), toCatTypeString(Category::Type::INDIVIDUAL), QVariant(Category::toTypeString(Category::Type::INDIVIDUAL))); - comboBox->addItem(QIcon(":/material/icons/group.svg"), toCatTypeString(Category::Type::CLUB), QVariant(Category::toTypeString(Category::Type::CLUB))); + comboBox->addItem(QIcon(":/material/icons/male.svg"), CRHelper::toCategoryTypeString(Category::Type::MALE), CRHelper::toTypeString(Category::Type::MALE)); + comboBox->addItem(QIcon(":/material/icons/female.svg"), CRHelper::toCategoryTypeString(Category::Type::FEMALE), CRHelper::toTypeString(Category::Type::FEMALE)); + comboBox->addItem(QIcon(":/material/icons/transgender.svg"), CRHelper::toCategoryTypeString(Category::Type::RELAY_MF), CRHelper::toTypeString(Category::Type::RELAY_MF)); + comboBox->addItem(QIcon(":/material/icons/male.svg"), CRHelper::toCategoryTypeString(Category::Type::RELAY_Y), CRHelper::toTypeString(Category::Type::RELAY_Y)); + comboBox->addItem(QIcon(":/material/icons/female.svg"), CRHelper::toCategoryTypeString(Category::Type::RELAY_X), CRHelper::toTypeString(Category::Type::RELAY_X)); } QWidget *CategoryTypeDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const @@ -35,7 +38,7 @@ QWidget *CategoryTypeDelegate::createEditor(QWidget *parent, QStyleOptionViewIte Q_UNUSED(option) Q_UNUSED(index) - auto *comboBox = box.data(); + auto *comboBox = categoryTypeBox.data(); comboBox->setParent(parent); return comboBox; @@ -51,7 +54,7 @@ void CategoryTypeDelegate::setEditorData(QWidget *editor, QModelIndex const &ind { // Get the value via index of the Model and put it into the ComboBox auto *comboBox = static_cast(editor); - comboBox->setCurrentText(toCatTypeString(Category::toType(index.model()->data(index, Qt::EditRole).toString()))); + comboBox->setCurrentText(CRHelper::toCategoryTypeString(CRHelper::toCategoryType(index.model()->data(index, Qt::EditRole).toString()))); } void CategoryTypeDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const @@ -60,9 +63,12 @@ void CategoryTypeDelegate::setModelData(QWidget *editor, QAbstractItemModel *mod model->setData(index, comboBox->currentData(Qt::UserRole), Qt::EditRole); } -QSize CategoryTypeDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +QSize CategoryTypeDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const { - return this->box.data()->sizeHint(); + Q_UNUSED(option) + Q_UNUSED(index) + + return this->categoryTypeBox.data()->sizeHint(); } void CategoryTypeDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const @@ -71,15 +77,3 @@ void CategoryTypeDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionVie editor->setGeometry(option.rect); } - -QString CategoryTypeDelegate::toCatTypeString(Category::Type type) -{ - switch (type) { - case Category::Type::INDIVIDUAL: - return tr("Individual/Relay"); - case Category::Type::CLUB: - return tr("Club"); - default: - throw(ChronoRaceException(tr("Unexpected Type enum value '%1'").arg(static_cast(type)))); - } -} diff --git a/cattypedelegate.hpp b/cattypedelegate.hpp index 31522ce..6bf894f 100644 --- a/cattypedelegate.hpp +++ b/cattypedelegate.hpp @@ -35,13 +35,11 @@ class CategoryTypeDelegate : public QStyledItemDelegate void destroyEditor(QWidget *editor, const QModelIndex &index) const override; void setEditorData(QWidget *editor, QModelIndex const &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const override; void updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const override; private: - QScopedPointer box { new QComboBox }; - - static QString toCatTypeString(Category::Type type); + QScopedPointer categoryTypeBox { new QComboBox }; }; #endif // CATTYPEDELEGATE_HPP diff --git a/chronorace.ui b/chronorace.ui index 691fcc9..34088c2 100644 --- a/chronorace.ui +++ b/chronorace.ui @@ -43,60 +43,53 @@ - - - - Edit race information, including logos + + + + Competitors + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + - Edit Race Information + Rankings - - - :/material/icons/edit_document.svg:/material/icons/edit_document.svg + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - + + true - See the list of the clubs (that cannot be changed) + Add, remove, and change the definition of the categories - View Clubs List + Edit Categories - :/material/icons/format_list_bulleted.svg:/material/icons/format_list_bulleted.svg - - - - - - - - - - Clubs - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg - + - + - QFrame::Box + QFrame::Shape::Box - QFrame::Raised + QFrame::Shadow::Raised 1 @@ -106,37 +99,51 @@ - - + + + + true + + + Add, remove, and change competitors + - Competitors + Edit Start List - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + :/material/icons/format_list_numbered.svg:/material/icons/format_list_numbered.svg - + Timings - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - + + + + Save current race data to file + - Categories + Save Race - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + :/material/icons/save.svg:/material/icons/save.svg - + + + + true @@ -153,26 +160,47 @@ - - + + + + Categories + + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter + + + + + + + Edit race information, including logos + + + Edit Race Information + + + + :/material/icons/edit_document.svg:/material/icons/edit_document.svg + + + + + true - Add, remove, and change the definition of the categories + See the list of the clubs (that cannot be changed) - Edit Categories + View Clubs List - :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg + :/material/icons/format_list_bulleted.svg:/material/icons/format_list_bulleted.svg - - - @@ -187,34 +215,33 @@ - - - - true - - - Add, remove, and change competitors - + + - Edit Start List + Clubs - - - :/material/icons/format_list_numbered.svg:/material/icons/format_list_numbered.svg + + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTrailing|Qt::AlignmentFlag::AlignVCenter - - + + + + + + + + - Save current race data to file + Add, remove, and change the definition of the rankings - Save Race + Edit Rankings - :/material/icons/save.svg:/material/icons/save.svg + :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg @@ -389,7 +416,7 @@ 0 0 1024 - 19 + 20 @@ -409,9 +436,10 @@ &Edit + + - @@ -644,15 +672,28 @@ Set encoding for CSV and Plain Text + + + + :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg + + + Rankings + + + Add, remove, and change the definition of the rankings + + loadRace saveRace editRace + editCategories + editRankings editStartList editClubsList - editCategories editTimings diff --git a/chronoracetable.cpp b/chronoracetable.cpp index e5ca083..a5b4489 100644 --- a/chronoracetable.cpp +++ b/chronoracetable.cpp @@ -19,6 +19,7 @@ #include "chronoracetable.hpp" #include "crloader.hpp" +#include "crhelper.hpp" ChronoRaceTable::ChronoRaceTable(QWidget *parent) : QDialog(parent) { @@ -99,7 +100,7 @@ void ChronoRaceTable::modelImport() { if (QMessageBox::question(this, tr("CSV Encoding"), tr("The data being imported must be %1 encoded.\n" - "Continue?").arg(CRLoader::encodingToLabel(CRLoader::getEncoding())), + "Continue?").arg(CRHelper::encodingToLabel(CRLoader::getEncoding())), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { emit modelImported(); } @@ -109,7 +110,7 @@ void ChronoRaceTable::modelExport() { if (QMessageBox::question(this, tr("CSV Encoding"), tr("The data will be exported with %1 encoding.\n" - "Continue?").arg(CRLoader::encodingToLabel(CRLoader::getEncoding())), + "Continue?").arg(CRHelper::encodingToLabel(CRLoader::getEncoding())), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::Yes) { emit modelExported(); } diff --git a/classentry.cpp b/classentry.cpp index 5bc723d..ed442c3 100644 --- a/classentry.cpp +++ b/classentry.cpp @@ -18,11 +18,11 @@ #include "classentry.hpp" #include "crloader.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" // Static members QString ClassEntry::empty("*** ??? ***"); - QString ClassEntryElement::formatNameCSV(bool first, QString const &name, QString const &sex, QString const &year) const { return QString("%1%2,%3,%4").arg(first ? "" : ",", name, sex, year); @@ -39,12 +39,26 @@ void ClassEntryElement::addNames(bool csvFormat, bool first, QString &entryStrin if (c) entryString += csvFormat ? - formatNameCSV(first, c->getName(), Competitor::toSexString(c->getSex()), QString::number(c->getYear())) : - formatNameTxt(first, c->getName(static_cast(emptyName.size())), Competitor::toSexString(c->getSex()), QString::number(c->getYear())); + formatNameCSV(first, c->getName(), CRHelper::toSexString(c->getSex()), QString::number(c->getYear())) : + formatNameTxt(first, c->getName(static_cast(emptyName.size())), CRHelper::toSexString(c->getSex()), QString::number(c->getYear())); else entryString += csvFormat ? - formatNameCSV(first, emptyName, Competitor::toSexString(Competitor::Sex::UNDEFINED), QString("0")) : - formatNameTxt(first, emptyName, Competitor::toSexString(Competitor::Sex::UNDEFINED), QString(" 0")); + formatNameCSV(first, emptyName, CRHelper::toSexString(Competitor::Sex::UNDEFINED), QString("0")) : + formatNameTxt(first, emptyName, CRHelper::toSexString(Competitor::Sex::UNDEFINED), QString(" 0")); +} + +bool ClassEntryElement::hasCategory(const Category *cat) const +{ + bool found = false; + + if (this->competitor) { + QListIterator j { this->competitor->getCategories() }; + while (j.hasNext() && !found) { + found = (cat == j.next()); + } + } + + return found; } uint ClassEntry::getBib() const @@ -112,7 +126,7 @@ QString ClassEntry::getNamesCommon(bool csvFormat) const } if (entries.size() > 1) { - retString += QString(csvFormat ? ",%1" : " (%1)").arg(Competitor::toSexString(getSex())); + retString += QString(csvFormat ? ",%1" : " (%1)").arg(CRHelper::toSexString(getSex())); } return retString; @@ -122,8 +136,7 @@ QString ClassEntry::getNames(CRLoader::Format format) const { QString retString; - switch (format) - { + switch (format) { case CRLoader::Format::TEXT: retString = getNamesCommon(false); break; @@ -131,10 +144,11 @@ QString ClassEntry::getNames(CRLoader::Format format) const retString = getNamesCommon(true); break; case CRLoader::Format::PDF: - [[fallthrough]]; - default: retString = "***Error***"; break; + default: + Q_UNREACHABLE(); + break; } return retString; @@ -150,16 +164,30 @@ uint ClassEntry::getYear(uint legIdx) const Competitor::Sex ClassEntry::getSex() const { + uint males = 0; + uint females = 0; Competitor::Sex sex = Competitor::Sex::UNDEFINED; for (auto const &e : entries) { - if (sex == Competitor::Sex::UNDEFINED) - sex = e.competitor->getSex(); - else if (sex != e.competitor->getSex()) - sex = Competitor::Sex::MISC; + switch (e.competitor->getSex()) { + case Competitor::Sex::MALE: + males++; + break; + case Competitor::Sex::FEMALE: + females++; + break; + default: + throw(ChronoRaceException(tr("Unexpected sex value for bib %1 (%2)").arg(bib).arg(e.competitor->getName()))); + break; + } } - return sex; + if (males && !females) + sex = Competitor::Sex::MALE; + else if (!males && females) + sex = Competitor::Sex::FEMALE; + + return sex; } Competitor::Sex ClassEntry::getSex(uint legIdx) const @@ -174,21 +202,21 @@ QString ClassEntry::getTimes(CRLoader::Format format, int legRankWidth) const { QString retString; - switch (format) - { + switch (format) { case CRLoader::Format::TEXT: for (QVector::ConstIterator it = entries.constBegin(); it < entries.constEnd(); it++) - retString.append(QString("%1(%2) %3").arg((it == entries.constBegin()) ? "" : " - ").arg(it->legRanking, legRankWidth).arg(Timing::toTimeStr(it->time, it->status), 7)); + retString.append(QString("%1(%2) %3").arg((it == entries.constBegin()) ? "" : " - ").arg(it->legRanking, legRankWidth).arg(CRHelper::toTimeStr(it->time, it->status), 7)); break; case CRLoader::Format::CSV: for (QVector::ConstIterator it = entries.constBegin(); it < entries.constEnd(); it++) - retString.append(QString("%1%2,%3").arg((it == entries.constBegin()) ? "" : ",").arg(it->legRanking).arg(Timing::toTimeStr(it->time, Timing::Status::CLASSIFIED))); + retString.append(QString("%1%2,%3").arg((it == entries.constBegin()) ? "" : ",").arg(it->legRanking).arg(CRHelper::toTimeStr(it->time, Timing::Status::CLASSIFIED))); break; case CRLoader::Format::PDF: - [[fallthrough]]; - default: retString = "***Error***"; break; + default: + Q_UNREACHABLE(); + break; } return retString; @@ -199,7 +227,7 @@ QString ClassEntry::getTime(uint legIdx) const if (static_cast(legIdx) >= entries.size()) throw(ChronoRaceException(tr("Nonexistent leg %1 for bib %2").arg(legIdx + 1).arg(bib))); - return Timing::toTimeStr(entries[legIdx].time, entries[legIdx].status); + return CRHelper::toTimeStr(entries[legIdx].time, entries[legIdx].status); } uint ClassEntry::getTimeValue(uint legIdx) const @@ -215,7 +243,7 @@ uint ClassEntry::countEntries() const return static_cast(entries.size()); } -void ClassEntry::setTime(Competitor const *comp, Timing const &timing, QStringList &messages) +void ClassEntry::setTime(Competitor *comp, Timing const &timing, QStringList &messages) { Q_ASSERT(comp); @@ -297,24 +325,45 @@ uint ClassEntry::getToYear() const return toYear; } -QString const &ClassEntry::getClub() const +QString ClassEntry::getClub() const { + QStringList clubs { }; + for (auto const &it : entries) { if (it.competitor) - return it.competitor->getClub(); + clubs.append(it.competitor->getClub()); + } + + if (!clubs.isEmpty()) { + clubs.removeDuplicates(); } - return ClassEntry::empty; + return clubs.join(" - "); } -QString const &ClassEntry::getTeam() const +QString ClassEntry::getTeam() const { + QStringList teams { }; + for (auto const &it : entries) { if (it.competitor) - return it.competitor->getTeam(); + teams.append(it.competitor->getTeam()); } - return ClassEntry::empty; + if (!teams.isEmpty()) { + teams.removeDuplicates(); + } + + return teams.join(" - "); +} + +QString ClassEntry::getClubsAndTeam() const +{ + QStringList clubsAndTeam { this->getClub(), this->getTeam() }; + + clubsAndTeam.removeAll(""); + + return clubsAndTeam.join(" - "); } bool ClassEntry::isDnf() const @@ -331,22 +380,36 @@ bool ClassEntry::isDns() const }); } -QString const &ClassEntry::getCategory() const +Category const *ClassEntry::getCategory() const { return category; } -QString const &ClassEntry::getCategory(uint legIdx) const +Category const *ClassEntry::getCategory(uint legIdx) const { if (static_cast(legIdx) >= entries.size()) throw(ChronoRaceException(tr("Nonexistent leg %1 for bib %2").arg(legIdx + 1).arg(bib))); - return (entries[legIdx].competitor) ? entries[legIdx].competitor->getCategory() : ClassEntry::empty; + return (entries[legIdx].competitor) ? entries[legIdx].competitor->getCategory() : Q_NULLPTR; } -void ClassEntry::setCategory(QString const &value) +QStringList ClassEntry::setCategory() { - category = value; + QStringList messages; + + switch (entries.size()) { + case 0: + messages += tr("No competitors associated to bib %1").arg(bib); + break; + case 1: + ClassEntryHelper::setCategorySingleLeg(this, messages); + break; + default: + ClassEntryHelper::setCategoryMultiLeg(this, messages); + break; + } + + return messages; } uint ClassEntry::getTotalTime() const @@ -358,20 +421,21 @@ QString ClassEntry::getTotalTime(CRLoader::Format format) const { QString retString; - switch (format) - { + switch (format) { case CRLoader::Format::TEXT: + [[fallthrough]]; case CRLoader::Format::CSV: + [[fallthrough]]; case CRLoader::Format::PDF: if (isDns()) - retString = Timing::toTimeStr(totalTime, Timing::Status::DNS); + retString = CRHelper::toTimeStr(totalTime, Timing::Status::DNS); else if (isDnf()) - retString = Timing::toTimeStr(totalTime, Timing::Status::DNF); + retString = CRHelper::toTimeStr(totalTime, Timing::Status::DNF); else - retString = Timing::toTimeStr(totalTime, Timing::Status::CLASSIFIED); + retString = CRHelper::toTimeStr(totalTime, Timing::Status::CLASSIFIED); break; default: - retString = "***Error***"; + Q_UNREACHABLE(); break; } @@ -384,12 +448,198 @@ QString ClassEntry::getDiffTimeTxt(uint referenceTime) const return QString(""); if (totalTime > referenceTime) - return Timing::toTimeStr(totalTime - referenceTime, Timing::Status::CLASSIFIED, "+"); + return CRHelper::toTimeStr(totalTime - referenceTime, Timing::Status::CLASSIFIED, "+"); else - return Timing::toTimeStr(referenceTime - totalTime, Timing::Status::CLASSIFIED, "-"); + return CRHelper::toTimeStr(referenceTime - totalTime, Timing::Status::CLASSIFIED, "-"); } bool ClassEntry::operator< (ClassEntry const &rhs) const { return totalTime < rhs.totalTime; } bool ClassEntry::operator> (ClassEntry const &rhs) const { return totalTime > rhs.totalTime; } bool ClassEntry::operator<=(ClassEntry const &rhs) const { return totalTime <= rhs.totalTime; } bool ClassEntry::operator>=(ClassEntry const &rhs) const { return totalTime >= rhs.totalTime; } + +bool ClassEntryHelper::allCompetitorsShareTheSameClub(QVector const &entries, qsizetype fromLeg, qsizetype toLeg, QString const &club) +{ + Competitor const *competitor; + for ( ; fromLeg < toLeg; fromLeg++) { + if (!(competitor = entries[fromLeg].competitor)) + continue; + + if (competitor->getClub() != club) { + return false; + } + } + + return true; +} + +bool ClassEntryHelper::allCompetitorsAreOfTheSameSex(QVector const &entries, qsizetype fromLeg, qsizetype toLeg, Competitor::Sex sex) +{ + Competitor const *competitor; + for ( ; fromLeg < toLeg; fromLeg++) { + if (!(competitor = entries[fromLeg].competitor)) + continue; + + if (competitor->getSex() != sex) { + return false; + } + } + + return true; +} + +void ClassEntryHelper::removeImpossibleCategories(QVector &entries) +{ + Competitor *comp = entries[0].competitor; + Category const *cat; + Competitor::Sex sex = comp->getSex(); + + qsizetype count = entries.count(); + QString const &club = comp->getClub(); + + QMutableListIterator i { comp->getCategories() }; + while (i.hasNext()) { + cat = i.next(); + switch (cat->getType()) { + case Category::Type::MALE: + [[fallthrough]]; + case Category::Type::FEMALE: + /* Competitors must all belong to the same Club */ + if (!allCompetitorsShareTheSameClub(entries, 1, count, club)) + i.remove(); + break; + case Category::Type::RELAY_X: + [[fallthrough]]; + case Category::Type::RELAY_Y: + /* Competitors must belong to different Clubs */ + if (allCompetitorsShareTheSameClub(entries, 1, count, club)) + i.remove(); + break; + case Category::Type::RELAY_MF: + /* Competitors must have different sex */ + if (allCompetitorsAreOfTheSameSex(entries, 1, count, sex)) + i.remove(); + break; + default: + Q_UNREACHABLE(); + break; + } + } +} + +void ClassEntryHelper::removeLowerWeigthCategories(QList &categories, QStringList &messages, QString const &name, uint bib) +{ + Category::Type type; + Category const *cat1 = Q_NULLPTR; + Category const *cat2 = Q_NULLPTR; + + uint w1; + uint w2; + + QList fullList; + categories.swap(fullList); + + QMutableListIterator i { fullList }; + QMutableListIterator j { categories }; + + while (i.hasNext()) { + cat1 = i.next(); + type = cat1->getType(); + w1 = cat1->getWeight(); + i.remove(); + + j = categories; + while (j.hasNext()) { + cat2 = j.next(); + + if (cat2->getType() != type) { + continue; + } + + w2 = cat2->getWeight(); + if (w1 < w2) { + messages += tr("Removing candidate category '%1' associated to competitor %2 - bib %3").arg(cat2->getFullDescription(), name, QString::number(bib)); + cat1 = Q_NULLPTR; + break; + } + + if (w1 > w2) { + messages += tr("Removing candidate category '%1' associated to competitor %2 - bib %3").arg(j.peekPrevious()->getFullDescription(), name, QString::number(bib)); + j.remove(); + } + } + + if (cat1) + j.insert(cat1); + } +} + +void ClassEntryHelper::setCategorySingleLeg(ClassEntry *entry, QStringList &messages) +{ + Competitor *comp = entry->entries[0].competitor; + QString const &name = comp->getName(); + + if (QList &categories = comp->getCategories(); categories.isEmpty()) { + messages += tr("No categories associated to competitor %1 - bib %2").arg(name).arg(entry->bib); + } else { + /* Slim down the list removing impossible category types */ + removeImpossibleCategories(entry->entries); + + /* Slim down the list removing categories having same type but lower weight */ + removeLowerWeigthCategories(categories, messages, name, entry->bib); + + qsizetype i = categories.count(); + while (i-- > 1) { + qDebug() << tr("Dropping category '%1' associated to competitor %2 - bib %3").arg(categories[i]->getFullDescription(), name, QString::number(entry->bib)); + categories.removeAt(i); + } + + entry->category = comp->getCategory(); + } +} + +void ClassEntryHelper::setCategoryMultiLeg(ClassEntry *entry, QStringList &messages) +{ + QVector const *entries = &entry->entries; + qsizetype count = entries->count(); + Competitor *comp = entries->at(0).competitor; + qsizetype leg; + + /* Slim down the list removing impossible category types */ + removeImpossibleCategories(entry->entries); + + /* Slim down the list of the first leg scanning the lists of all other legs */ + Category const *cat; + QMutableListIterator i { comp->getCategories() }; + while (i.hasNext()) { + cat = i.next(); + + /* Category must be present in all the legs */ + for (leg = 1; leg < count; leg++) { + if (!entries->at(leg).hasCategory(cat)) { + qDebug() << tr("Dropping category '%1' associated to competitor %2 - bib %3 - leg 1").arg(i.peekPrevious()->getFullDescription(), comp->getName(), QString::number(entry->bib)); + i.remove(); + break; + } + } + } + + /* Slim down the list removing categories having same type but lower weight */ + removeLowerWeigthCategories(comp->getCategories(), messages, comp->getName(), entry->bib); + + /* Slim down the list of all other legs using the first one as reference */ + for (leg = 1; leg < count; leg++) { + if (!(comp = entries->at(leg).competitor)) + continue; + + i = comp->getCategories(); + while (i.hasNext()) { + if (!entry->entries[0].hasCategory(i.next())) { + qDebug() << tr("Dropping category '%1' associated to competitor %2 - bib %3 - leg 1").arg(i.peekPrevious()->getFullDescription(), comp->getName(), QString::number(entry->bib)); + i.remove(); + } + } + } + + entry->category = entries->at(0).competitor->getCategory(); +} diff --git a/classentry.hpp b/classentry.hpp index eae0c65..5f19e6c 100644 --- a/classentry.hpp +++ b/classentry.hpp @@ -22,36 +22,44 @@ #include #include "crloader.hpp" +#include "category.hpp" #include "competitor.hpp" #include "timing.hpp" namespace placement { -class ClassEntryElement; -class ClassEntry; + class ClassEntryElement; + class ClassEntry; + class ClassEntryHelper; } class ClassEntryElement { + Q_DECLARE_TR_FUNCTIONS(ClassEntry); + public: - Competitor const *competitor { Q_NULLPTR }; - uint time { 0u }; - Timing::Status status { Timing::Status::CLASSIFIED }; - uint legRanking { 0u }; + Competitor *competitor { Q_NULLPTR }; + uint time { 0u }; + Timing::Status status { Timing::Status::CLASSIFIED }; + uint legRanking { 0u }; QString formatNameCSV(bool first, QString const &name, QString const &sex, QString const &year) const; QString formatNameTxt(bool first, QString const &name, QString const &sex, QString const &year) const; void addNames(bool csvFormat, bool first, QString &entryString, QString const &emptyName) const; + + bool hasCategory(Category const *cat) const; }; class ClassEntry { Q_DECLARE_TR_FUNCTIONS(ClassEntry) + friend class ClassEntryHelper; + private: static QString empty; uint bib { 0u }; QVector entries { }; uint totalTime { 0u }; - QString category { "" }; + Category const *category { Q_NULLPTR }; QString getNamesCommon(bool csvFormat) const; @@ -69,24 +77,23 @@ class ClassEntry { QString getTime(uint legIdx) const; uint getTimeValue(uint legIdx) const; uint countEntries() const; - void setTime(Competitor const *comp, Timing const &timing, QStringList &messages); + void setTime(Competitor *comp, Timing const &timing, QStringList &messages); uint getLegRanking(uint legIdx) const; void setLegRanking(uint legIdx, uint ranking); uint getFromYear() const; uint getToYear() const; - QString const &getClub() const; - QString const &getTeam() const; + QString getClub() const; + QString getTeam() const; + QString getClubsAndTeam() const; uint getTotalTime() const; QString getTotalTime(CRLoader::Format format) const; QString getDiffTimeTxt(uint referenceTime) const; bool isDnf() const; bool isDns() const; - QString const &getCategory() const; - QString const &getCategory(uint legIdx) const; - void setCategory(QString const &value); - - static bool compare(ClassEntry const &a, ClassEntry const &b); + Category const *getCategory() const; + Category const *getCategory(uint legIdx) const; + QStringList setCategory(); bool operator< (ClassEntry const &rhs) const; bool operator> (ClassEntry const &rhs) const; @@ -94,5 +101,18 @@ class ClassEntry { bool operator>=(ClassEntry const &rhs) const; }; +class ClassEntryHelper { + Q_DECLARE_TR_FUNCTIONS(ClassEntry) + +private: + static bool allCompetitorsShareTheSameClub(QVector const &entries, qsizetype fromLeg, qsizetype toLeg, QString const &club); + static bool allCompetitorsAreOfTheSameSex(QVector const &entries, qsizetype fromLeg, qsizetype toLeg, Competitor::Sex sex); + static void removeImpossibleCategories(QVector &entries); + static void removeLowerWeigthCategories(QList &categories, QStringList &messages, QString const &name, uint bib); + +public: + static void setCategorySingleLeg(ClassEntry *entry, QStringList &messages); + static void setCategoryMultiLeg(ClassEntry *entry, QStringList &messages); +}; #endif // CLASSENTRY_H diff --git a/clubdelegate.cpp b/clubdelegate.cpp index 2a92618..b9269ad 100644 --- a/clubdelegate.cpp +++ b/clubdelegate.cpp @@ -21,7 +21,7 @@ ClubDelegate::ClubDelegate(QObject *parent) : QStyledItemDelegate(parent) { - auto *comboBox = box.data(); + auto *comboBox = clubBox.data(); comboBox->setEditable(true); comboBox->setInsertPolicy(QComboBox::InsertAlphabetically); comboBox->setDuplicatesEnabled(false); @@ -34,8 +34,10 @@ QWidget *ClubDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const Q_UNUSED(option) Q_UNUSED(index) - auto *comboBox = box.data(); + auto *comboBox = clubBox.data(); comboBox->setParent(parent); + comboBox->clear(); + comboBox->addItems(CRLoader::getClubs()); return comboBox; } @@ -59,9 +61,12 @@ void ClubDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QMod model->setData(index, comboBox->currentData(Qt::EditRole), Qt::EditRole); } -QSize ClubDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +QSize ClubDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const { - return this->box.data()->sizeHint(); + Q_UNUSED(option) + Q_UNUSED(index) + + return this->clubBox.data()->sizeHint(); } void ClubDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const diff --git a/clubdelegate.hpp b/clubdelegate.hpp index cf55f7c..532441c 100644 --- a/clubdelegate.hpp +++ b/clubdelegate.hpp @@ -33,11 +33,11 @@ class ClubDelegate : public QStyledItemDelegate void destroyEditor(QWidget *editor, const QModelIndex &index) const override; void setEditorData(QWidget *editor, QModelIndex const &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const override; void updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const override; private: - QScopedPointer box { new QComboBox }; + QScopedPointer clubBox { new QComboBox }; }; #endif // CLUBDELEGATE_HPP diff --git a/competitor.cpp b/competitor.cpp index 9dafb48..83c849d 100644 --- a/competitor.cpp +++ b/competitor.cpp @@ -17,8 +17,10 @@ #include "lbchronorace.hpp" #include "competitor.hpp" -#include "lbcrexception.hpp" +#include "crhelper.hpp" +// Static members +QString Competitor::empty(""); Competitor::Field CompetitorSorter::sortingField = Competitor::Field::CMF_FIRST; Qt::SortOrder CompetitorSorter::sortingOrder = Qt::AscendingOrder; @@ -26,7 +28,7 @@ QDataStream &operator<<(QDataStream &out, const Competitor &comp) { out << quint32(comp.bib) << comp.name - << Competitor::toSexString(comp.sex) + << CRHelper::toSexString(comp.sex) << quint32(comp.year) << comp.club << comp.team @@ -55,7 +57,7 @@ QDataStream &operator>>(QDataStream &in, Competitor &comp) >> offset32; comp.bib = bib32; - comp.sex = Competitor::toSex(sexStr); + comp.sex = CRHelper::toSex(sexStr); comp.year = year32; comp.leg = leg32; comp.offset = offset32; @@ -181,94 +183,57 @@ bool Competitor::isValid() const return ((bib != 0u) && !name.isEmpty() && (sex != Sex::UNDEFINED)); } -QString const &Competitor::getCategory() const +Category const *Competitor::getCategory() const { - return this->category; + return this->categories.isEmpty() ? Q_NULLPTR : this->categories[0]; } -void Competitor::setCategory(QString const &newCategory) +QList &Competitor::getCategories() { - this->category = newCategory; + return this->categories; } -Competitor::Sex Competitor::toSex(QString const &sex, bool const strict) +void Competitor::setCategories(QList const &newCategories) { - if (sex.length() != 1) { - throw(ChronoRaceException(tr("Illegal sex '%1'").arg(sex))); - } else { - if (sex.compare("M", Qt::CaseInsensitive) == 0) - return Sex::MALE; - else if (sex.compare("F", Qt::CaseInsensitive) == 0) - return Sex::FEMALE; - else if (!strict && (sex.compare("X", Qt::CaseInsensitive) == 0)) - return Sex::MISC; - } + this->categories.clear(); - return Sex::UNDEFINED; + for (auto const &category : newCategories) { + if (this->isInCategory(&category)) + this->categories.append(&category); + } } -QString Competitor::toSexString(Sex const sex) +bool Competitor::isInCategory(Category const *category) const { - switch (sex) { - case Sex::MALE: - return "M"; - case Sex::FEMALE: - return "F"; - case Sex::MISC: - return "X"; - case Sex::UNDEFINED: - return "U"; - default: - throw(ChronoRaceException(tr("Unexpected Sex enum value '%1'").arg(static_cast(sex)))); - } -} + if (!category) + return false; -int Competitor::toOffset(QString const &offset) -{ - QStringList list = offset.split(":"); - - int retval = -1; - int h; - int m; - int s; - int l = 0; - bool check_h; - bool check_m; - bool check_s; - bool check_l; - switch (list.count()) { - case 3: - h = list[0].toInt(&check_h, 10); - m = list[1].toInt(&check_m, 10); - s = list[2].toInt(&check_s, 10); - if (check_h && check_m && check_s) - retval = (h * 3600) + (m * 60) + s; - break; - case 2: - m = list[0].toInt(&check_m, 10); - s = list[1].toInt(&check_s, 10); - if (check_m && check_s) - retval = (m * 60) + s; - break; - case 1: - l = list[0].toInt(&check_l, 10); - if (check_l) - retval = -qAbs(l); + uint fromYear = category->getFromYear(); + uint toYear = category->getToYear(); + + if ((fromYear != 0) && (this->year < fromYear)) + return false; + + if ((toYear != 0) && (toYear < this->year)) + return false; + + Category::Type type = category->getType(); + switch (this->sex) { + case Competitor::Sex::MALE: + if ((type == Category::Type::FEMALE) || (type == Category::Type::RELAY_X)) + return false; break; - default: - // do nothing + case Competitor::Sex::FEMALE: + if ((type == Category::Type::MALE) || (type == Category::Type::RELAY_Y)) + return false; break; + case Competitor::Sex::UNDEFINED: + return false; + default: + Q_UNREACHABLE(); } - return retval; -} - -QString Competitor::toOffsetString(int offset) -{ - if (offset < 0) - return QString("%1").arg(qAbs(offset)); - else - return QString("%1:%2:%3").arg(((offset / 60) / 60)).arg(((offset / 60) % 60), 2, 10, QChar('0')).arg((offset % 60), 2, 10, QChar('0')); + return true; } bool Competitor::operator< (Competitor const &rhs) const @@ -323,7 +288,7 @@ bool CompetitorSorter::operator() (Competitor const &lhs, Competitor const &rhs) case Competitor::Field::CMF_NAME: return (sortingOrder == Qt::DescendingOrder) ? (lhs.getName() > rhs.getName()) : (lhs.getName() < rhs.getName()); case Competitor::Field::CMF_SEX: - return (sortingOrder == Qt::DescendingOrder) ? (Competitor::toSexString(lhs.getSex()) > Competitor::toSexString(rhs.getSex())) : (Competitor::toSexString(lhs.getSex()) < Competitor::toSexString(rhs.getSex())); + return (sortingOrder == Qt::DescendingOrder) ? (CRHelper::toSexString(lhs.getSex()) > CRHelper::toSexString(rhs.getSex())) : (CRHelper::toSexString(lhs.getSex()) < CRHelper::toSexString(rhs.getSex())); case Competitor::Field::CMF_YEAR: return (sortingOrder == Qt::DescendingOrder) ? (lhs.getYear() > rhs.getYear()) : (lhs.getYear() < rhs.getYear()); case Competitor::Field::CMF_CLUB: diff --git a/competitor.hpp b/competitor.hpp index 58e1b9f..57e8a73 100644 --- a/competitor.hpp +++ b/competitor.hpp @@ -21,6 +21,9 @@ #include #include #include +#include + +#include "category.hpp" namespace competitor { class Competitor; @@ -36,8 +39,7 @@ class Competitor { UNDEFINED, MALE, - FEMALE, - MISC + FEMALE }; enum class Field @@ -55,6 +57,7 @@ class Competitor }; private: + static QString empty; uint bib { 0u }; QString name { "" }; @@ -64,7 +67,9 @@ class Competitor QString team { "" }; uint leg { 1u }; int offset { -1 }; - QString category { "" }; + QList categories { }; + + bool isInCategory(Category const *category) const; public: Competitor() = default; @@ -97,13 +102,9 @@ class Competitor void setOffset(int const *newOffset); bool isValid() const; - QString const &getCategory() const; - void setCategory(QString const &newCategory); - - static Sex toSex(QString const &sex, bool const strict = false); - static QString toSexString(Sex const sex); - static int toOffset(QString const &offset); - static QString toOffsetString(int offset); + Category const *getCategory() const; + QList &getCategories(); + void setCategories(QList const &newCategories); bool operator< (Competitor const &rhs) const; bool operator> (Competitor const &rhs) const; diff --git a/sexdelegate.cpp b/compsexdelegate.cpp similarity index 53% rename from sexdelegate.cpp rename to compsexdelegate.cpp index bf5388f..4b6389a 100644 --- a/sexdelegate.cpp +++ b/compsexdelegate.cpp @@ -15,75 +15,63 @@ * along with this program. If not, see . * *****************************************************************************/ -#include "sexdelegate.hpp" -#include "lbcrexception.hpp" +#include "compsexdelegate.hpp" +#include "crhelper.hpp" -SexDelegate::SexDelegate(QObject *parent) : +CompetitorSexDelegate::CompetitorSexDelegate(QObject *parent) : QStyledItemDelegate(parent) { - auto *comboBox = box.data(); + auto *comboBox = competitorSexBox.data(); comboBox->setEditable(false); comboBox->setInsertPolicy(QComboBox::NoInsert); - comboBox->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); comboBox->setDuplicatesEnabled(false); comboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); - comboBox->addItem(QIcon(":/material/icons/unknown_med.svg"), toSexString(Competitor::Sex::UNDEFINED), Competitor::toSexString(Competitor::Sex::UNDEFINED)); - comboBox->addItem(QIcon(":/material/icons/male.svg"), toSexString(Competitor::Sex::MALE), Competitor::toSexString(Competitor::Sex::MALE)); - comboBox->addItem(QIcon(":/material/icons/female.svg"), toSexString(Competitor::Sex::FEMALE), Competitor::toSexString(Competitor::Sex::FEMALE)); + comboBox->addItem(QIcon(":/material/icons/unknown_med.svg"), CRHelper::toSexFullString(Competitor::Sex::UNDEFINED), CRHelper::toSexString(Competitor::Sex::UNDEFINED)); + comboBox->addItem(QIcon(":/material/icons/male.svg"), CRHelper::toSexFullString(Competitor::Sex::MALE), CRHelper::toSexString(Competitor::Sex::MALE)); + comboBox->addItem(QIcon(":/material/icons/female.svg"), CRHelper::toSexFullString(Competitor::Sex::FEMALE), CRHelper::toSexString(Competitor::Sex::FEMALE)); } -QWidget *SexDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const +QWidget *CompetitorSexDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const { Q_UNUSED(option) Q_UNUSED(index) - auto *comboBox = box.data(); + auto *comboBox = competitorSexBox.data(); comboBox->setParent(parent); return comboBox; } -void SexDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const +void CompetitorSexDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const { Q_UNUSED(editor) Q_UNUSED(index) } -void SexDelegate::setEditorData(QWidget *editor, QModelIndex const &index) const +void CompetitorSexDelegate::setEditorData(QWidget *editor, QModelIndex const &index) const { // Get the value via index of the Model and put it into the ComboBox auto *comboBox = static_cast(editor); - comboBox->setCurrentText(toSexString(Competitor::toSex(index.model()->data(index, Qt::EditRole).toString()))); + comboBox->setCurrentText(CRHelper::toSexFullString(CRHelper::toSex(index.model()->data(index, Qt::EditRole).toString()))); } -void SexDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const +void CompetitorSexDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const { auto const *comboBox = static_cast(editor); model->setData(index, comboBox->currentData(Qt::UserRole), Qt::EditRole); } -QSize SexDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +QSize CompetitorSexDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const { - return this->box.data()->sizeHint(); + Q_UNUSED(option) + Q_UNUSED(index) + + return this->competitorSexBox.data()->sizeHint(); } -void SexDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const +void CompetitorSexDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const { Q_UNUSED(index) editor->setGeometry(option.rect); } - -QString SexDelegate::toSexString(Competitor::Sex const sex) -{ - switch (sex) { - case Competitor::Sex::MALE: - return tr("Male"); - case Competitor::Sex::FEMALE: - return tr("Female"); - case Competitor::Sex::UNDEFINED: - return tr("Not set"); - default: - throw(ChronoRaceException(tr("Unexpected Sex enum value '%1'").arg(static_cast(sex)))); - } -} diff --git a/sexdelegate.hpp b/compsexdelegate.hpp similarity index 82% rename from sexdelegate.hpp rename to compsexdelegate.hpp index 26e0aec..d333f7f 100644 --- a/sexdelegate.hpp +++ b/compsexdelegate.hpp @@ -15,33 +15,29 @@ * along with this program. If not, see . * *****************************************************************************/ -#ifndef SEXDELEGATE_HPP -#define SEXDELEGATE_HPP +#ifndef COMPSEXDELEGATE_HPP +#define COMPSEXDELEGATE_HPP #include #include #include #include -#include "competitor.hpp" - -class SexDelegate : public QStyledItemDelegate +class CompetitorSexDelegate : public QStyledItemDelegate { Q_OBJECT public: - explicit SexDelegate(QObject *parent = Q_NULLPTR); + explicit CompetitorSexDelegate(QObject *parent = Q_NULLPTR); QWidget *createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const override; void destroyEditor(QWidget *editor, const QModelIndex &index) const override; void setEditorData(QWidget *editor, QModelIndex const &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const override; void updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const override; private: - QScopedPointer box { new QComboBox }; - - static QString toSexString(Competitor::Sex const sex); + QScopedPointer competitorSexBox { new QComboBox }; }; -#endif // SEXDELEGATE_HPP +#endif // COMPSEXDELEGATE_HPP diff --git a/crhelper.cpp b/crhelper.cpp new file mode 100644 index 0000000..6a1e540 --- /dev/null +++ b/crhelper.cpp @@ -0,0 +1,246 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#include "crhelper.hpp" +#include "lbcrexception.hpp" + +QString CRHelper::encodingToLabel(CRLoader::Encoding const &value) +{ + switch (value) { + case CRLoader::Encoding::UTF8: + return tr("UTF-8"); + case CRLoader::Encoding::LATIN1: + return tr("ISO-8859-1 (Latin-1)"); + default: + return tr("Unknown encoding %1").arg(static_cast(value)); + } +} + +QString CRHelper::formatToLabel(CRLoader::Format const &value) +{ + switch (value) { + case CRLoader::Format::PDF: + return tr("PDF"); + case CRLoader::Format::TEXT: + return tr("Text"); + case CRLoader::Format::CSV: + return tr("CSV"); + default: + return tr("Unknown format %1").arg(static_cast(value)); + } +} + +Competitor::Sex CRHelper::toSex(QString const &sex) +{ + if (sex.length() != 1) { + throw(ChronoRaceException(tr("Illegal sex '%1'").arg(sex))); + } else { + if (sex.compare("M", Qt::CaseInsensitive) == 0) + return Competitor::Sex::MALE; + else if (sex.compare("F", Qt::CaseInsensitive) == 0) + return Competitor::Sex::FEMALE; + } + + return Competitor::Sex::UNDEFINED; +} + +QString CRHelper::toSexString(Competitor::Sex const sex) +{ + switch (sex) { + case Competitor::Sex::MALE: + return "M"; + case Competitor::Sex::FEMALE: + return "F"; + case Competitor::Sex::UNDEFINED: + return "U"; + default: + throw(ChronoRaceException(tr("Unexpected Sex enum value '%1'").arg(static_cast(sex)))); + } +} + +QString CRHelper::toSexFullString(Competitor::Sex const sex) +{ + switch (sex) { + case Competitor::Sex::MALE: + return tr("Male"); + case Competitor::Sex::FEMALE: + return tr("Female"); + case Competitor::Sex::UNDEFINED: + return tr("Not set"); + default: + throw(ChronoRaceException(tr("Unexpected Sex enum value '%1'").arg(static_cast(sex)))); + } +} + +int CRHelper::toOffset(QString const &offset) +{ + QStringList list = offset.split(":"); + + int retval = -1; + int h; + int m; + int s; + int l = 0; + bool check_h; + bool check_m; + bool check_s; + bool check_l; + switch (list.count()) { + case 3: + h = list[0].toInt(&check_h, 10); + m = list[1].toInt(&check_m, 10); + s = list[2].toInt(&check_s, 10); + if (check_h && check_m && check_s) + retval = (h * 3600) + (m * 60) + s; + break; + case 2: + m = list[0].toInt(&check_m, 10); + s = list[1].toInt(&check_s, 10); + if (check_m && check_s) + retval = (m * 60) + s; + break; + case 1: + l = list[0].toInt(&check_l, 10); + if (check_l) + retval = -qAbs(l); + break; + default: + // do nothing + break; + } + + return retval; +} + +QString CRHelper::toOffsetString(int offset) +{ + if (offset < 0) + return QString("%1").arg(qAbs(offset)); + else + return QString("%1:%2:%3").arg(((offset / 60) / 60)).arg(((offset / 60) % 60), 2, 10, QChar('0')).arg((offset % 60), 2, 10, QChar('0')); +} + +Ranking::Type CRHelper::toRankingType(QString const &type) +{ + if (type.compare("I", Qt::CaseInsensitive) == 0) + return Ranking::Type::INDIVIDUAL; + else if (type.compare("T", Qt::CaseInsensitive) == 0) + return Ranking::Type::CLUB; + else + throw(ChronoRaceException(tr("Illegal type '%1'").arg(type))); +} + +QString CRHelper::toTypeString(Ranking::Type const type) +{ + switch (type) { + case Ranking::Type::INDIVIDUAL: + return "I"; + case Ranking::Type::CLUB: + return "T"; + default: + throw(ChronoRaceException(tr("Unexpected Type enum value '%1'").arg(static_cast(type)))); + } +} + +Category::Type CRHelper::toCategoryType(QString const &type) +{ + if (type.compare("M", Qt::CaseInsensitive) == 0) + return Category::Type::MALE; + else if (type.compare("F", Qt::CaseInsensitive) == 0) + return Category::Type::FEMALE; + else if (type.compare("U", Qt::CaseInsensitive) == 0) + return Category::Type::RELAY_MF; + else if (type.compare("Y", Qt::CaseInsensitive) == 0) + return Category::Type::RELAY_Y; + else if (type.compare("X", Qt::CaseInsensitive) == 0) + return Category::Type::RELAY_X; + else + throw(ChronoRaceException(tr("Illegal type '%1'").arg(type))); +} + +QString CRHelper::toTypeString(Category::Type const type) +{ + switch (type) { + case Category::Type::MALE: + return "M"; + case Category::Type::FEMALE: + return "F"; + case Category::Type::RELAY_MF: + return "U"; + case Category::Type::RELAY_Y: + return "Y"; + case Category::Type::RELAY_X: + return "X"; + default: + throw(ChronoRaceException(tr("Unexpected Type enum value '%1'").arg(static_cast(type)))); + } +} + +QString CRHelper::toRankingTypeString(Ranking::Type type) +{ + switch (type) { + case Ranking::Type::INDIVIDUAL: + return tr("Individual/Relay"); + case Ranking::Type::CLUB: + return tr("Club"); + default: + throw(ChronoRaceException(tr("Unexpected Type enum value '%1'").arg(static_cast(type)))); + } +} + +QString CRHelper::toCategoryTypeString(Category::Type const type) +{ + switch (type) { + case Category::Type::MALE: + return tr("Individual/Relay (M)"); + case Category::Type::FEMALE: + return tr("Individual/Relay (F)"); + case Category::Type::RELAY_MF: + return tr("Mixed Relay (M/F)"); + case Category::Type::RELAY_Y: + return tr("Mixed Clubs Relay (M)"); + case Category::Type::RELAY_X: + return tr("Mixed Clubs Relay (F)"); + default: + throw(ChronoRaceException(tr("Unexpected Type enum value '%1'").arg(static_cast(type)))); + } +} + +QString CRHelper::toTimeStr(uint const seconds, Timing::Status const status, char const *prefix) +{ + + QString retString(prefix ? prefix : ""); + switch (status) { + case Timing::Status::CLASSIFIED: + retString.append(QString("%1:%2:%3").arg(((seconds / 60) / 60)).arg(((seconds / 60) % 60), 2, 10, QLatin1Char('0')).arg((seconds % 60), 2, 10, QLatin1Char('0'))); + break; + case Timing::Status::DNF: + retString.append("DNF"); + break; + case Timing::Status::DNS: + retString.append("DNS"); + break; + default: + throw(ChronoRaceException(tr("Invalid status value %1").arg(static_cast(status)))); + } + return retString; +} + +QString CRHelper::toTimeStr(Timing const &timing) +{ + return toTimeStr(timing.getSeconds(), timing.getStatus()); +} diff --git a/crhelper.hpp b/crhelper.hpp new file mode 100644 index 0000000..1f89f19 --- /dev/null +++ b/crhelper.hpp @@ -0,0 +1,61 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#ifndef CRHELPER_H +#define CRHELPER_H + +#include +#include + +#include "crloader.hpp" +#include "competitor.hpp" +#include "ranking.hpp" +#include "category.hpp" +#include "timing.hpp" + +namespace helper { +class CRHelper; +} + +class CRHelper +{ + Q_DECLARE_TR_FUNCTIONS(CRHelper) + +public: + static QString encodingToLabel(CRLoader::Encoding const &value); + static QString formatToLabel(CRLoader::Format const &value); + + static Competitor::Sex toSex(QString const &sex); + static QString toSexString(Competitor::Sex const sex); + static QString toSexFullString(Competitor::Sex const sex); + static int toOffset(QString const &offset); + static QString toOffsetString(int offset); + + static Ranking::Type toRankingType(QString const &type); + static QString toTypeString(Ranking::Type const type); + + static Category::Type toCategoryType(QString const &type); + static QString toTypeString(Category::Type const type); + + static QString toRankingTypeString(Ranking::Type type); + static QString toCategoryTypeString(Category::Type const type); + + static QString toTimeStr(uint const seconds, Timing::Status const status, char const *prefix = Q_NULLPTR); + static QString toTimeStr(Timing const &timing); +}; + +#endif // CRHELPER_H diff --git a/crloader.cpp b/crloader.cpp index 777d7e1..dc8b0ed 100644 --- a/crloader.cpp +++ b/crloader.cpp @@ -20,12 +20,14 @@ #include #include "crloader.hpp" +#include "lbchronorace.hpp" #include "lbcrexception.hpp" // Members initialization StartListModel CRLoader::startListModel; TeamsListModel CRLoader::teamsListModel; TimingsModel CRLoader::timingsModel; +RankingsModel CRLoader::rankingsModel; CategoriesModel CRLoader::categoriesModel; QList CRLoader::standardItemList; CRLoader::Encoding CRLoader::encoding = CRLoader::Encoding::LATIN1; @@ -46,6 +48,11 @@ CRTableModel *CRLoader::getTimingsModel() return &timingsModel; } +CRTableModel *CRLoader::getRankingsModel() +{ + return &rankingsModel; +} + CRTableModel *CRLoader::getCategoriesModel() { return &categoriesModel; @@ -66,18 +73,6 @@ void CRLoader::setEncoding(Encoding const &value) encoding = value; } -QString CRLoader::encodingToLabel(Encoding const &value) -{ - switch (value) { - case CRLoader::Encoding::UTF8: - return tr("UTF-8"); - case CRLoader::Encoding::LATIN1: - return tr("ISO-8859-1 (Latin-1)"); - default: - return tr("Unknown encoding %1").arg(static_cast(value)); - } -} - CRLoader::Format CRLoader::getFormat() { return format; @@ -88,20 +83,6 @@ void CRLoader::setFormat(Format const &value) format = value; } -QString CRLoader::formatToLabel(Format const &value) -{ - switch (value) { - case CRLoader::Format::PDF: - return tr("PDF"); - case CRLoader::Format::TEXT: - return tr("Text"); - case CRLoader::Format::CSV: - return tr("CSV"); - default: - return tr("Unknown format %1").arg(static_cast(value)); - } -} - void CRLoader::loadCSV(QString const &filePath, QAbstractTableModel *model) { static QRegularExpression re("\r"); @@ -168,6 +149,7 @@ void CRLoader::saveRaceData(QDataStream &out) { out << startListModel << teamsListModel + << rankingsModel << categoriesModel << timingsModel; @@ -178,23 +160,27 @@ void CRLoader::loadRaceData(QDataStream &in) standardItemList.clear(); startListModel.reset(); teamsListModel.reset(); + rankingsModel.reset(); categoriesModel.reset(); timingsModel.reset(); in >> startListModel - >> teamsListModel - >> categoriesModel + >> teamsListModel; + if (LBChronoRace::binFormat > LBCHRONORACE_BIN_FMT_v3) { + in >> rankingsModel; + } + in >> categoriesModel >> timingsModel; startListModel.refreshCounters(0); teamsListModel.refreshCounters(0); + rankingsModel.refreshCounters(0); categoriesModel.refreshCounters(0); timingsModel.refreshCounters(0); } QPair CRLoader::importStartList(QString const &path) { - startListModel.reset(); teamsListModel.reset(); @@ -209,33 +195,6 @@ QPair CRLoader::importStartList(QString const &path) return QPair(rowCount, teamsListModel.rowCount()); } -void CRLoader::exportStartList(QString const &path) -{ - saveCSV(path, &startListModel); -} - -void CRLoader::exportTeams(QString const &path) -{ - - QFile outFile(path); - if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { - throw(ChronoRaceException(tr("Error: cannot open %1").arg(path))); - } - QTextStream outStream(&outFile); - - if (CRLoader::getEncoding() == Encoding::UTF8) - outStream.setEncoding(QStringConverter::Utf8); - else /* if (encoding == Encoding::LATIN1) */ //NOSONAR - outStream.setEncoding(QStringConverter::Latin1); - - int rowCount = teamsListModel.rowCount(); - for (int r = 0; r < rowCount; ++r) - outStream << teamsListModel.data(teamsListModel.index(r, 0, QModelIndex()), Qt::DisplayRole).toString() << Qt::endl; - - outStream.flush(); - outFile.close(); -} - QList CRLoader::getStartList() { return QList(startListModel.getStartList()); @@ -266,28 +225,6 @@ uint CRLoader::getTeamNameWidthMax() return startListModel.getTeamNameWidthMax(); } -int CRLoader::importTimings(QString const &path) -{ - - timingsModel.reset(); - - loadCSV(path, &timingsModel); - - int rowCount = timingsModel.rowCount(); - - if (int columnCount = timingsModel.columnCount(); columnCount != static_cast(Timing::Field::TMF_COUNT)) { - timingsModel.reset(); - throw(ChronoRaceException(tr("Wrong number of columns; expected %1 - found %2").arg(static_cast(Timing::Field::TMF_COUNT)).arg(columnCount))); - } - - return rowCount; -} - -void CRLoader::exportTimings(QString const &path) -{ - saveCSV(path, &timingsModel); -} - void CRLoader::clearTimings() { timingsModel.reset(); @@ -305,36 +242,103 @@ void CRLoader::addTiming(QString const &bib, QString const &timing) checkString(&timingsModel, temp); } -QVector CRLoader::getTimings() +QList const &CRLoader::getTimings() { - return QVector::fromList(timingsModel.getTimings()); + return timingsModel.getTimings(); } -int CRLoader::importCategories(QString const &path) +int CRLoader::importModel(Model model, QString const &path) { + int rowCount = 0; + int columnCount = 0; - categoriesModel.reset(); - - loadCSV(path, &categoriesModel); - - int rowCount = categoriesModel.rowCount(); - - if (int columnCount = categoriesModel.columnCount(); columnCount != static_cast(Category::Field::CTF_COUNT)) { + switch (model) { + case Model::RANKINGS: + rankingsModel.reset(); + loadCSV(path, &rankingsModel); + rowCount = rankingsModel.rowCount(); + if (columnCount = rankingsModel.columnCount(); columnCount != static_cast(Ranking::Field::RTF_COUNT)) { + rankingsModel.reset(); + throw(ChronoRaceException(tr("Wrong number of columns; expected %1 - found %2").arg(static_cast(Ranking::Field::RTF_COUNT)).arg(columnCount))); + } + rankingsModel.parseCategories(); + break; + case Model::CATEGORIES: categoriesModel.reset(); - throw(ChronoRaceException(tr("Wrong number of columns; expected %1 - found %2").arg(static_cast(Category::Field::CTF_COUNT)).arg(columnCount))); + loadCSV(path, &categoriesModel); + rowCount = categoriesModel.rowCount(); + if (columnCount = categoriesModel.columnCount(); columnCount != static_cast(Category::Field::CTF_COUNT)) { + categoriesModel.reset(); + throw(ChronoRaceException(tr("Wrong number of columns; expected %1 - found %2").arg(static_cast(Category::Field::CTF_COUNT)).arg(columnCount))); + } + break; + case Model::TIMINGS: + timingsModel.reset(); + loadCSV(path, &timingsModel); + rowCount = timingsModel.rowCount(); + if (columnCount = timingsModel.columnCount(); columnCount != static_cast(Timing::Field::TMF_COUNT)) { + timingsModel.reset(); + throw(ChronoRaceException(tr("Wrong number of columns; expected %1 - found %2").arg(static_cast(Timing::Field::TMF_COUNT)).arg(columnCount))); + } + break; + default: + throw(ChronoRaceException(tr("Unexpected model value %1 (import)").arg(static_cast(model)))); + break; } return rowCount; } -void CRLoader::exportCategories(QString const &path) +void CRLoader::exportModel(Model model, QString const &path) +{ + switch (model) { + case Model::STARTLIST: + saveCSV(path, &startListModel); + break; + case Model::TEAMSLIST: + { + QFile outFile(path); + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + throw(ChronoRaceException(tr("Error: cannot open %1").arg(path))); + } + QTextStream outStream(&outFile); + + if (CRLoader::getEncoding() == Encoding::UTF8) + outStream.setEncoding(QStringConverter::Utf8); + else /* if (encoding == Encoding::LATIN1) */ //NOSONAR + outStream.setEncoding(QStringConverter::Latin1); + + int rowCount = teamsListModel.rowCount(); + for (int r = 0; r < rowCount; ++r) + outStream << teamsListModel.data(teamsListModel.index(r, 0, QModelIndex()), Qt::DisplayRole).toString() << Qt::endl; + + outStream.flush(); + outFile.close(); + } + break; + case Model::RANKINGS: + saveCSV(path, &rankingsModel); + break; + case Model::CATEGORIES: + saveCSV(path, &categoriesModel); + break; + case Model::TIMINGS: + saveCSV(path, &timingsModel); + break; + default: + throw(ChronoRaceException(tr("Unexpected model value %1 (export)").arg(static_cast(model)))); + break; + } +} + +QList const &CRLoader::getRankings() { - saveCSV(path, &categoriesModel); + return rankingsModel.getRankings(); } -QVector CRLoader::getCategories() +QList const &CRLoader::getCategories() { - return QVector::fromList(categoriesModel.getCategories()); + return categoriesModel.getCategories(); } void CRLoader::checkString(QAbstractTableModel *model, QString &token, QChar character) diff --git a/crloader.hpp b/crloader.hpp index e6ab5c1..7759aa5 100644 --- a/crloader.hpp +++ b/crloader.hpp @@ -15,11 +15,12 @@ * along with this program. If not, see . * *****************************************************************************/ -#ifndef LBLOADER_H -#define LBLOADER_H +#ifndef CRLOADER_H +#define CRLOADER_H +#include #include -#include +#include #include #include "timing.hpp" @@ -30,6 +31,7 @@ #include "startlistmodel.hpp" #include "teamslistmodel.hpp" #include "timingsmodel.hpp" +#include "rankingsmodel.hpp" #include "categoriesmodel.hpp" namespace loader { @@ -54,10 +56,20 @@ class CRLoader CSV = 2 }; + enum class Model + { + STARTLIST, + TEAMSLIST, + RANKINGS, + CATEGORIES, + TIMINGS + }; + private: static StartListModel startListModel; static TeamsListModel teamsListModel; static TimingsModel timingsModel; + static RankingsModel rankingsModel; static CategoriesModel categoriesModel; static QList standardItemList; static Encoding encoding; @@ -72,34 +84,30 @@ class CRLoader static void saveRaceData(QDataStream &out); static void loadRaceData(QDataStream &in); static QPair importStartList(QString const &path); - static void exportStartList(QString const &path); - static void exportTeams(QString const &path); static QList getStartList(); static uint getStartListLegs(); static void setStartListLegs(uint leg); static uint getStartListBibMax(); static uint getStartListNameWidthMax(); static uint getTeamNameWidthMax(); - static int importTimings(QString const &path); - static void exportTimings(QString const &path); static void clearTimings(); static void addTiming(QString const &bib, QString const &timing); - static QVector getTimings(); - static int importCategories(QString const &path); - static void exportCategories(QString const &path); - static QVector getCategories(); + static QList const &getTimings(); + static int importModel(Model model, QString const &path); + static void exportModel(Model model, QString const &path); + static QList const &getRankings(); + static QList const &getCategories(); static CRTableModel *getStartListModel(); static CRTableModel *getTeamsListModel(); static CRTableModel *getTimingsModel(); + static CRTableModel *getRankingsModel(); static CRTableModel *getCategoriesModel(); static Encoding getEncoding(); static void setEncoding(Encoding const &value); - static QString encodingToLabel(Encoding const &value); static Format getFormat(); static void setFormat(Format const &value); - static QString formatToLabel(Format const &value); static QStringList getClubs(); }; -#endif // LBLOADER_H +#endif // CRLOADER_H diff --git a/csvrankingprinter.cpp b/csvrankingprinter.cpp index fc0cb67..ed64e77 100644 --- a/csvrankingprinter.cpp +++ b/csvrankingprinter.cpp @@ -19,11 +19,14 @@ #include "csvrankingprinter.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" -void CSVRankingPrinter::init(QString *outFileName, [[maybe_unused]] QString const &title) +void CSVRankingPrinter::init(QString *outFileName, QString const &title) { Q_ASSERT(!csvFile.isOpen()); + Q_UNUSED(title) + if (outFileName == Q_NULLPTR) { throw(ChronoRaceException(tr("Error: no file name supplied"))); } @@ -50,7 +53,7 @@ void CSVRankingPrinter::init(QString *outFileName, [[maybe_unused]] QString cons } } -void CSVRankingPrinter::printStartList(QList const &startList) +void CSVRankingPrinter::printStartList(QList const &startList) { if (!csvFile.isOpen()) { throw(ChronoRaceException(tr("Error: writing attempt on closed file"))); @@ -59,21 +62,21 @@ void CSVRankingPrinter::printStartList(QList const &startList) int offset; int i = 0; QTime startTime = getRaceInfo()->getStartTime(); - for (auto const &competitor : startList) { + for (auto const *competitor : startList) { i++; csvStream << i << ","; - csvStream << competitor.getBib() << ","; - csvStream << competitor.getName() << ","; - csvStream << competitor.getTeam() << ","; - csvStream << competitor.getYear() << ","; - csvStream << Competitor::toSexString(competitor.getSex()) << ","; - offset = competitor.getOffset(); + csvStream << competitor->getBib() << ","; + csvStream << competitor->getName() << ","; + csvStream << competitor->getTeam() << ","; + csvStream << competitor->getYear() << ","; + csvStream << CRHelper::toSexString(competitor->getSex()) << ","; + offset = competitor->getOffset(); if (offset >= 0) { offset += (3600 * startTime.hour()) + (60 * startTime.minute()) + startTime.second(); - csvStream << Competitor::toOffsetString(offset); + csvStream << CRHelper::toOffsetString(offset); } else if (CRLoader::getStartListLegs() == 1) { offset = (3600 * startTime.hour()) + (60 * startTime.minute()) + startTime.second(); - csvStream << Competitor::toOffsetString(offset); + csvStream << CRHelper::toOffsetString(offset); } else { csvStream << qAbs(offset); } @@ -82,7 +85,7 @@ void CSVRankingPrinter::printStartList(QList const &startList) csvStream << Qt::endl; } -void CSVRankingPrinter::printRanking(const Category &category, QList const &ranking) +void CSVRankingPrinter::printRanking(Ranking const &categories, QList const &ranking) { static Position position; @@ -94,7 +97,7 @@ void CSVRankingPrinter::printRanking(const Category &category, QListgetTotalTime(CRLoader::Format::CSV); @@ -112,14 +115,14 @@ void CSVRankingPrinter::printRanking(const Category &category, QList const &ranking) +void CSVRankingPrinter::printRanking(Ranking const &categories, QList const &ranking) { if (!csvFile.isOpen()) { throw(ChronoRaceException(tr("Error: writing attempt on closed file"))); } int i = 0; - csvStream << category.getShortDescription() << Qt::endl; + csvStream << categories.getShortDescription() << Qt::endl; for (auto const r : ranking) { i++; for (int j = 0; j < r->getClassEntryCount(); j++) { diff --git a/csvrankingprinter.hpp b/csvrankingprinter.hpp index ba95be4..31efd81 100644 --- a/csvrankingprinter.hpp +++ b/csvrankingprinter.hpp @@ -31,9 +31,9 @@ class CSVRankingPrinter final : public RankingPrinter public: void init(QString *outFileName, QString const &title) override; - void printStartList(QList const &startList) override; - void printRanking(Category const &category, QList const &ranking) override; - void printRanking(Category const &category, QList const &ranking) override; + void printStartList(QList const &startList) override; + void printRanking(Ranking const &categories, QList const &ranking) override; + void printRanking(Ranking const &categories, QList const &ranking) override; bool finalize() override; diff --git a/icons/filter_1.svg b/icons/filter_1.svg new file mode 100644 index 0000000..67344fe --- /dev/null +++ b/icons/filter_1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_2.svg b/icons/filter_2.svg new file mode 100644 index 0000000..0368ad5 --- /dev/null +++ b/icons/filter_2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_3.svg b/icons/filter_3.svg new file mode 100644 index 0000000..fd840e8 --- /dev/null +++ b/icons/filter_3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_4.svg b/icons/filter_4.svg new file mode 100644 index 0000000..5d0896c --- /dev/null +++ b/icons/filter_4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_5.svg b/icons/filter_5.svg new file mode 100644 index 0000000..e535344 --- /dev/null +++ b/icons/filter_5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_6.svg b/icons/filter_6.svg new file mode 100644 index 0000000..abab19e --- /dev/null +++ b/icons/filter_6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_7.svg b/icons/filter_7.svg new file mode 100644 index 0000000..78fa8fa --- /dev/null +++ b/icons/filter_7.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_8.svg b/icons/filter_8.svg new file mode 100644 index 0000000..7565937 --- /dev/null +++ b/icons/filter_8.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/filter_9.svg b/icons/filter_9.svg new file mode 100644 index 0000000..0f5d5c6 --- /dev/null +++ b/icons/filter_9.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lbchronorace.cpp b/lbchronorace.cpp index c85ebf6..d38e7a9 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -29,25 +29,30 @@ #include "lbchronorace.hpp" #include "lbcrexception.hpp" #include "rankingswizard.hpp" +#include "crhelper.hpp" // static members initialization QDir LBChronoRace::lastSelectedPath(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); -int LBChronoRace::binFormat = LBCHRONORACE_BIN_FMT; +uint LBChronoRace::binFormat = LBCHRONORACE_BIN_FMT; +QRegularExpression LBChronoRace::csvFilter("[,;]"); LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : QMainWindow(parent), raceInfo(parent), startListTable(parent), teamsTable(parent), + rankingsTable(parent), categoriesTable(parent), timingsTable(parent), sexDelegate(&startListTable), clubDelegate(&startListTable), - catSexDelegate(&categoriesTable), - catTypeDelegate(&categoriesTable) + rankingTypeDelegate(&rankingsTable), + rankingCatsDelegate(&rankingsTable), + categoryTypeDelegate(&categoriesTable) { startListFileName = lastSelectedPath.filePath(LBCHRONORACE_STARTLIST_DEFAULT); timingsFileName = lastSelectedPath.filePath(LBCHRONORACE_TIMINGS_DEFAULT); + rankingsFileName = lastSelectedPath.filePath(LBCHRONORACE_RANKINGS_DEFAULT); categoriesFileName = lastSelectedPath.filePath(LBCHRONORACE_CATEGORIES_DEFAULT); teamsFileName = lastSelectedPath.filePath(LBCHRONORACE_TEAMLIST_DEFAULT); @@ -65,20 +70,29 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : QObject::connect(&teamsTable, &ChronoRaceTable::modelExported, this, &LBChronoRace::exportTeamList); QObject::connect(&teamsTable, &ChronoRaceTable::newRowCount, this, &LBChronoRace::setCounterTeams); - auto const *startListModel = (StartListModel const *) CRLoader::getStartListModel(); - auto const *teamsListModel = (TeamsListModel const *) CRLoader::getTeamsListModel(); + auto const *startListModel = dynamic_cast(CRLoader::getStartListModel()); + auto const *teamsListModel = dynamic_cast(CRLoader::getTeamsListModel()); QObject::connect(startListModel, &StartListModel::newClub, teamsListModel, &TeamsListModel::addTeam); QObject::connect(startListModel, &StartListModel::error, this, &LBChronoRace::appendErrorMessage); - auto *categoriesModel = (CategoriesModel *) CRLoader::getCategoriesModel(); + auto *rankingsModel = dynamic_cast(CRLoader::getRankingsModel()); + rankingsTable.setWindowTitle(tr("Rankings")); + rankingsTable.setModel(rankingsModel); + QObject::connect(&rankingsTable, &ChronoRaceTable::modelImported, this, &LBChronoRace::importRankingsList); + QObject::connect(&rankingsTable, &ChronoRaceTable::modelExported, this, &LBChronoRace::exportRankingsList); + QObject::connect(&rankingsTable, &ChronoRaceTable::newRowCount, this, &LBChronoRace::setCounterRankings); + QObject::connect(rankingsModel, &RankingsModel::error, this, &LBChronoRace::appendErrorMessage); + + auto *categoriesModel = dynamic_cast(CRLoader::getCategoriesModel()); categoriesTable.setWindowTitle(tr("Categories")); categoriesTable.setModel(categoriesModel); QObject::connect(&categoriesTable, &ChronoRaceTable::modelImported, this, &LBChronoRace::importCategoriesList); QObject::connect(&categoriesTable, &ChronoRaceTable::modelExported, this, &LBChronoRace::exportCategoriesList); QObject::connect(&categoriesTable, &ChronoRaceTable::newRowCount, this, &LBChronoRace::setCounterCategories); QObject::connect(categoriesModel, &CategoriesModel::error, this, &LBChronoRace::appendErrorMessage); + rankingCatsDelegate.setCategories(categoriesModel); - auto *timingsModel = (TimingsModel *) CRLoader::getTimingsModel(); + auto *timingsModel = dynamic_cast(CRLoader::getTimingsModel()); timingsTable.setWindowTitle(tr("Timings List")); timingsTable.setModel(timingsModel); QObject::connect(&timingsTable, &ChronoRaceTable::modelImported, this, &LBChronoRace::importTimingsList); @@ -100,6 +114,7 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : QObject::connect(ui->editRace, &QPushButton::clicked, &raceInfo, &ChronoRaceData::show); QObject::connect(ui->editStartList, &QPushButton::clicked, &startListTable, &ChronoRaceTable::show); QObject::connect(ui->editClubsList, &QPushButton::clicked, &teamsTable, &ChronoRaceTable::show); + QObject::connect(ui->editRankings, &QPushButton::clicked, &rankingsTable, &ChronoRaceTable::show); QObject::connect(ui->editCategories, &QPushButton::clicked, &categoriesTable, &ChronoRaceTable::show); QObject::connect(ui->editTimings, &QPushButton::clicked, &timingsTable, &ChronoRaceTable::show); QObject::connect(ui->importTimings, &QPushButton::clicked, &timingsTable, &ChronoRaceTable::modelImport); @@ -115,6 +130,7 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : QObject::connect(ui->actionEditRace, &QAction::triggered, &raceInfo, &ChronoRaceData::show); QObject::connect(ui->actionEditStartList, &QAction::triggered, &startListTable, &ChronoRaceTable::show); QObject::connect(ui->actionEditTeams, &QAction::triggered, &teamsTable, &ChronoRaceTable::show); + QObject::connect(ui->actionEditRankings, &QAction::triggered, &rankingsTable, &ChronoRaceTable::show); QObject::connect(ui->actionEditCategories, &QAction::triggered, &categoriesTable, &ChronoRaceTable::show); QObject::connect(ui->actionEditTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::show); QObject::connect(ui->actionImportTimings, &QAction::triggered, &timingsTable, &ChronoRaceTable::modelImport); @@ -130,8 +146,9 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : // tie the views with the related delegate instances startListTable.setItemDelegateForColumn(static_cast(Competitor::Field::CMF_SEX), &sexDelegate); startListTable.setItemDelegateForColumn(static_cast(Competitor::Field::CMF_CLUB), &clubDelegate); - categoriesTable.setItemDelegateForColumn(static_cast(Category::Field::CTF_SEX), &catSexDelegate); - categoriesTable.setItemDelegateForColumn(static_cast(Category::Field::CTF_TEAM), &catTypeDelegate); + rankingsTable.setItemDelegateForColumn(static_cast(Ranking::Field::RTF_TEAM), &rankingTypeDelegate); + rankingsTable.setItemDelegateForColumn(static_cast(Ranking::Field::RTF_CATEGORIES), &rankingCatsDelegate); + categoriesTable.setItemDelegateForColumn(static_cast(Category::Field::CTF_TYPE), &categoryTypeDelegate); } void LBChronoRace::setCounterTeams(int count) const @@ -147,6 +164,11 @@ void LBChronoRace::setCounterCompetitors(int count) const setCounterTeams(CRLoader::getTeamsListModel()->rowCount()); } +void LBChronoRace::setCounterRankings(int count) const +{ + ui->counterRankings->display(count); +} + void LBChronoRace::setCounterCategories(int count) const { ui->counterCategories->display(count); @@ -188,6 +210,25 @@ void LBChronoRace::importStartList() } } +void LBChronoRace::importRankingsList() +{ + rankingsFileName = QFileDialog::getOpenFileName(this, tr("Select Rankings File"), + lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + + if (!rankingsFileName.isEmpty()) { + int count = 0; + appendInfoMessage(tr("Rankings File: %1").arg(rankingsFileName)); + try { + count = CRLoader::importModel(CRLoader::Model::RANKINGS, rankingsFileName); + appendInfoMessage(tr("Loaded: %n ranking(s)", "", count)); + lastSelectedPath = QFileInfo(rankingsFileName).absoluteDir(); + } catch (ChronoRaceException &e) { + appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + } + setCounterRankings(count); + } +} + void LBChronoRace::importCategoriesList() { categoriesFileName = QFileDialog::getOpenFileName(this, tr("Select Categories File"), @@ -197,7 +238,7 @@ void LBChronoRace::importCategoriesList() int count = 0; appendInfoMessage(tr("Categories File: %1").arg(categoriesFileName)); try { - count = CRLoader::importCategories(categoriesFileName); + count = CRLoader::importModel(CRLoader::Model::CATEGORIES, categoriesFileName); appendInfoMessage(tr("Loaded: %n category(s)", "", count)); lastSelectedPath = QFileInfo(categoriesFileName).absoluteDir(); } catch (ChronoRaceException &e) { @@ -216,7 +257,7 @@ void LBChronoRace::importTimingsList() int count = 0; appendInfoMessage(tr("Timings File: %1").arg(timingsFileName)); try { - count = CRLoader::importTimings(timingsFileName); + count = CRLoader::importModel(CRLoader::Model::TIMINGS, timingsFileName); appendInfoMessage(tr("Loaded: %n timing(s)", "", count)); lastSelectedPath = QFileInfo(timingsFileName).absoluteDir(); } catch (ChronoRaceException &e) { @@ -237,7 +278,7 @@ void LBChronoRace::exportStartList() startListFileName.append(".csv"); try { - CRLoader::exportStartList(startListFileName); + CRLoader::exportModel(CRLoader::Model::STARTLIST, startListFileName); appendInfoMessage(tr("Start List File saved: %1").arg(startListFileName)); lastSelectedPath = QFileInfo(startListFileName).absoluteDir(); } catch (ChronoRaceException &e) { @@ -257,7 +298,7 @@ void LBChronoRace::exportTeamList() teamsFileName.append(".csv"); try { - CRLoader::exportTeams(teamsFileName); + CRLoader::exportModel(CRLoader::Model::TEAMSLIST, teamsFileName); appendInfoMessage(tr("Teams File saved: %1").arg(teamsFileName)); lastSelectedPath = QFileInfo(teamsFileName).absoluteDir(); } catch (ChronoRaceException &e) { @@ -266,6 +307,26 @@ void LBChronoRace::exportTeamList() } } +void LBChronoRace::exportRankingsList() +{ + rankingsFileName = QFileDialog::getSaveFileName(this, tr("Select Rankings File"), + lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + + if (!rankingsFileName.isEmpty()) { + + if (!rankingsFileName.endsWith(".csv", Qt::CaseInsensitive)) + rankingsFileName.append(".csv"); + + try { + CRLoader::exportModel(CRLoader::Model::RANKINGS, rankingsFileName); + appendInfoMessage(tr("Rankings File saved: %1").arg(rankingsFileName)); + lastSelectedPath = QFileInfo(rankingsFileName).absoluteDir(); + } catch (ChronoRaceException &e) { + appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + } + } +} + void LBChronoRace::exportCategoriesList() { categoriesFileName = QFileDialog::getSaveFileName(this, tr("Select Categories File"), @@ -277,7 +338,7 @@ void LBChronoRace::exportCategoriesList() categoriesFileName.append(".csv"); try { - CRLoader::exportCategories(categoriesFileName); + CRLoader::exportModel(CRLoader::Model::CATEGORIES, categoriesFileName); appendInfoMessage(tr("Categories File saved: %1").arg(categoriesFileName)); lastSelectedPath = QFileInfo(categoriesFileName).absoluteDir(); } catch (ChronoRaceException &e) { @@ -297,7 +358,7 @@ void LBChronoRace::exportTimingsList() timingsFileName.append(".csv"); try { - CRLoader::exportTimings(timingsFileName); + CRLoader::exportModel(CRLoader::Model::TIMINGS, timingsFileName); appendInfoMessage(tr("Timings File saved: %1").arg(timingsFileName)); lastSelectedPath = QFileInfo(timingsFileName).absoluteDir(); } catch (ChronoRaceException &e) { @@ -354,7 +415,7 @@ void LBChronoRace::encodingSelector(int idx) const break; } - appendInfoMessage(tr("Selected encoding: %1").arg(CRLoader::encodingToLabel(CRLoader::getEncoding()))); + appendInfoMessage(tr("Selected encoding: %1").arg(CRHelper::encodingToLabel(CRLoader::getEncoding()))); } void LBChronoRace::formatSelector(int idx) const @@ -374,7 +435,7 @@ void LBChronoRace::formatSelector(int idx) const break; } - appendInfoMessage(tr("Selected format: %1").arg(CRLoader::formatToLabel(CRLoader::getFormat()))); + appendInfoMessage(tr("Selected format: %1").arg(CRHelper::formatToLabel(CRLoader::getFormat()))); } bool LBChronoRace::loadRaceFile(QString const &fileName) @@ -397,6 +458,7 @@ bool LBChronoRace::loadRaceFile(QString const &fileName) case LBCHRONORACE_BIN_FMT_v1: case LBCHRONORACE_BIN_FMT_v2: case LBCHRONORACE_BIN_FMT_v3: + case LBCHRONORACE_BIN_FMT_v4: QAbstractTableModel const *table; qint16 encodingIdx; qint16 formatIdx; @@ -424,6 +486,10 @@ bool LBChronoRace::loadRaceFile(QString const &fileName) tableCount = table->rowCount(); setCounterTeams(tableCount); appendInfoMessage(tr("Loaded: %n team(s)", "", tableCount)); + table = CRLoader::getRankingsModel(); + tableCount = table->rowCount(); + setCounterRankings(tableCount); + appendInfoMessage(tr("Loaded: %n ranking(s)", "", tableCount)); table = CRLoader::getCategoriesModel(); tableCount = table->rowCount(); setCounterCategories(tableCount); @@ -437,7 +503,7 @@ bool LBChronoRace::loadRaceFile(QString const &fileName) retval = true; break; default: - QMessageBox::information(this, tr("Race Data File Error"), tr("Data format %1 not supported.\nPlease uodate the application.").arg(binFmt)); + QMessageBox::information(this, tr("Race Data File Error"), tr("Data format %1 not supported.\nPlease update the application.").arg(binFmt)); break; } } else { @@ -513,8 +579,8 @@ void LBChronoRace::setEncoding() int current = 0; QStringList items = { - CRLoader::encodingToLabel(CRLoader::Encoding::LATIN1), - CRLoader::encodingToLabel(CRLoader::Encoding::UTF8) + CRHelper::encodingToLabel(CRLoader::Encoding::LATIN1), + CRHelper::encodingToLabel(CRLoader::Encoding::UTF8) }; switch (CRLoader::getEncoding()) { @@ -549,7 +615,7 @@ void LBChronoRace::setEncoding() void LBChronoRace::makeStartList() { - //NOSONAR ui->errorDisplay->clear(); + ui->errorDisplay->clear(); try { RankingsWizard wizard(&raceInfo, &lastSelectedPath, RankingsWizard::RankingsWizardTarget::StartList); @@ -569,7 +635,7 @@ void LBChronoRace::makeStartList() void LBChronoRace::makeRankings() { - //NOSONAR ui->errorDisplay->clear(); + ui->errorDisplay->clear(); try { RankingsWizard wizard(&raceInfo, &lastSelectedPath, RankingsWizard::RankingsWizardTarget::Rankings); diff --git a/lbchronorace.hpp b/lbchronorace.hpp index 86928d4..4e7a552 100644 --- a/lbchronorace.hpp +++ b/lbchronorace.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include "ui_chronorace.h" @@ -29,9 +30,10 @@ #include "chronoracetable.hpp" #include "chronoracedata.hpp" #include "chronoracetimings.hpp" -#include "sexdelegate.hpp" +#include "compsexdelegate.hpp" #include "clubdelegate.hpp" -#include "catsexdelegate.hpp" +#include "rankingtypedelegate.hpp" +#include "rankingcatsdelegate.hpp" #include "cattypedelegate.hpp" #ifndef LBCHRONORACE_NAME @@ -44,12 +46,14 @@ constexpr char LBCHRONORACE_STARTLIST_DEFAULT[] = "startlist.csv"; constexpr char LBCHRONORACE_TEAMLIST_DEFAULT[] = "teamlist.csv"; constexpr char LBCHRONORACE_TIMINGS_DEFAULT[] = "timings.csv"; +constexpr char LBCHRONORACE_RANKINGS_DEFAULT[] = "rankings.csv"; constexpr char LBCHRONORACE_CATEGORIES_DEFAULT[] = "categories.csv"; -constexpr int LBCHRONORACE_BIN_FMT_v1 = 1; -constexpr int LBCHRONORACE_BIN_FMT_v2 = 2; -constexpr int LBCHRONORACE_BIN_FMT_v3 = 3; -#define LBCHRONORACE_BIN_FMT LBCHRONORACE_BIN_FMT_v3 +constexpr uint LBCHRONORACE_BIN_FMT_v1 = 1u; +constexpr uint LBCHRONORACE_BIN_FMT_v2 = 2u; +constexpr uint LBCHRONORACE_BIN_FMT_v3 = 3u; +constexpr uint LBCHRONORACE_BIN_FMT_v4 = 4u; +#define LBCHRONORACE_BIN_FMT LBCHRONORACE_BIN_FMT_v4 class LBChronoRace : public QMainWindow { @@ -59,7 +63,9 @@ class LBChronoRace : public QMainWindow explicit LBChronoRace(QWidget *parent = Q_NULLPTR, QGuiApplication const *app = Q_NULLPTR); static QDir lastSelectedPath; - static int binFormat; + static uint binFormat; + + static QRegularExpression csvFilter; public slots: void initialize(); @@ -68,6 +74,7 @@ public slots: void setCounterTeams(int count) const; void setCounterCompetitors(int count) const; + void setCounterRankings(int count) const; void setCounterCategories(int count) const; void setCounterTimings(int count) const; @@ -80,6 +87,7 @@ public slots: QString raceDataFileName { "" }; QString startListFileName; QString timingsFileName; + QString rankingsFileName; QString categoriesFileName; QString teamsFileName; @@ -87,15 +95,17 @@ public slots: ChronoRaceTable startListTable; ChronoRaceTable teamsTable; + ChronoRaceTable rankingsTable; ChronoRaceTable categoriesTable; ChronoRaceTable timingsTable; ChronoRaceTimings timings; - SexDelegate sexDelegate; + CompetitorSexDelegate sexDelegate; ClubDelegate clubDelegate; - CategorySexDelegate catSexDelegate; - CategoryTypeDelegate catTypeDelegate; + RankingTypeDelegate rankingTypeDelegate; + RankingCategoriesDelegate rankingCatsDelegate; + CategoryTypeDelegate categoryTypeDelegate; bool loadRaceFile(QString const &fileName); @@ -115,11 +125,13 @@ private slots: void makeRankings(); void importStartList(); + void importRankingsList(); void importCategoriesList(); void importTimingsList(); void exportStartList(); void exportTeamList(); + void exportRankingsList(); void exportCategoriesList(); void exportTimingsList(); }; diff --git a/materialicons.qrc b/materialicons.qrc index 3195e6d..087c21b 100644 --- a/materialicons.qrc +++ b/materialicons.qrc @@ -6,6 +6,15 @@ icons/edit_document.svg icons/exit_to_app.svg icons/file_open.svg + icons/filter_1.svg + icons/filter_2.svg + icons/filter_3.svg + icons/filter_4.svg + icons/filter_5.svg + icons/filter_6.svg + icons/filter_7.svg + icons/filter_8.svg + icons/filter_9.svg icons/format_list_bulleted.svg icons/format_list_bulleted_add.svg icons/format_list_numbered.svg diff --git a/multiselectcombobox.cpp b/multiselectcombobox.cpp new file mode 100644 index 0000000..142330d --- /dev/null +++ b/multiselectcombobox.cpp @@ -0,0 +1,255 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#include + +#include "multiselectcombobox.hpp" + +namespace { + const int scSearchBarIndex = 0; +} + +MultiSelectComboBox::MultiSelectComboBox(QWidget *aParent) : + QComboBox(aParent) +{ + QListWidget *listWidget; + QLineEdit *searchBar; + QLineEdit *lineEdit; + + mListWidget.reset(new QListWidget(this)); + QComboBox::setLineEdit(new QLineEdit(this)); + + listWidget = mListWidget.data(); + lineEdit = QComboBox::lineEdit(); + + listWidget->insertItem(0, new QListWidgetItem); + auto curItem = listWidget->item(0); + listWidget->setItemWidget(curItem, new QLineEdit(this)); + searchBar = static_cast(listWidget->itemWidget(curItem)); + searchBar->setPlaceholderText(tr("Search…")); + searchBar->setClearButtonEnabled(true); + lineEdit->setReadOnly(true); + lineEdit->installEventFilter(this); + + QComboBox::setModel(listWidget->model()); + QComboBox::setView(listWidget); + QComboBox::setSizeAdjustPolicy(QComboBox::AdjustToContents); + + connect(searchBar, &QLineEdit::textChanged, this, &MultiSelectComboBox::onSearch); + connect(this, &QComboBox::activated, this, &MultiSelectComboBox::itemClicked); +} + +void MultiSelectComboBox::hidePopup() +{ + int width = this->width(); + int height = mListWidget.data()->height(); + int x = QCursor::pos().x() - mapToGlobal(geometry().topLeft()).x() + geometry().x(); + int y = QCursor::pos().y() - mapToGlobal(geometry().topLeft()).y() + geometry().y(); + if ((x >= 0) && (x <= width) && (y >= this->height()) && (y <= height + this->height())) { + // Item was clicked, do not hide popup + } else { + QComboBox::hidePopup(); + } +} + +void MultiSelectComboBox::stateChanged(int aState) +{ + QListWidget const *listWidget = mListWidget.data(); + QListWidgetItem *listItem; + QLineEdit *lineEdit = QComboBox::lineEdit(); + QCheckBox const *checkBox; + QStringList selectedData; + + Q_UNUSED(aState) + + int count = listWidget->count(); + + for (int i = 1; i < count; i++) { + listItem = listWidget->item(i); + checkBox = static_cast(listWidget->itemWidget(listItem)); + + if (checkBox->isChecked()) { + selectedData.append(listItem->data(Qt::UserRole).toString()); + } + } + + if (selectedData.isEmpty()) { + lineEdit->clear(); + } else { + lineEdit->setText(selectedData.join(',')); + } + + lineEdit->setToolTip(lineEdit->text()); + + emit selectionChanged(); +} + +QStringList MultiSelectComboBox::currentText() const +{ + return QComboBox::lineEdit()->text().split(','); + +} + +void MultiSelectComboBox::addItem(QString const &aText, QVariant const &aUserData) +{ + QListWidget *listWidget; + QListWidgetItem *listWidgetItem; + QCheckBox const *checkBox; + + listWidget = mListWidget.data(); + + auto numItems = listWidget->count(); + listWidget->insertItem(numItems, new QListWidgetItem); + listWidgetItem = listWidget->item(numItems); + listWidgetItem->setData(Qt::UserRole, aUserData); + listWidget->setItemWidget(listWidgetItem, new QCheckBox(aText, this)); + + checkBox = static_cast(listWidget->itemWidget(listWidgetItem)); + connect(checkBox, &QCheckBox::stateChanged, this, &MultiSelectComboBox::stateChanged); +} + +void MultiSelectComboBox::addItems(QStringList const &aTexts) +{ + for(auto const &string : aTexts) { + addItem(string); + } +} + +int MultiSelectComboBox::count() const +{ + int count = mListWidget.data()->count() - 1; // Do not count the search bar + + return (count < 0) ? 0 : count; +} + +void MultiSelectComboBox::onSearch(QString const &aSearchString) const +{ + QCheckBox const *checkBox; + QListWidget const *listWidget = mListWidget.data(); + auto count = listWidget->count(); + + for (int i = 1; i < count; i++) { + checkBox = static_cast(listWidget->itemWidget(listWidget->item(i))); + + if (checkBox->text().contains(aSearchString, Qt::CaseInsensitive)) { + listWidget->item(i)->setHidden(false); + } else { + listWidget->item(i)->setHidden(true); + } + } +} + +void MultiSelectComboBox::itemClicked(int aIndex) const +{ + if (aIndex != scSearchBarIndex) { // 0 means the search bar + QListWidget const *listWidget = mListWidget.data(); + auto checkBox = static_cast(listWidget->itemWidget(listWidget->item(aIndex))); + checkBox->setChecked(!checkBox->isChecked()); + } +} + +void MultiSelectComboBox::setSearchBarPlaceHolderText(QString const &aPlaceHolderText) const +{ + QListWidget const *listWidget = mListWidget.data(); + static_cast(listWidget->itemWidget(listWidget->item(0)))->setPlaceholderText(aPlaceHolderText); +} + +void MultiSelectComboBox::setPlaceHolderText(QString const &aPlaceHolderText) const +{ + return QComboBox::lineEdit()->setPlaceholderText(aPlaceHolderText); + +} + +void MultiSelectComboBox::clear() const +{ + QListWidget *listWidget = mListWidget.data(); + QListWidgetItem *searchBarItem = listWidget->takeItem(0); + + listWidget->clear(); + + listWidget->insertItem(0, searchBarItem); +} + +void MultiSelectComboBox::wheelEvent(QWheelEvent *aWheelEvent) +{ + // Do not handle the wheel event + Q_UNUSED(aWheelEvent) +} + +bool MultiSelectComboBox::eventFilter(QObject *aObject, QEvent *aEvent) +{ + if (QEvent::Type evType = aEvent->type(); + (aObject == QComboBox::lineEdit()) && ((evType == QEvent::MouseButtonRelease) || (evType == QEvent::KeyRelease))) { + showPopup(); + return false; + } + return false; +} + +void MultiSelectComboBox::keyPressEvent(QKeyEvent *aEvent) +{ + if (aEvent->key() == Qt::Key_Down) + showPopup(); +} + +void MultiSelectComboBox::setCurrentText(QString const &aText) const +{ + Q_UNUSED(aText) +} + +void MultiSelectComboBox::setCurrentText(QStringList const &aText) const +{ + QCheckBox *checkBox; + QListWidget const *listWidget = mListWidget.data(); + QListWidgetItem *listItem; + auto count = listWidget->count(); + + for (int i = 1; i < count; i++) { + listItem = listWidget->item(i); + checkBox = static_cast(listWidget->itemWidget(listItem)); + + if (aText.contains(listItem->data(Qt::UserRole))) { + checkBox->setChecked(true); + } + } +} + +void MultiSelectComboBox::resetSelection() const +{ + QCheckBox *checkBox; + QListWidget const *listWidget = mListWidget.data(); + int count = listWidget->count(); + + for (int i = 1; i < count; i++) { + checkBox = static_cast(listWidget->itemWidget(listWidget->item(i))); + checkBox->setChecked(false); + } +} + +QSize MultiSelectComboBox::sizeHint() const +{ + QListWidget const *listWidget = mListWidget.data(); + QLineEdit const *lineEdit = QComboBox::lineEdit(); + + QSize hint = QComboBox::sizeHint(); + if (lineEdit->sizeHint().width() > hint.width()) + hint.setWidth(lineEdit->sizeHint().width()); + if (listWidget->sizeHint().width() > hint.width()) + hint.setWidth(listWidget->sizeHint().width()); + + return hint; +} diff --git a/multiselectcombobox.hpp b/multiselectcombobox.hpp new file mode 100644 index 0000000..6241164 --- /dev/null +++ b/multiselectcombobox.hpp @@ -0,0 +1,67 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#ifndef MULTISELECTCOMBOBOX_H +#define MULTISELECTCOMBOBOX_H + +#include +#include +#include +#include +#include +#include +#include + +class MultiSelectComboBox : public QComboBox +{ + Q_OBJECT + +public: + explicit MultiSelectComboBox(QWidget* aParent = Q_NULLPTR); + + void addItem(QString const &aText, QVariant const &aUserData = QVariant()); // NOSONAR + void addItems(QStringList const &aTexts); // NOSONAR + QStringList currentText() const; // NOSONAR + int count() const; // NOSONAR + void hidePopup() override; + void setSearchBarPlaceHolderText(QString const &aPlaceHolderText) const; + void setPlaceHolderText(QString const &aPlaceHolderText) const; + void resetSelection() const; + QSize sizeHint() const override; + +signals: + void selectionChanged(); + +public slots: + void clear() const; // NOSONAR + void setCurrentText(QString const &aText) const; // NOSONAR + void setCurrentText(QStringList const &aText) const; // NOSONAR + +protected: + void wheelEvent(QWheelEvent *aWheelEvent) override; + bool eventFilter(QObject *aObject, QEvent *aEvent) override; + void keyPressEvent(QKeyEvent *aEvent) override; + +private: + void stateChanged(int aState); + void onSearch(QString const &aSearchString) const; + void itemClicked(int aIndex) const; + + QScopedPointer mListWidget; +}; + +#endif // MULTISELECTCOMBOBOX_H diff --git a/pdfrankingprinter.cpp b/pdfrankingprinter.cpp index 7e6bf42..9100cc4 100644 --- a/pdfrankingprinter.cpp +++ b/pdfrankingprinter.cpp @@ -22,6 +22,7 @@ #include "pdfrankingprinter.hpp" #include "chronoracedata.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" static constexpr qreal RANKING_TOP_MARGIN = 10.0; static constexpr qreal RANKING_LEFT_MARGIN = 10.0; @@ -51,7 +52,8 @@ void PDFRankingPrinter::init(QString *outFileName, QString const &title) Q_ASSERT(!painter.isActive()); if (outFileName == Q_NULLPTR) { - throw(ChronoRaceException(tr("Error: no file name supplied"))); + emit error(tr("Error: no file name supplied")); + return; } // append the .pdf extension if missing @@ -78,21 +80,27 @@ void PDFRankingPrinter::init(QString *outFileName, QString const &title) currentPage = 0; if (!painter.begin(w)) { - throw(ChronoRaceException(tr("Error: cannot start drawing"))); + emit error(tr("Error: cannot start drawing")); + return; } } -void PDFRankingPrinter::printStartList(QList const &startList) +void PDFRankingPrinter::printStartList(QList const &startList) { if (!painter.isActive()) { - throw(ChronoRaceException(tr("Error: drawing attempt on inactive painter"))); + emit error(tr("Error: drawing attempt on inactive painter")); + return; } + Category const *category = Q_NULLPTR; + QRectF writeRect; ChronoRaceData const *raceInfo = getRaceInfo(); QTime startTime = raceInfo->getStartTime(); + QStringList clubAndTeam { }; + auto pdfWriter = static_cast(painter.device()); // Split the list into pages @@ -126,6 +134,13 @@ void PDFRankingPrinter::printStartList(QList const &startList) continue; } + category = (*c)->getCategory(); + + clubAndTeam.clear(); + clubAndTeam.append((*c)->getClub()); + clubAndTeam.append((*c)->getTeam()); + clubAndTeam.removeAll(""); + // Move down writeRect.translate(0.0, toVdots(4.0)); writeRect.setLeft(toHdots(0.0)); @@ -154,7 +169,7 @@ void PDFRankingPrinter::printStartList(QList const &startList) painter.setFont(rnkFont); writeRect.translate(toHdots(60.0), 0.0); writeRect.setWidth(toHdots(45.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, (*c)->getClub() + " " + (*c)->getTeam()); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, clubAndTeam.join(" - ")); // Year writeRect.translate(toHdots(45.0), 0.0); writeRect.setWidth(toHdots(9.0)); @@ -162,11 +177,11 @@ void PDFRankingPrinter::printStartList(QList const &startList) // Sex writeRect.translate(toHdots(9.0), 0.0); writeRect.setWidth(toHdots(6.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, Competitor::toSexString((*c)->getSex())); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, CRHelper::toSexString((*c)->getSex())); // Category writeRect.translate(toHdots(6.0), 0.0); writeRect.setWidth(toHdots(28.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, (*c)->getCategory()); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, category ? category->getFullDescription() : "---"); // Start Time / Leg offset = (*c)->getOffset(); painter.setFont(rnkFontBold); @@ -174,10 +189,10 @@ void PDFRankingPrinter::printStartList(QList const &startList) writeRect.setWidth(toHdots(27.0)); if (offset >= 0) { offset += (3600 * startTime.hour()) + (60 * startTime.minute()) + startTime.second(); - painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignVCenter, Competitor::toOffsetString(offset)); + painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignVCenter, CRHelper::toOffsetString(offset)); } else if (CRLoader::getStartListLegs() == 1) { offset = (3600 * startTime.hour()) + (60 * startTime.minute()) + startTime.second(); - painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignVCenter, Competitor::toOffsetString(offset)); + painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignVCenter, CRHelper::toOffsetString(offset)); } else { painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignVCenter, tr("Leg %n", "", qAbs(offset))); } @@ -185,16 +200,18 @@ void PDFRankingPrinter::printStartList(QList const &startList) } } -void PDFRankingPrinter::printRanking(const Category &category, QList const &ranking) +void PDFRankingPrinter::printRanking(Ranking const &categories, QList const &ranking) { if (!painter.isActive()) { - throw(ChronoRaceException(tr("Error: drawing attempt on inactive painter"))); + emit error(tr("Error: drawing attempt on inactive painter")); + return; } uint individualLegs = CRLoader::getStartListLegs(); if (individualLegs == 0) { - throw(ChronoRaceException(tr("Error: cannot generate results for 0 legs"))); + emit error(tr("Error: cannot generate results for 0 legs")); + return; } uint referenceTime = 0u; @@ -204,17 +221,20 @@ void PDFRankingPrinter::printRanking(const Category &category, QList> pages = splitIndividualRanking(ranking); + + // Initialize the reference time + if (!pages.empty() && !pages.at(0).empty()) + referenceTime = pages.at(0).at(0)->getTotalTime(); + int i = 1; int p = 1; auto pp = static_cast(pages.size()); for (auto page = pages.constBegin(); page < pages.constEnd(); page++, p++) { - if (currentPage++) // this is not the first loop, add a new pages + if (currentPage++) // this is not the first loop, add a new page pdfWriter->newPage(); - else if (!page->empty()) // init the reference time - referenceTime = page->at(0)->getTotalTime(); - drawTemplatePortrait(tr("%1 Results").arg(category.getFullDescription()), p, pp); + drawTemplatePortrait(tr("%1 Results").arg(categories.getFullDescription()), p, pp); // Prepare fonts rnkFont.setPointSize(7); @@ -237,16 +257,18 @@ void PDFRankingPrinter::printRanking(const Category &category, QList const &ranking) +void PDFRankingPrinter::printRanking(Ranking const &categories, QList const &ranking) { if (!painter.isActive()) { - throw(ChronoRaceException(tr("Error: drawing attempt on inactive painter"))); + emit error(tr("Error: drawing attempt on inactive painter")); + return; } uint teamLegs = CRLoader::getStartListLegs(); if (teamLegs == 0) { - throw(ChronoRaceException(tr("Error: cannot generate results for 0 legs"))); + emit error(tr("Error: cannot generate results for 0 legs")); + return; } QRectF writeRect; @@ -255,7 +277,7 @@ void PDFRankingPrinter::printRanking(const Category &category, QList> pages = splitTeamRanking(ranking); - int i = 1; + int j = 1; int p = 1; auto pp = static_cast(pages.size()); for (auto page = pages.constBegin(); page < pages.constEnd(); page++, p++) { @@ -263,7 +285,7 @@ void PDFRankingPrinter::printRanking(const Category &category, QListnewPage(); - drawTemplatePortrait(tr("%1 Results").arg(category.getFullDescription()), p, pp); + drawTemplatePortrait(tr("%1 Results").arg(categories.getFullDescription()), p, pp); // Prepare fonts rnkFont.setPointSize(7); @@ -280,9 +302,9 @@ void PDFRankingPrinter::printRanking(const Category &category, QList> PDFRankingPrinter::splitStartList(QList const &startList) const +QList> PDFRankingPrinter::splitStartList(QList const &startList) const { return (CRLoader::getStartListLegs() > 1) ? splitStartListMultiLeg(startList) : splitStartListSingleLeg(startList); } -QList> PDFRankingPrinter::splitStartListSingleLeg(QList const &startList) const +QList> PDFRankingPrinter::splitStartListSingleLeg(QList const &startList) const { QList> pages; @@ -364,13 +386,13 @@ QList> PDFRankingPrinter::splitStartListSingleLeg(QLis availableEntriesOnPage = RANKING_PORTRAIT_SECOND_PAGE_LIMIT; } availableEntriesOnPage--; - pages.last().append(&*c); + pages.last().append(*c); } return pages; } -QList> PDFRankingPrinter::splitStartListMultiLeg(QList const &startList) const +QList> PDFRankingPrinter::splitStartListMultiLeg(QList const &startList) const { QList> pages; int offset; @@ -379,9 +401,9 @@ QList> PDFRankingPrinter::splitStartListMultiLeg(QList int availableEntriesOnPage = RANKING_PORTRAIT_FIRST_PAGE_LIMIT; auto c = startList.constBegin(); - int prevOffset = (c < startList.constEnd()) ? (*c).getOffset() : 0; + int prevOffset = (c < startList.constEnd()) ? (*c)->getOffset() : 0; while (c < startList.constEnd()) { - if ((offset = (*c).getOffset()) < 0) { + if ((offset = (*c)->getOffset()) < 0) { if (prevOffset != offset) { availableEntriesOnPage--; pages.last().append(Q_NULLPTR); // separator @@ -396,7 +418,7 @@ QList> PDFRankingPrinter::splitStartListMultiLeg(QList } availableEntriesOnPage--; - pages.last().append(&*c); + pages.last().append(*c); c++; } @@ -552,7 +574,7 @@ void PDFRankingPrinter::printHeaderSingleLeg(QRectF &writeRect, int page, Rankin } else { // individual or team single painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignBottom, tr("Time")); } - // Time diffeence (individual ranking only) + // Time difference (individual ranking only) if (type == RankingType::INDIVIDUAL_SINGLE) { writeRect.translate(toHdots(12.0), 0.0); writeRect.setWidth(toHdots(15.0)); @@ -560,7 +582,7 @@ void PDFRankingPrinter::printHeaderSingleLeg(QRectF &writeRect, int page, Rankin } break; default: - throw(ChronoRaceException(tr("Error: ranking type not allowed"))); + emit error(tr("Error: ranking type not allowed")); break; } } @@ -629,7 +651,7 @@ void PDFRankingPrinter::printHeaderMultiLeg(QRectF &writeRect, int page, Ranking painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignBottom, tr("Leg Time (and position)")); break; default: - throw(ChronoRaceException(tr("Error: ranking type not allowed"))); + emit error(tr("Error: ranking type not allowed")); break; } } @@ -638,6 +660,8 @@ void PDFRankingPrinter::printEntrySingleLeg(QRectF &writeRect, ClassEntry const { static Position position; + Category const *category = c->getCategory(0); + QString currTime = c->getTotalTime(CRLoader::Format::PDF); switch (type) { @@ -685,7 +709,7 @@ void PDFRankingPrinter::printEntrySingleLeg(QRectF &writeRect, ClassEntry const painter.setFont(rnkFont); writeRect.translate(toHdots(60.0), 0.0); writeRect.setWidth(toHdots(45.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, c->getClub() + " " + c->getTeam()); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, c->getClubsAndTeam()); // Year writeRect.translate(toHdots(45.0), 0.0); writeRect.setWidth(toHdots(9.0)); @@ -693,17 +717,17 @@ void PDFRankingPrinter::printEntrySingleLeg(QRectF &writeRect, ClassEntry const // Sex writeRect.translate(toHdots(9.0), 0.0); writeRect.setWidth(toHdots(6.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, Competitor::toSexString(c->getSex())); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, CRHelper::toSexString(c->getSex())); // Category writeRect.translate(toHdots(6.0), 0.0); writeRect.setWidth(toHdots((type == RankingType::INDIVIDUAL_SINGLE) ? 28.0 : 23.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, c->getCategory(0)); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, category ? category->getFullDescription() : "---"); // Time painter.setFont(rnkFontBold); writeRect.translate(toHdots((type == RankingType::INDIVIDUAL_SINGLE) ? 28.0 : 23.0), 0.0); writeRect.setWidth(toHdots((type == RankingType::INDIVIDUAL_SINGLE) ? 12.0 : 27.0)); painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignVCenter, currTime); - // Time diffeence + // Time difference if (type == RankingType::INDIVIDUAL_SINGLE) { painter.setFont(rnkFont); writeRect.translate(toHdots(12.0), 0.0); @@ -712,15 +736,20 @@ void PDFRankingPrinter::printEntrySingleLeg(QRectF &writeRect, ClassEntry const } break; default: - throw(ChronoRaceException(tr("Error: ranking type not allowed"))); + emit error(tr("Error: ranking type not allowed")); break; } } void PDFRankingPrinter::printPageSingleLeg(QRectF &writeRect, QList const &page, int &posIndex, uint referenceTime) { - for (auto c = page.constBegin(); c < page.constEnd(); c++) - printEntrySingleLeg(writeRect, *c, posIndex, -1, referenceTime, RankingType::INDIVIDUAL_SINGLE); + for (auto c = page.constBegin(); c < page.constEnd(); c++) { + try { + printEntrySingleLeg(writeRect, *c, posIndex, -1, referenceTime, RankingType::INDIVIDUAL_SINGLE); + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); + } + } } void PDFRankingPrinter::printPageSingleLeg(QRectF &writeRect, QList const &page, int &posIndex) @@ -743,7 +772,11 @@ void PDFRankingPrinter::printPageSingleLeg(QRectF &writeRect, QListgetClassEntry(j), posIndex, j + 1, 0, RankingType::TEAM_SINGLE); + try { + printEntrySingleLeg(writeRect, (*t)->getClassEntry(j), posIndex, j + 1, 0, RankingType::TEAM_SINGLE); + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); + } } } @@ -753,6 +786,8 @@ void PDFRankingPrinter::printEntryMultiLeg(QRectF &writeRect, ClassEntry const * auto entriesPerBlock = static_cast(CRLoader::getStartListLegs() + 1); + Category const *category = r->getCategory(); + QString currTime = r->getTotalTime(CRLoader::Format::PDF); switch (type) { @@ -797,18 +832,18 @@ void PDFRankingPrinter::printEntryMultiLeg(QRectF &writeRect, ClassEntry const * painter.setFont(rnkFontBold); writeRect.translate(toHdots(8.0), 0.0); writeRect.setWidth(toHdots(66.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, r->getClub() + " " + r->getTeam()); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, r->getClubsAndTeam()); // Category painter.setFont(rnkFont); writeRect.translate(toHdots(66.0), 0.0); writeRect.setWidth(toHdots(82.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, r->getCategory()); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, category ? category->getFullDescription() : "---"); // Time painter.setFont(rnkFontBold); writeRect.translate(toHdots(82.0), 0.0); writeRect.setWidth(toHdots((type == RankingType::INDIVIDUAL_MULTI) ? 12.0 : 22.0)); painter.drawText(writeRect.toRect(), Qt::AlignRight | Qt::AlignVCenter, currTime); - // Time diffeence + // Time difference if (type == RankingType::INDIVIDUAL_MULTI) { painter.setFont(rnkFont); writeRect.translate(toHdots(12.0), 0.0); @@ -837,7 +872,7 @@ void PDFRankingPrinter::printEntryMultiLeg(QRectF &writeRect, ClassEntry const * // Sex writeRect.translate(toHdots(9.0), 0.0); writeRect.setWidth(toHdots(6.0)); - painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, Competitor::toSexString(r->getSex(j))); + painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignVCenter, CRHelper::toSexString(r->getSex(j))); // Time in Leg writeRect.translate(toHdots(6.0), 0.0); writeRect.setWidth(toHdots(61.0)); @@ -849,15 +884,20 @@ void PDFRankingPrinter::printEntryMultiLeg(QRectF &writeRect, ClassEntry const * } break; default: - throw(ChronoRaceException(tr("Error: ranking type not allowed"))); + emit error(tr("Error: ranking type not allowed")); break; } } void PDFRankingPrinter::printPageMultiLeg(QRectF &writeRect, QList const &page, int &posIndex, uint referenceTime) { - for (auto c = page.constBegin(); c < page.constEnd(); c++) - printEntryMultiLeg(writeRect, *c, posIndex, -1, referenceTime, RankingType::INDIVIDUAL_MULTI); + for (auto c = page.constBegin(); c < page.constEnd(); c++) { + try { + printEntryMultiLeg(writeRect, *c, posIndex, -1, referenceTime, RankingType::INDIVIDUAL_MULTI); + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); + } + } } void PDFRankingPrinter::printPageMultiLeg(QRectF &writeRect, QList const &page, int &posIndex) @@ -880,7 +920,11 @@ void PDFRankingPrinter::printPageMultiLeg(QRectF &writeRect, QListgetClassEntry(j), posIndex, j + 1, 0, RankingType::TEAM_MULTI); + try { + printEntryMultiLeg(writeRect, (*t)->getClassEntry(j), posIndex, j + 1, 0, RankingType::TEAM_MULTI); + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); + } } } diff --git a/pdfrankingprinter.hpp b/pdfrankingprinter.hpp index 6e69cc1..2badda6 100644 --- a/pdfrankingprinter.hpp +++ b/pdfrankingprinter.hpp @@ -37,9 +37,9 @@ class PDFRankingPrinter final : public RankingPrinter void init(QString *outFileName, QString const &title) override; - void printStartList(QList const &startList) override; - void printRanking(Category const &category, QList const &ranking) override; - void printRanking(Category const &category, QList const &ranking) override; + void printStartList(QList const &startList) override; + void printRanking(Ranking const &categories, QList const &ranking) override; + void printRanking(Ranking const &categories, QList const &ranking) override; bool finalize() override; @@ -66,9 +66,9 @@ class PDFRankingPrinter final : public RankingPrinter qreal toVdots(qreal mm) const; void fitRectToLogo(QRectF &rect, QPixmap const &pixmap) const; - QList> splitStartList(QList const &startList) const; - QList> splitStartListSingleLeg(QList const &startList) const; - QList> splitStartListMultiLeg(QList const &startList) const; + QList> splitStartList(QList const &startList) const; + QList> splitStartListSingleLeg(QList const &startList) const; + QList> splitStartListMultiLeg(QList const &startList) const; QList> splitIndividualRanking(QList const ranking) const; QList> splitIndividualRankingSingleLeg(QList const ranking) const; QList> splitIndividualRankingMultiLeg(QList const ranking) const; diff --git a/ranking.cpp b/ranking.cpp new file mode 100644 index 0000000..fd2afbc --- /dev/null +++ b/ranking.cpp @@ -0,0 +1,189 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#include "ranking.hpp" +#include "lbcrexception.hpp" + +Ranking::Field RankingSorter::sortingField = Ranking::Field::RTF_FIRST; +Qt::SortOrder RankingSorter::sortingOrder = Qt::AscendingOrder; + +Ranking::Ranking(QString const &team) +{ + if (team.length() != 1) { + throw(ChronoRaceException(tr("Illegal ranking type - expected 'I' or 'T' - found %1").arg(team))); + } else { + this->team = (team.compare("T", Qt::CaseInsensitive) == 0); + } +} + +QDataStream &operator<<(QDataStream &out, Ranking const &ranking) +{ + out << ranking.fullDescription + << ranking.shortDescription + << qint32(ranking.team) + << ranking.categories; + + return out; +} + +QDataStream &operator>>(QDataStream &in, Ranking &ranking) +{ + qint32 team32; + + in >> ranking.fullDescription + >> ranking.shortDescription + >> team32 + >> ranking.categories; + + ranking.team = (bool) team32; + + return in; +} + +QString const &Ranking::getFullDescription() const +{ + return fullDescription; +} + +void Ranking::setFullDescription(QString const &newFullDescription) +{ + this->fullDescription = newFullDescription; +} + +QString const &Ranking::getShortDescription() const +{ + return shortDescription; +} + +void Ranking::setShortDescription(QString const &newShortDescription) +{ + this->shortDescription = newShortDescription; +} + +bool Ranking::isTeam() const +{ + return team; +} + +void Ranking::setTeam(bool newTeam) +{ + this->team = newTeam; +} + +QStringList const &Ranking::getCategories() const +{ + return categories; +} + +void Ranking::setCategories(QStringList const &newCategories) +{ + this->categories = newCategories; +} + +void Ranking::parseCategories() +{ + if (this->categories.count() == 1) { + + QStringList list { this->categories[0].split('+') }; + + this->categories.clear(); + this->categories.append(list); + } +} + +bool Ranking::isValid() const +{ + return (!fullDescription.isEmpty() && !shortDescription.isEmpty()); +} + +bool Ranking::includes(Category const *category) const +{ + return ((category != Q_NULLPTR) && (this->categories.indexOf(category->getShortDescription()) >= 0)); +} + +bool Ranking::operator< (Ranking const &rhs) const +{ + return (!this->isTeam() && rhs.isTeam()); +} + +bool Ranking::operator> (Ranking const &rhs) const +{ + return (this->isTeam() && !rhs.isTeam()); +} + +bool Ranking::operator<=(Ranking const &rhs) const +{ + return !(*this > rhs); +} + +bool Ranking::operator>=(Ranking const &rhs) const +{ + return !(*this < rhs); +} + +bool RankingSorter::operator() (Ranking const &lhs, Ranking const &rhs) const +{ + switch(sortingField) { + case Ranking::Field::RTF_FULL_DESCR: + return (sortingOrder == Qt::DescendingOrder) ? (lhs.getFullDescription() > rhs.getFullDescription()) : (lhs.getFullDescription() < rhs.getFullDescription()); + case Ranking::Field::RTF_SHORT_DESCR: + return (sortingOrder == Qt::DescendingOrder) ? (lhs.getShortDescription() > rhs.getShortDescription()) : (lhs.getShortDescription() < rhs.getShortDescription()); + case Ranking::Field::RTF_TEAM: + [[fallthrough]]; + case Ranking::Field::RTF_CATEGORIES: + [[fallthrough]]; + default: + return (sortingOrder == Qt::DescendingOrder) ? (lhs > rhs) : (lhs < rhs); + } + + return false; +} + +Qt::SortOrder RankingSorter::getSortingOrder() +{ + return sortingOrder; +} + +void RankingSorter::setSortingOrder(Qt::SortOrder const &value) +{ + sortingOrder = value; +} + +Ranking::Field RankingSorter::getSortingField() +{ + return sortingField; +} + +void RankingSorter::setSortingField(Ranking::Field const &value) +{ + sortingField = value; +} + +Ranking::Field &operator++(Ranking::Field &field) +{ + field = static_cast(static_cast(field) + 1); + //NOSONAR if (field == Ranking::RTF_COUNT) + //NOSONAR field = Ranking::RTF_FIRST; + return field; +} + +Ranking::Field operator++(Ranking::Field &field, int) +{ + Ranking::Field tmp = field; + ++field; + return tmp; +} diff --git a/ranking.hpp b/ranking.hpp new file mode 100644 index 0000000..355b4ff --- /dev/null +++ b/ranking.hpp @@ -0,0 +1,104 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#ifndef RANKING_H +#define RANKING_H + +#include +#include +#include + +#include "category.hpp" + +namespace ranking { +class Ranking; +class RankingSorter; +} + +class Ranking { + Q_DECLARE_TR_FUNCTIONS(Ranking) + +public: + enum class Type + { + INDIVIDUAL, + CLUB, + }; + + enum class Field + { + RTF_FIRST = 0, + RTF_FULL_DESCR = 0, + RTF_SHORT_DESCR = 1, + RTF_TEAM = 2, + RTF_CATEGORIES = 3, + RTF_LAST = 3, + RTF_COUNT = 4 + }; + +private: + QString fullDescription { "" }; + QString shortDescription { "" }; + bool team { false }; + QStringList categories { }; + +public: + Ranking() = default; + explicit Ranking(QString const &team); + + friend QDataStream &operator<<(QDataStream &out, Ranking const &ranking); + friend QDataStream &operator>>(QDataStream &in, Ranking &ranking); + + QString const &getFullDescription() const; + void setFullDescription(QString const &newFullDescription); + QString const &getShortDescription() const; + void setShortDescription(QString const &newShortDescription); + bool isTeam() const; + void setTeam(bool newTeam); + QStringList const &getCategories() const; + void setCategories(QStringList const &newCategories); + void parseCategories(); + + bool isValid() const; + + bool includes(Category const *category) const; + + bool operator< (Ranking const &rhs) const; + bool operator> (Ranking const &rhs) const; + bool operator<= (Ranking const &rhs) const; + bool operator>= (Ranking const &rhs) const; +}; + +Ranking::Field &operator++(Ranking::Field &field); +Ranking::Field operator++(Ranking::Field &field, int); + +class RankingSorter { + +private: + static Qt::SortOrder sortingOrder; + static Ranking::Field sortingField; + +public: + static Qt::SortOrder getSortingOrder(); + static void setSortingOrder(Qt::SortOrder const &value); + static Ranking::Field getSortingField(); + static void setSortingField(Ranking::Field const &value); + + bool operator() (Ranking const &lhs, Ranking const &rhs) const; +}; + +#endif // RANKING_H diff --git a/rankingcatsdelegate.cpp b/rankingcatsdelegate.cpp new file mode 100644 index 0000000..a31fd0c --- /dev/null +++ b/rankingcatsdelegate.cpp @@ -0,0 +1,77 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#include "rankingcatsdelegate.hpp" + +QWidget *RankingCategoriesDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + Q_ASSERT(categories); + + auto *comboBox = box.data(); + comboBox->setParent(parent); + + for (auto const &item : categories->getCategories()) { + comboBox->addItem(item.getFullDescription(), item.getShortDescription()); + } + + return comboBox; +} + +void RankingCategoriesDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const +{ + Q_UNUSED(editor) + Q_UNUSED(index) + + auto const *comboBox = box.data(); + comboBox->clear(); +} + +void RankingCategoriesDelegate::setEditorData(QWidget *editor, QModelIndex const &index) const +{ + // Get the value via index of the Model and put it into the ComboBox + auto const *comboBox = static_cast(editor); + comboBox->setCurrentText(index.model()->data(index, Qt::EditRole).toStringList()); +} + +void RankingCategoriesDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const +{ + auto const *comboBox = static_cast(editor); + model->setData(index, comboBox->currentText(), Qt::EditRole); +} + +QSize RankingCategoriesDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + return this->box.data()->sizeHint(); +} + +void RankingCategoriesDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const +{ + Q_UNUSED(index) + + editor->setGeometry(option.rect); +} + +void RankingCategoriesDelegate::setCategories(CategoriesModel const *newCategories) +{ + categories = newCategories; +} diff --git a/rankingcatsdelegate.hpp b/rankingcatsdelegate.hpp new file mode 100644 index 0000000..9a80099 --- /dev/null +++ b/rankingcatsdelegate.hpp @@ -0,0 +1,48 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#ifndef RANKINGCATEGORIESDELEGATE_HPP +#define RANKINGCATEGORIESDELEGATE_HPP + +#include +#include +#include + +#include "categoriesmodel.hpp" +#include "multiselectcombobox.hpp" + +class RankingCategoriesDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit RankingCategoriesDelegate(QObject *parent = Q_NULLPTR) : QStyledItemDelegate(parent) { } + + QWidget *createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const override; + void destroyEditor(QWidget *editor, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, QModelIndex const &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const override; + QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const override; + void updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const override; + + void setCategories(const CategoriesModel *newCategories); + +private: + CategoriesModel const *categories { Q_NULLPTR }; + QScopedPointer box { new MultiSelectComboBox }; +}; + +#endif // RANKINGCATEGORIESDELEGATE_HPP diff --git a/rankingprinter.hpp b/rankingprinter.hpp index 005723a..a17cf34 100644 --- a/rankingprinter.hpp +++ b/rankingprinter.hpp @@ -24,7 +24,7 @@ #include "crloader.hpp" #include "chronoracedata.hpp" -#include "category.hpp" +#include "ranking.hpp" #include "classentry.hpp" #include "teamclassentry.hpp" @@ -38,9 +38,9 @@ class RankingPrinter : public QObject virtual void init(QString *outFileName, QString const &title) = 0; - virtual void printStartList(QList const &startList) = 0; - virtual void printRanking(Category const &category, QList const &ranking) = 0; - virtual void printRanking(Category const &category, QList const &ranking) = 0; + virtual void printStartList(QList const &startList) = 0; + virtual void printRanking(Ranking const &categories, QList const &ranking) = 0; + virtual void printRanking(Ranking const &categories, QList const &ranking) = 0; virtual bool finalize() = 0; diff --git a/rankingsbuilder.cpp b/rankingsbuilder.cpp index b9e9d59..1d400d8 100644 --- a/rankingsbuilder.cpp +++ b/rankingsbuilder.cpp @@ -15,17 +15,13 @@ * along with this program. If not, see . * *****************************************************************************/ -#include - #include "crloader.hpp" #include "rankingsbuilder.hpp" -#include "rankingprinter.hpp" uint RankingsBuilder::loadData() { QStringList messages; - QVector timings = CRLoader::getTimings(); - QVector categories = CRLoader::getCategories(); + QList timings { CRLoader::getTimings() }; uint bib; uint leg; @@ -35,10 +31,13 @@ uint RankingsBuilder::loadData() startList.clear(); // reset and fill the start list - fillStartList(); + prepareStartList(); + + // sort timings + std::sort(timings.begin(), timings.end(), [&](Timing const &t1, Timing const &t2) { return (t1 < t2); } ); // compute individual general classifications (all included, sorted by bib) - for (auto timing : timings) { + for (auto const &timing : timings) { bib = timing.getBib(); leg = timing.getLeg(); @@ -66,9 +65,6 @@ uint RankingsBuilder::loadData() if (!comp) { emit error(tr("Bib %1 not inserted in results; check for possible duplicated entries").arg(bib)); continue; - } else { - // Set the category for the competitor (if any) - setCompetitorCategory(categories, comp); } if (classEntryIt != rankingByBib.end()) { @@ -78,9 +74,7 @@ uint RankingsBuilder::loadData() } } - for (auto const &message : messages) - emit error(tr("Warning: %1").arg(message)); - messages.clear(); + emitMessages(messages); if (startList.size() != timings.size()) emit error(tr("Warning: the number of timings (%1) is not match the expected (%2); check for possible missing or duplicated entries").arg(timings.size()).arg(startList.size())); @@ -89,60 +83,35 @@ uint RankingsBuilder::loadData() QList::const_iterator c; for (auto classEntry = rankingByBib.begin(); classEntry != rankingByBib.end(); classEntry++) { c = rankingByTime.constBegin(); + messages += classEntry->setCategory(); while ((c != rankingByTime.constEnd()) && (*(*c) < classEntry.value())) ++c; rankingByTime.insert(c, &classEntry.value()); } + emitMessages(messages); + return static_cast(rankingByBib.size()); } -QList &RankingsBuilder::fillRanking(QList &ranking, Category const &category) const +QList &RankingsBuilder::fillRanking(QList &ranking, Ranking const *categories) const { - Q_ASSERT(!category.isTeam()); + Q_ASSERT(!categories->isTeam()); QList tmpRanking; - for (auto classEntry : rankingByTime) { - - // Sex - if ((category.getSex() != Competitor::Sex::UNDEFINED) && - (category.getSex() != classEntry->getSex())) { - continue; - } - - // To Year - if (category.getToYear() && - (category.getToYear() < classEntry->getToYear())) { - continue; - } + for (auto &classEntry : rankingByTime) { - // From Year - if (category.getFromYear() && - (category.getFromYear() > classEntry->getFromYear())) { + // exclude not included categories + if (!categories->includes(classEntry->getCategory())) { continue; } - classEntry->setCategory(category.getFullDescription()); - tmpRanking.append(classEntry); } // do the sorting of the single leg times - uint i; - uint legs = CRLoader::getStartListLegs(); PositionNumber position; - for (uint legIdx = 0u; legIdx < legs; legIdx++) { - QMultiMap sortedLegClassification; - for (auto classEntry : tmpRanking) { - if (classEntry->countEntries() == legs) - sortedLegClassification.insert(classEntry->getTimeValue(legIdx), classEntry); - } - i = 0; - for (auto classEntry : sortedLegClassification) { - i++; - classEntry->setLegRanking(legIdx, position.getCurrentPosition(i, classEntry->getTimeValue(legIdx))); - } - } + sortLegTimes(tmpRanking, CRLoader::getStartListLegs(), position); ranking.clear(); ranking.reserve(tmpRanking.size()); @@ -153,45 +122,34 @@ QList &RankingsBuilder::fillRanking(QList &RankingsBuilder::fillRanking(QList &ranking, Category const &category) +QList &RankingsBuilder::fillRanking(QList &ranking, Ranking const *categories) { - Q_ASSERT(category.isTeam()); + Q_ASSERT(categories->isTeam()); rankingsByTeam.emplaceBack(); auto &rankingByTeam = rankingsByTeam.last(); - for (auto classEntry : rankingByTime) { + QString club; + + for (auto &classEntry : rankingByTime) { // exclude DNS and DNF if (classEntry->isDns() || classEntry->isDnf()) { continue; } - // exclude competitors without club - if (classEntry->getClub().isEmpty()) { - continue; - } - - // Sex - if ((category.getSex() != Competitor::Sex::UNDEFINED) && - (category.getSex() != classEntry->getSex())) { - continue; - } + club = classEntry->getClub(); - // To Year - if (category.getToYear() && - (category.getToYear() < classEntry->getToYear())) { + // exclude competitors without club + if (club.isEmpty()) { continue; } - // From Year - if (category.getFromYear() && - (category.getFromYear() > classEntry->getFromYear())) { + // exclude not included categories + if (!categories->includes(classEntry->getCategory())) { continue; } - QString const &club = classEntry->getClub(); - QMap::iterator const teamRankingIt = rankingByTeam.find(club); if (teamRankingIt == rankingByTeam.end()) { rankingByTeam.insert(club, TeamClassEntry()).value().setClassEntry(classEntry); @@ -202,36 +160,99 @@ QList &RankingsBuilder::fillRanking(QList sortedTeamRanking; + sortTeamRanking(rankingByTeam, sortedTeamRanking); + + // copy and return the team rankings + ranking.clear(); + ranking.reserve(sortedTeamRanking.size()); + for (TeamClassEntry const *teamClassEntry : sortedTeamRanking) { + ranking.append(teamClassEntry); + } + + return ranking; +} + +void RankingsBuilder::sortTeamRanking(QMap &rankingByTeam, QList &sortedTeamRanking) const +{ + uint legs = CRLoader::getStartListLegs(); + PositionNumber position; QList::const_iterator t; for (auto &teamClassEntry : rankingByTeam) { + // do the sorting of the single leg times + sortLegTimes(teamClassEntry, legs, position); + + // actually sort the team ranking t = sortedTeamRanking.constBegin(); while ((t != sortedTeamRanking.constEnd()) && (*(*t) < teamClassEntry)) ++t; sortedTeamRanking.insert(t, &teamClassEntry); } +} - // copy and return the team rankings - ranking.clear(); - ranking.reserve(sortedTeamRanking.size()); - for (TeamClassEntry const *teamClassEntry : sortedTeamRanking) { - ranking.append(teamClassEntry); +void RankingsBuilder::sortLegTimes(QList const &ranking, uint legs, PositionNumber &position) const +{ + uint i; + for (uint legIdx = 0u; legIdx < legs; legIdx++) { + QMultiMap sortedLegClassification; + for (auto classEntry : ranking) { + if (classEntry->countEntries() == legs) + sortedLegClassification.insert(classEntry->getTimeValue(legIdx), classEntry); + } + i = 0; + for (auto classEntry : sortedLegClassification) { + i++; + classEntry->setLegRanking(legIdx, position.getCurrentPosition(i, classEntry->getTimeValue(legIdx))); + } } +} - return ranking; +void RankingsBuilder::sortLegTimes(TeamClassEntry const &teamClassEntry, uint legs, PositionNumber &position) const +{ + uint i; + int count; + for (uint legIdx = 0u; legIdx < legs; legIdx++) { + QMultiMap sortedLegClassification; + count = teamClassEntry.getClassEntryCount(); + for (int j = 0; j < count; j++) { + auto *classEntry = teamClassEntry.getClassEntry(j); + if (classEntry->countEntries() == legs) + sortedLegClassification.insert(classEntry->getTimeValue(legIdx), classEntry); + } + i = 0; + for (auto classEntry : sortedLegClassification) { + i++; + classEntry->setLegRanking(legIdx, position.getCurrentPosition(i, classEntry->getTimeValue(legIdx))); + } + } } -QList RankingsBuilder::makeStartList() +QList RankingsBuilder::fillStartList() const { - // compute and sort the start list - QList sortedStartList = CRLoader::getStartList(); - CompetitorSorter::setSortingField(Competitor::Field::CMF_BIB); - CompetitorSorter::setSortingOrder(Qt::AscendingOrder); - std::stable_sort(sortedStartList.begin(), sortedStartList.end(), CompetitorSorter()); + // create the start list container + QList sortedStartList; + + // fill (sorted) the start list + Competitor const *comp; + QMutableListIterator l { sortedStartList }; + QMultiMapIterator i { startList }; + while (i.hasNext()) { + comp = &i.next().value(); + + l.toFront(); + while (l.hasNext()) { + if (*comp < *l.next()) { + l.previous(); + break; + } + } + + l.insert(comp); + } return sortedStartList; } -void RankingsBuilder::fillStartList() +void RankingsBuilder::prepareStartList() { uint bib; uint mask; @@ -239,43 +260,47 @@ void RankingsBuilder::fillStartList() int offset; + QList const &categories = CRLoader::getCategories(); + rankingByTime.clear(); - QMultiMap::const_iterator element; - for (auto comp : CRLoader::getStartList()) { + for (auto &comp : CRLoader::getStartList()) { bib = comp.getBib(); - element = startList.constFind(bib); - if (element != startList.constEnd()) { - // check if there is a leg set for the competitor - // otherwise set it automatically - offset = comp.getOffset(); - comp.setLeg(static_cast((offset < 0) ? qAbs(offset) : (startList.count(bib) + 1))); - - // set a bit in the leg count array - mask = 0x1 << comp.getLeg(); - if (legCount.contains(bib)) - legCount[bib] |= mask; - else - legCount.insert(bib, mask); - } + + // check if there is a leg set for the competitor + // otherwise set it automatically + offset = comp.getOffset(); + comp.setLeg(static_cast((offset < 0) ? qAbs(offset) : (startList.count(bib) + 1))); + + // set a bit in the leg count array + mask = 0x1 << (comp.getLeg() - 1); + if (legCount.contains(bib)) + legCount[bib] |= mask; + else + legCount.insert(bib, mask); + + // Set the category for each competitor + comp.setCategories(categories); + startList.insert(bib, comp); } + uint prevBib = 0; uint prevMask = 0; for (QMap::const_key_value_iterator i = legCount.constKeyValueBegin(); i != legCount.constKeyValueEnd(); i++) { mask = i->second; - if (prevMask && (mask != prevMask)) { - emit error(tr("Warning: missing or extra legs for bib %1").arg(i->first)); + if (prevBib && (mask != prevMask)) { + emit error(tr("Warning: missing or extra legs for bib %1 or %2").arg(prevBib).arg(i->first)); } + prevBib = i->first; prevMask = mask; } } -void RankingsBuilder::setCompetitorCategory(QVector const &categories, Competitor *competitor) const +void RankingsBuilder::emitMessages(QStringList &messages) { - for (auto const &category : categories) { - if (category.includes(competitor)) { - competitor->setCategory(category.getFullDescription()); - break; - } + for (auto const &message : messages) { + if (message.length()) + emit error(tr("Warning: %1").arg(message)); } + messages.clear(); } diff --git a/rankingsbuilder.hpp b/rankingsbuilder.hpp index d386b80..e5a104e 100644 --- a/rankingsbuilder.hpp +++ b/rankingsbuilder.hpp @@ -21,11 +21,12 @@ #include #include #include +#include #include #include "classentry.hpp" #include "teamclassentry.hpp" -#include "category.hpp" +#include "rankingprinter.hpp" class RankingsBuilder : public QObject { @@ -34,14 +35,16 @@ class RankingsBuilder : public QObject public: uint loadData(); - QList &fillRanking(QList &ranking, Category const &category) const; - QList &fillRanking(QList &ranking, Category const &category); - - static QList makeStartList(); + QList &fillRanking(QList &ranking, Ranking const *categories) const; + QList &fillRanking(QList &ranking, Ranking const *categories); + QList fillStartList() const; private: - void fillStartList(); - void setCompetitorCategory(QVector const &categories, Competitor *competitor) const; + void sortTeamRanking(QMap &rankingByTeam, QList &sortedTeamRanking) const; + void sortLegTimes(QList const &ranking, uint legs, PositionNumber &position) const; + void sortLegTimes(TeamClassEntry const &teamClassEntry, uint legs, PositionNumber &position) const; + void prepareStartList(); + void emitMessages(QStringList &messages); QMultiMap startList { }; QMap rankingByBib { }; diff --git a/rankingsmodel.cpp b/rankingsmodel.cpp new file mode 100644 index 0000000..da486a3 --- /dev/null +++ b/rankingsmodel.cpp @@ -0,0 +1,230 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#include "lbchronorace.hpp" +#include "rankingsmodel.hpp" + +QDataStream &operator<<(QDataStream &out, RankingsModel const &data) +{ + out << data.rankings; + + return out; +} + +QDataStream &operator>>(QDataStream &in, RankingsModel &data) +{ + in >> data.rankings; + + return in; +} + +void RankingsModel::refreshCounters(int r) +{ + Q_UNUSED(r) +} + +int RankingsModel::rowCount(QModelIndex const &parent) const +{ + + Q_UNUSED(parent) + + return static_cast(rankings.count()); +} + +int RankingsModel::columnCount(QModelIndex const &parent) const +{ + + Q_UNUSED(parent) + + return static_cast(Ranking::Field::RTF_COUNT); +} + +QVariant RankingsModel::data(QModelIndex const &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() >= rankings.size()) + return QVariant(); + + if (role == Qt::DisplayRole) + switch (index.column()) { + case static_cast(Ranking::Field::RTF_FULL_DESCR): + return QVariant(rankings.at(index.row()).getFullDescription()); + case static_cast(Ranking::Field::RTF_SHORT_DESCR): + return QVariant(rankings.at(index.row()).getShortDescription()); + case static_cast(Ranking::Field::RTF_TEAM): + return QVariant(rankings.at(index.row()).isTeam() ? tr("T") : tr("I")); + case static_cast(Ranking::Field::RTF_CATEGORIES): + return QVariant(rankings.at(index.row()).getCategories().join('+')); + default: + return QVariant(); + } + else if (role == Qt::EditRole) + switch (index.column()) { + case static_cast(Ranking::Field::RTF_FULL_DESCR): + return QVariant(rankings.at(index.row()).getFullDescription()); + case static_cast(Ranking::Field::RTF_SHORT_DESCR): + return QVariant(rankings.at(index.row()).getShortDescription()); + case static_cast(Ranking::Field::RTF_TEAM): + return QVariant(rankings.at(index.row()).isTeam() ? tr("T") : tr("I")); + case static_cast(Ranking::Field::RTF_CATEGORIES): + return QVariant(rankings.at(index.row()).getCategories()); + default: + return QVariant(); + } + else if (role == Qt::ToolTipRole) + switch (index.column()) { + case static_cast(Ranking::Field::RTF_FULL_DESCR): + return QVariant(tr("Full ranking name")); + case static_cast(Ranking::Field::RTF_SHORT_DESCR): + return QVariant(tr("Short ranking name")); + case static_cast(Ranking::Field::RTF_TEAM): + return QVariant(tr("Individual/Relay (I) or Club (T)")); + case static_cast(Ranking::Field::RTF_CATEGORIES): + return QVariant(tr("The ranking will include all the categories listed here")); + default: + return QVariant(); + } + + return QVariant(); +} + +bool RankingsModel::setData(QModelIndex const &index, QVariant const &value, int role) +{ + bool retval = false; + + if (!index.isValid()) + return retval; + + if (role != Qt::EditRole) + return retval; + + if (value.toString().contains(LBChronoRace::csvFilter)) + return retval; + + switch (index.column()) { + case static_cast(Ranking::Field::RTF_FULL_DESCR): + rankings[index.row()].setFullDescription(value.toString().simplified()); + retval = true; + break; + case static_cast(Ranking::Field::RTF_SHORT_DESCR): + rankings[index.row()].setShortDescription(value.toString().simplified()); + retval = true; + break; + case static_cast(Ranking::Field::RTF_TEAM): + rankings[index.row()].setTeam(QString::compare(value.toString().trimmed(), "T", Qt::CaseInsensitive) == 0); + break; + case static_cast(Ranking::Field::RTF_CATEGORIES): + rankings[index.row()].setCategories(value.toStringList()); + break; + default: + break; + } + + if (retval) emit dataChanged(index, index); + + return retval; +} + +QVariant RankingsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Horizontal) + switch (section) { + case static_cast(Ranking::Field::RTF_FULL_DESCR): + return QString("%1").arg(tr("Ranking Full Name")); + case static_cast(Ranking::Field::RTF_SHORT_DESCR): + return QString("%1").arg(tr("Ranking Short Name")); + case static_cast(Ranking::Field::RTF_TEAM): + return QString("%1").arg(tr("Individual/Club")); + case static_cast(Ranking::Field::RTF_CATEGORIES): + return QString("%1").arg(tr("Categories")); + default: + return QString("%1").arg(section + 1); + } + else + return QString("%1").arg(section + 1); +} + +Qt::ItemFlags RankingsModel::flags(QModelIndex const &index) const +{ + if (!index.isValid()) + return Qt::ItemIsEnabled; + + return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; +} + +bool RankingsModel::insertRows(int position, int rows, QModelIndex const &parent) +{ + + Q_UNUSED(parent) + + beginInsertRows(QModelIndex(), position, position + rows - 1); + + for (int row = 0; row < rows; ++row) { + rankings.insert(position, Ranking()); + } + + endInsertRows(); + return true; +} + +bool RankingsModel::removeRows(int position, int rows, QModelIndex const &parent) +{ + + Q_UNUSED(parent) + + beginRemoveRows(QModelIndex(), position, position + rows - 1); + + for (int row = 0; row < rows; ++row) { + rankings.removeAt(position); + } + + endRemoveRows(); + return true; +} + +void RankingsModel::sort(int column, Qt::SortOrder order) +{ + + RankingSorter::setSortingField((Ranking::Field) column); + RankingSorter::setSortingOrder(order); + std::stable_sort(rankings.begin(), rankings.end(), RankingSorter()); + emit dataChanged(QModelIndex(), QModelIndex()); +} + +void RankingsModel::reset() +{ + beginResetModel(); + rankings.clear(); + endResetModel(); +} + +void RankingsModel::parseCategories() +{ + for (auto &ranking : rankings) { + ranking.parseCategories(); + } +} + +QList const &RankingsModel::getRankings() const +{ + return rankings; +} diff --git a/rankingsmodel.hpp b/rankingsmodel.hpp new file mode 100644 index 0000000..c03dba5 --- /dev/null +++ b/rankingsmodel.hpp @@ -0,0 +1,66 @@ +/***************************************************************************** + * Copyright (C) 2021 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#ifndef LBRANKINGSMODEL_H +#define LBRANKINGSMODEL_H + +#include +#include + +#include "crtablemodel.hpp" +#include "ranking.hpp" + +class RankingsModel : public CRTableModel +{ + Q_OBJECT + using CRTableModel::CRTableModel; + +public: + explicit RankingsModel(QObject *parent = Q_NULLPTR) : CRTableModel(parent) { }; + RankingsModel(QList const &rankingsList, QObject *parent = Q_NULLPTR) + : CRTableModel(parent), rankings(rankingsList) { }; + + friend QDataStream &operator<<(QDataStream &out, RankingsModel const &data); + friend QDataStream &operator>>(QDataStream &in, RankingsModel &data); + + int rowCount(QModelIndex const &parent = QModelIndex()) const override; + int columnCount(QModelIndex const &parent = QModelIndex()) const override; + QVariant data(QModelIndex const &index, int role) const override; + bool setData(QModelIndex const &index, QVariant const &value, int role = Qt::EditRole) override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(QModelIndex const &index) const override; + bool insertRows(int position, int rows, QModelIndex const &index = QModelIndex()) override; + bool removeRows(int position, int rows, QModelIndex const &index = QModelIndex()) override; + void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; + + void parseCategories(); + + QList const &getRankings() const; + + void reset(); + +public slots: + void refreshCounters(int r) override; + +private: + QList rankings; + +signals: + void error(QString const &message); +}; + +#endif // LBRANKINGSMODEL_H diff --git a/rankingswizard.cpp b/rankingswizard.cpp index 08dcfec..f2c4f2c 100644 --- a/rankingswizard.cpp +++ b/rankingswizard.cpp @@ -15,11 +15,9 @@ * along with this program. If not, see . * *****************************************************************************/ -#include #include #include "crloader.hpp" -#include "category.hpp" #include "lbcrexception.hpp" #include "rankingswizard.hpp" #include "rankingprinter.hpp" @@ -38,8 +36,19 @@ RankingsWizard::RankingsWizard(ChronoRaceData *data, QDir *path, RankingsWizardT setWizardStyle(ModernStyle); #endif - if (this->target == RankingsWizardTarget::Rankings) + QObject::connect(&rankingsBuilder, &RankingsBuilder::error, this, &RankingsWizard::storeErrorMessage); + + switch (this->target) { + case RankingsWizardTarget::Rankings: buildRankings(); + break; + case RankingsWizardTarget::StartList: + buildStartList(); + break; + default: + Q_UNREACHABLE(); + break; + } QObject::connect(&formatPage, &RankingsWizardFormat::error, this, &RankingsWizard::forwardErrorMessage); QObject::connect(&modePage, &RankingsWizardMode::error, this, &RankingsWizard::forwardErrorMessage); @@ -82,23 +91,26 @@ void RankingsWizard::setTarget(RankingsWizard::RankingsWizardTarget newTarget) target = newTarget; } +void RankingsWizard::buildStartList() +{ + // compute the startlist + rankingsBuilder.loadData(); +} + void RankingsWizard::buildRankings() { uint i = 0; - QVector categories = CRLoader::getCategories(); - auto const &builderErrorMessages = QObject::connect(&rankingsBuilder, &RankingsBuilder::error, this, &RankingsWizard::forwardErrorMessage); + QList const &rankings = CRLoader::getRankings(); // compute individual general classifications (all included, sorted by bib) numberOfCompetitors = rankingsBuilder.loadData(); - rankingsList.resize(categories.size()); + rankingsList.resize(rankings.count()); for (auto &rankingItem : rankingsList) { - rankingItem.category = &categories.at(i); + rankingItem.categories = &rankings.at(i); i++; } - - QObject::disconnect(builderErrorMessages); } void RankingsWizard::forwardInfoMessage(QString const &message) @@ -111,13 +123,20 @@ void RankingsWizard::forwardErrorMessage(QString const &message) emit error(message); } +void RankingsWizard::storeErrorMessage(QString const &message) +{ + if (!message.isEmpty()) + messages.append(message); +} + + void RankingsWizard::printStartList() { try { CRLoader::Format format = CRLoader::getFormat(); - // compute start list - QList startList = RankingsBuilder::makeStartList(); + // this call can be done only after the rankingsBuilder.loadData() call + QList startList = rankingsBuilder.fillStartList(); auto sWidth = static_cast(QString::number(startList.size()).size()); auto bWidth = static_cast(QString::number(CRLoader::getStartListBibMax()).size()); @@ -182,16 +201,16 @@ void RankingsWizard::printRankingsSingleFile() if (rankingItem.skip) continue; - if (rankingItem.category->isTeam()) { + if (rankingItem.categories->isTeam()) { // build the ranking - rankingsBuilder.fillRanking(rankingItem.teamRanking, *rankingItem.category).isEmpty(); + rankingsBuilder.fillRanking(rankingItem.teamRanking, rankingItem.categories); // print the team ranking - printer->printRanking(*rankingItem.category, rankingItem.teamRanking); + printer->printRanking(*rankingItem.categories, rankingItem.teamRanking); } else { // build the ranking - rankingsBuilder.fillRanking(rankingItem.ranking, *rankingItem.category).isEmpty(); + rankingsBuilder.fillRanking(rankingItem.ranking, rankingItem.categories); // print the individual ranking - printer->printRanking(*rankingItem.category, rankingItem.ranking); + printer->printRanking(*rankingItem.categories, rankingItem.ranking); } } @@ -242,24 +261,24 @@ void RankingsWizard::printRankingsMultiFile() if (rankingItem.skip) continue; - outFileBaseName = QDir(rankingsBasePath).filePath(QString("class%1_%2").arg(k, rWidth, 10, QChar('0')).arg(rankingItem.category->getShortDescription())); - printer->init(&outFileBaseName, raceData->getEvent() + " - " + tr("Results") + " - " + rankingItem.category->getFullDescription()); + outFileBaseName = QDir(rankingsBasePath).filePath(QString("class%1_%2").arg(k, rWidth, 10, QChar('0')).arg(rankingItem.categories->getShortDescription())); + printer->init(&outFileBaseName, raceData->getEvent() + " - " + tr("Results") + " - " + rankingItem.categories->getFullDescription()); - if (rankingItem.category->isTeam()) { + if (rankingItem.categories->isTeam()) { // build the ranking - rankingsBuilder.fillRanking(rankingItem.teamRanking, *rankingItem.category).isEmpty(); + rankingsBuilder.fillRanking(rankingItem.teamRanking, rankingItem.categories); // print the team ranking - printer->printRanking(*rankingItem.category, rankingItem.teamRanking); + printer->printRanking(*rankingItem.categories, rankingItem.teamRanking); } else { // build the ranking - rankingsBuilder.fillRanking(rankingItem.ranking, *rankingItem.category).isEmpty(); + rankingsBuilder.fillRanking(rankingItem.ranking, rankingItem.categories); // print the individual ranking - printer->printRanking(*rankingItem.category, rankingItem.ranking); + printer->printRanking(*rankingItem.categories, rankingItem.ranking); } if (printer->finalize()) { QFileInfo outFileInfo(outFileBaseName); - emit info(tr("Generated Results '%1': %2").arg(rankingItem.category->getFullDescription(), QDir::toNativeSeparators(outFileInfo.absoluteFilePath()))); + emit info(tr("Generated Results '%1': %2").arg(rankingItem.categories->getFullDescription(), QDir::toNativeSeparators(outFileInfo.absoluteFilePath()))); } } @@ -271,8 +290,15 @@ void RankingsWizard::printRankingsMultiFile() } } -void RankingsWizard::print([[maybe_unused]] bool checked) +void RankingsWizard::print(bool checked) { + Q_UNUSED(checked) + + for (auto const &message : messages) + if (!message.isEmpty()) + emit error(message); + messages.clear(); + switch (this->target) { case RankingsWizardTarget::StartList: printStartList(); diff --git a/rankingswizard.hpp b/rankingswizard.hpp index 0ec34d4..c1d0820 100644 --- a/rankingswizard.hpp +++ b/rankingswizard.hpp @@ -70,15 +70,19 @@ class RankingsWizard : public QWizard QList rankingsList { }; RankingsWizardTarget target { RankingsWizardTarget::Rankings }; + void buildStartList(); void buildRankings(); void printStartList(); void printRankingsSingleFile(); void printRankingsMultiFile(); + QStringList messages { }; + private slots: void forwardInfoMessage(QString const &message); void forwardErrorMessage(QString const &message); + void storeErrorMessage(QString const &message); void print(bool checked); signals: diff --git a/rankingswizardformat.cpp b/rankingswizardformat.cpp index deb599a..a863117 100644 --- a/rankingswizardformat.cpp +++ b/rankingswizardformat.cpp @@ -20,6 +20,7 @@ #include "crloader.hpp" #include "rankingswizard.hpp" #include "rankingswizardformat.hpp" +#include "crhelper.hpp" RankingsWizardFormat::RankingsWizardFormat(QWidget *parent) : QWizardPage(parent), @@ -30,15 +31,15 @@ RankingsWizardFormat::RankingsWizardFormat(QWidget *parent) : int encodingIdx; formatIdx = static_cast(CRLoader::getFormat()); - fileFormat.insertItem(static_cast(CRLoader::Format::PDF), CRLoader::formatToLabel(CRLoader::Format::PDF)); - fileFormat.insertItem(static_cast(CRLoader::Format::TEXT), CRLoader::formatToLabel(CRLoader::Format::TEXT)); - fileFormat.insertItem(static_cast(CRLoader::Format::CSV), CRLoader::formatToLabel(CRLoader::Format::CSV)); + fileFormat.insertItem(static_cast(CRLoader::Format::PDF), CRHelper::formatToLabel(CRLoader::Format::PDF)); + fileFormat.insertItem(static_cast(CRLoader::Format::TEXT), CRHelper::formatToLabel(CRLoader::Format::TEXT)); + fileFormat.insertItem(static_cast(CRLoader::Format::CSV), CRHelper::formatToLabel(CRLoader::Format::CSV)); fileFormat.setCurrentIndex(formatIdx); layout.addRow(new QLabel(tr("Format")), &fileFormat); encodingIdx = static_cast(CRLoader::getEncoding()); - fileEncoding.insertItem(static_cast(CRLoader::Encoding::UTF8), CRLoader::encodingToLabel(CRLoader::Encoding::UTF8)); - fileEncoding.insertItem(static_cast(CRLoader::Encoding::LATIN1), CRLoader::encodingToLabel(CRLoader::Encoding::LATIN1)); + fileEncoding.insertItem(static_cast(CRLoader::Encoding::UTF8), CRHelper::encodingToLabel(CRLoader::Encoding::UTF8)); + fileEncoding.insertItem(static_cast(CRLoader::Encoding::LATIN1), CRHelper::encodingToLabel(CRLoader::Encoding::LATIN1)); fileEncoding.setCurrentIndex(encodingIdx); layout.addRow(new QLabel(tr("Encoding")), &fileEncoding); diff --git a/rankingswizardselection.cpp b/rankingswizardselection.cpp index e906815..b18cc6d 100644 --- a/rankingswizardselection.cpp +++ b/rankingswizardselection.cpp @@ -27,14 +27,14 @@ RankingsWizardSelection::RankingsWizardSelection(QWidget *parent) : QListWidgetItem *item; uint i = 0; - QVector categories = CRLoader::getCategories(); + QList const &rankings = CRLoader::getRankings(); setTitle(tr("Rankings")); - setSubTitle(tr("You can exclude some categories from the generated rankings.")); + setSubTitle(tr("You can exclude some of the generated rankings.")); - for (auto const &category : categories) { - categoriesList.addItem(category.getFullDescription()); - item = categoriesList.item(i); + for (auto const &ranking : rankings) { + rankingsList.addItem(ranking.getFullDescription()); + item = rankingsList.item(i); flags = item->flags(); flags |= Qt::ItemIsUserCheckable; @@ -43,29 +43,29 @@ RankingsWizardSelection::RankingsWizardSelection(QWidget *parent) : i++; } - layout.addWidget(&categoriesList); + layout.addWidget(&rankingsList); setLayout(&layout); } void RankingsWizardSelection::initializePage() { - QList *rankingsList = (qobject_cast(wizard()))->getRankingsList(); + QList *rankings = (qobject_cast(wizard()))->getRankingsList(); - for (uint i = 0; i < rankingsList->size(); i++) { - categoriesList.item(i)->setCheckState((*rankingsList)[i].skip ? Qt::Unchecked : Qt::Checked); + for (uint i = 0; i < rankings->count(); i++) { + rankingsList.item(i)->setCheckState((*rankings)[i].skip ? Qt::Unchecked : Qt::Checked); } - QObject::connect(&categoriesList, &QListWidget::itemChanged, this, &RankingsWizardSelection::toggleSkipRanking); + QObject::connect(&rankingsList, &QListWidget::itemChanged, this, &RankingsWizardSelection::toggleSkipRanking); } void RankingsWizardSelection::cleanupPage() { - QObject::disconnect(&categoriesList, &QListWidget::itemChanged, this, &RankingsWizardSelection::toggleSkipRanking); + QObject::disconnect(&rankingsList, &QListWidget::itemChanged, this, &RankingsWizardSelection::toggleSkipRanking); } void RankingsWizardSelection::toggleSkipRanking(QListWidgetItem const *item) const { - (*(qobject_cast(wizard())->getRankingsList()))[categoriesList.row(item)].skip = (item->checkState() != Qt::Checked); + (*(qobject_cast(wizard())->getRankingsList()))[rankingsList.row(item)].skip = (item->checkState() != Qt::Checked); } diff --git a/rankingswizardselection.hpp b/rankingswizardselection.hpp index 0bf8b89..45f15b3 100644 --- a/rankingswizardselection.hpp +++ b/rankingswizardselection.hpp @@ -23,7 +23,7 @@ #include #include -#include "category.hpp" +#include "ranking.hpp" #include "classentry.hpp" #include "teamclassentry.hpp" @@ -37,7 +37,7 @@ class RankingsWizardSelection : public QWizardPage class RankingsWizardItem { public: - Category const *category { Q_NULLPTR }; + Ranking const *categories { Q_NULLPTR }; bool skip { false }; QList ranking { }; QList teamRanking { }; @@ -47,7 +47,7 @@ class RankingsWizardSelection : public QWizardPage void cleanupPage() override; private: - QListWidget categoriesList; + QListWidget rankingsList; QVBoxLayout layout; private slots: diff --git a/catsexdelegate.cpp b/rankingtypedelegate.cpp similarity index 53% rename from catsexdelegate.cpp rename to rankingtypedelegate.cpp index 1e92d0d..5eec0b9 100644 --- a/catsexdelegate.cpp +++ b/rankingtypedelegate.cpp @@ -15,78 +15,62 @@ * along with this program. If not, see . * *****************************************************************************/ -#include "catsexdelegate.hpp" -#include "lbcrexception.hpp" +#include "rankingtypedelegate.hpp" +#include "crhelper.hpp" -CategorySexDelegate::CategorySexDelegate(QObject *parent) : +RankingTypeDelegate::RankingTypeDelegate(QObject *parent) : QStyledItemDelegate(parent) { - auto *comboBox = box.data(); + auto *comboBox = rankingTypeBox.data(); comboBox->setEditable(false); comboBox->setInsertPolicy(QComboBox::NoInsert); - comboBox->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); comboBox->setDuplicatesEnabled(false); comboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); - comboBox->addItem(QIcon(":/material/icons/agender.svg"), toCatSexString(Competitor::Sex::UNDEFINED), Competitor::toSexString(Competitor::Sex::UNDEFINED)); - comboBox->addItem(QIcon(":/material/icons/male.svg"), toCatSexString(Competitor::Sex::MALE), Competitor::toSexString(Competitor::Sex::MALE)); - comboBox->addItem(QIcon(":/material/icons/female.svg"), toCatSexString(Competitor::Sex::FEMALE), Competitor::toSexString(Competitor::Sex::FEMALE)); - comboBox->addItem(QIcon(":/material/icons/transgender.svg"), toCatSexString(Competitor::Sex::MISC), Competitor::toSexString(Competitor::Sex::MISC)); + comboBox->addItem(QIcon(":/material/icons/person.svg"), CRHelper::toRankingTypeString(Ranking::Type::INDIVIDUAL), QVariant(CRHelper::toTypeString(Ranking::Type::INDIVIDUAL))); + comboBox->addItem(QIcon(":/material/icons/group.svg"), CRHelper::toRankingTypeString(Ranking::Type::CLUB), QVariant(CRHelper::toTypeString(Ranking::Type::CLUB))); } -QWidget *CategorySexDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const +QWidget *RankingTypeDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const { Q_UNUSED(option) Q_UNUSED(index) - auto *comboBox = box.data(); + auto *comboBox = rankingTypeBox.data(); comboBox->setParent(parent); return comboBox; } -void CategorySexDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const +void RankingTypeDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const { Q_UNUSED(editor) Q_UNUSED(index) } -void CategorySexDelegate::setEditorData(QWidget *editor, QModelIndex const &index) const +void RankingTypeDelegate::setEditorData(QWidget *editor, QModelIndex const &index) const { // Get the value via index of the Model and put it into the ComboBox auto *comboBox = static_cast(editor); - comboBox->setCurrentText(toCatSexString(Competitor::toSex(index.model()->data(index, Qt::EditRole).toString()))); + comboBox->setCurrentText(CRHelper::toRankingTypeString(CRHelper::toRankingType(index.model()->data(index, Qt::EditRole).toString()))); } -void CategorySexDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const +void RankingTypeDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const { auto const *comboBox = static_cast(editor); model->setData(index, comboBox->currentData(Qt::UserRole), Qt::EditRole); } -QSize CategorySexDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const +QSize RankingTypeDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const { - return this->box.data()->sizeHint(); + Q_UNUSED(option) + Q_UNUSED(index) + + return this->rankingTypeBox.data()->sizeHint(); } -void CategorySexDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const +void RankingTypeDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const { Q_UNUSED(index) editor->setGeometry(option.rect); } - -QString CategorySexDelegate::toCatSexString(Competitor::Sex const sex) -{ - switch (sex) { - case Competitor::Sex::MALE: - return tr("Men"); - case Competitor::Sex::FEMALE: - return tr("Women"); - case Competitor::Sex::MISC: - return tr("Mixed"); - case Competitor::Sex::UNDEFINED: - return tr("All"); - default: - throw(ChronoRaceException(tr("Unexpected Sex enum value '%1'").arg(static_cast(sex)))); - } -} diff --git a/catsexdelegate.hpp b/rankingtypedelegate.hpp similarity index 81% rename from catsexdelegate.hpp rename to rankingtypedelegate.hpp index 1c40c45..3e81dcc 100644 --- a/catsexdelegate.hpp +++ b/rankingtypedelegate.hpp @@ -15,33 +15,29 @@ * along with this program. If not, see . * *****************************************************************************/ -#ifndef CATSEXDELEGATE_HPP -#define CATSEXDELEGATE_HPP +#ifndef RANKINGTYPEDELEGATE_HPP +#define RANKINGTYPEDELEGATE_HPP #include #include #include #include -#include "competitor.hpp" - -class CategorySexDelegate : public QStyledItemDelegate +class RankingTypeDelegate : public QStyledItemDelegate { Q_OBJECT public: - explicit CategorySexDelegate(QObject *parent = Q_NULLPTR); + explicit RankingTypeDelegate(QObject *parent = Q_NULLPTR); QWidget *createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const override; void destroyEditor(QWidget *editor, const QModelIndex &index) const override; void setEditorData(QWidget *editor, QModelIndex const &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const override; void updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const override; private: - QScopedPointer box { new QComboBox }; - - static QString toCatSexString(Competitor::Sex const sex); + QScopedPointer rankingTypeBox { new QComboBox }; }; -#endif // CATSEXDELEGATE_HPP +#endif // RANKINGTYPEDELEGATE_HPP diff --git a/samples/mass_start/latin1/categories.csv b/samples/mass_start/latin1/categories.csv index 3e6901b..067e798 100644 --- a/samples/mass_start/latin1/categories.csv +++ b/samples/mass_start/latin1/categories.csv @@ -1,16 +1,10 @@ -Junior M,JM,M,1999,2000,I -Junior F,JF,F,1999,2000,I -Senior M,SM,M,1984,1998,I -Senior F,SF,F,1984,1998,I -Amateur A M,AMA,M,1974,1983,I -Amateur A F,AFA,F,1974,1983,I -Amateur B M,AMB,M,1964,1973,I -Amateur B F,AFB,F,1964,1973,I -Veterans M,VM,M,1900,1963,I -Veterans F,VF,F,1900,1963,I -General,GEN,U,0,0,I -General M,GEM,M,0,0,I -General F,GEF,F,0,0,I -Teams,T,U,0,0,T -Teams M,TM,M,0,0,T -Teams F,TF,F,0,0,T +Junior M,JM,M,2000,1999 +Junior F,JF,F,2000,1999 +Senior M,SM,M,1998,1984 +Senior F,SF,F,1998,1984 +Amateur A M,AMA,M,1983,1974 +Amateur A F,AFA,F,1983,1974 +Amateur B M,AMB,M,1973,1964 +Amateur B F,AFB,F,1973,1964 +Veterans M,VM,M,1963,1900 +Veterans F,VF,F,1963,1900 diff --git a/samples/mass_start/latin1/rankings.csv b/samples/mass_start/latin1/rankings.csv new file mode 100644 index 0000000..6182842 --- /dev/null +++ b/samples/mass_start/latin1/rankings.csv @@ -0,0 +1,6 @@ +General,GEN,I,JM+JF+SM+SF+AMA+AFA+AMB+AFB+VM+VF +General M,GEM,I,JM+SM+AMA+AMB+VM +General F,GEF,I,JF+SF+AFA+AFB+VF +Teams,T,T,JM+JF+SM+SF+AMA+AFA+AMB+AFB+VM+VF +Teams M,TM,T,JM+SM+AMA+AMB+VM +Teams F,TF,T,JF+SF+AFA+AFB+VF diff --git a/samples/mass_start/latin1/startlist.csv b/samples/mass_start/latin1/startlist.csv index 2660480..4001cf4 100644 --- a/samples/mass_start/latin1/startlist.csv +++ b/samples/mass_start/latin1/startlist.csv @@ -131,7 +131,7 @@ 211,AURELIO PERRELLA,M,1954,TEAM 8,,1 212,CYRIL MORRIS,M,1956,TEAM 2,,1 213,WALTER HARTZOG,M,1967,TEAM 9,,1 -214,CHAD CARRIZALES,M,1670,TEAM 9,,1 +214,CHAD CARRIZALES,M,1970,TEAM 9,,1 215,COLUMBUS BUTTON,M,1964,TEAM 10,,1 216,TOI DEDIOS,F,1962,TEAM I,,1 217,LEO DILLE,M,1967,TEAM B,,1 diff --git a/samples/mass_start/mass_start.crd b/samples/mass_start/mass_start.crd index ddc9fc24cd48c3b474c4b077a78d20a2543438b1..45dabf0b00af1b29bd47e24ae387e10a100912be 100644 GIT binary patch delta 859 zcmcIize_?<6h7MXnom!uWML4BA`Ritwrb|3FfFg!E80A-~1kK?b(`cCCyj7@+LFDr}>h6DN_NK7NBSr zecod^5%D|GsvUwBZA~Hfr;{+WDDAM+v5m_~Rlt+xM!&;R?yRC4p|qw7E!=e^K zLW}zMFgT({P~-U3{^S|wo65HmE!kL6=EYz5dsc3hsA=by42zx6VM=Kj=D;rlU8+R)k z&sH|xcdMA#+NQ5s#VfyEbT#h@L3W^i1||mI=~4%IrKj^ANG=vG z2jr^rL-jE*u& zYBPh}Q+J40e)`<~yaGU;8;GY3H6avaGYCw-wTD+3sM#071qy`i&{ yDI<`>!~oX@ z$JHvw-73ekRgU-kN>6s6CK-kxhE#?ehD3%+hGGT<26rGY4~UC^A~|4_gMeZXzzpU4 zLTNWB9WiYcuO=rWOm@1-DqdYU*KK;vDqeL*pdoWO@T@yyMZsV@ J-)i11>;MN?F;4&h delta 200 zcmZ4V-0aX(GY$p@24)~;m@X*ED%L31D#zF=$J8pv+$zV?D#zL?$JQ#x-YUn@D#zI> z$JHvw-73ekRgU-kN?|sTE+AlH@P#mdX2h-L)nsO3aGTz;n%9sYA{zmhowtft-INim yB@`wIq`4SE7*ZJ$8FCql!Ezx$5~2!WFxzTgMQ)(MFm2NfR`bek|G1iWBRc>DrXVT+ diff --git a/startlistmodel.cpp b/startlistmodel.cpp index 9e9c2e8..be370d3 100644 --- a/startlistmodel.cpp +++ b/startlistmodel.cpp @@ -18,6 +18,7 @@ #include "lbchronorace.hpp" #include "startlistmodel.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" QDataStream &operator<<(QDataStream &out, StartListModel const &data) { @@ -126,7 +127,7 @@ QVariant StartListModel::data(QModelIndex const &index, int role) const case static_cast(Competitor::Field::CMF_NAME): return QVariant(startList.at(index.row()).getName()); case static_cast(Competitor::Field::CMF_SEX): - return QVariant(Competitor::toSexString(startList.at(index.row()).getSex())); + return QVariant(CRHelper::toSexString(startList.at(index.row()).getSex())); case static_cast(Competitor::Field::CMF_YEAR): return QVariant(startList.at(index.row()).getYear()); case static_cast(Competitor::Field::CMF_CLUB): @@ -134,7 +135,7 @@ QVariant StartListModel::data(QModelIndex const &index, int role) const case static_cast(Competitor::Field::CMF_TEAM): return QVariant(startList.at(index.row()).getTeam()); case static_cast(Competitor::Field::CMF_OFFSET_LEG): - return QVariant(Competitor::toOffsetString(startList.at(index.row()).getOffset())); + return QVariant(CRHelper::toOffsetString(startList.at(index.row()).getOffset())); default: return QVariant(); } @@ -172,62 +173,65 @@ bool StartListModel::setData(QModelIndex const &index, QVariant const &value, in if (role != Qt::EditRole) return retval; + if (value.toString().contains(LBChronoRace::csvFilter)) + return retval; + switch (index.column()) { - case static_cast(Competitor::Field::CMF_BIB): - uval = value.toUInt(&retval); - if (retval && uval) { - int maxLeg = this->getMaxLeg(uval, index.row()); - startList[index.row()].setBib(uval); - startList[index.row()].setClub(this->getClub(uval)); - startList[index.row()].setTeam(this->getTeam(uval)); - startList[index.row()].setOffset((maxLeg < 0) ? &maxLeg : Q_NULLPTR); - } else { - retval = false; - } - break; - case static_cast(Competitor::Field::CMF_NAME): - startList[index.row()].setName(value.toString().simplified()); - retval = true; - break; - case static_cast(Competitor::Field::CMF_SEX): - try { - Competitor::Sex sex = Competitor::toSex(value.toString().trimmed(), true); - retval = (sex != Competitor::Sex::MISC); - if (retval) startList[index.row()].setSex(sex); - } catch (ChronoRaceException &ex) { - emit error(ex.getMessage()); - retval = false; - } - break; - case static_cast(Competitor::Field::CMF_YEAR): - uval = value.toUInt(&retval); - if (retval) startList[index.row()].setYear(uval); - break; - case static_cast(Competitor::Field::CMF_CLUB): - uval = startList[index.row()].getBib(); - if (uval == 0) { - startList[index.row()].setClub(value.toString().simplified()); - } else { - this->setClub(uval, value.toString().simplified()); - } - emit newClub(startList[index.row()].getClub()); - retval = true; - break; - case static_cast(Competitor::Field::CMF_TEAM): - uval = startList[index.row()].getBib(); - if (uval == 0) { - startList[index.row()].setTeam(value.toString().simplified()); - } else { - this->setTeam(uval, value.toString().simplified()); - } - retval = true; - break; - case static_cast(Competitor::Field::CMF_OFFSET_LEG): - startList[index.row()].setOffset(Competitor::toOffset(value.toString().simplified())); + case static_cast(Competitor::Field::CMF_BIB): + uval = value.toUInt(&retval); + if (retval && uval) { + int maxLeg = this->getMaxLeg(uval, index.row()); + startList[index.row()].setBib(uval); + startList[index.row()].setClub(this->getClub(uval)); + startList[index.row()].setTeam(this->getTeam(uval)); + startList[index.row()].setOffset((maxLeg < 0) ? &maxLeg : Q_NULLPTR); + } else { + retval = false; + } + break; + case static_cast(Competitor::Field::CMF_NAME): + startList[index.row()].setName(value.toString().simplified()); + retval = true; + break; + case static_cast(Competitor::Field::CMF_SEX): + try { + Competitor::Sex sex = CRHelper::toSex(value.toString().trimmed()); + startList[index.row()].setSex(sex); retval = true; - break; - default: - break; + } catch (ChronoRaceException &ex) { + emit error(ex.getMessage()); + retval = false; + } + break; + case static_cast(Competitor::Field::CMF_YEAR): + uval = value.toUInt(&retval); + if (retval) startList[index.row()].setYear(uval); + break; + case static_cast(Competitor::Field::CMF_CLUB): + //NOSONAR uval = startList[index.row()].getBib(); + //NOSONAR if (uval == 0) { + startList[index.row()].setClub(value.toString().simplified()); + //NOSONAR } else { + //NOSONAR this->setClub(uval, value.toString().simplified()); + //NOSONAR } + emit newClub(startList[index.row()].getClub()); + retval = true; + break; + case static_cast(Competitor::Field::CMF_TEAM): + uval = startList[index.row()].getBib(); + if (uval == 0) { + startList[index.row()].setTeam(value.toString().simplified()); + } else { + this->setTeam(uval, value.toString().simplified()); + } + retval = true; + break; + case static_cast(Competitor::Field::CMF_OFFSET_LEG): + startList[index.row()].setOffset(CRHelper::toOffset(value.toString().simplified())); + retval = true; + break; + default: + break; } if (retval) @@ -352,11 +356,24 @@ uint StartListModel::getTeamNameWidthMax() const QString const *StartListModel::getClub(uint bib) { - for (qsizetype row = 0; row < startList.count(); row++) - if (startList[row].getBib() == bib) - return &startList[row].getClub(); + QString const *clubPtr = Q_NULLPTR; - return Q_NULLPTR; + for (qsizetype row = 0; row < startList.count(); row++) { + if (startList[row].getBib() != bib) + continue; + + if (startList[row].getClub().isEmpty()) + continue; + + if (!clubPtr) { + clubPtr = &startList[row].getClub(); + } else if (*clubPtr != startList[row].getClub()) { + clubPtr = Q_NULLPTR; + break; + } + } + + return clubPtr; } QString const *StartListModel::getTeam(uint bib) diff --git a/teamclassentry.cpp b/teamclassentry.cpp index 1222ce9..eed4e98 100644 --- a/teamclassentry.cpp +++ b/teamclassentry.cpp @@ -23,7 +23,7 @@ QString const &TeamClassEntry::getClub() const return club; } -ClassEntry const *TeamClassEntry::getClassEntry(int index) const +ClassEntry *TeamClassEntry::getClassEntry(int index) const { if (index >= this->entryList.size()) throw(ChronoRaceException(tr("Requested index %1 exceeds the number of available entries %2").arg(index).arg(this->entryList.size()))); diff --git a/teamclassentry.hpp b/teamclassentry.hpp index ff9b322..ece247a 100644 --- a/teamclassentry.hpp +++ b/teamclassentry.hpp @@ -18,8 +18,6 @@ #ifndef TEAMCLASSENTRY_H #define TEAMCLASSENTRY_H -#include - #include "classentry.hpp" namespace placement { @@ -36,7 +34,7 @@ class TeamClassEntry public: QString const &getClub() const; - ClassEntry const *getClassEntry(int index) const; + ClassEntry *getClassEntry(int index) const; void setClassEntry(ClassEntry *entry); int getClassEntryCount() const; diff --git a/teamslistmodel.cpp b/teamslistmodel.cpp index 5559534..8255df2 100644 --- a/teamslistmodel.cpp +++ b/teamslistmodel.cpp @@ -17,6 +17,7 @@ #include +#include "lbchronorace.hpp" #include "teamslistmodel.hpp" QDataStream &operator<<(QDataStream &out, TeamsListModel const &data) @@ -88,12 +89,23 @@ QVariant TeamsListModel::headerData(int section, Qt::Orientation orientation, in bool TeamsListModel::setData(QModelIndex const &index, QVariant const &value, int role) { - if (index.isValid() && role == Qt::EditRole) { - teamsList[index.row()] = value.toString().simplified(); - emit dataChanged(index, index); - return true; - } - return false; + bool retval = false; + + if (!index.isValid()) + return retval; + + if (role != Qt::EditRole) + return retval; + + if (value.toString().contains(LBChronoRace::csvFilter)) + return retval; + + teamsList[index.row()] = value.toString().simplified(); + retval = true; + + emit dataChanged(index, index); + + return retval; } Qt::ItemFlags TeamsListModel::flags(QModelIndex const &index) const diff --git a/timing.cpp b/timing.cpp index 1ef35a7..3197b66 100644 --- a/timing.cpp +++ b/timing.cpp @@ -17,6 +17,7 @@ #include "timing.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" Timing::Field TimingSorter::sortingField = Timing::Field::TMF_TIME; Qt::SortOrder TimingSorter::sortingOrder = Qt::AscendingOrder; @@ -93,7 +94,7 @@ Timing::Status Timing::getStatus() const QString Timing::getTiming() const { - return toTimeStr(this->seconds, this->status); + return CRHelper::toTimeStr(this->seconds, this->status); } void Timing::setTiming(QString const &timing) @@ -142,31 +143,6 @@ bool Timing::isValid() const return ((bib != 0u) && ((status == Status::DNS) || (status == Status::DNF) || ((status == Status::CLASSIFIED) && (seconds != 0u)))); } -QString Timing::toTimeStr(uint const seconds, Timing::Status const status, char const *prefix) -{ - - QString retString(prefix ? prefix : ""); - switch (status) { - case Timing::Status::CLASSIFIED: - retString.append(QString("%1:%2:%3").arg(((seconds / 60) / 60)).arg(((seconds / 60) % 60), 2, 10, QLatin1Char('0')).arg((seconds % 60), 2, 10, QLatin1Char('0'))); - break; - case Timing::Status::DNF: - retString.append("DNF"); - break; - case Timing::Status::DNS: - retString.append("DNS"); - break; - default: - throw(ChronoRaceException(tr("Invalid status value %1").arg(static_cast(status)))); - } - return retString; -} - -QString Timing::toTimeStr(Timing const &timing) -{ - return toTimeStr(timing.getSeconds(), timing.getStatus()); -} - bool Timing::operator< (Timing const &rhs) const { return (((this->status == Status::DNF) && (rhs.status == Status::DNS)) || ((this->status == Status::CLASSIFIED) && ((rhs.status != Status::CLASSIFIED) || (this->seconds < rhs.seconds)))); diff --git a/timing.hpp b/timing.hpp index 266223e..debfc4e 100644 --- a/timing.hpp +++ b/timing.hpp @@ -74,9 +74,6 @@ class Timing { void setTiming(char const *timing); bool isValid() const; - static QString toTimeStr(uint const seconds, Timing::Status const status, char const *prefix = Q_NULLPTR); - static QString toTimeStr(Timing const &timing); - bool operator< (Timing const &rhs) const; bool operator> (Timing const &rhs) const; bool operator<= (Timing const &rhs) const; diff --git a/timingsmodel.cpp b/timingsmodel.cpp index 03fb525..9c82b2b 100644 --- a/timingsmodel.cpp +++ b/timingsmodel.cpp @@ -17,6 +17,7 @@ #include +#include "lbchronorace.hpp" #include "timingsmodel.hpp" #include "lbcrexception.hpp" @@ -91,39 +92,46 @@ QVariant TimingsModel::data(QModelIndex const &index, int role) const bool TimingsModel::setData(QModelIndex const &index, QVariant const &value, int role) { - bool retval = false; - if (index.isValid() && role == Qt::EditRole) { - uint uval; - switch (index.column()) { - case static_cast(Timing::Field::TMF_BIB): - uval = value.toUInt(&retval); - if (retval && uval) - timings[index.row()].setBib(uval); - else - retval = false; - break; - case static_cast(Timing::Field::TMF_LEG): - uval = value.toUInt(&retval); - if (retval) - timings[index.row()].setLeg(uval); - break; - case static_cast(Timing::Field::TMF_TIME): - try { - timings[index.row()].setTiming(value.toString().trimmed()); - retval = true; - } catch (ChronoRaceException &ex) { - emit error(ex.getMessage()); - retval = false; - } - break; - default: - break; + if (!index.isValid()) + return retval; + + if (role != Qt::EditRole) + return retval; + + if (value.toString().contains(LBChronoRace::csvFilter)) + return retval; + + uint uval; + switch (index.column()) { + case static_cast(Timing::Field::TMF_BIB): + uval = value.toUInt(&retval); + if (retval && uval) + timings[index.row()].setBib(uval); + else + retval = false; + break; + case static_cast(Timing::Field::TMF_LEG): + uval = value.toUInt(&retval); + if (retval) + timings[index.row()].setLeg(uval); + break; + case static_cast(Timing::Field::TMF_TIME): + try { + timings[index.row()].setTiming(value.toString().trimmed()); + retval = true; + } catch (ChronoRaceException &ex) { + emit error(ex.getMessage()); + retval = false; } - - if (retval) emit dataChanged(index, index); + break; + default: + break; } + + if (retval) emit dataChanged(index, index); + return retval; } diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index 038a76d..7d2a242 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -2,19 +2,7 @@ - CRLoader - - Error: cannot open %1 - Error: cannot open %1 - - - Wrong number of columns; expected %1 - found %2 - Wrong number of columns; expected %1 - found %2 - - - Wrong number of elements in CSV row; expected %1 - found %2 - Wrong number of elements in CSV row; expected %1 - found %2 - + CRHelper UTF-8 UTF-8 @@ -23,6 +11,10 @@ ISO-8859-1 (Latin-1) ISO-8859-1 (Latin-1) + + Unknown encoding %1 + Unknown encoding %1 + PDF PDF @@ -35,14 +27,93 @@ CSV CSV - - Unknown encoding %1 - Unknown encoding %1 - Unknown format %1 Unknown format %1 + + Illegal sex '%1' + Illegal sex '%1' + + + Unexpected Sex enum value '%1' + Unexpected Sex enum value '%1' + + + Male + Male + + + Female + Female + + + Not set + Not set + + + Illegal type '%1' + Illegal type '%1' + + + Unexpected Type enum value '%1' + Unexpected Type enum value '%1' + + + Individual/Relay + Individual/Relay + + + Club + Club + + + Individual/Relay (M) + Individual/Relay (M) + + + Individual/Relay (F) + Individual/Relay (F) + + + Mixed Relay (M/F) + Mixed Relay (M/F) + + + Mixed Clubs Relay (M) + Mixed Clubs Relay (M) + + + Mixed Clubs Relay (F) + Mixed Clubs Relay (F) + + + Invalid status value %1 + Invalid status value %1 + + + + CRLoader + + Error: cannot open %1 + Error: cannot open %1 + + + Wrong number of columns; expected %1 - found %2 + Wrong number of columns; expected %1 - found %2 + + + Wrong number of elements in CSV row; expected %1 - found %2 + Wrong number of elements in CSV row; expected %1 - found %2 + + + Unexpected model value %1 (import) + Unexpected model value %1 (import) + + + Unexpected model value %1 (export) + Unexpected model value %1 (export) + CSVRankingPrinter @@ -65,14 +136,6 @@ CategoriesModel - - T - T - - - I - I - The category will include competitors born up to and including this year (i.e. 2000); 0 to disable The category will include competitors born up to and including this year (i.e. 2000); 0 to disable @@ -89,10 +152,6 @@ Short category name Short category name - - Sex - Sex - Up to Up to @@ -110,69 +169,19 @@ Category Short Name - Men (M), Women (F), Mixed (X) or All (U) - Men (M), Women (F), Mixed (X) or All (U) - - - Individual/Relay (I) or Club (T) - Individual/Relay (I) or Club (T) + Male Individual/Relay (M), Female Individual/Relay (F), Mixed M/F Relay (X), Male Mixed Clubs Relay (Y), or Female Mixed Clubs Relay (Y) + Male Individual/Relay (M), Female Individual/Relay (F), Mixed M/F Relay (X), Male Mixed Clubs Relay (Y), or Female Mixed Clubs Relay (Y) - Individual/Club - Individual/Club + Type + Type Category - Illegal category type - expected 'I' or 'T' - found %1 - Illegal category type - expected 'I' or 'T' - found %1 - - - Unexpected Type enum value '%1' - Unexpected Type enum value '%1' - - - Illegal type '%1' - Illegal type '%1' - - - - CategorySexDelegate - - Men - Men - - - Women - Women - - - Mixed - Mixed - - - All - All - - - Unexpected Sex enum value '%1' - Unexpected Sex enum value '%1' - - - - CategoryTypeDelegate - - Club - Club - - - Unexpected Type enum value '%1' - Unexpected Type enum value '%1' - - - Individual/Relay - Individual/Relay + Illegal category type '%1' + Illegal category type '%1' @@ -638,16 +647,29 @@ Continue? Competitor mismatch for bib %1: found %2 replaced by %3 Competitor mismatch for bib %1: found %2 replaced by %3 - - - Competitor - Illegal sex '%1' - Illegal sex '%1' + Unexpected sex value for bib %1 (%2) + Unexpected sex value for bib %1 (%2) - Unexpected Sex enum value '%1' - Unexpected Sex enum value '%1' + No competitors associated to bib %1 + No competitors associated to bib %1 + + + No categories associated to competitor %1 - bib %2 + No categories associated to competitor %1 - bib %2 + + + Dropping category '%1' associated to competitor %2 - bib %3 + Dropping category '%1' associated to competitor %2 - bib %3 + + + Dropping category '%1' associated to competitor %2 - bib %3 - leg 1 + Dropping category '%1' associated to competitor %2 - bib %3 - leg 1 + + + Removing candidate category '%1' associated to competitor %2 - bib %3 + Removing candidate category '%1' associated to competitor %2 - bib %3 @@ -972,12 +994,6 @@ Continue? Cl&ubs Cl&ubs - - Data format %1 not supported. -Please uodate the application. - Data format %1 not supported. -Please uodate the application. - Clubs Clubs @@ -1030,6 +1046,50 @@ Please uodate the application. Error: unexpected encoding value (encoding not changed) Error: unexpected encoding value (encoding not changed) + + Rankings + Rankings + + + Add, remove, and change the definition of the rankings + Add, remove, and change the definition of the rankings + + + Edit Rankings + Edit Rankings + + + Select Rankings File + Select Rankings File + + + Rankings File: %1 + Rankings File: %1 + + + Loaded: %n ranking(s) + + Loaded: %n ranking + Loaded: %n rankings + + + + Rankings File saved: %1 + Rankings File saved: %1 + + + Data format %1 not supported. +Please update the application. + Data format %1 not supported. +Please update the application. + + + + MultiSelectComboBox + + Search… + Search… + PDFRankingPrinter @@ -1184,6 +1244,22 @@ Please uodate the application. Created with %1 %2 Created with %1 %2 + + Unknown + Unknown + + + ???? + ???? + + + ? + ? + + + ?:??:?? + ?:??:?? + QMessageBox @@ -1255,6 +1331,13 @@ Please uodate the application. Results + + Ranking + + Illegal ranking type - expected 'I' or 'T' - found %1 + Illegal ranking type - expected 'I' or 'T' - found %1 + + RankingPrinter @@ -1264,10 +1347,6 @@ Please uodate the application. RankingsBuilder - - Warning: missing or extra legs for bib %1 - Warning: missing or extra legs for bib %1 - Competitor not found for bib %1 Competitor not found for bib %1 @@ -1284,6 +1363,53 @@ Please uodate the application. Warning: the number of timings (%1) is not match the expected (%2); check for possible missing or duplicated entries Warning: the number of timings (%1) is not match the expected (%2); check for possible missing or duplicated entries + + Warning: missing or extra legs for bib %1 or %2 + Warning: missing or extra legs for bib %1 or %2 + + + + RankingsModel + + T + T + + + I + I + + + Full ranking name + Full ranking name + + + Short ranking name + Short ranking name + + + Individual/Relay (I) or Club (T) + Individual/Relay (I) or Club (T) + + + The ranking will include all the categories listed here + The ranking will include all the categories listed here + + + Ranking Full Name + Ranking Full Name + + + Ranking Short Name + Ranking Short Name + + + Individual/Club + Individual/Club + + + Categories + Categories + RankingsWizard @@ -1381,27 +1507,8 @@ Please uodate the application. Rankings - You can exclude some categories from the generated rankings. - You can exclude some categories from the generated rankings. - - - - SexDelegate - - Male - Male - - - Female - Female - - - Unexpected Sex enum value '%1' - Unexpected Sex enum value '%1' - - - Not set - Not set + You can exclude some of the generated rankings. + You can exclude some of the generated rankings. @@ -1525,10 +1632,6 @@ Please uodate the application. Illegal timing value '%1' for bib '%2' Illegal timing value '%1' for bib '%2' - - Invalid status value %1 - Invalid status value %1 - TimingsModel diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 30b1233..d3472c9 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -2,19 +2,7 @@ - CRLoader - - Error: cannot open %1 - Errore: impossibile aprire %1 - - - Wrong number of columns; expected %1 - found %2 - Numero colonne errato - atteso %1 - trovato %2 - - - Wrong number of elements in CSV row; expected %1 - found %2 - Numero elementi errato nel CSV row; atteso %1 - trovato %2 - + CRHelper UTF-8 UTF-8 @@ -23,6 +11,10 @@ ISO-8859-1 (Latin-1) ISO-8859-1 (Latin-1) + + Unknown encoding %1 + Codifica sconosciuta %1 + PDF PDF @@ -35,14 +27,93 @@ CSV CSV - - Unknown encoding %1 - Codifica sconosciuta %1 - Unknown format %1 Formato sconosciuto %1 + + Illegal sex '%1' + Sesso non valido '%1' + + + Unexpected Sex enum value '%1' + Valore enumerazione Sex '%1' non valido + + + Male + Maschio + + + Female + Femmina + + + Not set + Non impostato + + + Illegal type '%1' + Tipo non valido '%1' + + + Unexpected Type enum value '%1' + Valore enumerazione Tipo '%1' non valido + + + Individual/Relay + Individuale/Staffetta + + + Club + Società + + + Individual/Relay (M) + Individuale/Staffetta (M) + + + Individual/Relay (F) + Individuale/Staffetta (F) + + + Mixed Relay (M/F) + Staffetta Mista (M/F) + + + Mixed Clubs Relay (M) + Staffetta Mista di Società (M) + + + Mixed Clubs Relay (F) + Staffetta Mista di Società (F) + + + Invalid status value %1 + Valore di stato %1 non valido + + + + CRLoader + + Error: cannot open %1 + Errore: impossibile aprire %1 + + + Wrong number of columns; expected %1 - found %2 + Numero colonne errato - atteso %1 - trovato %2 + + + Wrong number of elements in CSV row; expected %1 - found %2 + Numero elementi errato nel CSV row; atteso %1 - trovato %2 + + + Unexpected model value %1 (import) + Valore modello inatteso %1 (importazione) + + + Unexpected model value %1 (export) + Valore modello inatteso %1 (esportazione) + CSVRankingPrinter @@ -65,14 +136,6 @@ CategoriesModel - - T - T - - - I - I - The category will include competitors born up to and including this year (i.e. 2000); 0 to disable La categoria include concorrenti nati fino a quest'anno incluso (es. 2000); 0 per disabilitare @@ -89,10 +152,6 @@ Short category name Abbreviazione categoria - - Sex - Sesso - Up to Fino al @@ -110,69 +169,19 @@ Abbreviazione Categoria - Men (M), Women (F), Mixed (X) or All (U) - Maschile (M), Femminile (F), Mista (X) o Tutti (U) - - - Individual/Relay (I) or Club (T) - Individuale/Staffetta (I) or Società (T) + Male Individual/Relay (M), Female Individual/Relay (F), Mixed M/F Relay (X), Male Mixed Clubs Relay (Y), or Female Mixed Clubs Relay (Y) + Individuale/Staffetta Maschile (M), Individuale/Staffetta Femminile (F), Staffetta Mista M/F (X), Staffetta Mista di Società M (Y) o Staffetta Mista di Società F (X) - Individual/Club - Individuale/Società + Type + Tipo Category - Illegal category type - expected 'I' or 'T' - found %1 - Categoria errata - valori ammessi 'I' o 'T' - trovato %1 - - - Unexpected Type enum value '%1' - Valore enumerazione Type '%1' non valido - - - Illegal type '%1' - Tipo non valido '%1' - - - - CategorySexDelegate - - Men - Maschile - - - Women - Femminile - - - Mixed - Mista - - - All - Tutti - - - Unexpected Sex enum value '%1' - Valore enumerazione Sex '%1' non valido - - - - CategoryTypeDelegate - - Club - Società - - - Unexpected Type enum value '%1' - Valore enumerazione Type '%1' non valido - - - Individual/Relay - Individuale/Staffetta + Illegal category type '%1' + Tipo categoria illegale '%1' @@ -638,16 +647,29 @@ Continuare? Competitor mismatch for bib %1: found %2 replaced by %3 Concorrente incompatibile per il pettorale %1: quello riscontrato %2 sarà rimpiazzato da %3 - - - Competitor - Illegal sex '%1' - Sesso non valido '%1' + Unexpected sex value for bib %1 (%2) + Valore sesso inatteso per il pettorale '%1' (%2) - Unexpected Sex enum value '%1' - Valore enumerazione Sex '%1' non valido + No competitors associated to bib %1 + Nessun concorrente asscoiato al pettorale %1 + + + No categories associated to competitor %1 - bib %2 + Nessuna categoria associata al concorrente %1 - pettorale %2 + + + Dropping category '%1' associated to competitor %2 - bib %3 + Scartata categoria '%1' associata al concorrente %2 - pettorale %3 + + + Dropping category '%1' associated to competitor %2 - bib %3 - leg 1 + Scartata categoria '%1' associata al concorrente %2 - pettorale %3 - frazione 1 + + + Removing candidate category '%1' associated to competitor %2 - bib %3 + Rimozione categoria candidata '%1' associata al concorrente %2 - pettorale %3 @@ -972,12 +994,6 @@ Continuare? Cl&ubs &Società - - Data format %1 not supported. -Please uodate the application. - Formato dati %1 non supportato. -Aggiornare l'applicazione. - Clubs Società @@ -1030,6 +1046,50 @@ Aggiornare l'applicazione. Error: unexpected encoding value (encoding not changed) Errore: valore codifica inatteso (codifica non modificata) + + Rankings + Classifiche + + + Add, remove, and change the definition of the rankings + Aggiunge, rimuove e modifica la definizione delle classifiche + + + Edit Rankings + Modifica Classifiche + + + Select Rankings File + Seleziona File Classifiche + + + Rankings File: %1 + File Classifiche: %1 + + + Loaded: %n ranking(s) + + Caricata: %n classifica + Caricate: %n classifiche + + + + Rankings File saved: %1 + File Classifiche salvato: %1 + + + Data format %1 not supported. +Please update the application. + Formato dati %1 non supportato. +Aggiornare l&apos;applicazione. + + + + MultiSelectComboBox + + Search… + Cerca… + PDFRankingPrinter @@ -1166,7 +1226,7 @@ Aggiornare l'applicazione. Error: cannot start drawing - Errore: scrittura su PDF inpossibile + Errore: scrittura su PDF impossibile Error: no file name supplied @@ -1184,6 +1244,22 @@ Aggiornare l'applicazione. Created with %1 %2 Creato con %1 %2 + + Unknown + Sconosciuto + + + ???? + ???? + + + ? + ? + + + ?:??:?? + ?:??:?? + QMessageBox @@ -1255,6 +1331,13 @@ Aggiornare l'applicazione. Risultati + + Ranking + + Illegal ranking type - expected 'I' or 'T' - found %1 + Tipo classifica errato - atteso 'I' o 'T' - trovato %1 + + RankingPrinter @@ -1264,10 +1347,6 @@ Aggiornare l'applicazione. RankingsBuilder - - Warning: missing or extra legs for bib %1 - Frazione extra o mancante per il pettorale %1 - Competitor not found for bib %1 Concorrente non trovato per il pettorale %1 @@ -1284,6 +1363,53 @@ Aggiornare l'applicazione. Warning: the number of timings (%1) is not match the expected (%2); check for possible missing or duplicated entries Attenzione: i tempi inseriti (%1) non corrispondono al numero dei partenti (%2); controllare possibili duplicazioni o mancanze nella lista tempi + + Warning: missing or extra legs for bib %1 or %2 + Frazione extra o mancante per il pettorale %1 o %2 + + + + RankingsModel + + T + T + + + I + I + + + Full ranking name + Nome classifica + + + Short ranking name + Abbreviazione classifica + + + Individual/Relay (I) or Club (T) + Individuale/Staffetta (I) or Società (T) + + + The ranking will include all the categories listed here + La classifica includerà tutte le categorie qui elencate + + + Ranking Full Name + Nome Classifica + + + Ranking Short Name + Abbreviazione Classifica + + + Individual/Club + Individuale/Società + + + Categories + Categorie + RankingsWizard @@ -1321,7 +1447,7 @@ Aggiornare l'applicazione. Generated Results: %1 - Generato Classifiche: %1 + Generate Classifiche: %1 Start List @@ -1381,27 +1507,8 @@ Aggiornare l'applicazione. Classifiche - You can exclude some categories from the generated rankings. - Puoi escludere alcune categorie dalle classifiche generate. - - - - SexDelegate - - Male - Maschio - - - Female - Femmina - - - Unexpected Sex enum value '%1' - Valore enumerazione Sex '%1' non valido - - - Not set - Non impostato + You can exclude some of the generated rankings. + Puoi escludere alcune dalle classifiche generate. @@ -1525,10 +1632,6 @@ Aggiornare l'applicazione. Illegal timing value '%1' for bib '%2' Valore cronometrico '%1' non valido per il pettorale '%2' - - Invalid status value %1 - Valore di stato %1 non valido - TimingsModel diff --git a/txtrankingprinter.cpp b/txtrankingprinter.cpp index 4a3ea5f..256ff7e 100644 --- a/txtrankingprinter.cpp +++ b/txtrankingprinter.cpp @@ -20,11 +20,14 @@ #include "txtrankingprinter.hpp" #include "chronoracedata.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" -void TXTRankingPrinter::init(QString *outFileName, [[maybe_unused]] QString const &title) +void TXTRankingPrinter::init(QString *outFileName, QString const &title) { Q_ASSERT(!txtFile.isOpen()); + Q_UNUSED(title) + if (outFileName == Q_NULLPTR) { throw(ChronoRaceException(tr("Error: no file name supplied"))); } @@ -51,7 +54,7 @@ void TXTRankingPrinter::init(QString *outFileName, [[maybe_unused]] QString cons } } -void TXTRankingPrinter::printStartList(QList const &startList) +void TXTRankingPrinter::printStartList(QList const &startList) { if (!txtFile.isOpen()) { throw(ChronoRaceException(tr("Error: writing attempt on closed file"))); @@ -70,13 +73,13 @@ void TXTRankingPrinter::printStartList(QList const &startList) for (auto const &competitor : startList) { i++; - offset = competitor.getOffset(); + offset = competitor->getOffset(); if (offset >= 0) { offset += (3600 * startTime.hour()) + (60 * startTime.minute()) + startTime.second(); - lastColumnValue = Competitor::toOffsetString(offset); + lastColumnValue = CRHelper::toOffsetString(offset); } else if (CRLoader::getStartListLegs() == 1) { offset = (3600 * startTime.hour()) + (60 * startTime.minute()) + startTime.second(); - lastColumnValue = Competitor::toOffsetString(offset); + lastColumnValue = CRHelper::toOffsetString(offset); } else { QString legValue = tr("Leg %n", "", qAbs(offset)); if (!lastColumnValue.isEmpty() && (legValue != lastColumnValue)) @@ -91,16 +94,16 @@ void TXTRankingPrinter::printStartList(QList const &startList) txtStream << " - "; txtStream.setFieldWidth(getBibFieldWidth()); txtStream.setFieldAlignment(QTextStream::AlignRight); - txtStream << competitor.getBib(); + txtStream << competitor->getBib(); txtStream.setFieldWidth(0); txtStream << " - "; - txtStream << competitor.getName(nWidth); + txtStream << competitor->getName(nWidth); txtStream << " - "; - txtStream << competitor.getTeam(tWidth); + txtStream << competitor->getTeam(tWidth); txtStream << " - "; - txtStream << competitor.getYear(); + txtStream << competitor->getYear(); txtStream << " - "; - txtStream << Competitor::toSexString(competitor.getSex()); + txtStream << CRHelper::toSexString(competitor->getSex()); txtStream << " - "; txtStream.setFieldWidth(15); txtStream.setFieldAlignment(QTextStream::AlignRight); @@ -111,7 +114,7 @@ void TXTRankingPrinter::printStartList(QList const &startList) txtStream << Qt::endl; } -void TXTRankingPrinter::printRanking(const Category &category, QList const &ranking) +void TXTRankingPrinter::printRanking(Ranking const &categories, QList const &ranking) { static Position position; @@ -125,7 +128,7 @@ void TXTRankingPrinter::printRanking(const Category &category, QListgetTotalTime(CRLoader::Format::TEXT); @@ -152,7 +155,7 @@ void TXTRankingPrinter::printRanking(const Category &category, QList const &ranking) +void TXTRankingPrinter::printRanking(Ranking const &categories, QList const &ranking) { if (!txtFile.isOpen()) { throw(ChronoRaceException(tr("Error: writing attempt on closed file"))); @@ -161,7 +164,7 @@ void TXTRankingPrinter::printRanking(const Category &category, QListgetClassEntryCount(); j++) { diff --git a/txtrankingprinter.hpp b/txtrankingprinter.hpp index a60636c..b4767bc 100644 --- a/txtrankingprinter.hpp +++ b/txtrankingprinter.hpp @@ -31,9 +31,9 @@ class TXTRankingPrinter final : public RankingPrinter public: void init(QString *outFileName, QString const &title) override; - void printStartList(QList const &startList) override; - void printRanking(Category const &category, QList const &ranking) override; - void printRanking(Category const &category, QList const &ranking) override; + void printStartList(QList const &startList) override; + void printRanking(Ranking const &categories, QList const &ranking) override; + void printRanking(Ranking const &categories, QList const &ranking) override; bool finalize() override; From eedaad0e451df2344e2e3c6874f3e337f44b04ac Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Mon, 29 Jul 2024 23:42:36 +0200 Subject: [PATCH 07/20] Change icons of 'Edit' buttons and menu In addition to the new icons used in the user interface, some square icons with a number in the middle have been added to the resources. These numbered icons can be used instead of the current ones to help the user follow the correct sequence of data entry. --- chronorace.ui | 22 +++++++++++----------- icons/acute.svg | 1 + icons/more.svg | 1 + icons/rule.svg | 1 + icons/user_attributes.svg | 1 + materialicons.qrc | 4 ++++ 6 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 icons/acute.svg create mode 100644 icons/more.svg create mode 100644 icons/rule.svg create mode 100644 icons/user_attributes.svg diff --git a/chronorace.ui b/chronorace.ui index 34088c2..e61f918 100644 --- a/chronorace.ui +++ b/chronorace.ui @@ -76,7 +76,7 @@ - :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg + :/material/icons/more.svg:/material/icons/more.svg @@ -112,7 +112,7 @@ - :/material/icons/format_list_numbered.svg:/material/icons/format_list_numbered.svg + :/material/icons/user_attributes.svg:/material/icons/user_attributes.svg @@ -156,7 +156,7 @@ - :/material/icons/format_list_numbered_rtl.svg:/material/icons/format_list_numbered_rtl.svg + :/material/icons/acute.svg:/material/icons/acute.svg @@ -197,7 +197,7 @@ - :/material/icons/format_list_bulleted.svg:/material/icons/format_list_bulleted.svg + :/material/icons/group.svg:/material/icons/group.svg @@ -241,7 +241,7 @@ - :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg + :/material/icons/rule.svg:/material/icons/rule.svg @@ -416,7 +416,7 @@ 0 0 1024 - 20 + 22 @@ -482,7 +482,7 @@ - :/material/icons/format_list_numbered.svg:/material/icons/format_list_numbered.svg + :/material/icons/user_attributes.svg:/material/icons/user_attributes.svg &Start List @@ -494,7 +494,7 @@ - :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg + :/material/icons/more.svg:/material/icons/more.svg &Categories @@ -506,7 +506,7 @@ - :/material/icons/format_list_numbered_rtl.svg:/material/icons/format_list_numbered_rtl.svg + :/material/icons/acute.svg:/material/icons/acute.svg &Timings @@ -548,7 +548,7 @@ - :/material/icons/format_list_bulleted.svg:/material/icons/format_list_bulleted.svg + :/material/icons/group.svg:/material/icons/group.svg Cl&ubs @@ -675,7 +675,7 @@ - :/material/icons/format_list_bulleted_add.svg:/material/icons/format_list_bulleted_add.svg + :/material/icons/rule.svg:/material/icons/rule.svg Rankings diff --git a/icons/acute.svg b/icons/acute.svg new file mode 100644 index 0000000..383f864 --- /dev/null +++ b/icons/acute.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/more.svg b/icons/more.svg new file mode 100644 index 0000000..a66eddb --- /dev/null +++ b/icons/more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/rule.svg b/icons/rule.svg new file mode 100644 index 0000000..6d1c2f4 --- /dev/null +++ b/icons/rule.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/user_attributes.svg b/icons/user_attributes.svg new file mode 100644 index 0000000..ed4d800 --- /dev/null +++ b/icons/user_attributes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/materialicons.qrc b/materialicons.qrc index 087c21b..29ab8fb 100644 --- a/materialicons.qrc +++ b/materialicons.qrc @@ -42,5 +42,9 @@ icons/person.svg icons/group.svg icons/abc.svg + icons/acute.svg + icons/more.svg + icons/rule.svg + icons/user_attributes.svg From 249f8038bf2e078b2044a67a76c4e05457fbee09 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Tue, 30 Jul 2024 17:09:39 +0200 Subject: [PATCH 08/20] Prevent writing or pasting into 'info' and 'error' areas --- chronorace.ui | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/chronorace.ui b/chronorace.ui index e61f918..7156cf4 100644 --- a/chronorace.ui +++ b/chronorace.ui @@ -266,6 +266,15 @@ 0 + + Qt::FocusPolicy::NoFocus + + + Qt::ContextMenuPolicy::NoContextMenu + + + false + General information messages generated by the application @@ -290,6 +299,15 @@ 0 + + Qt::FocusPolicy::NoFocus + + + Qt::ContextMenuPolicy::NoContextMenu + + + false + Error messages generated by the application From f700ff1ea3e7d88edf376e5ae2a2fddef62bd515 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Tue, 30 Jul 2024 18:13:51 +0200 Subject: [PATCH 09/20] Improve information and error messaging with colors --- categoriesmodel.cpp | 4 +-- chronorace.ui | 4 +-- lbchronorace.cpp | 40 +++++++++++++++---------- lbchronorace.hpp | 1 - pdfrankingprinter.cpp | 2 +- rankingsbuilder.cpp | 2 +- rankingswizard.cpp | 6 ++-- startlistmodel.cpp | 4 +-- timingsmodel.cpp | 4 +-- translations/LBChronoRace_en.ts | 52 +++++++++------------------------ translations/LBChronoRace_it.ts | 52 +++++++++------------------------ 11 files changed, 66 insertions(+), 105 deletions(-) diff --git a/categoriesmodel.cpp b/categoriesmodel.cpp index 05a2e22..d654edc 100644 --- a/categoriesmodel.cpp +++ b/categoriesmodel.cpp @@ -117,8 +117,8 @@ bool CategoriesModel::setData(QModelIndex const &index, QVariant const &value, i auto type = CRHelper::toCategoryType(value.toString().trimmed()); categories[index.row()].setType(type); retval = true; - } catch (ChronoRaceException &ex) { - emit error(ex.getMessage()); + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); retval = false; } break; diff --git a/chronorace.ui b/chronorace.ui index 7156cf4..948c54c 100644 --- a/chronorace.ui +++ b/chronorace.ui @@ -287,7 +287,7 @@ - Errors + Warnings/Errors @@ -434,7 +434,7 @@ 0 0 1024 - 22 + 20 diff --git a/lbchronorace.cpp b/lbchronorace.cpp index d38e7a9..54a86bd 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -181,12 +181,22 @@ void LBChronoRace::setCounterTimings(int count) const void LBChronoRace::appendInfoMessage(QString const &message) const { - ui->infoDisplay->appendPlainText(message); + if (auto sep = message.indexOf(':'); sep < 0) { + ui->infoDisplay->appendHtml("

" + message + "

"); + } else { + ui->infoDisplay->appendHtml("

" + message.first(sep) + "" + message.sliced(sep) + "

"); + } } void LBChronoRace::appendErrorMessage(QString const &message) const { - ui->errorDisplay->appendPlainText(message); + if (auto sep = message.indexOf(':'); sep < 0) { + ui->errorDisplay->appendHtml("

" + message + "

"); + } else if (message.at(sep + 1) == QChar(':')) { + ui->errorDisplay->appendHtml("

" + message.first(sep) + "" + message.sliced(sep + 1) + "

"); + } else { + ui->errorDisplay->appendHtml("

" + message.first(sep) + "" + message.sliced(sep) + "

"); + } } void LBChronoRace::importStartList() @@ -203,7 +213,7 @@ void LBChronoRace::importStartList() appendInfoMessage(tr("Loaded: %n team(s)", "", count.second)); lastSelectedPath = QFileInfo(startListFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } setCounterCompetitors(count.first); setCounterTeams(count.second); @@ -223,7 +233,7 @@ void LBChronoRace::importRankingsList() appendInfoMessage(tr("Loaded: %n ranking(s)", "", count)); lastSelectedPath = QFileInfo(rankingsFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } setCounterRankings(count); } @@ -242,7 +252,7 @@ void LBChronoRace::importCategoriesList() appendInfoMessage(tr("Loaded: %n category(s)", "", count)); lastSelectedPath = QFileInfo(categoriesFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } setCounterCategories(count); } @@ -261,7 +271,7 @@ void LBChronoRace::importTimingsList() appendInfoMessage(tr("Loaded: %n timing(s)", "", count)); lastSelectedPath = QFileInfo(timingsFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } setCounterTimings(count); } @@ -282,7 +292,7 @@ void LBChronoRace::exportStartList() appendInfoMessage(tr("Start List File saved: %1").arg(startListFileName)); lastSelectedPath = QFileInfo(startListFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } } } @@ -302,7 +312,7 @@ void LBChronoRace::exportTeamList() appendInfoMessage(tr("Teams File saved: %1").arg(teamsFileName)); lastSelectedPath = QFileInfo(teamsFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } } } @@ -322,7 +332,7 @@ void LBChronoRace::exportRankingsList() appendInfoMessage(tr("Rankings File saved: %1").arg(rankingsFileName)); lastSelectedPath = QFileInfo(rankingsFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } } } @@ -342,7 +352,7 @@ void LBChronoRace::exportCategoriesList() appendInfoMessage(tr("Categories File saved: %1").arg(categoriesFileName)); lastSelectedPath = QFileInfo(categoriesFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } } } @@ -362,7 +372,7 @@ void LBChronoRace::exportTimingsList() appendInfoMessage(tr("Timings File saved: %1").arg(timingsFileName)); lastSelectedPath = QFileInfo(timingsFileName).absoluteDir(); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } } } @@ -591,7 +601,7 @@ void LBChronoRace::setEncoding() current = 1; break; default: - appendErrorMessage(tr("Error: unexpected encoding value (fall back to the default)")); + appendErrorMessage(tr("Unexpected encoding value (fall back to the default)")); break; } @@ -609,7 +619,7 @@ void LBChronoRace::setEncoding() else if (item == items[1]) CRLoader::setEncoding(CRLoader::Encoding::UTF8); else - appendErrorMessage(tr("Error: unexpected encoding value (encoding not changed)")); + appendErrorMessage(tr("Unexpected encoding value (encoding not changed)")); } } @@ -629,7 +639,7 @@ void LBChronoRace::makeStartList() QObject::disconnect(wizardInfoMessages); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } } @@ -649,7 +659,7 @@ void LBChronoRace::makeRankings() QObject::disconnect(wizardInfoMessages); } catch (ChronoRaceException &e) { - appendErrorMessage(tr("Error: %1").arg(e.getMessage())); + appendErrorMessage(e.getMessage()); } } diff --git a/lbchronorace.hpp b/lbchronorace.hpp index 4e7a552..10636a0 100644 --- a/lbchronorace.hpp +++ b/lbchronorace.hpp @@ -26,7 +26,6 @@ #include "ui_chronorace.h" -#include "crloader.hpp" #include "chronoracetable.hpp" #include "chronoracedata.hpp" #include "chronoracetimings.hpp" diff --git a/pdfrankingprinter.cpp b/pdfrankingprinter.cpp index 9100cc4..221cfc0 100644 --- a/pdfrankingprinter.cpp +++ b/pdfrankingprinter.cpp @@ -317,7 +317,7 @@ bool PDFRankingPrinter::finalize() else if (painter.end()) result = true; else - emit error(tr("Error: cannot write to PDF")); + emit error(tr("Cannot write to PDF")); if (!writer.isNull()) writer.reset(Q_NULLPTR); diff --git a/rankingsbuilder.cpp b/rankingsbuilder.cpp index 1d400d8..d36c406 100644 --- a/rankingsbuilder.cpp +++ b/rankingsbuilder.cpp @@ -300,7 +300,7 @@ void RankingsBuilder::emitMessages(QStringList &messages) { for (auto const &message : messages) { if (message.length()) - emit error(tr("Warning: %1").arg(message)); + emit error(tr("Notice:: %1").arg(message)); } messages.clear(); } diff --git a/rankingswizard.cpp b/rankingswizard.cpp index f2c4f2c..4b318a8 100644 --- a/rankingswizard.cpp +++ b/rankingswizard.cpp @@ -169,7 +169,7 @@ void RankingsWizard::printStartList() QObject::disconnect(errorMessages); } } catch (ChronoRaceException &e) { - emit error(tr("Error: %1").arg(e.getMessage())); + emit error(e.getMessage()); } } @@ -224,7 +224,7 @@ void RankingsWizard::printRankingsSingleFile() QObject::disconnect(printerErrorMessages); QObject::disconnect(printerInfoMessages); } catch (ChronoRaceException &e) { - emit error(tr("Error: %1").arg(e.getMessage())); + emit error(e.getMessage()); } } @@ -286,7 +286,7 @@ void RankingsWizard::printRankingsMultiFile() QObject::disconnect(printerErrorMessages); QObject::disconnect(printerInfoMessages); } catch (ChronoRaceException &e) { - emit error(tr("Error: %1").arg(e.getMessage())); + emit error(e.getMessage()); } } diff --git a/startlistmodel.cpp b/startlistmodel.cpp index be370d3..cfd8103 100644 --- a/startlistmodel.cpp +++ b/startlistmodel.cpp @@ -198,8 +198,8 @@ bool StartListModel::setData(QModelIndex const &index, QVariant const &value, in Competitor::Sex sex = CRHelper::toSex(value.toString().trimmed()); startList[index.row()].setSex(sex); retval = true; - } catch (ChronoRaceException &ex) { - emit error(ex.getMessage()); + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); retval = false; } break; diff --git a/timingsmodel.cpp b/timingsmodel.cpp index 9c82b2b..fa5996e 100644 --- a/timingsmodel.cpp +++ b/timingsmodel.cpp @@ -121,8 +121,8 @@ bool TimingsModel::setData(QModelIndex const &index, QVariant const &value, int try { timings[index.row()].setTiming(value.toString().trimmed()); retval = true; - } catch (ChronoRaceException &ex) { - emit error(ex.getMessage()); + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); retval = false; } break; diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index 7d2a242..fb92ed9 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -719,8 +719,8 @@ Continue? Information - Errors - Errors + Warnings/Errors + Warnings/Errors Make Start List @@ -818,10 +818,6 @@ Continue? Timings List Timings List - - Error: %1 - Error: %1 - CSV (*.csv) CSV (*.csv) @@ -1030,10 +1026,6 @@ Continue? Set encoding for CSV and Plain Text Set encoding for CSV and Plain Text - - Error: unexpected encoding value (fall back to the default) - Error: unexpected encoding value (fall back to the default) - Settings Settings @@ -1042,10 +1034,6 @@ Continue? CSV and Plain Text Encoding CSV and Plain Text Encoding - - Error: unexpected encoding value (encoding not changed) - Error: unexpected encoding value (encoding not changed) - Rankings Rankings @@ -1083,6 +1071,14 @@ Please update the application. Data format %1 not supported. Please update the application. + + Unexpected encoding value (fall back to the default) + Unexpected encoding value (fall back to the default) + + + Unexpected encoding value (encoding not changed) + Unexpected encoding value (encoding not changed) +
MultiSelectComboBox @@ -1236,29 +1232,13 @@ Please update the application. Error: drawing attempt on inactive painter Error: drawing attempt on inactive painter - - Error: cannot write to PDF - Error: cannot write to PDF - Created with %1 %2 Created with %1 %2 - Unknown - Unknown - - - ???? - ???? - - - ? - ? - - - ?:??:?? - ?:??:?? + Cannot write to PDF + Cannot write to PDF @@ -1356,8 +1336,8 @@ Please update the application. Bib %1 not inserted in results; check for possible duplicated entries - Warning: %1 - Warning: %1 + Notice:: %1 + Notice:: %1 Warning: the number of timings (%1) is not match the expected (%2); check for possible missing or duplicated entries @@ -1425,10 +1405,6 @@ Please update the application. Generated Start List: %1 Generated Start List: %1 - - Error: %1 - Error: %1 - Select Results Destination Folder Select Results Destination Folder diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index d3472c9..218e9aa 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -719,8 +719,8 @@ Continuare? Informazioni - Errors - Errori + Warnings/Errors + Avvisi/Errori Make Start List @@ -818,10 +818,6 @@ Continuare? Timings List Lista Tempi - - Error: %1 - Errore: %1 - CSV (*.csv) CSV (*.csv) @@ -1030,10 +1026,6 @@ Continuare? Set encoding for CSV and Plain Text Imposta codifica per CSV e Testo - - Error: unexpected encoding value (fall back to the default) - Errore: valore codifica inatteso (ripiego sulla codifica predefinita) - Settings Impostazioni @@ -1042,10 +1034,6 @@ Continuare? CSV and Plain Text Encoding Codifica CVS e Testo - - Error: unexpected encoding value (encoding not changed) - Errore: valore codifica inatteso (codifica non modificata) - Rankings Classifiche @@ -1083,6 +1071,14 @@ Please update the application. Formato dati %1 non supportato. Aggiornare l&apos;applicazione. + + Unexpected encoding value (fall back to the default) + Valore codifica inatteso (ripiego sulla codifica predefinita) + + + Unexpected encoding value (encoding not changed) + Valore codifica inatteso (codifica non modificata) + MultiSelectComboBox @@ -1236,29 +1232,13 @@ Aggiornare l&apos;applicazione. Error: drawing attempt on inactive painter Errore: tentativo d'uso di painter non attivo - - Error: cannot write to PDF - Error: impossibile scrivere su PDF - Created with %1 %2 Creato con %1 %2 - Unknown - Sconosciuto - - - ???? - ???? - - - ? - ? - - - ?:??:?? - ?:??:?? + Cannot write to PDF + Impossibile scrivere il PDF @@ -1356,8 +1336,8 @@ Aggiornare l&apos;applicazione. Pettorale %1 non inserito in classifica; possibile duplicazione nella lista tempi - Warning: %1 - Attenzione: %1 + Notice:: %1 + Avviso:: %1 Warning: the number of timings (%1) is not match the expected (%2); check for possible missing or duplicated entries @@ -1425,10 +1405,6 @@ Aggiornare l&apos;applicazione. Generated Start List: %1 Generato Ordine di Partenza: %1 - - Error: %1 - Errore: %1 - Select Results Destination Folder Seleziona Cartella Destinazione Classifiche From 581f346b1c958ca092f54e8fcf16a09efdc01b0f Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Tue, 6 Aug 2024 11:06:13 +0200 Subject: [PATCH 10/20] Add in-app link for supporting the project The project can be supported by donations on PayPal. A button with a specific link to PayPal has been added in the "About" dialog widget. --- CMakeLists.txt | 3 +++ images.qrc | 6 ++++++ images/btn_donateCC_LG_en.gif | Bin 0 -> 4070 bytes images/btn_donateCC_LG_it.gif | Bin 0 -> 3425 bytes lbchronorace.cpp | 12 ++++++++---- translations/LBChronoRace_en.ts | 4 ++-- translations/LBChronoRace_it.ts | 4 ++-- 7 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 images.qrc create mode 100644 images/btn_donateCC_LG_en.gif create mode 100644 images/btn_donateCC_LG_it.gif diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d5923a..bf0e531 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,6 +163,9 @@ set_property(SOURCE icons.qrc PROPERTY SKIP_AUTORCC ON) # Google Material Icons (as standard resource) list(APPEND PROJECT_SOURCES materialicons.qrc) +# Images (as standard resource) +list(APPEND PROJECT_SOURCES images.qrc) + if(WIN32) set(APP_ICON_RESOURCE_WINDOWS "${CMAKE_SOURCE_DIR}/icons/LBChronoRace.rc") qt_add_executable(${CMAKE_PROJECT_NAME} WIN32 diff --git a/images.qrc b/images.qrc new file mode 100644 index 0000000..ae5eda0 --- /dev/null +++ b/images.qrc @@ -0,0 +1,6 @@ + + + images/btn_donateCC_LG_en.gif + images/btn_donateCC_LG_it.gif + + diff --git a/images/btn_donateCC_LG_en.gif b/images/btn_donateCC_LG_en.gif new file mode 100644 index 0000000000000000000000000000000000000000..7dd3a28cca4ba182cf24231fd93d1b314701dbe6 GIT binary patch literal 4070 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 WWy>;iH^;fyie=#Ea2kySw*4P#{3YiA literal 0 HcmV?d00001 diff --git a/images/btn_donateCC_LG_it.gif b/images/btn_donateCC_LG_it.gif new file mode 100644 index 0000000000000000000000000000000000000000..5a9250e32f5d7b98db201d03b0a29eb0edaf41d8 GIT binary patch literal 3425 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 literal 0 HcmV?d00001 diff --git a/lbchronorace.cpp b/lbchronorace.cpp index 54a86bd..a254f5e 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -685,11 +685,11 @@ void LBChronoRace::actionAbout() ); std::ignore = translatedTextAboutQtMessage; - QString const translatedTextAboutQtCaption = QMessageBox::tr( + QString const translatedTextAboutCaption = QMessageBox::tr( "

About %1

" "

Software for producing the results of footraces.

" ).arg(QStringLiteral(LBCHRONORACE_NAME)); - QString const translatedTextAboutQtText = QMessageBox::tr( + QString const translatedTextAboutText = QMessageBox::tr( "

Copyright© 2021-2022

" "

Version: %1 (source code on GitHub)

" "

Author: Lorenzo Buzzi (lorenzo@buzzi.pro)

" @@ -702,11 +702,15 @@ void LBChronoRace::actionAbout() "See the GNU General Public License for more details.

" "

You should have received a copy of the GNU General Public License along with %2. " "If not, see: https://www.gnu.org/licenses/.

" + "

" + "" + "" + "
If you found this application useful
and want to support its development,
you can make a donation:

" ).arg(QStringLiteral(LBCHRONORACE_VERSION), QStringLiteral(LBCHRONORACE_NAME)); QMessageBox msgBox(this); msgBox.setWindowTitle(tr("About %1").arg(QStringLiteral(LBCHRONORACE_NAME))); - msgBox.setText(translatedTextAboutQtCaption); - msgBox.setInformativeText(translatedTextAboutQtText); + msgBox.setText(translatedTextAboutCaption); + msgBox.setInformativeText(translatedTextAboutText); if (QPixmap pm(QStringLiteral(":/icons/LBChronoRace.png")); !pm.isNull()) { msgBox.setIconPixmap(pm); } diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index fb92ed9..839f872 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -1252,8 +1252,8 @@ Please update the application. <h3>About %1</h3><p>Software for producing the results of footraces.</p>
- <p>Copyright&copy; 2021-2022</p><p>Version: %1 (source code on <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Author: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Site: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.</p><p>%2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p><p>You should have received a copy of the GNU General Public License along with %2. If not, see: <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p> - <p>Copyright&copy; 2021-2022</p><p>Version: %1 (source code on <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Author: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Site: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.</p><p>%2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p><p>You should have received a copy of the GNU General Public License along with %2. If not, see: <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p> + <p>Copyright&copy; 2021-2022</p><p>Version: %1 (source code on <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Author: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Site: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.</p><p>%2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p><p>You should have received a copy of the GNU General Public License along with %2. If not, see: <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p><p><table><tbody><tr><td>If you found this application useful<br>and want to support its development,<br>you can make a donation:</td><td><a href="https://www.paypal.com/donate/?hosted_button_id=8NZWAMWPKCA7C"><img src=":/images/PayPal_Donate_en.gif" /></a></td></tr></tbody></table></p> + <p>Copyright&copy; 2021-2022</p><p>Version: %1 (source code on <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Author: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Site: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.</p><p>%2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p><p>You should have received a copy of the GNU General Public License along with %2. If not, see: <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p><p><table><tbody><tr><td>If you found this application useful<br>and want to support its development,<br>you can make a donation:</td><td><a href="https://www.paypal.com/donate/?hosted_button_id=8NZWAMWPKCA7C"><img src=":/images/PayPal_Donate_en.gif" /></a></td></tr></tbody></table></p>
diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 218e9aa..9713353 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -1252,8 +1252,8 @@ Aggiornare l&apos;applicazione. <h3>Informazioni su %1</h3><p>Software per produrre classifiche di corse podistiche.</p> - <p>Copyright&copy; 2021-2022</p><p>Version: %1 (source code on <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Author: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Site: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.</p><p>%2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p><p>You should have received a copy of the GNU General Public License along with %2. If not, see: <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p> - <p>Copyright&copy; 2021-2022</p><p>Versione: %1 (codice sorgente su <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Autore: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Sito: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 è software libero; ne è consentita la redistribuzione e/o modifica entro i termini della GNU General Public License, come pubblicata dalla Free Software Foundation, versione 3 o successiva.</p><p>%2 è distribuito sperando sia utile, ma SENZA ALCUNA GARANZIA, espressa o implicita, di COMMERCIABILITÀ o di IDONEITÀ AD UNO SCOPO PARTICOLARE. Si veda la GNU General Public License per ulteriori dettagli.</p><p>Se non di dovesse aver ricevuto una copia della GNU General Public License insieme a %2, si veda <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p> + <p>Copyright&copy; 2021-2022</p><p>Version: %1 (source code on <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Author: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Site: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.</p><p>%2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p><p>You should have received a copy of the GNU General Public License along with %2. If not, see: <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p><p><table><tbody><tr><td>If you found this application useful<br>and want to support its development,<br>you can make a donation:</td><td><a href="https://www.paypal.com/donate/?hosted_button_id=8NZWAMWPKCA7C"><img src=":/images/PayPal_Donate_en.gif" /></a></td></tr></tbody></table></p> + <p>Copyright&copy; 2021-2022</p><p>Versione: %1 (codice sorgente su <a href="http://github.com/flinco/LBChronoRace">GitHub</a>)</p><p>Autore: Lorenzo Buzzi (<a href="mailto:lorenzo@buzzi.pro">lorenzo@buzzi.pro</a>)</p><p>Sito: <a href="http://www.buzzi.pro/">http://www.buzzi.pro/</a></p><p>%2 è software libero; ne è consentita la redistribuzione e/o modifica entro i termini della GNU General Public License, come pubblicata dalla Free Software Foundation, versione 3 o successiva.</p><p>%2 è distribuito sperando sia utile, ma SENZA ALCUNA GARANZIA, espressa o implicita, di COMMERCIABILITÀ o di IDONEITÀ AD UNO SCOPO PARTICOLARE. Si veda la GNU General Public License per ulteriori dettagli.</p><p>Se non di dovesse aver ricevuto una copia della GNU General Public License insieme a %2, si veda <a href="https://www.gnu.org/licenses/">https://www.gnu.org/licenses/</a>.</p><p><table><tbody><tr><td>Se trovi utile questa applicazione<br>e vuoi sostenerne lo sviluppo,<br>puoi fare una donazione:</td><td><a href="https://www.paypal.com/donate/?hosted_button_id=B3TPDBK3ZF5YJ"><img src=":/images/PayPal_Donate_it.gif" /></a></td></tr></tbody></table></p> From 6d70eedb1fedc07c11b8643952d1b93bf62a0059 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Wed, 7 Aug 2024 23:41:56 +0200 Subject: [PATCH 11/20] Update preview image --- images/lbchronorace-preview.png | Bin 113429 -> 146648 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/images/lbchronorace-preview.png b/images/lbchronorace-preview.png index 47a8a0dd00b5f27b7204f9fe32d50547fe4105ec..188fda1eaf9aa4c2e066770fd02e90168acf8d53 100644 GIT binary patch literal 146648 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

p(@01B}s7dm38uq)xUbw&X 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≠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 literal 113429 zcmeFa30PBC*EW2hq7@vfBB&s=RY7J!Wgcp+QUyejc~&No8Du6TtyNS6RAd&SQb1-1 zLV!SmRfrG<6(NKGK@cJa2oN(7l6)tCR?w&K^ZwKKf7kou(hGAq=j^@LUVE*z?|ZH7 z_0y+}H;V2O1pr{g z-*1il#!mjb`Zs{xEz55RA_UX#);&>wQ|9jt+)a%)iQ)K6g|zg%691~#Nu>A4WYwsx0$aC$x7nrd8ktw z4{5Z(7mVk#kLylxn9yh60l@Xh|C}ppx5Lz&>JR-|qq;%_!0^C*;h9?x)@)dL8Rc|A zsCJ=jYhV?b#2R_)&YjC0VZRN56+$SRNor^gE*VN+_>jQt_W1lc*Xu2bzS)i?P}ckC z=FKu(MD4;{)&$w?g%}X%Kvc5swe0JEqE}z$U{$LI0#LDZ@iHleXxzj^PU~- zk1W7slj|FpMyyAy&f-q`E3i+W^{1j-a}TGa!_yreRC)e4WWf4}_HgEtD-y7Sgn3S8 zDG;MsUy4+I1tBMxhu=NjW!&gn0O)s43kUA#oZS!Uq?;SPb9qP}+5fLzI(t<-67S4Y z>97EJZUf`|n=!;7Xi(n) zBH9>tny2E_Ck3mjd?XzBl-YT*&Ulor$JQXtKZT2z$DN;ep_QWuB%M{r({${*cm_q{ z{#C~#*Rz>%UM8#NO+D;2ejC1{3ZK^(y?>1KN?Bw$$%t1QF_vO;ir=fxcmTvWU-DJO zLdci9MG{*m;a4T!iNmlH7AIA*UBdHTKhT9!W;mlNO}IaS&vmg0-FI`cN;B)_hw#+g z(&6Y|!FRtCp2_2nzM6oaY9Yg+&JD9IQq3+4%`V~??o>d$%fg6c0bIP<2g;=8@Fv_R zj|tVrpr{iTb~>u5C$Onxu-Tm!C#@o+^J#U70YYIeYIlSWd^YI-=lnrLSHJJ^ZjxPU z$-C$aV3Uu8XN-6le$Z^V^SoZOiCUv&1Eb%knZ${Pma~%hvqln|W%T#+x^m%M41ZWw zyvv9mGg}4cmaxeD1-K-?>m`|wC(Sz)kcKc?0G=e_pr9oImmsZ&Z#<(0q*3^t>s;<( zj#Nuy79P@7o_YoBF@zA`(~PkZ4DMCoaauX=PnJJK&v>)|v+$TTov5|Tyt3szEEXC7)qLdEb}1{bB6hzEmcp18Y^+^ zqa9|&nMbn>nQixJy@BQs^1J@#QENb8@IQWBTbFeI5I1|QP%?IN>4L*9?%H7vlPXE# zF_7_6TK?kZIr<9dg*jZe`KgvJD5L5jtC6I{dkST3ZpQHN{P_)-@(K zLY;2>39)Q@3=Sru(R%I8(SqvVM~*A6Ye8<4P3l*dmgE~g+PLd&^72VEmy+fTP}ce7((RJ!Kzn zKhF?5)-tr&@9KaOx6^@xF>X7PO56Uq^@knKigG6opZ7h^u*>SWFt3LbH))Eq=Ba=N z3Od^?*(}R_DZ3lPWfqsCb-CV8S>S*%ta!|RG4oGiUN{*~{Q& zr5uvVOcqYjrSCPU*C7TY7B+Gqw{<09PuXSga!>+FqyU}p6pd&WyN~mr<5sjsUnDX1 z>};#UZ{|jZyG5HmPA~YgX_uJ+!Y@$$(>!}v0H?HPgExcuIM0k+`BHeu0E%-Ukgjc0yMoDgz@*wUmnWQB4 znyqnGvJ?C09wnb!Dt;PiLX?awx9$vJv5ybnqt=JDb6SWW{)6; zr;h5UmfRoM8(UsI+;3sgc)?F(IM^tOzQD+6L-2nGF{wK}NezK6vabEp4&j(;W)&!c}jFISd`|XU0^g z$YZBzcR5>)a9BxW(oUB^U)}Lp=9_8GyUBTma>6M;W}Ts|S>CJo1{_>Qt3mCSe0Q~a<)u{>Hb zG?qhbPIHNkoFnHhxf{jiC-zUM4J+EYj3p*KJWW}WJMq2YJVk1e++#u&8QeZ z>L9Rv)9bRPE~)9{G_*40o5P1#k0Q92&ExS<_OY?jnhxes=8qR97NQM~Cq2AKm+3st z=O9glSOu~%chNf0qYtS)G!pZDeLs2|y-~J*L!-^fvv6btbd_6n&$mdSfF!;=I}g>M4R$jkT-H5m`3fs9y@R9L(wL^nyQzK zt1lM&eP}yOtFyDCjN&BmqmlMtdG8q&8-_t@LKT(16C&1RjHPvt{&jJ@dyB|$ZipQ; zIt&pr4)!tsVZd2+)+FsW&@ew>p7efC%m11yG~45l>XmC=$vl0sDOa=!O1FWA&KEpn zG5I_aoaY_gKD5remCDRz>gpMc_0Vdx1_AMAuUyh z^Pi&_0=qCHXdYD?N%NfjOH56g521C9g7yKl6a*uV7nZ*ez@Nfw$M88A=mNQ;FH3la z5bPI_tzsN8rDQUE8I|M}tyOczAb`VK}BA8=@O9ah zSTHyEHdfmxHI{iEPlZr=ayhdJ44>#N^*IzxFKo;a=KB^xW5naZj{2cTCCS`A>J7}opZ@*6Jvc|2BRH~`JgXKC@rN#guD7ap!VugEP`cqW?}Pj>Np z$&NNsKyz9O9t_|sEo*mT7Dj3xv9oJC^?E{k@mk&)VaA1XwS6(x-|6?K<&MEO?|Txk zdx{!9GOIP&EzZ&JL4Q!lYoTn%!Dq=>PS89JaoR4m21nI1xEw93~f z8+v}6Xl<;bH*QYswTW#Q?RWkoUKubCh59~&@F!-Yvf@#0)VR6Kl-vdXg-CDEJ+T|I zxtvmIc|LXxfhUPs_%OMkxMC9kC_i-<8UDd-UU+7DiNh6z^B&#Tnz8i?jJiY3f-YO*(#Xvn@3YP#v9pY>lMf<;KCnuQP>K8M z412Z)(rkY|LHkjxDNkWKDWTM70Krvf9;HW(lf9oez9Khdr^IBnsNo=nM#on!U6{uX zOMu6L@~P(MXGMlx{4pt2@rZ911~+R4NNyaBfWT!ie%jojH6HzjV@(OkBXcj&*|Bu@ zyPWx#vr;#!sJ^C)A?bv}$pkzm*k$3|R03j5yT4#=W6>Dd>`K?Ww;PM)u@*bWqAmt7 zOQF;VVryeJqcJ}0rO0rCI@9=4>_*hx<~Y)3zW`w9U3B>eg8+|K7K>zD;G!p!6`BT@ z^>bOV>i}Hsz-{g4%LV{okt?k_80Gx_KlzH|ybGTKT@iGEBGLGtERrwBUQz%oj3

    OzFlPy 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 z z!>tHUJ!Z58N2H{>2JS??*(lJC1k~?|R#rVfy>GZxkLYxUMQxs}NNB5~B6NMoht_iR zU-zS3^t-olTjXNDe(PVjC)r}BX$e{WjXBy=4ZSPz*n+>?JFlmRw?UG#R3&H=UP-#0 zBzB`CQzn(H4Q zqAv&N%7zd(vxmg^dbRoJ1Dq=_Mn!pTMZ)oiXm0VC1uDHedPwd}`ITQWa00^^uvgq$ z3n)U_4rW@GAU%7a`0n6)Mx!USDY11lMLJl54cst?F($S~h)-EnrQwPGo`KixDy}!# zCfuTvM3(eu zNPGjuWEC{xDMzoD<1;VIjEzjeCuk>U1Gf-J{Ok1IA7Hs?(;#^NhxHB}VX+9xpy*C0 zzH)4|M~B@yDI~in@&|QK`9}^(*?kxdY}+udVK4Fi$W@^*O^r-%Vi2?ZbbL58MtBxR zwrVqu%Slb$a`#)?_MuAqQC-hK)7h-3Wlg4)eg<1~$fCP@5|gud1A*%!#zLH~x4<%1 zShr`cKw>QMBCp#2KD)Z*wVm+s5^Z7_SnbW}XK52l`$(G&p^X5PL5pfGCRm!%n+_iY zb6VwjJ2}o~zx3r;(L2<~f0;A27Cc*!K=j2urs4ui)Fg~;H^CBmskdOnUj_dIdXv5~ z9_sDmk9m4cTOln{;^XU~H){hz7?C!Y1ut>*=4bTVwnr_8B&Nn^=4n$WvI$mKly9rM zoTXZX`bQvKce7Qn>5BpCW=rEcb2Q!SLte?D`lKr#8NdD?tdqQL>e3fdT`#{A=8FBj zMe8(#4r>YcFq%SY!wMW=n#4Iw=jYt`x&dTU%pHAO`!3Dvu}%)&eze0Qd^CB@319n|=(D;6fV+tT_IPkVE^TM%l!oaM>W zIyoTd8_m6F`ro&p%Jy6TqXv2RzGc^Ob**dXKh>}cq0TNhYn~noAGE4!2$24lvHfqb zSFmL`SvY0UG#}@pPZoZG&^@K^IBKw7llliP>BJy&k)`4IAWbWMHf9muvGRxO_h(ks6@XHKitM4I-NcTc1Er935^*nF2< z%S*qM3eNE-2V#9jiD#s+#v!AD-5vt-aQF`&{CgjA!(z`Y5<0G%ZHA1Ho>!KMOFD2% z3im@(J4eR2a(70@O*C9Gs zDdVMKmri8`gda{`AM@g5z5!)3bB)tR9drKFEutzXlL7&~vHFp**5vr5nDM^swshsF zS z&QBCfsvg*-taSLWV=LgpUhPo-huPXpN*mavGbc~CI?84$ z=YWl#3_b||G?n~d@9*fzt=`i460& z5)En4M99~Af#mKRVs+Vqq;b~=jk-M%KBbblOKXOxp+R$`-7}HQjSq)A!9~yEDv70w z<0jINy5?{?*&q?owPzDAwA&3vP5;+F2o8F_%1ticc}*9-;KEV6!Lf47+b?|n7g0;2 z_IbGXL^+ku?jKXGeWP?LEa~K1nx8~tZl3egcO6tkUX1SUBYZ*q)5FI2>cv)7Gq2{= zAD+DZpTC){$bYIaTMGo@Kh^k8H6|)qexh CnB4RL From 8a86e5b7f8f33030f5b7d93dc28953386327e814 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Thu, 8 Aug 2024 16:22:58 +0200 Subject: [PATCH 12/20] Add warning message when opening file with old format --- lbchronorace.cpp | 4 ++++ translations/LBChronoRace_en.ts | 10 ++++++++++ translations/LBChronoRace_it.ts | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/lbchronorace.cpp b/lbchronorace.cpp index a254f5e..af4f48e 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -466,8 +466,12 @@ bool LBChronoRace::loadRaceFile(QString const &fileName) switch (binFmt) { case LBCHRONORACE_BIN_FMT_v1: + [[fallthrough]]; case LBCHRONORACE_BIN_FMT_v2: + [[fallthrough]]; case LBCHRONORACE_BIN_FMT_v3: + QMessageBox::warning(this, tr("Race Data File Format"), tr("This Race Data File was saved with a previous release of the application.\nThe definitions of Categories and Rankings must be reviewed and corrected.")); + [[fallthrough]]; case LBCHRONORACE_BIN_FMT_v4: QAbstractTableModel const *table; qint16 encodingIdx; diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index 839f872..c12edf9 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -1079,6 +1079,16 @@ Please update the application. Unexpected encoding value (encoding not changed) Unexpected encoding value (encoding not changed) + + Race Data File Format + Race Data File Format + + + This Race Data File was saved with a previous release of the application. +The definitions of Categories and Rankings must be reviewed and corrected. + This Race Data File was saved with a previous release of the application. +The definitions of Categories and Rankings must be reviewed and corrected. + MultiSelectComboBox diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 9713353..a3140cf 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -1079,6 +1079,16 @@ Aggiornare l&apos;applicazione. Unexpected encoding value (encoding not changed) Valore codifica inatteso (codifica non modificata) + + Race Data File Format + Formato File Dati Gara + + + This Race Data File was saved with a previous release of the application. +The definitions of Categories and Rankings must be reviewed and corrected. + Il File Dati Gara è stato salvato con una precedente versione dell'applicazione. +Le definizioni di Categorie e Classifiche devono essere riviste e corrette. + MultiSelectComboBox From da611a28538cd1074ee7fad8b274b54e2204c772 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Sun, 11 Aug 2024 12:02:19 +0200 Subject: [PATCH 13/20] Allow club reset and case correction on competitor --- clubdelegate.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/clubdelegate.cpp b/clubdelegate.cpp index b9269ad..ce460f3 100644 --- a/clubdelegate.cpp +++ b/clubdelegate.cpp @@ -15,6 +15,8 @@ * along with this program. If not, see . * *****************************************************************************/ +#include + #include "clubdelegate.hpp" #include "crloader.hpp" @@ -23,9 +25,11 @@ ClubDelegate::ClubDelegate(QObject *parent) : { auto *comboBox = clubBox.data(); comboBox->setEditable(true); + comboBox->completer()->setCaseSensitivity(Qt::CaseSensitive); comboBox->setInsertPolicy(QComboBox::InsertAlphabetically); comboBox->setDuplicatesEnabled(false); comboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); + comboBox->addItem(""); comboBox->addItems(CRLoader::getClubs()); } @@ -37,6 +41,7 @@ QWidget *ClubDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const auto *comboBox = clubBox.data(); comboBox->setParent(parent); comboBox->clear(); + comboBox->addItem(""); comboBox->addItems(CRLoader::getClubs()); return comboBox; From a58d5d6142a1ec561fe76f145b4a36e201ad8050 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Sun, 11 Aug 2024 12:22:28 +0200 Subject: [PATCH 14/20] Use background color on bib cells Now the empty bib cells in the timing recorder are marked red, so the user is suggested to fill them with a value. Confirmed bibs are marked with a green background instead. When the background of the bib cell is white, any entered digit will be added to that cell. --- chronoracetimings.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/chronoracetimings.cpp b/chronoracetimings.cpp index 6cbcbb2..e4dd2b3 100644 --- a/chronoracetimings.cpp +++ b/chronoracetimings.cpp @@ -100,6 +100,8 @@ bool ChronoRaceTimings::eventFilter(QObject *watched, QEvent *event) break; case Qt::Key_Return: case Qt::Key_Enter: + if (!bibBuffer.isEmpty() && (bibRowCount > 0)) + ui->dataArea->item(bibRowCount - 1, 0)->setBackground(Qt::green); bibBuffer.clear(); break; case Qt::Key_Backspace: @@ -212,9 +214,13 @@ void ChronoRaceTimings::recordTiming(qint64 seconds) { auto newTiming = QString("%1:%2:%3").arg(seconds / 3600).arg(seconds / 60, 2, 10, QChar('0')).arg(seconds % 60, 2, 10, QChar('0')); - if (timingRowCount == ui->dataArea->rowCount()) + if (timingRowCount == ui->dataArea->rowCount()) { ui->dataArea->setRowCount(timingRowCount + 1); + ui->dataArea->setItem(timingRowCount, 0, new QTableWidgetItem); + ui->dataArea->item(timingRowCount, 0)->setBackground(Qt::red); + } ui->dataArea->setItem(timingRowCount, 1, new QTableWidgetItem(newTiming)); + ui->dataArea->scrollToBottom(); timingRowCount += 1; } @@ -245,15 +251,20 @@ void ChronoRaceTimings::deleteBib() bibBuffer.removeLast(); if (bibBuffer.isEmpty()) { if (bibRowCount > 0) { - if (bibRowCount-- > timingRowCount) + if (bibRowCount-- > timingRowCount) { ui->dataArea->setRowCount(bibRowCount); - else + } else { ui->dataArea->item(bibRowCount, 0)->setText(bibBuffer); - if (bibRowCount) + ui->dataArea->item(bibRowCount, 0)->setBackground(Qt::red); + } + if (bibRowCount) { bibBuffer = ui->dataArea->item(bibRowCount - 1, 0)->text(); + ui->dataArea->item(bibRowCount - 1, 0)->setBackground(Qt::white); + } } } else { ui->dataArea->item(bibRowCount - 1, 0)->setText(bibBuffer); + ui->dataArea->item(bibRowCount - 1, 0)->setBackground(Qt::white); } } From 73eab582273f5973673e92c20fddcabcbd2cc9de Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Wed, 14 Aug 2024 23:27:54 +0200 Subject: [PATCH 15/20] Fix separators in export/import file path The path of the file was shown with Unix directory separators even on Windows. --- lbchronorace.cpp | 54 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/lbchronorace.cpp b/lbchronorace.cpp index af4f48e..9b5fac7 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -201,8 +201,10 @@ void LBChronoRace::appendErrorMessage(QString const &message) const void LBChronoRace::importStartList() { - startListFileName = QFileDialog::getOpenFileName(this, tr("Select Start List"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + startListFileName = QDir::toNativeSeparators( + QFileDialog::getOpenFileName(this, tr("Select Start List"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!startListFileName.isEmpty()) { QPair count(0, 0); @@ -222,8 +224,10 @@ void LBChronoRace::importStartList() void LBChronoRace::importRankingsList() { - rankingsFileName = QFileDialog::getOpenFileName(this, tr("Select Rankings File"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + rankingsFileName = QDir::toNativeSeparators( + QFileDialog::getOpenFileName(this, tr("Select Rankings File"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!rankingsFileName.isEmpty()) { int count = 0; @@ -241,8 +245,10 @@ void LBChronoRace::importRankingsList() void LBChronoRace::importCategoriesList() { - categoriesFileName = QFileDialog::getOpenFileName(this, tr("Select Categories File"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + categoriesFileName = QDir::toNativeSeparators( + QFileDialog::getOpenFileName(this, tr("Select Categories File"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!categoriesFileName.isEmpty()) { int count = 0; @@ -260,8 +266,10 @@ void LBChronoRace::importCategoriesList() void LBChronoRace::importTimingsList() { - timingsFileName = QFileDialog::getOpenFileName(this, tr("Select Timings File"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + timingsFileName = QDir::toNativeSeparators( + QFileDialog::getOpenFileName(this, tr("Select Timings File"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!timingsFileName.isEmpty()) { int count = 0; @@ -279,8 +287,10 @@ void LBChronoRace::importTimingsList() void LBChronoRace::exportStartList() { - startListFileName = QFileDialog::getSaveFileName(this, tr("Select Start List"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + startListFileName = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Select Start List"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!startListFileName.isEmpty()) { @@ -299,8 +309,10 @@ void LBChronoRace::exportStartList() void LBChronoRace::exportTeamList() { - teamsFileName = QFileDialog::getSaveFileName(this, tr("Select Clubs List"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + teamsFileName = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Select Clubs List"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!teamsFileName.isEmpty()) { @@ -319,8 +331,10 @@ void LBChronoRace::exportTeamList() void LBChronoRace::exportRankingsList() { - rankingsFileName = QFileDialog::getSaveFileName(this, tr("Select Rankings File"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + rankingsFileName = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Select Rankings File"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!rankingsFileName.isEmpty()) { @@ -339,8 +353,10 @@ void LBChronoRace::exportRankingsList() void LBChronoRace::exportCategoriesList() { - categoriesFileName = QFileDialog::getSaveFileName(this, tr("Select Categories File"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + categoriesFileName = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Select Categories File"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!categoriesFileName.isEmpty()) { @@ -359,8 +375,10 @@ void LBChronoRace::exportCategoriesList() void LBChronoRace::exportTimingsList() { - timingsFileName = QFileDialog::getSaveFileName(this, tr("Select Timings File"), - lastSelectedPath.absolutePath(), tr("CSV (*.csv)")); + timingsFileName = QDir::toNativeSeparators( + QFileDialog::getSaveFileName(this, tr("Select Timings File"), + lastSelectedPath.absolutePath(), + tr("CSV (*.csv)"))); if (!timingsFileName.isEmpty()) { From af0a8c0be3fdba1096521f5bae4c202ebaf33be5 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Sun, 11 Aug 2024 22:58:25 +0200 Subject: [PATCH 16/20] Add the DSQ (disqualified) status for competitors --- CMakeLists.txt | 2 + classentry.cpp | 64 +-- classentry.hpp | 1 + crhelper.cpp | 66 +++- crhelper.hpp | 8 +- crloader.cpp | 2 + icons/classified.svg | 1 + icons/dnf.svg | 1 + icons/dns.svg | 1 + icons/dsq.svg | 1 + .../meta/package.xml.in | 4 +- lbchronorace.cpp | 4 +- lbchronorace.hpp | 2 + materialicons.qrc | 4 + ranking.cpp | 2 +- rankingprinter.cpp | 4 +- rankingsbuilder.cpp | 4 +- samples/mass_start/latin1/timings.csv | 374 +++++++++--------- samples/mass_start/mass_start.crd | Bin 904806 -> 904806 bytes samples/mass_start/utf8/timings.csv | 374 +++++++++--------- samples/relay/latin1/timings.csv | 246 ++++++------ samples/relay/relay.crd | Bin 897954 -> 897954 bytes timing.cpp | 166 ++++++-- timing.hpp | 25 +- timingsmodel.cpp | 18 +- timingstatusdelegate.cpp | 78 ++++ timingstatusdelegate.hpp | 45 +++ translations/LBChronoRace_en.ts | 36 +- translations/LBChronoRace_it.ts | 36 +- 29 files changed, 979 insertions(+), 590 deletions(-) create mode 100644 icons/classified.svg create mode 100644 icons/dnf.svg create mode 100644 icons/dns.svg create mode 100644 icons/dsq.svg create mode 100644 timingstatusdelegate.cpp create mode 100644 timingstatusdelegate.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bf0e531..4125072 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,8 @@ set(PROJECT_SOURCES timing.hpp timingsmodel.cpp timingsmodel.hpp + timingstatusdelegate.cpp + timingstatusdelegate.hpp txtrankingprinter.cpp txtrankingprinter.hpp) diff --git a/classentry.cpp b/classentry.cpp index ed442c3..89cba34 100644 --- a/classentry.cpp +++ b/classentry.cpp @@ -205,11 +205,11 @@ QString ClassEntry::getTimes(CRLoader::Format format, int legRankWidth) const switch (format) { case CRLoader::Format::TEXT: for (QVector::ConstIterator it = entries.constBegin(); it < entries.constEnd(); it++) - retString.append(QString("%1(%2) %3").arg((it == entries.constBegin()) ? "" : " - ").arg(it->legRanking, legRankWidth).arg(CRHelper::toTimeStr(it->time, it->status), 7)); + retString.append(QString("%1(%2) %3").arg((it == entries.constBegin()) ? "" : " - ").arg(it->legRanking, legRankWidth).arg(CRHelper::toTimeString(it->time, it->status), 7)); break; case CRLoader::Format::CSV: for (QVector::ConstIterator it = entries.constBegin(); it < entries.constEnd(); it++) - retString.append(QString("%1%2,%3").arg((it == entries.constBegin()) ? "" : ",").arg(it->legRanking).arg(CRHelper::toTimeStr(it->time, Timing::Status::CLASSIFIED))); + retString.append(QString("%1%2,%3").arg((it == entries.constBegin()) ? "" : ",").arg(it->legRanking).arg(CRHelper::toTimeString(it->time, Timing::Status::CLASSIFIED))); break; case CRLoader::Format::PDF: retString = "***Error***"; @@ -227,7 +227,7 @@ QString ClassEntry::getTime(uint legIdx) const if (static_cast(legIdx) >= entries.size()) throw(ChronoRaceException(tr("Nonexistent leg %1 for bib %2").arg(legIdx + 1).arg(bib))); - return CRHelper::toTimeStr(entries[legIdx].time, entries[legIdx].status); + return CRHelper::toTimeString(entries[legIdx].time, entries[legIdx].status); } uint ClassEntry::getTimeValue(uint legIdx) const @@ -235,7 +235,16 @@ uint ClassEntry::getTimeValue(uint legIdx) const if (static_cast(legIdx) >= entries.size()) throw(ChronoRaceException(tr("Nonexistent leg %1 for bib %2").arg(legIdx + 1).arg(bib))); - return entries[legIdx].time; + switch (entries[legIdx].status) { + case Timing::Status::DNS: + return UINT_MAX; + case Timing::Status::DNF: + return UINT_MAX - 1; + //NOSONAR case Timing::Status::DSQ: + //NOSONAR return UINT_MAX - 2; + default: + return entries[legIdx].time; + } } uint ClassEntry::countEntries() const @@ -264,21 +273,23 @@ void ClassEntry::setTime(Competitor *comp, Timing const &timing, QStringList &me entries[legIndex].competitor = comp; entries[legIndex].status = timing.getStatus(); - if (isDns()) { - totalTime = UINT_MAX; - } else if (isDnf()) { + entries[legIndex].time = timing.getSeconds(); + if (isDnf()) { totalTime = UINT_MAX - 1; + } else if (isDns()) { + totalTime = UINT_MAX; } else { if (offset < 0) { // no offset; use standard timing logic // good for both individual and relay races - entries[legIndex].time = timing.getSeconds() - totalTime; - totalTime = timing.getSeconds(); + uint seconds = entries[legIndex].time; + entries[legIndex].time -= totalTime; + totalTime = seconds; } else { // competitor with offset; maybe individual // race without mass start or relay race with // timings sum - uint seconds = timing.getSeconds() - static_cast(offset); + uint seconds = entries[legIndex].time - static_cast(offset); entries[legIndex].time = seconds; totalTime += seconds; } @@ -380,6 +391,13 @@ bool ClassEntry::isDns() const }); } +bool ClassEntry::isDsq() const +{ + return std::any_of(entries.constBegin(), entries.constEnd(), [&](ClassEntryElement const &el) { + return (el.status == Timing::Status::DSQ); + }); +} + Category const *ClassEntry::getCategory() const { return category; @@ -414,7 +432,7 @@ QStringList ClassEntry::setCategory() uint ClassEntry::getTotalTime() const { - return totalTime; + return isDsq() ? (UINT_MAX - 2) : totalTime; } QString ClassEntry::getTotalTime(CRLoader::Format format) const @@ -427,12 +445,14 @@ QString ClassEntry::getTotalTime(CRLoader::Format format) const case CRLoader::Format::CSV: [[fallthrough]]; case CRLoader::Format::PDF: - if (isDns()) - retString = CRHelper::toTimeStr(totalTime, Timing::Status::DNS); + if (isDsq()) + retString = CRHelper::toTimeString(totalTime, Timing::Status::DSQ); else if (isDnf()) - retString = CRHelper::toTimeStr(totalTime, Timing::Status::DNF); + retString = CRHelper::toTimeString(totalTime, Timing::Status::DNF); + else if (isDns()) + retString = CRHelper::toTimeString(totalTime, Timing::Status::DNS); else - retString = CRHelper::toTimeStr(totalTime, Timing::Status::CLASSIFIED); + retString = CRHelper::toTimeString(totalTime, Timing::Status::CLASSIFIED); break; default: Q_UNREACHABLE(); @@ -444,19 +464,19 @@ QString ClassEntry::getTotalTime(CRLoader::Format format) const QString ClassEntry::getDiffTimeTxt(uint referenceTime) const { - if (isDns() || isDnf() || (totalTime == referenceTime)) + if (isDsq() || isDns() || isDnf() || (totalTime == referenceTime)) return QString(""); if (totalTime > referenceTime) - return CRHelper::toTimeStr(totalTime - referenceTime, Timing::Status::CLASSIFIED, "+"); + return CRHelper::toTimeString(totalTime - referenceTime, Timing::Status::CLASSIFIED, "+"); else - return CRHelper::toTimeStr(referenceTime - totalTime, Timing::Status::CLASSIFIED, "-"); + return CRHelper::toTimeString(referenceTime - totalTime, Timing::Status::CLASSIFIED, "-"); } -bool ClassEntry::operator< (ClassEntry const &rhs) const { return totalTime < rhs.totalTime; } -bool ClassEntry::operator> (ClassEntry const &rhs) const { return totalTime > rhs.totalTime; } -bool ClassEntry::operator<=(ClassEntry const &rhs) const { return totalTime <= rhs.totalTime; } -bool ClassEntry::operator>=(ClassEntry const &rhs) const { return totalTime >= rhs.totalTime; } +bool ClassEntry::operator< (ClassEntry const &rhs) const { return getTotalTime() < rhs.getTotalTime(); } +bool ClassEntry::operator> (ClassEntry const &rhs) const { return getTotalTime() > rhs.getTotalTime(); } +bool ClassEntry::operator<=(ClassEntry const &rhs) const { return getTotalTime() <= rhs.getTotalTime(); } +bool ClassEntry::operator>=(ClassEntry const &rhs) const { return getTotalTime() >= rhs.getTotalTime(); } bool ClassEntryHelper::allCompetitorsShareTheSameClub(QVector const &entries, qsizetype fromLeg, qsizetype toLeg, QString const &club) { diff --git a/classentry.hpp b/classentry.hpp index 5f19e6c..1471606 100644 --- a/classentry.hpp +++ b/classentry.hpp @@ -90,6 +90,7 @@ class ClassEntry { QString getDiffTimeTxt(uint referenceTime) const; bool isDnf() const; bool isDns() const; + bool isDsq() const; Category const *getCategory() const; Category const *getCategory(uint legIdx) const; diff --git a/crhelper.cpp b/crhelper.cpp index 6a1e540..1fe8b8e 100644 --- a/crhelper.cpp +++ b/crhelper.cpp @@ -220,14 +220,17 @@ QString CRHelper::toCategoryTypeString(Category::Type const type) } } -QString CRHelper::toTimeStr(uint const seconds, Timing::Status const status, char const *prefix) +QString CRHelper::toTimeString(uint const seconds, Timing::Status const status, char const *prefix) { + QString retString { prefix ? prefix : "" }; - QString retString(prefix ? prefix : ""); switch (status) { case Timing::Status::CLASSIFIED: retString.append(QString("%1:%2:%3").arg(((seconds / 60) / 60)).arg(((seconds / 60) % 60), 2, 10, QLatin1Char('0')).arg((seconds % 60), 2, 10, QLatin1Char('0'))); break; + case Timing::Status::DSQ: + retString.append("DSQ"); + break; case Timing::Status::DNF: retString.append("DNF"); break; @@ -237,10 +240,65 @@ QString CRHelper::toTimeStr(uint const seconds, Timing::Status const status, cha default: throw(ChronoRaceException(tr("Invalid status value %1").arg(static_cast(status)))); } + return retString; } -QString CRHelper::toTimeStr(Timing const &timing) +QString CRHelper::toTimeString(Timing const &timing) +{ + return toTimeString(timing.getSeconds(), timing.getStatus()); +} + +Timing::Status CRHelper::toTimingStatus(QString const &status) +{ + if (status.compare("CLS", Qt::CaseInsensitive) == 0) + return Timing::Status::CLASSIFIED; + else if (status.compare("DSQ", Qt::CaseInsensitive) == 0) + return Timing::Status::DSQ; + else if (status.compare("DNF", Qt::CaseInsensitive) == 0) + return Timing::Status::DNF; + else if (status.compare("DNS", Qt::CaseInsensitive) == 0) + return Timing::Status::DNS; + else + throw(ChronoRaceException(tr("Illegal status value '%1'").arg(status))); +} + +QString CRHelper::toStatusString(Timing::Status const status) { - return toTimeStr(timing.getSeconds(), timing.getStatus()); + QString retString { "" }; + + switch (status) { + case Timing::Status::CLASSIFIED: + retString.append("CLS"); + break; + case Timing::Status::DSQ: + retString.append("DSQ"); + break; + case Timing::Status::DNF: + retString.append("DNF"); + break; + case Timing::Status::DNS: + retString.append("DNS"); + break; + default: + throw(ChronoRaceException(tr("Invalid status value %1").arg(static_cast(status)))); + } + + return retString; +} + +QString CRHelper::toStatusFullString(Timing::Status const status) +{ + switch (status) { + case Timing::Status::CLASSIFIED: + return tr("Classified"); + case Timing::Status::DSQ: + return tr("Disqualified"); + case Timing::Status::DNF: + return tr("Did not finish"); + case Timing::Status::DNS: + return tr("Did not start"); + default: + throw(ChronoRaceException(tr("Invalid status value %1").arg(static_cast(status)))); + } } diff --git a/crhelper.hpp b/crhelper.hpp index 1f89f19..9b8884b 100644 --- a/crhelper.hpp +++ b/crhelper.hpp @@ -54,8 +54,12 @@ class CRHelper static QString toRankingTypeString(Ranking::Type type); static QString toCategoryTypeString(Category::Type const type); - static QString toTimeStr(uint const seconds, Timing::Status const status, char const *prefix = Q_NULLPTR); - static QString toTimeStr(Timing const &timing); + static QString toTimeString(uint const seconds, Timing::Status const status, char const *prefix = Q_NULLPTR); + static QString toTimeString(Timing const &timing); + + static Timing::Status toTimingStatus(QString const &status); + static QString toStatusString(Timing::Status const status); + static QString toStatusFullString(Timing::Status const status); }; #endif // CRHELPER_H diff --git a/crloader.cpp b/crloader.cpp index dc8b0ed..67bf3c8 100644 --- a/crloader.cpp +++ b/crloader.cpp @@ -239,6 +239,8 @@ void CRLoader::addTiming(QString const &bib, QString const &timing) temp = "0"; checkString(&timingsModel, temp, ','); temp = timing; + checkString(&timingsModel, temp, ','); + temp = "CLS"; checkString(&timingsModel, temp); } diff --git a/icons/classified.svg b/icons/classified.svg new file mode 100644 index 0000000..9b92c68 --- /dev/null +++ b/icons/classified.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/dnf.svg b/icons/dnf.svg new file mode 100644 index 0000000..2471b3f --- /dev/null +++ b/icons/dnf.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/dns.svg b/icons/dns.svg new file mode 100644 index 0000000..b758426 --- /dev/null +++ b/icons/dns.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icons/dsq.svg b/icons/dsq.svg new file mode 100644 index 0000000..c6880f7 --- /dev/null +++ b/icons/dsq.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/installer/packages/pro.buzzi.lbchronorace.samples/meta/package.xml.in b/installer/packages/pro.buzzi.lbchronorace.samples/meta/package.xml.in index 58dadd9..b6879f6 100644 --- a/installer/packages/pro.buzzi.lbchronorace.samples/meta/package.xml.in +++ b/installer/packages/pro.buzzi.lbchronorace.samples/meta/package.xml.in @@ -4,8 +4,8 @@ Esempi Ranking data samples. Dati di esempio. - 1.0.2 - 2024-05-03 + 1.0.3 + 2024-08-22 pro.buzzi.lbchronorace.samples pro.buzzi.lbchronorace true diff --git a/lbchronorace.cpp b/lbchronorace.cpp index 9b5fac7..0399cc4 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -48,7 +48,8 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : clubDelegate(&startListTable), rankingTypeDelegate(&rankingsTable), rankingCatsDelegate(&rankingsTable), - categoryTypeDelegate(&categoriesTable) + categoryTypeDelegate(&categoriesTable), + timingStatusDelegate(&timingsTable) { startListFileName = lastSelectedPath.filePath(LBCHRONORACE_STARTLIST_DEFAULT); timingsFileName = lastSelectedPath.filePath(LBCHRONORACE_TIMINGS_DEFAULT); @@ -149,6 +150,7 @@ LBChronoRace::LBChronoRace(QWidget *parent, QGuiApplication const *app) : rankingsTable.setItemDelegateForColumn(static_cast(Ranking::Field::RTF_TEAM), &rankingTypeDelegate); rankingsTable.setItemDelegateForColumn(static_cast(Ranking::Field::RTF_CATEGORIES), &rankingCatsDelegate); categoriesTable.setItemDelegateForColumn(static_cast(Category::Field::CTF_TYPE), &categoryTypeDelegate); + timingsTable.setItemDelegateForColumn(static_cast(Timing::Field::TMF_STATUS), &timingStatusDelegate); } void LBChronoRace::setCounterTeams(int count) const diff --git a/lbchronorace.hpp b/lbchronorace.hpp index 10636a0..2f14d94 100644 --- a/lbchronorace.hpp +++ b/lbchronorace.hpp @@ -34,6 +34,7 @@ #include "rankingtypedelegate.hpp" #include "rankingcatsdelegate.hpp" #include "cattypedelegate.hpp" +#include "timingstatusdelegate.hpp" #ifndef LBCHRONORACE_NAME #error "LBCHRONORACE_NAME not set" @@ -105,6 +106,7 @@ public slots: RankingTypeDelegate rankingTypeDelegate; RankingCategoriesDelegate rankingCatsDelegate; CategoryTypeDelegate categoryTypeDelegate; + TimingStatusDelegate timingStatusDelegate; bool loadRaceFile(QString const &fileName); diff --git a/materialicons.qrc b/materialicons.qrc index 29ab8fb..883957c 100644 --- a/materialicons.qrc +++ b/materialicons.qrc @@ -46,5 +46,9 @@ icons/more.svg icons/rule.svg icons/user_attributes.svg + icons/classified.svg + icons/dnf.svg + icons/dns.svg + icons/dsq.svg diff --git a/ranking.cpp b/ranking.cpp index fd2afbc..45d5500 100644 --- a/ranking.cpp +++ b/ranking.cpp @@ -49,7 +49,7 @@ QDataStream &operator>>(QDataStream &in, Ranking &ranking) >> team32 >> ranking.categories; - ranking.team = (bool) team32; + ranking.team = static_cast(team32); return in; } diff --git a/rankingprinter.cpp b/rankingprinter.cpp index 3f35e0f..24dc9e2 100644 --- a/rankingprinter.cpp +++ b/rankingprinter.cpp @@ -64,8 +64,8 @@ int Position::getCurrentPositionNumber(int posIndex, QString const &currTime) int returnedPosition = 0; // Return 0 if the position has not changed - if ((posIndex == 1) || (time.compare(currTime) != 0) || (currTime.startsWith("DN"))) { - // First entry, DNS, DNF, or Different time + if ((posIndex == 1) || (time.compare(currTime) != 0) || (currTime.startsWith("D"))) { + // First entry, DNS, DNF, DSQ, or Different time time = currTime; returnedPosition = position = posIndex; } diff --git a/rankingsbuilder.cpp b/rankingsbuilder.cpp index d36c406..4c9b8eb 100644 --- a/rankingsbuilder.cpp +++ b/rankingsbuilder.cpp @@ -133,8 +133,8 @@ QList &RankingsBuilder::fillRanking(QListisDns() || classEntry->isDnf()) { + // exclude DNS, DNF, and DSQ + if (classEntry->isDns() || classEntry->isDnf() || classEntry->isDsq()) { continue; } diff --git a/samples/mass_start/latin1/timings.csv b/samples/mass_start/latin1/timings.csv index 404aa43..8ca3fd1 100644 --- a/samples/mass_start/latin1/timings.csv +++ b/samples/mass_start/latin1/timings.csv @@ -1,187 +1,187 @@ -113,0,0:33:05 -34,0,0:33:26 -135,0,0:33:39 -107,0,0:33:46 -109,0,0:34:04 -110,0,0:34:12 -3,0,0:34:22 -46,0,0:35:05 -238,0,0:35:19 -264,0,0:35:22 -12,0,0:35:25 -48,0,0:35:50 -39,0,0:36:09 -31,0,0:36:26 -225,0,0:36:39 -51,0,0:36:40 -219,0,0:36:53 -159,0,0:36:55 -11,0,0:36:59 -56,0,0:37:06 -252,0,0:37:11 -214,0,0:37:20 -106,0,0:37:25 -53,0,0:37:26 -205,0,0:37:37 -231,0,0:37:42 -5,0,0:37:47 -108,0,0:37:58 -101,0,0:38:01 -226,0,0:38:03 -246,0,0:38:05 -150,0,0:38:10 -127,0,0:38:13 -131,0,0:38:19 -59,0,0:38:22 -43,0,0:38:37 -247,0,0:38:51 -130,0,0:38:54 -50,0,0:39:06 -229,0,0:39:09 -146,0,0:39:13 -250,0,0:39:15 -8,0,0:39:17 -45,0,0:39:25 -228,0,0:39:27 -265,0,0:39:39 -258,0,0:39:42 -115,0,0:39:46 -245,0,0:39:49 -102,0,0:40:01 -132,0,0:40:03 -234,0,0:40:06 -9,0,0:40:18 -23,0,0:40:41 -213,0,0:40:45 -268,0,0:40:47 -18,0,0:40:59 -37,0,0:41:07 -133,0,0:41:16 -237,0,0:41:24 -227,0,0:41:25 -49,0,0:41:35 -243,0,0:41:39 -7,0,0:41:41 -134,0,0:41:44 -58,0,0:41:52 -118,0,0:41:54 -19,0,0:42:04 -242,0,0:42:05 -121,0,0:42:07 -126,0,0:42:09 -207,0,0:42:14 -230,0,0:42:15 -52,0,0:42:19 -251,0,0:42:21 -215,0,0:42:23 -261,0,0:42:24 -2,0,0:42:25 -13,0,0:42:49 -120,0,0:43:04 -116,0,0:43:06 -263,0,0:43:09 -54,0,0:43:12 -125,0,0:43:16 -22,0,0:43:19 -4,0,0:43:30 -57,0,0:43:33 -262,0,0:43:55 -29,0,0:43:59 -249,0,0:44:00 -259,0,0:44:02 -35,0,0:44:04 -140,0,0:44:05 -141,0,0:44:06 -145,0,0:44:07 -104,0,0:44:10 -256,0,0:44:13 -6,0,0:44:18 -21,0,0:44:28 -232,0,0:44:34 -143,0,0:44:35 -217,0,0:44:35 -136,0,0:44:43 -112,0,0:44:44 -202,0,0:44:45 -55,0,0:44:46 -206,0,0:44:52 -220,0,0:45:03 -38,0,0:45:05 -239,0,0:45:07 -260,0,0:45:16 -26,0,0:45:25 -209,0,0:45:30 -216,0,0:45:32 -236,0,0:45:37 -44,0,0:45:39 -148,0,0:45:51 -201,0,0:46:14 -25,0,0:46:42 -24,0,0:46:44 -137,0,0:46:56 -122,0,0:47:02 -142,0,0:47:33 -203,0,0:47:47 -40,0,0:47:53 -117,0,0:47:58 -114,0,0:48:03 -160,0,0:48:20 -1,0,0:48:22 -151,0,0:48:49 -152,0,0:48:53 -129,0,0:49:00 -14,0,0:49:03 -244,0,0:49:10 -241,0,0:49:24 -222,0,0:49:25 -119,0,0:49:27 -20,0,0:49:37 -218,0,0:49:37 -221,0,0:49:51 -144,0,0:50:06 -147,0,0:50:47 -105,0,0:50:49 -233,0,0:50:51 -139,0,0:50:53 -267,0,0:50:57 -36,0,0:50:58 -138,0,0:51:17 -153,0,0:51:40 -10,0,0:51:57 -30,0,0:52:11 -16,0,0:52:13 -33,0,0:52:16 -42,0,0:52:19 -156,0,0:52:27 -240,0,0:52:28 -149,0,0:52:35 -155,0,0:53:30 -154,0,0:53:31 -223,0,0:53:47 -17,0,0:53:52 -28,0,0:53:57 -41,0,0:53:58 -210,0,0:54:29 -98,0,0:54:30 -124,0,0:54:36 -15,0,0:54:45 -211,0,0:54:45 -32,0,0:54:46 -212,0,0:55:55 -257,0,0:56:28 -204,0,0:56:43 -47,0,0:57:30 -128,0,0:57:39 -158,0,0:57:42 -123,0,0:59:26 -224,0,0:59:35 -27,0,0:59:37 -266,0,1:00:09 -235,0,1:00:10 -248,0,1:03:19 -157,0,1:10:13 -60,0,1:26:21 -61,0,1:26:23 -208,0,DNF -253,0,DNF -255,0,DNS +113,0,0:33:05,CLS +34,0,0:33:26,CLS +135,0,0:33:39,CLS +107,0,0:33:46,CLS +109,0,0:34:04,CLS +110,0,0:34:12,CLS +3,0,0:34:22,CLS +46,0,0:35:05,CLS +238,0,0:35:19,CLS +264,0,0:35:22,CLS +12,0,0:35:25,CLS +48,0,0:35:50,CLS +39,0,0:36:09,CLS +31,0,0:36:26,CLS +225,0,0:36:39,CLS +51,0,0:36:40,CLS +219,0,0:36:53,CLS +159,0,0:36:55,CLS +11,0,0:36:59,CLS +56,0,0:37:06,CLS +252,0,0:37:11,CLS +214,0,0:37:20,CLS +106,0,0:37:25,CLS +53,0,0:37:26,CLS +205,0,0:37:37,CLS +231,0,0:37:42,CLS +5,0,0:37:47,CLS +108,0,0:37:58,CLS +101,0,0:38:01,CLS +226,0,0:38:03,CLS +246,0,0:38:05,CLS +150,0,0:38:10,CLS +127,0,0:38:13,CLS +131,0,0:38:19,CLS +59,0,0:38:22,CLS +43,0,0:38:37,CLS +247,0,0:38:51,CLS +130,0,0:38:54,CLS +50,0,0:39:06,CLS +229,0,0:39:09,CLS +146,0,0:39:13,CLS +250,0,0:39:15,CLS +8,0,0:39:17,CLS +45,0,0:39:25,CLS +228,0,0:39:27,CLS +265,0,0:39:39,CLS +258,0,0:39:42,CLS +115,0,0:39:46,CLS +245,0,0:39:49,CLS +102,0,0:40:01,CLS +132,0,0:40:03,CLS +234,0,0:40:06,CLS +9,0,0:40:18,CLS +23,0,0:40:41,CLS +213,0,0:40:45,CLS +268,0,0:40:47,CLS +18,0,0:40:59,CLS +37,0,0:41:07,CLS +133,0,0:41:16,CLS +237,0,0:41:24,CLS +227,0,0:41:25,CLS +49,0,0:41:35,CLS +243,0,0:41:39,CLS +7,0,0:41:41,CLS +134,0,0:41:44,CLS +58,0,0:41:52,CLS +118,0,0:41:54,CLS +19,0,0:42:04,CLS +242,0,0:42:05,CLS +121,0,0:42:07,CLS +126,0,0:42:09,CLS +207,0,0:42:14,CLS +230,0,0:42:15,CLS +52,0,0:42:19,CLS +251,0,0:42:21,CLS +215,0,0:42:23,CLS +261,0,0:42:24,CLS +2,0,0:42:25,CLS +13,0,0:42:49,CLS +120,0,0:43:04,CLS +116,0,0:43:06,CLS +263,0,0:43:09,CLS +54,0,0:43:12,CLS +125,0,0:43:16,CLS +22,0,0:43:19,CLS +4,0,0:43:30,CLS +57,0,0:43:33,CLS +262,0,0:43:55,CLS +29,0,0:43:59,CLS +249,0,0:44:00,CLS +259,0,0:44:02,CLS +35,0,0:44:04,CLS +140,0,0:44:05,CLS +141,0,0:44:06,CLS +145,0,0:44:07,CLS +104,0,0:44:10,CLS +256,0,0:44:13,CLS +6,0,0:44:18,CLS +21,0,0:44:28,CLS +232,0,0:44:34,CLS +143,0,0:44:35,CLS +217,0,0:44:35,CLS +136,0,0:44:43,CLS +112,0,0:44:44,CLS +202,0,0:44:45,CLS +55,0,0:44:46,CLS +206,0,0:44:52,CLS +220,0,0:45:03,CLS +38,0,0:45:05,CLS +239,0,0:45:07,CLS +260,0,0:45:16,CLS +26,0,0:45:25,CLS +209,0,0:45:30,CLS +216,0,0:45:32,CLS +236,0,0:45:37,CLS +44,0,0:45:39,CLS +148,0,0:45:51,CLS +201,0,0:46:14,CLS +25,0,0:46:42,CLS +24,0,0:46:44,CLS +137,0,0:46:56,CLS +122,0,0:47:02,CLS +142,0,0:47:33,CLS +203,0,0:47:47,CLS +40,0,0:47:53,CLS +117,0,0:47:58,CLS +114,0,0:48:03,CLS +160,0,0:48:20,CLS +1,0,0:48:22,CLS +151,0,0:48:49,CLS +152,0,0:48:53,CLS +129,0,0:49:00,CLS +14,0,0:49:03,CLS +244,0,0:49:10,CLS +241,0,0:49:24,CLS +222,0,0:49:25,CLS +119,0,0:49:27,CLS +20,0,0:49:37,CLS +218,0,0:49:37,CLS +221,0,0:49:51,CLS +144,0,0:50:06,CLS +147,0,0:50:47,CLS +105,0,0:50:49,CLS +233,0,0:50:51,CLS +139,0,0:50:53,CLS +267,0,0:50:57,CLS +36,0,0:50:58,CLS +138,0,0:51:17,CLS +153,0,0:51:40,CLS +10,0,0:51:57,CLS +30,0,0:52:11,CLS +16,0,0:52:13,CLS +33,0,0:52:16,CLS +42,0,0:52:19,CLS +156,0,0:52:27,CLS +240,0,0:52:28,CLS +149,0,0:52:35,CLS +155,0,0:53:30,CLS +154,0,0:53:31,CLS +223,0,0:53:47,CLS +17,0,0:53:52,CLS +28,0,0:53:57,CLS +41,0,0:53:58,CLS +210,0,0:54:29,CLS +98,0,0:54:30,CLS +124,0,0:54:36,CLS +15,0,0:54:45,CLS +211,0,0:54:45,CLS +32,0,0:54:46,CLS +212,0,0:55:55,CLS +257,0,0:56:28,CLS +204,0,0:56:43,CLS +47,0,0:57:30,CLS +128,0,0:57:39,CLS +158,0,0:57:42,CLS +123,0,0:59:26,CLS +224,0,0:59:35,CLS +27,0,0:59:37,CLS +266,0,1:00:09,CLS +235,0,1:00:10,CLS +248,0,1:03:19,CLS +157,0,1:10:13,CLS +60,0,1:26:21,CLS +61,0,1:26:23,CLS +208,0,0:00:00,DNF +253,0,0:00:00,DNF +255,0,0:00:00,DNS diff --git a/samples/mass_start/mass_start.crd b/samples/mass_start/mass_start.crd index 45dabf0b00af1b29bd47e24ae387e10a100912be..cbdadfe96df957c09c4afa204897d9574ef5fb87 100644 GIT binary patch delta 2993 zcmZA3d$3kh9LDjrJDtwHy+=51rR0)`NU4JynduKRa+};{%=iPtxDUAtZ?uKnFLlB} z#>5O}3^M+cVNT;3Mdg+dhdN5-k}$^e-TT?b_|3e}KA*ML`>wtBT675Sn@?;FjQ5W-&isM=O4@NGz5tH2i_dA$N(g56Iv@Qep!_H*`O_DeQbu8Ln(aKhX+Emre)~6jgFVmQVl%HQZvLudf>}@ve=6`KB>%0z2cQs= zh5~Oz@=yi71PEAV$n9;3hyAgzCLh?0gspa!s3*n1-3)?vP=O1c#ITO?0b z$Wc-X{*bWy8E`0)>s`!d#9TT3tC0MYLXKKXhGbLaP_OxByHWM0EEV6s*51{=vYJ1n zL#l?zw8c(%Qh zj;fRw5I^t(lBX*$yIGa;c;Qdce|)<=Lr3~Avu~)}4DYysTg!k>o6Uol+B}Y!&8JG4 z81IW@t`xI3ROy2XIjWbWr*dGaRp~-S3+unEbV#2q8$$TRt{=M}?HApDp#8G_kiFP` z+g@q!X7jXgf^0rj`ir84`_BtQl@ScMouRgL>V apBheF)zPJ(pCkXv>6lDI_r#@*vHt+@iPt>< delta 2993 zcmZA3d5m6D9LDi;XF8qtZlf4mt5r*tQmWMsYEd+&2a{F3*%{hV{&_uPBWxp&&%+tuIOHLHk< zmZG&7RJ0X?i}s>X#6?o1MYb^Sd!yMBLfAteRr?ABz6Hr^6!-!puT$WQko=bdUyMR% z3{v1$BzGwA0Z2Ynfj1!e=n$GTL=86_qay<{`#5`?eU3fazQ*3oo@;+?_t+EcdG_b_ z0?l7e_=Fpt@qpBR&OXF`$>z#cjaL<%Fn3PXcwK?d*GJXpQ{ah6UaY|HBYCOjFB`ru z9UAo#wAq{~U%1fjx6iiM+TYj%_HXuj`yqR?&Ah5c^H(Jk%!X?CQ-LQV`ELc@4}}ot z3cLl$!xZ=?B#%(w@kkz{z~3QxtODPMwEpoyN*exv8o*j%?}?aOhxwu^?pEM$kvv5q zM{&szPqi7NijP?lJ8W=kvDaGcjd4hy;_I(Q8$dDL4HcX58RC8=M{K8k{2rQ8YI7>z}!hyyhwq6 zM)I2q+>Ycv1)if1`yhVH4TtHdia%5^;7TMfQ{e59{DlH$w4qqdSE-dMRDL;of_ulf^~itk@-?`&UQ z%^#AXRYT-klnnQPN&dn}`+9pXdq;b;%^Ogaj8`xrZ#-2pQNalhE*TPU(lETdehoOG z8W6U6z={4st3A#Ad)g=4ci8nNX0xf1Qx%-Ry+}SyfzL(q=?YHB^M~Y&sv*>8INjbs zM^(ZLh#&X?$uks~-K;VY1QgaWfv zs`MxYp0AH8We=5q|D)0&tv5}NeW{LoVV=zo;~Q7R+HH=TFSApU^(ISlk6J>q{@AkejpO&>)(! z$D8$&{2c|pg8UQAXOqVl^g|VS+)^a$c&9vube$+A(se#Ejx&X~-t$KWzhD2T;}npn z@YZ?)Ru>y!BrK-ko_a;GOUd&atCQj&9Mmg{J4zlONGJXb_p!40Ljj3O-{>2Vu(Zu) zq$=YJB4xRrailDxQ@&489#QiBNM0UqxF4-7UowGzvhp8{l$CeJ4?wc&D2$}l85l{c z>#$!`QQcMY{YY3f|CHK)S?H)e>a5V|6!5iKN|lL^#xqU&)y6QFP-A eAbIzhiY;%OAFSv`$;KO=xmCRvp%G#uBoYcyNEAd=h)AeB{{K6_^Ck10{e9;(d(Lm})R)PrFOz5LfqJkWs$2DN zJyNH2sIxk++ja4v9GLD6CP^}&M=^9!$y}MlSCq_aOyX-w=0cBRXhzB0GKpU+ndh3s zZI9y5s#Dw+Kviq;_|^QcLDSjjx!BtEMB`-LTa zPFm;T9KHarfG@!-;p^}m_#wO;ege;epTljq_nyLH3h@dSG&~%SG=+CEei6J2UIp)l z7sC7C)o?tq&~R^L0|VAHg>(yyl<8J@i2l7WlBOqNBuj6@NSWS&kv4q_H%|4E^e=_3 zT1D`i`;L$*_+-pT6S^=`g)tcG!+Lm~T17akV6!`Z@@T~ zKZ92_`zQH33Va3mCz#JBk1vRXc|1}iZ2P3VA=0&@lt|b9NI%XLK6;NI?*IS#MfK<7 z0umKIS~FG`n_(m@CgHAHMX^`O;~T4!;s_knDvD_(_xGg}e};QlS^S}ZM5QNs0uq+C z+3ZJA#ur4&a+8YgL&`Eb<#~$oxRU$FOyW~Y;$F0}eAyHde#y#zFj7`N883ij)d?6$ ztMf3DRySe4sG_>3E8*+zpywKo(fr(+6 diff --git a/timing.cpp b/timing.cpp index 3197b66..1e888ff 100644 --- a/timing.cpp +++ b/timing.cpp @@ -16,6 +16,7 @@ *****************************************************************************/ #include "timing.hpp" +#include "lbchronorace.hpp" #include "lbcrexception.hpp" #include "crhelper.hpp" @@ -47,7 +48,26 @@ QDataStream &operator>>(QDataStream &in, Timing &timing) timing.bib = bib32; timing.leg = leg32; timing.seconds = seconds32; - timing.status = (Timing::Status) status32; + + switch (status32) { + case static_cast(Timing::Status::DNS): + timing.status = Timing::Status::DNS; + break; + case static_cast(Timing::Status::DNF): + timing.status = Timing::Status::DNF; + break; + case static_cast(Timing::Status::DSQ): + timing.status = (LBChronoRace::binFormat < LBCHRONORACE_BIN_FMT_v4) ? Timing::Status::CLASSIFIED : Timing::Status::DSQ; + break; + case static_cast(Timing::Status::CLASSIFIED): + timing.status = Timing::Status::CLASSIFIED; + break; + default: + /* Since tr() cannot be used here, let's use classified just + * to avoid loosing the recorded timing value (if any). */ + timing.status = Timing::Status::CLASSIFIED; + break; + } return in; } @@ -82,6 +102,11 @@ bool Timing::isDns() const return (status == Status::DNS); } +bool Timing::isDsq() const +{ + return (status == Status::DSQ); +} + uint Timing::getSeconds() const { return seconds; @@ -92,44 +117,43 @@ Timing::Status Timing::getStatus() const return status; } +void Timing::setStatus(Timing::Status newStatus) +{ + this->status = newStatus; +} + +void Timing::setStatus(QString const &newStatus) +{ + this->status = CRHelper::toTimingStatus(newStatus); + + if ((this->status == Status::DNF) || (this->status == Status::DNS)) + this->seconds = 0u; +} + QString Timing::getTiming() const { - return CRHelper::toTimeStr(this->seconds, this->status); + return CRHelper::toTimeString(this->seconds, Status::CLASSIFIED); // } void Timing::setTiming(QString const &timing) { - - if (timing.length() == 3) { - if (!timing.compare("DNS", Qt::CaseInsensitive)) { - this->status = Status::DNS; - this->seconds = 0u; - } else if (!timing.compare("DNF", Qt::CaseInsensitive)) { - this->status = Status::DNF; - this->seconds = 0u; + uint val; + uint secs = 0u; + bool converted; + QString sep { timing.contains(".") ? "." : ":" }; + + for (auto const &token : timing.split(sep, Qt::KeepEmptyParts, Qt::CaseInsensitive)) { + converted = true; + val = token.toUInt(&converted); + if (!converted) { + throw(ChronoRaceException(tr("Illegal timing value '%1' for bib '%2'").arg(token).arg(this->bib))); } else { - throw(ChronoRaceException(tr("Illegal timing value '%1'").arg(timing))); - } - } else { - uint val; - uint secs = 0u; - bool converted; - QString sep(":"); - if (timing.contains(".")) { - sep.clear(); - sep.append("."); + secs = (secs * 60) + val; } - for (auto const &token : timing.split(sep, Qt::KeepEmptyParts, Qt::CaseInsensitive)) { - converted = true; - val = token.toUInt(&converted); - if (!converted) { - //NOSONAR seconds = 0u; - throw(ChronoRaceException(tr("Illegal timing value '%1' for bib '%2'").arg(token).arg(this->bib))); - } else { - secs = (secs * 60) + val; - } - } - this->seconds = secs; + } + + if ((this->seconds = secs)) { + this->status = Status::CLASSIFIED; } } @@ -140,17 +164,91 @@ void Timing::setTiming(char const *timing) bool Timing::isValid() const { - return ((bib != 0u) && ((status == Status::DNS) || (status == Status::DNF) || ((status == Status::CLASSIFIED) && (seconds != 0u)))); + return ((bib != 0u) && ((status == Status::DNS) || (status == Status::DNF) || (status == Status::DSQ) || ((status == Status::CLASSIFIED) && (seconds != 0u)))); } bool Timing::operator< (Timing const &rhs) const { - return (((this->status == Status::DNF) && (rhs.status == Status::DNS)) || ((this->status == Status::CLASSIFIED) && ((rhs.status != Status::CLASSIFIED) || (this->seconds < rhs.seconds)))); + bool lessThen = false; + + //NOSONAR switch (this->status) { + //NOSONAR case Status::CLASSIFIED: + //NOSONAR lessThen = ((rhs.status != Status::CLASSIFIED) || (this->seconds < rhs.seconds)); + //NOSONAR break; + //NOSONAR case Status::DSQ: + //NOSONAR lessThen = ((rhs.status == Status::DNF) || (rhs.status == Status::DNS)); + //NOSONAR break; + //NOSONAR case Status::DNF: + //NOSONAR lessThen = (rhs.status == Status::DNS); + //NOSONAR break; + //NOSONAR case Status::DNS: + //NOSONAR // nothing to assign + //NOSONAR break; + //NOSONAR default: + //NOSONAR Q_UNREACHABLE(); + //NOSONAR break; + //NOSONAR } + + switch (this->status) { + case Status::CLASSIFIED: + [[fallthrough]]; + case Status::DSQ: + lessThen = (rhs.status == Status::DNS) || (rhs.status == Status::DNF) || (this->seconds < rhs.seconds); + break; + case Status::DNF: + lessThen = (rhs.status == Status::DNS); + break; + case Status::DNS: + // nothing to assign + break; + default: + Q_UNREACHABLE(); + break; + } + + return lessThen; } bool Timing::operator> (Timing const &rhs) const { - return (((this->status == Status::DNS) && (rhs.status != Status::DNS)) || ((rhs.status == Status::CLASSIFIED) && ((this->status == Status::DNF) || (this->seconds > rhs.seconds)))); + bool greaterThen = false; + + //NOSONAR switch (this->status) { + //NOSONAR case Status::CLASSIFIED: + //NOSONAR greaterThen = ((rhs.status == Status::CLASSIFIED) && (this->seconds > rhs.seconds)); + //NOSONAR break; + //NOSONAR case Status::DSQ: + //NOSONAR greaterThen = (rhs.status == Status::CLASSIFIED); + //NOSONAR break; + //NOSONAR case Status::DNF: + //NOSONAR greaterThen = ((rhs.status == Status::CLASSIFIED) || (rhs.status == Status::DSQ)); + //NOSONAR break; + //NOSONAR case Status::DNS: + //NOSONAR greaterThen = (rhs.status != Status::DNS); + //NOSONAR break; + //NOSONAR default: + //NOSONAR Q_UNREACHABLE(); + //NOSONAR break; + //NOSONAR } + + switch (this->status) { + case Status::CLASSIFIED: + [[fallthrough]]; + case Status::DSQ: + greaterThen = ((rhs.status == Status::CLASSIFIED) || (rhs.status == Status::DSQ)) && (this->seconds > rhs.seconds); + break; + case Status::DNF: + greaterThen = ((rhs.status == Status::CLASSIFIED) || (rhs.status == Status::DSQ)); + break; + case Status::DNS: + greaterThen = (rhs.status != Status::DNS); + break; + default: + Q_UNREACHABLE(); + break; + } + + return greaterThen; } bool Timing::operator<=(Timing const &rhs) const diff --git a/timing.hpp b/timing.hpp index debfc4e..320bd1c 100644 --- a/timing.hpp +++ b/timing.hpp @@ -33,26 +33,28 @@ class Timing { public: enum class Status { - DNS, - DNF, - CLASSIFIED + DNS = 0, + DNF = 1, + DSQ = 2, + CLASSIFIED = 3 }; enum class Field { - TMF_FIRST = 0, - TMF_BIB = 0, - TMF_LEG = 1, - TMF_TIME = 2, - TMF_LAST = 2, - TMF_COUNT = 3 + TMF_FIRST = 0, + TMF_BIB = 0, + TMF_LEG = 1, + TMF_TIME = 2, + TMF_STATUS = 3, + TMF_LAST = 3, + TMF_COUNT = 4 }; private: uint bib { 0u }; uint leg { 0u }; uint seconds { 0u }; - Status status { Status::CLASSIFIED}; + Status status { Status::CLASSIFIED }; public: Timing() = default; @@ -67,7 +69,10 @@ class Timing { void setLeg(uint newLeg); bool isDnf() const; bool isDns() const; + bool isDsq() const; Status getStatus() const; + void setStatus(Status newStatus); + void setStatus(QString const &newStatus); uint getSeconds() const; QString getTiming() const; void setTiming(QString const &timing); diff --git a/timingsmodel.cpp b/timingsmodel.cpp index fa5996e..382657b 100644 --- a/timingsmodel.cpp +++ b/timingsmodel.cpp @@ -20,6 +20,7 @@ #include "lbchronorace.hpp" #include "timingsmodel.hpp" #include "lbcrexception.hpp" +#include "crhelper.hpp" QDataStream &operator<<(QDataStream &out, TimingsModel const &data) { @@ -72,6 +73,8 @@ QVariant TimingsModel::data(QModelIndex const &index, int role) const return QVariant(timings.at(index.row()).getLeg()); case static_cast(Timing::Field::TMF_TIME): return QVariant(timings.at(index.row()).getTiming()); + case static_cast(Timing::Field::TMF_STATUS): + return QVariant(CRHelper::toStatusString(timings.at(index.row()).getStatus())); default: return QVariant(); } @@ -82,7 +85,9 @@ QVariant TimingsModel::data(QModelIndex const &index, int role) const case static_cast(Timing::Field::TMF_LEG): return QVariant(tr("Leg number (0 for automatic detection)")); case static_cast(Timing::Field::TMF_TIME): - return QVariant(tr("Timing (i.e. 0:45:23) or DNF or DNS")); + return QVariant(tr("Timing (i.e. 0:45:23)")); + case static_cast(Timing::Field::TMF_STATUS): + return QVariant(tr("Classified (CLS), Disqualified (DSQ), Did not finish (DNF), or Did not start (DNS)")); default: return QVariant(); } @@ -126,6 +131,15 @@ bool TimingsModel::setData(QModelIndex const &index, QVariant const &value, int retval = false; } break; + case static_cast(Timing::Field::TMF_STATUS): + try { + timings[index.row()].setStatus(value.toString().trimmed()); + retval = true; + } catch (ChronoRaceException &e) { + emit error(e.getMessage()); + retval = false; + } + break; default: break; } @@ -148,6 +162,8 @@ QVariant TimingsModel::headerData(int section, Qt::Orientation orientation, int return QString("%1").arg(tr("Leg")); case static_cast(Timing::Field::TMF_TIME): return QString("%1").arg(tr("Timing")); + case static_cast(Timing::Field::TMF_STATUS): + return QString("%1").arg(tr("Status")); default: return QString("%1").arg(section + 1); } diff --git a/timingstatusdelegate.cpp b/timingstatusdelegate.cpp new file mode 100644 index 0000000..7501194 --- /dev/null +++ b/timingstatusdelegate.cpp @@ -0,0 +1,78 @@ +/***************************************************************************** + * Copyright (C) 2024 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#include "timingstatusdelegate.hpp" +#include "crhelper.hpp" + +TimingStatusDelegate::TimingStatusDelegate(QObject *parent) : + QStyledItemDelegate(parent) +{ + auto *comboBox = timingStatusBox.data(); + comboBox->setEditable(false); + comboBox->setInsertPolicy(QComboBox::NoInsert); + comboBox->setDuplicatesEnabled(false); + comboBox->setSizeAdjustPolicy(QComboBox::AdjustToContents); + comboBox->addItem(QIcon(":/material/icons/classified.svg"), CRHelper::toStatusFullString(Timing::Status::CLASSIFIED), CRHelper::toStatusString(Timing::Status::CLASSIFIED)); + comboBox->addItem(QIcon(":/material/icons/dsq.svg"), CRHelper::toStatusFullString(Timing::Status::DSQ), CRHelper::toStatusString(Timing::Status::DSQ)); + comboBox->addItem(QIcon(":/material/icons/dnf.svg"), CRHelper::toStatusFullString(Timing::Status::DNF), CRHelper::toStatusString(Timing::Status::DNF)); + comboBox->addItem(QIcon(":/material/icons/dns.svg"), CRHelper::toStatusFullString(Timing::Status::DNS), CRHelper::toStatusString(Timing::Status::DNS)); +} + +QWidget *TimingStatusDelegate::createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + auto *comboBox = timingStatusBox.data(); + comboBox->setParent(parent); + + return comboBox; +} + +void TimingStatusDelegate::destroyEditor(QWidget *editor, const QModelIndex &index) const +{ + Q_UNUSED(editor) + Q_UNUSED(index) +} + +void TimingStatusDelegate::setEditorData(QWidget *editor, QModelIndex const &index) const +{ + // Get the value via index of the Model and put it into the ComboBox + auto *comboBox = static_cast(editor); + comboBox->setCurrentText(CRHelper::toStatusString(CRHelper::toTimingStatus(index.model()->data(index, Qt::EditRole).toString()))); +} + +void TimingStatusDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const +{ + auto const *comboBox = static_cast(editor); + model->setData(index, comboBox->currentData(Qt::UserRole), Qt::EditRole); +} + +QSize TimingStatusDelegate::sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const +{ + Q_UNUSED(option) + Q_UNUSED(index) + + return this->timingStatusBox.data()->sizeHint(); +} + +void TimingStatusDelegate::updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const +{ + Q_UNUSED(index) + + editor->setGeometry(option.rect); +} diff --git a/timingstatusdelegate.hpp b/timingstatusdelegate.hpp new file mode 100644 index 0000000..a64d5ad --- /dev/null +++ b/timingstatusdelegate.hpp @@ -0,0 +1,45 @@ +/***************************************************************************** + * Copyright (C) 2024 by Lorenzo Buzzi (lorenzo@buzzi.pro) * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *****************************************************************************/ + +#ifndef TIMINGSTATUSDELEGATE_HPP +#define TIMINGSTATUSDELEGATE_HPP + +#include +#include +#include +#include + +#include "timing.hpp" + +class TimingStatusDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit TimingStatusDelegate(QObject *parent = Q_NULLPTR); + + QWidget *createEditor(QWidget *parent, QStyleOptionViewItem const &option, QModelIndex const &index) const override; + void destroyEditor(QWidget *editor, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, QModelIndex const &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, QModelIndex const &index) const override; + QSize sizeHint(QStyleOptionViewItem const &option, QModelIndex const &index) const override; + void updateEditorGeometry(QWidget *editor, QStyleOptionViewItem const &option, QModelIndex const &index) const override; + +private: + QScopedPointer timingStatusBox { new QComboBox }; +}; + +#endif // TIMINGSTATUSDELEGATE_HPP diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index c12edf9..d1ab65f 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -91,6 +91,26 @@ Invalid status value %1 Invalid status value %1 + + Illegal status value '%1' + Illegal status value '%1' + + + Classified + Classified + + + Disqualified + Disqualified + + + Did not finish + Did not finish + + + Did not start + Did not start + CRLoader @@ -1610,10 +1630,6 @@ The definitions of Categories and Rankings must be reviewed and corrected. Timing - - Illegal timing value '%1' - Illegal timing value '%1' - Illegal timing value '%1' for bib '%2' Illegal timing value '%1' for bib '%2' @@ -1630,8 +1646,16 @@ The definitions of Categories and Rankings must be reviewed and corrected.Leg number (0 for automatic detection) - Timing (i.e. 0:45:23) or DNF or DNS - Timing (i.e. 0:45:23) or DNF or DNS + Timing (i.e. 0:45:23) + Timing (i.e. 0:45:23) + + + Classified (CLS), Disqualified (DSQ), Did not finish (DNF), or Did not start (DNS) + Classified (CLS), Disqualified (DSQ), Did not finish (DNF), or Did not start (DNS) + + + Status + Status Bib diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index a3140cf..38c9a22 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -91,6 +91,26 @@ Invalid status value %1 Valore di stato %1 non valido + + Illegal status value '%1' + Stato concorrente '%1' non valido + + + Classified + Classificato + + + Disqualified + Squalificato + + + Did not finish + Ritirato + + + Did not start + Non partito + CRLoader @@ -1610,10 +1630,6 @@ Le definizioni di Categorie e Classifiche devono essere riviste e corrette. Timing - - Illegal timing value '%1' - Valore cronometrico '%1' non valido - Illegal timing value '%1' for bib '%2' Valore cronometrico '%1' non valido per il pettorale '%2' @@ -1630,8 +1646,16 @@ Le definizioni di Categorie e Classifiche devono essere riviste e corrette.Frazione (0 per rilevamento automatico) - Timing (i.e. 0:45:23) or DNF or DNS - Tempo (es. 0:45:23), DNF (non classificato) o DNS (non partito) + Timing (i.e. 0:45:23) + Tempo (es. 0:45:23) + + + Classified (CLS), Disqualified (DSQ), Did not finish (DNF), or Did not start (DNS) + Classificato (CLS), Squalificato (DSQ), Ritirato (DNF) o Non partito (DNS) + + + Status + Stato Bib From 82049677a62bf898c88e0b5cf98eebd4c3cc783a Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Wed, 21 Aug 2024 15:30:57 +0200 Subject: [PATCH 17/20] Allow support of encodings other than UTF-8 and Latin1 At present, input and output in plain text and CSV formats are handled correctly only if the encoding is UTF-8 or Latin1. With the current change, future addition of other encodings will be easier. --- chronoracetimings.cpp | 5 +--- crhelper.cpp | 8 +++---- crhelper.hpp | 2 +- crloader.cpp | 20 ++++++---------- crloader.hpp | 13 ++++------ csvrankingprinter.cpp | 13 +--------- lbchronorace.cpp | 42 ++++++++++++++++++++++----------- rankingswizardformat.cpp | 24 ++++--------------- translations/LBChronoRace_en.ts | 16 +++++++++++-- translations/LBChronoRace_it.ts | 16 +++++++++++-- txtrankingprinter.cpp | 13 +--------- 11 files changed, 80 insertions(+), 92 deletions(-) diff --git a/chronoracetimings.cpp b/chronoracetimings.cpp index e4dd2b3..815d17b 100644 --- a/chronoracetimings.cpp +++ b/chronoracetimings.cpp @@ -46,10 +46,7 @@ void TimingsWorker::writeToDisk(QString const &buffer) { } QTextStream outStream(&outFile); - if (CRLoader::getEncoding() == CRLoader::Encoding::UTF8) - outStream.setEncoding(QStringConverter::Utf8); - else //NOSONAR if (CRLoader::getEncoding() == CRLoader::Encoding::LATIN1) - outStream.setEncoding(QStringConverter::Latin1); + outStream.setEncoding(CRLoader::getEncoding()); outStream << buffer; outStream.flush(); diff --git a/crhelper.cpp b/crhelper.cpp index 1fe8b8e..9c65056 100644 --- a/crhelper.cpp +++ b/crhelper.cpp @@ -18,15 +18,15 @@ #include "crhelper.hpp" #include "lbcrexception.hpp" -QString CRHelper::encodingToLabel(CRLoader::Encoding const &value) +QString CRHelper::encodingToLabel(QStringConverter::Encoding const &value) { switch (value) { - case CRLoader::Encoding::UTF8: + case QStringConverter::Encoding::Utf8: return tr("UTF-8"); - case CRLoader::Encoding::LATIN1: + case QStringConverter::Encoding::Latin1: return tr("ISO-8859-1 (Latin-1)"); default: - return tr("Unknown encoding %1").arg(static_cast(value)); + return tr("Unsupported encoding %1").arg(static_cast(value)); } } diff --git a/crhelper.hpp b/crhelper.hpp index 9b8884b..7d4ef53 100644 --- a/crhelper.hpp +++ b/crhelper.hpp @@ -36,7 +36,7 @@ class CRHelper Q_DECLARE_TR_FUNCTIONS(CRHelper) public: - static QString encodingToLabel(CRLoader::Encoding const &value); + static QString encodingToLabel(QStringConverter::Encoding const &value); static QString formatToLabel(CRLoader::Format const &value); static Competitor::Sex toSex(QString const &sex); diff --git a/crloader.cpp b/crloader.cpp index 67bf3c8..8f2f7ef 100644 --- a/crloader.cpp +++ b/crloader.cpp @@ -30,7 +30,7 @@ TimingsModel CRLoader::timingsModel; RankingsModel CRLoader::rankingsModel; CategoriesModel CRLoader::categoriesModel; QList CRLoader::standardItemList; -CRLoader::Encoding CRLoader::encoding = CRLoader::Encoding::LATIN1; +QStringConverter::Encoding CRLoader::encoding = QStringConverter::Encoding::Latin1; CRLoader::Format CRLoader::format = CRLoader::Format::PDF; CRTableModel *CRLoader::getStartListModel() @@ -63,12 +63,12 @@ QStringList CRLoader::getClubs() return QStringList(teamsListModel.getTeamsList()); } -CRLoader::Encoding CRLoader::getEncoding() +QStringConverter::Encoding CRLoader::getEncoding() { return encoding; } -void CRLoader::setEncoding(Encoding const &value) +void CRLoader::setEncoding(QStringConverter::Encoding const &value) { encoding = value; } @@ -90,9 +90,9 @@ void CRLoader::loadCSV(QString const &filePath, QAbstractTableModel *model) QFile file (filePath); if (file.open(QIODevice::ReadOnly)) { QString data; - if (encoding == Encoding::UTF8) + if (encoding == QStringConverter::Encoding::Utf8) data = QString::fromUtf8(file.readAll()); - else /* if (encoding == Encoding::LATIN1) */ //NOSONAR + else /* if (encoding == QStringConverter::Encoding::Latin1) */ //NOSONAR data = QString::fromLatin1(file.readAll()); standardItemList.clear(); data.remove(re); //remove all ocurrences of CR (Carriage Return) @@ -125,10 +125,7 @@ void CRLoader::saveCSV(QString const &filePath, QAbstractTableModel const *model } QTextStream outStream(&outFile); - if (CRLoader::getEncoding() == Encoding::UTF8) - outStream.setEncoding(QStringConverter::Utf8); - else /* if (encoding == Encoding::LATIN1) */ //NOSONAR - outStream.setEncoding(QStringConverter::Latin1); + outStream.setEncoding(CRLoader::getEncoding()); int rowCount = model->rowCount(); int columnCount = model->columnCount(); @@ -305,10 +302,7 @@ void CRLoader::exportModel(Model model, QString const &path) } QTextStream outStream(&outFile); - if (CRLoader::getEncoding() == Encoding::UTF8) - outStream.setEncoding(QStringConverter::Utf8); - else /* if (encoding == Encoding::LATIN1) */ //NOSONAR - outStream.setEncoding(QStringConverter::Latin1); + outStream.setEncoding(CRLoader::getEncoding()); int rowCount = teamsListModel.rowCount(); for (int r = 0; r < rowCount; ++r) diff --git a/crloader.hpp b/crloader.hpp index 7759aa5..0171b3e 100644 --- a/crloader.hpp +++ b/crloader.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -43,12 +44,6 @@ class CRLoader Q_DECLARE_TR_FUNCTIONS(CRLoader) public: - enum class Encoding - { - LATIN1 = 0, - UTF8 = 1 - }; - enum class Format { PDF = 0, @@ -72,7 +67,7 @@ class CRLoader static RankingsModel rankingsModel; static CategoriesModel categoriesModel; static QList standardItemList; - static Encoding encoding; + static QStringConverter::Encoding encoding; static Format format; static void loadCSV(QString const &filePath, QAbstractTableModel *model); @@ -102,8 +97,8 @@ class CRLoader static CRTableModel *getTimingsModel(); static CRTableModel *getRankingsModel(); static CRTableModel *getCategoriesModel(); - static Encoding getEncoding(); - static void setEncoding(Encoding const &value); + static QStringConverter::Encoding getEncoding(); + static void setEncoding(QStringConverter::Encoding const &value); static Format getFormat(); static void setFormat(Format const &value); diff --git a/csvrankingprinter.cpp b/csvrankingprinter.cpp index ed64e77..b80a677 100644 --- a/csvrankingprinter.cpp +++ b/csvrankingprinter.cpp @@ -39,18 +39,7 @@ void CSVRankingPrinter::init(QString *outFileName, QString const &title) throw(ChronoRaceException(tr("Error: cannot open %1").arg(*outFileName))); } csvStream.setDevice(&csvFile); - - switch (CRLoader::getEncoding()) { - case CRLoader::Encoding::UTF8: - csvStream.setEncoding(QStringConverter::Utf8); - break; - case CRLoader::Encoding::LATIN1: - csvStream.setEncoding(QStringConverter::Latin1); - break; - default: - Q_UNREACHABLE(); - break; - } + csvStream.setEncoding(CRLoader::getEncoding()); } void CSVRankingPrinter::printStartList(QList const &startList) diff --git a/lbchronorace.cpp b/lbchronorace.cpp index 0399cc4..0450b67 100644 --- a/lbchronorace.cpp +++ b/lbchronorace.cpp @@ -434,14 +434,15 @@ void LBChronoRace::resizeDialogs(QScreen const *screen) void LBChronoRace::encodingSelector(int idx) const { switch (idx) { - case static_cast(CRLoader::Encoding::LATIN1): - CRLoader::setEncoding(CRLoader::Encoding::LATIN1); + case 1: + CRLoader::setEncoding(QStringConverter::Encoding::Utf8); break; - case static_cast(CRLoader::Encoding::UTF8): - CRLoader::setEncoding(CRLoader::Encoding::UTF8); + case 0: + CRLoader::setEncoding(QStringConverter::Encoding::Latin1); break; default: - CRLoader::setEncoding(CRLoader::Encoding::LATIN1); + appendInfoMessage(tr("Unknown encoding %1; loaded default").arg(idx)); + CRLoader::setEncoding(QStringConverter::Encoding::Latin1); break; } @@ -579,9 +580,22 @@ void LBChronoRace::saveRace() QDataStream out(&raceDataFile); out.setVersion(QDataStream::Qt_5_15); - out << quint32(LBCHRONORACE_BIN_FMT) - << qint16(CRLoader::getEncoding()) - << qint16(CRLoader::getFormat()) + out << quint32(LBCHRONORACE_BIN_FMT); + + switch (CRLoader::getEncoding()) { + case QStringConverter::Encoding::Utf8: + out << qint16(1); + break; + case QStringConverter::Encoding::Latin1: + out << qint16(0); + break; + default: + appendInfoMessage(tr("Unknown encoding %1; default saved").arg(static_cast(CRLoader::getEncoding()))); + out << qint16(0); + break; + } + + out << qint16(CRLoader::getFormat()) << raceInfo; CRLoader::saveRaceData(out); @@ -613,15 +627,15 @@ void LBChronoRace::setEncoding() int current = 0; QStringList items = { - CRHelper::encodingToLabel(CRLoader::Encoding::LATIN1), - CRHelper::encodingToLabel(CRLoader::Encoding::UTF8) + CRHelper::encodingToLabel(QStringConverter::Encoding::Latin1), + CRHelper::encodingToLabel(QStringConverter::Encoding::Utf8) }; switch (CRLoader::getEncoding()) { - case CRLoader::Encoding::LATIN1: + case QStringConverter::Encoding::Latin1: current = 0; break; - case CRLoader::Encoding::UTF8: + case QStringConverter::Encoding::Utf8: current = 1; break; default: @@ -639,9 +653,9 @@ void LBChronoRace::setEncoding() if (ok) { if (item == items[0]) - CRLoader::setEncoding(CRLoader::Encoding::LATIN1); + CRLoader::setEncoding(QStringConverter::Encoding::Latin1); else if (item == items[1]) - CRLoader::setEncoding(CRLoader::Encoding::UTF8); + CRLoader::setEncoding(QStringConverter::Encoding::Utf8); else appendErrorMessage(tr("Unexpected encoding value (encoding not changed)")); } diff --git a/rankingswizardformat.cpp b/rankingswizardformat.cpp index a863117..8d77056 100644 --- a/rankingswizardformat.cpp +++ b/rankingswizardformat.cpp @@ -27,20 +27,16 @@ RankingsWizardFormat::RankingsWizardFormat(QWidget *parent) : fileFormat(parent), fileEncoding(parent) { - int formatIdx; - int encodingIdx; - - formatIdx = static_cast(CRLoader::getFormat()); + auto formatIdx = static_cast(CRLoader::getFormat()); fileFormat.insertItem(static_cast(CRLoader::Format::PDF), CRHelper::formatToLabel(CRLoader::Format::PDF)); fileFormat.insertItem(static_cast(CRLoader::Format::TEXT), CRHelper::formatToLabel(CRLoader::Format::TEXT)); fileFormat.insertItem(static_cast(CRLoader::Format::CSV), CRHelper::formatToLabel(CRLoader::Format::CSV)); fileFormat.setCurrentIndex(formatIdx); layout.addRow(new QLabel(tr("Format")), &fileFormat); - encodingIdx = static_cast(CRLoader::getEncoding()); - fileEncoding.insertItem(static_cast(CRLoader::Encoding::UTF8), CRHelper::encodingToLabel(CRLoader::Encoding::UTF8)); - fileEncoding.insertItem(static_cast(CRLoader::Encoding::LATIN1), CRHelper::encodingToLabel(CRLoader::Encoding::LATIN1)); - fileEncoding.setCurrentIndex(encodingIdx); + fileEncoding.addItem(CRHelper::encodingToLabel(QStringConverter::Encoding::Utf8), QVariant(QStringConverter::Encoding::Utf8)); + fileEncoding.addItem(CRHelper::encodingToLabel(QStringConverter::Encoding::Latin1), QVariant(QStringConverter::Encoding::Latin1)); + fileEncoding.setCurrentText(CRHelper::encodingToLabel(CRLoader::getEncoding())); layout.addRow(new QLabel(tr("Encoding")), &fileEncoding); formatChange(formatIdx); @@ -98,15 +94,5 @@ void RankingsWizardFormat::formatChange(int index) void RankingsWizardFormat::encodingChange(int index) const { - switch (index) { - case static_cast(CRLoader::Encoding::UTF8): - CRLoader::setEncoding(CRLoader::Encoding::UTF8); - break; - case static_cast(CRLoader::Encoding::LATIN1): - CRLoader::setEncoding(CRLoader::Encoding::LATIN1); - break; - default: - Q_UNREACHABLE(); - break; - } + CRLoader::setEncoding(fileEncoding.itemData(index, Qt::UserRole).value()); } diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index d1ab65f..f500757 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -12,8 +12,8 @@ ISO-8859-1 (Latin-1) - Unknown encoding %1 - Unknown encoding %1 + Unsupported encoding %1 + Unsupported encoding %1 PDF @@ -1109,6 +1109,18 @@ The definitions of Categories and Rankings must be reviewed and corrected.This Race Data File was saved with a previous release of the application. The definitions of Categories and Rankings must be reviewed and corrected. + + Unknown encoding %1; loaded saved + Unknown encoding %1; loaded saved + + + Unknown encoding %1; default saved + Unknown encoding %1; default saved + + + Unknown encoding %1; loaded default + Unknown encoding %1; loaded default + MultiSelectComboBox diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 38c9a22..08e013d 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -12,8 +12,8 @@ ISO-8859-1 (Latin-1) - Unknown encoding %1 - Codifica sconosciuta %1 + Unsupported encoding %1 + Codifica %1 non supportata PDF @@ -1109,6 +1109,18 @@ The definitions of Categories and Rankings must be reviewed and corrected.Il File Dati Gara è stato salvato con una precedente versione dell'applicazione. Le definizioni di Categorie e Classifiche devono essere riviste e corrette. + + Unknown encoding %1; loaded saved + Codifica sconosciuta %1; + + + Unknown encoding %1; default saved + Codifica sconosciuta %1; codifca predefinita salvata + + + Unknown encoding %1; loaded default + Codifica sconosciuta %1; codifca predefinita caricata + MultiSelectComboBox diff --git a/txtrankingprinter.cpp b/txtrankingprinter.cpp index 256ff7e..6ec651f 100644 --- a/txtrankingprinter.cpp +++ b/txtrankingprinter.cpp @@ -40,18 +40,7 @@ void TXTRankingPrinter::init(QString *outFileName, QString const &title) throw(ChronoRaceException(tr("Error: cannot open %1").arg(*outFileName))); } txtStream.setDevice(&txtFile); - - switch (CRLoader::getEncoding()) { - case CRLoader::Encoding::UTF8: - txtStream.setEncoding(QStringConverter::Utf8); - break; - case CRLoader::Encoding::LATIN1: - txtStream.setEncoding(QStringConverter::Latin1); - break; - default: - Q_UNREACHABLE(); - break; - } + txtStream.setEncoding(CRLoader::getEncoding()); } void TXTRankingPrinter::printStartList(QList const &startList) From c59ce34e21fe43b429348589e474793aa60254e8 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Wed, 28 Aug 2024 18:22:16 +0200 Subject: [PATCH 18/20] Omit "Results type" in start-list PDF --- pdfrankingprinter.cpp | 18 +++++++++++------- pdfrankingprinter.hpp | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pdfrankingprinter.cpp b/pdfrankingprinter.cpp index 221cfc0..e76326c 100644 --- a/pdfrankingprinter.cpp +++ b/pdfrankingprinter.cpp @@ -114,7 +114,7 @@ void PDFRankingPrinter::printStartList(QList const &startLis if (currentPage++) // this is not the first loop, add a new pages pdfWriter->newPage(); - drawTemplatePortrait(tr("Start List"), p, pp); + drawTemplatePortrait(tr("Start List"), p, pp, true); // Prepare fonts rnkFont.setPointSize(7); @@ -929,7 +929,7 @@ void PDFRankingPrinter::printPageMultiLeg(QRectF &writeRect, QListareaWidth, toVdots(-35.0))); - painter.drawText(writeRect.toRect(), Qt::AlignHCenter | Qt::AlignTop, tr("Results") + ":\u00a0" + raceInfo->getResults()); + if (!startList) { + writeRect.setBottomRight(QPointF(this->areaWidth, toVdots(-35.0))); + painter.drawText(writeRect.toRect(), Qt::AlignHCenter | Qt::AlignTop, tr("Results") + ":\u00a0" + raceInfo->getResults()); + } writeRect.setBottomRight(QPointF(toHdots(30.0), toVdots(-35.0))); painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignTop, tr("Page %n", "", page) + " " + tr("of %n", "", pages)); writeRect.setTopLeft(QPointF(toHdots(-30.0), toVdots(-40.5))); @@ -1063,7 +1065,7 @@ void PDFRankingPrinter::drawTemplatePortrait(QString const &fullDescription, int } } -//NOSONAR void PDFRankingPrinter::drawTemplateLandscape(QString const &fullDescription, int page, int pages) +//NOSONAR void PDFRankingPrinter::drawTemplateLandscape(QString const &fullDescription, int page, int pages, bool startList) //NOSONAR { //NOSONAR QRect boundingRect; //NOSONAR QRectF writeRect; @@ -1157,8 +1159,10 @@ void PDFRankingPrinter::drawTemplatePortrait(QString const &fullDescription, int //NOSONAR // Results, page and editing timestamp //NOSONAR painter.setFont(rnkFont); //NOSONAR writeRect.setTopLeft(QPointF(toHdots(0.0), toVdots(-40.5))); -//NOSONAR writeRect.setBottomRight(QPointF(this->areaWidth, toVdots(-35.0))); -//NOSONAR painter.drawText(writeRect.toRect(), Qt::AlignHCenter | Qt::AlignTop, tr("Results") + ":\u00a0" + raceInfo->getResults()); +//NOSONAR if (!startList) { +//NOSONAR writeRect.setBottomRight(QPointF(this->areaWidth, toVdots(-35.0))); +//NOSONAR painter.drawText(writeRect.toRect(), Qt::AlignHCenter | Qt::AlignTop, tr("Results") + ":\u00a0" + raceInfo->getResults()); +//NOSONAR } //NOSONAR writeRect.setBottomRight(QPointF(toHdots(30.0), toVdots(-35.0))); //NOSONAR painter.drawText(writeRect.toRect(), Qt::AlignLeft | Qt::AlignTop, tr("Page %n", "", page) + " " + tr("of %n", "", pages)); //NOSONAR writeRect.setTopLeft(QPointF(toHdots(-30.0), toVdots(-40.5))); diff --git a/pdfrankingprinter.hpp b/pdfrankingprinter.hpp index 2badda6..fc018c0 100644 --- a/pdfrankingprinter.hpp +++ b/pdfrankingprinter.hpp @@ -86,8 +86,8 @@ class PDFRankingPrinter final : public RankingPrinter void printPageMultiLeg(QRectF &writeRect, QList const &page, int &posIndex, uint referenceTime); void printPageMultiLeg(QRectF &writeRect, QList const &page, int &posIndex); - void drawTemplatePortrait(QString const &fullDescription, int page, int pages); - //NOSONAR void drawTemplateLandscape(QString const &fullDescription, int page, int pages); + void drawTemplatePortrait(QString const &fullDescription, int page, int pages, bool startList = false); + //NOSONAR void drawTemplateLandscape(QString const &fullDescription, int page, int pages, bool startList = false); qreal ratioX; qreal ratioY; From c73e42ddc53fa4be93c0256b11195ebae5404ac3 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Thu, 22 Aug 2024 20:36:20 +0200 Subject: [PATCH 19/20] Improve PDF metadata --- csvrankingprinter.cpp | 3 +- csvrankingprinter.hpp | 2 +- pdfrankingprinter.cpp | 74 +++++++++++++++++++++++++++++++-- pdfrankingprinter.hpp | 2 +- rankingprinter.hpp | 2 +- rankingswizard.cpp | 6 +-- translations/LBChronoRace_en.ts | 4 ++ translations/LBChronoRace_it.ts | 4 ++ txtrankingprinter.cpp | 3 +- txtrankingprinter.hpp | 2 +- 10 files changed, 90 insertions(+), 12 deletions(-) diff --git a/csvrankingprinter.cpp b/csvrankingprinter.cpp index b80a677..703dbe7 100644 --- a/csvrankingprinter.cpp +++ b/csvrankingprinter.cpp @@ -21,11 +21,12 @@ #include "lbcrexception.hpp" #include "crhelper.hpp" -void CSVRankingPrinter::init(QString *outFileName, QString const &title) +void CSVRankingPrinter::init(QString *outFileName, QString const &title, QString const &subject) { Q_ASSERT(!csvFile.isOpen()); Q_UNUSED(title) + Q_UNUSED(subject) if (outFileName == Q_NULLPTR) { throw(ChronoRaceException(tr("Error: no file name supplied"))); diff --git a/csvrankingprinter.hpp b/csvrankingprinter.hpp index 31efd81..aa437dc 100644 --- a/csvrankingprinter.hpp +++ b/csvrankingprinter.hpp @@ -29,7 +29,7 @@ class CSVRankingPrinter final : public RankingPrinter using RankingPrinter::RankingPrinter; public: - void init(QString *outFileName, QString const &title) override; + void init(QString *outFileName, QString const &title, QString const &subject) override; void printStartList(QList const &startList) override; void printRanking(Ranking const &categories, QList const &ranking) override; diff --git a/pdfrankingprinter.cpp b/pdfrankingprinter.cpp index e76326c..60d6f0f 100644 --- a/pdfrankingprinter.cpp +++ b/pdfrankingprinter.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "pdfrankingprinter.hpp" #include "chronoracedata.hpp" @@ -45,7 +46,7 @@ PDFRankingPrinter::PDFRankingPrinter(uint indexFieldWidth, uint bibFieldWidth) : } -void PDFRankingPrinter::init(QString *outFileName, QString const &title) +void PDFRankingPrinter::init(QString *outFileName, QString const &title, QString const &subject) { QPdfWriter *w; @@ -67,8 +68,75 @@ void PDFRankingPrinter::init(QString *outFileName, QString const &title) w->setPageSize(QPageSize::A4); w->setPageOrientation(QPageLayout::Portrait); w->setPageMargins(QMarginsF(RANKING_LEFT_MARGIN, RANKING_TOP_MARGIN, RANKING_RIGHT_MARGIN, RANKING_BOTTOM_MARGIN), QPageLayout::Millimeter); - w->setTitle(title); - w->setCreator(LBCHRONORACE_NAME); + //NOSONAR w->setTitle(title); + //NOSONAR w->setCreator(LBCHRONORACE_NAME); + + // append more details to the .pdf + QString metaData = QStringLiteral( + "" + "" + " " + " Qt %1" + " %2" + " " + "" + " " + " " + "" + " " + " " + "" + " " + " %3" + " %4" + " %4" + " %4" + " " + "" + " " + " uuid:%5" + " uuid:%6" + " " + "" + " " + " application/pdf" + " " + " " + " %7" + " " + " " + " " + " " + " %8" + " " + " " + " " + " " + " %3" + " " + " " + " " + " " + " %2" + " " + " " + " " + "" + " " + " 1" + " B" + " " + "" + "" + ).arg(QT_VERSION_STR, + tr("Rankings"), // keywords (can be a comma separated) + LBCHRONORACE_NAME, + QDateTime::currentDateTime().toString(Qt::ISODate), + QUuid::createUuid().toByteArray(QUuid::WithoutBraces), + QUuid::createUuid().toByteArray(QUuid::WithoutBraces), + title, + subject); + w->setDocumentXmpMetadata(metaData.toUtf8()); // Set global values to convert from mm to dots this->ratioX = w->logicalDpiX() / 25.4; diff --git a/pdfrankingprinter.hpp b/pdfrankingprinter.hpp index fc018c0..4d68401 100644 --- a/pdfrankingprinter.hpp +++ b/pdfrankingprinter.hpp @@ -35,7 +35,7 @@ class PDFRankingPrinter final : public RankingPrinter public: explicit PDFRankingPrinter(uint indexFieldWidth, uint bibFieldWidth); - void init(QString *outFileName, QString const &title) override; + void init(QString *outFileName, QString const &title, QString const &subject) override; void printStartList(QList const &startList) override; void printRanking(Ranking const &categories, QList const &ranking) override; diff --git a/rankingprinter.hpp b/rankingprinter.hpp index a17cf34..a22c6cb 100644 --- a/rankingprinter.hpp +++ b/rankingprinter.hpp @@ -36,7 +36,7 @@ class RankingPrinter : public QObject static QScopedPointer getRankingPrinter(CRLoader::Format format, uint indexFieldWidth, uint bibFieldWidth); - virtual void init(QString *outFileName, QString const &title) = 0; + virtual void init(QString *outFileName, QString const &title, QString const &subject) = 0; virtual void printStartList(QList const &startList) = 0; virtual void printRanking(Ranking const &categories, QList const &ranking) = 0; diff --git a/rankingswizard.cpp b/rankingswizard.cpp index 4b318a8..24d326d 100644 --- a/rankingswizard.cpp +++ b/rankingswizard.cpp @@ -153,7 +153,7 @@ void RankingsWizard::printStartList() auto const &infoMessages = QObject::connect(printer.data(), &RankingPrinter::info, this, &RankingsWizard::forwardInfoMessage); auto const &errorMessages = QObject::connect(printer.data(), &RankingPrinter::error, this, &RankingsWizard::forwardErrorMessage); - printer->init(&startListFileName, raceData->getEvent() + " - " + tr("Start List")); + printer->init(&startListFileName, raceData->getEvent(), tr("Start List")); // print the startlist printer->printStartList(startList); @@ -194,7 +194,7 @@ void RankingsWizard::printRankingsSingleFile() auto const &printerInfoMessages = QObject::connect(printer.data(), &RankingPrinter::info, this, &RankingsWizard::forwardInfoMessage); auto const &printerErrorMessages = QObject::connect(printer.data(), &RankingPrinter::error, this, &RankingsWizard::forwardErrorMessage); - printer->init(&rankingsFileName, raceData->getEvent() + " - " + tr("Results")); + printer->init(&rankingsFileName, raceData->getEvent(), tr("Results")); // now print each ranking for (auto &rankingItem : rankingsList) { @@ -262,7 +262,7 @@ void RankingsWizard::printRankingsMultiFile() continue; outFileBaseName = QDir(rankingsBasePath).filePath(QString("class%1_%2").arg(k, rWidth, 10, QChar('0')).arg(rankingItem.categories->getShortDescription())); - printer->init(&outFileBaseName, raceData->getEvent() + " - " + tr("Results") + " - " + rankingItem.categories->getFullDescription()); + printer->init(&outFileBaseName, raceData->getEvent(), tr("Results") + " - " + rankingItem.categories->getFullDescription()); if (rankingItem.categories->isTeam()) { // build the ranking diff --git a/translations/LBChronoRace_en.ts b/translations/LBChronoRace_en.ts index f500757..5881622 100644 --- a/translations/LBChronoRace_en.ts +++ b/translations/LBChronoRace_en.ts @@ -1282,6 +1282,10 @@ The definitions of Categories and Rankings must be reviewed and corrected.Cannot write to PDF Cannot write to PDF + + Rankings + Rankings + QMessageBox diff --git a/translations/LBChronoRace_it.ts b/translations/LBChronoRace_it.ts index 08e013d..4e99786 100644 --- a/translations/LBChronoRace_it.ts +++ b/translations/LBChronoRace_it.ts @@ -1282,6 +1282,10 @@ Le definizioni di Categorie e Classifiche devono essere riviste e corrette.Cannot write to PDF Impossibile scrivere il PDF + + Rankings + Classifiche + QMessageBox diff --git a/txtrankingprinter.cpp b/txtrankingprinter.cpp index 6ec651f..b4873e1 100644 --- a/txtrankingprinter.cpp +++ b/txtrankingprinter.cpp @@ -22,11 +22,12 @@ #include "lbcrexception.hpp" #include "crhelper.hpp" -void TXTRankingPrinter::init(QString *outFileName, QString const &title) +void TXTRankingPrinter::init(QString *outFileName, QString const &title, QString const &subject) { Q_ASSERT(!txtFile.isOpen()); Q_UNUSED(title) + Q_UNUSED(subject) if (outFileName == Q_NULLPTR) { throw(ChronoRaceException(tr("Error: no file name supplied"))); diff --git a/txtrankingprinter.hpp b/txtrankingprinter.hpp index b4767bc..41afb5d 100644 --- a/txtrankingprinter.hpp +++ b/txtrankingprinter.hpp @@ -29,7 +29,7 @@ class TXTRankingPrinter final : public RankingPrinter using RankingPrinter::RankingPrinter; public: - void init(QString *outFileName, QString const &title) override; + void init(QString *outFileName, QString const &title, QString const &subject) override; void printStartList(QList const &startList) override; void printRanking(Ranking const &categories, QList const &ranking) override; From 3fa8d8c06be94f86217aff859d706e309fb3d529 Mon Sep 17 00:00:00 2001 From: Lorenzo Buzzi Date: Sun, 25 Aug 2024 12:28:41 +0200 Subject: [PATCH 20/20] Remove previous version at install time This should work around the limitation of the Qt Installer Framework that allows upgrading a previous installation only if its installer is started in maintenance mode and the repository is on-line. --- installer/config/config.xml.in | 1 + installer/config/uninstall.qs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 installer/config/uninstall.qs diff --git a/installer/config/config.xml.in b/installer/config/config.xml.in index becee3d..4119f7d 100644 --- a/installer/config/config.xml.in +++ b/installer/config/config.xml.in @@ -26,4 +26,5 @@ LBChronoRace @at@ApplicationsDirX64@at@/LBChronoRace true + uninstall.qs diff --git a/installer/config/uninstall.qs b/installer/config/uninstall.qs new file mode 100644 index 0000000..1f54255 --- /dev/null +++ b/installer/config/uninstall.qs @@ -0,0 +1,16 @@ +function Controller() { +} + +Controller.prototype.TargetDirectoryPageCallback = function() { + var uninstaller = installer.toNativeSeparators(installer.value("TargetDir") + "/maintenancetool.exe"); + if (installer.fileExists(uninstaller)) { + console.log("Try uninstalling previous version..."); + var isAdmin = installer.hasAdminRights(); + if (isAdmin || installer.gainAdminRights()) { + var results = installer.execute(uninstaller, [ "--confirm-command", "pr" ]); + console.log("Uninstall returned " + results[0] + "(" + results[1] + ")"); + if (!isAdmin) + installer.dropAdminRights(); + } + } +} \ No newline at end of file