feat: Model templates for color radios (update SD card for bundled templates) (#1569)

* Model templates - select and load after Create Model for color radios

* Model templates - save model as template, only for color radios

* Small fix to drawTextLines(...) to handle consecutive spaces

* Change title of button for empty template folder

* I found out why "no trailing slash = important" in path names!

* Reworked file and path name string manipulation
No longer using chdir, as Lua scripts etc. could be messing with the working dir
Use buffers more and std::string less

* Adjusting the size of buffers
I think that I got it right now. Really hard to figure out the right size constants!
Also added storageDirty(EE_MODEL) after reading the template file and renaming the model

* Added storage checks

* Slight simplification - all template buttons are similar

* Added "Blank Model" button

* Wizard Lua script
If there is a Lua script with the same name as the template file in the folder, then it is run after reading the template.
This commit is contained in:
Jesper Frickmann
2022-03-09 11:00:38 +10:00
committed by GitHub
parent e0e196a6e6
commit dcc5cfb352
22 changed files with 399 additions and 32 deletions
+1 -1
View File
@@ -539,8 +539,8 @@ void drawTextLines(BitmapBuffer * dc, coord_t left, coord_t top, coord_t width,
case '(':
case '{':
case '[':
nxt++;
case ' ':
nxt++;
case '\n':
case '\0':
done = true;
+268 -6
View File
@@ -24,26 +24,270 @@
#include "opentx.h"
#include "storage/modelslist.h"
#include "libopenui.h"
#include "standalone_lua.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;
inline tmr10ms_t getTicks() { return g_tmr10ms; }
class TemplatePage : public Page
{
public:
TemplatePage() : Page(ICON_MODEL_SELECT)
{ }
void 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 onEvent(event_t event) override
{
if (event == EVT_KEY_LONG(KEY_EXIT) || event == EVT_KEY_BREAK(KEY_EXIT)) {
killEvents(event);
deleteLater();
} else {
Page::onEvent(event);
}
}
#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;
void 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);
}
}
};
char TemplatePage::path[LEN_PATH + 1];
class TemplateButton : public TextButton
{
public:
TemplateButton(FormGroup* parent, const rect_t& rect, std::string name, std::function<uint8_t(void)> pressHandler = nullptr)
: TextButton(parent, rect, name, pressHandler)
{ }
};
class SelectTemplate : public TemplatePage
{
public:
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<std::string> 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);
// Update model list with new name
auto model = modelslist.getCurrentModel();
model->setModelName(g_model.header.name);
modelslist.save();
// Dismiss template pages
deleteLater();
templateFolderPage->deleteLater();
// 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);
StandaloneLuaWindow::instance()->attach(focusWindow);
}
return 0;
});
tb->setFocusHandler([=](bool active) {
if (active) {
snprintf(buffer, LEN_BUFFER, "%s%c%s%s", path, '/', name.c_str(), TEXT_EXT);
updateInfo();
}
});
grid.nextLine();
}
body.setInnerHeight(grid.getWindowHeight());
}
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;
rect.h = PAGE_LINE_HEIGHT;
new StaticText(&body, rect, STR_NO_TEMPLATES, 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;
rect.w = 100;
rect.h = PAGE_LINE_HEIGHT;
new TemplateButton(&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()
{
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(), "Blank Model", [=]() -> uint8_t {
deleteLater();
return 0;
});
tfb->setFocusHandler([=](bool active) {
if (active) {
snprintf(infoText, LEN_INFO_TEXT, "%c", '\0');
invalidate();
}
});
grid.nextLine();
std::list<std::string> 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) {
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();
}
});
grid.nextLine();
}
body.setInnerHeight(grid.getWindowHeight());
}
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);
}
}
};
class ModelButton : public Button
{
public:
@@ -258,6 +502,23 @@ class ModelCategoryPageBody : public FormWindow
POPUP_WARNING("Invalid File");
}
});
menu->addLine(STR_SAVE_TEMPLATE, [=]() {
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);
if (isFileAvailable(templatePath)) {
new ConfirmDialog(parent, 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);
}
});
if (model != modelslist.getCurrentModel()) {
// Move
if(modelslist.getCategories().size() > 1) {
@@ -370,6 +631,7 @@ class ModelCategoryPageBody : public FormWindow
modelslist.setCurrentModel(model);
modelslist.save();
update(category->size() - 1);
new SelectTemplateFolder();
};
}
};
@@ -451,7 +713,7 @@ class CategoryEditPage : public PageTab
// Details
char cnt[19];
snprintf(cnt, sizeof(cnt), "%u %s", category->size(), STR_MODELS);
snprintf(cnt, sizeof(cnt), "%u %s", (unsigned)category->size(), STR_MODELS);
new StaticText(window, grid.getFieldSlot(3,1), cnt);
if(category->empty()) {