FreeTRX/MDUV380_firmware/application/source/user_interface/menuSystem.c
Marcus Kida 15bbc1bc3a SMS: pick recipient from contacts; contacts list shows all types
- New SMS recipient picker: GREEN in the compose recipient field opens a list
  of private-call DMR contacts; selecting one fills the destination (using the
  contact's plain DMR ID) and moves to the message text. Manual ID entry still
  works (DOWN advances to the text field).
- Menu -> Contacts now defaults to a combined 'Contacts' view of all contact
  types instead of the group-only filter; HASH still cycles the per-type
  filters. Per-type code paths are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:08:25 +02:00

1037 lines
31 KiB
C

/*
* Copyright (C) 2019-2025 Roger Clark, VK3KYY / G4KYF
* Daniel Caujolle-Bert, F1RMB
*
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
* incorporation in a commercial product or incorporation into a product or project which allows commercial use.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
#include "user_interface/uiGlobals.h"
#include "user_interface/menuSystem.h"
#include "user_interface/uiLocalisation.h"
#include "user_interface/uiUtilities.h"
menuDataGlobal_t menuDataGlobal =
{
.currentItemIndex = 0, // each menu can re-use this var to hold the position in their display list. To save wasted memory if they each had their own variable
.startIndex = 0, // as above
.numItems = 0, // as above
.lightTimer = -1,
.currentMenuList = NULL,
.controlData =
{
.stackPosition = 0,
.stack =
{
MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY
}
},
/*
* ---------------------- IMPORTANT ----------------------------
*
* The menuFunctions array and the menusData array.....
*
* MUST match the enum MENU_SCREENS in menuSystem.h
*
* ---------------------- IMPORTANT ----------------------------
*/
.data =
{
&menuDataMainMenu,
&menuDataContact,
NULL,// zone
NULL,// RadioInfos
NULL,// RSSI
NULL,// LastHeard
&menuDataOptions,// Options
NULL,// General options
NULL,// Radio options
NULL,// Display options
NULL,// Sound options
NULL,// SatelliteScreen
#if defined(HAS_GPS)
NULL,// GPS
#endif
NULL,// Contact List
NULL,// DTMF Contact List
NULL,// Contact Quick List (SK2+#)
NULL,// Contact List Quick Menu
NULL,// Contact Details
NULL,// Language
NULL,// Calibration
#if defined(HAS_COLOURS)
NULL,// Theme options
#endif
NULL,// Quick menu - Channel
NULL,// Quick menu - VFO
NULL,// Channel Details
NULL,// Firmwareinfo
#if !defined(PLATFORM_GD77S)
NULL,// APRS options
#endif
&menuDataSMS,// SMS hub
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
NULL,// SMS compose
NULL,// SMS inbox
NULL,// SMS sent
NULL,// SMS view
NULL,// SMS quick texts
NULL,// SMS options
NULL,// SMS contact picker
NULL,// SMS RX popup
NULL,// MessageBox
NULL,// hotspot mode
NULL,// CPS
NULL,// Numerical entry
NULL,// Tx
NULL,// splash
NULL,// power off
NULL,// vfo mode
NULL,// channel mode
NULL,// Lock screen
NULL,// Private Call
NULL,// New Contact
#if defined(HAS_COLOURS)
NULL,// Theme items browser
NULL,// Colour picker
#endif
}
};
static menuFunctionData_t menuFunctions[] =
{
{ menuDisplayMenuList, NULL, NULL, 0 },// display Main menu using the menu display system
{ menuDisplayMenuList, NULL, NULL, 0 },// display Contact menu using the menu display system
{ menuZoneList, NULL, NULL, 0 },
{ menuRadioInfos, NULL, NULL, 0 },
{ menuRSSIScreen, NULL, NULL, 0 },
{ menuLastHeard, NULL, NULL, 0 },
{ menuDisplayMenuList, NULL, NULL, 0 },
{ menuGeneralOptions, NULL, NULL, 0 },
{ menuRadioOptions, NULL, NULL, 0 },
{ menuDisplayOptions, NULL, NULL, 0 },
{ menuSoundOptions, NULL, NULL, 0 },
#if defined(USING_EXTERNAL_DEBUGGER)
{ menuSoundOptions, NULL, NULL, 0 },// hack to remove satellite screen from the build when external debugger is selected, otherwise there is not enough space in the ROM to build the firmware
#else
{ menuSatelliteScreen, NULL, NULL, 0 },
#endif
#if defined(HAS_GPS)
{ menuGPS, NULL, NULL, 0 },
#endif
{ menuContactList, NULL, NULL, 0 },
{ menuContactList, NULL, NULL, 0 },
{ menuContactList, NULL, NULL, 0 },
{ menuContactListSubMenu, NULL, NULL, 0 },
{ menuContactDetails, NULL, NULL, 0 },
{ menuLanguage, NULL, NULL, 0 },
{ menuCalibration, NULL, NULL, 0 },
#if defined(HAS_COLOURS)
{ menuThemeOptions, NULL, NULL, 0 },
#endif
{ uiChannelModeQuickMenu, NULL, NULL, 0 },
{ uiVFOModeQuickMenu, NULL, NULL, 0 },
{ menuChannelDetails, NULL, NULL, 0 },
{ menuFirmwareInfoScreen, NULL, NULL, 0 },
#if !defined(PLATFORM_GD77S)
{ menuAPRSOptions, NULL, NULL, 0 },
#endif
{ menuDisplayMenuList, NULL, NULL, 0 },// SMS hub
// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
{ menuSMSCompose, NULL, NULL, 0 },
{ menuSMSList, NULL, NULL, 0 },// SMS inbox
{ menuSMSList, NULL, NULL, 0 },// SMS sent
{ menuSMSList, NULL, NULL, 0 },// SMS view
{ menuSMSQuickText, NULL, NULL, 0 },
{ menuSMSOptions, NULL, NULL, 0 },
{ menuSMSContact, NULL, NULL, 0 },
{ uiSMSPopup, NULL, NULL, 0 },
{ uiMessageBox, NULL, NULL, 0 },
{ menuHotspotMode, NULL, NULL, 0 },
{ uiCPS, NULL, NULL, 0 },
{ menuNumericalEntry, NULL, NULL, 0 },
{ menuTxScreen, NULL, NULL, 0 },
{ uiSplashScreen, NULL, NULL, 0 },
{ uiPowerOff, NULL, NULL, 0 },
{ uiVFOMode, NULL, NULL, 0 },
{ uiChannelMode, NULL, NULL, 0 },
{ menuLockScreen, NULL, NULL, 0 },
{ menuPrivateCall, NULL, NULL, 0 },
{ menuContactDetails, NULL, NULL, 0 }, // Contact New
#if defined(HAS_COLOURS)
{ menuThemeItemsBrowser, NULL, NULL, 0 },
{ menuColourPicker, NULL, NULL, 0 },
#endif
};
static void menuSystemCheckForFirstEntryAudible(menuStatus_t status)
{
if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_BEEP)
{
// If VP is currently playing, we should not set the next beep, otherwise it
// will be played at the wrong time (e.g entering TG/PC input window, ACK beep will be
// played on the next beep playback event
if ((nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && voicePromptsIsPlaying())
{
return;
}
if (status & MENU_STATUS_ERROR)
{
nextKeyBeepMelody = (int16_t *)MELODY_ERROR_BEEP;
}
else if (((status & MENU_STATUS_LIST_TYPE) && (menuDataGlobal.currentItemIndex == 0)) || (status & MENU_STATUS_FORCE_FIRST))
{
nextKeyBeepMelody = (int16_t *)MELODY_KEY_BEEP_FIRST_ITEM;
}
else if (status & MENU_STATUS_INPUT_TYPE)
{
nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP;
}
}
}
static void menuSystemPushMenuFirstRun(void)
{
uiEvent_t ev = { .buttons = 0, .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = ticksGetMillis() };
menuStatus_t status;
if (uiNotificationIsVisible() && uiNotificationHasTimedOut())
{
uiNotificationHide(false);
}
// Due to QuickKeys, menu list won't go through menuDisplayMenuList() first, so those
// two members won't get always initialized. Hence, we need to tag them as uninitialized,
// and check if they got initialized after entering a menu.
menuDataGlobal.numItems = INT32_MIN;
menuDataGlobal.currentMenuList = NULL;
menuDataGlobal.currentItemIndex = menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex;
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback = NULL;
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data = NULL;
displayLightTrigger(false);
// This only happens when on Cal Data or Flash init errors.
if (menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] == MENU_EMPTY)
{
status = MENU_STATUS_SUCCESS;
displayClearBuf();
displayPrintCentered(((DISPLAY_SIZE_Y - FONT_SIZE_3_HEIGHT) >> 1), globalFailureMessage, FONT_SIZE_3);
displayRender();
}
else
{
status = menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].function(&ev, true);
}
if (menuDataGlobal.numItems == INT32_MIN)
{
menuDataGlobal.numItems = ((menuDataGlobal.data[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]] != NULL) ? menuDataGlobal.data[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]]->numItems : 0);
}
if (menuDataGlobal.currentMenuList == NULL)
{
menuDataGlobal.currentMenuList = ((menuDataGlobal.data[menuDataGlobal.controlData.stackPosition] != NULL) ? (menuItemNewData_t *)menuDataGlobal.data[menuDataGlobal.controlData.stackPosition]->items : NULL);
}
if (menuDataGlobal.currentItemIndex > menuDataGlobal.numItems)
{
menuDataGlobal.currentItemIndex = 0;
}
menuSystemCheckForFirstEntryAudible(status);
}
int menuSystemGetLastItemIndex(int stackPos)
{
if ((stackPos >= 0) && (stackPos <= menuDataGlobal.controlData.stackPosition))
{
return menuFunctions[menuDataGlobal.controlData.stack[stackPos]].lastItemIndex;
}
return -1;
}
void menuSystemRegisterExitCallback(menuExitCallback_t f, void *data)
{
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback = f;
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data = data;
}
void menuSystemPushNewMenu(int menuNumber)
{
if (menuDataGlobal.controlData.stackPosition < 15)
{
keyboardReset();
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;
menuDataGlobal.controlData.stackPosition++;
menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] = menuNumber;
#if defined(PLATFORM_MD9600)
if ((menuNumber != UI_VFO_MODE) && (menuNumber != UI_CHANNEL_MODE))
{
uiDataGlobal.sk2latched = false;
}
#endif
menuSystemPushMenuFirstRun();
}
}
void menuSystemCallExitCallback(void)
{
if (menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback != NULL)
{
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback(menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data);
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback = NULL;
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data = NULL;
}
}
void menuSystemPopPreviousMenu(void)
{
keyboardReset();
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;
menuSystemCallExitCallback();
// Clear stackPosition + 1 menu trace
if (menuDataGlobal.controlData.stackPosition < 15)
{
menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition + 1] = MENU_EMPTY;
}
// Avoid crashing if something goes wrong.
if (menuDataGlobal.controlData.stackPosition > 0)
{
menuDataGlobal.controlData.stackPosition -= 1;
}
menuSystemPushMenuFirstRun();
}
void menuSystemPopAllAndDisplayRootMenu(void)
{
keyboardReset();
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;
while (menuDataGlobal.controlData.stackPosition >= 1)
{
menuSystemCallExitCallback();
menuDataGlobal.controlData.stackPosition--;
}
// MENU_EMPTY is equal to -1 (0xFFFFFFFF), hence the following works, even if it's an int32_t array
memset(&menuDataGlobal.controlData.stack[1], MENU_EMPTY, sizeof(menuDataGlobal.controlData.stack) - sizeof(int));
menuDataGlobal.controlData.stackPosition = 0;
menuSystemPushMenuFirstRun();
}
void menuSystemPopAllAndDisplaySpecificRootMenu(int newRootMenu, bool resetKeyboard)
{
if (resetKeyboard)
{
keyboardReset();
}
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;
while (menuDataGlobal.controlData.stackPosition >= 1)
{
menuSystemCallExitCallback();
menuDataGlobal.controlData.stackPosition--;
}
// MENU_EMPTY is equal to -1 (0xFFFFFFFF), hence the following works, even if it's an int32_t array
memset(&menuDataGlobal.controlData.stack[1], MENU_EMPTY, sizeof(menuDataGlobal.controlData.stack) - sizeof(int));
menuDataGlobal.controlData.stack[0] = newRootMenu;
menuDataGlobal.controlData.stackPosition = 0;
menuSystemPushMenuFirstRun();
}
void menuSystemSetCurrentMenu(int menuNumber)
{
keyboardReset();
menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] = menuNumber;
menuSystemPushMenuFirstRun();
}
int menuSystemGetCurrentMenuNumber(void)
{
return menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition];
}
int menuSystemGetPreviousMenuNumber(void)
{
if (menuDataGlobal.controlData.stackPosition >= 1)
{
return menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition - 1];
}
return MENU_ANY;
}
int menuSystemGetPreviouslyPushedMenuNumber(bool keepIt)
{
if (menuDataGlobal.controlData.stackPosition < 15)
{
int m = menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition + 1];
if (keepIt == false)
{
menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition + 1] = MENU_EMPTY;
}
return m;
}
return MENU_EMPTY;
}
int menuSystemGetRootMenuNumber(void)
{
return menuDataGlobal.controlData.stack[0];
}
static void menuSystemPreProcessEvent(uiEvent_t *ev)
{
#if defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
if (ev->hasEvent || ((uiDataGlobal.displayQSOState == QSO_DISPLAY_CALLER_DATA) && (nonVolatileSettings.backlightMode != BACKLIGHT_MODE_BUTTONS)))
{
if ((ev->events & SYNTHETIC_EVENT) == 0)
{
displayLightTrigger(true);
}
}
#else // ! MK22
if (ev->hasEvent)
{
int currentMenu = menuSystemGetCurrentMenuNumber();
#if defined(PLATFORM_MD9600)
bool isEnteringDTMF = ((trxIsTransmitting || (currentMenu == MENU_NUMERICAL_ENTRY)) && (trxGetMode() == RADIO_MODE_ANALOG));
#endif
if ((currentMenu != UI_CHANNEL_MODE) && (currentMenu != UI_VFO_MODE))
{
switch(ev->keys.key)
{
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_MD9600)
case KEY_ROTARY_INCREMENT:
ev->keys.key = KEY_RIGHT;
break;
case KEY_ROTARY_DECREMENT:
ev->keys.key = KEY_LEFT;
break;
#endif
#if defined(PLATFORM_MD9600)
case KEY_B:
if (isEnteringDTMF == false)
{
ev->keys.key = KEY_RIGHT;
}
break;
case KEY_C:
if (isEnteringDTMF == false)
{
ev->keys.key = KEY_LEFT;
}
break;
case KEY_A:
if ((trxIsTransmitting == false) &&
((isEnteringDTMF == false) ||
(isEnteringDTMF && ((ev->keys.event & KEY_MOD_LONG) == KEY_MOD_LONG))))
{
ev->keys.key = KEY_RED;
if (isEnteringDTMF)
{
ev->keys.event = KEY_MOD_UP;
}
}
break;
#endif
case KEY_FRONT_UP:
ev->keys.key = KEY_UP;
break;
case KEY_FRONT_DOWN:
ev->keys.key = KEY_DOWN;
break;
}
}
else
{
#if defined(PLATFORM_MD9600) || !defined(PLATFORM_RT84_DM1701) // This block is for MD9600, MDUV380 or MD380
switch(ev->keys.key)
{
#if ! (defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) || defined(PLATFORM_MD9600))
case KEY_UP:
ev->keys.key = KEY_LEFT;
break;
case KEY_DOWN:
ev->keys.key = KEY_RIGHT;
break;
#elif defined(PLATFORM_MD2017)
// The two following events are possibly issued from the Trackball, check that and convert if needed
case KEY_UP:
// In Channel and VFO, KEY_UP has to be remapped to KEY_FONT_UP
if (ev->keys.event == (KEY_MOD_UP | KEY_MOD_PRESS))
{
ev->keys.key = KEY_FRONT_UP;
}
break;
case KEY_DOWN:
// In Channel and VFO, KEY_DOWN has to be remapped to KEY_FONT_DOWN
if (ev->keys.event == (KEY_MOD_UP | KEY_MOD_PRESS))
{
ev->keys.key = KEY_FRONT_DOWN;
}
break;
#endif
#if defined(PLATFORM_MD9600)
case KEY_B:
ev->keys.key = KEY_RIGHT;
break;
case KEY_C:
ev->keys.key = KEY_LEFT;
break;
case KEY_A:
ev->keys.key = KEY_RED;
break;
case KEY_ROTARY_INCREMENT:
ev->keys.key = KEY_UP;
break;
case KEY_ROTARY_DECREMENT:
ev->keys.key = KEY_DOWN;
break;
case KEY_FRONT_UP:
if ((ev->keys.event & (KEY_MOD_DOWN | KEY_MOD_LONG)) == (KEY_MOD_DOWN | KEY_MOD_LONG))
{
ev->keys.key = KEY_UP;
}
else
{
ev->keys.key = KEY_RIGHT;
}
break;
case KEY_FRONT_DOWN:
if ((ev->keys.event & (KEY_MOD_DOWN | KEY_MOD_LONG)) == (KEY_MOD_DOWN | KEY_MOD_LONG))
{
ev->keys.key = KEY_DOWN;
}
else
{
ev->keys.key = KEY_LEFT;
}
break;
#endif
#if defined(PLATFORM_MD9600) || !defined(PLATFORM_RT84_DM1701)
}
#endif
#endif
}
if ((ev->events & SYNTHETIC_EVENT) == 0)
{
displayLightTrigger(true);
}
}
else
{
if (((uiDataGlobal.displayQSOState == QSO_DISPLAY_CALLER_DATA) && (nonVolatileSettings.backlightMode != BACKLIGHT_MODE_BUTTONS)) )
{
displayLightTrigger(true);
}
}
#endif // ! MK22
}
static void menuSystemPostProcessEvent(uiEvent_t *ev)
{
if (uiDataGlobal.displayQSOState == QSO_DISPLAY_CALLER_DATA)
{
uiDataGlobal.displayQSOState = QSO_DISPLAY_IDLE;
}
}
void menuSystemCallCurrentMenuTick(uiEvent_t *ev)
{
menuStatus_t status;
menuSystemPreProcessEvent(ev);
if (menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] == MENU_EMPTY)
{
status = MENU_STATUS_SUCCESS;
displayClearBuf();
displayPrintCentered(((DISPLAY_SIZE_Y - FONT_SIZE_3_HEIGHT) >> 1), globalFailureMessage, FONT_SIZE_3);
displayRender();
}
else
{
status = menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].function(ev, false);
}
menuSystemPostProcessEvent(ev);
if (ev->hasEvent)
{
menuSystemCheckForFirstEntryAudible(status);
}
}
void displayLightTrigger(bool fromKeyEvent)
{
// BACKLIGHT_MODE_MANUAL is handled in main.c
if ((menuSystemGetCurrentMenuNumber() != UI_TX_SCREEN) &&
(((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO) || (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH))
|| ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_BUTTONS) && fromKeyEvent)))
{
menuDataGlobal.lightTimer = nonVolatileSettings.backLightTimeout * 1000;
displayEnableBacklight(true, nonVolatileSettings.displayBacklightPercentageOff);
}
}
// use -1 to force LED on all the time
void displayLightOverrideTimeout(int timeout)
{
int prevTimer = menuDataGlobal.lightTimer;
menuDataGlobal.lightTimer = timeout;
if ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO) || (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH))
{
// Backlight is OFF, or timeout override (-1) as just been set
if ((displayIsBacklightLit() == false) || ((timeout == -1) && (prevTimer != -1)))
{
displayEnableBacklight(true,nonVolatileSettings.displayBacklightPercentageOff);
}
}
}
void menuSystemInit(time_t_custom UTCdateTimeInSecs)
{
uiEvent_t ev = { .buttons = 0, .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = ticksGetMillis() };
uiSetUTCDateTimeInSecs(UTCdateTimeInSecs);
menuDataGlobal.lightTimer = -1;
menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] = UI_SPLASH_SCREEN;// set the very first screen as the splash screen
menuDataGlobal.currentItemIndex = 0;
if ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_MANUAL) ||
(nonVolatileSettings.backlightMode == BACKLIGHT_MODE_BUTTONS) ||
(nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH))
{
if (nonVolatileSettings.displayBacklightPercentageOff > 0)
{
displayEnableBacklight(false, nonVolatileSettings.displayBacklightPercentageOff);
}
}
else
{
displayLightTrigger(false);
}
menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].function(&ev, true);// Init and display this screen
}
void menuSystemLanguageHasChanged(void)
{
// Force full update of menuChannelMode() on next call (if isFirstRun arg. is true)
if (menuSystemGetRootMenuNumber() == UI_CHANNEL_MODE)
{
uiChannelModeColdStart();
}
}
const menuItemNewData_t mainMenuItems[] =
{
{ 3, MENU_ZONE_LIST },
{ 6, MENU_CONTACTS_MENU },
{ 282, MENU_SMS },// messages
{ 12, MENU_CHANNEL_DETAILS },
{ 4, MENU_RSSI_SCREEN },
{ 8, MENU_FIRMWARE_INFO },
{ 9, MENU_OPTIONS },
{ 7, MENU_LAST_HEARD },
{ 150, MENU_RADIO_INFOS },
{ 173, MENU_SATELLITE },
#if defined(HAS_GPS)
{ 195, MENU_GPS },
#endif
};
const menuItemsList_t menuDataMainMenu =
{
.numItems = (sizeof(mainMenuItems) / sizeof(mainMenuItems[0])),
.items = mainMenuItems
};
static const menuItemNewData_t smsMenuItems[] =
{
{ 283, MENU_SMS_COMPOSE },// new message
{ 284, MENU_SMS_INBOX },// inbox
{ 285, MENU_SMS_SENT },// sent
{ 286, MENU_SMS_QUICKTEXT },// quick text
{ 9, MENU_SMS_OPTIONS },// options
};
const menuItemsList_t menuDataSMS =
{
.numItems = (sizeof(smsMenuItems) / sizeof(smsMenuItems[0])),
.items = smsMenuItems
};
static const menuItemNewData_t contactMenuItems[] =
{
{ 15, MENU_CONTACT_LIST },
{ 139, MENU_DTMF_CONTACT_LIST },
{ 14, MENU_CONTACT_NEW },
};
const menuItemsList_t menuDataContact =
{
.numItems = (sizeof(contactMenuItems) / sizeof(contactMenuItems[0])),
.items = contactMenuItems
};
static const menuItemNewData_t optionsMenuItems[] =
{
{ 190, MENU_GENERAL },
{ 191, MENU_RADIO },
{ 10, MENU_DISPLAY },
{ 11, MENU_SOUND },
{ 13, MENU_LANGUAGE },
{ 206, MENU_CALIBRATION },
#if defined(HAS_COLOURS)
{ 218, MENU_THEME },
#endif
#if !defined(PLATFORM_GD77S)
{ 257, MENU_APRS },
#endif
};
const menuItemsList_t menuDataOptions =
{
.numItems = (sizeof(optionsMenuItems) / sizeof(optionsMenuItems[0])),
.items = optionsMenuItems
};
void menuDisplayTitle(const char *title)
{
themeItem_t fgItem, bgItem;
displayThemeGetForegroundAndBackgroundItems(&fgItem, &bgItem);
#if defined(HAS_COLOURS)
displayThemeApply(THEME_ITEM_BG_MENU_NAME, THEME_ITEM_COLOUR_NONE);
displayFillRect(0, 0, DISPLAY_SIZE_X, MENU_HEADER_HEIGHT, false);
#endif
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG);
displayDrawFastHLine(0, (MENU_HEADER_HEIGHT - 1), DISPLAY_SIZE_X, true);
displayThemeApply(THEME_ITEM_FG_MENU_NAME, THEME_ITEM_BG);
displayPrintCore(0, 3, title, FONT_SIZE_2, TEXT_ALIGN_CENTER, false);
displayThemeApply(fgItem, bgItem);
}
// optStart:
// - if less than 0, entryText will use fgOptItem colour
// - if equal to 0, no coloured option will be handled
void menuDisplayEntry(int loopOffset, int focusedItem, const char *entryText, int32_t optStart, themeItem_t fgItem, themeItem_t fgOptItem, themeItem_t bgItem)
{
bool focused = (focusedItem == menuDataGlobal.currentItemIndex);
int16_t textXPos = DISPLAY_X_POS_MENU_TEXT_OFFSET;
if (focused)
{
displayThemeApply(THEME_ITEM_BG_MENU_ITEM_SELECTED, bgItem);
displayFillRoundRect(DISPLAY_X_POS_MENU_OFFSET, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT
#if defined(PLATFORM_RD5R)
- 1 // Small V offset due to small font usage
#endif
+ (loopOffset * MENU_ENTRY_HEIGHT), DISPLAY_SIZE_X - (DISPLAY_X_POS_MENU_OFFSET * 2), MENU_ENTRY_HEIGHT, 2, true);
}
displayThemeApply(fgItem, bgItem);
if ((focused == false) && ((optStart < 0) || (optStart > 0)))
{
char buffer[SCREEN_LINE_BUFFER_SIZE];
snprintf(buffer, (optStart > 0) ? (optStart + 1) : (strlen(entryText) + 1), "%s", entryText);
if (optStart < 0)
{
displayThemeApply(fgOptItem, bgItem);
}
displayPrintCore(textXPos, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT), buffer, FONT_SIZE_3, TEXT_ALIGN_LEFT, focused);
if (optStart > 0)
{
displayThemeApply(fgOptItem, bgItem);
displayPrintCore(textXPos + (optStart * 8), DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT), (entryText + optStart), FONT_SIZE_3, TEXT_ALIGN_LEFT, focused);
}
}
else
{
displayPrintCore(textXPos, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT), entryText, FONT_SIZE_3, TEXT_ALIGN_LEFT, focused);
}
displayThemeResetToDefault();
}
// Returns menu offset, -1 if the line is before the first menu item, -2 if the line is after the last menu item
int menuGetMenuOffset(int maxMenuItems, int loopOffset)
{
int offset = menuDataGlobal.currentItemIndex + loopOffset;
int startOffset = 0;
int iter = (loopOffset + (MENU_MAX_DISPLAYED_ENTRIES / 2) + ((MENU_MAX_DISPLAYED_ENTRIES & 0x01) ? 1 : 0));
if (maxMenuItems < MENU_MAX_DISPLAYED_ENTRIES)
{
startOffset = (((MENU_MAX_DISPLAYED_ENTRIES - ((MENU_MAX_DISPLAYED_ENTRIES & 0x01) ? 0 : 1)) - maxMenuItems) / 2);
if (iter <= startOffset)
{
return MENU_OFFSET_BEFORE_FIRST_ENTRY;
}
else if (iter > (startOffset + maxMenuItems))
{
return MENU_OFFSET_AFTER_LAST_ENTRY;
}
}
if (offset < 0)
{
if ((maxMenuItems == 1) && (maxMenuItems < MENU_MAX_DISPLAYED_ENTRIES))
{
offset = MENU_MAX_DISPLAYED_ENTRIES - 1;
}
else
{
offset = maxMenuItems + offset;
}
}
if (offset >= maxMenuItems)
{
offset = offset - maxMenuItems;
}
return offset;
}
/*
* Returns 99 if key is unknown, or not numerical when digitsOnly is true
*/
#if defined(PLATFORM_MD9600)
#define NON_NUMERICAL_KEYS 7
#else
#define NON_NUMERICAL_KEYS 6
#endif
int menuGetKeypadKeyValue(uiEvent_t *ev, bool digitsOnly)
{
#if !defined(PLATFORM_GD77S)
uint32_t keypadKeys[] =
{
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380)
KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
KEY_UP, 0, KEY_DOWN, 0, KEY_STAR, KEY_HASH
#elif defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
KEY_LEFT, KEY_UP, KEY_DOWN, KEY_RIGHT, KEY_STAR, KEY_HASH
#elif defined(PLATFORM_MD9600)
KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
KEY_A, KEY_B, KEY_C, KEY_D, KEY_STAR, KEY_HASH, KEY_GREEN
#else // MK22, minus GD77S
KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
KEY_LEFT, KEY_UP, KEY_DOWN, KEY_RIGHT, KEY_STAR, KEY_HASH
#endif
};
for (int i = 0; i < ((sizeof(keypadKeys) / sizeof(keypadKeys[0])) - (digitsOnly ? NON_NUMERICAL_KEYS : 0 )); i++)
{
if (
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380)
(keypadKeys[i] != 0) &&
#endif
KEYCHECK_PRESS(ev->keys, keypadKeys[i]))
{
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380)
if (BUTTONCHECK_DOWN(ev, BUTTON_SK1))
{
if (i == 10) // Simulate 'B'
{
return 11;
}
else if (i == 12) // Simulate 'D'
{
return 13;
}
}
#endif
return i;
}
}
#endif // GD77S
return 99;
}
void menuUpdateCursor(int pos, bool moved, bool render)
{
#if defined(PLATFORM_RD5R)
const int MENU_CURSOR_Y = 35;
#elif defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
#if defined(PLATFORM_VARIANT_DM1701)
const int MENU_CURSOR_Y = 66;
#else
const int MENU_CURSOR_Y = 78;
#endif
#else
const int MENU_CURSOR_Y = 46;
#endif
static uint32_t lastBlink = 0;
static bool blink = false;
uint32_t m = ticksGetMillis();
if (moved)
{
blink = true;
}
if (moved || (m - lastBlink) > 500)
{
displayDrawFastHLine(((pos * 8) + DISPLAY_X_POS_MENU_TEXT_OFFSET), MENU_CURSOR_Y, 8, blink);
blink = !blink;
lastBlink = m;
if (render)
{
displayRenderRows(MENU_CURSOR_Y / 8, MENU_CURSOR_Y / 8 + 1);
}
}
}
void moveCursorLeftInString(char *str, int *pos, bool delete)
{
int nLen = strlen(str);
if (*pos > 0)
{
if (nLen == 16)
{
if (*pos != 15)
{
*pos -=1;
}
}
else
{
*pos -=1;
}
announceChar(str[*pos]); // speak the new char or the char about to be backspaced out.
if (delete)
{
for (int i = *pos; i <= nLen; i++)
{
str[i] = str[i + 1];
}
}
}
}
void moveCursorRightInString(char *str, int *pos, int max, bool insert)
{
int nLen = strlen(str);
if (*pos < strlen(str))
{
if (insert)
{
if (nLen < max)
{
for (int i = nLen; i > *pos; i--)
{
str[i] = str[i - 1];
}
str[*pos] = ' ';
}
}
if (*pos < max-1)
{
*pos += 1;
announceChar(str[*pos]); // speak the new char or the char about to be backspaced out.
}
}
}
void menuSystemMenuIncrement(int32_t *currentItem, int32_t numItems)
{
*currentItem = (*currentItem + 1) % numItems;
}
void menuSystemMenuDecrement(int32_t *currentItem, int32_t numItems)
{
*currentItem = (*currentItem + numItems - 1) % numItems;
}
// For QuickKeys
void menuDisplaySettingOption(const char *entryText, const char *valueText)
{
displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG);
#if defined(PLATFORM_RD5R)
displayDrawRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 6, DISPLAY_SIZE_X - 4, (MENU_ENTRY_HEIGHT * 2) + 8, 2, true);
displayFillRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 6, DISPLAY_SIZE_X - 4, MENU_ENTRY_HEIGHT + 3, 2, true);
displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG);
if (entryText)
{
displayPrintCore(0, DISPLAY_Y_POS_MENU_START - MENU_ENTRY_HEIGHT - 4, entryText, FONT_SIZE_2, TEXT_ALIGN_CENTER, true);
}
#else
displayDrawRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 2, DISPLAY_SIZE_X - 4, (MENU_ENTRY_HEIGHT * 2) + 4, 2, true);
displayFillRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 2, DISPLAY_SIZE_X - 4, MENU_ENTRY_HEIGHT, 2, true);
displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG);
if (entryText)
{
displayPrintCore(0, DISPLAY_Y_POS_MENU_START - MENU_ENTRY_HEIGHT + DISPLAY_V_OFFSET + 2, entryText, FONT_SIZE_2, TEXT_ALIGN_CENTER, true);
}
#endif
displayPrintCore(0, DISPLAY_Y_POS_MENU_START + DISPLAY_V_OFFSET, valueText, FONT_SIZE_3, TEXT_ALIGN_CENTER, false);
displayThemeResetToDefault();
}