diff --git a/companion/src/firmwares/radiodata.cpp b/companion/src/firmwares/radiodata.cpp index 3dc3b5b85..4de3c4a20 100644 --- a/companion/src/firmwares/radiodata.cpp +++ b/companion/src/firmwares/radiodata.cpp @@ -106,6 +106,8 @@ void RadioData::convert(RadioDataConversionState & cstate) void RadioData::addLabel(QString label) { + label.replace("/c",","); + label.replace("//","/"); if(labels.indexOf(label) == -1) labels.append(label); } @@ -113,12 +115,16 @@ void RadioData::addLabel(QString label) bool RadioData::deleteLabel(QString label) { bool deleted = false; + QString csvLabel = label; + csvLabel.replace("/","//"); + csvLabel.replace(",","/c"); + // Remove labels in the models for(auto& model : models) { QStringList modelLabels = QString(model.labels).split(',',QString::SkipEmptyParts); - if(modelLabels.indexOf(label) >= 0) + if(modelLabels.indexOf(csvLabel) >= 0) deleted = true; - modelLabels.removeAll(label); + modelLabels.removeAll(csvLabel); strcpy(model.labels, QString(modelLabels.join(',')).toLocal8Bit().data()); } @@ -143,6 +149,13 @@ bool RadioData::renameLabel(QString from, QString to) { bool success = true; int lengthdiff = to.size() - from.size(); + QString csvFrom = from; + QString csvTo = to; + csvFrom.replace("/","//"); + csvFrom.replace(",","/c"); + csvTo.replace("/","//"); + csvTo.replace(",","/c"); + // Check that rename is possible, not too long for(auto& model : models) { @@ -153,10 +166,12 @@ bool RadioData::renameLabel(QString from, QString to) if(success) { for(auto& model : models) { QStringList modelLabels = QString(model.labels).split(',',QString::SkipEmptyParts); - int ind = modelLabels.indexOf(from); // TOOD: Make sure it will fit + int ind = modelLabels.indexOf(csvFrom); if(ind != -1) { - modelLabels.replace(ind, to); - strcpy(model.labels, QString(modelLabels.join(',')).toLocal8Bit().data()); + modelLabels.replace(ind, csvTo); + QString outputcsv = QString(modelLabels.join(',')); + if(outputcsv.toLocal8Bit().size() < (int)sizeof(model.labels)); + strcpy(model.labels, outputcsv.toLocal8Bit().data()); } } int ind = labels.indexOf(from); @@ -178,6 +193,9 @@ bool RadioData::addLabelToModel(int index, QString label) { if(index >= models.size()) return false; + label.replace("/","//"); + label.replace(",","/c"); + char *modelLabelCsv = models[index].labels; // Make sure it will fit if(strlen(modelLabelCsv) + label.size() + 1 < sizeof(models[index].labels)-1) { @@ -194,6 +212,8 @@ bool RadioData::addLabelToModel(int index, QString label) bool RadioData::removeLabelFromModel(int index, QString label) { if(index >= models.size()) return false; + label.replace("/","//"); + label.replace(",","/c"); char *modelLabelCsv = models[index].labels; QStringList modelLabels = QString(modelLabelCsv).split(',',QString::SkipEmptyParts); diff --git a/companion/src/labels.cpp b/companion/src/labels.cpp index 6cb4ef92f..97880d8fa 100644 --- a/companion/src/labels.cpp +++ b/companion/src/labels.cpp @@ -52,7 +52,7 @@ bool LabelsModel::setData(const QModelIndex &index, const QVariant &value, int r return true; } else if(role == Qt::EditRole) { QString replFrom = labels[index.row()].label; - QString replTo = value.toString().replace(',','_'); // Don't allow comma, replace with _ + QString replTo = value.toString(); if(radioData->labels.indexOf(replTo) == -1) { // Don't allow duplicates bool modelsChanged = radioData->renameLabel(replFrom,replTo); labels[index.row()].label = replTo; @@ -84,6 +84,8 @@ QVariant LabelsModel::data(const QModelIndex &index, int role) const if(index.column() == 0 && selectedModel >= 0 && selectedModel < (int)radioData->models.size()) { QStringList modelLabels = QString(radioData->models.at(selectedModel).labels).split(',',QString::SkipEmptyParts); + label.replace("/","//"); + label.replace(",","/c"); return modelLabels.indexOf(label)==-1?Qt::Unchecked:Qt::Checked; } else if (index.column() == 0 && selectedModel == -1) { return Qt::Unchecked; diff --git a/companion/src/mdichild.cpp b/companion/src/mdichild.cpp index 745c198b4..c7d49d698 100644 --- a/companion/src/mdichild.cpp +++ b/companion/src/mdichild.cpp @@ -665,76 +665,6 @@ void MdiChild::onFirmwareChanged() } } -/* - * Categories CRUD -*/ - -/* -void MdiChild::categoryAdd() -{ - // Translators do NOT use accent for this, this is the default category name on Horus. - CategoryData category(qPrintable(tr("New category"))); - radioData.categories.push_back(category); - setModified(); - QModelIndex idx = modelsListModel->getIndexForCategory(radioData.categories.size()-1); - if (idx.isValid()) { - ui->modelsList->scrollTo(idx); - ui->modelsList->setCurrentIndex(idx); - ui->modelsList->edit(idx); - } -} - -// NOTE: this does not refresh the data/view, need to call setModified() at some point afterwards. -bool MdiChild::deleteCategory(int categoryIndex, QString * error) -{ - if (categoryIndex < 0) - categoryIndex = modelsListModel->getCategoryIndex(getCurrentIndex()); - if (categoryIndex < 0 || categoryIndex >= (int)radioData.categories.size()) { - if (error) - *error = tr("Category index out of range."); - return false; - } - if (radioData.categories.size() <= 1) { - if (error) - *error = tr("Cannot delete the last category."); - return false; - } - if (countUsedModels(categoryIndex)) { - if (error) - *error = tr("This category is not empty!"); - return false; - } - - radioData.categories.erase(radioData.categories.begin() + categoryIndex); - - for (unsigned i=0; i < radioData.models.size(); ++i) { - ModelData & model = radioData.models.at(i); - if (model.used && model.category > categoryIndex) { - --radioData.models[i].category; - } - } - - return true; -} - -void MdiChild::deleteSelectedCats() -{ - bool modified = false; - QString error; - QVector cats = getSelectedCategories(); - std::sort(cats.begin(), cats.end()); - std::reverse(cats.begin(), cats.end()); - foreach (const int cat, cats) { - error.clear(); - if (deleteCategory(cat, &error)) - modified = true; - else - showWarning(error); - } - if (modified) - setModified(); -}*/ - /* * Models CRUD */ diff --git a/companion/src/modelslist.cpp b/companion/src/modelslist.cpp index 7979520e8..82f4ac6f1 100644 --- a/companion/src/modelslist.cpp +++ b/companion/src/modelslist.cpp @@ -630,19 +630,10 @@ void ModelsListModel::onRowsRemoved(const QModelIndex & parent, int first, int l void ModelsListModel::refresh() { - ModelListItem * defaultCategoryItem = NULL; - this->blockSignals(true); // make sure onRowsRemoved is not triggered removeRows(0, rowCount()); this->blockSignals(false); - /*if (hasCategories) { - for (unsigned i = 0; i < radioData->categories.size(); i++) { - ModelListItem * current = rootItem->appendChild(i, -1); - current->setData(0, QString(radioData->categories[i].name)); - } - }*/ - for (unsigned i = 0; i < radioData->models.size(); i++) { ModelData & model = radioData->models[i]; int currentColumn = 0; diff --git a/radio/src/storage/modelslist.cpp b/radio/src/storage/modelslist.cpp index 65a437abb..9b72ffbd7 100644 --- a/radio/src/storage/modelslist.cpp +++ b/radio/src/storage/modelslist.cpp @@ -323,7 +323,6 @@ LabelsVector ModelMap::getLabels() * index to it. If label doesn't exist it adds it at the end of the * list and returns the new index * Won't allow creation of the special case label "Unlabeled" STR_UNLABELEDMODEL - * All commas(',') are replaced with underscore('_') * * @param lbl Adds a label to the list * @return int -1 on failure, label index on success @@ -334,9 +333,6 @@ int ModelMap::addLabel(std::string lbl) if (lbl.size() == 0) return -1; if (lbl == STR_UNLABELEDMODEL) return -1; - // Remove any commas - std::replace(lbl.begin(), lbl.end(), ',', '_'); - // Add a new label if if doesn't already exist in the list // Returns the index to the label int ind = getIndexByLabel(lbl); @@ -364,12 +360,8 @@ bool ModelMap::addLabelToModel(const std::string &lbl, ModelCell *cell, { // First check that there aren't too many labels on this model LabelsVector lbs = getLabelsByModel(cell); - int sz = 0; - for (auto const &it : lbs) { - sz = it.size() + 1; // Label length + ',' - } - sz += lbl.size() + 1; // New label + ',' - if (sz > LABELS_LENGTH - 1) { + lbs.push_back(lbl); + if(ModelMap::toCSV(lbs).size() > LABELS_LENGTH - 1) { TRACE("Cannot add the %s label to the model. Too many labels", lbl.c_str()); return true; } @@ -409,6 +401,73 @@ void ModelMap::setSortOrder(ModelsSortBy sortby) } } +/** + * @brief Convert a vector of labels to a comma separated list + * + * @param labels Vector of the labels + * @return labels list in CSV format + */ + +std::string ModelMap::toCSV(const LabelsVector &labels) +{ + std::string csv; + bool comma=false; + for(auto lbl: labels) { + if(lbl == "") + continue; + if(comma) { + csv += ','; + } + // Escape out commas + replace_all(lbl, "/", "//"); + replace_all(lbl, ",", "/c"); + csv += lbl; + comma = true; + } + return csv; +} + +/** + * @brief Convert a Comma separated string list to LabelsVector + * + * @param str Comma separated string of labels + * @return vector of all labels + */ + +LabelsVector ModelMap::fromCSV(const char *str) +{ + LabelsVector lbls; + std::istringstream f(str); + std::string lbl; + while (std::getline(f, lbl, ',')) { + // Un-escape commas + replace_all(lbl, "/c", ","); + replace_all(lbl, "//", "/"); + lbls.push_back(lbl); + } + return lbls; +} + +/** + * @brief Replace all occurances of to in + * + * @param str String + * @param from String to search + * @param to String to replace + */ + +void ModelMap::replace_all(std::string &str, + const std::string &from, + const std::string &to) +{ + if(from.empty()) return; + size_t pos = 0; + while((pos = str.find(from, pos)) != std::string::npos) { + str.replace(pos, from.length(), to); + pos += to.length(); + } +} + /** * @brief Removes a label from a model * @@ -440,9 +499,8 @@ bool ModelMap::removeLabelFromModel(const std::string &label, ModelCell *cell, /** * @brief Removes a label * @details Remove a label from the list, only if there are no models that have - * the label selected. The label isn't actually removed only set blank - * this is to keep the proper index of the multimap. - * On reboot it will be re-synced again. + * the label selected. The label is set blank, when writing this will be + * skipped. Labels are re-synced * * @param label Label to be removed * @return true Label wasn't found or not empty @@ -468,6 +526,11 @@ bool ModelMap::removeLabel( addLabel(STR_FAVORITE_LABEL); } + // Save and rescan + modelslist.save(); + modelslist.clear(); + modelslist.load(); + return renameFault; } @@ -546,8 +609,18 @@ bool ModelMap::renameLabel( readModelYaml(modcell->modelFilename, (uint8_t *)modeldata, sizeof(ModelData)); - // Make sure there is room to rename - int nlen = strlen(modeldata->header.labels) + to.size() - from.size(); + // Make sure there is room to rename, use toCSV to capture escape chars + LabelsVector tmpvect; + tmpvect.push_back(to); + int newlen = ModelMap::toCSV(tmpvect).size(); + tmpvect.clear(); + tmpvect.push_back(from); + int oldlen = ModelMap::toCSV(tmpvect).size(); + + // Separate Curent CSV + LabelsVector lbls = ModelMap::fromCSV(modeldata->header.labels); + + int nlen = lbls.size() + newlen - oldlen; if (nlen > LABELS_LENGTH - 1) { fault = true; TRACE("Labels: Rename Error! Labels too long on %s - %s", @@ -555,37 +628,20 @@ bool ModelMap::renameLabel( continue; } - // Separate CSV - LabelsVector lbls; - char *cma; - cma = strtok(modeldata->header.labels, ","); - int numTokens = 0; - while (cma != NULL) { - lbls.push_back(cma); - cma = strtok(NULL, ","); - numTokens++; - } - // Replace from->to strings for (auto &lbl : lbls) { if (lbl == from) lbl = to; } - // Remove duplicates + // Remove possible duplicates std::sort(lbls.begin(), lbls.end()); auto last = std::unique(lbls.begin(), lbls.end()); lbls.erase(last, lbls.end()); lbls.resize(std::distance(lbls.begin(), last)); // Write back - bool comma = false; - modeldata->header.labels[0] = '\0'; - for (auto lbl : lbls) { - if (lbl == "") continue; - if (comma) strcat(modeldata->header.labels, ","); - strcat(modeldata->header.labels, lbl.c_str()); - comma = true; - } + strncpy(modeldata->header.labels, ModelMap::toCSV(lbls).c_str(), LABELS_LENGTH); + modeldata->header.labels[LABELS_LENGTH-1] = '\0'; char path[256]; getModelPath(path, modcell->modelFilename); @@ -631,25 +687,20 @@ bool ModelMap::renameLabel( } /** - * @brief Returns a comma separated list of the labels + * @brief Returns a comma separated list of the labels, used in model_setup * - * @param curmod Module - * @return std::string + * @param curmod Model Cell + * @param noresults String to return when no labels found + * @return std::string of all Labels, if no results return */ std::string ModelMap::getLabelString(ModelCell *curmod, const char *noresults) { - std::string allLabels; - int numModels = 0; - for (auto &label : getSelectedLabels(curmod)) { - if (label.second) { - allLabels = allLabels + (numModels != 0 ? "," : "") + label.first; - numModels++; - } + std::string lbls = ModelMap::toCSV(getLabelsByModel(curmod)); + if(lbls.size() == 0) { + return noresults; } - if (numModels == 0) allLabels = noresults; - - return allLabels; + return lbls; } /** @@ -781,6 +832,9 @@ void ModelsList::init() void ModelsList::clear() { + for(ModelCell *mdl: *this) { + delete(mdl); + } std::vector::clear(); init(); } @@ -843,11 +897,9 @@ void ModelMap::updateModelCell(ModelCell *cell) readModelYaml(cell->modelFilename, (uint8_t *)model, sizeof(ModelData)); strcpy(cell->modelName, model->header.name); strcpy(cell->modelBitmap, model->header.bitmap); - char *cma; - cma = strtok(model->header.labels, ","); - while (cma != NULL) { - modelslabels.addLabelToModel(cma, cell); - cma = strtok(NULL, ","); + LabelsVector labels = ModelMap::fromCSV(model->header.labels); + for(const auto &lbl : labels ) { + modelslabels.addLabelToModel(lbl,cell); } // Save Module Data @@ -1105,17 +1157,7 @@ const char *ModelsList::save(LabelsVector newOrder) (unsigned int)model->moduleData[i].subType); } - f_puts(" labels: \"", &file); - LabelsVector labels = modelslabels.getLabelsByModel(model); - bool comma = false; - for (auto const &label : labels) { - if (comma) { - f_puts(",", &file); - } - f_puts(label.c_str(), &file); - comma = true; - } - f_puts("\"\r\n", &file); + f_printf(&file, " labels: \"%s\"\r\n", ModelMap::toCSV(modelslabels.getLabelsByModel(model)).c_str()); #if LEN_BITMAP_NAME > 0 f_puts(" bitmap: \"", &file); @@ -1202,7 +1244,7 @@ bool ModelsList::readNextLine(char *line, int maxlen) * * @param name Model File Name * @param save True=Update labels.yml right away - * @param copyCell If duplicating, copy the data from this cell. If duplicating, leave null + * @param copyCell If duplicating copy the data from this cell, otherwise leave null * @return ModelCell* New Model */ diff --git a/radio/src/storage/modelslist.h b/radio/src/storage/modelslist.h index 1fd724540..ca7328e60 100644 --- a/radio/src/storage/modelslist.h +++ b/radio/src/storage/modelslist.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include "sdcard.h" @@ -158,6 +159,12 @@ class ModelMap : protected std::multimap void setSortOrder(ModelsSortBy sortby); ModelsSortBy sortOrder() {return _sortOrder;} + static std::string toCSV(const LabelsVector &labels); + static LabelsVector fromCSV(const char *str); + static void replace_all(std::string &str, + const std::string &from, + const std::string &to); + protected: ModelsSortBy _sortOrder = DEFAULT_MODEL_SORT; bool _isDirty = true; diff --git a/radio/src/storage/yaml/yaml_labelslist.cpp b/radio/src/storage/yaml/yaml_labelslist.cpp index b86411f38..2f9302483 100644 --- a/radio/src/storage/yaml/yaml_labelslist.cpp +++ b/radio/src/storage/yaml/yaml_labelslist.cpp @@ -111,7 +111,6 @@ static bool find_node(void* ctx, char* buf, uint8_t len) TRACE_LABELS_YAML("YAML Found Node. Level = %u, Section= %u, %s", mi->level, mi->section, mi->current_attr); - // Switch between the different maps in labels.yml if (mi->level == 0) { if (strcasecmp(mi->current_attr,"labels") == 0) { @@ -131,7 +130,11 @@ static bool find_node(void* ctx, char* buf, uint8_t len) bool found=false; for(auto &filehash : modelslist.fileHashInfo) { if(filehash.name == mi->current_attr) { - TRACE_LABELS_YAML(" Model %s has a real file, creating a modelcell"); + TRACE_LABELS_YAML(" Model %s has a real file, creating a modelcell", filehash.name); + if(filehash.celladded) { + TRACE_LABELS_YAML(" Duplicate found labels.yml model cell %s already added", filehash.name); + break; + } ModelCell *model = new ModelCell(mi->current_attr); strcpy(model->modelFinfoHash, filehash.hash); modelslist.push_back(model); @@ -172,81 +175,71 @@ static void set_attr(void* ctx, char* buf, uint8_t len) TRACE_LABELS_YAML("YAML Attr Level %u, Section %u, %s = %s", mi->level, mi->section, mi->current_attr, value); // Model Section - if(mi->level == 2 && mi->section == labelslist_iter::SEC_Models) { + if(mi->level == 2 && mi->section == labelslist_iter::SEC_Models && mi->curmodel != NULL) { // File Hash if(!strcasecmp(mi->current_attr, "hash")) { - if(mi->curmodel != NULL) { - if(!strcmp(mi->curmodel->modelFinfoHash, value)) { - TRACE_LABELS_YAML("FILE HASH MATCHES, No need to scan this model, just load the settings"); - mi->modeldatavalid = true; - mi->curmodel->valid_rfData = true; - mi->curmodel->_isDirty = false; - } else { - TRACE_LABELS_YAML("FILE HASH Does not Match, Open model and rebuild modelcell"); - mi->modeldatavalid = false; - mi->curmodel->_isDirty = true; - } + if(!strcmp(mi->curmodel->modelFinfoHash, value)) { + TRACE_LABELS_YAML("FILE HASH MATCHES, No need to scan this model, just load the settings"); + mi->modeldatavalid = true; + mi->curmodel->valid_rfData = true; + mi->curmodel->_isDirty = false; + } else { + TRACE_LABELS_YAML("FILE HASH Does not Match, Open model and rebuild modelcell"); + mi->modeldatavalid = false; + mi->curmodel->_isDirty = true; } + } - // Model Name - } else if(mi->modeldatavalid && !strcasecmp(mi->current_attr, "name")) { - if(mi->curmodel != NULL) { + // Don't bother filling in values below if hash didn't match + if(mi->modeldatavalid) { + // Model Name + if(!strcasecmp(mi->current_attr, "name")) { mi->curmodel->setModelName(value); TRACE_LABELS_YAML(" Set the models name"); - } - // Last Opened - } else if(mi->modeldatavalid && !strcasecmp(mi->current_attr, "lastopen")) { - if(mi->curmodel != NULL) { + // Last Opened + } else if(!strcasecmp(mi->current_attr, "lastopen")) { mi->curmodel->lastOpened = (gtime_t)strtol(value, NULL, 0); TRACE_LABELS_YAML(" Last Opened %lu", value); - } - // Model Bitmap -#if LEN_BITMAP_NAME > 0 - } else if(mi->modeldatavalid && !strcasecmp(mi->current_attr, "bitmap")) { - if(mi->curmodel != NULL) { + // Model Bitmap + #if LEN_BITMAP_NAME > 0 + } else if(!strcasecmp(mi->current_attr, "bitmap")) { // TODO Check if it exists ? strcpy(mi->curmodel->modelBitmap, value); TRACE_LABELS_YAML(" Set the models bitmap"); - } -#endif + #endif - // Model Labels - } else if(mi->modeldatavalid && !strcasecmp(mi->current_attr, "labels")) { - if(mi->curmodel != NULL) { - char *cma; - cma = strtok(value, ","); - int numTokens = 0; - while(cma != NULL) { - modelslabels.addLabelToModel(cma,mi->curmodel); - TRACE_LABELS_YAML(" Adding the label - %s", cma); - cma = strtok(NULL, ","); - numTokens++; + // Model Labels + } else if(!strcasecmp(mi->current_attr, "labels")) { + LabelsVector labels = ModelMap::fromCSV(value); + for(const auto &lbl : labels ) { + modelslabels.addLabelToModel(lbl,mi->curmodel); + TRACE_LABELS_YAML(" Adding the label - %s", lbl.c_str()); } - } - // RF Module Data - } else { - char cmp[15]; - for(int i=0; i < NUM_MODULES; i++) { - snprintf(cmp, sizeof(cmp), MODULE_ID_STR, i); - cmp[sizeof(cmp)-1] = '\0'; - if(mi->curmodel != NULL && mi->modeldatavalid && !strcasecmp(mi->current_attr, cmp)) { - mi->curmodel->modelId[i] = strtol(value,NULL,10); - TRACE_LABELS_YAML( " Set the module %d rfId to %s", i, value); - } - snprintf(cmp, sizeof(cmp), MODULE_TYPE_STR, i); - cmp[sizeof(cmp)-1] = '\0'; - if(mi->curmodel != NULL && mi->modeldatavalid && !strcasecmp(mi->current_attr, cmp)) { - mi->curmodel->moduleData[i].type = strtol(value,NULL,10); - TRACE_LABELS_YAML(" Set the module %d rfType to %s", i, value); - } - snprintf(cmp, sizeof(cmp), MODULE_RFPROTOCOL_STR, i); - cmp[sizeof(cmp)-1] = '\0'; - if(mi->curmodel != NULL && mi->modeldatavalid && !strcasecmp(mi->current_attr, cmp)) { - mi->curmodel->moduleData[i].subType = strtol(value,NULL,10); - TRACE_LABELS_YAML(" Set the module %d rfProtocol to %s", i, value); + // RF Module Data + } else { + char cmp[15]; + for(int i=0; i < NUM_MODULES; i++) { + snprintf(cmp, sizeof(cmp), MODULE_ID_STR, i); + cmp[sizeof(cmp)-1] = '\0'; + if(!strcasecmp(mi->current_attr, cmp)) { + mi->curmodel->modelId[i] = strtol(value,NULL,10); + TRACE_LABELS_YAML( " Set the module %d rfId to %s", i, value); + } + snprintf(cmp, sizeof(cmp), MODULE_TYPE_STR, i); + cmp[sizeof(cmp)-1] = '\0'; + if(!strcasecmp(mi->current_attr, cmp)) { + mi->curmodel->moduleData[i].type = strtol(value,NULL,10); + TRACE_LABELS_YAML(" Set the module %d rfType to %s", i, value); + } + snprintf(cmp, sizeof(cmp), MODULE_RFPROTOCOL_STR, i); + cmp[sizeof(cmp)-1] = '\0'; + if(!strcasecmp(mi->current_attr, cmp)) { + mi->curmodel->moduleData[i].subType = strtol(value,NULL,10); + TRACE_LABELS_YAML(" Set the module %d rfProtocol to %s", i, value); + } } } }