diff --git a/companion/src/CMakeLists.txt b/companion/src/CMakeLists.txt index fa3439263..7f5e2ef3b 100644 --- a/companion/src/CMakeLists.txt +++ b/companion/src/CMakeLists.txt @@ -211,6 +211,7 @@ set(companion_SRCS helpers_html.cpp mdichild.cpp modelslist.cpp + labels.cpp apppreferencesdialog.cpp fwpreferencesdialog.cpp burnconfigdialog.cpp @@ -269,6 +270,7 @@ set(companion_MOC_HDRS modelprinter.h multimodelprinter.h modelslist.h + labels.h styleeditdialog.h helpers_html.h dialogs/filesyncdialog.h diff --git a/companion/src/comparedialog.cpp b/companion/src/comparedialog.cpp index 448985db6..ef224251e 100644 --- a/companion/src/comparedialog.cpp +++ b/companion/src/comparedialog.cpp @@ -75,7 +75,7 @@ bool CompareDialog::handleMimeData(const QMimeData * mimeData) { QVector mList; GeneralSettings gs; - if (!TreeModel::decodeMimeData(mimeData, &mList, &gs) || mList.isEmpty()) + if (!ModelsListModel::decodeMimeData(mimeData, &mList, &gs) || mList.isEmpty()) return false; for (int i=0; i < mList.size(); ++i) { GMData data; diff --git a/companion/src/firmwares/edgetx/edgetxinterface.cpp b/companion/src/firmwares/edgetx/edgetxinterface.cpp index 7a768d5c6..18169054d 100644 --- a/companion/src/firmwares/edgetx/edgetxinterface.cpp +++ b/companion/src/firmwares/edgetx/edgetxinterface.cpp @@ -89,39 +89,23 @@ static void writeYamlToByteArray(const YAML::Node& node, QByteArray& data, bool qDebug() << data.toStdString().c_str(); } -bool loadModelsListFromYaml(std::vector& categories, +bool loadLabelsListFromYaml(QStringList& labels, EtxModelfiles& modelFiles, const QByteArray& data) { + labels.clear(); if (data.size() == 0) return true; - YAML::Node node = loadYamlFromByteArray(data); - if (!node.IsSequence()) return false; - - int modelIdx = 0; - for (const auto& cat : node) { - if (!cat.IsMap()) continue; - - for (const auto& cat_map : cat) { - categories.push_back(cat_map.first.Scalar().c_str()); - - const auto& models = cat_map.second; - if (!models.IsSequence()) continue; - - for (const auto& model : models) { - std::string filename, name; - model["filename"] >> filename; - model["name"] >> name; - modelFiles.push_back( - {filename, name, (int)categories.size() - 1, modelIdx++}); - } - } - } + if (!node.IsMap()) return false; // Root Map (Labels, Models) + for (const auto& lbl : node["Labels"]) { + labels.append(QString::fromStdString(lbl.first.as())); + } return true; } + bool loadModelFromYaml(ModelData& model, const QByteArray& data) { YAML::Node node = loadYamlFromByteArray(data); @@ -168,46 +152,12 @@ bool loadRadioSettingsFromYaml(GeneralSettings& settings, const QByteArray& data return true; } -// TODO: -// 'modelFiles' should be ordered by Category index to avoid -// turning the sequence into a map. -// -bool writeModelsListToYaml(const std::vector& categories, - const EtxModelfiles& modelFiles, - QByteArray& data) +bool writeLabelsListToYaml(const RadioData &radioData, QByteArray& data) { YAML::Node node; - std::vector cats = categories; - std::vector files = { modelFiles.begin(), modelFiles.end() }; - - std::stable_sort(files.begin(), files.end(), - [](const EtxModelMetadata &a, const EtxModelMetadata &b) { - return a.category < b.category; - }); - - int catIdx = 0; - for (const auto& cat : cats) { - node[catIdx++][cat.name] = YAML::Node(); + foreach(QString label, radioData.labels) { + node["Labels"][label.toStdString()] = YAML::Null; } - - for (const auto& modelFile: modelFiles) { - - YAML::Node cat_attrs; - cat_attrs["filename"] = modelFile.filename; - cat_attrs["name"] = modelFile.name; - - catIdx = modelFile.category; - if (catIdx >= (int)cats.size()) { - catIdx = 0; - if (cats.size() == 0) { - cats.push_back("Models"); - } - } - - const std::string cat_name = cats[catIdx].name; - node[catIdx][cat_name].push_back(cat_attrs); - } - writeYamlToByteArray(node, data); return true; } diff --git a/companion/src/firmwares/edgetx/edgetxinterface.h b/companion/src/firmwares/edgetx/edgetxinterface.h index 55b374ac9..f5d5f03f3 100644 --- a/companion/src/firmwares/edgetx/edgetxinterface.h +++ b/companion/src/firmwares/edgetx/edgetxinterface.h @@ -32,22 +32,19 @@ constexpr unsigned int CPN_CURRENT_SETTINGS_VERSION = { 221 }; struct EtxModelMetadata { std::string filename; std::string name; - int category; int modelIdx; }; typedef std::list EtxModelfiles; -bool loadModelsListFromYaml(std::vector& categories, +bool loadLabelsListFromYaml(QStringList& labels, EtxModelfiles& modelFiles, const QByteArray& data); bool loadModelFromYaml(ModelData& model, const QByteArray& data); bool loadRadioSettingsFromYaml(GeneralSettings& settings, const QByteArray& data); -bool writeModelsListToYaml(const std::vector& categories, - const EtxModelfiles& modelFiles, - QByteArray& data); +bool writeLabelsListToYaml(const RadioData &radioData, QByteArray& data); bool writeModelToYaml(const ModelData& model, QByteArray& data); bool writeRadioSettingsToYaml(const GeneralSettings& settings, QByteArray& data); diff --git a/companion/src/firmwares/edgetx/yaml_modeldata.cpp b/companion/src/firmwares/edgetx/yaml_modeldata.cpp index 718b5f309..7f65516ad 100644 --- a/companion/src/firmwares/edgetx/yaml_modeldata.cpp +++ b/companion/src/firmwares/edgetx/yaml_modeldata.cpp @@ -743,6 +743,7 @@ Node convert::encode(const ModelData& rhs) Node header; header["name"] = rhs.name; header["bitmap"] = rhs.bitmap; + header["labels"] = rhs.labels; for (int i=0; i::decode(const Node& node, ModelData& rhs) if (header.IsMap()) { header["name"] >> rhs.name; header["bitmap"] >> rhs.bitmap; + header["labels"] >> rhs.labels; header["modelId"] >> modelIds; } } diff --git a/companion/src/firmwares/eeprominterface.h b/companion/src/firmwares/eeprominterface.h index 2b27f27d7..e6cc8be01 100644 --- a/companion/src/firmwares/eeprominterface.h +++ b/companion/src/firmwares/eeprominterface.h @@ -153,7 +153,7 @@ enum Capability { HasMixerExpo, HasBatMeterRange, DangerousFunctions, - HasModelCategories, + HasModelLabels, HasSwitchableJack, HasSportConnector, PwrButtonPress, diff --git a/companion/src/firmwares/modeldata.h b/companion/src/firmwares/modeldata.h index 3d63a65e2..92b6f8f3f 100644 --- a/companion/src/firmwares/modeldata.h +++ b/companion/src/firmwares/modeldata.h @@ -111,9 +111,9 @@ class ModelData { char semver[8 + 1]; bool used; - int category; char name[15+1]; char filename[16+1]; + char labels[100]; int modelIndex; // Companion only, temporary index position managed by data model. TimerData timers[CPN_MAX_TIMERS]; diff --git a/companion/src/firmwares/opentx/opentxinterface.cpp b/companion/src/firmwares/opentx/opentxinterface.cpp index f9a5cb695..577325876 100644 --- a/companion/src/firmwares/opentx/opentxinterface.cpp +++ b/companion/src/firmwares/opentx/opentxinterface.cpp @@ -742,7 +742,7 @@ int OpenTxFirmware::getCapability(::Capability capability) return (IS_HORUS_OR_TARANIS(board) ? true : id.contains("battgraph")); case DangerousFunctions: return id.contains("danger") ? 1 : 0; - case HasModelCategories: + case HasModelLabels: return IS_FAMILY_HORUS_OR_T16(board); case HasSwitchableJack: return IS_TARANIS_XLITES(board); diff --git a/companion/src/firmwares/radiodata.cpp b/companion/src/firmwares/radiodata.cpp index 56f68a5c8..a36c43595 100644 --- a/companion/src/firmwares/radiodata.cpp +++ b/companion/src/firmwares/radiodata.cpp @@ -94,13 +94,6 @@ void RadioData::convert(RadioDataConversionState & cstate) models[i].convert(cstate.withModelIndex(i)); } - if (categories.size() == 0) { - categories.push_back(CategoryData(qPrintable(tr("Models")))); - for (unsigned i=0; igetCapability(Models)); } } + +void RadioData::addLabel(QString label) +{ + if(!labels.contains(label)) + labels.append(label); +} + +bool RadioData::deleteLabel(QString label) +{ + bool deleted = false; + // Remove labels in the models + for(auto& model : models) { + QStringList modelLabels = QString(model.labels).split(',',QString::SkipEmptyParts); + if(modelLabels.contains(label)) + deleted = true; + modelLabels.removeAll(label); + strcpy(model.labels, QString(modelLabels.join(',')).toLocal8Bit().data()); + } + + // Remove the label from the global list + labels.removeAll(label); + + // If no labels remain, add a Favorites one + if(!labels.size()) { + addLabel(tr("Favorites")); + } + return deleted; +} + +bool RadioData::deleteLabel(int index) +{ + if(index >= labels.size()) return false; + QString modelLabel = labels.at(index); + return deleteLabel(modelLabel); +} + +bool RadioData::renameLabel(QString from, QString to) +{ + bool success = true; + int lengthdiff = to.size() - from.size(); + + // Check that rename is possible, not too long + for(auto& model : models) { + if((int)strlen(model.labels) + lengthdiff > (int)sizeof(model.labels) - 1) { + success = false; + } + } + 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 + if(ind != -1) { + modelLabels.replace(ind, to); + strcpy(model.labels, QString(modelLabels.join(',')).toLocal8Bit().data()); + } + } + int ind = labels.indexOf(from); + if(ind != -1) { + labels.replace(ind, to); + } + } + return success; +} + +bool RadioData::renameLabel(int index, QString to) +{ + if(index >= labels.size()) return false; + QString from = labels.at(index); + return renameLabel(from, to); +} + +bool RadioData::addLabelToModel(int index, QString label) +{ + if(index >= models.size()) return false; + + char *modelLabelCsv = models[index].labels; + // Make sure it will fit + if(strlen(modelLabelCsv) + label.size() + 1 < sizeof(models[index].labels)-1) { + QStringList modelLabels = QString(modelLabelCsv).split(',',QString::SkipEmptyParts); + if(!modelLabels.contains(label)) { + modelLabels.append(label); + strcpy(models[index].labels, QString(modelLabels.join(',')).toLocal8Bit().data()); + return true; + } + } + return false; +} + +bool RadioData::removeLabelFromModel(int index, QString label) +{ + if(index >= models.size()) return false; + + char *modelLabelCsv = models[index].labels; + QStringList modelLabels = QString(modelLabelCsv).split(',',QString::SkipEmptyParts); + if(modelLabels.contains(label)) { + modelLabels.removeAll(label); + strcpy(models[index].labels, QString(modelLabels.join(',')).toLocal8Bit().data()); + return true; + } + return false; +} diff --git a/companion/src/firmwares/radiodata.h b/companion/src/firmwares/radiodata.h index 7b5caa9b6..987154be7 100644 --- a/companion/src/firmwares/radiodata.h +++ b/companion/src/firmwares/radiodata.h @@ -2,6 +2,7 @@ #include "generalsettings.h" #include "modeldata.h" +#include "labels.h" #include "datahelpers.h" // required for getElementName @@ -9,15 +10,6 @@ class RadioDataConversionState; -class CategoryData { - public: - CategoryData(const char * name) { - memset(this->name, 0, sizeof(CategoryData::name)); - strncpy(this->name, name, sizeof(CategoryData::name)-1); - } - char name[15+1]; -}; - class RadioData { Q_DECLARE_TR_FUNCTIONS(RadioData) @@ -25,11 +17,19 @@ class RadioData { RadioData(); GeneralSettings generalSettings; - std::vector categories; + QStringList labels; std::vector models; void convert(RadioDataConversionState & cstate); + void addLabel(QString label); + bool deleteLabel(QString label); + bool deleteLabel(int index); + bool renameLabel(QString from, QString to); + bool renameLabel(int index, QString to); + bool addLabelToModel(int index, QString label); + bool removeLabelFromModel(int index, QString label); + void setCurrentModel(unsigned int index); void fixModelFilenames(); QString getNextModelFilename(); diff --git a/companion/src/labels.cpp b/companion/src/labels.cpp new file mode 100644 index 000000000..4c9bd6427 --- /dev/null +++ b/companion/src/labels.cpp @@ -0,0 +1,276 @@ +#include "labels.h" + +LabelsModel::LabelsModel(QItemSelectionModel *selectionModel, + RadioData *radioData, QObject *parent) : + QAbstractItemModel(parent), + modelsSelection(selectionModel), + radioData(radioData), + selectedModel(-1) +{ + connect(modelsSelection, &QItemSelectionModel::currentChanged, + this, &LabelsModel::modelsSelectionChanged ); + + buildLabelsList(); +} + +LabelsModel::~LabelsModel() +{ + +} + +Qt::ItemFlags LabelsModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags flags = Qt::ItemIsEnabled; + + if(index.column() == 0) { + flags |= Qt::ItemIsUserCheckable; + flags |= Qt::ItemIsSelectable; + flags |= Qt::ItemIsDropEnabled; + flags |= Qt::ItemIsEditable; + if(index.isValid()) + flags |= Qt::ItemIsDragEnabled; + } + return flags; +} + +bool LabelsModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if(!index.isValid() || index.column() != 0 || + index.row() >= radioData->labels.size() || + selectedModel == -1) + return false; + + if(role == Qt::CheckStateRole) { + if(value==Qt::Unchecked) { + if(radioData->removeLabelFromModel(selectedModel, radioData->labels.at(index.row()))) + emit modelChanged(selectedModel); + } else { + if(radioData->addLabelToModel(selectedModel, radioData->labels.at(index.row()))) + emit modelChanged(selectedModel); + } + emit dataChanged(this->index(index.row(), 0), + this->index(index.row(), 0)); + return true; + } else if(role == Qt::EditRole) { + // Don't allow comma, replace with _ + if(radioData->renameLabel(index.row(),value.toString().replace(',','_'))) { + labels[index.row()].label = value.toString().replace(',','_'); + emit dataChanged(this->index(index.row(), 0), + this->index(index.row(), 0)); + emit modelChanged(selectedModel); + } + return true; + } + return false; +} + +QVariant LabelsModel::data(const QModelIndex &index, int role) const +{ + // if(index.row() >= radioData->labels.size() || !index.isValid() ) + if(index.row() >= labels.size() || !index.isValid() ) + return QVariant(); + + QString label = radioData->labels.at(index.row()); + + if(role == Qt::DisplayRole || role == Qt::EditRole) { + if(index.column() == 0) { + return labels.at(index.row()).label; + //return label; + } + } else if (role == Qt::CheckStateRole) { + if(index.column() == 0 && selectedModel >= 0 && + selectedModel < (int)radioData->models.size()) { + return QString(radioData->models.at(selectedModel).labels).contains(label)?Qt::Checked:Qt::Unchecked; + } + } + return QVariant(); +} + +QModelIndex LabelsModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent) + + if(row < labels.size()) + return createIndex(row,0); + + return QModelIndex(); +} + +QModelIndex LabelsModel::parent(const QModelIndex &index) const +{ + return QModelIndex(); +} + +int LabelsModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + //return radioData->labels.size(); + return labels.size(); +} + +int LabelsModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 1; +} + +QVariant LabelsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role != Qt::DisplayRole || + orientation != Qt::Horizontal || + section != 0) + return QVariant(); + return tr("Labels"); +} + +Qt::DropActions LabelsModel::supportedDropActions() const +{ + return Qt::MoveAction; +} + +QStringList LabelsModel::mimeTypes() const +{ + QStringList types; + types << "application/edglbl.text.list"; + return types; +} + +QMimeData *LabelsModel::mimeData(const QModelIndexList &indexes) const +{ + QMimeData *mimeData = new QMimeData; + QByteArray encodedData; + + QDataStream stream(&encodedData, QIODevice::WriteOnly); + + for (const QModelIndex &index : indexes) { + if (index.isValid()) { + QString text = data(index, Qt::DisplayRole).toString(); + stream << text; + } + } + + mimeData->setData("application/edglbl.text.list", encodedData); + return mimeData; +} + +bool LabelsModel::canDropMimeData(const QMimeData *data, Qt::DropAction action, + int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(action); + Q_UNUSED(row); + Q_UNUSED(parent); + + if (!data->hasFormat("application/edglbl.text.list")) + return false; + + if (column > 0) + return false; + + return true; +} + +bool LabelsModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + // TODO: This needs to be completed + if (!canDropMimeData(data, action, row, column, parent)) + return false; + + if(row < 0) return false; + + if (action == Qt::IgnoreAction) + return true; + else if (action != Qt::MoveAction) + return false; + + QByteArray encodedData = data->data("application/edglbl.text.list"); + QDataStream stream(&encodedData, QIODevice::ReadOnly); + QStringList newItems; + int rows = 0; + + while (!stream.atEnd()) { + QString text; + stream >> text; + newItems << text; + ++rows; + } + + /*insertRows(row, rows, QModelIndex()); + for (const QString &text : qAsConst(newItems)) + { + QModelIndex idx = index(row, 0, QModelIndex()); + row++; + }*/ + + return true; +} + +bool LabelsModel::insertRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + + // Find a New Number + for (int i = 0; i < count; i++) { + int newno=0; + QString newstr; + do { + newstr = QString(tr("New%1").arg(newno)); + if(newno == 0) + newstr = QString(tr("New")); + newno++; + } while(radioData->labels.contains(newstr)); + // Add it to radioData + radioData->labels.insert(row+i, newstr); + } + buildLabelsList(); + return true; +} + +bool LabelsModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (parent.isValid()) + return false; + bool deleted=false; + beginRemoveRows(parent, row, row + count - 1); + for (int i = 0; i != count; ++i) + if(radioData->deleteLabel(row+i)) { + deleted = true; + } + endRemoveRows(); + // Refresh all + if(deleted) + emit modelChanged(-1); + buildLabelsList(); +return true; +} + +void LabelsModel::buildLabelsList() +{ + labels.clear(); + + int i=0; + foreach(QString lbl, radioData->labels) { + LabelItem itm; + itm.label = lbl; + itm.radioLabelIndex = i++; + labels.append(itm); + } + if (i) { + emit dataChanged(index(0,0), + index(i-1,0)); + } +} + +void LabelsModel::modelsSelectionChanged() +{ + QModelIndex index = modelsSelection->currentIndex(); + if (index.isValid()) { + int mi = modelsSelection->currentIndex().row(); + if(mi < (int)radioData->models.size()) { + selectedModel = mi; + buildLabelsList(); + } + } + else + selectedModel = -1; +} diff --git a/companion/src/labels.h b/companion/src/labels.h new file mode 100644 index 000000000..811a6bc5a --- /dev/null +++ b/companion/src/labels.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include + +#include "radiodata.h" + +class RadioData; + +typedef struct { + QString label; + QModelIndex index; + int radioLabelIndex; +} LabelItem; + +class LabelsModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + LabelsModel(QItemSelectionModel *selectionModel, RadioData *radioData, QObject *parent = nullptr); + ~LabelsModel(); + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + Qt::DropActions supportedDropActions() const override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + bool canDropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) const override; + bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + bool insertRows(int row, int count, const QModelIndex &parent) override; + bool removeRows(int row, int count, const QModelIndex &parent) override; + +private slots: + void buildLabelsList(); + void modelsSelectionChanged(); + +signals: + void modelChanged(int index); + +private: + QItemSelectionModel *modelsSelection; + RadioData *radioData; + int selectedModel; + QList modelIndices; + QList labels; +}; diff --git a/companion/src/mdichild.cpp b/companion/src/mdichild.cpp index 7f9331bf4..46f9871db 100644 --- a/companion/src/mdichild.cpp +++ b/companion/src/mdichild.cpp @@ -33,6 +33,7 @@ #include "radiointerface.h" #include "radiodataconversionstate.h" #include "filtereditemmodels.h" +#include "labels.h" #include #include @@ -41,14 +42,16 @@ MdiChild::MdiChild(QWidget * parent, QWidget * parentWin, Qt::WindowFlags f): QWidget(parent, f), ui(new Ui::MdiChild), modelsListModel(NULL), + labelsListModel(NULL), parentWindow(parentWin), radioToolbar(NULL), - categoriesToolbar(NULL), modelsToolbar(NULL), + labelsToolbar(NULL), + lblLabels(NULL), + firmware(getCurrentFirmware()), lastSelectedModel(-1), isUntitled(true), - showCatToolbar(true), forceCloseFlag(false), stateDataVersion(1) { @@ -73,6 +76,7 @@ MdiChild::MdiChild(QWidget * parent, QWidget * parentWin, Qt::WindowFlags f): ui->modelsList->setDragDropMode(QAbstractItemView::DragDrop); ui->modelsList->setStyle(new ItemViewProxyStyle(ui->modelsList->style())); ui->modelsList->setStyleSheet("QTreeView::item {margin: 2px 0;}"); // a little more space for our drop indicators + ui->lstLabels->setContextMenuPolicy(Qt::CustomContextMenu); retranslateUi(); @@ -82,6 +86,7 @@ MdiChild::MdiChild(QWidget * parent, QWidget * parentWin, Qt::WindowFlags f): connect(ui->modelsList, &QTreeView::pressed, this, &MdiChild::onItemSelected); connect(ui->modelsList->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MdiChild::onCurrentItemChanged); connect(QGuiApplication::clipboard(), &QClipboard::dataChanged, this, &MdiChild::updateNavigation); + connect(ui->lstLabels, &QTreeView::customContextMenuRequested, this, &MdiChild::showLabelsContextMenu); if (!(isMaximized() || isMinimized())) { QByteArray geo = g.mdiWinGeo(); @@ -103,8 +108,7 @@ MdiChild::MdiChild(QWidget * parent, QWidget * parentWin, Qt::WindowFlags f): stream >> ver; if (ver <= stateDataVersion) { bool visMdl, visGen; - stream >> showCatToolbar >> visMdl >> visGen; - categoriesToolbar->setVisible(showCatToolbar); + stream >> showLabelToolbar >> visMdl >> visGen; modelsToolbar->setVisible(visMdl); radioToolbar->setVisible(visGen); } @@ -141,7 +145,7 @@ void MdiChild::closeEvent(QCloseEvent *event) QByteArray state; QDataStream stream(&state, QIODevice::WriteOnly); stream << stateDataVersion - << (firmware->getCapability(Capability::HasModelCategories) ? categoriesToolbar->isVisible() : showCatToolbar) + << false << modelsToolbar->isVisible() << radioToolbar->isVisible(); g.mdiWinState(state); @@ -212,16 +216,13 @@ void MdiChild::setupNavigation() addAct(ACT_ITM_EDT, "edit.png", SLOT(edit()), Qt::Key_Enter); addAct(ACT_ITM_DEL, "clear.png", SLOT(confirmDelete()), QKeySequence::Delete); - addAct(ACT_CAT_ADD, "add.png", SLOT(categoryAdd()), tr("Alt+C")); - //addAct(ACT_CAT_EDT, "edit.png", SLOT(edit()), Qt::Key_Enter); - //addAct(ACT_CAT_DEL, "clear.png", SLOT(confirmDelete()), QKeySequence::Delete); - action[ACT_CAT_SEP] = new QAction(this); - action[ACT_CAT_SEP]->setSeparator(true); - addAct(ACT_MDL_ADD, "add.png", SLOT(modelAdd()), tr("Alt+A")); addAct(ACT_MDL_RTR, "open.png", SLOT(loadBackup()), tr("Alt+R")); addAct(ACT_MDL_WIZ, "wizard.png", SLOT(wizardEdit()), tr("Alt+W")); + addAct(ACT_LBL_ADD, "add.png", SLOT(labelAdd()), tr("Alt-L")); + addAct(ACT_LBL_DEL, "clear.png", SLOT(labelDelete()), tr("Alt-L")); + addAct(ACT_MDL_DFT, "currentmodel.png", SLOT(setDefault()), tr("Alt+U")); addAct(ACT_MDL_PRT, "print.png", SLOT(print()), QKeySequence::Print); addAct(ACT_MDL_SIM, "simulate.png", SLOT(modelSimulate()), tr("Alt+S")); @@ -243,16 +244,24 @@ void MdiChild::setupNavigation() QSize tbIcnSz(16, 16); QString tbCss = "QToolBar {border: 1px solid palette(midlight);}"; - if (categoriesToolbar) - categoriesToolbar->deleteLater(); - categoriesToolbar = new QToolBar(this); - categoriesToolbar->setObjectName("TB_CATEGORIES"); - categoriesToolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - categoriesToolbar->setFloatable(false); - categoriesToolbar->setIconSize(tbIcnSz); - categoriesToolbar->setStyleSheet(tbCss); - categoriesToolbar->addAction(getAction(ACT_CAT_ADD)); - ui->topToolbarLayout->addWidget(categoriesToolbar); + // Add labels Label to bottom layout + if(lblLabels) + lblLabels->deleteLater(); + lblLabels = new QLabel(tr("Labels")); + lblLabels->setStyleSheet("font-weight: bold"); + ui->bottomLayout->addWidget(lblLabels); + + if(labelsToolbar) + labelsToolbar->deleteLater(); + labelsToolbar = new QToolBar(this); + labelsToolbar->setObjectName("TB_LABELS"); + labelsToolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + labelsToolbar->setFloatable(false); + labelsToolbar->setIconSize(tbIcnSz); + labelsToolbar->setStyleSheet(tbCss); + labelsToolbar->addAction(getAction(ACT_LBL_ADD)); + labelsToolbar->addAction(getAction(ACT_LBL_DEL)); + ui->bottomLayout->addWidget(labelsToolbar); if (radioToolbar) radioToolbar->deleteLater(); @@ -282,7 +291,7 @@ void MdiChild::setupNavigation() modelsToolbar->setFloatable(false); modelsToolbar->setIconSize(tbIcnSz); modelsToolbar->setStyleSheet(tbCss); - modelsToolbar->addActions(getEditActions(false)); + modelsToolbar->addActions(getEditActions()); modelsToolbar->addSeparator(); modelsToolbar->addActions(getModelActions()); if ((btn = qobject_cast(modelsToolbar->widgetForAction(action[ACT_MDL_ADD])))) { @@ -293,7 +302,6 @@ void MdiChild::setupNavigation() } ui->botToolbarLayout->addWidget(modelsToolbar); - connect(categoriesToolbar, &QToolBar::visibilityChanged, this, &MdiChild::adjustToolbarLayout); connect(radioToolbar, &QToolBar::visibilityChanged, this, &MdiChild::adjustToolbarLayout); connect(modelsToolbar, &QToolBar::visibilityChanged, this, &MdiChild::adjustToolbarLayout); } @@ -301,73 +309,32 @@ void MdiChild::setupNavigation() void MdiChild::updateNavigation() { const int modelsSelected = countSelectedModels(); - const int catsSelected = countSelectedCats(); const bool singleModelSelected = (modelsSelected == 1); const bool hasModelSlotSelcted = (getCurrentModel() > -1); - const bool hasCats = firmware->getCapability(Capability::HasModelCategories); - const bool hasCatSelected = hasCats && modelsListModel->isCategoryType(getCurrentIndex()); + const bool hasLabels = firmware->getCapability(Capability::HasModelLabels); const int numOnClipbrd = modelsListModel->countModelsInMimeData(QApplication::clipboard()->mimeData()); const QString modelsRemvTxt = tr("%n Model(s)", "As in \"Copy 3 Models\" or \"Cut 1 Model\" or \"Delete 3 Models\" action).", modelsSelected); const QString modelsAddTxt = tr("%n Model(s)", "As in \"Paste 3 Models\" or \"Insert 1 Model.\"", numOnClipbrd); - const QString catsRemvTxt = tr("%n Category(ies)", "As in \"Delete 3 Categories\" or \"Delete 1 Category.\"", catsSelected); static const QString noSelection = tr("Nothing selected"); static const QString sp = " "; static const QString ns; - categoriesToolbar->setVisible(hasCats && showCatToolbar); - + labelsToolbar->setVisible(hasLabels); + ui->lstLabels->setVisible(hasLabels); + lblLabels->setVisible(hasLabels); action[ACT_GEN_PST]->setEnabled(hasClipboardData(1)); - action[ACT_ITM_EDT]->setEnabled(singleModelSelected || catsSelected == 1); - action[ACT_ITM_EDT]->setText((hasCatSelected ? tr("Rename Category") : modelsSelected ? tr("Edit Model") : noSelection)); - action[ACT_ITM_DEL]->setEnabled(modelsSelected || catsSelected); - action[ACT_ITM_DEL]->setText((action[ACT_ITM_DEL]->isEnabled() ? tr("Delete") % " " % (hasCatSelected ? catsRemvTxt : modelsRemvTxt) : noSelection)); - - action[ACT_CAT_ADD]->setVisible(hasCats); - action[ACT_CAT_SEP]->setVisible(hasCats); -// action[ACT_CAT_EDT]->setVisible(hasCats); -// action[ACT_CAT_EDT]->setEnabled(catsSelected == 1); -// action[ACT_CAT_DEL]->setVisible(hasCats); -// action[ACT_CAT_DEL]->setEnabled(catsSelected); -// action[ACT_CAT_DEL]->setText((hasCatSelected ? tr("Delete") % " " % catsRemTxt : noSelection)); - action[ACT_MDL_CUT]->setEnabled(modelsSelected); action[ACT_MDL_CUT]->setText(tr("Cut") % (modelsSelected ? sp % modelsRemvTxt : ns)); action[ACT_MDL_CPY]->setEnabled(modelsSelected); action[ACT_MDL_CPY]->setText(tr("Copy") % (modelsSelected ? sp % modelsRemvTxt : ns)); action[ACT_MDL_PST]->setEnabled(numOnClipbrd); action[ACT_MDL_PST]->setText(tr("Paste") % (numOnClipbrd ? sp % modelsAddTxt : ns)); - action[ACT_MDL_INS]->setEnabled(numOnClipbrd && (hasModelSlotSelcted || catsSelected)); + action[ACT_MDL_INS]->setEnabled(numOnClipbrd && hasModelSlotSelcted); action[ACT_MDL_INS]->setText(tr("Insert") % QString(action[ACT_MDL_INS]->isEnabled() ? sp % modelsAddTxt : ns)); action[ACT_MDL_SAV]->setEnabled(modelsSelected); action[ACT_MDL_SAV]->setText(tr("Save") % (modelsSelected ? sp % modelsRemvTxt : ns)); - - if (hasCats && action[ACT_MDL_MOV]->menu()) { - action[ACT_MDL_MOV]->setVisible(true); - QModelIndex modelIndex = getCurrentIndex(); - QMenu * catsMenu = action[ACT_MDL_MOV]->menu(); - catsMenu->clear(); - if (modelsSelected && modelsListModel && radioData.categories.size() > 1) { - action[ACT_MDL_MOV]->setEnabled(true); - for (unsigned i=0; i < radioData.categories.size(); ++i) { - QAction * act = catsMenu->addAction(QString(radioData.categories[i].name), this, SLOT(onModelMoveToCategory())); - act->setProperty("categoryId", i); - if ((int)i < modelsListModel->getCategoryIndex(modelIndex)) - act->setIcon(CompanionIcon("moveup.png")); - else if ((int)i > modelsListModel->getCategoryIndex(modelIndex)) - act->setIcon(CompanionIcon("movedown.png")); - else - act->setEnabled(false); - } - } - else { - action[ACT_MDL_MOV]->setDisabled(true); - } - } - else { - action[ACT_MDL_MOV]->setVisible(false); - } - + action[ACT_MDL_MOV]->setVisible(false); action[ACT_MDL_DUP]->setEnabled(singleModelSelected); action[ACT_MDL_RTR]->setEnabled(singleModelSelected); action[ACT_MDL_WIZ]->setEnabled(singleModelSelected); @@ -383,9 +350,11 @@ void MdiChild::retranslateUi() action[ACT_GEN_PST]->setText(tr("Paste Radio Settings")); action[ACT_GEN_SIM]->setText(tr("Simulate Radio")); - action[ACT_CAT_ADD]->setText(tr("Add Category")); - action[ACT_CAT_ADD]->setIconText(tr("Category")); - //action[ACT_CAT_EDT]->setText(tr("Rename Category")); + action[ACT_ITM_EDT]->setText(tr("Edit Model")); + action[ACT_ITM_DEL]->setText(tr("Delete")); + + action[ACT_LBL_ADD]->setText(tr("Add Label")); + action[ACT_LBL_DEL]->setText(tr("Delete Label")); action[ACT_MDL_ADD]->setText(tr("Add Model")); action[ACT_MDL_ADD]->setIconText(tr("Model")); @@ -398,9 +367,6 @@ void MdiChild::retranslateUi() action[ACT_MDL_SIM]->setText(tr("Simulate Model")); action[ACT_MDL_DUP]->setText(tr("Duplicate Model")); - action[ACT_MDL_MOV]->setText(tr("Move to Category")); - - categoriesToolbar->setWindowTitle(tr("Show Category Actions Toolbar")); radioToolbar->setWindowTitle(tr("Show Radio Actions Toolbar")); modelsToolbar->setWindowTitle(tr("Show Model Actions Toolbar")); } @@ -415,13 +381,9 @@ QList MdiChild::getGeneralActions() return actGrp; } -QList MdiChild::getEditActions(bool incCatNew) +QList MdiChild::getEditActions() { QList actGrp; - if (incCatNew) { - actGrp.append(getAction(ACT_CAT_ADD)); - actGrp.append(getAction(ACT_CAT_SEP)); - } actGrp.append(action[ACT_MDL_ADD]); QAction * sep2 = new QAction(this); sep2->setSeparator(true); @@ -449,15 +411,13 @@ QList MdiChild::getModelActions() return actGrp; } -//QList MdiChild::getCategoryActions() -//{ -// QList actGrp; -// actGrp.append(getAction(ACT_CAT_ADD)); -// actGrp.append(getAction(ACT_CAT_EDT)); -// actGrp.append(getAction(ACT_CAT_DEL)); -// actGrp.append(getAction(ACT_CAT_SEP)); -// return actGrp; -//} +QList MdiChild::getLabelsActions() +{ + QList actGrp; + actGrp.append(getAction(ACT_LBL_ADD)); + actGrp.append(getAction(ACT_LBL_DEL)); + return actGrp; +} QAction * MdiChild::getAction(const MdiChild::Actions type) { @@ -474,15 +434,6 @@ void MdiChild::showModelsListContextMenu(const QPoint & pos) updateNavigation(); - if (firmware->getCapability(Capability::HasModelCategories)) { - contextMenu.addAction(action[ACT_CAT_ADD]); - if(modelsListModel->isCategoryType(modelIndex)) { - contextMenu.addAction(action[ACT_ITM_EDT]); - contextMenu.addAction(action[ACT_ITM_DEL]); - } - contextMenu.addSeparator(); - } - if (modelsListModel->isModelType(modelIndex)) { contextMenu.addActions(getEditActions()); if (countSelectedModels() == 1) { @@ -500,11 +451,20 @@ void MdiChild::showModelsListContextMenu(const QPoint & pos) contextMenu.exec(ui->modelsList->mapToGlobal(pos)); } +void MdiChild::showLabelsContextMenu(const QPoint &pos) +{ + QMenu contextMenu; + + contextMenu.addAction(action[ACT_LBL_ADD]); + contextMenu.addAction(action[ACT_LBL_DEL]); + + if (!contextMenu.isEmpty()) + contextMenu.exec(ui->lstLabels->mapToGlobal(pos)); +} + void MdiChild::showContextMenu(const QPoint & pos) { QMenu contextMenu; - if (firmware->getCapability(Capability::HasModelCategories)) - contextMenu.addAction(categoriesToolbar->toggleViewAction()); contextMenu.addAction(modelsToolbar->toggleViewAction()); contextMenu.addAction(radioToolbar->toggleViewAction()); if (!contextMenu.isEmpty()) @@ -534,26 +494,39 @@ void MdiChild::initModelsList() if (modelsListModel) delete modelsListModel; - modelsListModel = new TreeModel(&radioData, this); - connect(modelsListModel, &TreeModel::modelsDropped, this, &MdiChild::pasteModelData); - connect(modelsListModel, &TreeModel::modelsRemoved, this, &MdiChild::deleteModels); - connect(modelsListModel, &TreeModel::refreshRequested, this, &MdiChild::refresh); + modelsListModel = new ModelsListModel(&radioData, this); + connect(modelsListModel, &ModelsListModel::modelsDropped, this, &MdiChild::pasteModelData); + connect(modelsListModel, &ModelsListModel::modelsRemoved, this, &MdiChild::deleteModels); + connect(modelsListModel, &ModelsListModel::refreshRequested, this, &MdiChild::refresh); connect(modelsListModel, &QAbstractItemModel::dataChanged, this, &MdiChild::onDataChanged); ui->modelsList->setModel(modelsListModel); - if (IS_FAMILY_HORUS_OR_T16(board)) { - ui->modelsList->setIndentation(20); - // ui->modelsList->resetIndentation(); // introduced in next Qt versions ... - } - else { - ui->modelsList->setIndentation(0); - } + ui->modelsList->selectionModel()->currentIndex().row(); + + // Labels Editor + Model + if(labelsListModel) + delete labelsListModel; + labelsListModel = new LabelsModel(ui->modelsList->selectionModel(), + &radioData, this); + connect(labelsListModel, &LabelsModel::modelChanged, this, &MdiChild::refresh); + ui->lstLabels->setModel(labelsListModel); + ui->lstLabels->setSelectionMode(QAbstractItemView::SingleSelection); + ui->lstLabels->setDragEnabled(true); + ui->lstLabels->setAcceptDrops(true); + ui->lstLabels->setDropIndicatorShown(true); + ui->lstLabels->setDragDropOverwriteMode(false); + ui->lstLabels->setDragDropMode(QAbstractItemView::InternalMove); + ui->lstLabels->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed); + + ui->modelsList->setIndentation(0); + refresh(); - if (firmware->getCapability(Capability::HasModelCategories)) { + if (firmware->getCapability(Capability::HasModelLabels)) { ui->modelsList->header()->resizeSection(0, ui->modelsList->header()->sectionSize(0) * 2); // pad out categories and model names - } - else { + } else if (firmware->getCapability(Capability::HasModelLabels)) { + + } else { ui->modelsList->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); // minimise Index ui->modelsList->header()->resizeSection(1, ui->modelsList->header()->sectionSize(1) * 1.5); // pad out model names } @@ -602,7 +575,10 @@ void MdiChild::onCurrentItemChanged(const QModelIndex &, const QModelIndex &) void MdiChild::onDataChanged(const QModelIndex & index) { - if (!modelsListModel->isCategoryType(index)) + return; + + // TODO *** + /*if (!modelsListModel->isCategoryType(index)) return; int categoryIndex = modelsListModel->getCategoryIndex(index); @@ -612,7 +588,7 @@ void MdiChild::onDataChanged(const QModelIndex & index) strcpy(radioData.categories[categoryIndex].name, modelsListModel->data(index, 0).toString().left(sizeof(CategoryData::name)-1).toStdString().c_str()); setWindowModified(true); - emit modified(); + emit modified();*/ } /* @@ -624,7 +600,7 @@ QModelIndex MdiChild::getCurrentIndex() const return ui->modelsList->selectionModel()->currentIndex(); } -int MdiChild::getCurrentCategory() const +/*int MdiChild::getCurrentCategory() const { return modelsListModel->getCategoryIndex(getCurrentIndex()); } @@ -653,7 +629,7 @@ QVector MdiChild::getSelectedCategories() const cats.append(modelsListModel->getCategoryIndex(index)); } return cats; -} +}*/ int MdiChild::getCurrentModel() const { @@ -736,9 +712,10 @@ void MdiChild::onFirmwareChanged() * Categories CRUD */ +/* void MdiChild::categoryAdd() { - /*: Translators do NOT use accent for this, this is the default category name on Horus. */ + // 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(); @@ -799,7 +776,7 @@ void MdiChild::deleteSelectedCats() } if (modified) setModified(); -} +}*/ /* * Models CRUD @@ -827,8 +804,8 @@ void MdiChild::findNewDefaultModel(const unsigned startAt) radioData.setCurrentModel(0); } -// NOTE: insertModelRows() does not update the TreeModel, only modifies radioData.models[] array by inserting row(s) of blank model(s). -// TreeModel::refresh() needs to be called at some point afterwards to sync the data. +// NOTE: insertModelRows() does not update the ModelsListModel, only modifies radioData.models[] array by inserting row(s) of blank model(s). +// ModelsListModel::refresh() needs to be called at some point afterwards to sync the data. // This invalidates any model indices stored previously. bool MdiChild::insertModelRows(int atModelIdx, int count) { @@ -859,7 +836,7 @@ bool MdiChild::insertModelRows(int atModelIdx, int count) // Finds the first empty slot and inserts the model into it. In case of category-style models, will append to end of list. // Return -1 if no slot was found, otherwise new array index. -// TreeModel::refresh() needs to be called at some point afterwards to sync the data. +// ModelsListModel::refresh() needs to be called at some point afterwards to sync the data. int MdiChild::modelAppend(const ModelData model) { int newIdx = -1; @@ -895,12 +872,6 @@ int MdiChild::newModel(int modelIndex, int categoryIndex) bool isNewModel = radioData.models[modelIndex].isEmpty(); checkAndInitModel(modelIndex); - if (isNewModel && firmware->getCapability(Capability::HasModelCategories) && categoryIndex > -1) { - radioData.models[modelIndex].category = categoryIndex; - strcpy(radioData.models[modelIndex].filename, radioData.getNextModelFilename().toStdString().c_str()); - /*: Translators: do NOT use accents here, this is a default model name. */ - strcpy(radioData.models[modelIndex].name, qPrintable(tr("New model"))); // TODO: Why not just use existing default model name? - } // Only set the default model if we just added the first one. if (countUsedModels() == 1) { radioData.setCurrentModel(modelIndex); @@ -926,11 +897,11 @@ int MdiChild::newModel(int modelIndex, int categoryIndex) } -// NOTE: deleteModelss() does not update the TreeModel, only modifies radioData.models[] array by clearing the model data. +// NOTE: deleteModelss() does not update the ModelsListModel, only modifies radioData.models[] array by clearing the model data. // If (removeModelSlotsWhenDeleting == true) then removes array rows entirely (and pads w/blank model at the end if needed). -// TreeModel::refresh() needs to be called at some point afterwards to sync the data +// ModelsListModel::refresh() needs to be called at some point afterwards to sync the data // We delete using stored indexes because actual indexes may change during inserts/deletes. -// Obviously this only works before the stored indexes get updated in TreeModel::refresh(). +// Obviously this only works before the stored indexes get updated in ModelsListModel::refresh(). unsigned MdiChild::deleteModels(const QVector modelIndices) { unsigned deletes = 0; @@ -975,6 +946,7 @@ void MdiChild::deleteSelectedModels() deleteModels(getSelectedModels()); } +/* void MdiChild::moveModelsToCategory(const QVector models, const int toCategoryId) { if (toCategoryId < 0 || !models.size()) @@ -998,14 +970,14 @@ void MdiChild::moveModelsToCategory(const QVector models, const int toCateg void MdiChild::moveSelectedModelsToCat(const int toCategoryId) { moveModelsToCategory(getSelectedModels(), toCategoryId); -} +}*/ unsigned MdiChild::countUsedModels(const int categoryId) { unsigned count = 0; for (unsigned i=0; i < radioData.models.size(); ++i) { ModelData & model = radioData.models.at(i); - if (!model.isEmpty() && (categoryId < 0 || model.category == categoryId)) + if (!model.isEmpty()) ++count; } return count; @@ -1014,12 +986,12 @@ unsigned MdiChild::countUsedModels(const int categoryId) void MdiChild::pasteModelData(const QMimeData * mimeData, const QModelIndex row, bool insert, bool move) { QVector modelsList; - if (!TreeModel::decodeMimeData(mimeData, &modelsList)) + if (!ModelsListModel::decodeMimeData(mimeData, &modelsList)) return; bool modified = false; int modelIdx = modelsListModel->getModelIndex(row); - int categoryIdx = modelsListModel->getCategoryIndex(row); +// int categoryIdx = modelsListModel->getCategoryIndex(row); unsigned inserts = 0; QVector deletesList; @@ -1084,9 +1056,6 @@ void MdiChild::pasteModelData(const QMimeData * mimeData, const QModelIndex row, // We don't want to create an index value conflict so use an invalid one (it will get updated after we're done here) // this is esp. important because otherwise we may delete this model during a move operation (eg. after a cut) radioData.models[modelIdx].modelIndex = -modelIdx; - // Set the destination category, so a user can copy/paste across categories. - if (categoryIdx > -1) - radioData.models[modelIdx].category = categoryIdx; strcpy(radioData.models[modelIdx].filename, radioData.getNextModelFilename().toStdString().c_str()); lastSelectedModel = modelIdx; // after refresh the last pasted model will be selected modified = true; @@ -1117,7 +1086,7 @@ void MdiChild::pasteGeneralData(const QMimeData * mimeData) GeneralSettings gs; bool hasGenSettings = false; - if (!TreeModel::decodeMimeData(mimeData, NULL, &gs, &hasGenSettings)) + if (!ModelsListModel::decodeMimeData(mimeData, NULL, &gs, &hasGenSettings)) return; if (hasGenSettings && askQuestion(tr("Do you want to overwrite radio general settings?")) == QMessageBox::Yes) { @@ -1225,11 +1194,6 @@ void MdiChild::confirmDelete() deleteSelectedModels(); } } - else if (hasSelectedCat()) { - if (askQuestion(tr("Delete %n selected category(ies)?", 0, countSelectedCats())) == QMessageBox::Yes) { - deleteSelectedCats(); - } - } } void MdiChild::modelAdd() @@ -1261,7 +1225,7 @@ void MdiChild::modelDuplicate() } } -void MdiChild::onModelMoveToCategory() +/*void MdiChild::onModelMoveToCategory() { if (!sender()) { return; @@ -1271,7 +1235,7 @@ void MdiChild::onModelMoveToCategory() if (ok && toCatId >= 0) { moveSelectedModelsToCat(toCatId); } -} +}*/ void MdiChild::modelEdit() { @@ -1368,10 +1332,6 @@ void MdiChild::newFile(bool createDefaults) isUntitled = true; curFile = QString("document%1.etx").arg(sequenceNumber++); updateTitle(); - - if (createDefaults && firmware->getCapability(Capability::HasModelCategories)) { - categoryAdd(); - } } bool MdiChild::loadFile(const QString & filename, bool resetCurrentFile) @@ -1775,11 +1735,6 @@ void MdiChild::openModelTemplate(int row) for (int i = 0; i < CPN_MAX_MODULES; i++) { radioData.models[row].moduleData[i].modelId = row + 1; } - - setModified(); - setSelectedModel(row); - - openModelEditWindow(row); } void MdiChild::openModelPrompt(int row) @@ -1826,6 +1781,17 @@ void MdiChild::modelSave() saveSelectedModels(); } +void MdiChild::labelAdd() +{ + labelsListModel->insertRow(0); +} + +void MdiChild::labelDelete() +{ + int row = ui->lstLabels->selectionModel()->currentIndex().row(); + labelsListModel->removeRow(row); +} + unsigned MdiChild::saveModels(const QVector modelIndices) { unsigned saves = 0; @@ -1854,8 +1820,7 @@ unsigned MdiChild::saveModels(const QVector modelIndices) ++saves; } - - return saves; + return saves; } bool MdiChild::saveModel(const int modelIndex) diff --git a/companion/src/mdichild.h b/companion/src/mdichild.h index 55d07020e..8bf41155b 100644 --- a/companion/src/mdichild.h +++ b/companion/src/mdichild.h @@ -23,12 +23,15 @@ #include "eeprominterface.h" #include "modelslist.h" +#include "labels.h" #include #include #include #include #include +#include +#include class QToolBar; @@ -36,6 +39,9 @@ namespace Ui { class MdiChild; } +class LabelsDelegate; +class LabelsProxy; + class MdiChild : public QWidget { Q_OBJECT @@ -48,10 +54,8 @@ class MdiChild : public QWidget ACT_GEN_SIM, ACT_ITM_EDT, // edit model/rename category ACT_ITM_DEL, // delete model or cat - ACT_CAT_ADD, // category actions... - //ACT_CAT_EDT, // not sure these are needed... - //ACT_CAT_DEL, // the ACT_ITM_* actions do the same thing - ACT_CAT_SEP, // convenience separator shown/hidden with category actions + ACT_LBL_ADD, + ACT_LBL_DEL, ACT_MDL_ADD, // model actions... ACT_MDL_CPY, ACT_MDL_CUT, @@ -76,9 +80,9 @@ class MdiChild : public QWidget QVector getSelectedCategories() const; QVector getSelectedModels() const; QList getGeneralActions(); - QList getEditActions(bool incCatNew = true); + QList getEditActions(); QList getModelActions(); - //QList getCategoryActions(); + QList getLabelsActions(); QAction * getAction(const Actions type); public slots: @@ -109,6 +113,7 @@ class MdiChild : public QWidget void setModified(); void retranslateUi(); void showModelsListContextMenu(const QPoint & pos); + void showLabelsContextMenu(const QPoint & pos); void showContextMenu(const QPoint & pos); void adjustToolbarLayout(); @@ -130,13 +135,13 @@ class MdiChild : public QWidget void insert(); void edit(); void confirmDelete(); - void categoryAdd(); void modelAdd(); void modelEdit(); void modelSave(); + void labelAdd(); + void labelDelete(); void wizardEdit(); void modelDuplicate(); - void onModelMoveToCategory(); void openModelWizard(int row = -1); void openModelEditWindow(int row = -1); @@ -159,12 +164,6 @@ class MdiChild : public QWidget int countSelectedModels() const; bool hasSelectedModel(); bool setSelectedModel(const int modelIndex); - int getCurrentCategory() const; - int countSelectedCats() const; - bool hasSelectedCat(); - - bool deleteCategory(int categoryIndex = -1, QString * error = NULL); - void deleteSelectedCats(); void checkAndInitModel(int row); void findNewDefaultModel(const unsigned startAt = 0); @@ -174,8 +173,6 @@ class MdiChild : public QWidget unsigned deleteModels(const QVector modelIndices); bool deleteModel(const int modelIndex); void deleteSelectedModels(); - void moveModelsToCategory(const QVector models, const int toCategoryId); - void moveSelectedModelsToCat(const int toCategoryId); unsigned countUsedModels(const int categoryId = -1); unsigned saveModels(const QVector modelIndices); bool saveModel(const int modelIndex); @@ -197,22 +194,24 @@ class MdiChild : public QWidget QList * getModelEditDialogsList(); Ui::MdiChild * ui; - TreeModel * modelsListModel; + ModelsListModel * modelsListModel; + LabelsModel * labelsListModel; QWidget * parentWindow; QString curFile; QVector cutModels; QVector action; QToolBar * radioToolbar; - QToolBar * categoriesToolbar; QToolBar * modelsToolbar; + QToolBar * labelsToolbar; + QLabel *lblLabels; Firmware * firmware; RadioData radioData; int lastSelectedModel; bool isUntitled; - bool showCatToolbar; + bool showLabelToolbar; bool forceCloseFlag; const quint16 stateDataVersion; }; diff --git a/companion/src/mdichild.ui b/companion/src/mdichild.ui index d4b48cfa7..7ba95757f 100644 --- a/companion/src/mdichild.ui +++ b/companion/src/mdichild.ui @@ -10,10 +10,7 @@ 575 - - - 5 - + @@ -35,6 +32,25 @@ + + + + + + + + 16777215 + 150 + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + diff --git a/companion/src/modelslist.cpp b/companion/src/modelslist.cpp index 99a059aac..7979520e8 100644 --- a/companion/src/modelslist.cpp +++ b/companion/src/modelslist.cpp @@ -20,7 +20,7 @@ #include "modelslist.h" -TreeItem::TreeItem(const QVector & itemData): +ModelListItem::ModelListItem(const QVector & itemData): itemData(itemData), parentItem(NULL), categoryIndex(-1), @@ -30,60 +30,60 @@ TreeItem::TreeItem(const QVector & itemData): { } -TreeItem::TreeItem(TreeItem * parent, int categoryIndex, int modelIndex): - TreeItem(QVector(parent->columnCount())) +ModelListItem::ModelListItem(ModelListItem * parent, int categoryIndex, int modelIndex): + ModelListItem(QVector(parent->columnCount())) { setParent(parent); setCategoryIndex(categoryIndex); setModelIndex(modelIndex); } -TreeItem::~TreeItem() +ModelListItem::~ModelListItem() { qDeleteAll(childItems); } -TreeItem * TreeItem::child(int number) +ModelListItem * ModelListItem::child(int number) { return childItems.value(number); } -int TreeItem::childCount() const +int ModelListItem::childCount() const { return childItems.count(); } -int TreeItem::childNumber() const +int ModelListItem::childNumber() const { if (parentItem) - return parentItem->childItems.indexOf(const_cast(this)); + return parentItem->childItems.indexOf(const_cast(this)); return 0; } -int TreeItem::columnCount() const +int ModelListItem::columnCount() const { return itemData.count(); } -QVariant TreeItem::data(int column) const +QVariant ModelListItem::data(int column) const { return itemData.value(column); } -TreeItem *TreeItem::insertChild(const int row, int categoryIndex, int modelIndex) +ModelListItem *ModelListItem::insertChild(const int row, int categoryIndex, int modelIndex) { - TreeItem * item = new TreeItem(this, categoryIndex, modelIndex); + ModelListItem * item = new ModelListItem(this, categoryIndex, modelIndex); childItems.insert(row, item); return item; } -TreeItem * TreeItem::appendChild(int categoryIndex, int modelIndex) +ModelListItem * ModelListItem::appendChild(int categoryIndex, int modelIndex) { return insertChild(childItems.size(), categoryIndex, modelIndex); } -bool TreeItem::removeChildren(int position, int count) +bool ModelListItem::removeChildren(int position, int count) { if (position < 0 || position + count > childItems.size()) return false; @@ -94,7 +94,7 @@ bool TreeItem::removeChildren(int position, int count) return true; } -bool TreeItem::insertChildren(int row, int count) +bool ModelListItem::insertChildren(int row, int count) { for (int i=0; i < count; ++i) { insertChild(row + i, -1, -1); @@ -102,7 +102,7 @@ bool TreeItem::insertChildren(int row, int count) return true; } -bool TreeItem::setData(int column, const QVariant & value) +bool ModelListItem::setData(int column, const QVariant & value) { if (column < 0 || column >= itemData.size()) return false; @@ -111,7 +111,7 @@ bool TreeItem::setData(int column, const QVariant & value) return true; } -void TreeItem::setFlag(const quint16 & flag, const bool on) +void ModelListItem::setFlag(const quint16 & flag, const bool on) { if (on) flags |= flag; @@ -119,57 +119,60 @@ void TreeItem::setFlag(const quint16 & flag, const bool on) flags &= ~flag; } -bool TreeItem::isCategory() const +bool ModelListItem::isCategory() const { return (modelIndex < 0 && categoryIndex > -1); } -bool TreeItem::isModel() const +bool ModelListItem::isModel() const { return (modelIndex > -1); } /* - * TreeModel + * ModelsListModel */ -TreeModel::TreeModel(RadioData * radioData, QObject * parent): +ModelsListModel::ModelsListModel(RadioData * radioData, QObject * parent): QAbstractItemModel(parent), radioData(radioData) { - hasCategories = getCurrentFirmware()->getCapability(Capability::HasModelCategories); + hasLabels = getCurrentFirmware()->getCapability(Capability::HasModelLabels); QVector labels; - if (!hasCategories) + if (!hasLabels) labels << tr("Index"); labels << tr("Name"); labels << tr("RX #"); - rootItem = new TreeItem(labels); + if (hasLabels) + labels << tr("Labels"); + + rootItem = new ModelListItem(labels); // uniqueId and version for drag/drop operations (see encodeHeaderData()) mimeHeaderData.instanceId = QUuid::createUuid(); mimeHeaderData.dataVersion = 1; refresh(); - //connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, this, &TreeModel::onRowsAboutToBeRemoved); - connect(this, &QAbstractItemModel::rowsRemoved, this, &TreeModel::onRowsRemoved); + //connect(this, &QAbstractItemModel::rowsAboutToBeRemoved, this, &ModelsListModel::onRowsAboutToBeRemoved); + connect(this, &QAbstractItemModel::rowsRemoved, this, &ModelsListModel::onRowsRemoved); } -TreeModel::~TreeModel() +ModelsListModel::~ModelsListModel() { delete rootItem; } -int TreeModel::columnCount(const QModelIndex & /* parent */) const +int ModelsListModel::columnCount(const QModelIndex & /* parent */) const { return rootItem->columnCount(); } -QVariant TreeModel::data(const QModelIndex & index, int role) const +QVariant ModelsListModel::data(const QModelIndex & index, int role) const { if (!index.isValid()) return QVariant(); - TreeItem * item = getItem(index); + ModelListItem * item = getItem(index); if (role == Qt::DisplayRole || role == Qt::EditRole) { return item->data(index.column()); @@ -181,12 +184,15 @@ QVariant TreeModel::data(const QModelIndex & index, int role) const return font; } - if (role == Qt::ForegroundRole && (item->getFlags() & TreeItem::MarkedForCut)) { + if (role == Qt::ForegroundRole && (item->getFlags() & ModelListItem::MarkedForCut)) { return QPalette().brush(QPalette::Disabled, QPalette::Text); } if (role == Qt::ForegroundRole && item->isModel()) { - if (index.column() == (item->columnCount() - 1) && item->isHighlightRX()) { + int col = item->columnCount() - 1; + if(hasLabels) + col --; + if (index.column() == col && item->isHighlightRX()) { QBrush brush; brush.setColor(Qt::red); return brush; @@ -196,23 +202,18 @@ QVariant TreeModel::data(const QModelIndex & index, int role) const return QVariant(); } -Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const +Qt::ItemFlags ModelsListModel::flags(const QModelIndex &index) const { Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled; - if (index.isValid()) { - if (getItem(index)->isCategory()) - f |= Qt::ItemIsEditable; - else - f |= Qt::ItemIsDragEnabled; // TODO drag/drop categories - } + f |= Qt::ItemIsDragEnabled; f |= Qt::ItemIsDropEnabled; //qDebug() << f; return f; } -QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant ModelsListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) return rootItem->data(section); @@ -220,36 +221,27 @@ QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int rol return QVariant(); } -QModelIndex TreeModel::index(int row, int column, const QModelIndex & parent) const +QModelIndex ModelsListModel::index(int row, int column, const QModelIndex & parent) const { if (parent.isValid() && parent.column() != 0) return QModelIndex(); - TreeItem * parentItem = getItem(parent); - TreeItem * childItem = parentItem->child(row); + ModelListItem * parentItem = getItem(parent); + ModelListItem * childItem = parentItem->child(row); if (childItem) return createIndex(row, column, childItem); else return QModelIndex(); } -QModelIndex TreeModel::parent(const QModelIndex & index) const +QModelIndex ModelsListModel::parent(const QModelIndex & index) const { - if (!index.isValid()) - return QModelIndex(); - - TreeItem * childItem = getItem(index); - TreeItem * parentItem = childItem->parent(); - - if (parentItem == rootItem) - return QModelIndex(); - - return createIndex(parentItem->childNumber(), 0, parentItem); + return QModelIndex(); } -bool TreeModel::removeRows(int position, int rows, const QModelIndex & parent) +bool ModelsListModel::removeRows(int position, int rows, const QModelIndex & parent) { - TreeItem * parentItem = getItem(parent); + ModelListItem * parentItem = getItem(parent); if (!parentItem) return false; @@ -264,9 +256,9 @@ bool TreeModel::removeRows(int position, int rows, const QModelIndex & parent) } /* unused but possibly useful in future -bool TreeModel::insertRows(int row, int count, const QModelIndex & parent) +bool ModelsListModel::insertRows(int row, int count, const QModelIndex & parent) { - TreeItem * parentItem = getItem(parent); + ModelListItem * parentItem = getItem(parent); if (!parentItem) return false; @@ -280,13 +272,13 @@ bool TreeModel::insertRows(int row, int count, const QModelIndex & parent) return success; } */ -int TreeModel::rowCount(const QModelIndex &parent) const +int ModelsListModel::rowCount(const QModelIndex &parent) const { - TreeItem * parentItem = getItem(parent); + ModelListItem * parentItem = getItem(parent); return parentItem->childCount(); } -bool TreeModel::setData(const QModelIndex & index, const QVariant & value, int role) +bool ModelsListModel::setData(const QModelIndex & index, const QVariant & value, int role) { if (role != Qt::EditRole) return false; @@ -294,7 +286,7 @@ bool TreeModel::setData(const QModelIndex & index, const QVariant & value, int r if (!index.isValid()) return false; - TreeItem * item = getItem(index); + ModelListItem * item = getItem(index); bool result = item->setData(index.column(), value); if (result) { @@ -304,7 +296,7 @@ bool TreeModel::setData(const QModelIndex & index, const QVariant & value, int r return result; } -QStringList TreeModel::mimeTypes() const +QStringList ModelsListModel::mimeTypes() const { QStringList types; types << "application/x-companion-modeldata"; @@ -313,18 +305,18 @@ QStringList TreeModel::mimeTypes() const return types; } -Qt::DropActions TreeModel::supportedDropActions() const +Qt::DropActions ModelsListModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } -Qt::DropActions TreeModel::supportedDragActions() const +Qt::DropActions ModelsListModel::supportedDragActions() const { return Qt::CopyAction | Qt::MoveAction; } // This method encodes all the data on default drag operation, including general radio settings. This is useful for eg. Compare dialog/model printer. -QMimeData * TreeModel::mimeData(const QModelIndexList & indexes) const +QMimeData * ModelsListModel::mimeData(const QModelIndexList & indexes) const { QMimeData * mimeData = new QMimeData(); getModelsMimeData(indexes, mimeData); @@ -333,7 +325,7 @@ QMimeData * TreeModel::mimeData(const QModelIndexList & indexes) const return mimeData; } -bool TreeModel::canDropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) const +bool ModelsListModel::canDropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) const { Q_UNUSED(action); //qDebug() << action << row << column << parent.row(); @@ -345,7 +337,7 @@ bool TreeModel::canDropMimeData(const QMimeData * data, Qt::DropAction action, i return false; } -bool TreeModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) +bool ModelsListModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) { if (!canDropMimeData(data, action, row, column, parent)) return false; @@ -380,7 +372,7 @@ bool TreeModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int return true; } -QMimeData *TreeModel::getModelsMimeData(const QModelIndexList & indexes, QMimeData * mimeData) const +QMimeData *ModelsListModel::getModelsMimeData(const QModelIndexList & indexes, QMimeData * mimeData) const { if (!mimeData) mimeData = new QMimeData(); @@ -390,7 +382,7 @@ QMimeData *TreeModel::getModelsMimeData(const QModelIndexList & indexes, QMimeDa return mimeData; } -QMimeData *TreeModel::getGeneralMimeData(QMimeData * mimeData) const +QMimeData *ModelsListModel::getGeneralMimeData(QMimeData * mimeData) const { if (!mimeData) mimeData = new QMimeData(); @@ -400,7 +392,7 @@ QMimeData *TreeModel::getGeneralMimeData(QMimeData * mimeData) const return mimeData; } -QMimeData *TreeModel::getHeaderMimeData(QMimeData * mimeData) const +QMimeData *ModelsListModel::getHeaderMimeData(QMimeData * mimeData) const { if (!mimeData) mimeData = new QMimeData(); @@ -410,14 +402,14 @@ QMimeData *TreeModel::getHeaderMimeData(QMimeData * mimeData) const return mimeData; } -QUuid TreeModel::getMimeDataSourceId(const QMimeData * mimeData) const +QUuid ModelsListModel::getMimeDataSourceId(const QMimeData * mimeData) const { MimeHeaderData header; decodeHeaderData(mimeData, &header); return header.instanceId; } -bool TreeModel::hasSupportedMimeData(const QMimeData * mimeData) const +bool ModelsListModel::hasSupportedMimeData(const QMimeData * mimeData) const { foreach (const QString & mtype, mimeTypes()) { if (mimeData->hasFormat(mtype)) @@ -426,28 +418,28 @@ bool TreeModel::hasSupportedMimeData(const QMimeData * mimeData) const return false; } -bool TreeModel::hasModelsMimeData(const QMimeData * mimeData) const +bool ModelsListModel::hasModelsMimeData(const QMimeData * mimeData) const { return mimeData->hasFormat("application/x-companion-modeldata"); } -bool TreeModel::hasGenralMimeData(const QMimeData * mimeData) const +bool ModelsListModel::hasGenralMimeData(const QMimeData * mimeData) const { return mimeData->hasFormat("application/x-companion-generaldata"); } -bool TreeModel::hasHeaderMimeData(const QMimeData * mimeData) const +bool ModelsListModel::hasHeaderMimeData(const QMimeData * mimeData) const { return mimeData->hasFormat("application/x-companion-radiodata-header"); } // returns true if mime data origin was this data model (vs. from another file window) -bool TreeModel::hasOwnMimeData(const QMimeData * mimeData) const +bool ModelsListModel::hasOwnMimeData(const QMimeData * mimeData) const { return (getMimeDataSourceId(mimeData) == mimeHeaderData.instanceId); } -void TreeModel::encodeModelsData(const QModelIndexList & indexes, QByteArray * data) const +void ModelsListModel::encodeModelsData(const QModelIndexList & indexes, QByteArray * data) const { foreach (const QModelIndex &index, indexes) { if (index.isValid() && index.column() == 0) { @@ -459,15 +451,15 @@ void TreeModel::encodeModelsData(const QModelIndexList & indexes, QByteArray * d } } -void TreeModel::encodeGeneralData(QByteArray * data) const +void ModelsListModel::encodeGeneralData(QByteArray * data) const { data->append('G'); data->append((char *)&radioData->generalSettings, sizeof(GeneralSettings)); } -void TreeModel::encodeHeaderData(QByteArray * data) const +void ModelsListModel::encodeHeaderData(QByteArray * data) const { - // We use a unique ID representing this TreeModel instance (a unique file). + // We use a unique ID representing this ModelsListModel instance (a unique file). // This can be used eg. to detect cross-file drop operations. QDataStream stream(data, QIODevice::WriteOnly); stream << mimeHeaderData.dataVersion; @@ -475,7 +467,7 @@ void TreeModel::encodeHeaderData(QByteArray * data) const } // static -bool TreeModel::decodeHeaderData(const QMimeData * mimeData, MimeHeaderData * header) +bool ModelsListModel::decodeHeaderData(const QMimeData * mimeData, MimeHeaderData * header) { if (header && mimeData->hasFormat("application/x-companion-radiodata-header")) { QByteArray data = mimeData->data("application/x-companion-radiodata-header"); @@ -487,7 +479,7 @@ bool TreeModel::decodeHeaderData(const QMimeData * mimeData, MimeHeaderData * he } // static -bool TreeModel::decodeMimeData(const QMimeData * mimeData, QVector * models, GeneralSettings * gs, bool * hasGenSet) +bool ModelsListModel::decodeMimeData(const QMimeData * mimeData, QVector * models, GeneralSettings * gs, bool * hasGenSet) { bool ret = false; char * gData; @@ -528,7 +520,7 @@ bool TreeModel::decodeMimeData(const QMimeData * mimeData, QVector * } // static -int TreeModel::countModelsInMimeData(const QMimeData * mimeData) +int ModelsListModel::countModelsInMimeData(const QMimeData * mimeData) { int ret = 0; if (mimeData->hasFormat("application/x-companion-modeldata")) { @@ -539,10 +531,10 @@ int TreeModel::countModelsInMimeData(const QMimeData * mimeData) } -TreeItem * TreeModel::getItem(const QModelIndex & index) const +ModelListItem * ModelsListModel::getItem(const QModelIndex & index) const { if (index.isValid()) { - TreeItem * item = static_cast(index.internalPointer()); + ModelListItem * item = static_cast(index.internalPointer()); if (item) { return item; } @@ -551,7 +543,7 @@ TreeItem * TreeModel::getItem(const QModelIndex & index) const } // recursive -QModelIndex TreeModel::getIndexForModel(const int modelIndex, QModelIndex parent) +QModelIndex ModelsListModel::getIndexForModel(const int modelIndex, QModelIndex parent) { for (int i = 0; i < rowCount(parent); ++i) { QModelIndex idx = index(i, 0, parent); @@ -563,7 +555,7 @@ QModelIndex TreeModel::getIndexForModel(const int modelIndex, QModelIndex parent return QModelIndex(); } -QModelIndex TreeModel::getIndexForCategory(const int categoryIndex) +QModelIndex ModelsListModel::getIndexForCategory(const int categoryIndex) { for (int i = 0; i < rowCount(); ++i) { if (getItem(index(i, 0))->getCategoryIndex() == categoryIndex) @@ -572,51 +564,51 @@ QModelIndex TreeModel::getIndexForCategory(const int categoryIndex) return QModelIndex(); } -int TreeModel::getModelIndex(const QModelIndex & index) const +int ModelsListModel::getModelIndex(const QModelIndex & index) const { return getItem(index)->getModelIndex(); } -int TreeModel::getCategoryIndex(const QModelIndex & index) const +int ModelsListModel::getCategoryIndex(const QModelIndex & index) const { return getItem(index)->getCategoryIndex(); } -int TreeModel::rowNumber(const QModelIndex & index) const +int ModelsListModel::rowNumber(const QModelIndex & index) const { return getItem(index)->childNumber(); } -bool TreeModel::isCategoryType(const QModelIndex & index) const +bool ModelsListModel::isCategoryType(const QModelIndex & index) const { return index.isValid() && getItem(index)->isCategory(); } -bool TreeModel::isModelType(const QModelIndex & index) const +bool ModelsListModel::isModelType(const QModelIndex & index) const { return index.isValid() && getItem(index)->isModel(); } -void TreeModel::markItemForCut(const QModelIndex & index, bool on) +void ModelsListModel::markItemForCut(const QModelIndex & index, bool on) { if (index.isValid() && index.column() == 0) - getItem(index)->setFlag(TreeItem::MarkedForCut, on); + getItem(index)->setFlag(ModelListItem::MarkedForCut, on); } -void TreeModel::markItemsForCut(const QModelIndexList & indexes, bool on) +void ModelsListModel::markItemsForCut(const QModelIndexList & indexes, bool on) { foreach (const QModelIndex &index, indexes) markItemForCut(index, on); } // onRowsAboutToBeRemoved could be a way to deal with models being drag-drop moved to another window/file. -// TreeModel detects these as removals and runs removeRows(), which deletes the Model indexes +// ModelsListModel detects these as removals and runs removeRows(), which deletes the Model indexes // but not the actual models from the RadioData::models array. // BUT this also runs when moving rows within our own tree, and if there is an error during the move, // or the user cancels the operation, removeRows() is still called automatically somewhere inside QAbstractItemModel(). // If a solution could be found to this problem then we could enable DnD-moving models between file windows. /* -void TreeModel::onRowsAboutToBeRemoved(const QModelIndex & parent, int first, int last) +void ModelsListModel::onRowsAboutToBeRemoved(const QModelIndex & parent, int first, int last) { qDebug() << parent << first << last; QVector modelIndices; @@ -628,55 +620,38 @@ void TreeModel::onRowsAboutToBeRemoved(const QModelIndex & parent, int first, in } */ -void TreeModel::onRowsRemoved(const QModelIndex & parent, int first, int last) +void ModelsListModel::onRowsRemoved(const QModelIndex & parent, int first, int last) { // This is a workaround to deal with models being DnD moved to another window/file or if user cancels a DnD move within our own. - // TreeModel detects these as removals and runs removeRows(), which deletes the Model indexes but not our actual models. See notes above. + // ModelsListModel detects these as removals and runs removeRows(), which deletes the Model indexes but not our actual models. See notes above. //qDebug() << parent << first << last; emit refreshRequested(); // request refresh from View because it may have it's own ideas } -void TreeModel::refresh() +void ModelsListModel::refresh() { - TreeItem * defaultCategoryItem = NULL; + ModelListItem * defaultCategoryItem = NULL; this->blockSignals(true); // make sure onRowsRemoved is not triggered removeRows(0, rowCount()); this->blockSignals(false); - if (hasCategories) { + /*if (hasCategories) { for (unsigned i = 0; i < radioData->categories.size(); i++) { - TreeItem * current = rootItem->appendChild(i, -1); + 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; - TreeItem * current = NULL; + ModelListItem * current = NULL; model.modelIndex = i; - if (hasCategories) { - if (!model.isEmpty()) { - TreeItem * categoryItem; - // TODO category should be set to -1 if not Horus - if (model.category >= 0 && model.category < rootItem->childCount()) { - categoryItem = rootItem->child(model.category); - } - else { - model.category = 0; - if (!defaultCategoryItem) { - defaultCategoryItem = rootItem->appendChild(0, -1); - /*: Translators do NOT use accent for this, this is the default category name on Horus. */ - defaultCategoryItem->setData(0, tr("Models")); - radioData->categories.push_back(qPrintable(tr("Models"))); - } - categoryItem = defaultCategoryItem; - } - current = categoryItem->appendChild(model.category, i); - } + if (hasLabels) { + current = rootItem->appendChild(0, i); } else { current = rootItem->appendChild(0, i); @@ -715,10 +690,13 @@ void TreeModel::refresh() } current->setData(currentColumn++, rxs); } + if (hasLabels) { + current->setData(currentColumn++, QString(model.labels)); + } } } -bool TreeModel::isModelIdUnique(unsigned modelIdx, unsigned module, unsigned protocol) +bool ModelsListModel::isModelIdUnique(unsigned modelIdx, unsigned module, unsigned protocol) { int cnt = 0; if (protocol== PULSES_PXX_XJT_D8) @@ -736,3 +714,4 @@ bool TreeModel::isModelIdUnique(unsigned modelIdx, unsigned module, unsigned pro } return true; } + diff --git a/companion/src/modelslist.h b/companion/src/modelslist.h index 8fce2ef2c..fc9b42ead 100644 --- a/companion/src/modelslist.h +++ b/companion/src/modelslist.h @@ -25,29 +25,29 @@ #include #include -class TreeItem +class ModelListItem { public: - enum TreeItemFlags { MarkedForCut = 0x01 }; + enum ModelListItemFlags { MarkedForCut = 0x01 }; - explicit TreeItem(const QVector & itemData); - explicit TreeItem(TreeItem * parent, int categoryIndex, int modelIndex); - ~TreeItem(); + explicit ModelListItem(const QVector & itemData); + explicit ModelListItem(ModelListItem * parent, int categoryIndex, int modelIndex); + ~ModelListItem(); - TreeItem * child(int number); + ModelListItem * child(int number); int childCount() const; int columnCount() const; QVariant data(int column) const; - TreeItem * appendChild(int categoryIndex, int modelIndex); - TreeItem * insertChild(const int row, int categoryIndex, int modelIndex); + ModelListItem * appendChild(int categoryIndex, int modelIndex); + ModelListItem * insertChild(const int row, int categoryIndex, int modelIndex); bool removeChildren(int position, int count); bool insertChildren(int row, int count); int childNumber() const; bool setData(int column, const QVariant &value); - TreeItem * parent() { return parentItem; } - void setParent(TreeItem * p) { parentItem = p; } + ModelListItem * parent() { return parentItem; } + void setParent(ModelListItem * p) { parentItem = p; } int getModelIndex() const { return modelIndex; } void setModelIndex(int value) { modelIndex = value; } int getCategoryIndex() const { return categoryIndex; } @@ -63,9 +63,9 @@ class TreeItem bool isModel() const; private: - QList childItems; + QList childItems; QVector itemData; - TreeItem * parentItem; + ModelListItem * parentItem; int categoryIndex; int modelIndex; quint16 flags; @@ -73,7 +73,7 @@ class TreeItem }; -class TreeModel : public QAbstractItemModel +class ModelsListModel : public QAbstractItemModel { Q_OBJECT @@ -83,8 +83,8 @@ class TreeModel : public QAbstractItemModel quint16 dataVersion; }; - TreeModel(RadioData * radioData, QObject *parent = 0); - virtual ~TreeModel(); + ModelsListModel(RadioData * radioData, QObject *parent = 0); + virtual ~ModelsListModel(); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; @@ -92,7 +92,7 @@ class TreeModel : public QAbstractItemModel virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; - virtual Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; + virtual Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; @@ -146,11 +146,11 @@ class TreeModel : public QAbstractItemModel void onRowsRemoved(const QModelIndex & parent, int first, int last); private: - TreeItem * getItem(const QModelIndex & index) const; + ModelListItem * getItem(const QModelIndex & index) const; bool isModelIdUnique(unsigned modelId, unsigned module, unsigned protocol); - TreeItem * rootItem; + ModelListItem * rootItem; RadioData * radioData; MimeHeaderData mimeHeaderData; - bool hasCategories; + bool hasLabels; }; diff --git a/companion/src/storage/CMakeLists.txt b/companion/src/storage/CMakeLists.txt index f39b82475..874dfb5ce 100644 --- a/companion/src/storage/CMakeLists.txt +++ b/companion/src/storage/CMakeLists.txt @@ -10,7 +10,7 @@ set(storage_NAMES bineeprom eepe hexeeprom - categorized + labeled sdcard etx otx diff --git a/companion/src/storage/etx.cpp b/companion/src/storage/etx.cpp index 8ec2085ef..db49c4f4c 100644 --- a/companion/src/storage/etx.cpp +++ b/companion/src/storage/etx.cpp @@ -43,7 +43,7 @@ bool EtxFormat::load(RadioData & radioData) return false; } - bool result = CategorizedStorageFormat::load(radioData); + bool result = LabelsStorageFormat::load(radioData); mz_zip_reader_end(&zip_archive); return result; } @@ -58,7 +58,7 @@ bool EtxFormat::write(const RadioData & radioData) return false; } - bool result = CategorizedStorageFormat::write(radioData); + bool result = LabelsStorageFormat::write(radioData); if (result) { // finalize archive and get contents char * archiveContents; diff --git a/companion/src/storage/etx.h b/companion/src/storage/etx.h index dd65c4eb2..69bcd1b8f 100644 --- a/companion/src/storage/etx.h +++ b/companion/src/storage/etx.h @@ -20,17 +20,17 @@ #pragma once -#include "categorized.h" +#include "labeled.h" #include -class EtxFormat : public CategorizedStorageFormat +class EtxFormat : public LabelsStorageFormat { Q_DECLARE_TR_FUNCTIONS(EtxFormat) public: EtxFormat(const QString & filename): - CategorizedStorageFormat(filename) + LabelsStorageFormat(filename) { } diff --git a/companion/src/storage/categorized.cpp b/companion/src/storage/labeled.cpp similarity index 66% rename from companion/src/storage/categorized.cpp rename to companion/src/storage/labeled.cpp index f87d6849e..0289582d7 100644 --- a/companion/src/storage/categorized.cpp +++ b/companion/src/storage/labeled.cpp @@ -18,14 +18,14 @@ * GNU General Public License for more details. */ -#include "categorized.h" +#include "labeled.h" #include "firmwares/opentx/opentxinterface.h" #include "firmwares/edgetx/edgetxinterface.h" #include "miniz.c" // Can only be included once! #include -bool CategorizedStorageFormat::load(RadioData & radioData) +bool LabelsStorageFormat::load(RadioData & radioData) { StorageType st = getStorageType(filename); if (st == STORAGE_TYPE_UNKNOWN) { @@ -39,7 +39,7 @@ bool CategorizedStorageFormat::load(RadioData & radioData) } } -bool CategorizedStorageFormat::write(const RadioData & radioData) +bool LabelsStorageFormat::write(const RadioData & radioData) { StorageType st = getStorageType(filename); if (st == STORAGE_TYPE_ETX || st == STORAGE_TYPE_YML || @@ -50,7 +50,7 @@ bool CategorizedStorageFormat::write(const RadioData & radioData) return false; } -StorageType CategorizedStorageFormat::probeFormat() +StorageType LabelsStorageFormat::probeFormat() { if (QFile(filename + "/RADIO/radio.yml").exists()) // converted return getStorageType("radio.yml"); @@ -60,7 +60,7 @@ StorageType CategorizedStorageFormat::probeFormat() return getStorageType(filename); } -bool CategorizedStorageFormat::loadBin(RadioData & radioData) +bool LabelsStorageFormat::loadBin(RadioData & radioData) { QByteArray radioSettingsBuffer; if (!loadFile(radioSettingsBuffer, "RADIO/radio.bin")) { @@ -82,24 +82,15 @@ bool CategorizedStorageFormat::loadBin(RadioData & radioData) } QList lines = modelsListBuffer.split('\n'); - int modelIndex = 0; - int categoryIndex = -1; - foreach (const QByteArray & lineArray, lines) { - QString line = QString(lineArray).trimmed(); - if (line.isEmpty()) continue; - // qDebug() << "parsing line" << line; + int modelIndex = 0; + foreach (const QByteArray & lineArray, lines) { + QString line = QString(lineArray).trimmed(); + if (line.isEmpty()) continue; + // qDebug() << "parsing line" << line; - if (line.startsWith('[') && line.endsWith(']')) { - // ignore categories for boards that do not support them - if (getCurrentFirmware()->getCapability(HasModelCategories)) { - QString name = line.mid(1, line.size() - 2); - CategoryData category(qPrintable(name)); - radioData.categories.push_back(category); - categoryIndex++; - qDebug() << "added category" << name; - } - continue; - } + if (line.startsWith('[') && line.endsWith(']')) { + continue; + } // determine if we have a model number QStringList parts = line.split(QRegExp("\\s+"), QString::SkipEmptyParts); @@ -136,9 +127,6 @@ bool CategorizedStorageFormat::loadBin(RadioData & radioData) radioData.generalSettings.currModelIndex = modelIndex; qDebug() << "currModelIndex =" << modelIndex; } - if (getCurrentFirmware()->getCapability(HasModelCategories)) { - radioData.models[modelIndex].category = categoryIndex; - } radioData.models[modelIndex].used = true; modelIndex++; continue; @@ -152,12 +140,11 @@ bool CategorizedStorageFormat::loadBin(RadioData & radioData) return true; } -bool CategorizedStorageFormat::writeBin(const RadioData & radioData) +bool LabelsStorageFormat::writeBin(const RadioData & radioData) { - QByteArray modelsList; // models.txt QByteArray radioSettingsData; // radio.bin size_t numModels = radioData.models.size(); - size_t numCategories = radioData.categories.size(); + size_t numCategories = radioData.labels.size(); std::vector> sortedModels(numCategories); writeRadioSettingsToByteArray(radioData.generalSettings, radioSettingsData); @@ -175,41 +162,12 @@ bool CategorizedStorageFormat::writeBin(const RadioData & radioData) if (!writeFile(modelData, modelFilename)) { return false; } - - // For firmware that doesn't support categories, we can just construct - // models.txt as we iterate thru the models vector. For firmware that does - // support categories, we have a bit of work to do in order to sort the - // models first by category index, so we use the sortedModels data - // structure to do that. - if (!getCurrentFirmware()->getCapability(HasModelCategories)) { - // Use format with model number and file name. This is needed because - // radios without category support can have unused model slots - modelsList.append(QString("%1 %2\n").arg(m).arg(model.filename).toLocal8Bit()); - } else { - sortedModels[model.category].push_back(QString("%1\n").arg(model.filename)); - } - } - - if (getCurrentFirmware()->getCapability(HasModelCategories)) { - for (size_t c=0; c filelist; - if (!getFileList(filelist)) { - return false; - } - - // push default category - radioData.categories.push_back("Models"); - - const std::regex yml_regex("MODELS/(model([0-9]+)\\.yml)"); - for(const auto& f : filelist) { - std::smatch match; - if (std::regex_match(f, match, yml_regex)) { - if (match.size() == 3) { - std::ssub_match modelFile = match[1]; - std::ssub_match modelIdx = match[2]; - modelFiles.push_back({ modelFile.str(), "", 0, std::stoi(modelIdx.str()) }); - } - } - } + setError(tr("Can't load MODELS/labels.yml") + ":\n" + QString(e.what())); } - } else { - // fetch "MODELS/modelXX.yml" + + // Scan for all models std::list filelist; if (!getFileList(filelist)) { - setError(tr("Cannot list files")); return false; } @@ -297,18 +235,18 @@ bool CategorizedStorageFormat::loadYaml(RadioData & radioData) if (match.size() == 3) { std::ssub_match modelFile = match[1]; std::ssub_match modelIdx = match[2]; - modelFiles.push_back({ modelFile.str(), "", 0, std::stoi(modelIdx.str()) }); + modelFiles.push_back({ modelFile.str(), "", std::stoi(modelIdx.str()) }); } } } } int modelIdx = 0; - bool hasCategories = getCurrentFirmware()->getCapability(HasModelCategories); + bool hasLabels = getCurrentFirmware()->getCapability(HasModelLabels); radioData.models.resize(modelFiles.size()); for (const auto& mc : modelFiles) { - qDebug() << "Filename: " << mc.filename.c_str() << " / Category: " << mc.category; + qDebug() << "Filename: " << mc.filename.c_str(); QByteArray modelBuffer; QString filename = "MODELS/" + QString::fromStdString(mc.filename); @@ -332,24 +270,34 @@ bool CategorizedStorageFormat::loadYaml(RadioData & radioData) return false; } - model.category = mc.category; model.modelIndex = modelIdx; strncpy(model.filename, mc.filename.c_str(), sizeof(model.filename)-1); - if (hasCategories && !strncmp(radioData.generalSettings.currModelFilename, + if (hasLabels && !strncmp(radioData.generalSettings.currModelFilename, model.filename, sizeof(model.filename))) { - radioData.generalSettings.currModelIndex = modelIdx; - qDebug() << "currModelIndex =" << modelIdx; + radioData.generalSettings.currModelIndex = modelIdx; + } + + if (hasLabels) { + QStringList labels = QString(model.labels).split(',',QString::SkipEmptyParts); + foreach(QString label, labels) { + radioData.addLabel(label); + } } model.used = true; modelIdx++; } + // If no labels, add a Favorites + if(hasLabels && !radioData.labels.size()) { + radioData.labels.append(tr("Favorites")); + } + return true; } -bool CategorizedStorageFormat::writeYaml(const RadioData & radioData) +bool LabelsStorageFormat::writeYaml(const RadioData & radioData) { QByteArray radioSettingsBuffer; if (!writeRadioSettingsToYaml(radioData.generalSettings, radioSettingsBuffer)) { @@ -360,12 +308,12 @@ bool CategorizedStorageFormat::writeYaml(const RadioData & radioData) return false; } - bool hasCategories = getCurrentFirmware()->getCapability(HasModelCategories); + bool hasLabels = getCurrentFirmware()->getCapability(HasModelLabels); // B&W radios do not use the models.yml file and scan the MODELS folder for modelxx.yml files // models have been deleted or reordered in Companion so delete all old modelxx.yml and just in case models.yml files // from radio MODELS folder before writing new modelxx.yml files - if (!hasCategories) { + if (!hasLabels) { // fetch "MODELS/modelXX.yml" std::list filelist; if (!getFileList(filelist)) { @@ -393,12 +341,12 @@ bool CategorizedStorageFormat::writeYaml(const RadioData & radioData) continue; QString modelFilename; - if (hasCategories) { + if (hasLabels) { std::string ymlFilename = patchFilenameToYaml(model.filename); modelFilename = QString("MODELS/%1").arg(QString::fromStdString(ymlFilename)); modelFiles.push_back( - {ymlFilename, std::string(model.name), model.category}); + {ymlFilename, std::string(model.name)}); } else { modelFilename = QString("MODELS/model%1.yml") .arg(model.modelIndex, 2, 10, QLatin1Char('0')); @@ -411,11 +359,10 @@ bool CategorizedStorageFormat::writeYaml(const RadioData & radioData) } } - if (hasCategories) { - // TODO: sort 'modelFiles' by category index - QByteArray modelslistBuffer; - if (!writeModelsListToYaml(radioData.categories, modelFiles, modelslistBuffer) - || !writeFile(modelslistBuffer, "MODELS/models.yml")) { + if (hasLabels) { + QByteArray labelsListBuffer; + if (!writeLabelsListToYaml(radioData, labelsListBuffer) + || !writeFile(labelsListBuffer, "MODELS/labels.yml")) { return false; } } diff --git a/companion/src/storage/categorized.h b/companion/src/storage/labeled.h similarity index 90% rename from companion/src/storage/categorized.h rename to companion/src/storage/labeled.h index ae279a88e..feb178899 100644 --- a/companion/src/storage/categorized.h +++ b/companion/src/storage/labeled.h @@ -27,12 +27,12 @@ #include #include -class CategorizedStorageFormat : public StorageFormat +class LabelsStorageFormat : public StorageFormat { - Q_DECLARE_TR_FUNCTIONS(CategorizedStorageFormat) + Q_DECLARE_TR_FUNCTIONS(LabelsStorageFormat) public: - CategorizedStorageFormat(const QString & filename): + LabelsStorageFormat(const QString & filename): StorageFormat(filename) { } diff --git a/companion/src/storage/otx.cpp b/companion/src/storage/otx.cpp index 44dbd9ef2..0275c940f 100644 --- a/companion/src/storage/otx.cpp +++ b/companion/src/storage/otx.cpp @@ -43,7 +43,7 @@ bool OtxFormat::load(RadioData & radioData) return false; } - bool result = CategorizedStorageFormat::load(radioData); + bool result = LabelsStorageFormat::load(radioData); mz_zip_reader_end(&zip_archive); return result; } @@ -58,7 +58,7 @@ bool OtxFormat::write(const RadioData & radioData) return false; } - bool result = CategorizedStorageFormat::write(radioData); + bool result = LabelsStorageFormat::write(radioData); if (result) { // finalize archive and get contents char * archiveContents; diff --git a/companion/src/storage/otx.h b/companion/src/storage/otx.h index ee398476c..9d3235503 100644 --- a/companion/src/storage/otx.h +++ b/companion/src/storage/otx.h @@ -20,17 +20,17 @@ #pragma once -#include "categorized.h" +#include "labeled.h" #include -class OtxFormat : public CategorizedStorageFormat +class OtxFormat : public LabelsStorageFormat { Q_DECLARE_TR_FUNCTIONS(OtxFormat) public: OtxFormat(const QString & filename): - CategorizedStorageFormat(filename) + LabelsStorageFormat(filename) { } diff --git a/companion/src/storage/sdcard.cpp b/companion/src/storage/sdcard.cpp index 034741fa4..d9b69d9c2 100644 --- a/companion/src/storage/sdcard.cpp +++ b/companion/src/storage/sdcard.cpp @@ -27,7 +27,7 @@ bool SdcardFormat::write(const RadioData & radioData) QDir dir(filename); dir.mkdir("RADIO"); dir.mkdir("MODELS"); - return CategorizedStorageFormat::write(radioData); + return LabelsStorageFormat::write(radioData); } bool SdcardFormat::loadFile(QByteArray & filedata, const QString & filename) diff --git a/companion/src/storage/sdcard.h b/companion/src/storage/sdcard.h index 2f5b39124..262f2cfae 100644 --- a/companion/src/storage/sdcard.h +++ b/companion/src/storage/sdcard.h @@ -21,17 +21,17 @@ #ifndef _SDCARD_H2_ #define _SDCARD_H2_ -#include "categorized.h" +#include "labeled.h" #include -class SdcardFormat : public CategorizedStorageFormat +class SdcardFormat : public LabelsStorageFormat { Q_DECLARE_TR_FUNCTIONS(SdcardFormat) public: SdcardFormat(const QString & filename): - CategorizedStorageFormat(filename) + LabelsStorageFormat(filename) { } diff --git a/companion/src/storage/yaml.cpp b/companion/src/storage/yaml.cpp index f47320bcd..91ff7bc81 100644 --- a/companion/src/storage/yaml.cpp +++ b/companion/src/storage/yaml.cpp @@ -55,8 +55,8 @@ bool YamlFormat::writeFile(const QByteArray & filedata) } bool YamlFormat::load(RadioData & radioData) -{ - bool hasCategories = getCurrentFirmware()->getCapability(HasModelCategories); +{ + //bool hasLabels = getCurrentFirmware()->getCapability(HasModelLabels); int modelIdx = 0; QByteArray data; @@ -78,10 +78,10 @@ bool YamlFormat::load(RadioData & radioData) if (node["header"].IsMap()) { qDebug() << "File" << filename << "appears to contain model data"; - if (hasCategories) { + /*if (hasLabels) { CategoryData category(qPrintable(tr("New category"))); radioData.categories.push_back(category); - } + }*/ radioData.models.resize(1); @@ -97,7 +97,7 @@ bool YamlFormat::load(RadioData & radioData) return false; } - model.category = 0; + /*model.category = 0;*/ model.modelIndex = modelIdx; strncpy(model.filename, qPrintable(filename), sizeof(model.filename) - 1); model.used = true; diff --git a/companion/src/wizarddata.cpp b/companion/src/wizarddata.cpp index d3344c71c..40b9455e7 100644 --- a/companion/src/wizarddata.cpp +++ b/companion/src/wizarddata.cpp @@ -92,7 +92,7 @@ WizMix::operator ModelData() int throttleChannel = -1; ModelData model; - model.category = originalModelData.category; + model.labels[0] = '\0'; model.used = true; model.moduleData[0].modelId = modelId; model.setDefaultInputs(settings); diff --git a/radio/src/CMakeLists.txt b/radio/src/CMakeLists.txt index 9aa1aba10..b14e28b42 100644 --- a/radio/src/CMakeLists.txt +++ b/radio/src/CMakeLists.txt @@ -46,6 +46,7 @@ option(TRACE_SIMPGMSPACE "Turn on traces in simpgmspace.cpp" ON) option(TRACE_LUA_INTERNALS "Turn on traces for Lua internals" OFF) option(DEBUG_WINDOWS "Turn on windows traces" OFF) option(DEBUG_YAML "Turn on YAML traces" OFF) +option(DEBUG_LABELS "Turn on Labels traces" OFF) option(FRSKY_STICKS "Reverse sticks for FrSky sticks" OFF) option(NANO "Use nano newlib and binalloc") option(TEST_BUILD_WARNING "Warn this is a test build" OFF) @@ -210,11 +211,11 @@ if(LUA) if(LUA_MIXER) add_definitions(-DLUA_MODEL_SCRIPTS) endif() - + # TODO: LUA CMAKE - set(SRC ${SRC} - lua/interface.cpp - lua/api_general.cpp + set(SRC ${SRC} + lua/interface.cpp + lua/api_general.cpp lua/api_model.cpp lua/api_filesystem.cpp lua/lua_event.cpp @@ -226,37 +227,37 @@ if(LUA) set(SRC ${SRC} lua/api_stdlcd.cpp) endif() - set(LUA_SRC - lapi.c - lcode.c - lctype.c - ldebug.c - ldo.c - ldump.c - lfunc.c - lgc.c - llex.c - lmem.c - lobject.c - lopcodes.c + set(LUA_SRC + lapi.c + lcode.c + lctype.c + ldebug.c + ldo.c + ldump.c + lfunc.c + lgc.c + llex.c + lmem.c + lobject.c + lopcodes.c lparser.c - lstate.c - lstring.c - ltable.c - lrotable.c - ltm.c - lundump.c - lvm.c - lzio.c + lstate.c + lstring.c + ltable.c + lrotable.c + ltm.c + lundump.c + lvm.c + lzio.c linit.c - lbaselib.c - lmathlib.c - lbitlib.c - loadlib.c - lauxlib.c - ltablib.c - lcorolib.c - liolib.c + lbaselib.c + lmathlib.c + lbitlib.c + loadlib.c + lauxlib.c + ltablib.c + lcorolib.c + liolib.c lstrlib.c ) @@ -393,6 +394,10 @@ if(DEBUG_YAML) add_definitions(-DDEBUG_YAML) endif() +if(DEBUG_LABELS) + add_definitions(-DDEBUG_LABELS) +endif() + if(FRSKY_STICKS) add_definitions(-DFRSKY_STICKS) endif() diff --git a/radio/src/bitmaps/480x272/default_theme/mask_sort_alpha_down.png b/radio/src/bitmaps/480x272/default_theme/mask_sort_alpha_down.png new file mode 100644 index 000000000..718486eb7 Binary files /dev/null and b/radio/src/bitmaps/480x272/default_theme/mask_sort_alpha_down.png differ diff --git a/radio/src/bitmaps/480x272/default_theme/mask_sort_alpha_up.png b/radio/src/bitmaps/480x272/default_theme/mask_sort_alpha_up.png new file mode 100644 index 000000000..b736e8752 Binary files /dev/null and b/radio/src/bitmaps/480x272/default_theme/mask_sort_alpha_up.png differ diff --git a/radio/src/bitmaps/480x272/default_theme/mask_sort_date_down.png b/radio/src/bitmaps/480x272/default_theme/mask_sort_date_down.png new file mode 100644 index 000000000..9cd801d62 Binary files /dev/null and b/radio/src/bitmaps/480x272/default_theme/mask_sort_date_down.png differ diff --git a/radio/src/bitmaps/480x272/default_theme/mask_sort_date_up.png b/radio/src/bitmaps/480x272/default_theme/mask_sort_date_up.png new file mode 100644 index 000000000..a87ab486b Binary files /dev/null and b/radio/src/bitmaps/480x272/default_theme/mask_sort_date_up.png differ diff --git a/radio/src/dataconstants.h b/radio/src/dataconstants.h index 489f9a3be..3a685baa3 100644 --- a/radio/src/dataconstants.h +++ b/radio/src/dataconstants.h @@ -37,6 +37,9 @@ #define LUA_EXPORT_EXTRA(...) #endif +#define LABELS_LENGTH 100 // Maximum length of the label string +#define LABEL_LENGTH 20 // Maximum length of a single label + #if defined(PCBHORUS) || defined(PCBNV14) #define MAX_MODELS 60 #define MAX_OUTPUT_CHANNELS 32 // number of real output channels CH1-CH32 diff --git a/radio/src/datastructs.h b/radio/src/datastructs.h index 25fe13ed5..aecf7274b 100644 --- a/radio/src/datastructs.h +++ b/radio/src/datastructs.h @@ -83,7 +83,7 @@ static inline void check_struct() CHKSIZE(FlightModeData, 44); CHKSIZE(TimerData, 17); CHKSIZE(SwashRingData, 8); - CHKSIZE(ModelHeader, 31); + CHKSIZE(ModelHeader, 131); CHKSIZE(CurveHeader, 4); CHKSIZE(CustomScreenData, 850); CHKTYPE(TopBarPersistentData, 300); @@ -137,14 +137,14 @@ static inline void check_struct() #elif defined(PCBHORUS) #if defined(PCBX10) CHKSIZE(RadioData, 924); - CHKSIZE(ModelData, 11028); + CHKSIZE(ModelData, 11128); #else CHKSIZE(RadioData, 906); - CHKSIZE(ModelData, 11026); + CHKSIZE(ModelData, 11126); #endif #elif defined(PCBNV14) CHKSIZE(RadioData, 852); - CHKSIZE(ModelData, 10842); + CHKSIZE(ModelData, 10942); #endif #undef CHKSIZE diff --git a/radio/src/datastructs_private.h b/radio/src/datastructs_private.h index 22a3657d9..8d8f15e71 100644 --- a/radio/src/datastructs_private.h +++ b/radio/src/datastructs_private.h @@ -550,6 +550,9 @@ PACK(struct ModelHeader { char name[LEN_MODEL_NAME]; // must be first for eeLoadModelName uint8_t modelId[NUM_MODULES]; MODEL_HEADER_BITMAP_FIELD +#if defined(STORAGE_MODELSLIST) + char labels[LABELS_LENGTH]; +#endif }); #if defined(COLORLCD) diff --git a/radio/src/debug.h b/radio/src/debug.h index d7074ba69..72030e97c 100644 --- a/radio/src/debug.h +++ b/radio/src/debug.h @@ -389,6 +389,7 @@ enum DebugTimers { debugTimerAudioIterval, debugTimerAudioDuration, debugTimerAudioConsume, + debugTimerYamlScan, DEBUG_TIMERS_COUNT }; diff --git a/radio/src/gui/colorlcd/CMakeLists.txt b/radio/src/gui/colorlcd/CMakeLists.txt index 3a66af71b..8466fdccf 100644 --- a/radio/src/gui/colorlcd/CMakeLists.txt +++ b/radio/src/gui/colorlcd/CMakeLists.txt @@ -67,6 +67,7 @@ set(GUI_SRC model_outputs.cpp model_curves.cpp model_gvars.cpp + model_templates.cpp model_logical_switches.cpp special_functions.cpp model_telemetry.cpp diff --git a/radio/src/gui/colorlcd/draw_functions.cpp b/radio/src/gui/colorlcd/draw_functions.cpp index 97f8d8f99..15bedead4 100644 --- a/radio/src/gui/colorlcd/draw_functions.cpp +++ b/radio/src/gui/colorlcd/draw_functions.cpp @@ -595,8 +595,8 @@ void drawTextLines(BitmapBuffer * dc, coord_t left, coord_t top, coord_t width, case '(': case '{': case '[': - case ' ': nxt++; + case ' ': case '\n': case '\0': done = true; diff --git a/radio/src/gui/colorlcd/listbox.cpp b/radio/src/gui/colorlcd/listbox.cpp index 7f7c54ac6..4a9570128 100644 --- a/radio/src/gui/colorlcd/listbox.cpp +++ b/radio/src/gui/colorlcd/listbox.cpp @@ -107,6 +107,18 @@ void ListBase::setSelected(int selected) select(selected, 0); } +void ListBase::setSelected(std::set selected) +{ + if(!multiSelect) return; + + for(int i=0; i < getRowCount(); i++) { + if(selected.find(i) != selected.end()) + lv_table_add_cell_ctrl(lvobj, i, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); + else + lv_table_clear_cell_ctrl(lvobj, i, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); + } +} + int ListBase::getSelected() const { uint16_t row, col; @@ -115,6 +127,17 @@ int ListBase::getSelected() const return -1; } +std::set ListBase::getSelection() +{ + if(!multiSelect) return std::set(); + std::set selectedIndexes; + for(int i=0; i < getRowCount(); i++) { + if(lv_table_has_cell_ctrl(lvobj, i, 0, LV_TABLE_CELL_CTRL_CUSTOM_1)) + selectedIndexes.insert(i); + } + return selectedIndexes; +} + void ListBase::setActiveItem(int item) { if (item != activeItem) { @@ -133,7 +156,20 @@ void ListBase::onPress(uint16_t row, uint16_t col) if (row == LV_TABLE_CELL_NONE) return; TRACE("SHORT_PRESS"); - if (pressHandler) { pressHandler(); } + + if(multiSelect && row < getRowCount()) { + std::set lastSelection = getSelection(); + + bool chk = lv_table_has_cell_ctrl(lvobj, row, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); + if(chk) lv_table_clear_cell_ctrl(lvobj, row, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); + else lv_table_add_cell_ctrl(lvobj, row, 0, LV_TABLE_CELL_CTRL_CUSTOM_1); + + if(_multiSelectHandler) { + _multiSelectHandler(getSelection(),lastSelection); + } + } else { + if (pressHandler) { pressHandler(); } + } } void ListBase::onLongPressed() @@ -147,6 +183,7 @@ void ListBase::onClicked() { if (!getAutoEdit()) { lv_group_set_editing((lv_group_t*)lv_obj_get_group(lvobj), true); + } else { TableField::onClicked(); } @@ -164,7 +201,9 @@ void ListBase::onCancel() void ListBase::onDrawEnd(uint16_t row, uint16_t col, lv_obj_draw_part_dsc_t* dsc) { - if (row != activeItem) return; + if ((multiSelect == false && row != activeItem) || + (multiSelect == true && !lv_table_has_cell_ctrl(lvobj, dsc->id, 0, LV_TABLE_CELL_CTRL_CUSTOM_1))) + return; lv_area_t coords; lv_coord_t area_h = lv_area_get_height(dsc->draw_area); diff --git a/radio/src/gui/colorlcd/listbox.h b/radio/src/gui/colorlcd/listbox.h index 5bc981749..c4fc4c290 100644 --- a/radio/src/gui/colorlcd/listbox.h +++ b/radio/src/gui/colorlcd/listbox.h @@ -35,6 +35,7 @@ class ListBase : public TableField { std::function longPressHandler = nullptr; std::function pressHandler = nullptr; + std::function, std::set)> _multiSelectHandler = nullptr; bool autoEdit = false; public: @@ -49,11 +50,23 @@ class ListBase : public TableField void setLineHeight(uint8_t height); virtual void setSelected(int selected); + virtual void setSelected(std::set selected); + int getSelected() const; + std::set getSelection(); + + void setMultiSelect(bool mode) { + multiSelect = mode; + } virtual void setActiveItem(int item); int getActiveItem() const; - + + void setMultiSelectHandler(std::function, std::set)> handler) + { + _multiSelectHandler = std::move(handler); + } + void setLongPressHandler(std::function handler) { longPressHandler = std::move(handler); @@ -71,6 +84,7 @@ class ListBase : public TableField protected: static void event_cb(lv_event_t* e); int activeItem = -1; + bool multiSelect = false; void onPress(uint16_t row, uint16_t col) override; void onLongPressed(); diff --git a/radio/src/gui/colorlcd/model_select.cpp b/radio/src/gui/colorlcd/model_select.cpp index 24829f792..8bd11d91a 100644 --- a/radio/src/gui/colorlcd/model_select.cpp +++ b/radio/src/gui/colorlcd/model_select.cpp @@ -19,300 +19,184 @@ * GNU General Public License for more details. */ -#include #include "model_select.h" -#include "opentx.h" -#include "storage/modelslist.h" + +#include +#include +#include + #include "libopenui.h" +#include "listbox.h" +#include "model_templates.h" +#include "opentx.h" #include "standalone_lua.h" #include "str_functions.h" -#if LCD_W > LCD_H -constexpr int MODEL_CELLS_PER_LINE = 3; -#else -constexpr int MODEL_CELLS_PER_LINE = 2; -#endif -constexpr coord_t MODEL_CELL_PADDING = 6; -constexpr coord_t MODEL_SELECT_CELL_WIDTH = - (LCD_W - (MODEL_CELLS_PER_LINE + 1) * MODEL_CELL_PADDING) / - MODEL_CELLS_PER_LINE; -constexpr coord_t MODEL_SELECT_CELL_HEIGHT = 92; -// constexpr coord_t MODEL_IMAGE_WIDTH = MODEL_SELECT_CELL_WIDTH; -// constexpr coord_t MODEL_IMAGE_HEIGHT = 72; -constexpr size_t LEN_INFO_TEXT = 300; -constexpr size_t LEN_PATH = sizeof(TEMPLATES_PATH) + TEXT_FILENAME_MAXLEN; -constexpr size_t LEN_BUFFER = sizeof(TEMPLATES_PATH) + 2 * TEXT_FILENAME_MAXLEN + 1; +// bitmaps for toolbar +const uint8_t _mask_sort_alpha_up[] = { +#include "mask_sort_alpha_up.lbm" +}; +STATIC_LZ4_BITMAP(mask_sort_alpha_up); + +const uint8_t _mask_sort_alpha_down[] = { +#include "mask_sort_alpha_down.lbm" +}; +STATIC_LZ4_BITMAP(mask_sort_alpha_down); + +const uint8_t _mask_sort_date_up[] = { +#include "mask_sort_date_up.lbm" +}; +STATIC_LZ4_BITMAP(mask_sort_date_up); + +const uint8_t _mask_sort_date_down[] = { +#include "mask_sort_date_down.lbm" +}; +STATIC_LZ4_BITMAP(mask_sort_date_down); + inline tmr10ms_t getTicks() { return g_tmr10ms; } -class TemplatePage : public Page -{ - public: +constexpr coord_t MODEL_CELL_PADDING = 6; +constexpr coord_t MODEL_SELECT_CELL_HEIGHT = 92; +constexpr int BUTTONS_HEIGHT = 30; +constexpr int MODEL_CELLS_PER_LINE = 2; - TemplatePage() : Page(ICON_MODEL_SELECT) - { } - - void updateInfo() +#if LCD_W > LCD_H // Landscape +constexpr LcdFlags textFont = FONT(STD); +constexpr int LABELS_WIDTH = 132; +constexpr int LAY_MARGIN = 5; +constexpr coord_t MODEL_SELECT_CELL_WIDTH = + (LCD_W - LABELS_WIDTH - + (MODEL_CELLS_PER_LINE + 1) * MODEL_CELL_PADDING) / + MODEL_CELLS_PER_LINE; +#else // Portrait +constexpr LcdFlags textFont = FONT(XS); +constexpr int LAY_MARGIN = 8; +constexpr int LABELS_HEIGHT = 140; +constexpr coord_t MODEL_SELECT_CELL_WIDTH = + (LCD_W - LAY_MARGIN - (MODEL_CELLS_PER_LINE + 1) * MODEL_CELL_PADDING) / + MODEL_CELLS_PER_LINE; +#endif + +class ToolbarButton : public Button +{ + public: + ToolbarButton(FormGroup *parent, const rect_t &rect, const uint8_t *bitmap, + std::function pressHandler = nullptr) : + Button(parent, rect, pressHandler, 0), _bitmap(bitmap) { - FIL fp; - FRESULT res = f_open (&fp, buffer, FA_READ); - unsigned int bytesRead = 0; - if (res == FR_OK) { - f_read (&fp, infoText, LEN_INFO_TEXT, &bytesRead); - f_close(&fp); - } - infoText[bytesRead] = '\0'; + lv_obj_set_style_border_width(lvobj, lv_dpx(2), 0); + lv_obj_set_style_border_opa(lvobj, LV_OPA_TRANSP, 0); + + lv_obj_set_style_border_color(lvobj, makeLvColor(COLOR_THEME_FOCUS), LV_STATE_FOCUSED); + lv_obj_set_style_border_opa(lvobj, LV_OPA_100, LV_STATE_FOCUSED); + } + + inline bool getSelected() { return _selected; } + void setSelected(bool selected) + { + _selected = selected; invalidate(); } -#if defined(HARDWARE_KEYS) - void onEvent(event_t event) override + void setBitmap(const uint8_t *bitmap) { - if (event == EVT_KEY_LONG(KEY_EXIT) || event == EVT_KEY_BREAK(KEY_EXIT)) { - killEvents(event); - deleteLater(); - } else { - Page::onEvent(event); - } + _bitmap = bitmap; + invalidate(); } -#endif -#if defined(DEBUG_WINDOWS) - std::string getName() const { return "TemplatePage"; } -#endif - - protected: - - static char path[LEN_PATH + 1]; - char buffer[LEN_BUFFER + 1]; - char infoText[LEN_INFO_TEXT + 1] = { 0 }; - unsigned int count = 0; - static std::function update; void paint(BitmapBuffer *dc) override { - Page::paint(dc); - rect_t rect = body.getRect(); - coord_t x = LCD_W / 2 + PAGE_PADDING; - coord_t y = rect.y + PAGE_PADDING; - coord_t w = LCD_W / 2 - 2 * PAGE_PADDING; - coord_t h = rect.h - 2 * PAGE_PADDING; - if (count > 0) { - if (infoText[0] == '\0') - drawTextLines(dc, x, y, w, h, STR_NO_INFORMATION, COLOR_THEME_DISABLED); - else - drawTextLines(dc, x, y, w, h, infoText, COLOR_THEME_PRIMARY1); - } + int width; + uint32_t bgColor = + !_selected ? COLOR_THEME_SECONDARY3 : COLOR_THEME_SECONDARY2; + auto bm = getBitmap(_bitmap, bgColor, COLOR_THEME_PRIMARY1, &width); + dc->drawScaledBitmap(bm, 2, 2, this->width() - 4, this->height() - 4); + delete bm; + } + + protected: + const uint8_t *_bitmap; + bool _selected = false; + + BitmapBuffer *getBitmap(const uint8_t *maskData, uint32_t bgColor, + uint32_t fgColor, int *width) + { + auto mask = BitmapBuffer::load8bitMask(maskData); + BitmapBuffer *newBm = + new BitmapBuffer(BMP_RGB565, mask->width(), mask->height()); + newBm->clear(bgColor); + newBm->drawMask(0, 0, mask, fgColor); + delete mask; + return newBm; } }; -char TemplatePage::path[LEN_PATH + 1]; -std::function TemplatePage::update = nullptr; - -class TemplateButton : public TextButton -{ - public: - TemplateButton(FormGroup* parent, const rect_t& rect, std::string name, std::function pressHandler = nullptr) - : TextButton(parent, rect, name, pressHandler) - { } -}; - -class SelectTemplate : public TemplatePage +class ButtonHolder : public FormWindow { - public: - SelectTemplate(TemplatePage* tp) - : templateFolderPage(tp) + public: + struct ButtonInfo { + ToolbarButton *button; + const uint8_t *states[2]; + int sortState; + }; + + ButtonHolder(Window *parent, const rect_t &rect) : FormWindow(parent, rect) { - rect_t rect = {PAGE_TITLE_LEFT, PAGE_TITLE_TOP + 10, LCD_W - PAGE_TITLE_LEFT, PAGE_LINE_HEIGHT}; - new StaticText(&header, rect, STR_SELECT_TEMPLATE, 0, COLOR_THEME_PRIMARY2); + setHeight(25); + addButton(mask_sort_alpha_up, mask_sort_alpha_down); + addButton(mask_sort_date_up, mask_sort_date_down); - FormGridLayout grid; - grid.spacer(PAGE_PADDING); - std::list files; - FILINFO fno; - DIR dir; - FRESULT res = f_opendir(&dir, path); + _buttons[0].button->setSelected(true); + } - if (res == FR_OK) { - // read all entries - bool firstTime = true; - for (;;) { - res = sdReadDir(&dir, &fno, firstTime); - firstTime = false; - if (res != FR_OK || fno.fname[0] == 0) - break; // Break on error or end of dir - if (strlen((const char*)fno.fname) > SD_SCREEN_FILE_LENGTH) - continue; - if (fno.fname[0] == '.') - continue; - if (!(fno.fattrib & AM_DIR)) { - const char *ext = getFileExtension(fno.fname); - if(ext && !strcasecmp(ext, YAML_EXT)) { - int len = ext - fno.fname; - if (len < FF_MAX_LFN) { - char name[FF_MAX_LFN] = { 0 }; - strncpy(name, fno.fname, len); - files.push_back(name); - } - } + inline void setPressHandler( + std::function pressHandler) + { + _pressHandler = std::move(pressHandler); + } + + void addButton(const uint8_t *state1Bm, const uint8_t *state2Bm) + { + int buttonNumber = _buttons.size(); + auto tb = new ToolbarButton( + this, {buttonNumber * (height() + 4), 0, height(), height()}, state1Bm); + tb->setPressHandler([=]() { + bool isSelected = tb->getSelected(); + + ButtonInfo *b = nullptr; + int buttonIndex = -1; + for (int i = 0; i < (int)_buttons.size(); i++) { + _buttons[i].button->setSelected(false); + if (tb == _buttons[i].button) { + b = &_buttons[i]; + buttonIndex = i; } } + tb->setSelected(true); - files.sort(compare_nocase); + if (isSelected && b != nullptr) { + if (b->sortState == 0) + b->sortState = 1; + else + b->sortState = 0; - for (auto name: files) { - auto tb = new TemplateButton(&body, grid.getLabelSlot(), name, [=]() -> uint8_t { - // Read model template - loadModelTemplate((name + YAML_EXT).c_str(), path); - storageDirty(EE_MODEL); - storageCheck(true); - // Dismiss template pages - deleteLater(); - templateFolderPage->deleteLater(); -#if defined(LUA) - // If there is a wizard Lua script, fire it up - snprintf(buffer, LEN_BUFFER, "%s%c%s%s", path, '/', name.c_str(), SCRIPT_EXT); - if (f_stat(buffer, 0) == FR_OK) { - luaExec(buffer); - // Need to update() the ModelCategoryPageBody before attaching StandaloneLuaWindow to not mess up focus - update(); - update = nullptr; - StandaloneLuaWindow::instance()->attach(); - } -#endif - return 0; - }); - tb->setFocusHandler([=](bool active) { - if (active) { - snprintf(buffer, LEN_BUFFER, "%s%c%s%s", path, '/', name.c_str(), TEXT_EXT); - updateInfo(); - } - }); - tb->setHeight(PAGE_LINE_HEIGHT * 2); - grid.spacer(tb->height() + 5); + tb->setBitmap(b->states[b->sortState]); } - } - - f_closedir(&dir); - count = files.size(); - if (count == 0) { - rect_t rect = body.getRect(); - rect.x = PAGE_PADDING; - rect.y = PAGE_PADDING; - rect.w = rect.w - (2 * PAGE_PADDING); -#if LCD_W > LCD_H - rect.h = PAGE_LINE_HEIGHT; - int charBreak = 60; -#else - rect.h = PAGE_LINE_HEIGHT * 2; - int charBreak = 40; -#endif - new StaticText(&body, rect, wrap(STR_NO_TEMPLATES, charBreak), 0, COLOR_THEME_PRIMARY1); + if (_pressHandler != nullptr && b != nullptr) + _pressHandler(buttonIndex, b); - // The following button is needed because the EXIT key does not work without... - rect = body.getRect(); - rect.x = rect.w - PAGE_PADDING - 100; - rect.y = rect.h - PAGE_PADDING - (PAGE_LINE_HEIGHT * 2); - rect.w = 100; - rect.h = PAGE_LINE_HEIGHT * 2; - new TextButton(&body, rect, STR_EXIT, [=]() -> uint8_t { deleteLater(); return 0; }); - } else { - snprintf(buffer, LEN_BUFFER, "%s%c%s%s", path, '/', files.front().c_str(), TEXT_EXT); - updateInfo(); - } - } - - protected: - TemplatePage* templateFolderPage; -}; - -class SelectTemplateFolder : public TemplatePage -{ - public: - SelectTemplateFolder(std::function update) - { - TemplatePage::update = update; - rect_t rect = {PAGE_TITLE_LEFT, PAGE_TITLE_TOP + 10, LCD_W - PAGE_TITLE_LEFT, PAGE_LINE_HEIGHT}; - new StaticText(&header, rect, STR_SELECT_TEMPLATE_FOLDER, 0, COLOR_THEME_PRIMARY2); - - FormGridLayout grid; - grid.spacer(PAGE_PADDING); - - auto tfb = new TemplateButton(&body, grid.getLabelSlot(), STR_BLANK_MODEL, [=]() -> uint8_t { - deleteLater(); return 0; }); - snprintf(infoText, LEN_INFO_TEXT, "%s", STR_BLANK_MODEL_INFO); - tfb->setFocusHandler([=](bool active) { - if (active) { - snprintf(infoText, LEN_INFO_TEXT, "%s", STR_BLANK_MODEL_INFO); - invalidate(); - } - }); - tfb->setHeight(PAGE_LINE_HEIGHT * 2); - grid.spacer(tfb->height() + 5); - std::list directories; - FILINFO fno; - DIR dir; - FRESULT res = f_opendir(&dir, TEMPLATES_PATH); + ButtonInfo bi = {tb, state1Bm, state2Bm, 0}; - if (res == FR_OK) { - // read all entries - bool firstTime = true; - for (;;) { - res = sdReadDir(&dir, &fno, firstTime); - firstTime = false; - if (res != FR_OK || fno.fname[0] == 0) - break; // Break on error or end of dir - if (strlen((const char*)fno.fname) > SD_SCREEN_FILE_LENGTH) - continue; - if (fno.fname[0] == '.') - continue; - if (fno.fattrib & AM_DIR) - directories.push_back((char*)fno.fname); - } - - directories.sort(compare_nocase); - - for (auto name: directories) { -#if not defined(LUA) - // Don't show wizards dir if no lua - if (!strcasecmp(name.c_str(), "WIZARD") == 0) { -#endif - auto tfb = new TemplateButton(&body, grid.getLabelSlot(), name, - [=]() -> uint8_t { - snprintf(path, LEN_PATH, "%s%c%s", TEMPLATES_PATH, '/', name.c_str()); - new SelectTemplate(this); - return 0; - }); - tfb->setFocusHandler([=](bool active) { - if (active) { - snprintf(buffer, LEN_BUFFER, "%s%c%s%c%s%s", TEMPLATES_PATH, '/', name.c_str(), '/', "about", TEXT_EXT); - updateInfo(); - } - }); - tfb->setHeight(PAGE_LINE_HEIGHT * 2); - grid.spacer(tfb->height() + 5); - } -#if not defined(LUA) - } -#endif - } - - f_closedir(&dir); - count = directories.size(); - if (count == 0) { - rect_t rect = grid.getLabelSlot(); - rect.w = body.getRect().w - 2 * PAGE_PADDING; - new StaticText(&body, rect, STR_NO_TEMPLATES, 0, COLOR_THEME_PRIMARY1); - } + _buttons.push_back(bi); } - ~SelectTemplateFolder() - { - if (update) - update(); - } + protected: + std::vector _buttons; + std::function _pressHandler; }; class ModelButton : public Button @@ -323,67 +207,19 @@ class ModelButton : public Button { setWidth(MODEL_SELECT_CELL_WIDTH); setHeight(MODEL_SELECT_CELL_HEIGHT); - - load(); } ~ModelButton() { - if (buffer) { delete buffer; } + if (buffer) { + delete buffer; + } } - + void load() { -#if defined(SDCARD_RAW) - uint8_t version; -#endif - - PACK(struct { - ModelHeader header; - TimerData timers[MAX_TIMERS]; - }) - partialModel; const char *error = nullptr; - if (strncmp(modelCell->modelFilename, g_eeGeneral.currModelFilename, - LEN_MODEL_FILENAME) == 0) { - memcpy(&partialModel.header, &g_model.header, sizeof(partialModel)); -#if defined(SDCARD_RAW) - version = EEPROM_VER; -#endif - } else { -#if defined(SDCARD_RAW) - error = - readModelBin(modelCell->modelFilename, (uint8_t *)&partialModel.header, - sizeof(partialModel), &version); -#else - error = readModel(modelCell->modelFilename, - (uint8_t *)&partialModel.header, sizeof(partialModel)); -#endif - } - - if (!error) { - if (modelCell->modelName[0] == '\0' && - partialModel.header.name[0] != '\0') { - -#if defined(SDCARD_RAW) - if (version == 219) { - int len = (int)sizeof(partialModel.header.name); - char* str = partialModel.header.name; - for (int i=0; i < len; i++) { - str[i] = zchar2char(str[i]); - } - // Trim string - while(len > 0 && str[len-1]) { - if (str[len - 1] != ' ' && str[len - 1] != '\0') break; - str[--len] = '\0'; - } - } -#endif - modelCell->setModelName(partialModel.header.name); - } - } - delete buffer; buffer = new BitmapBuffer(BMP_RGB565, width(), height()); if (buffer == nullptr) { @@ -392,10 +228,10 @@ class ModelButton : public Button buffer->clear(COLOR_THEME_PRIMARY2); if (error) { - buffer->drawText(width() / 2, 2, "(Invalid Model)", + buffer->drawText(width() / 2, height() / 2, "(Invalid Model)", COLOR_THEME_SECONDARY1 | CENTERED); } else { - GET_FILENAME(filename, BITMAPS_PATH, partialModel.header.bitmap, ""); + GET_FILENAME(filename, BITMAPS_PATH, modelCell->modelBitmap, ""); const BitmapBuffer *bitmap = BitmapBuffer::loadBitmap(filename); if (bitmap) { buffer->drawScaledBitmap(bitmap, 0, 0, width(), height()); @@ -409,29 +245,25 @@ class ModelButton : public Button void paint(BitmapBuffer *dc) override { + if (!loaded) { // Load them on the fly + load(); + loaded = true; + } FormField::paint(dc); - if (buffer) - dc->drawBitmap(0, 0, buffer); + if (buffer) dc->drawBitmap(0, 0, buffer); if (modelCell == modelslist.getCurrentModel()) { dc->drawSolidFilledRect(0, 0, width(), 20, COLOR_THEME_ACTIVE); - dc->drawSizedText(width() / 2, 2, modelCell->modelName, - LEN_MODEL_NAME, + dc->drawSizedText(width() / 2, 2, modelCell->modelName, LEN_MODEL_NAME, COLOR_THEME_SECONDARY1 | CENTERED); } else { LcdFlags textColor; - // if (hasFocus()) { - // dc->drawFilledRect(0, 0, width(), 20, SOLID, COLOR_THEME_FOCUS, 5); - // textColor = COLOR_THEME_PRIMARY2; - // } - // else { - dc->drawFilledRect(0, 0, width(), 20, SOLID, COLOR_THEME_PRIMARY2, 5); - textColor = COLOR_THEME_SECONDARY1; - // } + dc->drawFilledRect(0, 0, width(), 20, SOLID, COLOR_THEME_PRIMARY2); - dc->drawSizedText(width() / 2, 2, modelCell->modelName, - LEN_MODEL_NAME, + textColor = COLOR_THEME_SECONDARY1; + + dc->drawSizedText(width() / 2, 2, modelCell->modelName, LEN_MODEL_NAME, textColor | CENTERED); } @@ -442,336 +274,550 @@ class ModelButton : public Button } } - const char* modelFilename() { return modelCell->modelFilename; } - ModelCell* getModelCell() const { return modelCell; } + const char *modelFilename() { return modelCell->modelFilename; } + ModelCell *getModelCell() const { return modelCell; } protected: + bool loaded = false; ModelCell *modelCell; BitmapBuffer *buffer = nullptr; }; -static void _saveTemplate(ModelCell* model) -{ - storageDirty(EE_MODEL); - storageCheck(true); - constexpr size_t size = sizeof(model->modelName) + sizeof(YAML_EXT); - char modelName[size]; - snprintf(modelName, size, "%s%s", model->modelName, YAML_EXT); - char templatePath[FF_MAX_LFN]; - snprintf(templatePath, FF_MAX_LFN, "%s%c%s", PERS_TEMPL_PATH, '/', modelName); - sdCheckAndCreateDirectory(TEMPLATES_PATH); - sdCheckAndCreateDirectory(PERS_TEMPL_PATH); - if (isFileAvailable(templatePath)) { - new ConfirmDialog(Layer::back(), STR_FILE_EXISTS, STR_ASK_OVERWRITE, [=] { - sdCopyFile(model->modelFilename, MODELS_PATH, modelName, PERS_TEMPL_PATH); - }); - } else { - sdCopyFile(model->modelFilename, MODELS_PATH, modelName, PERS_TEMPL_PATH); - } -} +//----------------------------------------------------------------------------- -class ModelCategoryPageBody : public FormWindow +class MyMenu : public Menu { public: - ModelCategoryPageBody(Window* parent, ModelsCategory* category) : - FormWindow(parent, rect_t{}), category(category) + using Menu::Menu; + void setFinishHandler(std::function finishHandler) { - setFlexLayout(LV_FLEX_FLOW_ROW_WRAP, MODEL_CELL_PADDING); - lv_obj_set_style_pad_row(lvobj, MODEL_CELL_PADDING, 0); - lv_obj_set_style_pad_all(lvobj, MODEL_CELL_PADDING, 0); + _finishHandler = std::move(finishHandler); + } - for (auto &model : *category) - addModelButton(model); + void deleteLater(bool detach = true, bool trash = true) override + { + if (_finishHandler != nullptr) { + _finishHandler(); + } + Menu::deleteLater(detach, trash); } protected: - ModelsCategory *category; + std::function _finishHandler = nullptr; +}; - void addModelButton(ModelCell *model) - { - auto btn = new ModelButton(this, rect_t{}, model); - btn->setPressHandler([=]() -> uint8_t { - modelMenu(btn); +//----------------------------------------------------------------------------- + +ModelsPageBody::ModelsPageBody(Window *parent, const rect_t &rect) : + FormWindow(parent, rect) +{ + setFlexLayout(LV_FLEX_FLOW_ROW_WRAP, MODEL_CELL_PADDING); + padRow(MODEL_CELL_PADDING); + update(); +} + +void ModelsPageBody::selectModel(ModelCell *model) +{ + bool modelConnected = + TELEMETRY_STREAMING() && !g_eeGeneral.disableRssiPoweroffAlarm; + if (modelConnected) { + AUDIO_ERROR_MESSAGE(AU_MODEL_STILL_POWERED); + if (!confirmationDialog(STR_MODEL_STILL_POWERED, nullptr, false, []() { + tmr10ms_t startTime = getTicks(); + while (!TELEMETRY_STREAMING()) { + if (getTicks() - startTime > TELEMETRY_CHECK_DELAY10ms) break; + } + return !TELEMETRY_STREAMING() || g_eeGeneral.disableRssiPoweroffAlarm; + })) { + return; // stop if connected but not confirmed + } + } + + // store changes (if any) and load selected model + storageFlushCurrentModel(); + storageCheck(true); + memcpy(g_eeGeneral.currModelFilename, model->modelFilename, + LEN_MODEL_FILENAME); + + loadModel(g_eeGeneral.currModelFilename, true); + storageDirty(EE_GENERAL); + storageCheck(true); + + modelslist.setCurrentModel(model); + checkAll(); + + // Exit to main view + auto w = Layer::back(); + if (w) w->onCancel(); +} + +void ModelsPageBody::duplicateModel(ModelCell* model) +{ + char duplicatedFilename[LEN_MODEL_FILENAME + 1]; + memcpy(duplicatedFilename, model->modelFilename, + sizeof(duplicatedFilename)); + if (findNextFileIndex(duplicatedFilename, LEN_MODEL_FILENAME, + MODELS_PATH)) { + sdCopyFile(model->modelFilename, MODELS_PATH, duplicatedFilename, + MODELS_PATH); + // 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); + } + update(); + } else { + TRACE("ModelsListError: Invalid File"); + } +} + +void ModelsPageBody::deleteModel(ModelCell *model) +{ + new ConfirmDialog( + parent, STR_DELETE_MODEL, + std::string(model->modelName, sizeof(model->modelName)).c_str(), [=] { + modelslist.removeModel(model); + if (refreshLabels != nullptr) refreshLabels(); + + update(); + }); +} + +void ModelsPageBody::editLabels(ModelCell* model) +{ + auto labels = modelslabels.getLabels(); + + // dont display menu if there will be no labels + if (labels.size()) { + MyMenu *menu = new MyMenu(getParent(), true); + menu->setTitle(model->modelName); + menu->setFinishHandler([=]() { + if (isDirty) { + isDirty = false; + update(); + } + }); + + for (auto &label : modelslabels.getLabels()) { + menu->addLine( + label, + [=]() { + if (!modelslabels.isLabelSelected(label, model)) + modelslabels.addLabelToModel(label, model, true); + else + modelslabels.removeLabelFromModel(label, model, true); + isDirty = true; + if (refreshLabels != nullptr) refreshLabels(); + }, + [=]() { return modelslabels.isLabelSelected(label, model); }); + } + } +} + +void ModelsPageBody::update(int selected) +{ + clear(); + + ModelsVector models; + if (selectedLabels.size()) { + models = modelslabels.getModelsInLabels(selectedLabels); + } else { + models = modelslabels.getAllModels(); + } + + for (auto &model : models) { + auto button = new ModelButton(this, rect_t{}, model); + button->setPressHandler([=]() -> uint8_t { return 1; }); + + // Long Press Handler for Models + button->setLongPressHandler([=]() -> uint8_t { + Menu *menu = new Menu(this); + menu->setTitle(model->modelName); + if (model != modelslist.getCurrentModel()) { + menu->addLine(STR_SELECT_MODEL, [=]() { selectModel(model); }); + } + menu->addLine(STR_DUPLICATE_MODEL, [=]() { duplicateModel(model); }); + if (model != modelslist.getCurrentModel()) { + menu->addLine(STR_DELETE_MODEL, [=]() { deleteModel(model); }); + } + menu->addLine(STR_EDIT_LABELS, [=]() { editLabels(model); }); return 0; }); } +} - void modelMenu(ModelButton* btn) - { - Menu *menu = new Menu(parent); - ModelCell* model = btn->getModelCell(); - if (model != modelslist.getCurrentModel()) { - menu->addLine(STR_SELECT_MODEL, [=]() { selectModel(btn); }); - } - menu->addLine(STR_CREATE_MODEL, [=]() { createModel(); }); - menu->addLine(STR_DUPLICATE_MODEL, [=]() { duplicateModel(model); }); - menu->addLine(STR_SAVE_TEMPLATE, std::bind(_saveTemplate, model)); - if (model != modelslist.getCurrentModel()) { - // Move - if(modelslist.getCategories().size() > 1) { - menu->addLine(STR_MOVE_MODEL, [=]() { moveModel(btn); }); - } - menu->addLine(STR_DELETE_MODEL, [=]() { deleteModel(btn); }); - } - } - - void selectModel(ModelButton* btn) - { - bool modelConnected = - TELEMETRY_STREAMING() && !g_eeGeneral.disableRssiPoweroffAlarm; - if (modelConnected) { - AUDIO_ERROR_MESSAGE(AU_MODEL_STILL_POWERED); - if (!confirmationDialog(STR_MODEL_STILL_POWERED, nullptr, false, []() { - tmr10ms_t startTime = getTicks(); - while (!TELEMETRY_STREAMING()) { - if (getTicks() - startTime > TELEMETRY_CHECK_DELAY10ms) break; - } - return !TELEMETRY_STREAMING() || - g_eeGeneral.disableRssiPoweroffAlarm; - })) { - return; // stop if connected but not confirmed - } - } +//----------------------------------------------------------------------------- - // store changes (if any) and load selected model - storageFlushCurrentModel(); - storageCheck(true); - memcpy(g_eeGeneral.currModelFilename, btn->modelFilename(), - LEN_MODEL_FILENAME); - - loadModel(g_eeGeneral.currModelFilename, true); - storageDirty(EE_GENERAL); - storageCheck(true); - - modelslist.setCurrentModel(btn->getModelCell()); - modelslist.setCurrentCategory(category); - - // Exit to main view - auto w = Layer::back(); - if (w) w->onCancel(); - } - - void createModel() - { - storageCheck(true); - auto model = modelslist.addModel(category, ::createModel(), nullptr, false); - modelslist.setCurrentModel(model); - new SelectTemplateFolder([=]() { - model->setModelName(g_model.header.name); - modelslist.save(); - addModelButton(model); - }); - } - - void duplicateModel(ModelCell* model) - { - char duplicatedFilename[LEN_MODEL_FILENAME + 1]; - memcpy(duplicatedFilename, model->modelFilename, - sizeof(duplicatedFilename)); - if (findNextFileIndex(duplicatedFilename, LEN_MODEL_FILENAME, - MODELS_PATH)) { - sdCopyFile(model->modelFilename, MODELS_PATH, duplicatedFilename, - MODELS_PATH); - auto new_model = modelslist.addModel(category, duplicatedFilename, model->modelName); - addModelButton(new_model); - } else { - POPUP_WARNING("Invalid File"); // TODO: translation - } - } - - void deleteModel(ModelButton* btn) - { - ModelCell* model = btn->getModelCell(); - new ConfirmDialog( - parent, STR_DELETE_MODEL, - std::string(model->modelName, sizeof(model->modelName)).c_str(), [=] { - modelslist.removeModel(category, model); - btn->deleteLater(); - }); - } - - void moveModel(ModelButton* btn) - { - auto moveToMenu = new Menu(parent); - moveToMenu->setTitle(STR_MOVE_MODEL); - - auto model = btn->getModelCell(); - for (auto newcategory : modelslist.getCategories()) { - if (category != newcategory) { - moveToMenu->addLine( - std::string(newcategory->name, sizeof(newcategory->name)), [=]() { - modelslist.moveModel(model, category, newcategory); - modelslist.save(); - btn->deleteLater(); - }); - } - } - } - - void addFirstModel() { - Menu *menu = new Menu(this); - menu->addLine(STR_CREATE_MODEL, [=]() { createModel(); }); - } - - void onClicked() override - { - if (category->size() == 0) addFirstModel(); - } -}; - -class ModelCategoryPage : public PageTab +class LabelDialog : public Dialog { public: - explicit ModelCategoryPage(ModelsCategory *category) : - PageTab(category->name, ICON_MODEL_SELECT_CATEGORY), category(category) + LabelDialog(Window *parent, char *label, + std::function _saveHandler = nullptr) : + Dialog(parent, STR_ENTER_LABEL, rect_t{}), + saveHandler(std::move(_saveHandler)) { + strncpy(this->label, label, MAX_LABEL_SIZE); + this->label[MAX_LABEL_SIZE] = '\0'; + + auto form = &content->form; + form->padRow(lv_dpx(8)); + + auto form_obj = form->getLvObj(); + lv_obj_set_style_flex_cross_place(form_obj, LV_FLEX_ALIGN_CENTER, 0); + + new TextEdit(form, rect_t{}, label, MAX_LABEL_SIZE); + + auto box = new FormGroup(form, rect_t{}); + box->setFlexLayout(LV_FLEX_FLOW_ROW); + + auto box_obj = box->getLvObj(); + lv_obj_set_style_flex_main_place(box_obj, LV_FLEX_ALIGN_SPACE_EVENLY, 0); + + auto btn = new TextButton( + box, rect_t{}, STR_SAVE, + [=]() { + if (saveHandler != nullptr) saveHandler(label); + deleteLater(); + return 0; + }, + BUTTON_BACKGROUND | OPAQUE, textFont); + btn->setWidth(LV_DPI_DEF); + + btn = new TextButton( + box, rect_t{}, STR_CANCEL, + [=]() { + deleteLater(); + return 0; + }, + BUTTON_BACKGROUND | OPAQUE, textFont); + btn->setWidth(LV_DPI_DEF); + + content->setWidth(LCD_W * 0.8); + content->updateSize(); } protected: - ModelsCategory *category; - - void build(FormWindow *window) override - { - window->padAll(0); - new ModelCategoryPageBody(window, category); - } + std::function saveHandler; + char label[MAX_LABEL_SIZE + 1]; }; -class CategoryGroup: public FormGroup +//----------------------------------------------------------------------------- + +ModelLabelsWindow::ModelLabelsWindow() : Page(ICON_MODEL) { - public: - CategoryGroup(Window * parent, const rect_t & rect, ModelsCategory *category) : - FormGroup(parent, rect), - category(category) - { - } + buildBody(&body); + buildHead(&header); - void paint(BitmapBuffer * dc) override - { - dc->drawSolidFilledRect(0, 0, width(), height(), COLOR_THEME_PRIMARY2); - FormGroup::paint(dc); - } - - protected: - ModelsCategory *category; -}; - -class CategoryEditPage : public PageTab -{ - public: - explicit CategoryEditPage(ModelSelectMenu *modelselectmenu, bool scrolltobot=false) : - - PageTab(STR_MODEL_CATEGORIES, ICON_MODEL_SETUP), - modelselectmenu(modelselectmenu), - scrolltobot(scrolltobot) - { - } - - protected: - void update() - { - modelselectmenu->build(0); - } - - void build(FormWindow *window) override - { - FormGridLayout grid; - grid.setMarginRight(15); - grid.setLabelWidth(0); - grid.spacer(); - window->padAll(0); - - coord_t y = 2; - - for (auto category: modelslist.getCategories()) { - // NAME - auto catname = new TextEdit(window, grid.getFieldSlot(3,0), category->name, sizeof(category->name)); - catname->setChangeHandler([=]() { - if(category->name[0] == '\0') { - category->name[0] = ' '; category->name[1] = '\0'; - } - modelslist.save(); - }); - - // Details - char cnt[19]; - snprintf(cnt, sizeof(cnt), "%u %s", (unsigned)category->size(), STR_MODELS); - new StaticText(window, grid.getFieldSlot(3,1), cnt); - - if(category->empty()) { - new TextButton(window, grid.getFieldSlot(3,2),STR_DELETE, [=]() -> uint8_t { - new ConfirmDialog(window, STR_DELETE_CATEGORY, - std::string(category->name, sizeof(category->name)).c_str(), - [=] { - modelslist.removeCategory(category); - modelslist.save(); - update(); - }); - return 0; - }); - } else { -#ifdef CATEGORIES_SHOW_DELETE_NON_EMPTY - new TextButton(window, grid.getFieldSlot(3,2),STR_DELETE, [=]() -> uint8_t { - new MessageDialog(window, STR_DELETE_CATEGORY, STR_CAT_NOT_EMPTY); - return 0; - }); -#endif - } - - grid.nextLine(); - - grid.spacer(); - coord_t height = grid.getWindowHeight(); - //window->setHeight(height); - y += height + 2; + // find the first label of the current model and make that label active + auto currentModel = modelslist.getCurrentModel(); + if (currentModel != nullptr) { + auto modelLabels = modelslabels.getLabelsByModel(currentModel); + if (modelLabels.size() > 0) { + auto allLabels = getLabels(); + auto found = + std::find(allLabels.begin(), allLabels.end(), modelLabels[0]); + if (found != allLabels.end()) { + lblselector->setSelected(found - allLabels.begin()); } - - new TextButton(window, grid.getCenteredSlot(LCD_W/2), STR_CREATE_CATEGORY, [=]() -> uint8_t { - modelslist.createCategory("New"); - update(); + } else { + // the current model has no labels so set the active label to "Unlabeled" + lblselector->setSelected(getLabels().size() - 1); + } + } +} + +void ModelLabelsWindow::newModel() +{ + // Save current + storageFlushCurrentModel(); + storageCheck(true); + + // Create a new blank ModelCell and activate it first, createmodel() will modify + // the model in memory. + auto newCell = modelslist.addModel("", false); + modelslist.setCurrentModel(newCell); + + // Make the new model + createModel(); + + new SelectTemplateFolder([=]() { + // On complete update the current cell's data + modelslist.updateCurrentModelCell(); + + auto labels = getLabels(); + lblselector->setNames(labels); + lblselector->setSelected(modelslabels.getLabels().size()); + mdlselector->update(0); + }); +} + +void ModelLabelsWindow::buildHead(PageHeader *hdr) +{ + // page title + auto curModel = modelslist.getCurrentModel(); + auto modelName = curModel != nullptr ? curModel->modelName : STR_NONE; + + std::string titleName = STR_SELECT_MODEL; + titleName += "\n"; + titleName += STR_CURRENT_MODEL; + titleName += ": "; + titleName += modelName; + + hdr->setTitle(titleName); + + // new model button + auto btn = new TextButton( + hdr, rect_t{}, STR_NEW_MODEL, + [=]() { + newModel(); return 0; + }, + BUTTON_BACKGROUND | OPAQUE, textFont); + + btn->padAll(lv_dpx(4)); + + // button placement + hdr->padRight(lv_dpx(8)); + lv_obj_align(btn->getLvObj(), LV_ALIGN_RIGHT_MID, 0, 0); +} + +#if LCD_W > LCD_H +static const lv_coord_t col_dsc[] = {LABELS_WIDTH, LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; +static const lv_coord_t row_dsc[] = {LV_GRID_FR(1), BUTTONS_HEIGHT, LV_GRID_TEMPLATE_LAST}; +#else +static const lv_coord_t col_dsc[] = {LV_GRID_FR(1), LV_GRID_TEMPLATE_LAST}; +static const lv_coord_t row_dsc[] = {LABELS_HEIGHT, LV_GRID_FR(1), BUTTONS_HEIGHT, LV_GRID_TEMPLATE_LAST}; +#endif + +void ModelLabelsWindow::buildBody(FormWindow *window) +{ + auto win_obj = window->getLvObj(); + + lv_obj_set_grid_dsc_array(win_obj, col_dsc, row_dsc); + + // Models List + mdlselector = new ModelsPageBody(window, rect_t{}); + mdlselector->setLblRefreshFunc([=]() { labelRefreshRequest(); }); + auto mdl_obj = mdlselector->getLvObj(); + + // Labels + lblselector = new ListBox(window, rect_t{}, getLabels()); + auto lbl_obj = lblselector->getLvObj(); + + // Sort Buttons + auto btnh = new ButtonHolder(window, rect_t{}); + auto buth_obj = btnh->getLvObj(); + + // Labels top left + lv_obj_set_grid_cell(lbl_obj, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 0, 1); + + window->padAll(LAY_MARGIN); + window->padRow(LAY_MARGIN); + window->padColumn(LAY_MARGIN); + +#if LCD_W > LCD_H + // Buttons bottom left + // Models right + lv_obj_set_grid_cell(mdl_obj, LV_GRID_ALIGN_STRETCH, 1, 1, LV_GRID_ALIGN_STRETCH, 0, 2); + lv_obj_set_width(mdl_obj, lv_pct(100)); + window->padRight(0); + lv_obj_set_grid_cell(buth_obj, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1); +#else + // Models middle + // Buttons bottom + lv_obj_set_grid_cell(mdl_obj, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 1, 1); + lv_obj_set_width(mdl_obj, lv_pct(100)); + + lv_obj_set_grid_cell(buth_obj, LV_GRID_ALIGN_STRETCH, 0, 1, LV_GRID_ALIGN_STRETCH, 2, 1); +#endif + + lv_obj_update_layout(mdl_obj); + lblselector->setColumnWidth(0, lv_obj_get_content_width(lbl_obj)); + + btnh->setPressHandler([=](int index, ButtonHolder::ButtonInfo *button) { + if (index == 0) { // alpha + sort = button->sortState == 0 ? NAME_ASC : NAME_DES; + } else { + sort = button->sortState == 0 ? DATE_ASC : DATE_DES; + } + // Update the list asynchronously + mdlselector->setSortOrder(sort); }); - grid.nextLine(); + //lv_obj_align(btnh->getLvObj(), LV_ALIGN_BOTTOM_LEFT, 0, 0); - if(scrolltobot) { - lv_obj_scroll_to_y(window->getLvObj(), y + 40, LV_ANIM_OFF); + lblselector->setMultiSelect(true); + lblselector->setSelected(modelslabels.filteredLabels()); + updateFilteredLabels(modelslabels.filteredLabels(), false); + lv_obj_set_scrollbar_mode(lbl_obj, LV_SCROLLBAR_MODE_AUTO); + lv_obj_set_scrollbar_mode(mdl_obj, LV_SCROLLBAR_MODE_AUTO); + + lblselector->setMultiSelectHandler([=](std::set selected, + std::set oldselection) { + if (modelslabels.getUnlabeledModels().size() != 0) { + // Special case for mutually exclusive Unsorted + bool unsrt_is_selected = + selected.find(lblselector->getRowCount() - 1) != selected.end(); + bool unsrt_was_selected = oldselection.find(lblselector->getRowCount() - + 1) != oldselection.end(); + + // Unsorted was just picked + if (unsrt_is_selected && !unsrt_was_selected) { + selected.clear(); + selected.insert(lblselector->getRowCount() - 1); + } else if (unsrt_is_selected && unsrt_was_selected) { + selected.erase(selected.find(lblselector->getRowCount() - 1)); } - } - private: - ModelSelectMenu *modelselectmenu; - bool scrolltobot; -}; - -ModelSelectMenu::ModelSelectMenu(): - TabsGroup(ICON_MODEL_SELECT) -{ - build(); -} - -void ModelSelectMenu::build(int index) -{ - modelslist.clear(); - modelslist.load(); - - removeAllTabs(); - - TRACE("TabsGroup: %p", this); - - addTab(new CategoryEditPage(this)); - - for (auto category: modelslist.getCategories()) { - addTab(new ModelCategoryPage(category)); - } - - if(index < 0) { - int idx = modelslist.getCurrentCategoryIdx(); - if (idx >= 0) { - setCurrentTab(idx+1); } + + lblselector->setSelected(selected); + updateFilteredLabels(selected); + }); + + lblselector->setLongPressHandler([=]() { + int selected = lblselector->getSelected(); + auto labels = getLabels(); + + if (selected < (int)labels.size()) { + Menu *menu = new Menu(window); + std::string selectedLabel = labels.at(selected); + menu->setTitle(selectedLabel); + menu->addLine(STR_NEW_LABEL, [=]() { + new LabelDialog(parent, tmpLabel, [=](std::string label) { + int newlabindex = modelslabels.addLabel(label); + if (newlabindex >= 0) { + std::set newset; + newset.insert(newlabindex); + auto labels = getLabels(); + lblselector->setNames(labels); + lblselector->setSelected(newset); + updateFilteredLabels(newset); + } + }); + return 0; + }); + if (selectedLabel != STR_UNLABELEDMODEL) { + menu->addLine(STR_RENAME_LABEL, [=]() { + auto oldLabel = labels[selected]; + strncpy(tmpLabel, oldLabel.c_str(), MAX_LABEL_SIZE); + tmpLabel[MAX_LABEL_SIZE] = '\0'; + + new LabelDialog(this, tmpLabel, [=](std::string newLabel) { + auto rndialog = + new ProgressDialog(this, STR_RENAME_LABEL, [=]() {}); + modelslabels.renameLabel( + oldLabel, newLabel, [=](const char *name, int percentage) { + rndialog->updateProgress(name, percentage); + }); + auto labels = getLabels(); + lblselector->setNames(labels); + }); + return 0; + }); + menu->addLine(STR_DELETE_LABEL, [=]() { + auto labelToDelete = labels[selected]; + new ConfirmDialog( + parent, STR_DELETE_LABEL, labelToDelete.c_str(), [=]() { + auto deldialog = + new ProgressDialog(this, STR_DELETE_LABEL, [=]() {}); + modelslabels.removeLabel( + labelToDelete, [=](const char *name, int percentage) { + deldialog->updateProgress(name, percentage); + }); + auto labels = getLabels(); + std::set newset; + lblselector->setNames(labels); + lblselector->setSelected(newset); + updateFilteredLabels(newset); + }); + return 0; + }); + if (modelslabels.getLabels().size() > 1) { + if (selected != 0) { + menu->addLine("Move Up", [=]() { + modelslabels.moveLabelTo(selected, selected - 1); + std::set newset; + newset.insert(selected - 1); + auto labels = getLabels(); + lblselector->setNames(labels); + lblselector->setSelected(newset); + updateFilteredLabels(newset); + return 0; + }); + } + if (selected != (int)modelslabels.getLabels().size() - 1) { + menu->addLine("Move Down", [=]() { + modelslabels.moveLabelTo(selected, selected + 1); + std::set newset; + newset.insert(selected + 1); + auto labels = getLabels(); + lblselector->setNames(labels); + lblselector->setSelected(newset); + updateFilteredLabels(newset); + return 0; + }); + } + } + } + } + }); +} + +void ModelLabelsWindow::updateFilteredLabels(std::set selected, + bool setdirty) +{ + LabelsVector sellabels; + LabelsVector labels = getLabels(); + for (auto sel : selected) { + if (sel < labels.size()) sellabels.push_back(labels[sel]); + } + if (setdirty) { // Save to file? + modelslabels.setFilteredLabels(selected); + modelslabels.setDirty(); + } + mdlselector->setLabels(sellabels); // Update the list +} + +void ModelLabelsWindow::labelRefreshRequest() +{ + auto labels = getLabels(); + lblselector->setNames(labels); +} + +//----------------------------------------------------------------------------- + +ProgressDialog::ProgressDialog(Window *parent, std::string title, + std::function onClose) : + Dialog(parent, title, rect_t{}), + progress(new Progress(&content->form, rect_t{})), + onClose(std::move(onClose)), + _title(title) +{ + progress->setHeight(LV_DPI_DEF / 4); + + content->setWidth(LCD_W * 0.8); + content->updateSize(); + + auto content_w = lv_obj_get_content_width(content->form.getLvObj()); + progress->setWidth(content_w); + + // disable canceling dialog + setCloseWhenClickOutside(false); +} + +void ProgressDialog::updateProgress(const char *filename, int percentage) +{ + content->setTitle(_title + " " + filename); + progress->setValue(percentage); + if (percentage >= 100) { + deleteLater(); + onClose(); } else { - if(index < static_cast(modelslist.getCategories().size())) - setCurrentTab(index); + lv_refr_now(nullptr); } } + +// disable keys +void ProgressDialog::onEvent(event_t) { return; } diff --git a/radio/src/gui/colorlcd/model_select.h b/radio/src/gui/colorlcd/model_select.h index 9685b036d..2b2413615 100644 --- a/radio/src/gui/colorlcd/model_select.h +++ b/radio/src/gui/colorlcd/model_select.h @@ -22,12 +22,100 @@ #ifndef _MODEL_SELECT_H_ #define _MODEL_SELECT_H_ +#include +#include + +#include "libopenui.h" +#include "listbox.h" +#include "storage/modelslist.h" #include "tabsgroup.h" -class ModelSelectMenu: public TabsGroup { - public: - ModelSelectMenu(); - void build(int index=-1); +constexpr int MAX_LABEL_SIZE = 20; + +class ModelButton; + +class ModelsPageBody : public FormWindow +{ + public: + ModelsPageBody(Window *parent, const rect_t &rect); + + void setLabels(LabelsVector labels) + { + selectedLabels = labels; + update(); + } + void update(int selected = -1); + + inline void setSortOrder(ModelsSortBy sortOrder) + { + modelslabels.setSortOrder(sortOrder); + update(); + } + + void setLblRefreshFunc(std::function fnc) + { + refreshLabels = std::move(fnc); + } + + protected: + ModelsSortBy _sortOrder; + bool isDirty = false; + bool refresh = false; + std::string selectedLabel; + LabelsVector selectedLabels; + std::function refreshLabels = nullptr; + + void selectModel(ModelCell* model); + void duplicateModel(ModelCell* model); + void deleteModel(ModelCell* model); + void editLabels(ModelCell* model); }; -#endif // _MODEL_SELECT_H_ +class ModelLabelsWindow : public Page +{ + public: + ModelLabelsWindow(); + + protected: + ModelsSortBy sort = DEFAULT_MODEL_SORT; + char tmpLabel[MAX_LABEL_SIZE + 1] = "\0"; + ListBox *lblselector; + ModelsPageBody *mdlselector; + std::string currentLabel; + + LabelsVector getLabels() + { + auto labels = modelslabels.getLabels(); + if (modelslabels.getUnlabeledModels().size() > 0) + labels.emplace_back(STR_UNLABELEDMODEL); + return labels; + } + + void newModel(); + void buildHead(PageHeader *window); + void buildBody(FormWindow *window); + void updateFilteredLabels(std::set selected, bool setdirty = true); + void labelRefreshRequest(); +}; + +class ProgressDialog : public Dialog +{ + uint32_t lastUpdate = 0; + Progress *progress; + + std::function onClose; + + public: + ProgressDialog(Window *parent, std::string title, + std::function onClose); + + void updateProgress(const char *filename, int percentage); + + // disable keys + void onEvent(event_t) override; + + protected: + std::string _title; +}; + +#endif // _MODEL_SELECT_H_ diff --git a/radio/src/gui/colorlcd/model_setup.cpp b/radio/src/gui/colorlcd/model_setup.cpp index afef6ff7a..1bc6a3ee9 100644 --- a/radio/src/gui/colorlcd/model_setup.cpp +++ b/radio/src/gui/colorlcd/model_setup.cpp @@ -45,11 +45,9 @@ ModelSetupPage::ModelSetupPage() : static void onModelNameChanged() { - modelslist.load(); auto model = modelslist.getCurrentModel(); if (model) { model->setModelName(g_model.header.name); - modelslist.save(); } SET_DIRTY(); } @@ -233,6 +231,31 @@ void ModelSetupPage::build(FormWindow * window) new StaticText(line, rect_t{}, STR_MODELNAME, 0, COLOR_THEME_PRIMARY1); new ModelNameEdit(line, rect_t{}); + // Model labels + line = window->newLine(&grid); + new StaticText(line, rect_t{}, STR_LABELS, 0, COLOR_THEME_PRIMARY1); + auto curmod = modelslist.getCurrentModel(); + labelTextButton = + new TextButton(line, rect_t{}, modelslabels.getLabelString(curmod ,STR_UNLABELEDMODEL), [=] () { + Menu *menu = new Menu(window, true); + menu->setTitle(STR_LABELS); + for (auto &label: modelslabels.getLabels()) { + menu->addLine(label, + [=] () { + if (!modelslabels.isLabelSelected(label, curmod)) + modelslabels.addLabelToModel(label, curmod); + else + modelslabels.removeLabelFromModel(label, curmod); + labelTextButton->setText(modelslabels.getLabelString(curmod,STR_UNLABELEDMODEL)); + strcpy(g_model.header.labels, modelslabels.getLabelString(curmod,STR_UNLABELEDMODEL).c_str()); + SET_DIRTY(); + }, [=] () { + return modelslabels.isLabelSelected(label, curmod); + }); + } + return 0; + }); + // Bitmap line = window->newLine(&grid); new StaticText(line, rect_t{}, STR_BITMAP, 0, COLOR_THEME_PRIMARY1); diff --git a/radio/src/gui/colorlcd/model_setup.h b/radio/src/gui/colorlcd/model_setup.h index f4bd6cb8d..993a68d79 100644 --- a/radio/src/gui/colorlcd/model_setup.h +++ b/radio/src/gui/colorlcd/model_setup.h @@ -26,4 +26,6 @@ class ModelSetupPage: public PageTab { ModelSetupPage(); void build(FormWindow * window) override; + private: + TextButton *labelTextButton = nullptr; }; diff --git a/radio/src/gui/colorlcd/model_templates.cpp b/radio/src/gui/colorlcd/model_templates.cpp new file mode 100644 index 000000000..aee79033b --- /dev/null +++ b/radio/src/gui/colorlcd/model_templates.cpp @@ -0,0 +1,260 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "model_templates.h" +#include "standalone_lua.h" +#include "str_functions.h" + +char TemplatePage::path[LEN_PATH + 1]; +std::function TemplatePage::update = nullptr; + + +void TemplatePage::updateInfo() +{ + FIL fp; + FRESULT res = f_open (&fp, buffer, FA_READ); + unsigned int bytesRead = 0; + if (res == FR_OK) { + f_read (&fp, infoText, LEN_INFO_TEXT, &bytesRead); + f_close(&fp); + } + infoText[bytesRead] = '\0'; + invalidate(); +} + +#if defined(HARDWARE_KEYS) +void TemplatePage::onEvent(event_t event) +{ + if (event == EVT_KEY_LONG(KEY_EXIT) || event == EVT_KEY_BREAK(KEY_EXIT)) { + killEvents(event); + deleteLater(); + } else { + Page::onEvent(event); + } +} +#endif + +void TemplatePage::paint(BitmapBuffer *dc) +{ + Page::paint(dc); + rect_t rect = body.getRect(); + coord_t x = LCD_W / 2 + PAGE_PADDING; + coord_t y = rect.y + PAGE_PADDING; + coord_t w = LCD_W / 2 - 2 * PAGE_PADDING; + coord_t h = rect.h - 2 * PAGE_PADDING; + if (count > 0) { + if (infoText[0] == '\0') + drawTextLines(dc, x, y, w, h, STR_NO_INFORMATION, COLOR_THEME_DISABLED); + else + drawTextLines(dc, x, y, w, h, infoText, COLOR_THEME_PRIMARY1); + } +} + + +SelectTemplate::SelectTemplate(TemplatePage* tp) + : templateFolderPage(tp) +{ + rect_t rect = {PAGE_TITLE_LEFT, PAGE_TITLE_TOP + 10, LCD_W - PAGE_TITLE_LEFT, PAGE_LINE_HEIGHT}; + new StaticText(&header, rect, STR_SELECT_TEMPLATE, 0, COLOR_THEME_PRIMARY2); + + FormGridLayout grid; + grid.spacer(PAGE_PADDING); + std::list files; + FILINFO fno; + DIR dir; + FRESULT res = f_opendir(&dir, path); + + if (res == FR_OK) { + // read all entries + bool firstTime = true; + for (;;) { + res = sdReadDir(&dir, &fno, firstTime); + firstTime = false; + if (res != FR_OK || fno.fname[0] == 0) + break; // Break on error or end of dir + if (strlen((const char*)fno.fname) > SD_SCREEN_FILE_LENGTH) + continue; + if (fno.fname[0] == '.') + continue; + if (!(fno.fattrib & AM_DIR)) { + const char *ext = getFileExtension(fno.fname); + if(ext && !strcasecmp(ext, YAML_EXT)) { + int len = ext - fno.fname; + if (len < FF_MAX_LFN) { + char name[FF_MAX_LFN] = { 0 }; + strncpy(name, fno.fname, len); + files.push_back(name); + } + } + } + } + + files.sort(compare_nocase); + + for (auto name: files) { + auto tb = new TemplateButton(&body, grid.getLabelSlot(), name, [=]() -> uint8_t { + // Read model template + loadModelTemplate((name + YAML_EXT).c_str(), path); + storageDirty(EE_MODEL); + storageCheck(true); + // Dismiss template pages + deleteLater(); + templateFolderPage->deleteLater(); +#if defined(LUA) + // If there is a wizard Lua script, fire it up + snprintf(buffer, LEN_BUFFER, "%s%c%s%s", path, '/', name.c_str(), SCRIPT_EXT); + if (f_stat(buffer, 0) == FR_OK) { + luaExec(buffer); + // Need to update() the ModelCategoryPageBody before attaching StandaloneLuaWindow to not mess up focus + update(); + update = nullptr; + StandaloneLuaWindow::instance()->attach(); + } +#endif + return 0; + }); + tb->setFocusHandler([=](bool active) { + if (active) { + snprintf(buffer, LEN_BUFFER, "%s%c%s%s", path, '/', name.c_str(), TEXT_EXT); + updateInfo(); + } + }); + tb->setHeight(PAGE_LINE_HEIGHT * 2); + grid.spacer(tb->height() + 5); + } + } + + f_closedir(&dir); + count = files.size(); + if (count == 0) { + rect_t rect = body.getRect(); + rect.x = PAGE_PADDING; + rect.y = PAGE_PADDING; + rect.w = rect.w - (2 * PAGE_PADDING); + +#if LCD_W > LCD_H + rect.h = PAGE_LINE_HEIGHT; + int charBreak = 60; +#else + rect.h = PAGE_LINE_HEIGHT * 2; + int charBreak = 40; +#endif + new StaticText(&body, rect, wrap(STR_NO_TEMPLATES, charBreak), 0, COLOR_THEME_PRIMARY1); + + // The following button is needed because the EXIT key does not work without... + rect = body.getRect(); + rect.x = rect.w - PAGE_PADDING - 100; + rect.y = rect.h - PAGE_PADDING - (PAGE_LINE_HEIGHT * 2); + rect.w = 100; + rect.h = PAGE_LINE_HEIGHT * 2; + new TextButton(&body, rect, STR_EXIT, [=]() -> uint8_t { deleteLater(); return 0; }); + } else { + snprintf(buffer, LEN_BUFFER, "%s%c%s%s", path, '/', files.front().c_str(), TEXT_EXT); + updateInfo(); + } +} + + +SelectTemplateFolder::SelectTemplateFolder(std::function update) +{ + TemplatePage::update = update; + rect_t rect = {PAGE_TITLE_LEFT, PAGE_TITLE_TOP + 10, LCD_W - PAGE_TITLE_LEFT, PAGE_LINE_HEIGHT}; + new StaticText(&header, rect, STR_SELECT_TEMPLATE_FOLDER, 0, COLOR_THEME_PRIMARY2); + + FormGridLayout grid; + grid.spacer(PAGE_PADDING); + + auto tfb = new TemplateButton(&body, grid.getLabelSlot(), STR_BLANK_MODEL, [=]() -> uint8_t { + deleteLater(); + return 0; + }); + snprintf(infoText, LEN_INFO_TEXT, "%s", STR_BLANK_MODEL_INFO); + tfb->setFocusHandler([=](bool active) { + if (active) { + snprintf(infoText, LEN_INFO_TEXT, "%s", STR_BLANK_MODEL_INFO); + invalidate(); + } + }); + tfb->setHeight(PAGE_LINE_HEIGHT * 2); + grid.spacer(tfb->height() + 5); + + std::list directories; + FILINFO fno; + DIR dir; + FRESULT res = f_opendir(&dir, TEMPLATES_PATH); + + if (res == FR_OK) { + // read all entries + bool firstTime = true; + for (;;) { + res = sdReadDir(&dir, &fno, firstTime); + firstTime = false; + if (res != FR_OK || fno.fname[0] == 0) + break; // Break on error or end of dir + if (strlen((const char*)fno.fname) > SD_SCREEN_FILE_LENGTH) + continue; + if (fno.fname[0] == '.') + continue; + if (fno.fattrib & AM_DIR) + directories.push_back((char*)fno.fname); + } + + directories.sort(compare_nocase); + + for (auto name: directories) { +#if not defined(LUA) + // Don't show wizards dir if no lua + if (!strcasecmp(name.c_str(), "WIZARD") == 0) { +#endif + auto tfb = new TemplateButton(&body, grid.getLabelSlot(), name, + [=]() -> uint8_t { + snprintf(path, LEN_PATH, "%s%c%s", TEMPLATES_PATH, '/', name.c_str()); + new SelectTemplate(this); + return 0; + }); + tfb->setFocusHandler([=](bool active) { + if (active) { + snprintf(buffer, LEN_BUFFER, "%s%c%s%c%s%s", TEMPLATES_PATH, '/', name.c_str(), '/', "about", TEXT_EXT); + updateInfo(); + } + }); + tfb->setHeight(PAGE_LINE_HEIGHT * 2); + grid.spacer(tfb->height() + 5); + } +#if not defined(LUA) + } +#endif + } + + f_closedir(&dir); + count = directories.size(); + if (count == 0) { + rect_t rect = grid.getLabelSlot(); + rect.w = body.getRect().w - 2 * PAGE_PADDING; + new StaticText(&body, rect, STR_NO_TEMPLATES, 0, COLOR_THEME_PRIMARY1); + } +} + +SelectTemplateFolder::~SelectTemplateFolder() +{ + if (update) + update(); +} diff --git a/radio/src/gui/colorlcd/model_templates.h b/radio/src/gui/colorlcd/model_templates.h new file mode 100644 index 000000000..9c65cc2ca --- /dev/null +++ b/radio/src/gui/colorlcd/model_templates.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#pragma once + +#include +#include "opentx.h" + +constexpr size_t LEN_INFO_TEXT = 300; +constexpr size_t LEN_PATH = sizeof(TEMPLATES_PATH) + TEXT_FILENAME_MAXLEN; +constexpr size_t LEN_BUFFER = sizeof(TEMPLATES_PATH) + 2 * TEXT_FILENAME_MAXLEN + 1; + +class TemplatePage : public Page +{ + public: + + TemplatePage() : Page(ICON_MODEL_SELECT) + { } + + void updateInfo(); + +#if defined(HARDWARE_KEYS) + void onEvent(event_t event) override; +#endif + +#if defined(DEBUG_WINDOWS) + std::string getName() const { return "TemplatePage"; } +#endif + + protected: + + static char path[LEN_PATH + 1]; + char buffer[LEN_BUFFER + 1]; + char infoText[LEN_INFO_TEXT + 1] = { 0 }; + unsigned int count = 0; + static std::function update; + + void paint(BitmapBuffer *dc) override; +}; + +class TemplateButton : public TextButton +{ + public: + TemplateButton(FormGroup* parent, const rect_t& rect, std::string name, std::function pressHandler = nullptr) + : TextButton(parent, rect, name, pressHandler) + { } +}; + +class SelectTemplate : public TemplatePage +{ + public: + SelectTemplate(TemplatePage* tp); + + protected: + TemplatePage* templateFolderPage; +}; + +class SelectTemplateFolder : public TemplatePage +{ + public: + SelectTemplateFolder(std::function update); + ~SelectTemplateFolder(); +}; \ No newline at end of file diff --git a/radio/src/gui/colorlcd/module_setup.cpp b/radio/src/gui/colorlcd/module_setup.cpp index e3d8b0091..fa296926a 100644 --- a/radio/src/gui/colorlcd/module_setup.cpp +++ b/radio/src/gui/colorlcd/module_setup.cpp @@ -31,6 +31,7 @@ #include "custom_failsafe.h" #include "ppm_settings.h" #include "channel_range.h" +#include "storage/modelslist.h" #if defined(PXX2) #include "access_settings.h" @@ -70,7 +71,7 @@ class ModuleWindow : public FormGroup void updateSubType(); uint8_t getModuleIdx() const { return moduleIdx; } - + protected: uint8_t moduleIdx; @@ -82,8 +83,11 @@ class ModuleWindow : public FormGroup TextButton *registerButton = nullptr; Window *fsLine = nullptr; Choice *rfPower = nullptr; + StaticText *idUnique = nullptr; void startRSSIDialog(std::function closeHandler = nullptr); + + void updateIDStaticText(int mdIdx); }; struct FailsafeChoice : public FormGroup { @@ -138,6 +142,22 @@ ModuleWindow::ModuleWindow(Window* parent, uint8_t moduleIdx) : updateModule(); } +void ModuleWindow::updateIDStaticText(int mdIdx) +{ + if(idUnique == nullptr) + return; + char buffer[50]; + std::string idStr = STR_MODELIDUNIQUE; + if(!modelslist.isModelIdUnique(mdIdx, buffer, sizeof(buffer))) { + idStr = STR_MODELIDUSED; + idStr = idStr + buffer; + idUnique->setTextFlags(COLOR_THEME_WARNING); + } else { + idUnique->setTextFlags(COLOR_THEME_PRIMARY1); + } + idUnique->setText(idStr); +} + void ModuleWindow::updateModule() { FlexGridLayout grid(col_dsc, row_dsc, 2); @@ -194,6 +214,15 @@ void ModuleWindow::updateModule() // Bind and Range buttons if (!isModuleRFAccess(moduleIdx) && (isModuleModelIndexAvailable(moduleIdx) || isModuleBindRangeAvailable(moduleIdx))) { + // Is Reciever ID Unique + if(isModuleModelIndexAvailable(moduleIdx)) { + auto line = newLine(&grid); + new StaticText(line, rect_t{},""); + auto box = new FormGroup(line, rect_t{}); + box->setFlexLayout(LV_FLEX_FLOW_ROW, lv_dpx(8)); + idUnique = new StaticText(box, rect_t{}, "", 0, 0); + updateIDStaticText(moduleIdx); + } auto line = newLine(&grid); new StaticText(line, rect_t{}, STR_RECEIVER, 0, COLOR_THEME_PRIMARY1); @@ -207,6 +236,8 @@ void ModuleWindow::updateModule() GET_DEFAULT(*modelId), [=](int32_t newValue) { if (newValue != *modelId) { *modelId = newValue; + modelslist.updateCurrentModelCell(); + updateIDStaticText(moduleIdx); #if defined(CROSSFIRE) if (isModuleCrossfire(moduleIdx)) { moduleState[moduleIdx].counter = @@ -217,6 +248,7 @@ void ModuleWindow::updateModule() } }); + if (isModuleBindRangeAvailable(moduleIdx)) { bindButton = new TextButton(box, rect_t{},STR_MODULE_BIND); bindButton->setPressHandler([=]() -> uint8_t { @@ -303,7 +335,7 @@ void ModuleWindow::updateModule() #if defined(PXX2) else if (isModuleRFAccess(moduleIdx)) { - // Register and Range buttons +// Register and Range buttons auto line = newLine(&grid); new StaticText(line, rect_t{}, STR_MODULE, 0, COLOR_THEME_PRIMARY1); @@ -517,7 +549,7 @@ void ModuleSubTypeChoice::update() setValues(STR_R9M_REGION); setGetValueHandler(GET_DEFAULT(md->subType)); setSetValueHandler(SET_DEFAULT(md->subType)); - setAvailableHandler(nullptr); + setAvailableHandler(nullptr); } #if defined(PXX2) else if (isModulePXX2(moduleIdx)) { @@ -526,7 +558,7 @@ void ModuleSubTypeChoice::update() setValues(STR_ISRM_RF_PROTOCOLS); setGetValueHandler(GET_DEFAULT(md->subType)); setSetValueHandler(SET_DEFAULT(md->subType)); - setAvailableHandler(nullptr); + setAvailableHandler(nullptr); } #endif #if defined(AFHDS2) || defined(AFHDS3) @@ -590,9 +622,9 @@ void ModuleSubTypeChoice::update() lv_obj_add_flag(lvobj, LV_OBJ_FLAG_HIDDEN); return; } - + lv_obj_clear_flag(lvobj, LV_OBJ_FLAG_HIDDEN); - + // update choice value lv_event_send(lvobj, LV_EVENT_VALUE_CHANGED, nullptr); } @@ -669,7 +701,7 @@ ModulePage::ModulePage(uint8_t moduleIdx) : Page(ICON_MODEL_SETUP) : isExternalModuleAvailable(moduleType); }); - auto subTypeChoice = new ModuleSubTypeChoice(box, moduleIdx); + auto subTypeChoice = new ModuleSubTypeChoice(box, moduleIdx); auto moduleWindow = new ModuleWindow(form, moduleIdx); // This needs to be after moduleWindow has been created diff --git a/radio/src/gui/colorlcd/view_main.cpp b/radio/src/gui/colorlcd/view_main.cpp index 76cec7fc3..7feafeaec 100644 --- a/radio/src/gui/colorlcd/view_main.cpp +++ b/radio/src/gui/colorlcd/view_main.cpp @@ -240,7 +240,7 @@ void ViewMain::onEvent(event_t event) break; case EVT_KEY_LONG(KEY_MODEL): - new ModelSelectMenu(); + new ModelLabelsWindow(); break; // TODO: diff --git a/radio/src/gui/colorlcd/view_main_menu.cpp b/radio/src/gui/colorlcd/view_main_menu.cpp index 3789c4875..bf71f11e3 100644 --- a/radio/src/gui/colorlcd/view_main_menu.cpp +++ b/radio/src/gui/colorlcd/view_main_menu.cpp @@ -46,7 +46,7 @@ ViewMainMenu::ViewMainMenu(Window* parent) : auto carousel = new SelectFabCarousel(this); carousel->addButton(ICON_MODEL_SELECT, STR_MAIN_MENU_SELECT_MODEL, [=]() -> uint8_t { deleteLater(); - new ModelSelectMenu(); + new ModelLabelsWindow(); return 0; }); diff --git a/radio/src/myeeprom.h b/radio/src/myeeprom.h index 14766f7e4..7e0c70537 100644 --- a/radio/src/myeeprom.h +++ b/radio/src/myeeprom.h @@ -284,5 +284,6 @@ PACK(union u_int8int16_t { constexpr uint8_t EE_GENERAL = 0x01; constexpr uint8_t EE_MODEL = 0x02; +constexpr uint8_t EE_LABELS = 0x04; #endif // _MYEEPROM_H_ diff --git a/radio/src/sdcard.h b/radio/src/sdcard.h index fd5500814..a58e2f498 100644 --- a/radio/src/sdcard.h +++ b/radio/src/sdcard.h @@ -34,6 +34,7 @@ extern FIL g_oLogFile; #define PATH_SEPARATOR "/" #define ROOT_PATH PATH_SEPARATOR #define MODELS_PATH ROOT_PATH "MODELS" // no trailing slash = important +#define DELETED_MODELS_PATH MODELS_PATH PATH_SEPARATOR "DELETED" #define RADIO_PATH ROOT_PATH "RADIO" // no trailing slash = important #define TEMPLATES_PATH ROOT_PATH "TEMPLATES" #define PERS_TEMPL_PATH TEMPLATES_PATH "/PERSONAL" @@ -65,8 +66,11 @@ extern FIL g_oLogFile; const char RADIO_MODELSLIST_PATH[] = RADIO_PATH PATH_SEPARATOR "models.txt"; const char RADIO_SETTINGS_PATH[] = RADIO_PATH PATH_SEPARATOR RADIO_FILENAME; #if defined(SDCARD_YAML) -const char MODELSLIST_YAML_PATH[] = MODELS_PATH PATH_SEPARATOR "models.yml"; -const char FALLBACK_MODELSLIST_YAML_PATH[] = RADIO_PATH PATH_SEPARATOR "models.yml"; +#define LABELS_FILENAME "labels.yml" +#define MODELS_FILENAME "models.yml" +const char MODELSLIST_YAML_PATH[] = MODELS_PATH PATH_SEPARATOR MODELS_FILENAME; +const char FALLBACK_MODELSLIST_YAML_PATH[] = RADIO_PATH PATH_SEPARATOR MODELS_FILENAME; +const char LABELSLIST_YAML_PATH[] = MODELS_PATH PATH_SEPARATOR LABELS_FILENAME; const char RADIO_SETTINGS_YAML_PATH[] = RADIO_PATH PATH_SEPARATOR "radio.yml"; const char RADIO_SETTINGS_TMPFILE_YAML_PATH[] = RADIO_PATH PATH_SEPARATOR "radio_new.yml"; const char RADIO_SETTINGS_ERRORFILE_YAML_PATH[] = RADIO_PATH PATH_SEPARATOR "radio_error.yml"; diff --git a/radio/src/storage/conversions/conversions.cpp b/radio/src/storage/conversions/conversions.cpp index 15949cbb2..149230bc0 100644 --- a/radio/src/storage/conversions/conversions.cpp +++ b/radio/src/storage/conversions/conversions.cpp @@ -105,56 +105,37 @@ void convertBinRadioData(const char * path, int version) #endif const char* error = nullptr; - for (auto category_ptr : modelslist.getCategories()) { + for(auto model_it = modelslist.begin(); model_it != modelslist.end(); ++model_it) { + uint8_t model_version = 0; + auto* model_ptr = *model_it; + char* filename = model_ptr->modelFilename; - auto model_it = category_ptr->begin(); + TRACE("converting '%s' (%d/%d)", filename, converted, to_convert); + drawProgressScreen(filename, converted, to_convert); - while(model_it != category_ptr->end()) { + // read only the version number (size=0) + error = readModelBin(filename, nullptr, 0, &model_version); + if (!error) { + // TODO: error handling + error = convertBinModelData(filename, model_version); - uint8_t model_version = 0; - auto* model_ptr = *model_it; - char* filename = model_ptr->modelFilename; - - TRACE("converting '%s' (%d/%d)", filename, converted, to_convert); - drawProgressScreen(filename, converted, to_convert); - - // read only the version number (size=0) - error = readModelBin(filename, nullptr, 0, &model_version); - if (!error) { - // TODO: error handling - error = convertBinModelData(filename, model_version); - ++model_it; - - if (error) { - TRACE("ERROR converting '%s': %s", filename, error); - category_ptr->removeModel(model_ptr); - } else { - PartialModel partial; - memclear(&partial, sizeof(PartialModel)); - - readModelYaml(filename, reinterpret_cast(&partial), sizeof(partial)); - model_ptr->setModelName(partial.header.name); - } - } else { - TRACE("ERROR reading '%s': %s", filename, error); - - // remove that file from the models list - ++model_it; - category_ptr->removeModel(model_ptr); + if (error) { + TRACE("ERROR converting '%s': %s", filename, error); } + } else { + TRACE("ERROR reading '%s': %s", filename, error); + } - converted++; + converted++; #if defined(SIMU) - RTOS_WAIT_MS(200); + RTOS_WAIT_MS(200); #endif - } } #if defined(SDCARD_YAML) || defined(STORAGE_MODELSLIST) - modelslist.save(); - // trigger models list reload modelslist.clear(); + modelslist.load(); #endif } @@ -179,7 +160,7 @@ const char* convertBinModelData(char* filename, int version) memcpy(path, MODELS_PATH, sizeof(MODELS_PATH)-1); path[sizeof(MODELS_PATH)-1] = '/'; strcpy(&path[sizeof(MODELS_PATH)], filename); - + #if STORAGE_CONVERSIONS < 220 if (version == 219) { const char* error = convertModelData_219_to_220(path); @@ -206,7 +187,7 @@ const char* convertBinModelData(char* filename, int version) void eeConvertModel(int id, int version) { TRACE("eeConvertModel(%d,%d)", id, version); - + #if STORAGE_CONVERSIONS < 220 if (version == 219) { convertModelData_219_to_220(id); @@ -269,7 +250,7 @@ bool eeConvert(uint8_t start_version) #if defined(STORAGE_MODELSLIST) modelslist.clear(); #endif - + #if LCD_W >= 212 lcdDrawRect(60, 6*FH+4, 132, 3); #else diff --git a/radio/src/storage/modelslist.cpp b/radio/src/storage/modelslist.cpp index 09b7e9f6b..89a9cd792 100644 --- a/radio/src/storage/modelslist.cpp +++ b/radio/src/storage/modelslist.cpp @@ -20,68 +20,75 @@ */ #include "modelslist.h" + +#include + using std::list; #if defined(SDCARD_YAML) +#include "opentx.h" +#include "storage/sdcard_yaml.h" +#include "yaml/yaml_datastructs.h" +#include "yaml/yaml_labelslist.h" #include "yaml/yaml_parser.h" -#include "yaml/yaml_modelslist.h" -#endif -#include "myeeprom.h" -#include "datastructs.h" -#include "pulses/modules_helpers.h" -#include "strhelpers.h" +#endif #include -ModelsList modelslist; +#include "datastructs.h" +#include "myeeprom.h" +#include "pulses/modules_helpers.h" +#include "strhelpers.h" -ModelCell::ModelCell(const char* name) : valid_rfData(false) +#ifdef DEBUG_LABELS +#define TRACE_LABELS(...) TRACE(__VA_ARGS__) +#else +#define TRACE_LABELS(...) +#endif + +ModelsList modelslist; +ModelMap modelslabels; + +ModelCell::ModelCell(const char *fileName) : valid_rfData(false) { - strncpy(modelFilename, name, sizeof(modelFilename)-1); - modelFilename[sizeof(modelFilename)-1] = '\0'; + strncpy(modelFilename, fileName, sizeof(modelFilename) - 1); + modelFilename[sizeof(modelFilename) - 1] = '\0'; } -ModelCell::ModelCell(const char* name, uint8_t len) : valid_rfData(false) +ModelCell::ModelCell(const char *fileName, uint8_t len) : valid_rfData(false) { if (len > sizeof(modelFilename) - 1) len = sizeof(modelFilename) - 1; - memcpy(modelFilename, name, len); + memcpy(modelFilename, fileName, len); modelFilename[len] = '\0'; } -ModelCell::~ModelCell() -{ -} - -void ModelCell::setModelName(char * name) +void ModelCell::setModelName(char *name) { strncpy(modelName, name, LEN_MODEL_NAME); modelName[LEN_MODEL_NAME] = '\0'; if (modelName[0] == '\0') { - char * tmp; + char *tmp; strncpy(modelName, modelFilename, LEN_MODEL_NAME); - tmp = (char *) memchr(modelName, '.', LEN_MODEL_NAME); - if (tmp != nullptr) - *tmp = 0; + tmp = (char *)memchr(modelName, '.', LEN_MODEL_NAME); + if (tmp != nullptr) *tmp = 0; } } -void ModelCell::setModelName(char* name, uint8_t len) +void ModelCell::setModelName(char *name, uint8_t len) { - if (len > LEN_MODEL_NAME-1) - len = LEN_MODEL_NAME-1; + if (len > LEN_MODEL_NAME - 1) len = LEN_MODEL_NAME - 1; memcpy(modelName, name, len); modelName[len] = '\0'; - if (modelName[0] == 0) { - char * tmp; + if (modelName[0] == '\0') { + char *tmp; strncpy(modelName, modelFilename, LEN_MODEL_NAME); - tmp = (char *) memchr(modelName, '.', LEN_MODEL_NAME); - if (tmp != nullptr) - *tmp = 0; + tmp = (char *)memchr(modelName, '.', LEN_MODEL_NAME); + if (tmp != nullptr) *tmp = 0; } } @@ -90,255 +97,712 @@ void ModelCell::setModelId(uint8_t moduleIdx, uint8_t id) modelId[moduleIdx] = id; } - -void ModelCell::save(FIL* file) -{ -#if !defined(SDCARD_YAML) - f_puts(modelFilename, file); - f_putc('\n', file); -#else - f_puts(" - filename: \"", file); - f_puts(modelFilename, file); - f_puts("\"\n", file); - - f_puts(" name: \"", file); - f_puts(modelName, file); - f_puts("\"\n", file); -#endif -} - -void ModelCell::setRfData(ModelData* model) +void ModelCell::setRfData(ModelData *model) { for (uint8_t i = 0; i < NUM_MODULES; i++) { modelId[i] = model->header.modelId[i]; setRfModuleData(i, &(model->moduleData[i])); - TRACE("<%s/%i> : %X,%X,%X", - strlen(modelName) ? modelName : modelFilename, + TRACE("<%s/%i> : %X,%X,%X", strlen(modelName) ? modelName : modelFilename, i, moduleData[i].type, moduleData[i].subType, modelId[i]); } valid_rfData = true; } -void ModelCell::setRfModuleData(uint8_t moduleIdx, ModuleData* modData) +void ModelCell::setRfModuleData(uint8_t moduleIdx, ModuleData *modData) { moduleData[moduleIdx].type = modData->type; if (modData->type != MODULE_TYPE_MULTIMODULE) { moduleData[moduleIdx].subType = (uint8_t)modData->subType; - } - else { + } else { // do we care here about MM_RF_CUSTOM_SELECTED? probably not... moduleData[moduleIdx].subType = modData->multi.rfProtocol; } } -bool ModelCell::fetchRfData() +//----------------------------------------------------------------------------- +/** + * @brief Gets all models which don't have any labels selected + * + * @param sortby + * @return ModelsVector vector + */ + +ModelsVector ModelMap::getUnlabeledModels() { -#if !defined(SDCARD_YAML) - //TODO: use g_model in case fetching data for current model - // - char buf[256]; - getModelPath(buf, modelFilename); + ModelsVector unlabeledModels; + for (auto model : modelslist) { + if (modelslabels.getLabelsByModel(model).size() == 0) + unlabeledModels.emplace_back(model); + } + sortModelsBy(unlabeledModels, _sortOrder); + return unlabeledModels; +} - FIL file; - uint16_t size; - uint8_t version; +/** + * @brief Returns a sorted list of all models + */ - const char * err = openFileBin(buf, &file, &size, &version); - if (err || version != EEPROM_VER) return false; +ModelsVector ModelMap::getAllModels() +{ + ModelsVector all = modelslist; + sortModelsBy(all, _sortOrder); + return all; +} - FSIZE_t start_offset = f_tell(&file); +/** + * @brief Returns all models that have a sepecified label + * + * @param lbl Label to search + * @return ModelsVector aka vector of all models belonging to a + * label + */ - UINT read; - if ((f_read(&file, buf, LEN_MODEL_NAME, &read) != FR_OK) || (read != LEN_MODEL_NAME)) - goto error; +ModelsVector ModelMap::getModelsByLabel(const std::string &lbl) +{ + int index = getIndexByLabel(lbl); + if (index < 0) return ModelsVector(); + ModelsVector rv; + for (auto it = begin(); it != end(); ++it) { + if (it->first == index) rv.push_back(it->second); + } + sortModelsBy(rv, _sortOrder); + return rv; +} - setModelName(buf); +/** + * @brief Returns all models that are in multiple labels (OR function) + * + * @param lbls Labels to search + * @return ModelsVector aka vector of all models belonging to a + * label + */ - // 1. fetch modelId: NUM_MODULES @ offsetof(ModelHeader, modelId) - // if (f_lseek(&file, start_offset + offsetof(ModelHeader, modelId)) != FR_OK) - // goto error; - if ((f_read(&file, modelId, NUM_MODULES, &read) != FR_OK) || (read != NUM_MODULES)) - goto error; - - // 2. fetch ModuleData: sizeof(ModuleData)*NUM_MODULES @ offsetof(ModelData, moduleData) - if (f_lseek(&file, start_offset + offsetof(ModelData, moduleData)) != FR_OK) - goto error; - - for(uint8_t i=0; i idxvect; + for (const auto &lbl : lbls) { + if (lbl == STR_UNLABELEDMODEL) addunlabeled = true; + int index = getIndexByLabel(lbl); + if (index >= 0) idxvect.push_back(index); } - valid_rfData = true; - f_close(&file); + ModelsVector rv; + for (auto it = begin(); it != end(); ++it) { + for (auto idx : idxvect) { + if (it->first == idx) rv.push_back(it->second); + } + } + + if (addunlabeled) { + ModelsVector unlabeled = getUnlabeledModels(); + rv.insert(rv.end(), unlabeled.begin(), unlabeled.end()); + } + + sortModelsBy(rv, _sortOrder); + return rv; +} + +/** + * @brief Returns all models that are in multiple labels (AND function) + * + * @param lbls Labels to search + * @return ModelsVector aka vector of all models belonging to a + * label + */ + +ModelsVector ModelMap::getModelsInLabels(const LabelsVector &lbls) +{ + if (lbls.size() == 0) return ModelsVector(); + + // Requesting only Unlabeled models + if (lbls.size() == 1 && lbls.at(0) == STR_UNLABELEDMODEL) + return getUnlabeledModels(); + + ModelsVector rv; + + for (const auto &mdl : modelslist) { + bool hasAllLabels = true; + LabelsVector mdllables = getLabelsByModel(mdl); + for (const auto &lbl : lbls) { + if (lbl == STR_UNLABELEDMODEL) // If requesting unlabeled model ignore it + break; + if (std::find(mdllables.begin(), mdllables.end(), lbl) == + mdllables.end()) { + hasAllLabels = false; + break; + } + } + if (hasAllLabels) rv.push_back(mdl); + } + + sortModelsBy(rv, _sortOrder); + return rv; +} + +/** + * @brief Gets all labels that a model currently has selected + * + * @param mdl Model to search + * @return LabelsVector aka vector of all labels selected by a model + */ + +LabelsVector ModelMap::getLabelsByModel(ModelCell *mdl) +{ + if (mdl == nullptr) return LabelsVector(); + LabelsVector rv; + for (auto it = begin(); it != end(); ++it) { + if (it->second == mdl) { + rv.push_back(getLabelByIndex(it->first)); + } + } + return rv; +} + +/** + * @brief Get a map of all labels and their selection status + * @details Returns a map of all the labels and if they are selected in a model. + * + * @param cell ModelCell to search + * @return std::map Map Containing all labels with a boolean + * if they are selected + */ + +std::map ModelMap::getSelectedLabels(ModelCell *cell) +{ + std::map rval; + for (auto lbl : labels) { // Add all labels + if (lbl == "") continue; + rval[lbl] = false; + } + LabelsVector modlbl = getLabelsByModel(cell); + for (const auto &ml : modlbl) { // Set to true if selected by the model + rval[ml] = true; + } + return rval; +} + +/** + * @brief Returns if a label is selected in the model + * + * @param label Label to search + * @param cell Model to search + * @return true Label is selected + * @return false Label not seleced OR label not found + */ + +bool ModelMap::isLabelSelected(const std::string &label, ModelCell *cell) +{ + auto lbm = getLabelsByModel(cell); + if (std::find(lbm.begin(), lbm.end(), label) == lbm.end()) { + return false; + } return true; - - error: - f_close(&file); +} + +/** + * @brief Returns a list of the current labels + * + * @return LabelsVector vector of all labels + */ + +LabelsVector ModelMap::getLabels() +{ + LabelsVector rv; + for (auto &label : labels) { + if (label.size() > 0) rv.emplace_back(label); + } + return rv; +} + +/** + * @brief Adds a label + * @details Checks if the label already exists. If it does it returns the + * 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 + */ + +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); + if (ind < 0) { + labels.push_back(lbl); + setDirty(); + TRACE_LABELS("Added a label %s", lbl.c_str()); + return labels.size() - 1; + } + return ind; +} + +/** + * @brief Adds a label to a model + * + * @param lbl Label to be added + * @param cell Model to add the label + * @param update If true will update the model file + * @return true Couldn't add label, not enough memory available in labels string + * @return false Success + */ + +bool ModelMap::addLabelToModel(const std::string &lbl, ModelCell *cell, + bool update) +{ + // 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) { + TRACE("Cannot add the %s label to the model. Too many labels", lbl.c_str()); + return true; + } + + setDirty(); + int labelindex = addLabel(lbl); + insert(std::pair(labelindex, cell)); + + if (update) updateModelFile(cell); // Write labels into model + return false; +} -#else +/** + * @brief Adds a label to the filter, used in yaml_labelslist on load + * + * @param label Label to be added + */ + +void ModelMap::addFilteredLabel(const std::string &lbl) +{ + int ind = getIndexByLabel(lbl); + if (ind >= 0) filtlbls.insert(ind); +} + +bool ModelMap::isLabelFiltered(const std::string &lbl) +{ + if (filtlbls.find(getIndexByLabel(lbl)) != filtlbls.end()) return true; return false; +} + +void ModelMap::setSortOrder(ModelsSortBy sortby) +{ + if(sortby < SORT_COUNT && sortby >= NO_SORT) { + _sortOrder = sortby; + setDirty(); + } +} + +/** + * @brief Removes a label from a model + * + * @param label Label to be removed + * @param cell Model to remove the label from + * @return true Couldn't find the label + * @return false Success + */ + +bool ModelMap::removeLabelFromModel(const std::string &label, ModelCell *cell, + bool update) +{ + int lblind = getIndexByLabel(label); + if (lblind < 0) return true; + bool rv = true; + // Erase items that match in the map + for (ModelMap::const_iterator itr = cbegin(); itr != cend();) { + itr = (itr->first == lblind && itr->second == cell) ? erase(itr) + : std::next(itr); + setDirty(); + rv = false; + } + + if (update) updateModelFile(cell); // Write labels into model + + return rv; +} + +/** + * @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. + * + * @param label Label to be removed + * @return true Label wasn't found or not empty + * @return false Success + */ + +bool ModelMap::removeLabel( + const std::string &label, + std::function progress) +{ + renameLabel(label, "", std::move(progress)); + for (auto &lbl : labels) { + if (lbl == label && getModelsByLabel(lbl).size() == 0) { + lbl = ""; + setDirty(); + return false; + } + } + return true; +} + +/** + * @brief Moves a labels order + * + * @param current Index of the label to move + * @param toindex Index where to place the new label + * @return 1 Failure + * @return 0 Success + */ + +bool ModelMap::moveLabelTo(unsigned curind, unsigned newind) +{ + if (curind == newind || curind >= labels.size() || newind >= labels.size()) + return true; + + if (labels.at(curind) == "") return true; + + LabelsVector newOrder = labels; + + if (curind < newind) { // Move forward + std::rotate(newOrder.rend() - curind - 1, newOrder.rend() - curind, + newOrder.rend() - newind); + } else { // Move back + std::rotate(newOrder.begin() + curind, newOrder.begin() + curind + 1, + newOrder.begin() + newind + 1); + } + + // Reload the new labels order + modelslist.save(newOrder); + modelslist.clear(); + modelslist.load(); + + return false; +} + +/** + * @brief Rename a label + * @details Opens all models which have a label that matches the string. + * Renames the label with string and saves file. + * If working on the current model it replaces the label string in + * g_model and saves the current copy in memory. + * + * @param from Label to search + * @param to Replacement label + * @return true failure Label couldn't be found, YAML couldn't be read + * @return false success + */ + +bool ModelMap::renameLabel( + const std::string &from, const std::string &to, + std::function progress) +{ + DEBUG_TIMER_START(debugTimerYamlScan); + + if (from == "") return true; + + ModelData *modeldata = (ModelData *)malloc(sizeof(ModelData)); + if (!modeldata) { + TRACE("Labels: Out Of Memory"); + return true; + } + + // Force a write of any changes in memory + storageCheck(true); + + bool fault = false; + ModelsVector mods = getModelsByLabel(from); // Find all models to be renamed + int i = 0; + for (const auto &modcell : mods) { + if (progress != nullptr) { + progress(modcell->modelFilename, (i++) * 100 / mods.size()); + } + + 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(); + if (nlen > LABELS_LENGTH - 1) { + fault = true; + TRACE("Labels: Rename Error! Labels too long on %s - %s", + modeldata->header.name, modcell->modelFilename); + 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 + 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; + } + + char path[256]; + getModelPath(path, modcell->modelFilename); + + if (modcell == modelslist.getCurrentModel()) { + // If working on the current model, write current data to file instead + memcpy(g_model.header.labels, modeldata->header.labels, LABELS_LENGTH); + fault = (writeFileYaml(path, get_modeldata_nodes(), + (uint8_t *)&g_model, 0) != NULL); + } else { + fault = (writeFileYaml(path, get_modeldata_nodes(), + (uint8_t *)modeldata, 0) != NULL); + } +#if defined(SIMU) + if (SIMU_SLEEP_OR_EXIT_MS(100)) break; #endif -} - -ModelsCategory::ModelsCategory(const char * name) -{ - memset(this->name, 0, sizeof(this->name)); - strncpy(this->name, name, sizeof(this->name)); -} - -ModelsCategory::ModelsCategory(const char * name, uint8_t len) -{ - if (len > sizeof(this->name)-1) - len = sizeof(this->name)-1; - - memset(this->name, 0, sizeof(this->name)); - memcpy(this->name, name, len); - this->name[len] = '\0'; -} - -ModelsCategory::~ModelsCategory() -{ - for (auto * model: *this) { - delete model; } -} -ModelCell * ModelsCategory::addModel(const char * fileName, const char* modelName) -{ - if (!name) return NULL; - - ModelCell * result = new ModelCell(fileName); - if(modelName) - strncpy(result->modelName, modelName, LEN_MODEL_NAME); - push_back(result); - return result; -} - -void ModelsCategory::removeModel(ModelCell * model) -{ - delete model; - remove(model); -} - -void ModelsCategory::moveModel(ModelCell * model, int8_t step) -{ - ModelsCategory::iterator current = begin(); - for (; current != end(); current++) { - if (*current == model) { - break; + // Rename the label + for (auto &lbl : labels) { + if (lbl == from) { + lbl = to; + setDirty(true); } } - ModelsCategory::iterator new_position = current; - if (step > 0) { - while (step >= 0 && new_position != end()) { - new_position++; - step--; - } - } - else { - while (step < 0 && new_position != begin()) { - new_position--; - step++; - } - } + // Make sure to leave at 100, to kill rename dialog + if (progress != nullptr) progress("", 100); - insert(new_position, 1, *current); - erase(current); -} + free(modeldata); -int ModelsCategory::getModelIndex(const ModelCell* model) -{ - int idx = 0; - for (auto m : *this) { - if (model == m) - return idx; + // Issue a rescan all of all models. + modelslist.clear(); + modelslist.load(); - ++idx; - } - - return -1; -} - -void ModelsCategory::save(FIL * file) -{ -#if !defined(SDCARD_YAML) - f_puts("[", file); - f_puts(name, file); - f_puts("]", file); - f_putc('\n', file); -#else - f_puts("- \"", file); - f_puts(name, file); - f_puts("\":\n", file); +#if defined(DEBUG_TIMERS) + DEBUG_TIMER_SAMPLE(debugTimerYamlScan); + TRACE("Labels: Time to rename %d labels %luus", mods.size(), + debugTimers[debugTimerYamlScan].getLast()); #endif - for (list::iterator it = begin(); it != end(); ++it) { - (*it)->save(file); + + return fault; +} + +/** + * @brief Returns a comma separated list of the labels + * + * @param curmod Module + * @return std::string + */ + +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++; + } + } + if (numModels == 0) allLabels = noresults; + + return allLabels; +} + +/** + * @brief Removes all models from the map + * + * @param cell Model to remove + * @return true Failure finding a model + * @return false Success + */ + +bool ModelMap::removeModels(ModelCell *cell) +{ + bool rv = true; + // Erase items that match in the map + for (ModelMap::const_iterator itr = cbegin(); itr != cend();) { + if (itr->second == cell) { + itr = erase(itr); + setDirty(); + rv = false; + } else { + itr = std::next(itr); + } + } + return rv; +} + +/** + * @brief Opens a model.yml File and writes labels data into it. + * @details If the cell is current model then write the labels data to g_model + * and mark as dirty + * + * @param cell + * @return true + * @return false + */ + +bool ModelMap::updateModelFile(ModelCell *cell) +{ + // Update memory copy if on current model + if (cell == modelslist.getCurrentModel()) { + strncpy(g_model.header.labels, getLabelString(cell).c_str(), + LABELS_LENGTH - 1); + g_model.header.labels[LABELS_LENGTH - 1] = '\0'; + storageDirty(EE_MODEL); + return false; + } + + ModelData *modeldata = (ModelData *)malloc(sizeof(ModelData)); + if (!modeldata) { + TRACE("Labels: Out Of Memory"); + return true; + } + + bool fault = false; + readModelYaml(cell->modelFilename, (uint8_t *)modeldata, sizeof(ModelData)); + + strncpy(modeldata->header.labels, getLabelString(cell).c_str(), + LABELS_LENGTH - 1); + modeldata->header.labels[LABELS_LENGTH - 1] = '\0'; + + char path[256]; + getModelPath(path, cell->modelFilename); + fault = (writeFileYaml(path, get_modeldata_nodes(), (uint8_t *)modeldata, 0) != + NULL); + + free(modeldata); + +#if defined(DEBUG_TIMERS) + DEBUG_TIMER_SAMPLE(debugTimerYamlScan); + TRACE("Labels: Time to add/remove labels %luus", + debugTimers[debugTimerYamlScan].getLast()); +#endif + + return fault; +} + +/** + * @brief Sorts a ModelsVector by sortby + * + * @param mv ModeslVector to sort + * @param sortby NO_SORT, NAME_ASC, NAME_DES, DATE_ASC, DATE_DES, + */ + +void ModelMap::sortModelsBy(ModelsVector &mv, ModelsSortBy sortby) +{ + if (sortby == DATE_DES) { + std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool { + return a->lastOpened > b->lastOpened; + }); + } else if (sortby == DATE_ASC) { + std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool { + return a->lastOpened < b->lastOpened; + }); + } else if (sortby == NAME_ASC) { + std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool { + return strcmp(a->modelName, b->modelName) < 0; + }); + } else if (sortby == NAME_DES) { + std::sort(mv.begin(), mv.end(), [](ModelCell *a, ModelCell *b) -> bool { + return strcmp(a->modelName, b->modelName) > 0; + }); } } -ModelsList::ModelsList() +/** + * @brief Sets the ModelMap to dirty. + * @details Causes labels.yml to be written after a delay in + * sdcard_common.cpp->storageCheck() + */ + +void ModelMap::setDirty(bool save) { - init(); + _isDirty = true; + storageDirty(EE_LABELS); + if (save) storageCheck(true); } -ModelsList::~ModelsList() -{ - clear(); -} +//----------------------------------------------------------------------------- + +ModelsList::ModelsList() { init(); } + +ModelsList::~ModelsList() { clear(); } void ModelsList::init() { loaded = false; - currentCategory = nullptr; currentModel = nullptr; - modelsCount = 0; } void ModelsList::clear() { - for (list::iterator it = categories.begin(); it != categories.end(); ++it) { - delete *it; - } - categories.clear(); + std::vector::clear(); init(); } +/** + * @brief Load and parse the models.txt file + * + * @return true On Success + * @return false On Failure + */ + bool ModelsList::loadTxt() { - char line[LEN_MODELS_IDX_LINE+1]; - ModelsCategory * category = nullptr; - ModelCell * model = nullptr; + char line[LEN_MODELS_IDX_LINE + 1]; + ModelCell *model = nullptr; - FRESULT result = f_open(&file, RADIO_MODELSLIST_PATH, FA_OPEN_EXISTING | FA_READ); + FRESULT result = + f_open(&file, RADIO_MODELSLIST_PATH, FA_OPEN_EXISTING | FA_READ); if (result == FR_OK) { - // TXT reader while (readNextLine(line, LEN_MODELS_IDX_LINE)) { - int len = strlen(line); // TODO could be returned by readNextLine - if (len > 2 && line[0] == '[' && line[len-1] == ']') { - line[len-1] = '\0'; - category = new ModelsCategory(&line[1]); - categories.push_back(category); - } - else if (len > 0) { + int len = strlen(line); // TODO could be returned by readNextLine + if (len > 2 && line[0] == '[' && line[len - 1] == ']') { + line[len - 1] = '\0'; + } else if (len > 0) { model = new ModelCell(line); - if (!category) { - category = new ModelsCategory("Models"); - categories.push_back(category); - } - category->push_back(model); + push_back(model); if (!strncmp(line, g_eeGeneral.currModelFilename, LEN_MODEL_FILENAME)) { - currentCategory = category; currentModel = model; } - model->fetchRfData(); - modelsCount += 1; } } @@ -350,58 +814,195 @@ bool ModelsList::loadTxt() } #if defined(SDCARD_YAML) + +/** + * @brief Opens a YAML file, reads the data and updates the ModelCell + * + * @param cell Model to update + */ + +void ModelMap::updateModelCell(ModelCell *cell) +{ + modelslabels.removeModels(cell); + + ModelData *model = (ModelData *)malloc(sizeof(ModelData)); + if (!model) { + TRACE("Labels: Out Of Memory"); + return; + } + + TRACE("Labels: Updating model %s", cell->modelFilename); + 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, ","); + } + + // Save Module Data + cell->setRfData(model); + + cell->_isDirty = false; + free(model); +} + +/** + * @brief Creates a Hash based on the file information + * + * @param buffer Buffer to store the output data + * @param finfo Files info handle + * @return char* Pointer to buffer supplied + */ + +char *FILInfoToHexStr(char buffer[17], FILINFO *finfo) +{ + char *str = buffer; + for (unsigned int i = 0; i < sizeof(FInfoH); i++) { + sprintf(str, "%02x", *((uint8_t *)finfo + i)); + str += 2; + } + return buffer; +} + +/** + * @brief Loads the Labels and Models from the labels.yml file + * + * @return true On success + * @return false On failure + */ + bool ModelsList::loadYaml() { - char line[LEN_MODELS_IDX_LINE+1]; - FRESULT result = f_open(&file, MODELSLIST_YAML_PATH, FA_OPEN_EXISTING | FA_READ); + // Clear labels + map + modelslist.clear(); + modelslabels.clear(); + fileHashInfo.clear(); - if (result != FR_OK) { - // move the YaML models list from the old to the new place - FILINFO fno; - if (f_stat(FALLBACK_MODELSLIST_YAML_PATH, &fno) == FR_OK) { - if (!sdCopyFile(FALLBACK_MODELSLIST_YAML_PATH, MODELSLIST_YAML_PATH)) { - f_unlink(FALLBACK_MODELSLIST_YAML_PATH); - result = - f_open(&file, MODELSLIST_YAML_PATH, FA_OPEN_EXISTING | FA_READ); + DEBUG_TIMER_START(debugTimerYamlScan); + + // Scan all models in folder + DIR moddir; + FILINFO finfo; + if (f_opendir(&moddir, PATH_SEPARATOR MODELS_PATH) == FR_OK) { + for (;;) { + FRESULT res = f_readdir(&moddir, &finfo); + if (res != FR_OK || finfo.fname[0] == 0) break; + if (finfo.fattrib & AM_DIR) continue; + int len = strlen(finfo.fname); + if (len < 5 || + strcasecmp(finfo.fname + len - 4, YAML_EXT) || // Skip non .yml files + strcasecmp(finfo.fname, LABELS_FILENAME) == + 0 || // Skip labels.yml file + strcasecmp(finfo.fname, MODELS_FILENAME) == + 0 || // Skip models.yml file + (finfo.fattrib & AM_DIR)) { // Skip sub dirs + continue; } + + // Store hash & filename + filedat cf; + FILInfoToHexStr(cf.hash, &finfo); + cf.name = finfo.fname; + cf.celladded = false; + if (!strncmp(finfo.fname, g_eeGeneral.currModelFilename, + LEN_MODEL_FILENAME)) + cf.curmodel = true; + else + cf.curmodel = false; + fileHashInfo.push_back(cf); + TRACE_LABELS("File - %s \r\n HASH - %s - CM: %s", finfo.fname, cf.hash, + cf.curmodel ? "Y" : "N"); } + f_closedir(&moddir); } +#if defined(DEBUG_TIMERS) + DEBUG_TIMER_SAMPLE(debugTimerYamlScan); + TRACE("Lables: Time to scan models folder %luus", + debugTimers[debugTimerYamlScan].getLast()); +#endif + + // Scan labels.yml + char line[LEN_MODELS_IDX_LINE + 1]; + FRESULT result = + f_open(&file, LABELSLIST_YAML_PATH, FA_OPEN_EXISTING | FA_READ); if (result == FR_OK) { - // YAML reader - TRACE("YAML modelslist reader"); - YamlParser yp; - void* ctx = get_modelslist_iter( - g_eeGeneral.currModelFilename, - strnlen(g_eeGeneral.currModelFilename, LEN_MODEL_FILENAME)); - - yp.init(get_modelslist_parser_calls(), ctx); - - UINT bytes_read=0; + void *ctx = get_labelslist_iter(); + yp.init(get_labelslist_parser_calls(), ctx); + UINT bytes_read = 0; while (f_read(&file, line, sizeof(line), &bytes_read) == FR_OK) { - - // reached EOF? - if (bytes_read == 0) - break; - + if (bytes_read == 0) break; if (f_eof(&file)) yp.set_eof(); - if (yp.parse(line, bytes_read) != YamlParser::CONTINUE_PARSING) - break; + if (yp.parse(line, bytes_read) != YamlParser::CONTINUE_PARSING) break; } - f_close(&file); - return true; } - return false; +#if defined(DEBUG_TIMERS) + DEBUG_TIMER_SAMPLE(debugTimerYamlScan); + TRACE("Lables: Time to scan labels.yml %luus", + debugTimers[debugTimerYamlScan].getLast()); +#endif + + // Add modelcells for any remaining models that weren't in labels.yml + for (auto &filehash : fileHashInfo) { + ModelCell *model = NULL; + if (filehash.celladded == false) { + TRACE_LABELS(" Created a modelcell for %s, not in labels.yml", + filehash.name.c_str()); + model = new ModelCell(filehash.name.c_str()); + strcpy(model->modelFinfoHash, filehash.hash); + modelslist.push_back(model); + filehash.celladded = true; + model->_isDirty = true; + } + } + + // Scan all models, to see if update needed + bool updatelabelsyml = false; + for (auto &model : modelslist) { + // Open and read each model if it's marked as dirty + if (model->_isDirty) { + updatelabelsyml = true; + modelslabels.updateModelCell(model); + } + } + + fileHashInfo.clear(); + + // If any items differed save the file + if (updatelabelsyml == true) { + TRACE_LABELS("LABELS.YML Wasn't in sync. Needs to be saved"); + modelslist.save(); + } else { + TRACE_LABELS("LABELS.YML Is in Sync! No models were read"); + } + + // If no labels found. Add a favorites label + if (modelslabels.getLabels().size() == 0) { + modelslabels.addLabel(STR_FAVORITE_LABEL); + } + + return true; } #endif +/** + * @brief Called to load the model data from file + * + * @param fmt Format::txt - Opens models.txt file, Format::yaml_txt - Opens + * labels.yml + * @return true on success + * @return false on failure + */ + bool ModelsList::load(Format fmt) { - if (loaded) - return true; + if (loaded) return true; bool res = false; #if !defined(SDCARD_YAML) @@ -410,8 +1011,7 @@ bool ModelsList::load(Format fmt) #else FILINFO fno; if (fmt == Format::txt || - (fmt == Format::yaml_txt && - f_stat(MODELSLIST_YAML_PATH, &fno) != FR_OK && + (fmt == Format::yaml_txt && f_stat(MODELSLIST_YAML_PATH, &fno) != FR_OK && f_stat(FALLBACK_MODELSLIST_YAML_PATH, &fno) != FR_OK)) { res = loadTxt(); } else { @@ -420,14 +1020,16 @@ bool ModelsList::load(Format fmt) #endif if (!currentModel) { - if (categories.empty()) { - currentCategory = new ModelsCategory("Models"); - categories.push_back(currentCategory); + TRACE("ERROR no Current Model Found"); + if (modelslist.size()) { + modelslist.setCurrentModel(modelslist.at(0)); + TRACE(" - Set current model to first available"); } else { - currentCategory = *categories.begin(); - if (!currentCategory->empty()) { - currentModel = *currentCategory->begin(); - } + TRACE(" - No Models Found, making a new one"); + // No models found, make a new one + auto model = modelslist.addModel(createModel(), true); + modelslist.setCurrentModel(model); + updateCurrentModelCell(); } } @@ -435,60 +1037,149 @@ bool ModelsList::load(Format fmt) return res; } -void ModelsList::save() +/** + * @brief Writes labels.yml file + * + * @return const char* NULL on success + * @return const char* Error String on failure + */ + +const char *ModelsList::save(LabelsVector newOrder) { #if !defined(SDCARD_YAML) - FRESULT result = f_open(&file, RADIO_MODELSLIST_PATH, FA_CREATE_ALWAYS | FA_WRITE); + FRESULT result = + f_open(&file, RADIO_MODELSLIST_PATH, FA_CREATE_ALWAYS | FA_WRITE); #else - FRESULT result = f_open(&file, MODELSLIST_YAML_PATH, FA_CREATE_ALWAYS | FA_WRITE); + FRESULT result = + f_open(&file, LABELSLIST_YAML_PATH, FA_CREATE_ALWAYS | FA_WRITE); #endif - if (result != FR_OK) return; + if (result != FR_OK) return "Couldn't open labels.yml for writing"; - for (list::iterator it = categories.begin(); - it != categories.end(); ++it) { - (*it)->save(&file); + // Save current selection + f_puts("Labels:\r\n", &file); + + std::string cursel = modelslabels.getCurrentLabel(); + if(newOrder.empty()) + newOrder = modelslabels.getLabels(); + for (auto &lbl : newOrder) { + f_printf(&file, " %s:\r\n", lbl.c_str()); + if (modelslabels.isLabelFiltered(lbl)) + f_printf(&file, " selected: true\r\n", lbl.c_str()); } + // Save current sort order + f_printf( &file, "Sort: %d\r\n", modelslabels.sortOrder()); + + f_puts("Models:\r\n", &file); + for (auto &model : modelslist) { + f_puts(" ", &file); + f_puts(model->modelFilename, &file); + f_puts(":\r\n", &file); + + f_puts(" hash: \"", &file); + f_puts(model->modelFinfoHash, &file); + f_puts("\"\r\n", &file); + + f_puts(" name: \"", &file); + f_puts(model->modelName, &file); + f_puts("\"\r\n", &file); + + for (int i = 0; i < NUM_MODULES; i++) { + if (model->modelId[i]) + f_printf(&file, " " MODULE_ID_STR ": %u\r\n", i, + (unsigned int)model->modelId[i]); + if (model->moduleData[i].type) + f_printf(&file, " " MODULE_TYPE_STR ": %u\r\n", i, + (unsigned int)model->moduleData[i].type); + if (model->moduleData[i].subType) + f_printf(&file, " " MODULE_RFPROTOCOL_STR ": %u\r\n", i, + (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); + +#if LEN_BITMAP_NAME > 0 + f_puts(" bitmap: \"", &file); + f_puts(model->modelBitmap, &file); + f_puts("\"\r\n", &file); +#endif + f_puts(" lastopen: ", &file); + f_puts(std::to_string(model->lastOpened).c_str(), &file); + f_puts("\r\n", &file); + } + + f_puts("\r\n", &file); f_close(&file); + modelslabels._isDirty = false; + + return NULL; } -void ModelsList::setCurrentCategory(ModelsCategory * cat) -{ - currentCategory = cat; -} +/** + * @brief set the currently loaded model. + * + * @param cell Model to set as current + */ -int ModelsList::getCurrentCategoryIdx() const -{ - if (!currentCategory) - return -1; - - int idx = 0; - for (auto cat : categories) { - if (currentCategory == cat) - return idx; - - ++idx; - } - - return -1; -} - -void ModelsList::setCurrentModel(ModelCell * cell) +void ModelsList::setCurrentModel(ModelCell *cell) { currentModel = cell; - if (!currentModel->valid_rfData) - currentModel->fetchRfData(); + struct gtm t; + gettime(&t); + cell->lastOpened = gmktime(&t); + modelslabels.setDirty(); } -bool ModelsList::readNextLine(char * line, int maxlen) +/** + * @brief Takes the current g_model + filename data in memory and update + * the ModelCell data to match. + */ + +void ModelsList::updateCurrentModelCell() +{ + if (currentModel) { +#if LEN_BITMAP_NAME > 0 + strncpy(currentModel->modelBitmap, g_model.header.bitmap, LEN_BITMAP_NAME); + currentModel->modelBitmap[LEN_BITMAP_NAME - 1] = '\0'; +#endif + strncpy(currentModel->modelFilename, g_eeGeneral.currModelFilename, LEN_MODEL_FILENAME); + currentModel->modelFilename[LEN_MODEL_FILENAME] = '\0'; + currentModel->setModelName(g_model.header.name); + currentModel->setRfData(&g_model); + modelslabels.setDirty(); + } else { + TRACE("ModelList Error - No Current Model"); + } +} + +/** + * @brief Reads a line from a file. Used by loadTxt + * + * @param line Storage for the read line + * @param maxlen maximum read length + * @return true Success + * @return false Failure + */ + +bool ModelsList::readNextLine(char *line, int maxlen) { if (f_gets(line, maxlen, &file) != NULL) { int curlen = strlen(line) - 1; - if (line[curlen] == '\n') { // remove unwanted chars if file was edited using windows + if (line[curlen] == + '\n') { // remove unwanted chars if file was edited using windows if (line[curlen - 1] == '\r') { line[curlen - 1] = 0; - } - else { + } else { line[curlen] = 0; } } @@ -497,58 +1188,121 @@ bool ModelsList::readNextLine(char * line, int maxlen) return false; } -ModelsCategory * ModelsList::createCategory(bool save) -{ - return createCategory("Category", save); -} +/** + * @brief Creates a new ModelCell + * + * @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 + * @return ModelCell* New Model + */ -ModelsCategory * ModelsList::createCategory(const char* name, bool save) +ModelCell *ModelsList::addModel(const char *fileName, bool save, ModelCell *copyCell) { - ModelsCategory * result = new ModelsCategory(name); - categories.push_back(result); + ModelCell *result = new ModelCell(fileName); + if(copyCell != nullptr) { // Duplicate all data + memcpy(result, copyCell, sizeof(ModelCell)); + } + + // Set the new File Name + strncpy(result->modelFilename, fileName, LEN_MODEL_FILENAME); + result->modelFilename[LEN_MODEL_FILENAME] = '\0'; + + // Add to the ModelsList + push_back(result); + + // Force save to labels.yml if (save) this->save(); return result; } -ModelCell * ModelsList::addModel(ModelsCategory * category, const char * fileName, const char* modelName, bool save) +/** + * @brief Removes a model from the list and the modelmap. + * @details The models filename is moved into the sub folder DELETED_MODELS_PATH + * rather than deleting the model. If left in current location would + * be re-scanned on boot + * + * @param model Model to be deleted + * @return false Successfully removed + * @return true Failure + */ + +bool ModelsList::removeModel(ModelCell *model) { - ModelCell * result = category->addModel(fileName, modelName); - TRACE("modelName is %s", result->modelName); - modelsCount++; - if (save) this->save(); - return result; + erase(std::remove(begin(), end(), model), end()); + modelslabels.removeModels(model); + + // Create deleted folder if it doesn't exist + DIR deletedFolder; + FRESULT result = f_opendir(&deletedFolder, DELETED_MODELS_PATH); + if (result != FR_OK) { + if (result == FR_NO_PATH) result = f_mkdir(DELETED_MODELS_PATH); + if (result != FR_OK) { + TRACE("Unable to create deleted models folder"); + return true; + } + } + + // Move model into deleted folder. If not moved will be re-added on next + // reboot + if (!sdCopyFile(model->modelFilename, MODELS_PATH, model->modelFilename, + DELETED_MODELS_PATH)) { + char curFilename[sizeof(MODELS_PATH) + LEN_MODEL_FILENAME + 2] = ""; + strcat(curFilename, MODELS_PATH PATH_SEPARATOR); + strcat(curFilename, model->modelFilename); + TRACE_LABELS("Deleting Model %s", model->modelFilename); + + if (f_unlink(curFilename) != FR_OK) { + TRACE("Labels: Unable to delete file"); + return true; + } + } + + // Free memory + delete(model); + + return false; } -void ModelsList::removeCategory(ModelsCategory * category) +/** + * @brief Moves a model in the list for customization of order + * + * @param curindex Index of the model to move + * @param toindex Destination index of the model + * @return true Failure + * @return false Success + */ + +bool ModelsList::moveModelTo(unsigned curindex, unsigned toindex) { - modelsCount -= category->size(); - delete category; - categories.remove(category); + if (curindex == toindex || curindex >= size() || toindex >= size()) + return true; + + if (curindex < toindex) { // Move forward + std::rotate(rend() - curindex - 1, rend() - curindex, rend() - toindex); + } else { // Move back + std::rotate(begin() + curindex, begin() + curindex + 1, + begin() + toindex + 1); + } + + modelslabels.setDirty(); + return false; } -void ModelsList::removeModel(ModelsCategory * category, ModelCell * model) -{ - category->removeModel(model); - modelsCount--; - save(); -} +/** + * @brief Checks if a models RF module has a unique id + * + * @param moduleIdx Module number 0 <-> NUM_MODULES + * @param warn_buf Buffer to store the warning string + * @param warn_buf_len Length of buffer + * @return true ID is unique + * @return false ID is used + */ -void ModelsList::moveModel(ModelsCategory * category, ModelCell * model, int8_t step) +bool ModelsList::isModelIdUnique(uint8_t moduleIdx, char *warn_buf, + size_t warn_buf_len) { - category->moveModel(model, step); - save(); -} - -void ModelsList::moveModel(ModelCell * model, ModelsCategory * previous_category, ModelsCategory * new_category) -{ - previous_category->remove(model); - new_category->push_back(model); - save(); -} - -bool ModelsList::isModelIdUnique(uint8_t moduleIdx, char* warn_buf, size_t warn_buf_len) -{ - ModelCell* modelCell = modelslist.getCurrentModel(); + ModelCell *modelCell = modelslist.getCurrentModel(); if (!modelCell || !modelCell->valid_rfData) { // in doubt, pretend it's unique return true; @@ -559,45 +1313,37 @@ bool ModelsList::isModelIdUnique(uint8_t moduleIdx, char* warn_buf, size_t warn_ uint8_t subType = modelCell->moduleData[moduleIdx].subType; uint8_t additionalOnes = 0; - char* curr = warn_buf; + char *curr = warn_buf; curr[0] = 0; bool hit_found = false; - const std::list& cats = modelslist.getCategories(); - std::list::const_iterator catIt = cats.begin(); - for (;catIt != cats.end(); catIt++) { - for (ModelsCategory::const_iterator it = (*catIt)->begin(); it != (*catIt)->end(); it++) { - if (modelCell == *it) - continue; - if (!(*it)->valid_rfData) - continue; + for (auto it = begin(); it != end(); ++it) { + if (modelCell == *it) continue; - if ((type != MODULE_TYPE_NONE) && - (type == (*it)->moduleData[moduleIdx].type) && - (subType == (*it)->moduleData[moduleIdx].subType) && - (modelId == (*it)->modelId[moduleIdx])) { + if (!(*it)->valid_rfData) continue; - // Hit found! - hit_found = true; + if ((type != MODULE_TYPE_NONE) && + (type == (*it)->moduleData[moduleIdx].type) && + (subType == (*it)->moduleData[moduleIdx].subType) && + (modelId == (*it)->modelId[moduleIdx])) { + // Hit found! + hit_found = true; - const char * modelName = (*it)->modelName; - const char * modelFilename = (*it)->modelFilename; + const char *modelName = (*it)->modelName; + const char *modelFilename = (*it)->modelFilename; - // you cannot rely exactly on WARNING_LINE_LEN so using WARNING_LINE_LEN-2 (-2 for the ",") - if ((warn_buf_len - 2 - (curr - warn_buf)) > LEN_MODEL_NAME) { - if (warn_buf[0] != 0) - curr = strAppend(curr, ", "); - if (modelName[0] == 0) { - size_t len = min(strlen(modelFilename),LEN_MODEL_NAME); - curr = strAppendFilename(curr, modelFilename, len); - } - else - curr = strAppend(curr, modelName, LEN_MODEL_NAME); - } - else { - additionalOnes++; - } + // you cannot rely exactly on WARNING_LINE_LEN so using WARNING_LINE_LEN-2 + // (-2 for the ",") + if ((warn_buf_len - 2 - (curr - warn_buf)) > LEN_MODEL_NAME) { + if (warn_buf[0] != 0) curr = strAppend(curr, ", "); + if (modelName[0] == 0) { + size_t len = min(strlen(modelFilename), LEN_MODEL_NAME); + curr = strAppendFilename(curr, modelFilename, len); + } else + curr = strAppend(curr, modelName, LEN_MODEL_NAME); + } else { + additionalOnes++; } } } @@ -611,9 +1357,17 @@ bool ModelsList::isModelIdUnique(uint8_t moduleIdx, char* warn_buf, size_t warn_ return !hit_found; } +/** + * @brief Finds the next free RF Module ID + * + * @param moduleIdx RF Module to search (Internal/External) 0 <-> NUM_MODULES + * @return 0 No unused ID found + * @return uint8_t Next free ID + */ + uint8_t ModelsList::findNextUnusedModelId(uint8_t moduleIdx) { - ModelCell * modelCell = modelslist.getCurrentModel(); + ModelCell *modelCell = modelslist.getCurrentModel(); if (!modelCell || !modelCell->valid_rfData) { return 0; } @@ -623,25 +1377,19 @@ uint8_t ModelsList::findNextUnusedModelId(uint8_t moduleIdx) uint8_t usedModelIds[(MAX_RXNUM + 7) / 8]; memset(usedModelIds, 0, sizeof(usedModelIds)); - - const std::list & cats = modelslist.getCategories(); - for (auto catIt = cats.begin(); catIt != cats.end(); catIt++) { - for (auto it = (*catIt)->begin(); it != (*catIt)->end(); it++) { - if (modelCell == *it) - continue; - if (!(*it)->valid_rfData) - continue; + for (auto it = begin(); it != end(); it++) { + if (modelCell == *it) continue; - // match module type and RF protocol - if ((type != MODULE_TYPE_NONE) && - (type == (*it)->moduleData[moduleIdx].type) && - (subType == (*it)->moduleData[moduleIdx].subType)) { + if (!(*it)->valid_rfData) continue; - uint8_t id = (*it)->modelId[moduleIdx]; - uint8_t mask = 1 << (id & 7u); - usedModelIds[id >> 3u] |= mask; - } + // match module type and RF protocol + if ((type != MODULE_TYPE_NONE) && + (type == (*it)->moduleData[moduleIdx].type) && + (subType == (*it)->moduleData[moduleIdx].subType)) { + uint8_t id = (*it)->modelId[moduleIdx]; + uint8_t mask = 1 << (id & 7u); + usedModelIds[id >> 3u] |= mask; } } @@ -656,13 +1404,3 @@ uint8_t ModelsList::findNextUnusedModelId(uint8_t moduleIdx) // failed finding something... return 0; } - -void ModelsList::onNewModelCreated(ModelCell* cell, ModelData* model) -{ - cell->setModelName(model->header.name); - cell->setRfData(model); - - uint8_t new_id = findNextUnusedModelId(INTERNAL_MODULE); - model->header.modelId[INTERNAL_MODULE] = new_id; - cell->setModelId(INTERNAL_MODULE, new_id); -} diff --git a/radio/src/storage/modelslist.h b/radio/src/storage/modelslist.h index 6efe21ab0..1fd724540 100644 --- a/radio/src/storage/modelslist.h +++ b/radio/src/storage/modelslist.h @@ -23,86 +23,189 @@ #define _MODELSLIST_H_ #include + +#include +#include #include +#include +#include +#include +#include #include "sdcard.h" + #if !defined(SDCARD_YAML) #include "sdcard_raw.h" #endif #include "dataconstants.h" - -// #define MODELCELL_WIDTH 172 -// #define MODELCELL_HEIGHT 59 +#include "rtc.h" // modelXXXXXXX.bin F,FF F,3F,FF\r\n -#define LEN_MODELS_IDX_LINE (LEN_MODEL_FILENAME + sizeof(" F,FF F,3F,FF\r\n")-1) +#define LEN_MODELS_IDX_LINE \ + (LEN_MODEL_FILENAME + sizeof(" F,FF F,3F,FF\r\n") - 1) + +#define DEFAULT_MODEL_SORT NAME_ASC struct ModelData; struct ModuleData; -struct SimpleModuleData -{ - uint8_t type; - uint8_t subType; +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] = {}; + 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] = ""; +#endif + gtime_t lastOpened = 0; + bool _isDirty = true; - bool valid_rfData; - uint8_t modelId[NUM_MODULES]; - SimpleModuleData moduleData[NUM_MODULES]; + bool valid_rfData; + uint8_t modelId[NUM_MODULES] = {0, 0}; + SimpleModuleData moduleData[NUM_MODULES]; - explicit ModelCell(const char * name); - explicit ModelCell(const char * name, uint8_t len); - ~ModelCell(); + explicit ModelCell(const char *fileName); + explicit ModelCell(const char *fileName, uint8_t len); - void save(FIL * file); + void setModelName(char *name); + void setModelName(char *name, uint8_t len); + void setRfData(ModelData *model); - void setModelName(char * name); - void setModelName(char* name, uint8_t len); - void setRfData(ModelData * model); - - void setModelId(uint8_t moduleIdx, uint8_t id); - void setRfModuleData(uint8_t moduleIdx, ModuleData* modData); - - bool fetchRfData(); + void setModelId(uint8_t moduleIdx, uint8_t id); + void setRfModuleData(uint8_t moduleIdx, ModuleData *modData); + bool fetchRfData(); }; -class ModelsCategory: public std::list +typedef struct { + std::string icon; + // Anything else? +} SLabelDetail; + +typedef std::vector> ModelLabelsVector; +typedef std::vector LabelsVector; +typedef std::vector ModelsVector; +typedef enum { + NO_SORT, + NAME_ASC, + NAME_DES, + DATE_ASC, + DATE_DES, + SORT_COUNT +} ModelsSortBy; + +/** + * @brief ModelMap is a multimap of all models and their cooresponding + * labels. Lables are referenced by index, stored in var labels + */ + +class ModelMap : protected std::multimap { -public: - char name[LEN_MODEL_FILENAME + 1]; + public: + ModelsVector getUnlabeledModels(); + ModelsVector getAllModels(); + ModelsVector getModelsByLabel(const std::string &); + ModelsVector getModelsByLabels(const LabelsVector &); + ModelsVector getModelsInLabels(const LabelsVector &lbls); + LabelsVector getLabelsByModel(ModelCell *); + std::map getSelectedLabels(ModelCell *); + bool isLabelSelected(const std::string &, ModelCell *); + 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 progress = nullptr); + bool moveLabelTo(unsigned current, unsigned newind); + bool renameLabel( + const std::string &from, const std::string &to, + std::function progress = nullptr); + std::string getCurrentLabel() { return currentlabel; }; + void setCurrentLabel(const std::string &lbl) + { + currentlabel = lbl; + setDirty(); + } + std::string getLabelString(ModelCell *, const char *noresults = ""); + void setDirty(bool save = false); + bool isDirty() { return _isDirty; } - explicit ModelsCategory(const char * name); - explicit ModelsCategory(const char * name, uint8_t len); - ~ModelsCategory(); + // Currently selected labels in the GUI + void setFilteredLabels(std::set filtlbls) + { + this->filtlbls = std::move(filtlbls); + } + void addFilteredLabel(const std::string &lbl); + bool isLabelFiltered(const std::string &lbl); + std::set filteredLabels() { return filtlbls; } - ModelCell* addModel(const char* fileName, const char* modelName = nullptr); - void removeModel(ModelCell * model); - void moveModel(ModelCell * model, int8_t step); + void setSortOrder(ModelsSortBy sortby); + ModelsSortBy sortOrder() {return _sortOrder;} - int getModelIndex(const ModelCell* model); + protected: + ModelsSortBy _sortOrder = DEFAULT_MODEL_SORT; + bool _isDirty = true; + std::set filtlbls; + std::string currentlabel = ""; - void save(FIL * file); + void updateModelCell(ModelCell *); + bool removeModels( + ModelCell *); // Should only be called from ModelsList remove model + bool updateModelFile(ModelCell *); + void sortModelsBy(ModelsVector &mv, ModelsSortBy sortby); + + void clear() + { + _isDirty = true; + labels.clear(); + std::multimap::clear(); + } + + 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(); + } + + friend class ModelsList; + + private: + LabelsVector labels; // Storage space for discovered labels }; -class ModelsList +class ModelsList : public ModelsVector { bool loaded; - std::list categories; - ModelsCategory * currentCategory; - ModelCell * currentModel; - unsigned int modelsCount; + + ModelCell *currentModel; void init(); -public: - + public: enum class Format { txt, #if defined(SDCARD_YAML) @@ -113,73 +216,54 @@ public: load_default = txt, #endif }; - + ModelsList(); ~ModelsList(); bool load(Format fmt = Format::load_default); - void save(); + const char *save(LabelsVector newOrder=LabelsVector()); void clear(); - const std::list & getCategories() const - { - return categories; - } - - std::list& getCategories() - { - return categories; - } + void setCurrentModel(ModelCell *cell); + void updateCurrentModelCell(); - void setCurrentCategory(ModelsCategory * cat); - - ModelsCategory * getCurrentCategory() const - { - return currentCategory; - } - int getCurrentCategoryIdx() const; - - void setCurrentModel(ModelCell * cell); - - ModelCell * getCurrentModel() const - { - return currentModel; - } - - void incModelsCount() { - modelsCount++; - } + ModelCell *getCurrentModel() const { return currentModel; } unsigned int getModelsCount() const { - return modelsCount; + return std::vector::size(); } - bool readNextLine(char * line, int maxlen); + bool readNextLine(char *line, int maxlen); - ModelsCategory * createCategory(bool save=true); - ModelsCategory * createCategory(const char * name, bool save=true); - void removeCategory(ModelsCategory * category); + ModelCell *addModel(const char *name, bool save = true, ModelCell *copyCell = nullptr); + bool removeModel(ModelCell *model); + bool moveModelTo(unsigned curindex, unsigned toindex); - ModelCell * addModel(ModelsCategory * category, const char * name, const char* modelName = nullptr, bool save=true); - void removeModel(ModelsCategory * category, ModelCell * model); - void moveModel(ModelsCategory * category, ModelCell * model, int8_t step); - void moveModel(ModelCell * model, ModelsCategory * previous_category, ModelsCategory * new_category); - - bool isModelIdUnique(uint8_t moduleIdx, char* warn_buf, size_t warn_buf_len); + bool isModelIdUnique(uint8_t moduleIdx, char *warn_buf, size_t warn_buf_len); uint8_t findNextUnusedModelId(uint8_t moduleIdx); - void onNewModelCreated(ModelCell* cell, ModelData* model); + typedef struct { + std::string name; + char hash[FILE_HASH_LENGTH + 1]; + bool curmodel = false; + bool celladded = false; + } filedat; + std::vector fileHashInfo; -protected: + protected: FIL file; bool loadTxt(); #if defined(SDCARD_YAML) bool loadYaml(); + bool loadYamlDirScanner(); #endif }; -extern ModelsList modelslist; +ModelLabelsVector getUniqueLabels(); -#endif // _MODELSLIST_H_ +extern ModelsList modelslist; +extern ModelMap modelslabels; + +#endif // _MODELSLIST_H_ diff --git a/radio/src/storage/sdcard_common.cpp b/radio/src/storage/sdcard_common.cpp index 7f1b5e8e4..2f87482ed 100644 --- a/radio/src/storage/sdcard_common.cpp +++ b/radio/src/storage/sdcard_common.cpp @@ -26,9 +26,6 @@ #include "conversions/conversions.h" #include "model_init.h" -// defined either in sdcard_raw.cpp or sdcard_yaml.cpp -void storageCreateModelsList(); - void getModelPath(char * path, const char * filename, const char* pathName) { unsigned int len = strlen(pathName); @@ -71,9 +68,6 @@ void storageFormat() sdCheckAndCreateDirectory(MODELS_PATH); generalDefault(); setModelDefaults(); -#if defined(STORAGE_MODELSLIST) - storageCreateModelsList(); -#endif } void storageCheck(bool immediately) @@ -87,10 +81,24 @@ void storageCheck(bool immediately) } } +#if defined(STORAGE_MODELSLIST) + if (storageDirtyMsk & EE_LABELS) { + TRACE("SD card write labels"); + storageDirtyMsk &= ~EE_LABELS; + const char * error = modelslist.save(); + if (error) { + TRACE("writeLabels error=%s", error); + } + } +#endif + if (storageDirtyMsk & EE_MODEL) { TRACE("eeprom write model"); storageDirtyMsk &= ~EE_MODEL; const char * error = writeModel(); +#if defined(STORAGE_MODELSLIST) + modelslist.updateCurrentModelCell(); +#endif if (error) { TRACE("writeModel error=%s", error); } @@ -206,14 +214,14 @@ void storageReadAll() storageDirty(EE_GENERAL); storageCheck(true); } - + if (loadModel(g_eeGeneral.currModelFilename, false) != nullptr) { TRACE("No current model or SD card error"); } #else if (loadModel(g_eeGeneral.currModel, false) != nullptr) { TRACE("No current model or SD card error"); - } + } #endif } diff --git a/radio/src/storage/sdcard_common.h b/radio/src/storage/sdcard_common.h index a64ec5167..5b7c17130 100644 --- a/radio/src/storage/sdcard_common.h +++ b/radio/src/storage/sdcard_common.h @@ -25,8 +25,6 @@ #include "ff.h" #include "translations.h" -#define DEFAULT_CATEGORY "Models" - #define MODEL_FILENAME_PREFIX "model" #if defined(SDCARD_RAW) #define MODEL_FILENAME_SUFFIX ".bin" diff --git a/radio/src/storage/sdcard_raw.cpp b/radio/src/storage/sdcard_raw.cpp index b84c75554..1d5224fc0 100644 --- a/radio/src/storage/sdcard_raw.cpp +++ b/radio/src/storage/sdcard_raw.cpp @@ -191,15 +191,3 @@ const char * writeGeneralSettingsBin() return writeFileBin(RADIO_SETTINGS_PATH, (uint8_t *)&g_eeGeneral, sizeof(g_eeGeneral)); } -#if !defined(SDCARD_YAML) -void storageCreateModelsList() -{ - FIL file; - - FRESULT result = f_open(&file, RADIO_MODELSLIST_PATH, FA_CREATE_ALWAYS | FA_WRITE); - if (result == FR_OK) { - f_puts("[" DEFAULT_CATEGORY "]\n" DEFAULT_MODEL_FILENAME "\n", &file); - f_close(&file); - } -} -#endif diff --git a/radio/src/storage/sdcard_yaml.cpp b/radio/src/storage/sdcard_yaml.cpp index bf827ca2e..8582b46f1 100644 --- a/radio/src/storage/sdcard_yaml.cpp +++ b/radio/src/storage/sdcard_yaml.cpp @@ -122,27 +122,6 @@ const char * readYamlFile(const char* fullpath, const YamlParserCalls* calls, vo return NULL; } - -// -// Generic storage interface -// - -#if defined(STORAGE_MODELSLIST) -void storageCreateModelsList() -{ - modelslist.clear(); - ModelsCategory* cat = modelslist.createCategory(DEFAULT_CATEGORY, false); - ModelCell* model = nullptr; - if (g_eeGeneral.currModelFilename[0] != '\0') { - model = modelslist.addModel(cat, g_eeGeneral.currModelFilename, nullptr, false); - } else { - model = modelslist.addModel(cat, DEFAULT_MODEL_FILENAME, nullptr, false); - } - model->setModelName(g_model.header.name); - modelslist.save(); -} -#endif - // // SDCARD storage interface // @@ -224,12 +203,16 @@ const char * loadRadioSettings() #endif } +#if defined(DEFAULT_INTERNAL_MODULE) + g_eeGeneral.internalModule = DEFAULT_INTERNAL_MODULE; +#endif + const char* error = loadRadioSettingsYaml(); if (!error) { g_eeGeneral.chkSum = evalChkSum(); } postRadioSettingsLoad(); - + return error; } @@ -345,7 +328,7 @@ const char * writeGeneralSettings() return p; } f_unlink(RADIO_SETTINGS_YAML_PATH); - + FRESULT result = f_rename(RADIO_SETTINGS_TMPFILE_YAML_PATH, RADIO_SETTINGS_YAML_PATH); if(result != FR_OK) return SDCARD_ERROR(result); diff --git a/radio/src/storage/sdcard_yaml.h b/radio/src/storage/sdcard_yaml.h index 5f94fbee9..ce124c808 100644 --- a/radio/src/storage/sdcard_yaml.h +++ b/radio/src/storage/sdcard_yaml.h @@ -22,6 +22,7 @@ #pragma once enum class ChecksumResult {Success, Failed, None}; +#include "sdcard_common.h" // TODO CHECK REQUIRED constexpr uint8_t MODELIDX_STRLEN = sizeof(MODEL_FILENAME_PREFIX "00"); diff --git a/radio/src/storage/yaml/CMakeLists.txt b/radio/src/storage/yaml/CMakeLists.txt index e131915a6..964d07e97 100644 --- a/radio/src/storage/yaml/CMakeLists.txt +++ b/radio/src/storage/yaml/CMakeLists.txt @@ -6,9 +6,10 @@ set(SRC ${SRC} storage/yaml/yaml_tree_walker.cpp storage/yaml/yaml_datastructs.cpp) if(STORAGE_MODELSLIST) - set(SRC ${SRC} storage/yaml/yaml_modelslist.cpp) + set(SRC ${SRC} storage/yaml/yaml_labelslist.cpp) + add_definitions(-DSTORAGE_MODELSLIST) endif() - + # Warning: this file must be kept in sync with 'yaml_datastructs.cpp' # in the same directory. diff --git a/radio/src/storage/yaml/yaml_datastructs_nv14.cpp b/radio/src/storage/yaml/yaml_datastructs_nv14.cpp index 9a7d04ec3..549d86f82 100644 --- a/radio/src/storage/yaml/yaml_datastructs_nv14.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_nv14.cpp @@ -455,6 +455,7 @@ static const struct YamlNode struct_ModelHeader[] = { YAML_STRING("name", 15), YAML_ARRAY("modelId", 8, 2, struct_unsigned_8, NULL), YAML_STRING("bitmap", 14), + YAML_STRING("labels", 100), YAML_END }; static const struct YamlNode struct_TimerData[] = { @@ -837,7 +838,7 @@ static const struct YamlNode struct_TopBarPersistentData[] = { }; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), - YAML_STRUCT("header", 248, struct_ModelHeader, NULL), + YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), YAML_ARRAY("timers", 136, 3, struct_TimerData, NULL), YAML_UNSIGNED( "telemetryProtocol", 3 ), YAML_UNSIGNED( "thrTrim", 1 ), @@ -889,7 +890,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_END }; static const struct YamlNode struct_PartialModel[] = { - YAML_STRUCT("header", 248, struct_ModelHeader, NULL), + YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), YAML_ARRAY("timers", 136, 3, struct_TimerData, NULL), YAML_END }; diff --git a/radio/src/storage/yaml/yaml_datastructs_x10.cpp b/radio/src/storage/yaml/yaml_datastructs_x10.cpp index d2afb8c36..d3483afd6 100644 --- a/radio/src/storage/yaml/yaml_datastructs_x10.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_x10.cpp @@ -480,6 +480,7 @@ static const struct YamlNode struct_ModelHeader[] = { YAML_STRING("name", 15), YAML_ARRAY("modelId", 8, 2, struct_unsigned_8, NULL), YAML_STRING("bitmap", 14), + YAML_STRING("labels", 100), YAML_END }; static const struct YamlNode struct_TimerData[] = { @@ -862,7 +863,7 @@ static const struct YamlNode struct_TopBarPersistentData[] = { }; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), - YAML_STRUCT("header", 248, struct_ModelHeader, NULL), + YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), YAML_ARRAY("timers", 136, 3, struct_TimerData, NULL), YAML_UNSIGNED( "telemetryProtocol", 3 ), YAML_UNSIGNED( "thrTrim", 1 ), @@ -914,7 +915,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_END }; static const struct YamlNode struct_PartialModel[] = { - YAML_STRUCT("header", 248, struct_ModelHeader, NULL), + YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), YAML_ARRAY("timers", 136, 3, struct_TimerData, NULL), YAML_END }; diff --git a/radio/src/storage/yaml/yaml_datastructs_x12s.cpp b/radio/src/storage/yaml/yaml_datastructs_x12s.cpp index 85f697c12..e5a321359 100644 --- a/radio/src/storage/yaml/yaml_datastructs_x12s.cpp +++ b/radio/src/storage/yaml/yaml_datastructs_x12s.cpp @@ -478,6 +478,7 @@ static const struct YamlNode struct_ModelHeader[] = { YAML_STRING("name", 15), YAML_ARRAY("modelId", 8, 2, struct_unsigned_8, NULL), YAML_STRING("bitmap", 14), + YAML_STRING("labels", 100), YAML_END }; static const struct YamlNode struct_TimerData[] = { @@ -860,7 +861,7 @@ static const struct YamlNode struct_TopBarPersistentData[] = { }; static const struct YamlNode struct_ModelData[] = { YAML_CUSTOM("semver",nullptr,w_semver), - YAML_STRUCT("header", 248, struct_ModelHeader, NULL), + YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), YAML_ARRAY("timers", 136, 3, struct_TimerData, NULL), YAML_UNSIGNED( "telemetryProtocol", 3 ), YAML_UNSIGNED( "thrTrim", 1 ), @@ -912,7 +913,7 @@ static const struct YamlNode struct_ModelData[] = { YAML_END }; static const struct YamlNode struct_PartialModel[] = { - YAML_STRUCT("header", 248, struct_ModelHeader, NULL), + YAML_STRUCT("header", 1048, struct_ModelHeader, NULL), YAML_ARRAY("timers", 136, 3, struct_TimerData, NULL), YAML_END }; diff --git a/radio/src/storage/yaml/yaml_labelslist.cpp b/radio/src/storage/yaml/yaml_labelslist.cpp new file mode 100644 index 000000000..d97fd73c0 --- /dev/null +++ b/radio/src/storage/yaml/yaml_labelslist.cpp @@ -0,0 +1,301 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * th9x - http://code.google.com/p/th9x + * er9x - http://code.google.com/p/er9x + * gruvin9x - http://code.google.com/p/gruvin9x + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * 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. + */ + +#include "yaml_labelslist.h" +#include "yaml_parser.h" + +#include "debug.h" + +#include "storage/modelslist.h" + +#include + +#ifdef DEBUG_LABELS +#define TRACE_LABELS_YAML(...) TRACE(__VA_ARGS__) +#else +#define TRACE_LABELS_YAML(...) +#endif + +using std::list; + +struct labelslist_iter +{ + enum ItemData { + Root=0, + ModelsRoot=1, + ModelName=2, + ModelData=3, + LabelsRoot=4, + LabelName=5, + LabelData=6, + SortRoot=7, + }; + + ModelCell *curmodel; + bool modeldatavalid; // Used to determine if reading yaml values is necessary + uint8_t level; + char current_attr[LABEL_LENGTH+1]; // set after find_node() + char current_label[LABEL_LENGTH+1]; // set after find_node() +}; + +static labelslist_iter __labelslist_iter_inst; + +void* get_labelslist_iter() +{ + // Thoughts... record the time here. + // If time > xxx while parsing models. pop up a window showing updating? + + __labelslist_iter_inst.modeldatavalid = false; + __labelslist_iter_inst.curmodel = NULL; + __labelslist_iter_inst.level = 0; + + TRACE_LABELS_YAML("YAML Label Reader Start %u", 0); + + return &__labelslist_iter_inst; +} + +static bool to_parent(void* ctx) +{ + labelslist_iter* mi = (labelslist_iter*)ctx; + + if (mi->level == labelslist_iter::Root) + return false; + + if(mi->level == labelslist_iter::ModelName || + mi->level == labelslist_iter::LabelName) { + mi->level = labelslist_iter::Root; + } + else + mi->level--; + + TRACE_LABELS_YAML("YAML To Parent %u", mi->level); + + return true; +} + +static bool to_child(void* ctx) +{ + labelslist_iter* mi = (labelslist_iter*)ctx; + + mi->level++; + + TRACE_LABELS_YAML("YAML To Child"); + TRACE_LABELS_YAML("YAML Level %u", mi->level); + return true; +} + +static bool to_next_elmt(void* ctx) +{ + labelslist_iter* mi = (labelslist_iter*)ctx; + if (mi->level == labelslist_iter::Root) { + return false; + } + + TRACE_LABELS_YAML("YAML To Next Element"); + TRACE_LABELS_YAML("YAML Current Level %u", mi->level); + return true; +} + +static bool find_node(void* ctx, char* buf, uint8_t len) +{ + labelslist_iter* mi = (labelslist_iter*)ctx; + + memcpy(mi->current_attr, buf, len); + mi->current_attr[len] = '\0'; + + TRACE_LABELS_YAML("YAML On Node %s", mi->current_attr); + TRACE_LABELS_YAML("YAML Current Level %u", mi->level); + + // If in the labels node, force to labelsroot enum + if(mi->level == labelslist_iter::Root && strcasecmp(mi->current_attr,"labels") == 0) { + TRACE_LABELS_YAML("Forced root"); + mi->level = labelslist_iter::LabelsRoot; + TRACE_LABELS_YAML("YAML New Level %u", mi->level); + } + + if(mi->level == labelslist_iter::Root && strcasecmp(mi->current_attr,"models") == 0) { + TRACE_LABELS_YAML("Forced root"); + mi->level = labelslist_iter::ModelsRoot; + TRACE_LABELS_YAML("YAML New Level %u", mi->level); + } + + if(mi->level == labelslist_iter::Root && strcasecmp(mi->current_attr,"sort") == 0) { + TRACE_LABELS_YAML("Forced root"); + mi->level = labelslist_iter::SortRoot; + TRACE_LABELS_YAML("YAML New Level %u", mi->level); + } + + // Model List + if(mi->level == labelslist_iter::ModelName) { + 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"); + 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) { + mi->curmodel = NULL; + TRACE_LABELS_YAML("File does not exist in /MODELS"); + } + } + + // Labels List + if(mi->level == labelslist_iter::LabelName) { + TRACE_LABELS_YAML("Label Found -- %s", mi->current_attr); + modelslabels.addLabel(mi->current_attr); + strncpy(mi->current_label,mi->current_attr, LABEL_LENGTH); + mi->current_label[LABEL_LENGTH] = '\0'; + } + + return true; +} + +static void set_attr(void* ctx, char* buf, uint8_t len) +{ + char value[40]; + memcpy(value, buf, len); + value[len] = '\0'; + + labelslist_iter* mi = (labelslist_iter*)ctx; + TRACE_LABELS_YAML("YAML Attr Level %u, %s = %s", mi->level, mi->current_attr, value); + + // Model Section + if(mi->level == labelslist_iter::ModelData) { + + // 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; + } + } + + // Model Name + } else if(mi->modeldatavalid && !strcasecmp(mi->current_attr, "name")) { + if(mi->curmodel != NULL) { + 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) { + 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) { + // TODO Check if it exists ? + strcpy(mi->curmodel->modelBitmap, value); + TRACE_LABELS_YAML("Set the models bitmap"); + } +#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++; + } + } + + // 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); + } + } + } + + // Label Section + } else if(mi->level == labelslist_iter::LabelData) { + 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")) { + TRACE("FOUND %s Label is selected", mi->current_label); + modelslabels.addFilteredLabel(mi->current_label); + } + + // Sort Order + } else if (mi->level == labelslist_iter::SortRoot) { + TRACE("Sort Order Found -- %s", mi->current_attr); + mi->level = labelslist_iter::Root; + modelslabels.setSortOrder((ModelsSortBy)strtol(value,NULL,10)); + } +} + +static const YamlParserCalls labelslistCalls = { + to_parent, + to_child, + to_next_elmt, + find_node, + set_attr +}; + +const YamlParserCalls* get_labelslist_parser_calls() +{ + return &labelslistCalls; +} diff --git a/radio/src/storage/yaml/yaml_modelslist.h b/radio/src/storage/yaml/yaml_labelslist.h similarity index 75% rename from radio/src/storage/yaml/yaml_modelslist.h rename to radio/src/storage/yaml/yaml_labelslist.h index 153fd0aaf..030fdfb8f 100644 --- a/radio/src/storage/yaml/yaml_modelslist.h +++ b/radio/src/storage/yaml/yaml_labelslist.h @@ -19,12 +19,16 @@ * GNU General Public License for more details. */ -#ifndef _YAML_MODELSLIST_H_ -#define _YAML_MODELSLIST_H_ +#ifndef _YAML_LABELSLIST_H_ +#define _YAML_LABELSLIST_H_ + +#define MODULE_ID_STR "mod%did" +#define MODULE_TYPE_STR "mod%dtype" +#define MODULE_RFPROTOCOL_STR "mod%drf" struct YamlParserCalls; -void* get_modelslist_iter(const char* currentModel, unsigned currentModel_len); -const YamlParserCalls* get_modelslist_parser_calls(); +void* get_labelslist_iter(); +const YamlParserCalls* get_labelslist_parser_calls(); #endif diff --git a/radio/src/storage/yaml/yaml_modelslist.cpp b/radio/src/storage/yaml/yaml_modelslist.cpp deleted file mode 100644 index 7e0062938..000000000 --- a/radio/src/storage/yaml/yaml_modelslist.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) EdgeTX - * - * Based on code named - * opentx - https://github.com/opentx/opentx - * th9x - http://code.google.com/p/th9x - * er9x - http://code.google.com/p/er9x - * gruvin9x - http://code.google.com/p/gruvin9x - * - * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2 as - * published by the Free Software Foundation. - * - * 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. - */ - -#include "yaml_modelslist.h" -#include "yaml_parser.h" - -#include "storage/modelslist.h" - -#include - -using std::list; - -struct modelslist_iter -{ - enum Level { - Root=0, - Category=1, - Model=2 - }; - - ModelsList* root; - const char* currentModel; - unsigned currentModel_len; - uint8_t level; - char current_attr[16]; // set after find_node() -}; - -static modelslist_iter __modelslist_iter_inst; - -void* get_modelslist_iter(const char* currentModel, unsigned currentModel_len) -{ - __modelslist_iter_inst.root = &modelslist; - __modelslist_iter_inst.currentModel = currentModel; - __modelslist_iter_inst.currentModel_len = currentModel_len; - __modelslist_iter_inst.level = 0; - - return &__modelslist_iter_inst; -} - -static bool to_parent(void* ctx) -{ - modelslist_iter* mi = (modelslist_iter*)ctx; - if (mi->level == modelslist_iter::Root) { - return false; - } - - mi->level--; - return true; -} - -static bool to_child(void* ctx) -{ - modelslist_iter* mi = (modelslist_iter*)ctx; - if (mi->level == modelslist_iter::Model) { - return false; - } - - mi->level++; - return true; -} - -static bool to_next_elmt(void* ctx) -{ - modelslist_iter* mi = (modelslist_iter*)ctx; - if (mi->level == modelslist_iter::Root) { - return false; - } - return true; -} - -static bool find_node(void* ctx, char* buf, uint8_t len) -{ - modelslist_iter* mi = (modelslist_iter*)ctx; - - if (len > sizeof(modelslist_iter::current_attr)-1) - len = sizeof(modelslist_iter::current_attr)-1; - - memcpy(mi->current_attr, buf, len); - mi->current_attr[len] = '\0'; - - list& cats = mi->root->getCategories(); - if (mi->level == modelslist_iter::Category) { - ModelsCategory* cat = new ModelsCategory(buf,len); - cats.push_back(cat); - } - - return true; -} - -static void set_attr(void* ctx, char* buf, uint8_t len) -{ - modelslist_iter* mi = (modelslist_iter*)ctx; - auto ml = mi->root; - list& cats = ml->getCategories(); - - switch (mi->level) { - case modelslist_iter::Model: - if (!strcmp(mi->current_attr, "filename")) { - if (!cats.empty()) { - ModelCell* model = new ModelCell(buf, len); - auto cat = cats.back(); - cat->push_back(model); - ml->incModelsCount(); - - if (!strncmp(model->modelFilename, mi->currentModel, - mi->currentModel_len)) { - ml->setCurrentCategory(cat); - ml->setCurrentModel(model); - } - } - } else if (!strcmp(mi->current_attr, "name")) { - if (!cats.empty()) { - auto cat = cats.back(); - if (!cat->empty()) { - auto model = cat->back(); - model->setModelName(buf, len); - } - } - } - break; - } -} - -static const YamlParserCalls modelslistCalls = { - to_parent, - to_child, - to_next_elmt, - find_node, - set_attr -}; - -const YamlParserCalls* get_modelslist_parser_calls() -{ - return &modelslistCalls; -} diff --git a/radio/src/targets/horus/libopenui_config.h b/radio/src/targets/horus/libopenui_config.h index fa4ac7c1d..9a95b98c5 100644 --- a/radio/src/targets/horus/libopenui_config.h +++ b/radio/src/targets/horus/libopenui_config.h @@ -65,6 +65,7 @@ constexpr uint32_t MENU_HEADER_BACK_BUTTON_WIDTH = MENU_HEADER_HEIGHT; constexpr uint32_t MENU_HEADER_BACK_BUTTON_HEIGHT = MENU_HEADER_HEIGHT; constexpr coord_t PAGE_PADDING = 6; +constexpr uint32_t LABELS_LINE_HEIGHT = 30; constexpr uint32_t PAGE_LINE_HEIGHT = 20; constexpr coord_t LINE_HEIGHT = PAGE_LINE_HEIGHT; diff --git a/radio/src/translations.cpp b/radio/src/translations.cpp index 38ffee712..bd3f93146 100644 --- a/radio/src/translations.cpp +++ b/radio/src/translations.cpp @@ -130,6 +130,7 @@ const char STR_DELETEMODEL[] = TR_DELETEMODEL; const char STR_COPYINGMODEL[] = TR_COPYINGMODEL; const char STR_MOVINGMODEL[] = TR_MOVINGMODEL; const char STR_LOADINGMODEL[] = TR_LOADINGMODEL; +const char STR_UNLABELEDMODEL[] = TR_UNLABELEDMODEL; const char STR_NAME[] = TR_NAME; const char STR_BITMAP[] = TR_BITMAP; const char STR_TIMER[] = TR_TIMER; @@ -329,17 +330,17 @@ const char STR_CURRENT_CALIB[] = TR_CURRENT_CALIB; const char STR_UNITS_SYSTEM[] = TR_UNITS_SYSTEM; const char STR_VOICE_LANGUAGE[] = TR_VOICE_LANGUAGE; const char STR_MODELIDUSED[] = TR_MODELIDUSED; +const char STR_MODELIDUNIQUE[] = TR_MODELIDUNIQUE; const char STR_BEEP_VOLUME[] = INDENT TR_BEEP_VOLUME; const char STR_WAV_VOLUME[] = INDENT TR_WAV_VOLUME; const char STR_BG_VOLUME[] = INDENT TR_BG_VOLUME; const char STR_SELECT_MODEL[] = TR_SELECT_MODEL; -const char STR_MODEL_CATEGORIES[] = TR_MODEL_CATEGORIES; const char STR_MODELS[] = TR_MODELS; const char STR_SELECT_MODE[] = TR_SELECT_MODE; -const char STR_CREATE_CATEGORY[] = TR_CREATE_CATEGORY; -const char STR_RENAME_CATEGORY[] = TR_RENAME_CATEGORY; -const char STR_DELETE_CATEGORY[] = TR_DELETE_CATEGORY; const char STR_CREATE_MODEL[] = TR_CREATE_MODEL; +const char STR_FAVORITE_LABEL[] = TR_FAVORITE_LABEL; +const char STR_NEW_MODEL[] = TR_NEW_MODEL; +const char STR_EDIT_LABELS[] = TR_EDIT_LABELS; const char STR_DUPLICATE_MODEL[] = TR_DUPLICATE_MODEL; const char STR_COPY_MODEL[] = TR_COPY_MODEL; const char STR_MOVE_MODEL[] = TR_MOVE_MODEL; @@ -405,7 +406,6 @@ const char STR_WAITING_FOR_RX[] = TR_WAITING_FOR_RX; const char STR_WAITING_FOR_TX[] = TR_WAITING_FOR_TX; const char STR_WAITING_FOR_MODULE[] = TR_WAITING_FOR_MODULE; const char STR_WARN_5VOLTS[] = TR_WARN_5VOLTS; -const char STR_CAT_NOT_EMPTY[] = TR_CAT_NOT_EMPTY; const char STR_WARNING[] = TR_WARNING; const char STR_THROTTLE_WARNING[] = TR_THROTTLE_WARNING; const char STR_CUSTOM_THROTTLE_WARNING[] = TR_CUSTOM_THROTTLE_WARNING; @@ -998,8 +998,17 @@ const char STR_EDIT_THEME[] = TR_EDIT_THEME; const char STR_DETAILS[] = TR_DETAILS; const char STR_THEME_EDITOR[] = TR_THEME_EDITOR; + #if defined(COLORLCD) +const char STR_ENTER_LABEL[] = TR_ENTER_LABEL; +const char STR_LABEL[] = TR_LABEL; +const char STR_LABELS[] = TR_LABELS; +const char STR_CURRENT_MODEL[] = TR_CURRENT_MODEL; +const char STR_NEW[] = TR_NEW; +const char STR_NEW_LABEL[] = TR_NEW_LABEL; +const char STR_RENAME_LABEL[] = TR_RENAME_LABEL; +const char STR_DELETE_LABEL[] = TR_DELETE_LABEL; const char STR_MAIN_MENU_SELECT_MODEL[] = TR_MAIN_MENU_SELECT_MODEL; const char STR_MAIN_MENU_MODEL_NOTES[] = TR_MAIN_MENU_MODEL_NOTES; const char STR_MAIN_MENU_CHANNEL_MONITOR[] = TR_MAIN_MENU_CHANNEL_MONITOR; diff --git a/radio/src/translations.h b/radio/src/translations.h index 9d65971ac..1e2b90262 100644 --- a/radio/src/translations.h +++ b/radio/src/translations.h @@ -234,6 +234,7 @@ extern const char STR_DELETEMODEL[]; extern const char STR_COPYINGMODEL[]; extern const char STR_MOVINGMODEL[]; extern const char STR_LOADINGMODEL[]; +extern const char STR_UNLABELEDMODEL[]; extern const char STR_NAME[]; extern const char STR_BITMAP[]; extern const char STR_TIMER[]; @@ -528,17 +529,17 @@ extern const char STR_CURRENT_CALIB[]; extern const char STR_UNITS_SYSTEM[]; extern const char STR_VOICE_LANGUAGE[]; extern const char STR_MODELIDUSED[]; +extern const char STR_MODELIDUNIQUE[]; extern const char STR_BEEP_VOLUME[]; extern const char STR_WAV_VOLUME[]; extern const char STR_BG_VOLUME[]; extern const char STR_SELECT_MODEL[]; -extern const char STR_MODEL_CATEGORIES[]; extern const char STR_MODELS[]; extern const char STR_SELECT_MODE[]; -extern const char STR_CREATE_CATEGORY[]; -extern const char STR_RENAME_CATEGORY[]; -extern const char STR_DELETE_CATEGORY[]; extern const char STR_CREATE_MODEL[]; +extern const char STR_FAVORITE_LABEL[]; +extern const char STR_NEW_MODEL[]; +extern const char STR_EDIT_LABELS[]; extern const char STR_DUPLICATE_MODEL[]; extern const char STR_COPY_MODEL[]; extern const char STR_MOVE_MODEL[]; @@ -631,7 +632,6 @@ extern const char STR_ASK_OVERWRITE[]; extern const char STR_NEEDS_FILE[]; extern const char STR_EXT_MULTI_SPEC[]; extern const char STR_INT_MULTI_SPEC[]; -extern const char STR_CAT_NOT_EMPTY[]; extern const char STR_WARNING[]; extern const char STR_STORAGE_WARNING[]; extern const char STR_EEPROM_CONVERTING[]; @@ -1196,8 +1196,20 @@ extern const char STR_EDIT_THEME[]; extern const char STR_DETAILS[]; extern const char STR_THEME_EDITOR[]; + + #if defined(COLORLCD) +extern const char STR_ENTER_LABEL[]; +extern const char STR_LABEL[]; +extern const char STR_LABELS[]; +extern const char STR_SELECT_MODEL[]; +extern const char STR_NONE[]; +extern const char STR_CURRENT_MODEL[]; +extern const char STR_NEW[]; +extern const char STR_NEW_LABEL[]; +extern const char STR_RENAME_LABEL[]; +extern const char STR_DELETE_LABEL[]; extern const char STR_MAIN_MENU_SELECT_MODEL[]; extern const char STR_MAIN_MENU_MODEL_NOTES[]; extern const char STR_MAIN_MENU_CHANNEL_MONITOR[]; diff --git a/radio/src/translations/cn.h b/radio/src/translations/cn.h index 07ce50a82..000bdeb29 100644 --- a/radio/src/translations/cn.h +++ b/radio/src/translations/cn.h @@ -331,6 +331,7 @@ #define TR_COPYINGMODEL "复制模型..." #define TR_MOVINGMODEL "移动模型..." #define TR_LOADINGMODEL "载入模型..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "名称" #define TR_MODELNAME "模型名称" #define TR_PHASENAME "模式名称" @@ -626,13 +627,20 @@ #define TR_VOLTAGE TR(INDENT "电压源", INDENT "电压来源") #define TR_CURRENT TR(INDENT "电流源", INDENT "电流来源") #define TR_SELECT_MODEL "选择模型" -#define TR_MODEL_CATEGORIES "模型分类" #define TR_MODELS "个模型" #define TR_SELECT_MODE "选择模式" -#define TR_CREATE_CATEGORY "创建分类" -#define TR_RENAME_CATEGORY "分类改名" -#define TR_DELETE_CATEGORY "删除分类" #define TR_CREATE_MODEL "创建模型" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "复制模型" #define TR_COPY_MODEL "复制模型" #define TR_MOVE_MODEL "移动模型" @@ -640,7 +648,6 @@ #define TR_DELETE_MODEL "删除模型" #define TR_RESTORE_MODEL "恢复模型" #define TR_DELETE_ERROR "删除失败" -#define TR_CAT_NOT_EMPTY "分类未清空" #define TR_SDCARD_ERROR TR("存储卡错误", "存储卡错误") #define TR_SDCARD "存储卡" #define TR_NO_FILES_ON_SD "存储卡中没有文件!" @@ -911,11 +918,8 @@ #define TR_TRAINER "教练" #define TR_CHANS "通道" #define TR_ANTENNAPROBLEM CENTER "发射机天线故障!" -#if defined(COLORLCD) - #define TR_MODELIDUSED "ID已使用:" -#else - #define TR_MODELIDUSED TR("ID已使用:","ID已使用:") -#endif +#define TR_MODELIDUSED "ID已使用:" +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "模块" #define TR_RX_NAME "接收机名称" #define TR_TELEMETRY_TYPE TR("类型", "回传类型") @@ -1334,4 +1338,4 @@ #define TR_PROTOCOL "协议" #define TR_YEAR "年" #define TR_MONTH "月" -#define TR_DAY "日" \ No newline at end of file +#define TR_DAY "日" diff --git a/radio/src/translations/cz.h b/radio/src/translations/cz.h index 8ddbc528e..3b3d27c57 100644 --- a/radio/src/translations/cz.h +++ b/radio/src/translations/cz.h @@ -349,6 +349,7 @@ #define TR_COPYINGMODEL "Kopíruji model..." #define TR_MOVINGMODEL "Přesouvám model..." #define TR_LOADINGMODEL "Aktivuji model..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Název" #define TR_MODELNAME TR("Model", "Název modelu") #define TR_PHASENAME "Název" @@ -642,13 +643,20 @@ #define TR_VOLTAGE INDENT"Napětí" #define TR_CURRENT INDENT"Proud" #define TR_SELECT_MODEL "Vyber model" -#define TR_MODEL_CATEGORIES "Kategorie modelů" #define TR_MODELS "Modely" #define TR_SELECT_MODE "Vybrat mód" -#define TR_CREATE_CATEGORY "Nová kategorie" -#define TR_RENAME_CATEGORY "Přejmenovat kategorii" -#define TR_DELETE_CATEGORY "Odstranit kategorii" #define TR_CREATE_MODEL "Nový model" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplikovat model" #define TR_COPY_MODEL "Kopírovat" #define TR_MOVE_MODEL "Přesunout model" @@ -656,7 +664,6 @@ #define TR_DELETE_MODEL "Smaž model" #define TR_RESTORE_MODEL "Obnov model z SD" #define TR_DELETE_ERROR "Nelze odstranit" -#define TR_CAT_NOT_EMPTY "Kategorie není prázdná" #define TR_SDCARD_ERROR "Chyba SD karty" #define TR_SDCARD "SD karta" #define TR_NO_FILES_ON_SD "Žádné soubory na SD kartě!" @@ -927,6 +934,7 @@ #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "Problém s TX anténou!" #define TR_MODELIDUSED TR("ID již použito","ID modelu je již použito") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Modul" #define TR_RX_NAME "Jméno RX" #define TR_TELEMETRY_TYPE TR("Typ", "Typ telemetrie") diff --git a/radio/src/translations/da.h b/radio/src/translations/da.h index af6e91043..1f0647daf 100644 --- a/radio/src/translations/da.h +++ b/radio/src/translations/da.h @@ -332,6 +332,7 @@ #define TR_COPYINGMODEL "Kopier model..." #define TR_MOVINGMODEL "Flytter model..." #define TR_LOADINGMODEL "Indlæser model..." +#define TR_UNLABELEDMODEL "Typeløs" #define TR_NAME "Navn" #define TR_MODELNAME "Model navn" #define TR_PHASENAME "Mode navn" @@ -601,7 +602,7 @@ #define TR_PROTOCOL_INVALID TR("Prot. ugyldig", "Protocol ugyldig") #define TR_MODULE_STATUS TR(INDENT "Status", INDENT "Module Status") #define TR_MODULE_SYNC TR(INDENT "Sync", INDENT "Proto Sync Status") -#define TR_MULTI_SERVOFREQ TR("Servo rate", "Servo opdaterings rate") +#define TR_MULTI_SERVOFREQ TR("Servo hastighed", "Servo opdaterings hastighed") #define TR_MULTI_MAX_THROW TR("Max. Throw", "Enable max. throw") #define TR_MULTI_RFCHAN TR("RF kanal", "Vælg RF kanal") #define TR_AFHDS3_RX_FREQ TR("RX frekv.", "RX frekvens") @@ -612,7 +613,7 @@ #define TR_SYNCMENU "[Sync]" #define TR_LIMIT INDENT "Grænse" #define TR_MINRSSI "Min Rssi" -#define TR_FLYSKY_TELEMETRY TR("FlySky RSSI #", "Brug FlySky RSSI value without rescalling") +#define TR_FLYSKY_TELEMETRY TR("FlySky RSSI #", "Brug FlySky RSSI værdi uden reskalering") #define TR_LATITUDE "Breddegrad" #define TR_LONGITUDE "Længdegrad" #define TR_GPS_COORDS_FORMAT TR("GPS Coords", "Coordinate format") @@ -627,13 +628,20 @@ #define TR_VOLTAGE TR(INDENT "Spænding", INDENT "Spænding kilde") #define TR_CURRENT TR(INDENT "Strøm", INDENT "Strøm kilde") #define TR_SELECT_MODEL "Vælg model" -#define TR_MODEL_CATEGORIES "Model kategorier" #define TR_MODELS "Modeller" #define TR_SELECT_MODE "Vælg tilstand" -#define TR_CREATE_CATEGORY "Opret category" -#define TR_RENAME_CATEGORY "Omdøb category" -#define TR_DELETE_CATEGORY "Slet category" #define TR_CREATE_MODEL "Opret model" +#define TR_FAVORITE_LABEL "Favoritter" +#define TR_NEW_MODEL "Ny model" +#define TR_EDIT_LABELS "Ret type" +#define TR_ENTER_LABEL "Vælg type" +#define TR_LABEL "Type" +#define TR_LABELS "Typer" +#define TR_CURRENT_MODEL "Aktuelle" +#define TR_NEW "Ny" +#define TR_NEW_LABEL "Ny type" +#define TR_RENAME_LABEL "Omdøb type" +#define TR_DELETE_LABEL "Slet type" #define TR_DUPLICATE_MODEL "Dupliker model" #define TR_COPY_MODEL "Kopier model" #define TR_MOVE_MODEL "Flyt model" @@ -641,7 +649,6 @@ #define TR_DELETE_MODEL "Slet model" #define TR_RESTORE_MODEL "Genskab model" #define TR_DELETE_ERROR "Slet fejl" -#define TR_CAT_NOT_EMPTY "Kategori er ikke tom" #define TR_SDCARD_ERROR TR("SD fejl", "SD kort fejl") #define TR_SDCARD "SD kort" #define TR_NO_FILES_ON_SD "Ingen filer på SD!" @@ -887,7 +894,7 @@ #define TR_BUZZER INDENT "Biper" #define TR_BYTES "bytes" #define TR_MODULE_BIND BUTTON(TR("Bnd", "Bind")) -#define TR_POWERMETER_ATTN_NEEDED "Attenuator needed" +#define TR_POWERMETER_ATTN_NEEDED "Attestering nøvendig" #define TR_PXX2_SELECT_RX "Vælg RX" #define TR_PXX2_DEFAULT "" #define TR_BT_SELECT_DEVICE "Vælg enhed" @@ -906,17 +913,14 @@ #define TR_DEBUG "Fejlfind" #define TR_KEYS_BTN BUTTON("Taster") #define TR_ANALOGS_BTN BUTTON(TR("Analo", "Analoge")) -#define TR_TOUCH_NOTFOUND "Touch hardware not found" -#define TR_TOUCH_EXIT "Touch screen to exit" +#define TR_TOUCH_NOTFOUND "Tryk skærm ikke fundet" +#define TR_TOUCH_EXIT "Tryk på skærm for at forlade" #define TR_SET BUTTON("Set") #define TR_TRAINER "Træner" #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "TX antenne problem!" -#if defined(COLORLCD) - #define TR_MODELIDUSED "ID brugt i:" -#else - #define TR_MODELIDUSED TR("ID brugt i:","Modtager ID brugt i:") -#endif +#define TR_MODELIDUSED "ID brugt i:" +#define TR_MODELIDUNIQUE "ID er entydig" #define TR_MODULE "Modul" #define TR_RX_NAME "Rx Navn" #define TR_TELEMETRY_TYPE TR("Type", "Telemetri type") @@ -980,13 +984,13 @@ #define TR_INPUTS "Indgange" #define TR_OUTPUTS "Udgange" #define STR_EEBACKUP "EEPROM backup" -#define STR_FACTORYRESET "Factory reset" +#define STR_FACTORYRESET "Nulstil til fabrik indstillinger" #define TR_CONFIRMRESET TR("Slet ALLE", "Slet ALLE modeller og indstillinger?") -#define TR_TOO_MANY_LUA_SCRIPTS "Too many Lua scripts!" +#define TR_TOO_MANY_LUA_SCRIPTS "For mannge Lua scripts!" #define TR_SPORT_UPDATE_POWER_MODE "SP Strøm" #define TR_SPORT_UPDATE_POWER_MODES "AUTO","TIL" #define TR_NO_TELEMETRY_SCREENS "Ingen telemetri skærme" -#define TR_TOUCH_PANEL "Touch panel:" +#define TR_TOUCH_PANEL "Tryk panel:" #define TR_FILE_SIZE "Fil størrelse" #define TR_FILE_OPEN "Åbn alligevel?" diff --git a/radio/src/translations/de.h b/radio/src/translations/de.h index 56ed24a8d..75ef901a6 100644 --- a/radio/src/translations/de.h +++ b/radio/src/translations/de.h @@ -336,6 +336,7 @@ #define TR_COPYINGMODEL "Kopiere Modell" #define TR_MOVINGMODEL "Schiebe Modell" #define TR_LOADINGMODEL "Lade Modell..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Name" #define TR_MODELNAME "Modellname" #define TR_PHASENAME "Phase-Name" @@ -625,13 +626,20 @@ #define TR_VOLTAGE TR(INDENT "Spg", INDENT "Spannungsquelle") //9XR-Pro #define TR_CURRENT TR(INDENT "Strom", INDENT "Stromquelle") #define TR_SELECT_MODEL "Modell auswählen" -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Modelltyp erstellen" -#define TR_RENAME_CATEGORY "Modelltyp umbenennen" -#define TR_DELETE_CATEGORY "Modelltyp löschen" #define TR_CREATE_MODEL TR("Neues Modell" , "Neues Modell erstellen") +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Kopiere Modell" #define TR_COPY_MODEL "Kopiere Modell" #define TR_MOVE_MODEL "Verschiebe Modell" @@ -639,7 +647,6 @@ #define TR_DELETE_MODEL "Lösche Modell" // TODO merged into DELETEMODEL? #define TR_RESTORE_MODEL TR("Modell wiederher.", "Modell wiederherstellen") #define TR_DELETE_ERROR "Fehler beim\nLöschen" -#define TR_CAT_NOT_EMPTY "Modelltyp nicht leer" #define TR_SDCARD_ERROR "SD-Kartenfehler" #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -911,11 +918,7 @@ #define TR_TRAINER "DSC Buchse PPM In/Out" #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "TX-Antennenproblem!" -#if defined(COLORLCD) - #define TR_MODELIDUSED "ID used in:" -#else - #define TR_MODELIDUSED TR("ID used in:","Receiver ID used in:") -#endif +#define TR_MODELIDUSED "ID used in:"#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Modul-Typ" #define TR_RX_NAME "Rx Name" #define TR_TELEMETRY_TYPE TR("Typ", "Telemetrietyp") diff --git a/radio/src/translations/en.h b/radio/src/translations/en.h index 80e6c1a65..130af0894 100644 --- a/radio/src/translations/en.h +++ b/radio/src/translations/en.h @@ -331,6 +331,7 @@ #define TR_COPYINGMODEL "Copying model..." #define TR_MOVINGMODEL "Moving model..." #define TR_LOADINGMODEL "Loading model..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Name" #define TR_MODELNAME "Model name" #define TR_PHASENAME "Mode name" @@ -626,13 +627,20 @@ #define TR_VOLTAGE TR(INDENT "Voltage", INDENT "Voltage source") #define TR_CURRENT TR(INDENT "Current", INDENT "Current source") #define TR_SELECT_MODEL "Select model" -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Create category" -#define TR_RENAME_CATEGORY "Rename category" -#define TR_DELETE_CATEGORY "Delete category" #define TR_CREATE_MODEL "Create model" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplicate model" #define TR_COPY_MODEL "Copy model" #define TR_MOVE_MODEL "Move model" @@ -640,7 +648,6 @@ #define TR_DELETE_MODEL "Delete model" #define TR_RESTORE_MODEL "Restore model" #define TR_DELETE_ERROR "Delete error" -#define TR_CAT_NOT_EMPTY "Category is not empty" #define TR_SDCARD_ERROR TR("SD error", "SD card error") #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -750,7 +757,7 @@ #define TR_SAMPLE_MODE "Sample Mode" #define TR_SAMPLE_MODES "Normal","OneBit" -#define TR_SELECT_TEMPLATE_FOLDER "SELECT A TEMPLATE FOLDER:" +#define TR_SELECT_TEMPLATE_FOLDER "Select a template folder" #define TR_SELECT_TEMPLATE "SELECT A MODEL TEMPLATE:" #define TR_NO_TEMPLATES "No model templates were found in this folder" #define TR_SAVE_TEMPLATE "Save as template" @@ -794,7 +801,7 @@ #define TR_PATH_TOO_LONG "Path too long" #define TR_VIEW_TEXT "View text" #define TR_FLASH_BOOTLOADER "Flash bootloader" -#define TR_FLASH_DEVICE TR("Flash device","Flash device") +#define TR_FLASH_DEVICE TR("Flash device","Flash device") #define TR_FLASH_EXTERNAL_DEVICE TR("Flash S.Port", "Flash S.Port device") #define TR_FLASH_RECEIVER_OTA "Flash receiver OTA" #define TR_FLASH_RECEIVER_BY_EXTERNAL_MODULE_OTA "Flash RX by ext. OTA" @@ -911,11 +918,8 @@ #define TR_TRAINER "Trainer" #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "TX antenna problem!" -#if defined(COLORLCD) - #define TR_MODELIDUSED "ID used in:" -#else - #define TR_MODELIDUSED TR("ID used in:","Receiver ID used in:") -#endif +#define TR_MODELIDUSED "ID used in:" +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Module" #define TR_RX_NAME "Rx Name" #define TR_TELEMETRY_TYPE TR("Type", "Telemetry type") @@ -1300,9 +1304,9 @@ #define TR_ADD_ALL_TRIMS_TO_SUBTRIMS "Add all Trims to Subtrims" #if LCD_W > LCD_H - #define TR_OPEN_CHANNEL_MONITORS "Open Channel Monitor" + #define TR_OPEN_CHANNEL_MONITORS "Open Channel Monitor" #else - #define TR_OPEN_CHANNEL_MONITORS "Open Channel Mon." + #define TR_OPEN_CHANNEL_MONITORS "Open Channel Mon." #endif #define TR_DUPLICATE "Duplicate" #define TR_ACTIVATE "Set Active" diff --git a/radio/src/translations/es.h b/radio/src/translations/es.h index db83f066c..4b4da7166 100644 --- a/radio/src/translations/es.h +++ b/radio/src/translations/es.h @@ -333,6 +333,7 @@ #define TR_COPYINGMODEL "Copiando modelo.." #define TR_MOVINGMODEL "Moviendo modelo..." #define TR_LOADINGMODEL "Cargando modelo..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Nombre" #define TR_MODELNAME TR("Nom. modelo", "Nombre modelo") #define TR_PHASENAME "Nombre fase " @@ -627,13 +628,20 @@ #define TR_VOLTAGE INDENT"Voltaje" #define TR_CURRENT INDENT"Actual" #define TR_SELECT_MODEL TR("Selec. modelo", "Seleccionar modelo") -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Crear categoría" -#define TR_RENAME_CATEGORY "Renombrar categoría" -#define TR_DELETE_CATEGORY "Borrar categoría" #define TR_CREATE_MODEL "Crear modelo" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplicar modelo" #define TR_COPY_MODEL "Copiar modelo" #define TR_MOVE_MODEL "Mover modelo" @@ -641,7 +649,6 @@ #define TR_DELETE_MODEL "Borrar modelo" #define TR_RESTORE_MODEL "Restaurar modelo" #define TR_DELETE_ERROR "Error de borrado" -#define TR_CAT_NOT_EMPTY "Categoría no esta vacía" #define TR_SDCARD_ERROR "Error SDCARD" #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -751,7 +758,7 @@ #define TR_SAMPLE_MODE "Modo de muestra" #define TR_SAMPLE_MODES "Normal","OneBit" -#define TR_SELECT_TEMPLATE_FOLDER "SELECT A TEMPLATE FOLDER:" +#define TR_SELECT_TEMPLATE_FOLDER "Select a template folder" #define TR_SELECT_TEMPLATE "SELECT A MODEL TEMPLATE:" #define TR_NO_TEMPLATES "No model templates were found in this folder" #define TR_SAVE_TEMPLATE "Save as template" @@ -913,6 +920,7 @@ #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "Problema antena TX!" #define TR_MODELIDUSED TR("ID en uso", "ID modelo en uso") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Módulo" #define TR_RX_NAME "Nombre Rx" #define TR_TELEMETRY_TYPE TR("Tipo", "Tipo telemetría") diff --git a/radio/src/translations/fi.h b/radio/src/translations/fi.h index 986360f31..a2b65803b 100644 --- a/radio/src/translations/fi.h +++ b/radio/src/translations/fi.h @@ -343,6 +343,7 @@ #define TR_COPYINGMODEL "Copying model..." #define TR_MOVINGMODEL "Moving model..." #define TR_LOADINGMODEL "Loading model..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Name" #define TR_MODELNAME "Model Name" #define TR_PHASENAME "Mode Name" @@ -641,13 +642,20 @@ #define TR_VOLTAGE INDENT"Jännite" #define TR_CURRENT INDENT"Virta" #define TR_SELECT_MODEL "Select Model" -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Create Category" -#define TR_RENAME_CATEGORY "Rename Category" -#define TR_DELETE_CATEGORY "Delete Category" #define TR_CREATE_MODEL "Create Model" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplicate Model" #define TR_COPY_MODEL "Copy Model" #define TR_MOVE_MODEL "Move Model" @@ -655,7 +663,6 @@ #define TR_DELETE_MODEL "Delete Model" #define TR_RESTORE_MODEL "Restore Model" #define TR_DELETE_ERROR "Delete error" -#define TR_CAT_NOT_EMPTY "Luokka ei ole tyhjä" #define TR_SDCARD_ERROR "SDCARD Error" #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -926,6 +933,7 @@ #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "Lähettimen antenniongelma!" #define TR_MODELIDUSED TR("ID used in:", "Receiver ID used in:") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Module" #define TR_RX_NAME "Rx Name" #define TR_TELEMETRY_TYPE TR("Type", "Telemetry Type") diff --git a/radio/src/translations/fr.h b/radio/src/translations/fr.h index 008122ea6..d1aaa3498 100644 --- a/radio/src/translations/fr.h +++ b/radio/src/translations/fr.h @@ -353,6 +353,7 @@ #define TR_COPYINGMODEL "Copie..." #define TR_MOVINGMODEL "Déplacement..." #define TR_LOADINGMODEL "Chargement..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Nom" #define TR_MODELNAME TR("Nom modèle", "Nom du modèle") #define TR_PHASENAME "Nom phase" @@ -646,13 +647,20 @@ #define TR_VOLTAGE TR(INDENT "Tension",INDENT "Source tension") #define TR_CURRENT TR(INDENT "Courant",INDENT "Source courant") #define TR_SELECT_MODEL "Sélect. modèle" -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Créer une catégorie" -#define TR_RENAME_CATEGORY "Renommer la catégorie" -#define TR_DELETE_CATEGORY "Supprimer la catégorie" #define TR_CREATE_MODEL "Créer modèle" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Dupliquer modèle" #define TR_COPY_MODEL "Copier modèle" #define TR_MOVE_MODEL "Déplacer modèle" @@ -660,7 +668,6 @@ #define TR_DELETE_MODEL "Supprimer modèle" #define TR_RESTORE_MODEL "Restaurer modèle" #define TR_DELETE_ERROR "Effacement impossible" -#define TR_CAT_NOT_EMPTY "Categorie non vide" #define TR_SDCARD_ERROR "Erreur carte SD" #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -769,7 +776,7 @@ #define TR_BAUDRATE "Baudrate" #define TR_SAMPLE_MODE "Sample Mode" #define TR_SAMPLE_MODES "Normal""OneBit" -#define TR_SELECT_TEMPLATE_FOLDER "SELECT A TEMPLATE FOLDER:" +#define TR_SELECT_TEMPLATE_FOLDER "Select a template folder" #define TR_SELECT_TEMPLATE "SELECT A MODEL TEMPLATE:" #define TR_NO_TEMPLATES "No model templates were found in this folder" #define TR_SAVE_TEMPLATE "Save as template" @@ -930,6 +937,7 @@ #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "Antenne radio défect.!" #define TR_MODELIDUSED TR("ID affecté à :", "No de récepteur utilisé par :") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Module" #define TR_RX_NAME "Nom Rx" #define TR_TELEMETRY_TYPE TR("Type tél.", "Type télémétrie") diff --git a/radio/src/translations/it.h b/radio/src/translations/it.h index 5c5b22181..c9bc8ceb4 100644 --- a/radio/src/translations/it.h +++ b/radio/src/translations/it.h @@ -338,6 +338,7 @@ #define TR_COPYINGMODEL "Copia in corso.." #define TR_MOVINGMODEL "Spostamento..." #define TR_LOADINGMODEL "Caricamento..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Nome" #define TR_MODELNAME TR("Nome Mode.", "Nome Modello") #define TR_PHASENAME "Nome fase" @@ -627,13 +628,20 @@ #define TR_VOLTAGE TR(INDENT "Voltagg.",INDENT "Voltaggio") #define TR_CURRENT TR(INDENT "Corrente",INDENT "Corrente") #define TR_SELECT_MODEL TR("Scegli mod.", "Scegli Modello") -#define TR_MODEL_CATEGORIES "Categorie Modelli" #define TR_MODELS "Modelli" #define TR_SELECT_MODE "Seleziona modo" -#define TR_CREATE_CATEGORY "Crea Categoria" -#define TR_RENAME_CATEGORY "Rinomina Categoria" -#define TR_DELETE_CATEGORY "Cancella Categoria" #define TR_CREATE_MODEL "Crea Modello" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplica Modello" #define TR_COPY_MODEL "Copia Modello" #define TR_MOVE_MODEL "Sposta Modello" @@ -641,7 +649,6 @@ #define TR_DELETE_MODEL TR("Elim. Modello", "Elimina Modello") #define TR_RESTORE_MODEL TR("Ripr. Modello", "Ripristina Modello") #define TR_DELETE_ERROR "Errore cancell." -#define TR_CAT_NOT_EMPTY "Categoria non vuota" #define TR_SDCARD_ERROR TR("Errore SD", "Errore SDCard") #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "SD senza file!" @@ -912,6 +919,7 @@ #define TR_CHANS "Canali" #define TR_ANTENNAPROBLEM CENTER "Problemi antenna TX!" #define TR_MODELIDUSED TR("ID già usato", "ID Modello già usato") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Modulo" #define TR_RX_NAME "Nome RX" #define TR_TELEMETRY_TYPE "Tipo Telemetria" diff --git a/radio/src/translations/nl.h b/radio/src/translations/nl.h index 4d7708e6a..f9f499bb4 100644 --- a/radio/src/translations/nl.h +++ b/radio/src/translations/nl.h @@ -336,6 +336,7 @@ #define TR_COPYINGMODEL "Kopieer Model" #define TR_MOVINGMODEL "Verplaats Model" #define TR_LOADINGMODEL "Laad Model..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Naam" #define TR_MODELNAME "Modelnaam" #define TR_PHASENAME "Modus" @@ -632,13 +633,20 @@ #define TR_VOLTAGE TR(INDENT "Spg", INDENT "Spanningsbron") //9XR-Pro #define TR_CURRENT TR(INDENT "Stroom", INDENT "Stroombron") #define TR_SELECT_MODEL "Kies Model" -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Create Category" -#define TR_RENAME_CATEGORY "Rename Category" -#define TR_DELETE_CATEGORY "Delete Category" #define TR_CREATE_MODEL "Nieuw Model" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Dupliceer Model" #define TR_COPY_MODEL "Kopieer Model" #define TR_MOVE_MODEL "Verplaats Model" @@ -646,7 +654,6 @@ #define TR_DELETE_MODEL "Wis Model" #define TR_RESTORE_MODEL "Model Terugzetten" #define TR_DELETE_ERROR "Fout bij verwijderen" -#define TR_CAT_NOT_EMPTY "Categorie is niet leeg" #define TR_SDCARD_ERROR "SD-Kaart fout" #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -755,7 +762,7 @@ #define TR_BAUDRATE "Baudrate" #define TR_SAMPLE_MODE "Sample Mode" #define TR_SAMPLE_MODES "Normal""OneBit" -#define TR_SELECT_TEMPLATE_FOLDER "SELECT A TEMPLATE FOLDER:" +#define TR_SELECT_TEMPLATE_FOLDER "Select a template folder" #define TR_SELECT_TEMPLATE "SELECT A MODEL TEMPLATE:" #define TR_NO_TEMPLATES "No model templates were found in this folder" #define TR_SAVE_TEMPLATE "Save as template" @@ -916,6 +923,7 @@ #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "TX-Antenneprobleem!" #define TR_MODELIDUSED TR("ID al gebruikt", "Model-ID al gebruikt") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Module-Type" #define TR_RX_NAME "Rx Name" #define TR_TELEMETRY_TYPE TR("Type", "Telemetrietype") diff --git a/radio/src/translations/pl.h b/radio/src/translations/pl.h index 50a308595..3ca12686b 100644 --- a/radio/src/translations/pl.h +++ b/radio/src/translations/pl.h @@ -333,6 +333,7 @@ #define TR_COPYINGMODEL "Kopiuj model.." #define TR_MOVINGMODEL "Przenieś model." #define TR_LOADINGMODEL "Załaduj model.." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Nazwa" #define TR_MODELNAME "Nazwa modelu" #define TR_PHASENAME "Nazwa Fazy" @@ -623,13 +624,20 @@ #define TR_VOLTAGE TR (INDENT"Napięcie",INDENT"Źródło Napięcia") #define TR_CURRENT TR (INDENT"Prąd",INDENT"Źródło Prądu") #define TR_SELECT_MODEL "Wybór modelu" -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Create Category" -#define TR_RENAME_CATEGORY "Rename Category" -#define TR_DELETE_CATEGORY "Delete Category" #define TR_CREATE_MODEL "Nowy model" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplicate Model" #define TR_COPY_MODEL "Skopiuj model" #define TR_MOVE_MODEL "Przenieś model" @@ -637,7 +645,6 @@ #define TR_DELETE_MODEL "Skasuj model" #define TR_RESTORE_MODEL "Odtwórz model" #define TR_DELETE_ERROR "Delete error" -#define TR_CAT_NOT_EMPTY "Category is not empty" #define TR_SDCARD_ERROR "Błąd karty SD" #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -746,7 +753,7 @@ #define TR_BAUDRATE "Baudrate" #define TR_SAMPLE_MODE "Sample Mode" #define TR_SAMPLE_MODES "Normal","OneBit" -#define TR_SELECT_TEMPLATE_FOLDER "SELECT A TEMPLATE FOLDER:" +#define TR_SELECT_TEMPLATE_FOLDER "Select a template folder" #define TR_SELECT_TEMPLATE "SELECT A MODEL TEMPLATE:" #define TR_NO_TEMPLATES "No model templates were found in this folder" #define TR_SAVE_TEMPLATE "Save as template" @@ -907,6 +914,7 @@ #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "Problem z anteną TX" #define TR_MODELIDUSED TR("ID zajęte","ID modelu zajęte") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Moduł " #define TR_RX_NAME "Rx Name" #define TR_TELEMETRY_TYPE TR("Typ ", "Typ Telemetrii") diff --git a/radio/src/translations/pt.h b/radio/src/translations/pt.h index 066ce9e26..168e02780 100644 --- a/radio/src/translations/pt.h +++ b/radio/src/translations/pt.h @@ -330,6 +330,7 @@ #define TR_COPYINGMODEL "Copiando modelo" #define TR_MOVINGMODEL "Movendo modelo" #define TR_LOADINGMODEL "Carregar modelo" +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Nome" #define TR_MODELNAME "Model Name" #define TR_PHASENAME "Phase Name" @@ -628,13 +629,20 @@ #define TR_VOLTAGE INDENT"Volts" #define TR_CURRENT INDENT"Amperes" #define TR_SELECT_MODEL "Selec. Modelo" -#define TR_MODEL_CATEGORIES "Model Categories" #define TR_MODELS "Models" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Create Category" -#define TR_RENAME_CATEGORY "Rename Category" -#define TR_DELETE_CATEGORY "Delete Category" #define TR_CREATE_MODEL "Criar Modelo" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplicate Model" #define TR_COPY_MODEL "Copy Model" #define TR_MOVE_MODEL "Move Model" @@ -642,7 +650,6 @@ #define TR_DELETE_MODEL "Apagar Modelo" #define TR_RESTORE_MODEL "Restaura Modelo" #define TR_DELETE_ERROR "Delete error" -#define TR_CAT_NOT_EMPTY "Category is not empty" #define TR_SDCARD_ERROR "SDCARD Erro" #define TR_SDCARD "SD Card" #define TR_NO_FILES_ON_SD "No files on SD!" @@ -751,7 +758,7 @@ #define TR_BAUDRATE "Baudrate" #define TR_SAMPLE_MODE "Sample Mode" #define TR_SAMPLE_MODES "Normal","OneBit" -#define TR_SELECT_TEMPLATE_FOLDER "SELECT A TEMPLATE FOLDER:" +#define TR_SELECT_TEMPLATE_FOLDER "Select a template folder" #define TR_SELECT_TEMPLATE "SELECT A MODEL TEMPLATE:" #define TR_NO_TEMPLATES "No model templates were found in this folder" #define TR_SAVE_TEMPLATE "Save as template" @@ -912,6 +919,7 @@ #define TR_CHANS "Chans" #define TR_ANTENNAPROBLEM CENTER "TX Antenna problem!" #define TR_MODELIDUSED TR("ID used in:", "Receiver ID used in:") +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Module" #define TR_RX_NAME "Rx Name" #define TR_TELEMETRY_TYPE TR("Type", "Telemetry Type") diff --git a/radio/src/translations/se.h b/radio/src/translations/se.h index 2e2a6c204..c2ff3f6bc 100644 --- a/radio/src/translations/se.h +++ b/radio/src/translations/se.h @@ -354,6 +354,7 @@ #define TR_COPYINGMODEL "Kopierar..." #define TR_MOVINGMODEL "Flyttar..." #define TR_LOADINGMODEL "Laddar..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "Namn" #define TR_MODELNAME "Modellnamn" #define TR_PHASENAME "Lägesnamn" @@ -653,13 +654,20 @@ #define TR_VOLTAGE INDENT"Volt" #define TR_CURRENT INDENT"Ampere" #define TR_SELECT_MODEL "Välj modell" -#define TR_MODEL_CATEGORIES "Modellkategori" #define TR_MODELS "Modeller" #define TR_SELECT_MODE "Select mode" -#define TR_CREATE_CATEGORY "Skapa kategori" -#define TR_RENAME_CATEGORY "Byt kategorinamn" -#define TR_DELETE_CATEGORY "Ta bort kategori" #define TR_CREATE_MODEL "Skapa modell" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "Duplicera modell" #define TR_COPY_MODEL "Kopiera modell" #define TR_MOVE_MODEL "Flytta modell" @@ -667,7 +675,6 @@ #define TR_DELETE_MODEL "Ta bort modell" #define TR_RESTORE_MODEL "Återställ modell" #define TR_DELETE_ERROR "Delete error" -#define TR_CAT_NOT_EMPTY "Category is not empty" #define TR_SDCARD_ERROR "SD-kortfel" #define TR_SDCARD "SD-kort" #define TR_NO_FILES_ON_SD "Inga filer på SD!" @@ -941,11 +948,8 @@ #define TR_TRAINER "Lärare" #define TR_CHANS "Kan" #define TR_ANTENNAPROBLEM CENTER "Fel på TX-antennen" -#if defined(COLORLCD) - #define TR_MODELIDUSED "ID använt i:" -#else - #define TR_MODELIDUSED TR("ID använt i:","Mottagar-ID använt i:") -#endif +#define TR_MODELIDUSED "ID använt i:" +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "Modul" #define TR_RX_NAME "RX namn" #define TR_TELEMETRY_TYPE "Telemetrityp" diff --git a/radio/src/translations/tw.h b/radio/src/translations/tw.h index 3220f65eb..1829128ff 100644 --- a/radio/src/translations/tw.h +++ b/radio/src/translations/tw.h @@ -331,6 +331,7 @@ #define TR_COPYINGMODEL "複製模型..." #define TR_MOVINGMODEL "移動模型..." #define TR_LOADINGMODEL "載入模型..." +#define TR_UNLABELEDMODEL "Unlabeled" #define TR_NAME "名稱" #define TR_MODELNAME "模型名稱" #define TR_PHASENAME "模式名稱" @@ -626,13 +627,20 @@ #define TR_VOLTAGE TR(INDENT "電壓源", INDENT "電壓來源") #define TR_CURRENT TR(INDENT "電流源", INDENT "電流來源") #define TR_SELECT_MODEL "選擇模型" -#define TR_MODEL_CATEGORIES "模型分類" #define TR_MODELS "個模型" #define TR_SELECT_MODE "選擇模式" -#define TR_CREATE_CATEGORY "創建分類" -#define TR_RENAME_CATEGORY "分類改名" -#define TR_DELETE_CATEGORY "刪除分類" #define TR_CREATE_MODEL "創建模型" +#define TR_FAVORITE_LABEL "Favorites" +#define TR_NEW_MODEL "New Model" +#define TR_EDIT_LABELS "Edit Labels" +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_LABELS "Labels" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label" #define TR_DUPLICATE_MODEL "複製模型" #define TR_COPY_MODEL "複製模型" #define TR_MOVE_MODEL "移動模型" @@ -640,7 +648,6 @@ #define TR_DELETE_MODEL "刪除模型" #define TR_RESTORE_MODEL "恢復模型" #define TR_DELETE_ERROR "刪除失敗" -#define TR_CAT_NOT_EMPTY "分類未清空" #define TR_SDCARD_ERROR TR("存儲卡錯誤", "存儲卡錯誤") #define TR_SDCARD "存儲卡" #define TR_NO_FILES_ON_SD "存儲卡中沒有文件!" @@ -911,11 +918,8 @@ #define TR_TRAINER "教練" #define TR_CHANS "通道" #define TR_ANTENNAPROBLEM CENTER "發射機天線故障!" -#if defined(COLORLCD) - #define TR_MODELIDUSED "ID已使用:" -#else - #define TR_MODELIDUSED TR("ID已使用:","ID已使用:") -#endif +#define TR_MODELIDUSED "ID已使用:" +#define TR_MODELIDUNIQUE "ID is unique" #define TR_MODULE "模塊" #define TR_RX_NAME "接收機名稱" #define TR_TELEMETRY_TYPE TR("類型", "回傳類型") @@ -1334,4 +1338,4 @@ #define TR_PROTOCOL "協議" #define TR_YEAR "年" #define TR_MONTH "月" -#define TR_DAY "日" \ No newline at end of file +#define TR_DAY "日" diff --git a/radio/src/translations/untranslated.h b/radio/src/translations/untranslated.h index 8edddc826..d42bab8f3 100644 --- a/radio/src/translations/untranslated.h +++ b/radio/src/translations/untranslated.h @@ -371,3 +371,12 @@ #define STR_HOTT_ID_EAM_VV "EAvv" #define STR_HOTT_ID_EAM_RPM "EArp" #define STR_HOTT_ID_EAM_SPEED "EAsp" + +#define TR_ENTER_LABEL "Enter Label" +#define TR_LABEL "Label" +#define TR_NONE "None" +#define TR_CURRENT_MODEL "Current" +#define TR_NEW "New" +#define TR_NEW_LABEL "New Label" +#define TR_RENAME_LABEL "Rename Label" +#define TR_DELETE_LABEL "Delete Label"