/* * 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 // *** Add new menus to be accessed using quickkey (ID: 0..31) above this line *** 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 // *** Add new menus to be accessed using quickkey (ID: 0..31) above this line *** { 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 }, { 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 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 #if defined(HAS_COLOURS) // 16x16 1-bit main-menu icons (row-major, MSB-first) for displayDrawBitmap. static const uint8_t menuIcon_zone[32] = { 0x00, 0x00, 0x3F, 0xFC, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x2F, 0xF4, 0x20, 0x04, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_contacts[32] = { 0x00, 0x00, 0x03, 0xC0, 0x07, 0xE0, 0x05, 0xA0, 0x07, 0xE0, 0x03, 0xC0, 0x01, 0x80, 0x0F, 0xF0, 0x1F, 0xF8, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_channel[32] = { 0x00, 0x00, 0x00, 0x00, 0x21, 0x08, 0x21, 0x08, 0x71, 0x1C, 0x51, 0x14, 0x73, 0x9C, 0x22, 0x88, 0x23, 0x88, 0x21, 0x08, 0x21, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_rssi[32] = { 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x66, 0x00, 0x66, 0x00, 0x66, 0x06, 0x66, 0x06, 0x66, 0x06, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_info[32] = { 0x00, 0x00, 0x07, 0xE0, 0x18, 0x18, 0x21, 0x84, 0x21, 0x84, 0x40, 0x08, 0x43, 0xC2, 0x40, 0xC2, 0x40, 0xC2, 0x40, 0xC2, 0x21, 0xE2, 0x20, 0x04, 0x18, 0x18, 0x07, 0xE0, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_options[32] = { 0x00, 0x00, 0x03, 0xC0, 0x13, 0xC8, 0x1B, 0xD8, 0x3F, 0xFC, 0x38, 0x1C, 0x7B, 0xDE, 0x7B, 0xDE, 0x7B, 0xDE, 0x38, 0x1C, 0x3F, 0xFC, 0x1B, 0xD8, 0x13, 0xC8, 0x03, 0xC0, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_lastheard[32] = { 0x00, 0x00, 0x3F, 0xF8, 0x40, 0x04, 0x4C, 0x62, 0x40, 0x04, 0x4C, 0x62, 0x40, 0x04, 0x4C, 0x62, 0x40, 0x04, 0x3C, 0x7C, 0x04, 0x40, 0x02, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_radio[32] = { 0x00, 0x10, 0x00, 0x20, 0x00, 0x40, 0x1F, 0xC0, 0x10, 0x40, 0x17, 0x40, 0x17, 0x40, 0x10, 0x40, 0x15, 0x40, 0x15, 0x40, 0x15, 0x40, 0x10, 0x40, 0x1F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_satellite[32] = { 0x00, 0x00, 0x30, 0x00, 0x28, 0x10, 0x24, 0x28, 0x02, 0x44, 0x01, 0x94, 0x00, 0xA4, 0x01, 0x38, 0x02, 0x00, 0x0D, 0x00, 0x11, 0x00, 0x11, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; static const uint8_t menuIcon_gps[32] = { 0x00, 0x00, 0x07, 0xE0, 0x18, 0x18, 0x20, 0x04, 0x43, 0xC2, 0x44, 0x22, 0x44, 0x22, 0x44, 0x22, 0x43, 0xC2, 0x20, 0x04, 0x10, 0x08, 0x08, 0x10, 0x04, 0x20, 0x02, 0x40, 0x01, 0x80, 0x00, 0x00 }; static const uint8_t *menuIconForMenuNumber(int menuNum) { switch (menuNum) { case MENU_ZONE_LIST: return menuIcon_zone; case MENU_CONTACTS_MENU: return menuIcon_contacts; case MENU_CHANNEL_DETAILS: return menuIcon_channel; case MENU_RSSI_SCREEN: return menuIcon_rssi; case MENU_FIRMWARE_INFO: return menuIcon_info; case MENU_OPTIONS: return menuIcon_options; case MENU_LAST_HEARD: return menuIcon_lastheard; case MENU_RADIO_INFOS: return menuIcon_radio; case MENU_SATELLITE: return menuIcon_satellite; case MENU_GPS: return menuIcon_gps; default: return NULL; } } // One-shot: the next menuDisplayEntry() draws this icon and shifts its text right. static const uint8_t *nextMenuEntryIcon = NULL; void menuSetNextEntryIconByMenuNum(int menuNum) { nextMenuEntryIcon = menuIconForMenuNumber(menuNum); } #endif // HAS_COLOURS 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 defined(HAS_COLOURS) const uint8_t *entryIcon = nextMenuEntryIcon; nextMenuEntryIcon = NULL; // consume it (one-shot) #endif 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 defined(HAS_COLOURS) if (entryIcon != NULL) { int16_t iconY = DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT); displayDrawBitmap((DISPLAY_X_POS_MENU_OFFSET + 2), iconY, (uint8_t *)entryIcon, 16, 16, focused); textXPos = (DISPLAY_X_POS_MENU_OFFSET + 2 + 16 + 3); // icon width + gap } #endif 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(); }