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:
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user