UHSDR/UHSDR-active-devel/mchf-eclipse/drivers/ui/menu/ui_menu_internal.c
2022-11-08 16:13:55 +01:00

778 lines
24 KiB
C

/*
* 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 <stdio.h>
// 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;
}