diff --git a/companion/src/firmwares/edgetx/yaml_modeldata.cpp b/companion/src/firmwares/edgetx/yaml_modeldata.cpp index 0419d0a4b3c..1be95e629fc 100644 --- a/companion/src/firmwares/edgetx/yaml_modeldata.cpp +++ b/companion/src/firmwares/edgetx/yaml_modeldata.cpp @@ -777,6 +777,7 @@ Node convert::encode(const ModelData& rhs) node["extendedLimits"] = (int)rhs.extendedLimits; node["extendedTrims"] = (int)rhs.extendedTrims; node["throttleReversed"] = (int)rhs.throttleReversed; + node["checklistInteractive"] = (int)rhs.checklistInteractive; for (int i = 0; i < CPN_MAX_FLIGHT_MODES; i++) { if (!rhs.flightModeData[i].isEmpty(i)) { @@ -1012,6 +1013,7 @@ bool convert::decode(const Node& node, ModelData& rhs) node["extendedLimits"] >> rhs.extendedLimits; node["extendedTrims"] >> rhs.extendedTrims; node["throttleReversed"] >> rhs.throttleReversed; + node["checklistInteractive"] >> rhs.checklistInteractive; node["flightModeData"] >> rhs.flightModeData; node["mixData"] >> rhs.mixData; diff --git a/companion/src/firmwares/modeldata.h b/companion/src/firmwares/modeldata.h index 3d63a65e25e..8ec15233dde 100644 --- a/companion/src/firmwares/modeldata.h +++ b/companion/src/firmwares/modeldata.h @@ -131,6 +131,7 @@ class ModelData { bool extendedLimits; // TODO xml bool extendedTrims; bool throttleReversed; + bool checklistInteractive; FlightModeData flightModeData[CPN_MAX_FLIGHT_MODES]; MixData mixData[CPN_MAX_MIXERS]; LimitData limitData[CPN_MAX_CHNOUT]; diff --git a/companion/src/modeledit/setup.cpp b/companion/src/modeledit/setup.cpp index c604fdebd2a..f7f2bdf6b8c 100644 --- a/companion/src/modeledit/setup.cpp +++ b/companion/src/modeledit/setup.cpp @@ -1659,6 +1659,7 @@ void SetupPanel::update() ui->extendedLimits->setChecked(model->extendedLimits); ui->extendedTrims->setChecked(model->extendedTrims); ui->displayText->setChecked(model->displayChecklist); + ui->checklistInteractive->setChecked(model->checklistInteractive); ui->gfEnabled->setChecked(!model->noGlobalFunctions); ui->jitterFilter->setCurrentIndex(model->jitterFilter); @@ -1806,6 +1807,12 @@ void SetupPanel::on_displayText_toggled(bool checked) emit modified(); } +void SetupPanel::on_checklistInteractive_toggled(bool checked) +{ + model->checklistInteractive = checked; + emit modified(); +} + void SetupPanel::on_gfEnabled_toggled(bool checked) { model->noGlobalFunctions = !checked; diff --git a/companion/src/modeledit/setup.h b/companion/src/modeledit/setup.h index cdc7302cdb7..d68e0881f2b 100644 --- a/companion/src/modeledit/setup.h +++ b/companion/src/modeledit/setup.h @@ -188,6 +188,7 @@ class SetupPanel : public ModelPanel void on_customThrottleWarningPosition_valueChanged(int value); void on_throttleReverse_toggled(bool checked); void on_displayText_toggled(bool checked); + void on_checklistInteractive_toggled(bool checked); void on_gfEnabled_toggled(bool checked); void on_image_currentIndexChanged(int index); void on_trimIncrement_currentIndexChanged(int index); diff --git a/companion/src/modeledit/setup.ui b/companion/src/modeledit/setup.ui index f26dc9888ba..0f5e36902c9 100644 --- a/companion/src/modeledit/setup.ui +++ b/companion/src/modeledit/setup.ui @@ -579,6 +579,16 @@ + + + + Qt::LeftToRight + + + Interactive Checklist + + + diff --git a/radio/src/datastructs_private.h b/radio/src/datastructs_private.h index 4248046e27d..5b2142cdd76 100644 --- a/radio/src/datastructs_private.h +++ b/radio/src/datastructs_private.h @@ -643,8 +643,9 @@ PACK(struct ModelData { uint8_t extendedLimits:1; uint8_t extendedTrims:1; uint8_t throttleReversed:1; + uint8_t checklistInteractive:1; uint8_t enableCustomThrottleWarning:1; - uint8_t spare3:7 SKIP; + uint8_t spare3:6 SKIP; int8_t customThrottleWarningPosition; BeepANACenter beepANACenter; MixData mixData[MAX_MIXERS] NO_IDX; diff --git a/radio/src/gui/128x64/model_setup.cpp b/radio/src/gui/128x64/model_setup.cpp index 3efbc405a01..0197ce82e14 100644 --- a/radio/src/gui/128x64/model_setup.cpp +++ b/radio/src/gui/128x64/model_setup.cpp @@ -89,6 +89,7 @@ enum MenuModelSetupItems { ITEM_MODEL_SETUP_THROTTLE_TRIM_SWITCH, ITEM_MODEL_SETUP_PREFLIGHT_LABEL, ITEM_MODEL_SETUP_CHECKLIST_DISPLAY, + ITEM_MODEL_SETUP_CHECKLIST_INTERACTIVE, ITEM_MODEL_SETUP_THROTTLE_WARNING, ITEM_MODEL_SETUP_CUSTOM_THROTTLE_WARNING, ITEM_MODEL_SETUP_CUSTOM_THROTTLE_WARNING_VALUE, @@ -450,6 +451,7 @@ void menuModelSetup(event_t event) LABEL(PreflightCheck), 0, // Checklist + 0, // Checklist interactive mode 0, // Throttle warning 0, // Custom position for throttle warning enable 0, // Custom position for throttle warning value @@ -751,6 +753,10 @@ void menuModelSetup(event_t event) case ITEM_MODEL_SETUP_CHECKLIST_DISPLAY: g_model.displayChecklist = editCheckBox(g_model.displayChecklist, MODEL_SETUP_2ND_COLUMN, y, STR_CHECKLIST, attr, event); break; + + case ITEM_MODEL_SETUP_CHECKLIST_INTERACTIVE: + g_model.checklistInteractive = editCheckBox(g_model.checklistInteractive, MODEL_SETUP_2ND_COLUMN, y, STR_CHECKLIST_INTERACTIVE, attr, event); + break; case ITEM_MODEL_SETUP_THROTTLE_WARNING: g_model.disableThrottleWarning = !editCheckBox(!g_model.disableThrottleWarning, MODEL_SETUP_2ND_COLUMN, y, STR_THROTTLE_WARNING, attr, event); diff --git a/radio/src/gui/212x64/model_setup.cpp b/radio/src/gui/212x64/model_setup.cpp index e4b54b703a9..689b44d0989 100644 --- a/radio/src/gui/212x64/model_setup.cpp +++ b/radio/src/gui/212x64/model_setup.cpp @@ -74,6 +74,7 @@ enum MenuModelSetupItems { ITEM_MODEL_SETUP_THROTTLE_TRIM_SWITCH, ITEM_MODEL_SETUP_PREFLIGHT_LABEL, ITEM_MODEL_SETUP_CHECKLIST_DISPLAY, + ITEM_MODEL_SETUP_CHECKLIST_INTERACTIVE, ITEM_MODEL_SETUP_THROTTLE_WARNING, ITEM_MODEL_SETUP_CUSTOM_THROTTLE_WARNING, ITEM_MODEL_SETUP_CUSTOM_THROTTLE_WARNING_VALUE, @@ -394,6 +395,7 @@ void menuModelSetup(event_t event) LABEL(PreflightCheck), 0, // Checklist + 0, // Checklist interactive mode 0, // Throttle warning 0, // Custom position for throttle warning enable 0, // Custom position for throttle warning value @@ -661,6 +663,10 @@ void menuModelSetup(event_t event) case ITEM_MODEL_SETUP_CHECKLIST_DISPLAY: g_model.displayChecklist = editCheckBox(g_model.displayChecklist, MODEL_SETUP_2ND_COLUMN, y, STR_CHECKLIST, attr, event); break; + + case ITEM_MODEL_SETUP_CHECKLIST_INTERACTIVE: + g_model.checklistInteractive = editCheckBox(g_model.checklistInteractive, MODEL_SETUP_2ND_COLUMN, y, STR_CHECKLIST_INTERACTIVE, attr, event); + break; case ITEM_MODEL_SETUP_THROTTLE_WARNING: g_model.disableThrottleWarning = !editCheckBox(!g_model.disableThrottleWarning, MODEL_SETUP_2ND_COLUMN, y, STR_THROTTLE_WARNING, attr, event); diff --git a/radio/src/gui/colorlcd/model_setup.cpp b/radio/src/gui/colorlcd/model_setup.cpp index ac58aaea18c..0db79042100 100644 --- a/radio/src/gui/colorlcd/model_setup.cpp +++ b/radio/src/gui/colorlcd/model_setup.cpp @@ -1522,6 +1522,11 @@ void ModelSetupPage::build(FormWindow * window) new CheckBox(window, grid.getFieldSlot(), GET_SET_DEFAULT(g_model.displayChecklist)); grid.nextLine(); + // Interactive checklist + new StaticText(window, grid.getLabelSlot(true), STR_CHECKLIST_INTERACTIVE, 0, COLOR_THEME_PRIMARY1); + new CheckBox(window, grid.getFieldSlot(), GET_SET_DEFAULT(g_model.checklistInteractive)); + grid.nextLine(); + // Throttle warning new StaticText(window, grid.getLabelSlot(true), STR_THROTTLE_WARNING, 0, COLOR_THEME_PRIMARY1); new CheckBox(window, grid.getFieldSlot(), GET_SET_INVERTED(g_model.disableThrottleWarning)); diff --git a/radio/src/gui/colorlcd/view_main_menu.cpp b/radio/src/gui/colorlcd/view_main_menu.cpp index 22a86d03dae..62ed9e4dfe6 100644 --- a/radio/src/gui/colorlcd/view_main_menu.cpp +++ b/radio/src/gui/colorlcd/view_main_menu.cpp @@ -63,7 +63,7 @@ ViewMainMenu::ViewMainMenu(Window* parent) : if (modelHasNotes()) { carousel->addButton(ICON_MODEL_NOTES, STR_MAIN_MENU_MODEL_NOTES, [=]() -> uint8_t { deleteLater(); - readModelNotes(); + readModelNotes(true); return 0; }); } diff --git a/radio/src/gui/colorlcd/view_text.cpp b/radio/src/gui/colorlcd/view_text.cpp index d72013ff694..af01f8ce268 100644 --- a/radio/src/gui/colorlcd/view_text.cpp +++ b/radio/src/gui/colorlcd/view_text.cpp @@ -35,10 +35,60 @@ case EVT_KEY_BREAK(KEY_PGUP): \ case EVT_KEY_BREAK(KEY_UP) -#define CASE_EVT_START \ - case EVT_ENTRY: \ - case EVT_KEY_BREAK(KEY_ENTER): \ - case EVT_KEY_BREAK(KEY_TELEM) +constexpr char NON_CHECKABLE_PREFIX = '='; + +class CheckBoxStatic : public Window { + public: + CheckBoxStatic(Window * parent, const rect_t & rect, std::function getChecked, + std::function getFocus, std::function getVisible, WindowFlags flags = 0) : + Window(parent, rect, flags, 0), + _getChecked(getChecked), + _getFocus(getFocus), + _getVisible(getVisible) + { + coord_t size = min(rect.w, rect.h); + setWidth(size); + setHeight(size); + _checked = _getChecked(); + _focus = _getFocus(); + _visible = _getVisible(); + } + +#if defined(DEBUG_WINDOWS) + std::string getName() const override + { + return "CheckBoxStatic"; + } +#endif + + void paint(BitmapBuffer * dc) + { + if(_visible) + theme->drawCheckBox(dc, _checked, 0, FIELD_PADDING_TOP, _focus); + } + + void checkEvents() override + { + Window::checkEvents(); + bool checked = _getChecked(); + bool focus = _getFocus(); + bool visible = _getVisible(); + if (checked != _checked || focus != _focus || visible != _visible) { + _checked = checked; + _focus = focus; + _visible = visible; + invalidate(); + } + } + + protected: + std::function _getChecked; + std::function _getFocus; + std::function _getVisible; + bool _checked; + bool _focus; + bool _visible; +}; void ViewTextWindow::extractNameSansExt() { @@ -57,8 +107,9 @@ void ViewTextWindow::extractNameSansExt() void ViewTextWindow::buildBody(Window *window) { - GridLayout grid(window); + FormGridLayout grid(window->width()); grid.spacer(); + grid.setLabelWidth(PAGE_LINE_HEIGHT + 3UL * PAGE_LINE_SPACING); // width of "first column" for checkboxes int i; FIL file; @@ -98,11 +149,25 @@ void ViewTextWindow::buildBody(Window *window) loadOneScreen(openFromEnd ? (maxLines - maxScreenLines) : 0); for (i = 0; i < maxScreenLines; i++) { - new DynamicText(window, grid.getSlot(), [=]() { - std::string str = - (lines[i][0]) ? std::string(lines[i]) : std::string(" "); - return std::string(str); - }); + if (g_model.checklistInteractive) { + if (!fromMenu) { + new CheckBoxStatic(window, grid.getLabelSlot(), + [=]() { return i < checklistPosition-(int)textVerticalOffset;}, + [=]() { return i == checklistPosition-(int)textVerticalOffset;}, + [=]() { return lines[i][0] && lines[i][0]!=NON_CHECKABLE_PREFIX;}); + } + new DynamicText(window, grid.getFieldSlot(), [=]() { + std::string str = + (lines[i][0]) ? std::string(lines[i]).substr(lines[i][0]==NON_CHECKABLE_PREFIX ? 1 : 0, std::string::npos) : std::string(" "); + return std::string(str); + }); + } + else { + new DynamicText(window, grid.getSlot(), [=]() { + std::string str = (lines[i][0]) ? std::string(lines[i]) : std::string(" "); + return std::string(str); + }); + } grid.nextLine(); } } @@ -149,14 +214,28 @@ void ViewTextWindow::checkEvents() if (lineStep > (maxScreenLines >> 1)) lineStep = maxScreenLines >> 1; switch (event) { - CASE_EVT_START: - textVerticalOffset = 0; - readLinesCount = 0; - sdReadTextFileBlock(fullPath.c_str(), readLinesCount); + case EVT_KEY_BREAK(KEY_ENTER): + // check if interactive checklist enabled and process only items displayed on screen (cannot mark item that is out of the screen) + if (g_model.checklistInteractive && !fromMenu && checklistPosition-(int)textVerticalOffset >= 0) { + if (checklistPosition < readLinesCount) { + if (checklistPosition-(int)textVerticalOffset < maxScreenLines) { + do{ + ++checklistPosition; + if (checklistPosition-(int)textVerticalOffset >= maxScreenLines-1 && textVerticalOffset + maxScreenLines < readLinesCount) { + ++textVerticalOffset; + sdReadTextFileBlock(fullPath.c_str(), readLinesCount); + } + }while(checklistPosition < readLinesCount && lines[checklistPosition-(int)textVerticalOffset][0] == NON_CHECKABLE_PREFIX); + } + } + else { + Page::onEvent(EVT_KEY_BREAK(KEY_EXIT)); + } + } break; CASE_EVT_KEY_NEXT_LINE: - if (textBottom && textVerticalOffset) + if(textVerticalOffset + maxScreenLines >= readLinesCount) break; else { textVerticalOffset += lineStep; @@ -175,10 +254,19 @@ void ViewTextWindow::checkEvents() sdReadTextFileBlock(fullPath.c_str(), readLinesCount); break; - - default: + + case EVT_KEY_FIRST(KEY_EXIT): + case EVT_KEY_LONG(KEY_EXIT): + case EVT_KEY_REPT(KEY_EXIT): + case EVT_KEY_BREAK(KEY_EXIT): + if (!(g_model.checklistInteractive && !fromMenu)) { Page::onEvent(event); - break; + } + break; + + default: + Page::onEvent(event); + break; } } Page::checkEvents(); @@ -197,7 +285,7 @@ void ViewTextWindow::sdReadTextFileBlock(const char *filename, int &lines_count) int result; char c; unsigned int sz = 0; - int line_length = 1; + int line_length = 0; uint8_t escape = 0; char escape_chars[4] = {0}; int current_line = 0; @@ -205,7 +293,6 @@ void ViewTextWindow::sdReadTextFileBlock(const char *filename, int &lines_count) for (int i = 0; i < maxScreenLines; i++) { memclear(lines[i], maxLineLength + 1); - lines[i][0] = ' '; } result = f_open(&file, (TCHAR *)filename, FA_OPEN_EXISTING | FA_READ); @@ -216,7 +303,7 @@ void ViewTextWindow::sdReadTextFileBlock(const char *filename, int &lines_count) { if (c == '\n' || line_length >= maxLineLength) { ++current_line; - line_length = 1; + line_length = 0; escape = 0; } if (c != '\r' && c != '\n' && current_line >= textVerticalOffset && @@ -302,16 +389,17 @@ static void replaceSpaceWithUnderscore(std::string &name) #define MODEL_FILE_EXT MODELS_EXT #endif -bool openNotes(const char buf[], std::string modelNotesName) +bool openNotes(const char buf[], std::string modelNotesName, bool fromMenu = false) { - if (isFileAvailable(modelNotesName.c_str())) { - new ViewTextWindow(std::string(buf), modelNotesName, ICON_MODEL); + if (isFileAvailable(modelNotesName.c_str())) { + new ViewTextWindow(std::string(buf), modelNotesName, ICON_MODEL, fromMenu); return true; } else { return false; } } -void readModelNotes() + +void readModelNotes(bool fromMenu) { bool notesFound = false; LED_ERROR_BEGIN(); @@ -321,10 +409,10 @@ void readModelNotes() const char buf[] = {MODELS_PATH}; f_chdir((TCHAR *)buf); - notesFound = openNotes(buf, modelNotesName); + notesFound = openNotes(buf, modelNotesName, fromMenu); if (!notesFound) { replaceSpaceWithUnderscore(modelNotesName); - notesFound = openNotes(buf, modelNotesName); + notesFound = openNotes(buf, modelNotesName, fromMenu); } #if !defined(EEPROM) @@ -334,11 +422,11 @@ void readModelNotes() if (index != std::string::npos) { modelNotesName.erase(index); modelNotesName.append(TEXT_EXT); - notesFound = openNotes(buf, modelNotesName); + notesFound = openNotes(buf, modelNotesName, fromMenu); } if (!notesFound) { replaceSpaceWithUnderscore(modelNotesName); - notesFound = openNotes(buf, modelNotesName); + notesFound = openNotes(buf, modelNotesName, fromMenu); } } #endif diff --git a/radio/src/gui/colorlcd/view_text.h b/radio/src/gui/colorlcd/view_text.h index b126c0843a2..1e02f2546a0 100644 --- a/radio/src/gui/colorlcd/view_text.h +++ b/radio/src/gui/colorlcd/view_text.h @@ -30,8 +30,8 @@ class ViewTextWindow : public Page { public: ViewTextWindow(const std::string iPath, const std::string iName, - unsigned int icon = ICON_RADIO_SD_MANAGER) : - Page(icon), path(std::move(iPath)), name(std::move(iName)), icon(icon) + unsigned int icon = ICON_RADIO_SD_MANAGER, bool fromMenu = false) : + Page(icon), path(std::move(iPath)), name(std::move(iName)), icon(icon), fromMenu(fromMenu) { fullPath = path + std::string("/") + name; extractNameSansExt(); @@ -40,10 +40,14 @@ class ViewTextWindow : public Page maxPos = 0; maxLines = 0; isInSetup = true; + checklistPosition = 0; + textVerticalOffset = 0; + readLinesCount = 0; header.setWindowFlags(NO_SCROLLBAR); buildHeader(&header); buildBody(&body); + sdReadTextFileBlock(fullPath.c_str(), readLinesCount); }; void sdReadTextFileBlock(const char* filename, int& lines_count); @@ -84,6 +88,8 @@ class ViewTextWindow : public Page uint16_t readCount; int longestLine; + int checklistPosition; + bool fromMenu; char** lines = nullptr; int maxScreenLines; @@ -108,4 +114,4 @@ class ViewTextWindow : public Page }; }; -void readModelNotes(); +void readModelNotes(bool fromMenu = false); diff --git a/radio/src/gui/common/stdlcd/model_notes.cpp b/radio/src/gui/common/stdlcd/model_notes.cpp index 47b452b1b56..b61038c1e05 100644 --- a/radio/src/gui/common/stdlcd/model_notes.cpp +++ b/radio/src/gui/common/stdlcd/model_notes.cpp @@ -18,7 +18,6 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ - #include "opentx.h" void menuModelNotes(event_t event) @@ -28,7 +27,7 @@ void menuModelNotes(event_t event) char *buf = strcat_currentmodelname(&reusableBuffer.viewText.filename[sizeof(MODELS_PATH)], 0); strcpy(buf, TEXT_EXT); } - + reusableBuffer.viewText.pushMenu = true; menuTextView(event); } diff --git a/radio/src/gui/common/stdlcd/view_text.cpp b/radio/src/gui/common/stdlcd/view_text.cpp index 01aada192e0..be119664706 100644 --- a/radio/src/gui/common/stdlcd/view_text.cpp +++ b/radio/src/gui/common/stdlcd/view_text.cpp @@ -30,6 +30,8 @@ #endif constexpr uint32_t TEXT_FILE_MAXSIZE = 2048; +constexpr char NON_CHECKABLE_PREFIX = '='; +int checklistPosition; static void sdReadTextFile(const char * filename, char lines[TEXT_VIEWER_LINES][LCD_COLS + 1], int & lines_count) { @@ -107,7 +109,8 @@ void readModelNotes() waitKeysReleased(); event_t event = EVT_ENTRY; - while (event != EVT_KEY_BREAK(KEY_EXIT)) { + reusableBuffer.viewText.pushMenu = false; + while (true) { uint32_t power = pwrCheck(); if (power != e_power_press) { lcdRefreshWait(); @@ -122,6 +125,7 @@ void readModelNotes() } event = getEvent(); WDG_RESET(); + if (reusableBuffer.viewText.checklistComplete) break; } LED_ERROR_END(); @@ -132,6 +136,8 @@ void menuTextView(event_t event) switch (event) { case EVT_ENTRY: menuVerticalOffset = 0; + checklistPosition = 0; + reusableBuffer.viewText.checklistComplete = false; reusableBuffer.viewText.linesCount = 0; sdReadTextFile(reusableBuffer.viewText.filename, reusableBuffer.viewText.lines, reusableBuffer.viewText.linesCount); break; @@ -143,6 +149,24 @@ void menuTextView(event_t event) menuVerticalOffset--; sdReadTextFile(reusableBuffer.viewText.filename, reusableBuffer.viewText.lines, reusableBuffer.viewText.linesCount); break; + + case EVT_KEY_BREAK(KEY_ENTER): + if (g_model.checklistInteractive && !reusableBuffer.viewText.pushMenu && checklistPosition-(int)menuVerticalOffset >= 0){ + if (checklistPosition < reusableBuffer.viewText.linesCount) { + if (checklistPosition-(int)menuVerticalOffset < LCD_LINES-1) { + ++checklistPosition; + if (checklistPosition-(int)menuVerticalOffset >= LCD_LINES-2 && menuVerticalOffset+LCD_LINES-1 < reusableBuffer.viewText.linesCount) { + ++menuVerticalOffset; + sdReadTextFile(reusableBuffer.viewText.filename, reusableBuffer.viewText.lines, reusableBuffer.viewText.linesCount); + } + } + } + else { + if (reusableBuffer.viewText.pushMenu == true) popMenu(); + reusableBuffer.viewText.checklistComplete = true; + } + } + break; case EVT_KEY_NEXT_LINE: if (menuVerticalOffset+LCD_LINES-1 >= reusableBuffer.viewText.linesCount) @@ -153,12 +177,35 @@ void menuTextView(event_t event) break; case EVT_KEY_BREAK(KEY_EXIT): - popMenu(); + if (!g_model.checklistInteractive || reusableBuffer.viewText.pushMenu) { + if (reusableBuffer.viewText.pushMenu == true) popMenu(); + reusableBuffer.viewText.checklistComplete = true; + } break; } for (int i=0; i