/* * ui_menu_internal.c * * Created on: 24.12.2016 * Author: danilo */ #include "ui_menu.h" #include "ui_menu_internal.h" #include "ui_lcd_hy28.h" // for colors! #include // we show MENUSIZE items at the same time to the user. // right now the render code uses this global variable since // only a single active menu is supported right now. //Because of dynamic resolution change we have to declare the maximum amount of items displayed at once for the highest option MenuDisplaySlot menu[MAX_MENUSIZE]; bool init_done = false; // actions [this is an internal, not necessarily complete or accurate sketch of the used algorithms /API // read the source to find out how it is done. Left for the purpose of explaining the basic idea // show menu -> was previously displayed -> yes -> simply display all slots // -> no -> get first menu group, get first entry, fill first slot, run get next entry until all slots are filled // find next MenuEntry -> is menu group -> yes -> is unfolded -> yes -> get first item from menu group // -> no -> treat as normal menu entry // -> is there one more entry in my menu group ? -> yes -> takes this one // -> no -> go one level up -> get next entry in this group // -> there is no such entry -> fill with dummy entry // find prev MenuEntry -> there is prev entry in menu group -> yes -> is this unfolded menu group -> yes -> get last entry of this menu group // -> no -> fill slot with entry // -> no -> go one level up -> get prev entry of this level // unfold menu group -> mark as unfold, run get next entry until all menu display slots are filled // fold menu group -> mark as fold, run get next entry until all menu display slots are filled // move to next/previous page -> (this is n times prev/next) // ===================== BEGIN MENU LOW LEVEL MANAGEMENT ===================== const MenuGroupDescriptor* UiMenu_GetParentGroupForEntry(const MenuDescriptor* me) { return me==NULL?NULL:&groups[me->menuId]; } const MenuGroupDescriptor* UiMenu_GetGroupForGroupEntry(const MenuDescriptor* me) { return me==NULL?NULL:&groups[me->number]; } //inline bool UiMenu_IsEnabled(const MenuDescriptor *entry) bool UiMenu_IsEnabled(const MenuDescriptor *entry) { return entry==NULL?false:(entry->enabled != NULL?*entry->enabled:true); } inline bool UiMenu_IsGroup(const MenuDescriptor *entry) { return entry==NULL?false:entry->kind == MENU_GROUP; } inline bool UiMenu_IsItem(const MenuDescriptor *entry) { return entry==NULL?false:entry->kind == MENU_ITEM; } inline bool UiMenu_IsInfo(const MenuDescriptor *entry) { return entry==NULL?false:entry->kind == MENU_INFO; } inline bool UiMenu_SlotIsEmpty(MenuDisplaySlot* slot) { return slot==NULL?false:slot->entryItem == NULL; } inline bool UiMenu_GroupIsUnfolded(const MenuDescriptor *group) { return group==NULL?false:groups[group->number].state->unfolded; } inline uint16_t UiMenu_MenuGroupMemberCount(const MenuGroupDescriptor* gd) { uint16_t retval = 0; if (gd != NULL) { if (gd->state->count == 0) { const MenuDescriptor* entry; for (entry = gd->entries; entry->kind != MENU_STOP; entry++) { gd->state->count++; } } retval = gd->state->count; } return retval; } inline void UiMenu_GroupFold(const MenuDescriptor* entry, bool fold) { if (UiMenu_IsGroup(entry)) { groups[entry->number].state->unfolded = fold == false; } } inline const MenuDescriptor* UiMenu_GroupGetLast(const MenuGroupDescriptor *group) { const MenuDescriptor* retval = NULL; uint16_t count = UiMenu_MenuGroupMemberCount(group); if (count>0) { retval = &(group->entries[count-1]); } return retval; } inline const MenuDescriptor* UiMenu_GroupGetFirst(const MenuGroupDescriptor *group) { const MenuDescriptor* retval = NULL; uint16_t count = UiMenu_MenuGroupMemberCount(group); if (count>0) { retval = group->entries; } return retval; } const MenuDescriptor* UiMenu_GetNextEntryInGroup(const MenuDescriptor* me) { const MenuDescriptor* retval = NULL; if (me != NULL) { const MenuGroupDescriptor* group_ptr = UiMenu_GetParentGroupForEntry(me); for (const MenuDescriptor* nxt = me + 1; UiMenu_GroupGetLast(group_ptr)>= nxt; nxt++) { if (UiMenu_IsEnabled(nxt)) { retval = nxt; break; } } } return retval; } const MenuDescriptor* UiMenu_GetPrevEntryInGroup(const MenuDescriptor* me) { const MenuGroupDescriptor* group_ptr = UiMenu_GetParentGroupForEntry(me); const MenuDescriptor* retval = NULL; if (me != NULL && me != &group_ptr->entries[0]) { for (const MenuDescriptor* prv = me - 1; prv >= &group_ptr->entries[0]; prv--) { if (UiMenu_IsEnabled(prv)) { retval = prv; break; } } } return retval; } const MenuDescriptor* UiMenu_GetParentForEntry(const MenuDescriptor* me) { const MenuDescriptor* retval = NULL; if (me != NULL) { const MenuGroupDescriptor* gd = UiMenu_GetParentGroupForEntry(me); if (gd->parent != NULL) { if (gd->state->me == NULL ) { const MenuGroupDescriptor* gdp = &groups[gd->parent->menuId]; uint16_t count = UiMenu_MenuGroupMemberCount(gdp); uint16_t idx; for(idx = 0; idx < count; idx++) { if ((gdp->entries[idx].kind == MENU_GROUP) && (gdp->entries[idx].number == me->menuId)) { gd->state->me = &gdp->entries[idx]; break; } } } retval = gd->state->me; } } return retval; } /* * @returns true if this is the last ACTIVE entry in the menu group */ inline bool UiMenu_IsLastActiveItemInMenuGroup(const MenuDescriptor* here) { const MenuGroupDescriptor* gd = UiMenu_GetParentGroupForEntry(here); const MenuDescriptor* lastActive = UiMenu_GroupGetLast(gd); for (; lastActive != NULL && UiMenu_IsEnabled(lastActive) == false && lastActive != here; lastActive = UiMenu_GetPrevEntryInGroup(lastActive) ) { // we need to do nothing here } return lastActive == here; } inline bool UiMenu_IsFirstInMenuGroup(const MenuDescriptor* here) { const MenuGroupDescriptor* gd = UiMenu_GetParentGroupForEntry(here); return UiMenu_GroupGetFirst(gd) == here; } // ===================== END MENU LOW LEVEL MANAGEMENT ===================== // ===================== BEGIN MENU ITERATION STRATEGY ===================== // this code implements a specific strategy to walk through a menu structure // Helper Functions const MenuDescriptor* UiMenu_FindNextEntryInUpperLevel(const MenuDescriptor* here) { const MenuDescriptor* next = NULL, *focus = here; if (here != NULL) { while(focus != NULL && next == NULL) { // we have a parent group, we are member of a sub menu group, // we need next entry in containing menu group, no matter if our menu group is folded or not next = UiMenu_GetNextEntryInGroup(UiMenu_GetParentForEntry(focus)); if (next == NULL) { focus = UiMenu_GetParentForEntry(focus); } } } return next; } const MenuDescriptor* UiMenu_FindLastEntryInLowerLevel(const MenuDescriptor* here) { const MenuDescriptor *last = here; while (UiMenu_IsGroup(here) && UiMenu_GroupIsUnfolded(here) && here == last) { const MenuDescriptor* last = UiMenu_GroupGetLast(UiMenu_GetGroupForGroupEntry(here)); if (last) { here = last; } } return here; } // Main Strategy Functions /* * Strategy: Provide a 'virtual' flat list of menu entries, list members are dynamically inserted/removed if menu groups are (un)folded. * External code navigates through only with next/prev operations. * */ /* * @brief Get next menu entry. If a menu group is unfolded, next entry after menu group item is first item from menu group * */ const MenuDescriptor* UiMenu_NextMenuEntry(const MenuDescriptor* here) { const MenuDescriptor* next = NULL; if (here != NULL) { if (UiMenu_IsGroup(here)) { // is group entry if (UiMenu_GroupIsUnfolded(here)) { const MenuGroupDescriptor* group = &groups[here->number]; next = UiMenu_GroupGetFirst(group); if (next == NULL) { // this is an empty menu group, should not happen, does make sense // but we handle this anyway next = UiMenu_FindNextEntryInUpperLevel(here); } } else { // folded group, so we behave like a normal entry next = UiMenu_GetNextEntryInGroup(here); } } if (next == NULL) { // we are currently at a normal entry or a folded group or empty group (in this case these are treated as simple entries) // only 3 cases possible: // - final entry of menu, fill next slot with blank entry, return false // - next entry is normal entry (group or entry, no difference), just use this one // - last entry in menu group, go up, and search for next entry in this parent menu (recursively). if (UiMenu_IsLastActiveItemInMenuGroup(here)) { // we need the parent menu in order to ask for the entry after our // menu group entry // if we cannot find the parent group, we are top level and the last menu entry // so there is no further entry next = UiMenu_FindNextEntryInUpperLevel(here); } else { next = UiMenu_GetNextEntryInGroup(here); } } } return next; } /* * @brief Get previous menu entry. If on first item of a menu group, show the last entry of the previous menu group/menu item * */ const MenuDescriptor* UiMenu_PrevMenuEntry(const MenuDescriptor* here) { const MenuDescriptor* prev = NULL; if (here != NULL) { if (UiMenu_IsFirstInMenuGroup(here)) { // we go up, get previous entry // if first entry, go one further level up, ... // if not first entry -> get prev entry // if normal entry or folded menu -> we are done // if unfolded menu_entry -> go to last entry // -> if normal entry or folded menu -> we are done // -> if unfolded menu entry -> go to last entry prev = UiMenu_GetParentForEntry(here); } else { prev = UiMenu_GetPrevEntryInGroup(here); if (UiMenu_IsGroup(prev) && UiMenu_GroupIsUnfolded(prev)) { prev = UiMenu_FindLastEntryInLowerLevel(prev); } } } return prev; } bool UiMenu_FillSlotWithEntry(MenuDisplaySlot* here, const MenuDescriptor* entry) { bool retval = false; if (entry != NULL) { here->entryItem = entry; retval = true; } else { here->entryItem = NULL; } return retval; } // DISPLAY SPECIFIC CODE BEGIN void UiMenu_DisplayValue(const char* value,uint32_t clr,uint16_t pos) { UiLcdHy28_PrintTextRight(ts.Layout->MENU_CURSOR_X - 4, ts.Layout->MENU_IND.y + (pos * 12), value, clr, Black, 0); // yes, normal position } static void UiMenu_DisplayLabel(const char* label,uint32_t clr,uint16_t pos) { UiLcdHy28_PrintText(ts.Layout->MENU_IND.x, ts.Layout->MENU_IND.y + (12*(pos)),label,clr,Black,0); } static void UiMenu_DisplayCursor(const char* label,uint32_t clr,uint16_t pos) { UiLcdHy28_PrintText(ts.Layout->MENU_CURSOR_X, ts.Layout->MENU_IND.y + (12*(pos)),label,clr,Black,0); } // DISPLAY SPECIFIC CODE END void UiMenu_MoveCursor(uint32_t newpos) { static uint32_t oldpos = 999; // y position of option cursor, previous if(oldpos != 999) // was the position of a previous cursor stored? { UiMenu_DisplayCursor(" ", Green, oldpos); } oldpos = newpos; // save position of new "old" cursor position if (newpos != 999) { UiMenu_DisplayCursor("<", Green, newpos); } } static void UiMenu_UpdateHWInfoLines(uchar index, MenuProcessingMode_t mode, int pos) { uint32_t m_clr; const char* outs = UiMenu_GetSystemInfo(&m_clr, index); UiMenu_DisplayValue(outs,m_clr,pos); } /** * @brief Display and and change line items * @param select item to display/change * @param mode 0=display/update 1=change item 3=set default * @param pos (0-5) use this line as position */ static void UiMenu_UpdateLines(uint16_t select, MenuProcessingMode_t mode, int pos) { char options[32]; const char* txt_ptr = NULL; // if filled, we use this string for display, otherwise options uint32_t clr = White; // color used it display of adjusted options int var; // holder for the menu item value change if(mode == MENU_RENDER_ONLY) // are we in update/display mode? { var = 0; // prevent any change of variable } else // this is "change" mode { var = ts.menu_var; // change from encoder ts.menu_var = 0; // clear encoder change detect } strcpy(options, "ERROR"); // pre-load to catch error condition UiMenu_UpdateItem(select, mode, pos, var, options,&txt_ptr , &clr); if (txt_ptr == NULL) { txt_ptr = options; } UiMenu_DisplayValue(txt_ptr,clr,pos); if(mode == MENU_PROCESS_VALUE_CHANGE) // Shifted over { UiMenu_MoveCursor(pos); } return; } /* * Render a menu entry on a given menu position */ void UiMenu_UpdateMenuEntry(const MenuDescriptor* entry, MenuProcessingMode_t mode, uint8_t pos) { uint32_t m_clr; m_clr = Yellow; char out[40]; char blank[40] = " "; blank[ts.Layout->MENU_TEXT_SIZE_MAX-1]=0; //#else // const char blank[34] = " "; //#endif if (entry != NULL && (entry->kind == MENU_ITEM || entry->kind == MENU_GROUP || entry->kind == MENU_INFO || entry->kind == MENU_TEXT) ) { if (mode == MENU_RENDER_ONLY) { uint16_t level = 0; const MenuDescriptor* parent = entry; do { parent = UiMenu_GetParentForEntry(parent); level++; } while (parent != NULL); level--; // level = 3; // uint16_t labellen = strlen(entry->id)+strlen(entry->label) + 1; uint16_t labellen = level+strlen(entry->label); // snprintf(out,34,"%s-%s%s",entry->id,entry->label,(&blank[labellen>33?33:labellen])); //#ifdef USE_DISP_480_320 //snprintf(out,ts.40,"%s%s%s",(&blank[level>5?37-5:37-level]),entry->label,(&blank[labellen>39?39:labellen])); snprintf(out,ts.Layout->MENU_TEXT_SIZE_MAX,"%s%s%s", (&blank[level>5?ts.Layout->MENU_TEXT_SIZE_MAX-8:ts.Layout->MENU_TEXT_SIZE_MAX-3-level]), entry->label,(&blank[labellen>ts.Layout->MENU_TEXT_SIZE_MAX-1?ts.Layout->MENU_TEXT_SIZE_MAX-1:labellen])); // snprintf(out,34,"%s%s%s",(&blank[level>5?31-5:31-level]),entry->label,(&blank[labellen>33?33:labellen])); UiMenu_DisplayLabel(out,m_clr,pos); } switch(entry->kind) { case MENU_ITEM: // TODO: Better Handler Selection with need for change in this location to add new handlers UiMenu_UpdateLines(entry->number,mode,pos); break; case MENU_INFO: UiMenu_UpdateHWInfoLines(entry->number,mode,pos); break; case MENU_TEXT: break; case MENU_GROUP: if (mode == MENU_PROCESS_VALUE_CHANGE) { bool old_state = UiMenu_GroupIsUnfolded(entry); if (ts.menu_var < 0 ) { UiMenu_GroupFold(entry, !(ts.flags2 & FLAGS2_UI_INVERSE_SCROLLING)); } if (ts.menu_var > 0 ) { UiMenu_GroupFold(entry, ts.flags2 & FLAGS2_UI_INVERSE_SCROLLING); } if (old_state != UiMenu_GroupIsUnfolded(entry)) { int idx; for (idx = pos+1; idx < ts.Layout->MENUSIZE; idx++) { UiMenu_FillSlotWithEntry(&menu[idx],UiMenu_NextMenuEntry(menu[idx-1].entryItem)); UiMenu_UpdateMenuEntry(menu[idx].entryItem, MENU_RENDER_ONLY, idx); } } ts.menu_var = 0; } UiMenu_DisplayValue( UiMenu_GroupIsUnfolded(entry)?"HIDE":"SHOW", m_clr,pos); break; } } else { UiMenu_DisplayLabel(blank,m_clr,pos); } if (mode == MENU_PROCESS_VALUE_CHANGE) { UiMenu_MoveCursor(pos); } } void UiMenu_DisplayInitSlots(const MenuDescriptor* entry) { int idx; for (idx=0; idx < ts.Layout->MENUSIZE; idx++) { UiMenu_FillSlotWithEntry(&menu[idx],entry); entry = UiMenu_NextMenuEntry(entry); } } void UiMenu_DisplayInitSlotsBackwards(const MenuDescriptor* entry) { int idx; for (idx=ts.Layout->MENUSIZE; idx > 0; idx--) { UiMenu_FillSlotWithEntry(&menu[idx-1],entry); entry = UiMenu_PrevMenuEntry(entry); } } /* * @returns true if at least one slot was moved, false if no change done */ bool UiMenu_DisplayMoveSlotsBackwards(int16_t change) { int idx; int dist = (change % ts.Layout->MENUSIZE); int screens = change / ts.Layout->MENUSIZE; bool retval = false; // n for (idx = 0; idx < screens; idx++) { const MenuDescriptor *prev = UiMenu_PrevMenuEntry(menu[0].entryItem); if (prev != NULL) { retval = true; UiMenu_DisplayInitSlotsBackwards(prev); } else { // we stop here, since no more previous elements. // TODO: Decide if roll over, in this case we would have to get very last element and // then continue from there. dist = 0; break; } } if (dist != 0) { retval = true; for (idx = ts.Layout->MENUSIZE-dist; idx > 0; idx--) { UiMenu_FillSlotWithEntry(&menu[ts.Layout->MENUSIZE-idx],menu[ts.Layout->MENUSIZE-(dist+idx)].entryItem); } for (idx = ts.Layout->MENUSIZE-dist; idx >0; idx--) { UiMenu_FillSlotWithEntry(&menu[idx-1],UiMenu_PrevMenuEntry(menu[idx].entryItem)); } } return retval; } /* * @returns true if at least one slot was moved, false if no change done */ bool UiMenu_DisplayMoveSlotsForward(int16_t change) { int idx; int dist = (change % ts.Layout->MENUSIZE); int screens = change / ts.Layout->MENUSIZE; bool retval = false; // first jump screens. we have to iterate through the menu structure one by one // in order to respect fold/unfold state etc. for (idx = 0; idx < screens; idx++) { const MenuDescriptor *next = UiMenu_NextMenuEntry(menu[ts.Layout->MENUSIZE-1].entryItem); if (next != NULL) { UiMenu_DisplayInitSlots(next); retval = true; } else { // stop here // TODO: Rollover? dist = 0; break; } } if (dist != 0) { retval = true; for (idx = 0; idx < ts.Layout->MENUSIZE-dist; idx++) { UiMenu_FillSlotWithEntry(&menu[idx],menu[dist+idx].entryItem); } for (idx = ts.Layout->MENUSIZE-dist; idx < ts.Layout->MENUSIZE; idx++) { UiMenu_FillSlotWithEntry(&menu[idx],UiMenu_NextMenuEntry(menu[idx-1].entryItem)); } } return retval; } /* * @brief Display and change menu items * @param mode 0=show all, 1=update current item, 3=restore default setting for selected item * */ void UiMenu_RenderMenu(MenuProcessingMode_t mode) { if (init_done == false ) { UiMenu_DisplayInitSlots(groups[MENU_START_IDX].entries); init_done = true; } // UiMenu_DisplayMoveSlotsForward(6); // UiMenu_DisplayMoveSlotsForward(3); // UiMenu_DisplayMoveSlotsBackwards(10); switch (mode) { case MENU_RENDER_ONLY: // (re)draw all labels and values { int idx; for (idx = 0; idx < ts.Layout->MENUSIZE; idx++) { UiMenu_UpdateMenuEntry(menu[idx].entryItem,mode, idx); } UiMenu_MoveCursor(ts.menu_item%ts.Layout->MENUSIZE); //redraw of cursor } break; case MENU_PROCESS_VALUE_SETDEFAULT: case MENU_PROCESS_VALUE_CHANGE: { // wrapping to next screen (and from end to start and vice versa) if (ts.menu_item >= ts.Layout->MENUSIZE) { if (UiMenu_RenderNextScreen() == false) { UiMenu_RenderFirstScreen(); } } else if (ts.menu_item < 0) { if (UiMenu_RenderPrevScreen() == false) { UiMenu_RenderLastScreen(); } } ts.menu_item%=ts.Layout->MENUSIZE; if (ts.menu_item < 0) ts.menu_item+=ts.Layout->MENUSIZE; uint16_t current_item = ts.menu_item%ts.Layout->MENUSIZE; UiMenu_UpdateMenuEntry(menu[current_item].entryItem,mode, current_item); } break; default: break; } } void UiMenu_RenderChangeItemValue(int16_t pot_diff) { if(pot_diff < 0) { ts.menu_var--; // increment selected item } else { ts.menu_var++; // decrement selected item } UiMenu_RenderMenu(MENU_PROCESS_VALUE_CHANGE); // perform update of selected item } void UiMenu_RenderChangeItem(int16_t pot_diff) { if(ts.flags2 & FLAGS2_UI_INVERSE_SCROLLING) { pot_diff = -pot_diff; } if(pot_diff < 0) { ts.menu_item--; } else if(pot_diff > 0) { ts.menu_item++; } ts.menu_var = 0; // clear variable that is used to change a menu item UiMenu_RenderMenu(MENU_PROCESS_VALUE_CHANGE); // Update that menu item } void UiMenu_RenderLastScreen() { while (menu[ts.Layout->MENUSIZE-1].entryItem != NULL && UiMenu_NextMenuEntry(menu[ts.Layout->MENUSIZE-1].entryItem) != NULL ) { UiMenu_DisplayMoveSlotsForward(ts.Layout->MENUSIZE); } UiMenu_RenderMenu(MENU_RENDER_ONLY); } void UiMenu_RenderFirstScreen() { init_done = false; UiMenu_RenderMenu(MENU_RENDER_ONLY); } bool UiMenu_RenderNextScreen() { bool retval = UiMenu_DisplayMoveSlotsForward(ts.Layout->MENUSIZE); if (retval) { UiMenu_RenderMenu(MENU_RENDER_ONLY); } return retval; } bool UiMenu_RenderPrevScreen() { bool retval = UiMenu_DisplayMoveSlotsBackwards(ts.Layout->MENUSIZE); if (retval) { UiMenu_RenderMenu(MENU_RENDER_ONLY); } return retval; }