refactor(color): classes used to manage models and labels, improve duplication UX (#7739)

Co-authored-by: philmoz <phil.a.mitchell@gmail.com>
Co-authored-by: Peter Feerick <5500713+pfeerick@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
philmoz
2026-09-25 13:44:12 +10:00
committed by GitHub
co-authored by philmoz Peter Feerick Claude Opus 5.5
parent 61aa7a44c9
commit 6a1cf34079
22 changed files with 2110 additions and 1121 deletions
+1 -1
View File
@@ -230,7 +230,7 @@ bool RadioData::addLabelToModel(int index, QString label)
auto & modelLabelCsv = models[index].labels;
// Make sure it will fit
if (modelLabelCsv.size() + (size_t)label.size() + 1 < modelLabelCsv.capacity()) {
if (modelLabelCsv.size() + (size_t)label.size() + 1 <= modelLabelCsv.capacity()) {
QStringList modelLabels = QString::fromLatin1(modelLabelCsv.c_str()).split(',', Qt::SkipEmptyParts);
if (modelLabels.indexOf(label) == -1) {
modelLabels.append(label);
+1 -1
View File
@@ -37,7 +37,7 @@
#define LUA_EXPORT_EXTRA(...)
#endif
#define LABELS_LENGTH 100 // Maximum length of the label string
#define LABELS_LENGTH 99 // Maximum length of the label string
#define LABEL_LENGTH 16
#if defined(COLORLCD)
+1 -1
View File
@@ -608,7 +608,7 @@ PACK(struct ModelHeader {
uint8_t modelId[NUM_MODULES];
MODEL_HEADER_BITMAP_FIELD
#if defined(STORAGE_MODELSLIST)
char labels[LABELS_LENGTH];
char labels[LABELS_LENGTH + 1]; // alow null terminator
#endif
});
-4
View File
@@ -2039,11 +2039,7 @@ void menuModelSetup(event_t event)
else if (event == EVT_KEY_LONG(KEY_ENTER)) {
killEvents(event);
uint8_t newVal = 0;
#if defined(STORAGE_MODELSLIST)
newVal = modelslist.findNextUnusedModelId(moduleIdx);
#else
newVal = findNextUnusedModelId(g_eeGeneral.currModel, moduleIdx);
#endif
if (newVal != g_model.header.modelId[moduleIdx]) {
modelHeaders[g_eeGeneral.currModel].modelId[moduleIdx] = g_model.header.modelId[moduleIdx] = newVal;
storageDirty(EE_MODEL);
+5 -2
View File
@@ -173,7 +173,8 @@ void ConfirmDialog::onCancel()
//-----------------------------------------------------------------------------
LabelDialog::LabelDialog(const char *label, int length, const char* title,
std::function<void(std::string)> _saveHandler) :
std::function<void(std::string)> _saveHandler,
const char* excludedChars) :
ModalWindow(false), saveHandler(std::move(_saveHandler))
{
strncpy(this->label, label, std::min(length, MAX_LABEL_LENGTH));
@@ -197,7 +198,9 @@ LabelDialog::LabelDialog(const char *label, int length, const char* title,
lv_obj_set_flex_align(box->getLvObj(), LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_SPACE_BETWEEN);
new TextEdit(box, rect_t{0, 0, LV_PCT(100), 0}, this->label, length);
auto edit = new TextEdit(box, rect_t{0, 0, LV_PCT(100), 0}, this->label, length);
if (excludedChars)
edit->setExcludedCharacters(excludedChars);
box = new Window(form, rect_t{});
box->padAll(PAD_MEDIUM);
+2 -1
View File
@@ -134,7 +134,8 @@ class LabelDialog : public ModalWindow
{
public:
LabelDialog(const char *label, int length, const char* title,
std::function<void(std::string)> _saveHandler = nullptr);
std::function<void(std::string)> _saveHandler = nullptr,
const char* excludedChars = nullptr);
static constexpr int MAX_LABEL_LENGTH = 255;
+2 -2
View File
@@ -119,8 +119,8 @@ class NumberEdit : public TextButton
std::string prefix;
std::string suffix;
std::string zeroText;
std::function<std::string(int)> displayFunction;
std::function<bool(int)> isValueAvailable;
std::function<std::string(int)> displayFunction = nullptr;
std::function<bool(int)> isValueAvailable = nullptr;
std::string getDisplayVal();
+13
View File
@@ -74,10 +74,21 @@ class TextArea : public FormField
cancelHandler = std::move(handler);
}
void setExcludedChars(std::string& excludedChars) {
if (excludedChars.size() > 0) {
for (char c = ' '; c <= '~'; c += 1) {
if (excludedChars.find(c) == std::string::npos)
allowedChars += c;
}
lv_textarea_set_accepted_chars(lvobj, allowedChars.c_str());
}
}
protected:
char* value;
uint8_t length;
std::function<void(void)> cancelHandler = nullptr;
std::string allowedChars;
void trim()
{
@@ -165,6 +176,8 @@ void TextEdit::openEdit()
lv_group_focus_obj(lvobj);
edit->hide();
});
if (excludedChars.size() > 0)
edit->setExcludedChars(excludedChars);
}
edit->update();
edit->show();
+3
View File
@@ -36,8 +36,11 @@ class TextEdit : public TextButton
void preview(bool edited, char* text, uint8_t length);
void update();
void setExcludedCharacters(const char* chars) { excludedChars = chars; }
protected:
std::function<void(void)> updateHandler = nullptr;
std::string excludedChars = "";
TextArea* edit = nullptr;
char* text;
uint8_t length;
+46 -49
View File
@@ -98,7 +98,7 @@ class ModelButton : public Button
}
lv_label_set_long_mode(modelName->getLvObj(), LV_LABEL_LONG_DOT);
bool chk = (modelCell == modelslist.getCurrentModel());
bool chk = (modelCell == modelCellManager.getCurrentModel());
if (chk != checked()) {
check(chk);
if (chk)
@@ -197,15 +197,15 @@ class ModelsPageBody : public Window
ModelsVector models;
if (selectedLabels.size()) {
models = modelslabels.getModelsInLabels(selectedLabels);
models = modelCellManager.getFilteredModelList(selectedLabels);
} else {
models = modelslabels.getAllModels();
models = modelCellManager.getAllModels();
}
// Used to work out which button to set focus to.
// Priority -
// current active model
// previously selected model
// current active model
// first model in the list
ModelButton *firstButton = nullptr;
ModelButton *focusedButton = nullptr;
@@ -238,8 +238,8 @@ class ModelsPageBody : public Window
}
if (!firstButton) firstButton = button;
if (model == modelslist.getCurrentModel()) focusedButton = button;
if (model == focusedModel && !focusedButton) focusedButton = button;
if (model == focusedModel) focusedButton = button;
if (model == modelCellManager.getCurrentModel() && !focusedButton) focusedButton = button;
// Press Handler for Models
button->setPressHandler([=]() -> uint8_t {
@@ -251,7 +251,7 @@ class ModelsPageBody : public Window
} else {
focusedModel = model;
}
return model == modelslist.getCurrentModel();
return model == modelCellManager.getCurrentModel();
});
// Long Press Handler for Models
@@ -261,7 +261,7 @@ class ModelsPageBody : public Window
focusedModel = model;
}
openMenu();
return model == modelslist.getCurrentModel();
return model == modelCellManager.getCurrentModel();
});
}
@@ -293,11 +293,11 @@ class ModelsPageBody : public Window
inline void setSortOrder(ModelsSortBy sortOrder)
{
modelslabels.setSortOrder(sortOrder);
modelCellManager.setSortOrder(sortOrder);
update();
}
ModelsSortBy getSortOrder() const { return modelslabels.sortOrder(); }
ModelsSortBy getSortOrder() const { return modelCellManager.sortOrder(); }
void setLblRefreshFunc(std::function<void()> fnc)
{
@@ -305,7 +305,6 @@ class ModelsPageBody : public Window
}
protected:
ModelsSortBy _sortOrder;
bool isDirty = false;
bool refresh = false;
std::string selectedLabel;
@@ -328,13 +327,13 @@ class ModelsPageBody : public Window
Menu *menu = new Menu();
menu->setTitle(focusedModel->modelName);
if (g_eeGeneral.modelQuickSelect ||
focusedModel != modelslist.getCurrentModel()) {
focusedModel != modelCellManager.getCurrentModel()) {
menu->addLine(STR_SELECT_MODEL, [=]() { selectModel(focusedModel); });
}
menu->addLine(STR_DUPLICATE_MODEL, [=]() { duplicateModel(focusedModel); });
menu->addLine(STR_LABEL_MODEL, [=]() { editLabels(focusedModel); });
menu->addLine(STR_SAVE_TEMPLATE, [=]() { saveAsTemplate(focusedModel); });
if (focusedModel != modelslist.getCurrentModel()) {
if (focusedModel != modelCellManager.getCurrentModel()) {
menu->addLine(STR_DELETE_MODEL, [=]() { deleteModel(focusedModel); });
}
}
@@ -343,7 +342,7 @@ class ModelsPageBody : public Window
{
// Don't need to check connection to receiver if re-selecting the active
// model
if (model != modelslist.getCurrentModel()) {
if (model != modelCellManager.getCurrentModel()) {
bool modelConnected =
TELEMETRY_STREAMING() && !g_eeGeneral.disableRssiPoweroffAlarm;
if (modelConnected) {
@@ -364,7 +363,7 @@ class ModelsPageBody : public Window
closeHandler();
// Skip reloading model if re-selecting the active model
if (model != modelslist.getCurrentModel()) {
if (model != modelCellManager.getCurrentModel()) {
// store changes (if any) and load selected model
storageFlushCurrentModel();
storageCheck(true);
@@ -382,7 +381,7 @@ class ModelsPageBody : public Window
LayoutFactory::deleteTopBarWidgets();
loadModel(g_eeGeneral.currModelFilename, true);
modelslist.setCurrentModel(model);
modelCellManager.setCurrentModel(model);
// Load new main view layout
LayoutFactory::loadCustomScreens();
@@ -413,10 +412,12 @@ class ModelsPageBody : public Window
// Make a new model which is a copy of the selected one, set the
// same labels
auto new_model =
modelslist.addModel(duplicatedFilename, true, model);
for (const auto &lbl : modelslabels.getLabelsByModel(model)) {
modelslabels.addLabelToModel(lbl, new_model);
}
modelCellManager.addModel(duplicatedFilename, true, model);
// Give the copy a unique name and save it to disk
new_model->setUniqueName();
new_model->updateModelFile();
// Set new model as focused button
focusedModel = new_model;
update();
} else {
TRACE("ModelsListError: Invalid File");
@@ -429,7 +430,7 @@ class ModelsPageBody : public Window
new ConfirmDialog(
STR_DELETE_MODEL,
std::string(model->modelName, sizeof(model->modelName)).c_str(), [=] {
modelslist.removeModel(model);
modelCellManager.removeModel(model);
if (refreshLabels != nullptr) refreshLabels();
update();
@@ -438,7 +439,7 @@ class ModelsPageBody : public Window
void editLabels(ModelCell *model)
{
auto labels = modelslabels.getLabels();
auto labels = modelCellManager.getLabels();
// dont display menu if there will be no labels
if (labels.size()) {
@@ -451,18 +452,18 @@ class ModelsPageBody : public Window
}
});
for (auto &label : modelslabels.getLabels()) {
for (auto &label : modelCellManager.getLabels()) {
menu->addLineBuffered(
label,
[=]() {
if (!modelslabels.isLabelSelected(label, model))
modelslabels.addLabelToModel(label, model, true);
if (!model->hasLabel(label))
modelCellManager.addLabelToModel(label, model);
else
modelslabels.removeLabelFromModel(label, model, true);
modelCellManager.removeLabelFromModel(label, model);
isDirty = true;
if (refreshLabels != nullptr) refreshLabels();
},
[=]() { return modelslabels.isLabelSelected(label, model); });
[=]() { return model->hasLabel(label); });
}
menu->updateLines();
}
@@ -537,9 +538,9 @@ ModelLabelsWindow::ModelLabelsWindow() : Page(ICON_MODEL_SELECT, PAD_ZERO, true)
buildBody(body);
// find the first label of the current model and make that label active
auto currentModel = modelslist.getCurrentModel();
auto currentModel = modelCellManager.getCurrentModel();
if (currentModel != nullptr) {
auto modelLabels = modelslabels.getLabelsByModel(currentModel);
auto modelLabels = currentModel->getLabels();
if (modelLabels.size() > 0) {
auto allLabels = getLabels();
auto found =
@@ -613,8 +614,8 @@ void ModelLabelsWindow::newModel()
new SelectTemplateFolder([=](std::string folder, std::string name) {
// Create a new blank ModelCell and activate it first, createmodel() will
// modify the model in memory.
auto newCell = modelslist.addModel("", false);
modelslist.setCurrentModel(newCell);
auto newCell = modelCellManager.addModel("", false);
modelCellManager.setCurrentModel(newCell);
// Make the new model
createModel();
@@ -638,7 +639,7 @@ void ModelLabelsWindow::newModel()
storageCheck(true);
// Update the current cell's data
modelslist.updateCurrentModelCell();
modelCellManager.updateCurrentModelCell();
#if defined(LUA)
// If there is a wizard Lua script, fire it up
@@ -661,12 +662,12 @@ void ModelLabelsWindow::newLabel()
{
tmpLabel[0] = '\0';
new LabelDialog(tmpLabel, LABEL_LENGTH, STR_ENTER_LABEL, [=](std::string label) {
int newlabindex = modelslabels.addLabel(label);
int newlabindex = modelCellManager.addLabel(label);
if (newlabindex >= 0) {
auto labels = getLabels();
lblselector->setNames(labels);
}
});
}, labelExcludedChars);
}
void ModelLabelsWindow::buildHead(Window *hdr)
@@ -747,14 +748,14 @@ void ModelLabelsWindow::buildBody(Window *window)
});
#endif
std::set<uint32_t> filteredLabels = modelslabels.filteredLabels();
std::set<uint32_t> filteredLabels = modelCellManager.filteredLabels();
if (g_eeGeneral.labelSingleSelect == 0) {
lblselector->setMultiSelect(true);
lblselector->setSelected(filteredLabels);
lblselector->setMultiSelectHandler([=](std::set<uint32_t> selected,
std::set<uint32_t> oldselection) {
if (modelslabels.getUnlabeledModels().size() != 0) {
if (modelCellManager.getUnlabeledModels().size() != 0) {
// Special case for mutually exclusive Unsorted
bool unsrt_is_selected =
selected.find(lblselector->getRowCount() - 1) != selected.end();
@@ -831,7 +832,7 @@ void ModelLabelsWindow::buildBody(Window *window)
if (newLabel.size() > 0) {
auto rndialog =
new ProgressDialog(STR_RENAME_LABEL, [=]() {});
modelslabels.renameLabel(
modelCellManager.renameLabel(
oldLabel, newLabel, [=](const char *name, int percentage) {
rndialog->setTitle(std::string(STR_RENAME_LABEL) + " " +
name);
@@ -840,8 +841,7 @@ void ModelLabelsWindow::buildBody(Window *window)
});
auto labels = getLabels();
lblselector->setNames(labels);
mdlselector->clearButtons();
updateFilteredLabels(modelslabels.filteredLabels(), false);
updateFilteredLabels(modelCellManager.filteredLabels(), false);
}
});
return 0;
@@ -852,7 +852,7 @@ void ModelLabelsWindow::buildBody(Window *window)
STR_DELETE_LABEL, labelToDelete.c_str(), [=]() {
auto deldialog =
new ProgressDialog(STR_DELETE_LABEL, [=]() {});
modelslabels.removeLabel(
modelCellManager.removeLabel(
labelToDelete, [=](const char *name, int percentage) {
deldialog->setTitle(std::string(STR_DELETE_LABEL) + " " +
name);
@@ -869,19 +869,18 @@ void ModelLabelsWindow::buildBody(Window *window)
else
newset.insert(lblselector->getActiveItem());
}
mdlselector->clearButtons();
updateFilteredLabels(newset);
});
return 0;
});
if (modelslabels.getLabels().size() > 1) {
if (modelCellManager.getLabels().size() > 1) {
if (selected != 0) {
menu->addLine(STR_MOVE_UP, [=]() {
moveLabel(selected, -1);
return 0;
});
}
if (selected != (int)modelslabels.getLabels().size() - 1) {
if (selected != (int)modelCellManager.getLabels().size() - 1) {
menu->addLine(STR_MOVE_DOWN, [=]() {
moveLabel(selected, 1);
return 0;
@@ -897,7 +896,7 @@ void ModelLabelsWindow::moveLabel(int selected, int direction)
{
int swapSelected = selected + direction;
modelslabels.moveLabelTo(selected, swapSelected);
modelCellManager.moveLabelUp(selected > swapSelected ? selected : swapSelected);
std::set<uint32_t> newset = lblselector->getSelection();
bool isSelected = newset.find(selected) != newset.end();
@@ -937,10 +936,8 @@ void ModelLabelsWindow::updateFilteredLabels(std::set<uint32_t> selected,
for (auto sel : selected) {
if (sel < labels.size()) sellabels.push_back(labels[sel]);
}
if (setdirty) { // Save to file?
modelslabels.setFilteredLabels(selected);
modelslabels.setDirty();
}
if (setdirty) // Save to file?
modelCellManager.setFilteredLabels(selected);
mdlselector->setLabels(sellabels); // Update the list
}
@@ -952,7 +949,7 @@ void ModelLabelsWindow::labelRefreshRequest()
void ModelLabelsWindow::setTitle()
{
auto curModel = modelslist.getCurrentModel();
auto curModel = modelCellManager.getCurrentModel();
auto modelName = curModel != nullptr ? curModel->modelName : STR_NONE;
std::string title2 = STR_ACTIVE;
+2 -2
View File
@@ -58,8 +58,8 @@ class ModelLabelsWindow : public Page
LabelsVector getLabels()
{
auto labels = modelslabels.getLabels();
if (modelslabels.getUnlabeledModels().size() > 0)
auto labels = modelCellManager.getLabels();
if (modelCellManager.getUnlabeledModels().size() > 0)
labels.emplace_back(STR_UNLABELEDMODEL);
return labels;
}
+10 -16
View File
@@ -391,7 +391,7 @@ const static SetupLineDef setupLines[] = {
new ModelTextEdit(parent, {x, y, ModelSetupPage::NAM_W, 0},
g_model.header.name, sizeof(g_model.header.name),
[=]() {
auto model = modelslist.getCurrentModel();
auto model = modelCellManager.getCurrentModel();
if (model) {
model->setModelName(g_model.header.name);
}
@@ -403,29 +403,23 @@ const static SetupLineDef setupLines[] = {
// Model labels
STR_DEF(STR_LABELS),
[](Window* parent, coord_t x, coord_t y) {
auto curmod = modelslist.getCurrentModel();
TextButton* btn = new TextButton(parent, {x, y, 0, 0}, modelslabels.getBulletLabelString(curmod, STR_UNLABELEDMODEL));
auto curmod = modelCellManager.getCurrentModel();
TextButton* btn = new TextButton(parent, {x, y, 0, 0}, curmod->getBulletLabelString(STR_UNLABELEDMODEL));
btn->setPressHandler([=]() {
Menu *menu = new Menu(true);
menu->setTitle(STR_LABELS);
for (auto &label : modelslabels.getLabels()) {
for (auto &label : modelCellManager.getLabels()) {
menu->addLineBuffered(
label,
[=]() {
if (!modelslabels.isLabelSelected(label, curmod))
modelslabels.addLabelToModel(label, curmod);
if (!curmod->hasLabel(label))
modelCellManager.addLabelToModel(label, curmod);
else
modelslabels.removeLabelFromModel(label, curmod);
btn->setText(modelslabels.getBulletLabelString(
curmod, STR_UNLABELEDMODEL));
strncpy(g_model.header.labels,
ModelMap::toCSV(modelslabels.getLabelsByModel(curmod))
.c_str(),
sizeof(g_model.header.labels));
g_model.header.labels[sizeof(g_model.header.labels) - 1] = '\0';
modelCellManager.removeLabelFromModel(label, curmod);
btn->setText(curmod->getBulletLabelString(STR_UNLABELEDMODEL));
SET_DIRTY();
},
[=]() { return modelslabels.isLabelSelected(label, curmod); });
[=]() { return curmod->hasLabel(label); });
}
menu->updateLines();
return 0;
@@ -442,7 +436,7 @@ const static SetupLineDef setupLines[] = {
},
[=](std::string newValue) {
strncpy(g_model.header.bitmap, newValue.c_str(), LEN_BITMAP_NAME);
auto model = modelslist.getCurrentModel();
auto model = modelCellManager.getCurrentModel();
if (model) {
strncpy(model->modelBitmap, newValue.c_str(), LEN_BITMAP_NAME);
model->modelBitmap[LEN_BITMAP_NAME] = '\0';
@@ -233,7 +233,7 @@ class ModuleWindow : public Window
GET_DEFAULT(*modelId), [=](int32_t newValue) {
if (newValue != *modelId) {
*modelId = newValue;
modelslist.updateCurrentModelCell();
modelCellManager.updateCurrentModelCell();
updateIDStaticText(moduleIdx);
#if defined(CROSSFIRE)
if (isModuleCrossfire(moduleIdx)) {
@@ -546,7 +546,7 @@ class ModuleWindow : public Window
if (idUnique == nullptr) return;
char buffer[50];
std::string idStr = STR_MODELIDUNIQUE;
if (!modelslist.isModelIdUnique(mdIdx, buffer, sizeof(buffer))) {
if (!modelCellManager.isModelIdUnique(mdIdx, buffer, sizeof(buffer))) {
idStr = STR_MODELIDUSED;
idStr = idStr + buffer;
lv_obj_add_state(idUnique->getLvObj(), ETX_STATE_UNIQUE_ID_WARN);
+1 -1
View File
@@ -754,7 +754,7 @@ const static SetupLineDef manageModelsSetupLines[] = {
GET_DEFAULT(g_eeGeneral.labelSingleSelect),
[=](int newValue) {
g_eeGeneral.labelSingleSelect = newValue;
modelslabels.clearFilter();
modelCellManager.clearFilter();
Messaging::send(Messaging::REFRESH);
SET_DIRTY();
});
File diff suppressed because it is too large Load Diff
+112 -144
View File
@@ -21,66 +21,25 @@
#pragma once
#include <stdint.h>
#include <algorithm>
#include <functional>
#include <list>
#include <map>
#include <set>
#include <string>
#include <sstream>
#include <vector>
#include "sdcard.h"
#include "dataconstants.h"
#include "rtc.h"
#define DEFAULT_MODEL_SORT NAME_ASC
// Characters not (currently) allowed in labels
constexpr const char* labelExcludedChars = "\\\":'-";
struct ModelHeader;
struct ModuleData;
struct SimpleModuleData {
uint8_t type = 0;
uint8_t subType = 0;
};
typedef struct PACKED {
FSIZE_t fsize; /* File size */
WORD fdate; /* Modified date */
WORD ftime; /* Modified time */
} FInfoH;
#define FILE_HASH_LENGTH (sizeof(FInfoH) * 2) // Hex string output
class ModelCell
{
public:
char modelFilename[LEN_MODEL_FILENAME + 1] = "";
char modelName[LEN_MODEL_NAME + 1] = "";
char modelFinfoHash[FILE_HASH_LENGTH + 1] = "";
#if LEN_BITMAP_NAME > 0
char modelBitmap[LEN_BITMAP_NAME + 1] = "";
#endif
gtime_t lastOpened = 0;
bool _isDirty = true;
bool valid_rfData;
uint8_t modelId[NUM_MODULES] = {0, 0};
SimpleModuleData moduleData[NUM_MODULES];
explicit ModelCell(const char *fileName);
void setModelName(char *name);
void setRfData(ModelHeader *header, ModuleData* moduleData);
void setRfModuleData(uint8_t moduleIdx, ModuleData *modData);
};
typedef std::vector<std::string> LabelsVector;
typedef std::vector<ModelCell *> ModelsVector;
typedef enum {
NO_SORT,
NAME_ASC,
@@ -90,116 +49,101 @@ typedef enum {
SORT_COUNT
} ModelsSortBy;
/**
* @brief ModelMap is a multimap of all models and their cooresponding
* labels. Lables are referenced by index, stored in var labels
*/
struct SimpleModuleData {
uint8_t type = 0;
uint8_t subType = 0;
};
class ModelMap : protected std::multimap<uint16_t, ModelCell *>
// Must match start of the FILINFO struct
typedef struct PACKED {
FSIZE_t fsize; /* File size */
WORD fdate; /* Modified date */
WORD ftime; /* Modified time */
} FInfoH;
#define FILE_HASH_LENGTH (sizeof(FInfoH) * 2) // Hex string output
typedef std::vector<std::string> LabelsVector;
struct LabelsMap
{
public:
ModelsVector getUnlabeledModels();
ModelsVector getAllModels();
ModelsVector getModelsByLabel(const std::string &);
ModelsVector getModelsInLabels(const LabelsVector &lbls);
LabelsVector getLabelsByModel(ModelCell *);
std::map<std::string, bool> getSelectedLabels(ModelCell *);
bool isLabelSelected(const std::string &, ModelCell *);
LabelsMap() {}
void addLabel(uint8_t idx);
void removeLabel(uint8_t idx);
bool deleteLabel(uint8_t idx);
void moveLabelUp(uint8_t idx);
bool hasLabel(uint8_t idx);
LabelsVector getLabels();
int addLabel(std::string lbl);
bool addLabelToModel(const std::string &, ModelCell *, bool update = false);
bool removeLabelFromModel(const std::string &label, ModelCell *,
bool update = false);
bool removeLabel(
const std::string &,
std::function<void(const char *file, int progress)> progress = nullptr);
bool moveLabelTo(unsigned current, unsigned newind);
bool renameLabel(const std::string &from, std::string to,
std::function<void(const char *file, int progress)> progress = nullptr);
std::string getCurrentLabel() { return currentlabel; };
std::string getBulletLabelString(ModelCell *, const char *noresults = "");
void setDirty(bool save = false);
void resetDirty() { _isDirty = false; }
bool isDirty() { return _isDirty; }
// Currently selected labels in the GUI
void setFilteredLabels(std::set<uint32_t> filtlbls)
{
this->filtlbls = std::move(filtlbls);
}
void clearFilter() { filtlbls.clear(); }
void addFilteredLabel(const std::string &lbl);
bool isLabelFiltered(const std::string &lbl);
std::set<uint32_t> filteredLabels() { return filtlbls; }
void setSortOrder(ModelsSortBy sortby);
ModelsSortBy sortOrder() {return _sortOrder;}
static std::string toCSV(const LabelsVector &labels);
static LabelsVector fromCSV(const char *str);
static void escapeCSV(std::string &str);
static void unEscapeCSV(std::string &str);
static void removeYAMLChars(std::string &str);
void clear()
{
_isDirty = true;
labels.clear();
std::multimap<uint16_t, ModelCell *>::clear();
}
void updateModelCell(ModelCell *);
bool removeModels(ModelCell *);
bool writeModelLabels(ModelCell*, const char*);
protected:
ModelsSortBy _sortOrder = DEFAULT_MODEL_SORT;
bool _isDirty = true;
std::set<uint32_t> filtlbls;
std::string currentlabel = "";
bool updateModelFile(ModelCell *);
void sortModelsBy(ModelsVector &mv, ModelsSortBy sortby);
int getIndexByLabel(const std::string &str)
{
auto a = std::find(labels.begin(), labels.end(), str);
return a == labels.end() ? -1 : a - labels.begin();
}
std::string getLabelByIndex(uint16_t index)
{
if (index < (uint16_t)labels.size())
return labels.at(index);
else
return std::string();
}
private:
LabelsVector labels; // Storage space for discovered labels
std::vector<uint16_t> labelMap;
};
class ModelCell
{
public:
ModelCell(const char *fileName);
char modelFilename[LEN_MODEL_FILENAME + 1] = "";
char modelName[LEN_MODEL_NAME + 1] = "";
char modelFinfoHash[FILE_HASH_LENGTH + 1] = "";
#if LEN_BITMAP_NAME > 0
char modelBitmap[LEN_BITMAP_NAME + 1] = "";
#endif
gtime_t lastOpened = 0;
bool _isDirty = true;
bool valid_rfData = false;
uint8_t modelId[NUM_MODULES] = {0, 0};
SimpleModuleData moduleData[NUM_MODULES];
void setModelName(const char *name);
void setDefaultName();
void setUniqueName();
void setRfData(ModelHeader *header, ModuleData* moduleData);
void addLabel(uint8_t idx);
void removeLabel(uint8_t idx);
void deleteLabel(uint8_t idx);
void moveLabelUp(uint8_t idx);
bool hasLabel(uint8_t idx) { return labelsMap.hasLabel(idx); }
bool hasLabel(const std::string& label);
void updateModelFile();
void updateModelCell();
void updateFinfoHash();
void duplicateFrom(ModelCell* src);
bool writeModelLabels(const char*);
LabelsVector getLabels() { return labelsMap.getLabels(); }
std::string getBulletLabelString(const char *noresults = "");
protected:
LabelsMap labelsMap;
};
typedef std::vector<ModelCell *> ModelsVector;
class ModelsList : public ModelsVector
{
bool loaded;
ModelCell *currentModel;
void init();
public:
ModelsList();
~ModelsList();
bool load();
const char *save(LabelsVector newOrder=LabelsVector());
const char *save();
void clear();
void setCurrentModel(ModelCell *cell);
void updateCurrentModelCell();
ModelCell *getCurrentModel() const { return currentModel; }
ModelCell* getCurrentModel() const { return currentModel; }
ModelCell* getModel(const char* filename);
ModelCell* getModelWithName(const char* name);
ModelCell *addModel(const char *name, bool save = true, ModelCell *copyCell = nullptr);
bool removeModel(ModelCell *model);
@@ -207,19 +151,43 @@ class ModelsList : public ModelsVector
bool isModelIdUnique(uint8_t moduleIdx, char *warn_buf, size_t warn_buf_len);
uint8_t findNextUnusedModelId(uint8_t moduleIdx);
typedef struct _filedat {
std::string name;
char hash[FILE_HASH_LENGTH + 1];
bool curmodel = false;
bool celladded = false;
} filedat;
std::vector<filedat> fileHashInfo;
void setSortOrder(ModelsSortBy sortby);
ModelsSortBy sortOrder() {return _sortOrder;}
void sortModelsBy(ModelsVector &mv, ModelsSortBy sortby);
ModelsVector getUnlabeledModels();
ModelsVector getAllModels();
ModelsVector getFilteredModelList(const LabelsVector &lbls);
int getIndexByLabel(const std::string &str);
std::string getLabelByIndex(uint16_t index);
const LabelsVector& getLabels() const { return labels; }
int addLabel(std::string lbl);
void removeLabel(const std::string &,
std::function<void(const char *file, int progress)> progress = nullptr);
void moveLabelUp(uint16_t idx);
void renameLabel(const std::string &from, const std::string& to,
std::function<void(const char *file, int progress)> progress = nullptr);
bool addLabelToModel(const std::string &, ModelCell *, bool update = true);
void addLabelsToModel(const char* labels, ModelCell* cell);
void removeLabelFromModel(const std::string &, ModelCell *);
void setFilteredLabels(std::set<uint32_t> filtlbls);
void clearFilter() { filtlbls.clear(); }
void addFilteredLabel(const std::string &lbl);
bool isLabelFiltered(const std::string &lbl);
std::set<uint32_t> filteredLabels() { return filtlbls; }
protected:
FIL file;
ModelCell *currentModel = nullptr;
ModelsSortBy _sortOrder = DEFAULT_MODEL_SORT;
LabelsVector labels; // Storage space for discovered labels
std::set<uint32_t> filtlbls;
bool loadYaml();
};
extern ModelsList modelslist;
extern ModelMap modelslabels;
extern ModelsList modelCellManager;
+4 -4
View File
@@ -114,7 +114,7 @@ void storageCheck(bool immediately)
if (storageDirtyMsk & EE_LABELS) {
if (retryLabelsCount < retryLimit) {
TRACE("SD card write labels");
const char * error = modelslist.save();
const char * error = modelCellManager.save();
if (error) {
TRACE("writeLabels error=%s", error);
retryLabelsCount += 1;
@@ -137,7 +137,7 @@ void storageCheck(bool immediately)
TRACE("SD card write model settings");
const char * error = writeModel();
#if defined(STORAGE_MODELSLIST)
modelslist.updateCurrentModelCell();
modelCellManager.updateCurrentModelCell();
#endif
if (error) {
TRACE("writeModel error=%s", error);
@@ -206,7 +206,7 @@ void storageReadAll()
#if defined(STORAGE_MODELSLIST)
// Wipe models list in case
// it's being reloaded after USB connection
modelslist.clear();
modelCellManager.clear();
#endif
// Some radio defaults overriden by config loading:
@@ -245,7 +245,7 @@ void storageReadAll()
#if defined(STORAGE_MODELSLIST)
// and reload the list
modelslist.load();
modelCellManager.load();
// Current model filename is empty...
// Let's fix it!
+3 -1
View File
@@ -410,7 +410,9 @@ void getModelNumberStr(uint8_t idx, char* model_idx)
const char * writeModel()
{
#if defined(STORAGE_MODELSLIST)
return writeModelYaml(g_eeGeneral.currModelFilename);
auto rv = writeModelYaml(g_eeGeneral.currModelFilename);
modelCellManager.getCurrentModel()->updateFinfoHash();
return rv;
#else
char fname[MODELIDX_STRLEN + sizeof(YAML_EXT)];
getModelNumberStr(g_eeGeneral.currModel, fname);
+24 -57
View File
@@ -23,11 +23,9 @@
#include "yaml_parser.h"
#include "debug.h"
#include "strhelpers.h"
#include "storage/modelslist.h"
#include <cstring>
#if defined(_WIN32) || defined(_WIN64)
#define strcasecmp _stricmp
#define strncasecmp _strnicmp
@@ -39,23 +37,21 @@
#define TRACE_LABELS_YAML(...)
#endif
using std::list;
struct labelslist_iter
{
enum SectionType {
SEC_Unknown=0,
SEC_Labels=1,
SEC_Models=2,
SEC_Sort=3,
};
enum SectionType {
SEC_Unknown=0,
SEC_Labels=1,
SEC_Models=2,
SEC_Sort=3,
};
ModelCell *curmodel;
bool modeldatavalid; // Used to determine if reading yaml values is necessary
uint8_t level;
uint8_t section;
char current_attr[LABELS_LENGTH+1]; // set after find_node()
char current_label[LABELS_LENGTH+1]; // set after find_node()
ModelCell *curmodel;
bool modeldatavalid; // Used to determine if reading yaml values is necessary
uint8_t level;
uint8_t section;
char current_attr[LABELS_LENGTH+1]; // set after find_node()
char current_label[LABELS_LENGTH+1]; // set after find_node()
};
static labelslist_iter __labelslist_iter_inst;
@@ -132,28 +128,11 @@ static bool find_node(void* ctx, const char* buf, uint8_t len)
// Model List
if(mi->level == 1 && mi->section == labelslist_iter::SEC_Models) {
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", mi->current_attr);
if(filehash.celladded) {
TRACE_LABELS_YAML(" Duplicate found labels.yml model cell %s already added", mi->current_attr);
break;
}
ModelCell *model = new ModelCell(mi->current_attr);
strcpy(model->modelFinfoHash, filehash.hash);
modelslist.push_back(model);
filehash.celladded = true;
if(filehash.curmodel == true)
modelslist.setCurrentModel(model);
mi->curmodel = model;
mi->modeldatavalid = false;
mi->curmodel->_isDirty = true;
found = true;
break;
}
}
if(!found) {
auto model = modelCellManager.getModel(mi->current_attr);
if (model) {
mi->curmodel = model;
mi->modeldatavalid = false;
} else {
mi->curmodel = NULL;
TRACE_LABELS_YAML("File does not exist in /MODELS");
}
@@ -162,9 +141,8 @@ static bool find_node(void* ctx, const char* buf, uint8_t len)
// Labels List
if(mi->level == 1 && mi->section == labelslist_iter::SEC_Labels) {
TRACE_LABELS_YAML("Label Found -- %s", mi->current_attr);
modelslabels.addLabel(mi->current_attr);
strncpy(mi->current_label,mi->current_attr, LABELS_LENGTH);
mi->current_label[LABELS_LENGTH] = '\0';
modelCellManager.addLabel(mi->current_attr);
strAppend(mi->current_label, mi->current_attr, LABELS_LENGTH);
}
return true;
@@ -194,14 +172,10 @@ static void set_attr(void* ctx, const char* buf, uint16_t len)
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;
}
}
// Last Opened
// Hack: Always load this as the hash value is (currently) not in synch with the saved model file
// TODO: Ensure stored hash is up to date.
if (!strcasecmp(mi->current_attr, "lastopen")) {
mi->curmodel->lastOpened = (gtime_t)strtol(value, NULL, 0);
TRACE_LABELS_YAML(" Last Opened %lu", value);
@@ -223,11 +197,7 @@ static void set_attr(void* ctx, const char* buf, uint16_t len)
// 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());
}
modelCellManager.addLabelsToModel(value, mi->curmodel);
// RF Module Data
} else {
@@ -257,18 +227,15 @@ static void set_attr(void* ctx, const char* buf, uint16_t len)
// Label Section
} else if(mi->level == 2 && mi->section == labelslist_iter::SEC_Labels) {
if(!strcasecmp(mi->current_attr, "icon")) {
TRACE_LABELS_YAML(" Label Icon - %s", value);
// TODO - Check icon exists, or ignore it.
} else if(!strcasecmp(mi->current_attr, "selected")) {
if(!strcasecmp(mi->current_attr, "selected")) {
TRACE("FOUND %s Label is selected", mi->current_label);
modelslabels.addFilteredLabel(mi->current_label);
modelCellManager.addFilteredLabel(mi->current_label);
}
// Sort Order
} else if (mi->level == 0 && mi->section == labelslist_iter::SEC_Sort) {
TRACE_LABELS_YAML(" Sort Order Found -- %s", value);
modelslabels.setSortOrder((ModelsSortBy)strtol(value,NULL,10));
modelCellManager.setSortOrder((ModelsSortBy)strtol(value,NULL,10));
}
}
+7 -10
View File
@@ -137,7 +137,7 @@ TEST(PartialModel, HeaderParsesIdenticallyToModelData)
// Behavioural contract for ModelHeader::labels: it holds the CSV-joined
// list of *every* label attached to a model (declared as
// char[LABELS_LENGTH], currently 100 bytes) -- as distinct from the length
// char[LABELS_LENGTH+1], currently 100 bytes) -- as distinct from the length
// of one individual label name (LABEL_LENGTH, 16 bytes). A model with
// several labels attached should keep all of them when this field gets
// (re)written, up to the field's own declared capacity, not just as much
@@ -156,16 +156,16 @@ TEST(PartialModel, LabelsFieldRetainsFullCsvUpToItsOwnCapacity)
ASSERT_GT(strlen(csv), (size_t)LABEL_LENGTH);
ASSERT_LT(strlen(csv), (size_t)LABELS_LENGTH);
strAppend(partial.header.labels, csv, LABELS_LENGTH - 1);
strAppend(partial.header.labels, csv, LABELS_LENGTH);
EXPECT_STREQ(csv, partial.header.labels)
<< "header.labels should retain the full label list up to "
"LABELS_LENGTH-1 characters, not just a single label name's "
"LABELS_LENGTH characters, not just a single label name's "
"worth.";
}
#endif // defined(STORAGE_MODELSLIST)
// Real FatFS round-trip tests for ModelMap::writeModelLabels() itself -
// Real FatFS round-trip tests for ModelCell::writeModelLabels() itself -
// the raw file-surgery function these PartialModel tests don't exercise.
#if defined(SIMU) && defined(STORAGE_MODELSLIST)
@@ -198,8 +198,6 @@ static size_t modelFileSize(const char* modelFilename)
// model file.
TEST(ModelsList, WriteModelLabelsPreservesBodySize)
{
ModelMap map;
const char* fileA = "wml_test_a.yml";
const char* fileB = "wml_test_b.yml";
@@ -219,8 +217,8 @@ TEST(ModelsList, WriteModelLabelsPreservesBodySize)
ModelCell cellA(fileA);
ModelCell cellB(fileB);
EXPECT_TRUE(map.writeModelLabels(&cellA, "NewLabel"));
EXPECT_TRUE(map.writeModelLabels(&cellB, "NewLabel"));
EXPECT_TRUE(cellA.writeModelLabels("NewLabel"));
EXPECT_TRUE(cellB.writeModelLabels("NewLabel"));
size_t sizeA = modelFileSize(fileA);
size_t sizeB = modelFileSize(fileB);
@@ -235,13 +233,12 @@ TEST(ModelsList, WriteModelLabelsPreservesBodySize)
TEST(ModelsList, WriteModelLabelsFailsCleanlyWithoutHeader)
{
ModelMap map;
const char* file = "wml_test_noheader.yml";
writeRawModelFile(file, "notheader:\n foo: bar\n");
ModelCell cell(file);
EXPECT_FALSE(map.writeModelLabels(&cell, "NewLabel"));
EXPECT_FALSE(cell.writeModelLabels("NewLabel"));
std::filesystem::remove(simuFatfsGetRealPath(std::string(MODELS_PATH) + "/" + file));
}
+18 -20
View File
@@ -30,7 +30,7 @@
//
// Label management (renaming/adding/removing labels) routinely touches
// models other than the currently loaded one. These tests exercise that
// label-management path through its public ModelMap API and assert the
// label-management path through its public modelCellManager API and assert the
// currently loaded model's screen/topbar data is left alone, regardless of
// what label edits are made to other models on disk.
@@ -48,7 +48,7 @@
namespace fs = std::filesystem;
class ModelMapFsTest : public ::testing::Test
class ModelLabelsFsTest : public ::testing::Test
{
protected:
fs::path scratchDir;
@@ -56,7 +56,7 @@ class ModelMapFsTest : public ::testing::Test
void SetUp() override
{
scratchDir = fs::temp_directory_path() /
fs::path("edgetx-gtest-modelslist-labels");
fs::path("edgetx-gtest-modelCellManager-labels");
std::error_code ec;
fs::remove_all(scratchDir, ec);
fs::create_directories(scratchDir / "MODELS", ec);
@@ -64,15 +64,13 @@ class ModelMapFsTest : public ::testing::Test
simuFatfsSetPaths(scratchDir.string().c_str(), nullptr);
modelslist.clear();
modelslabels.clear();
modelCellManager.clear();
memclear(&g_model, sizeof(g_model));
}
void TearDown() override
{
modelslist.clear();
modelslabels.clear();
modelCellManager.clear();
simuFatfsSetPaths(TESTS_PATH, nullptr);
@@ -100,7 +98,7 @@ class ModelMapFsTest : public ::testing::Test
// a "header:" section at all. Giving it a name keeps the fixture
// realistic and avoids that unrelated edge case.
copyToUnTerminated(model.header.name, filename);
strAppend(model.header.labels, labels, LABELS_LENGTH - 1);
strAppend(model.header.labels, labels, LABELS_LENGTH);
model.getScreenData(0)->LayoutId = screenLayoutId;
model.getTopbarData()->zones[0].widgetName = widgetName;
@@ -112,25 +110,25 @@ class ModelMapFsTest : public ::testing::Test
}
};
TEST_F(ModelMapFsTest, RenamingLabelOnOtherModelLeavesActiveScreenDataUntouched)
TEST_F(ModelLabelsFsTest, RenamingLabelOnOtherModelLeavesActiveScreenDataUntouched)
{
// Seed a second, non-active model on disk with a label and screen/topbar
// data distinct from the active model's.
writeFixtureModel("model0002.yml", "Foo", "OtherLayout", "OtherWidget");
ModelCell* other = modelslist.addModel("model0002.yml", false);
ModelCell* other = modelCellManager.addModel("model0002.yml", false);
ASSERT_NE(other, nullptr);
ASSERT_FALSE(modelslabels.addLabelToModel("Foo", other, false));
ASSERT_FALSE(modelCellManager.addLabelToModel("Foo", other, false));
// Now "load" the active model: register its cell and mark it current, and
// set its screen/topbar data to its own values.
ModelCell* active = modelslist.addModel("model0001.yml", false);
ModelCell* active = modelCellManager.addModel("model0001.yml", false);
ASSERT_NE(active, nullptr);
modelslist.setCurrentModel(active);
modelCellManager.setCurrentModel(active);
g_model.getScreenData(0)->LayoutId = "ActiveLayout";
g_model.getTopbarData()->zones[0].widgetName = "ActiveWidget";
// Rename a label that only exists on the other, non-active model.
modelslabels.renameLabel("Foo", "Bar");
modelCellManager.renameLabel("Foo", "Bar");
// The active model's screen/topbar data must be unaffected by editing an
// unrelated model's labels.
@@ -147,22 +145,22 @@ TEST_F(ModelMapFsTest, RenamingLabelOnOtherModelLeavesActiveScreenDataUntouched)
EXPECT_STREQ(partial.header.labels, "Bar");
}
TEST_F(ModelMapFsTest,
TEST_F(ModelLabelsFsTest,
AddingLabelToOtherModelWithFileUpdateLeavesActiveScreenDataUntouched)
{
writeFixtureModel("model0002.yml", "", "OtherLayout", "OtherWidget");
ModelCell* other = modelslist.addModel("model0002.yml", false);
ModelCell* other = modelCellManager.addModel("model0002.yml", false);
ASSERT_NE(other, nullptr);
ModelCell* active = modelslist.addModel("model0001.yml", false);
ModelCell* active = modelCellManager.addModel("model0001.yml", false);
ASSERT_NE(active, nullptr);
modelslist.setCurrentModel(active);
modelCellManager.setCurrentModel(active);
g_model.getScreenData(0)->LayoutId = "ActiveLayout";
g_model.getTopbarData()->zones[0].widgetName = "ActiveWidget";
// update=true drives ModelMap::updateModelFile(), the second call site
// update=true drives ModelCell::updateModelFile(), the second call site
// that reads/writes a non-active model's file on disk.
EXPECT_FALSE(modelslabels.addLabelToModel("Baz", other, true));
EXPECT_FALSE(modelCellManager.addLabelToModel("Baz", other, true));
EXPECT_STREQ(g_model.getScreenData(0)->LayoutId.c_str(), "ActiveLayout");
EXPECT_STREQ(g_model.getTopbarData()->zones[0].widgetName.c_str(),
File diff suppressed because it is too large Load Diff