Skip to content
Alexander Spiridonov edited this page Oct 13, 2024 · 55 revisions

The following is a small reference on how to do common tasks when creating a menu:

Create menu object (AltSerialGraphicLCD version)

Support for AltSerialGraphicLCD library should be enabled first, see Configuration section of Readme for details.

With default settings, where glcd is an object of class GLCD that uses SoftwareSerial to communicate with LCD equipped with SparkFun Graphic LCD Serial Backpack:

// Constants for the pins SparkFun Graphic LCD Serial Backpack is connected to and SoftwareSerial object
const byte rxPin = 8;
const byte txPin = 9;
SoftwareSerial serialLCD(rxPin, txPin);

// Instance of GLCD class
GLCD glcd(serialLCD);

// Menu object 
GEM menu(glcd);
// Which is equivalent to the following call (you can adjust parameters to better fit your screen if necessary):
// GEM menu(glcd, /* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ 5, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86);

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Create menu object (U8g2 version)

First, choose proper constructor of U8g2 library, based on LCD controller being used and desired configuration (U8G2_KS0108_128X64_1 in the example). Then proceed as follows:

// Create an instance of the U8g2 library.
// Use constructor that matches your setup (see https://github.com/olikraus/u8g2/wiki/u8g2setupcpp for details).
// This instance is used to call all the subsequent U8g2 functions (internally from GEM library,
// or manually in your sketch if it is required).
// Please update the pin numbers according to your setup. Use U8X8_PIN_NONE if the reset pin is not connected
U8G2_KS0108_128X64_1 u8g2(U8G2_R0, 8, 9, 10, 11, 12, 13, 18, 19, /*enable=*/ A0, /*dc=*/ A1, /*cs0=*/ A3, /*cs1=*/ A2, /*cs2=*/ U8X8_PIN_NONE, /* reset=*/ U8X8_PIN_NONE);   // Set R/W to low!

// Menu object
GEM_u8g2 menu(u8g2);
// Which is equivalent to the following call (you can adjust parameters to better fit your screen if necessary):
// GEM_u8g2 menu(u8g2, /* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ GEM_ITEMS_COUNT_AUTO, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86);

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Create menu object (Adafruit GFX version)

Adafruit GFX library supports several different display controllers (through separately installed and included libraries). Choose a matching library for the correct initialization of the display. See available libraries and supported controllers in the documentation for Adafruit GFX library.

In case of ST7735-based display, we need to create an instance of the Adafruit_ST7735 class named tft:

// Hardware-specific library for ST7735.
// Include library that matches your setup (see https://learn.adafruit.com/adafruit-gfx-graphics-library for details)
#include <Adafruit_ST7735.h>

// Macro constants (aliases) for the pins TFT display is connected to. Please update the pin numbers according to your setup
#define TFT_CS    A2
#define TFT_RST   -1 // Set to -1 and connect to Arduino RESET pin
#define TFT_DC    A3

// Create an instance of the Adafruit GFX library.
// Use constructor that matches your setup (see https://learn.adafruit.com/adafruit-gfx-graphics-library for details).
// This instance is used to call all the subsequent Adafruit GFX functions (internally from GEM library,
// or manually in your sketch if it is required)
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// Menu object
GEM_adafruit_gfx menu(tft, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO);
// Which is equivalent to the following call (you can adjust parameters to better fit your screen if necessary):
// GEM_adafruit_gfx menu(tft, /* menuPointerType= */ GEM_POINTER_ROW, /* menuItemsPerScreen= */ GEM_ITEMS_COUNT_AUTO, /* menuItemHeight= */ 10, /* menuPageScreenTopOffset= */ 10, /* menuValuesLeftOffset= */ 86);

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Init and draw menu to the screen (AltSerialGraphicLCD version)

Generally in setup() function of the sketch:

// Serial communications setup
serialLCD.begin(115200);

// LCD reset
delay(500);
glcd.reset();
delay(1000);
// Uncomment the following lines in dire situations
// (e.g. when screen becomes unresponsive after shutdown)
glcd.reset();
delay(1000);

// Menu init
menu.init();

// Menu setup: add menu items to menu pages to menu here (see below for examples)
menuPageMain.addMenuItem(menuItemByte);
menu.setMenuPageCurrent(menuPageMain);

// Menu draw
menu.drawMenu();

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Init and draw menu to the screen (U8g2 version)

Generally in setup() function of the sketch:

// U8g2 library init.
u8g2.begin();

// Menu init
menu.init();

// Menu setup: add menu items to menu pages to menu here (see below for examples)
menuPageMain.addMenuItem(menuItemByte);
menu.setMenuPageCurrent(menuPageMain);

// Menu draw
menu.drawMenu();

Optionally, to utilize built-in key press event detection features of U8g2 library, supply u8g2.begin() method with pin numbers the push-buttons are attached to (read more for details):

// U8g2 library init. Pass pin numbers the buttons are connected to.
// The push-buttons should be wired with pullup resistors (so the LOW means that the button is pressed)
u8g2.begin(/*Select/OK=*/ 7, /*Right/Next=*/ 4, /*Left/Prev=*/ 3, /*Up=*/ 5, /*Down=*/ 2, /*Home/Cancel=*/ 6);

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Init and draw menu to the screen (Adafruit GFX version)

Generally in setup() function of the sketch:

// Use this initializer if using a 1.8" TFT screen:
tft.initR(INITR_BLACKTAB);      // Init ST7735S chip, black tab
// OR use this initializer if using a 1.8" TFT screen with offset such as WaveShare:
// tft.initR(INITR_GREENTAB);   // Init ST7735S chip, green tab
// See more options in Adafruit GFX library documentation

// Optionally, rotate display
// tft.setRotation(3); // See Adafruit GFX library documentation for details

// Menu init
menu.init();

// Menu setup: add menu items to menu pages to menu here (see below for examples)
menuPageMain.addMenuItem(menuItemByte);
menu.setMenuPageCurrent(menuPageMain);

// Menu draw
menu.drawMenu();

Note that some displays used in combination with Adafruit GFX (Adafruit_SSD1306, Adafruit_SH110X, etc.) will require to explicitly call display() method of Adafruit GFX library at the end of the loop() (and after each explicit call to menu.drawMenu()) to draw image buffer on screen:

// Menu draw
menu.drawMenu();
tft.display();

Alternatively it is possible to specify callback that will be called at the end of each drawMenu() method (since GEM ver. 1.5.2):

void drawMenuCallback() {
  display.display();
}
...
menu.setDrawMenuCallback(drawMenuCallback);

Important to note, when using Adafruit GFX version of GEM with an OLED display it may be required to set foreground and background colors explicitly (even for monochrome displays) to the values suitable for your display, before calling menu.init().

// Explicitly set correct colors for monochrome OLED screen
menu.setForegroundColor(SH110X_WHITE);
menu.setBackgroundColor(SH110X_BLACK);

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Enable Cyrillic

For U8g2 version of GEM only. You may use Cyrillic in menu title, menu item labels (including buttons and menu page links), and select options. Editable strings with Cyrillic characters are not supported (edit mode of such strings may lead to unpredictable results due to incompatibility with 2-byte characters). Increases required program storage space, use cautiously. By default Cyrillic typeset is off. To enable Cyrillic support, call enableCyrillic() method before init():

menu.enableCyrillic();

Related examples: Example 01: Basic

Create menu page

Create menu page labeled "Main Menu":

GEMPage menuPageMain("Main Menu");

Add menu page to menu and set it as current (generally in setup() function of the sketch, before menu.drawMenu() is called):

menu.setMenuPageCurrent(menuPageMain);

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Create byte menu item

Define byte variable:

byte byteNumber = 42;

Create menu item linked to byteNumber variable and labeled "Byte":

GEMItem menuItemByteNumber("Byte:", byteNumber);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemByteNumber);

Create int menu item

Define int variable:

int intNumber = -512;

Create menu item linked to intNumber variable and labeled "Int":

GEMItem menuItemIntNumber("Int:", intNumber);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemIntNumber);

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Create float menu item

Define float variable:

float floatNumber = 3.1416;

Create menu item linked to floatNumber variable and labeled "Float":

GEMItem menuItemFloatNumber("Float:", floatNumber);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemFloatNumber);

By default, float variable is displayed with 6 digits after decimal point (and rounded accordingly if necessary), i.e. 3.141600. To change that, call GEMItem::setPrecision() method:

menuItemFloatNumber.setPrecision(3);

That will result in 3.142 being displayed as a value of the menu item (and that will overwrite original value of 3.1416 upon saving).

See Floating-point variables section of Readme for more details on float and double variables support.

Create double menu item

Define double variable:

double doubleNumber = -2.7183;

Create menu item linked to doubleNumber variable and labeled "Double":

GEMItem menuItemDoubleNumber("Double:", doubleNumber);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemDoubleNumber);

By default, double variable is displayed with 6 digits after decimal point (and rounded accordingly if necessary), i.e. -2.718300. To change that, call GEMItem::setPrecision() method:

menuItemDoubleNumber.setPrecision(3);

That will result in -2.718 being displayed as a value of the menu item (and that will overwrite original value of -2.718 upon saving).

See Floating-point variables section of Readme for more details on float and double variables support.

Create bool menu item

Define bool variable:

bool flag = false;

Create menu item linked to flag variable and labeled "Bored?":

GEMItem menuItemFlag("Bored?", flag);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemFlag);

Related examples: Example 01: Basic, Example 03: Party Hard!, Example 06: Todo List

Create char[17] (a.k.a. char[GEM_STR_LEN]) menu item

GEM_STR_LEN (equals 17) is the only length of the editable char array currently supported by GEM (keep in mind that the last element is reserved for terminating character, so the actual length available for user data is GEM_STR_LEN - 1 or 16). Using length different from GEM_STR_LEN may cause overflows and unpredicted behaviour. Define char[17] or char[GEM_STR_LEN] variable:

char name[GEM_STR_LEN] = "Jack Daniels";

Create menu item linked to name variable and labeled "Name":

GEMItem menuItemName("Name", name);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemName);

Related examples: Example 02: Blink, Example 06: Todo List

Create byte option select

Define byte variable:

byte byteNumber = 42;

Create array of options:

SelectOptionByte optionsArray[] = {{"Opt 1", 10}, {"Opt 2", 42}, {"Opt 3", 115}};

Supply array of options to GEMSelect constructor (note that length of the array should be explicitly supplied/calculated because array is passed by reference):

GEMSelect myByteSelect(3, optionsArray);
// or
GEMSelect myByteSelect(sizeof(optionsArray)/sizeof(SelectOptionByte), optionsArray);
// or (no need to explicitly define optionsArray[] in this case)
GEMSelect myByteSelect(3, (SelectOptionByte[]){{"Opt 1", 10}, {"Opt 2", 42}, {"Opt 3", 115}});

Create menu item linked to byteNumber variable and to myByteSelect, and labeled "Number":

GEMItem menuItemByteNumber("Number:", byteNumber, myByteSelect);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemByteNumber);

Related examples: Example 03: Party Hard!, Example 05: Encoder, Example 06: Todo List

Create int option select

Define int variable:

int intNumber = -512;

Create array of options:

SelectOptionInt optionsArray[] = {{"Opt 1", -512}, {"Opt 2", 0}, {"Opt 3", 512}};

Supply array of options to GEMSelect constructor (note that length of the array should be explicitly supplied/calculated because array is passed by reference):

GEMSelect myIntSelect(3, optionsArray);
// or
GEMSelect myIntSelect(sizeof(optionsArray)/sizeof(SelectOptionInt), optionsArray);
// or (no need to explicitly define optionsArray[] in this case)
GEMSelect myIntSelect(3, (SelectOptionInt[]){{"Opt 1", -512}, {"Opt 2", 0}, {"Opt 3", 512}});

Create menu item linked to intNumber variable and to myIntSelect, and labeled "Number":

GEMItem menuItemIntNumber("Number:", intNumber, myIntSelect);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemIntNumber);

Create float option select

Define float variable:

float floatNumber = -3.14;

Create array of options:

SelectOptionFloat optionsArray[] = {{"Opt 1", -3.14}, {"Opt 2", 0}, {"Opt 3", 3.14}};

Supply array of options to GEMSelect constructor (note that length of the array should be explicitly supplied/calculated because array is passed by reference):

GEMSelect myFloatSelect(3, optionsArray);
// or
GEMSelect myFloatSelect(sizeof(optionsArray)/sizeof(SelectOptionFloat), optionsArray);
// or (no need to explicitly define optionsArray[] in this case)
GEMSelect myFloatSelect(3, (SelectOptionFloat[]){{"Opt 1", -3.14}, {"Opt 2", 0}, {"Opt 3", 3.14}});

Create menu item linked to floatNumber variable and to myFloatSelect, and labeled "Number":

GEMItem menuItemFloatNumber("Number:", floatNumber, myFloatSelect);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemFloatNumber);

Create double option select

Define double variable:

double doubleNumber = -2.7183;

Create array of options:

SelectOptionDouble optionsArray[] = {{"Opt 1", -2.7183}, {"Opt 2", 0}, {"Opt 3", 2.7183}};

Supply array of options to GEMSelect constructor (note that length of the array should be explicitly supplied/calculated because array is passed by reference):

GEMSelect myDoubleSelect(3, optionsArray);
// or
GEMSelect myDoubleSelect(sizeof(optionsArray)/sizeof(SelectOptionDouble), optionsArray);
// or (no need to explicitly define optionsArray[] in this case)
GEMSelect myDoubleSelect(3, (SelectOptionDouble[]){{"Opt 1", -2.7183}, {"Opt 2", 0}, {"Opt 3", 2.7183}});

Create menu item linked to doubleNumber variable and to myDoubleSelect, and labeled "Number":

GEMItem menuItemDoubleNumber("Number:", doubleNumber, myDoubleSelect);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemDoubleNumber);

Create char[n] option select

There is no restriction on the length of character array that can be used with option select. However, make sure that character array of associated with menu item variable (of type char[n]) is big enough to hold select option with the longest value to avoid overflows. Define char[n] variable:

char name[5] = "Alex";

Create array of options:

SelectOptionChar optionsArray[] = {{"Alexander", "Alex"}, {"Michael", "Mike"}, {"Peter", "Pete"}};

Supply array of options to GEMSelect constructor (note that length of the array should be explicitly supplied/calculated because array is passed by reference):

GEMSelect myNameSelect(3, optionsArray);
// or
GEMSelect myNameSelect(sizeof(optionsArray)/sizeof(SelectOptionChar), optionsArray);
// or (no need to explicitly define optionsArray[] in this case)
GEMSelect myNameSelect(3, (SelectOptionChar[]){{"Alexander", "Alex"}, {"Michael", "Mike"}, {"Peter", "Pete"}});

Create menu item linked to name variable and to myNameSelect, and labeled "Name":

GEMItem menuItemName("Name:", name, myNameSelect);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemName);

Create byte value spinner

Define byte variable:

byte byteNumber = 50;

Define settings (boundaries) of the spinner:

GEMSpinnerBoundariesByte mySpinnerBoundaries = { .step = 10, .min = 0, .max = 150 };
// or
GEMSpinnerBoundariesByte mySpinnerBoundaries = { 10, 0, 150 };

Supply settings to GEMSpinner constructor:

GEMSpinner myByteSpinner(mySpinnerBoundaries);

Create menu item linked to byteNumber variable and to myByteSpinner, and labeled "Number":

GEMItem menuItemByteNumber("Number:", byteNumber, myByteSpinner);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemByteNumber);

Related examples: test_73_gem-u8g2_sh1106_encoder_spinner-example.ino (GEM - Spinners! on Wokwi)

Create int value spinner

Define int variable:

int intNumber = 0;

Define settings (boundaries) of the spinner:

GEMSpinnerBoundariesInt mySpinnerBoundaries = { .step = 10, .min = -150, .max = 150 };
// or
GEMSpinnerBoundariesInt mySpinnerBoundaries = { 10, -150, 150 };

Supply settings to GEMSpinner constructor:

GEMSpinner myIntSpinner(mySpinnerBoundaries);

Create menu item linked to intNumber variable and to myIntSpinner, and labeled "Number":

GEMItem menuItemIntNumber("Number:", intNumber, myIntSpinner);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemIntNumber);

Related examples: test_73_gem-u8g2_sh1106_encoder_spinner-example.ino (GEM - Spinners! on Wokwi)

Create float value spinner

Define float variable:

float floatNumber = 0;

Define settings (boundaries) of the spinner:

GEMSpinnerBoundariesFloat mySpinnerBoundaries = { .step = 0.5, .min = -10, .max = 10 };
// or
GEMSpinnerBoundariesFloat mySpinnerBoundaries = { 0.5, -10, 10 };

Supply settings to GEMSpinner constructor:

GEMSpinner myFloatSpinner(mySpinnerBoundaries);

Create menu item linked to floatNumber variable and to myFloatSpinner, and labeled "Number":

GEMItem menuItemFloatNumber("Number:", floatNumber, myFloatSpinner);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemFloatNumber);

By default, float variable is displayed with 6 digits after decimal point (and rounded accordingly if necessary), i.e. 1.500000. To change that, call GEMItem::setPrecision() method:

menuItemFloatNumber.setPrecision(1);

That will result in 1.5 being displayed as a value of the menu item (and that value will be saved to a linked variable).

See Floating-point variables section of Readme for more details on float and double variables support.

Related examples: test_73_gem-u8g2_sh1106_encoder_spinner-example.ino (GEM - Spinners! on Wokwi)

Create double value spinner

Define double variable:

double doubleNumber = 0;

Define settings (boundaries) of the spinner:

GEMSpinnerBoundariesDouble mySpinnerBoundaries = { .step = 0.05, .min = -2, .max = 2 };
// or
GEMSpinnerBoundariesDouble mySpinnerBoundaries = { 0.05, -2, 2 };

Supply settings to GEMSpinner constructor:

GEMSpinner myDoubleSpinner(mySpinnerBoundaries);

Create menu item linked to doubleNumber variable and to myDoubleSpinner, and labeled "Number":

GEMItem menuItemDoubleNumber("Number:", doubleNumber, myDoubleSpinner);

Add menu item to existing menu page (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemDoubleNumber);

By default, double variable is displayed with 6 digits after decimal point (and rounded accordingly if necessary), i.e. -1.050000. To change that, call GEMItem::setPrecision() method:

menuItemDoubleNumber.setPrecision(2);

That will result in -1.05 being displayed as a value of the menu item (and that value will be saved to a linked variable).

See Floating-point variables section of Readme for more details on float and double variables support.

Related examples: test_73_gem-u8g2_sh1106_encoder_spinner-example.ino (GEM - Spinners! on Wokwi)

Add menu item callback (basic)

You can define function that will be called when associated with menu item variable is successfully saved. You should forward-declare it before supplying it to menu item GEMItem constructor:

void validateInterval();
GEMItem menuItemInterval("Interval:", interval, validateInterval);

void loadPreset();
GEMItem menuItemName("Mode:", mode, myModeSelect, loadPreset);

Later in the sketch define this function:

void validateInterval() {
  // Do something, e.g. validate value of the variable and change it accordingly:
  if (interval < 0) {
    interval = 0;
  }
}

void loadPreset() {
  // Do something, e.g. print selected value to Serial Monitor:
  Serial.print("Mode option set: ");
  Serial.println(mode);
  // ...or apply predefined values to other variables
  // based on the selected option, thus creating sort of preset selector
  switch (mode) {
    case 0:
      interval = 120;
      break;
    case 1:
      interval = 500;
      break;
    case 2:
      interval = 1000;
      break;
  }
}

Related examples: Example 02: Blink, Example 03: Party Hard!, Example 05: Encoder, Example 06: Todo List

Add menu item callback (with callback arguments)

Callback function (that will be called when associated with menu item variable is successfully saved) optionally can expect argument of type GEMCallbackData to be passed to it when it is executed. In this case optional user-defined value of an argument can be specified. You should forward-declare callback function before supplying it to menu item GEMItem constructor:

const int defaultInterval = 100;
void validateInterval(GEMCallbackData callbackData);
GEMItem menuItemInterval("Interval:", interval, validateInterval, defaultInterval);  // defaultInterval is user-defined callback value

void loadPreset(GEMCallbackData callbackData);
GEMItem menuItemName("Mode:", mode, myModeSelect, loadPreset, "Mode option set: "); // String "Mode option set: " is user-defined callback value

Later in the sketch define this function:

void validateInterval(GEMCallbackData callbackData) {
  // Do something, e.g. validate value of the variable and change it accordingly
  // based on user-defined callback argument:
  if (interval < callbackData.valInt) {
    interval = callbackData.valInt;
  }
}

void loadPreset(GEMCallbackData callbackData) {
  // Do something, e.g. print selected value to Serial Monitor
  // after printing user-defined callback argument:
  Serial.print(callbackData.valChar);
  Serial.println(mode);
  // ...or apply predefined values to other variables
  // based on the selected option, thus creating sort of preset selector
  switch (mode) {
    case 0:
      interval = 120;
      break;
    case 1:
      interval = 500;
      break;
    case 2:
      interval = 1000;
      break;
  }
}

Related examples: test_73_gem-u8g2_sh1106_encoder_spinner-example.ino (GEM - Spinners! on Wokwi)

Make menu item readonly

Readonly menu items are marked with symbol ^ and won't allow user to interact with the associated variable (as well as activate readonly menu links or buttons). Of course, it still can be changed programmatically.

You can mark menu item as readonly during its creation, e.g.:

GEMItem menuItemByteNumber("Byte:", byteNumber, GEM_READONLY);
GEMItem menuItemName("Name:", name, myNameSelect, GEM_READONLY);
GEMItem menuItemButton("Print", printData, GEM_READONLY);
GEMItem menuItemSettings("Settings", menuPageSettings, GEM_READONLY);

Note that you cannot specify both readonly mode and callback (see above) in the same constructor. However, you can set readonly mode for menu item with (or without) callback explicitly later using GEMItem::setReadonly() method:

menuItemByteNumber.setReadonly(); // Enable readonly mode
menuItemIntNumber.setReadonly(true); // Enable readonly mode
menuItemName.setReadonly(false); // Disable readonly mode

To get current state of the menu item use GEMItem::getReadonly() method:

bool isReadonly = menuItemByteNumber.getReadonly();

You can use it to toggle readonly mode of the menu item by supplying its negated output directly to GEMItem::setReadonly() method:

menuItemByteNumber.setReadonly(!menuItemByteNumber.getReadonly());

Related examples: Example 03: Party Hard!

Hide or show menu item

Hidden menu items won't be printed to the screen the next time menu is drawn (and user won't be able to activate them via graphical interface of the menu). To hide or show menu item use the following methods:

menuItemByteNumber.hide(); // Hide
menuItemIntButton.hide(true); // Hide
menuItemPrintButton.hide(false); // Show
menuItemName.show(); // Show

To get current state of the menu item use GEMItem::getHidden() method:

bool isHidden = menuItemByteNumber.getHidden();

You can use it to toggle menu item visibility by supplying its negated output directly to GEMItem::hide() method:

menuItemByteNumber.hide(!menuItemByteNumber.getHidden());

Related examples: Example 06: Todo List

Remove menu item

It is possible to remove menu item from menu page completely. Unlike GEMItem::hide(), this will unlink menu item from menu page, so it can be added to another (or the same) menu page later.

menuItemByteNumber.remove();

Related examples: Example 06: Todo List

Add menu item at a specified position

By default menu item is added at the end of the list of menu items of the page (including hidden ones). It is possible to add menu item at a specified position (zero-based number from 0 to 255), which is passed a second argument to GEMPage::addMenuItem() method. The following will add menu item as a third menu item (out of total number of menu items of the menu page including hidden ones) of the Settings menu page:

menuPageSettings.addMenuItem(menuItemByteNumber, 2);

If it is required to consider only visible menu items (out of all), it is possible to pass flag GEM_ITEMS_VISIBLE (equals to false) as a third argument:

menuPageSettings.addMenuItem(menuItemByteNumber, 2, GEM_ITEMS_VISIBLE);
// or
menuPageSettings.addMenuItem(menuItemByteNumber, 2, false);

If position is set to 0 and menu page has parent menu page, menu item will be added at position 1 instead (i.e. as a second menu item, after built-in Back button).

Related examples: Example 06: Todo List

Traverse menu items

If it is required, it is possible to iterate through the list of menu items on the menu page using GEMPage::getMenuItem() (to get menu item at a specified position on the menu page) and GEMItem::getMenuItemNext() (to get pointer to the following menu item) methods.

GEMItem* menuItemTmp = menuPageList.getMenuItem(1); // Get second menu item in a menu page to start traversing (counting hidden ones)
// GEMItem* menuItemTmp = menuPageList.getMenuItem(1, GEM_ITEMS_VISIBLE); // Get second menu item in a menu page to start traversing (counting only visible ones)

// Iterate until end of menu items list is reached
while (menuItemTmp != nullptr) {
  menuItemTmp = menuItemTmp->getMenuItemNext(); // Get pointer to next menu item, including hidden ones
  // menuItemTmp = menuItemTmp->getMenuItemNext(GEM_ITEMS_VISIBLE); // Get pointer to next menu item, including only visible ones
  // Do something, e.g. check menu item title...
  const char* title = menuItemTmp->getTitle();
  // ...or callback data, associated with menu item
  GEMCallbackData callbackData = menuItemTmp->getCallbackData();
}

Related examples: Example 06: Todo List

Create button (basic)

To create menu button that will call certain user-defined function, you should first forward-declare this function before supplying it to menu item GEMItem constructor:

void printData();
GEMItem menuItemPrintButton("Print", printData);

Later in the sketch define this function:

void printData() {
  Serial.print("Number is: ");
  Serial.println(number);
}

Related examples: Example 01: Basic, Example 06: Todo List

Create button (with callback arguments)

Callback function (that will be called when button is pressed) optionally can expect argument of type GEMCallbackData to be passed to it when it is executed. In this case optional user-defined value of an argument can be specified. You should forward-declare callback function before supplying it to menu item GEMItem constructor:

void pressButton(GEMCallbackData callbackData);
GEMItem menuItemButton("Light Switch", pressButton, "light_switch_1"); // String "light_switch_1" is user-defined callback value

Later in the sketch define this function:

void pressButton(GEMCallbackData callbackData) {
  Serial.print("Button ID: ");
  Serial.print(callbackData.valChar); // Accessing user-defined callback argument
  Serial.print(" | Button title: ");
  Serial.println(callbackData.pMenuItem->getTitle()); // Accessing menu item title
}

Create button (advanced, with custom context)

To create menu button that will create new context (which can have its own enter (setup) and exit callbacks as well as loop function), you should first forward-declare initial function before supplying it to menu item GEMItem constructor:

void buttonAction();
GEMItem menuItemButton("Blink!", buttonAction);

Later in the sketch define this function:

void buttonAction() {
  // Declaration of context functions
  menu.context.loop = buttonContextLoop;
  menu.context.enter = buttonContextEnter; // Optional
  menu.context.exit = buttonContextExit; // Optional
  // Call of context functions
  menu.context.enter();
  //menu.context.allowExit = false; // Set to false if required to manually exit
                                    // the context loop from within the loop itself
                                    // (default is true)
}

Then define all context-related functions (optional enter and exit callbacks, and loop function):

void buttonContextEnter() {
  // Running some user-defined routines that usually belongs to setup()
  // Clear LCD screen
  glcd.clearScreen();
  // Initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

void buttonContextLoop() {
  // Do something in the loop() function you would normally do in the sketch,
  // e.g. blink built in LED
  digitalWrite(LED_BUILTIN, HIGH);
  delay(500);
  digitalWrite(LED_BUILTIN, LOW);
  delay(500);
}

void buttonContextExit() {
  // Set GEM specific settings to their values
  menu.reInit();
  // Draw menu back to screen
  menu.drawMenu();
  // Clear context (assigns `nullptr` values to function pointers
  // of the `context` property of the `GEM` object
  // and resets `allowExit` flag to its default state)
  menu.clearContext();
}

To exit currently running context and return to menu, press button associated with GEM_KEY_CANCEL key (only if context.allowExit flag is set to its default value of true, otherwise you should handle exit from the loop manually and call context.exit() explicitly) - context.exit() callback will be called.

Related examples: Example 02: Blink, Example 03: Party Hard!

Create submenu

Create additional menu page and menu item linked to this page:

GEMPage menuPageSettings("Settings");
GEMItem menuItemMainSettings("Settings", menuPageSettings);

Add menu item to existing menu page, where you'd like the link to the submenu to appear, and specify parent menu page, so that menu would know what page to return to when exiting submenu (generally in setup() function of the sketch, before menu.drawMenu() is called):

menuPageMain.addMenuItem(menuItemMainSettings);
menuPageSettings.setParentMenuPage(menuPageMain);

Alternatively it is possible to specify parent menu page during submenu object creation (since GEM ver. 1.4.6):

GEMPage menuPageSettings("Settings", menuPageMain);

If GEMPage::setParentMenuPage() is called additional time, previously added parent menu page will be overridden with the newly passed one.

Related examples: Example 02: Blink, Example 05: Encoder, Example 06: Todo List

Chain operations

It is possible to chain calls to some of the common methods of GEM, GEM_u8g2, GEM_adafruit_gfx, GEMPage, GEMItem objects (since GEM ver. 1.4.6):

menuPageSettings
  .addMenuItem(menuItemInterval)
  .addMenuItem(menuItemLabel)
  .setParentMenuPage(menuPageMain);

menu
  .reInit()
  .drawMenu()
  .clearContext();

Add drawMenu() callback

It is possible to specify function that will be called at the end of each drawMenu() call of GEM, GEM_u8g2 or GEM_adafruit_gfx (including explicit calls in your sketch and implicit calls in GEM source code). This callback can be used to draw something on top of the menu, for example battery status icon in title row of the menu page (or any other graphics or text for that matter).

First, define sprites that you would like to draw. Use draw routines and sprite format of your selected graphics library (AltSerialGraphicLCD, U8g2 or Adafruit GFX). U8g2 used in the example below.

// Battery icons (in XBM format for U8g2)
#define batteryWidth  10
#define batteryHeight 6
static const unsigned char battery0Bits [] U8X8_PROGMEM = {
  0xFF, 0x01, 0x01, 0x01, 0x01, 0x03, 0x01, 0x03, 0x01, 0x01, 0xFF, 0x01
};
static const unsigned char battery1Bits [] U8X8_PROGMEM = {
  0xFF, 0x01, 0x01, 0x01, 0x05, 0x03, 0x05, 0x03, 0x01, 0x01, 0xFF, 0x01
};
static const unsigned char battery2Bits [] U8X8_PROGMEM = {
  0xFF, 0x01, 0x01, 0x01, 0x15, 0x03, 0x15, 0x03, 0x01, 0x01, 0xFF, 0x01
};
static const unsigned char battery3Bits [] U8X8_PROGMEM = {
  0xFF, 0x01, 0x01, 0x01, 0x55, 0x03, 0x55, 0x03, 0x01, 0x01, 0xFF, 0x01
};

// Array of battery icons
const unsigned char* batteryIcon[4] = {
	battery0Bits,
	battery1Bits,
	battery2Bits,
	battery3Bits
};

// Currently displayed icon (should be determined dynamically based on real battery status)
byte batteryLevel = 3;

Later in the sketch during menu setup set callback by calling setDrawMenuCallback() method of your menu object:

menu.setDrawMenuCallback(drawStatus);

And define its function:

void drawStatus() {
  // Draw battery icon in top right corner of the screen
  u8g2.drawXBMP((u8g2.getDisplayWidth() - batteryWidth) - 1, 0, batteryWidth, batteryHeight, batteryIcon[batteryLevel]);
}

This example can be enhanced by calling drawStatus() not only as a callback of drawMenu() method, but also by timer (so that battery icon updated more frequently) if necessary.

Note that for different versions of GEM this callback may be called different number of times: e.g. U8g2 version of GEM calls drawMenu() method more often than other versions do, especially when buffer modes _1 or _2 of U8g2 is enabled. Alternatively, Adafruit GFX and AltSerialGraphicLCD versions of GEM make use of partial updates of the screen, hence call to drawMenu() is less common. Keep that in mind when specifying callback function, and consider using U8g2 version of GEM with full buffer _F mode.

Related examples: test_62_gem-drawMenuCallback_u8g2_sh1106_encoder.ino (Wokwi)

Detect key presses with KeyDetector

KeyDetector library provides an easy way to detect single digital push-button presses (wired with pulldown resistor, so the HIGH means that the button is pressed). To make use of it, first install and include it into your sketch:

#include <KeyDetector.h>

Then define pins you want to detect signal on:

const byte downPin = 2;
const byte leftPin = 3;
const byte rightPin = 4;
const byte upPin = 5;
const byte cancelPin = 6;
const byte okPin = 7;

Create array of Key objects that will link GEM key identifiers with corresponding pin:

Key keys[] = {{GEM_KEY_UP, upPin}, {GEM_KEY_RIGHT, rightPin}, {GEM_KEY_DOWN, downPin}, {GEM_KEY_LEFT, leftPin}, {GEM_KEY_CANCEL, cancelPin}, {GEM_KEY_OK, okPin}};

Create KeyDetector object:

KeyDetector myKeyDetector(keys, sizeof(keys)/sizeof(Key));

To account for switch bounce effect of the buttons (if occur) you may want to specify debounceDelay (in ms) as the third argument to KeyDetector constructor:

KeyDetector myKeyDetector(keys, sizeof(keys)/sizeof(Key), 10);

Specify push-buttons pin modes in setup() routine:

pinMode(downPin, INPUT);
pinMode(leftPin, INPUT);
pinMode(rightPin, INPUT);
pinMode(upPin, INPUT);
pinMode(cancelPin, INPUT);
pinMode(okPin, INPUT);

Modify the loop() function to include key detection routines as follows:

void loop() {
  // If menu is ready to accept button press...
  if (menu.readyForKey()) {
    // ...detect key press using KeyDetector library
    myKeyDetector.detect();
    // Pass pressed button to menu
    // (pressed button ID is stored in trigger property of KeyDetector object)
    menu.registerKeyPress(myKeyDetector.trigger);
  }
}

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Detect key presses with U8g2

If you are using U8g2 version of GEM, you can utilize U8g2 library built-in key press event detection (although it is still possible to use other key detection methods as well). To use it, first supply u8g2.begin() method with pin numbers the push-buttons (wired with pullup resistor, so the LOW means that the button is pressed) are attached to:

u8g2.begin(/*Select/OK=*/ 7, /*Right/Next=*/ 4, /*Left/Prev=*/ 3, /*Up=*/ 5, /*Down=*/ 2, /*Home/Cancel=*/ 6);

Modify the loop() function to include press event detection through u8g2.getMenuEvent() method as follows:

void loop() {
  // If menu is ready to accept button press...
  if (menu.readyForKey()) {
    // ...detect key press using U8g2 library
    // and pass pressed button to menu
    menu.registerKeyPress(u8g2.getMenuEvent());
  }
}

Related examples: Example 01: Basic, Example 02: Blink, Example 03: Party Hard!

Control menu from your sketch

To make menu perform requested operation (navigate, enter or exit edit mode, save variable, etc.) call menu.registerKeyPress() method with one of the GEM key identifiers as an argument:

menu.registerKeyPress(GEM_KEY_NONE); // Do nothing

menu.registerKeyPress(GEM_KEY_UP); // Navigate up through the menu items list,
                                   // select next value of the digit/char
                                   // of editable variable, or previous option in select

menu.registerKeyPress(GEM_KEY_RIGHT); // Navigate through the link to another (child) menu page,
                                      // select next digit/char of editable variable,
                                      // execute code associated with button

menu.registerKeyPress(GEM_KEY_DOWN); // Navigate down through the menu items list,
                                     // select previous value of the digit/char
                                     // of editable variable, or next option in select

menu.registerKeyPress(GEM_KEY_LEFT); // Navigate through the Back button to the previous menu page,
                                     // select previous digit/char of editable variable

menu.registerKeyPress(GEM_KEY_CANCEL); // Navigate to the previous (parent) menu page,
                                       // exit edit mode without saving the variable,
                                       // exit context loop if allowed within context's settings

menu.registerKeyPress(GEM_KEY_OK); // Toggle bool menu item, enter edit mode
                                   // of the associated non-bool variable,
                                   // exit edit mode with saving the variable,
                                   // execute code associated with button

Check out an example of controlling menu with keyboard via Serial (e.g. Arduino Serial Monitor).

Control menu with rotary encoder

Refer to Example 05: Encoder for possible implementation. Using menu.invertKeysDuringEdit() method to invert order in which characters loop when editing char[17] or number variables may be beneficial to user experience, e.g. rotating knob clock-wise is generally associated with GEM_KEY_DOWN action during navigation through menu items, but in edit mode it seems more natural to increment a digit rather than to decrement it when performing the same clock-wise rotation.

Related examples: Example 05: Encoder, Example 06: Todo List

Disable floating-point support

It is possible to exclude support for editable float and double variables to save some space on your chip (up to 10% of program storage space on Arduino UNO R3). For that, locate file config.h that comes with the library, open it and comment out corresponding inclusion, i.e. change this line:

#include "config/support-float-edit.h"

to

// #include "config/support-float-edit.h"

Alternatively it is possible to disable floating-point support by defining GEM_DISABLE_FLOAT_EDIT flag. See Floating-point variables section of Readme for more details.

Note that option selects support float and double variables regardless of this setting.

Disable spinner support

It is possible to exclude support for spinner menu items to save some space on your chip. For that, locate file config.h that comes with the library, open it and comment out corresponding inclusion, i.e. change this line:

#include "config/support-spinner.h"

to

// #include "config/support-spinner.h"

Alternatively it is possible to disable spinner support by defining GEM_DISABLE_SPINNER flag. See GEMSpinner section of Readme for more details.

Inherit classes

It is possible to make derived classes by inheriting classes supplied with GEM. That way it is possible to extend functionality of GEM by adding your own custom methods for accessing (getting and setting) otherwise protected fields without the need to modify source code.

class GEMPageProxy : public GEMPage {
  public:
    using GEMPage::GEMPage;

    // Set currently selected (focused) menu item of the page by index (zero-based)
    void setCurrentItemNum(byte index) {
      currentItemNum = index;
    }
    
    // Get items count excluding hidden ones
    byte getItemsCount() {
      return itemsCount;
    }
    
    // Get items count incuding hidden ones
    byte getItemsCountTotal() {
      return itemsCountTotal;
    }
};

GEMPageProxy menuPageMain("Main Menu");

...

void foo() {
  // Get count of menu items on current page
  byte count = ((GEMPageProxy*)menu.getCurrentMenuPage())->getItemsCount();
  Serial.print("Items on current page: ");
  Serial.println(count);
  
  // Navigate to menu item with index 1
  ((GEMPageProxy*)menu.getCurrentMenuPage())->setCurrentItemNum(1);
}

Note that Advanced Mode is not required for simple inheritance described above.

Override methods in Advanced Mode

First, make sure that Advanced Mode is enabled (by edition of config.h or by defining GEM_ENABLE_ADVANCED_MODE flag). Then it will be possible to override some of library methods, potentially further expanding (or altering in other ways) GEM functionality without the need to modify source code.

class GEMProxy : public GEM_u8g2 {
  public:
    using GEM_u8g2::GEM_u8g2;

    GEM_u8g2& drawMenu() override {
      // Override drawMenu() code here
    }
};

GEMProxy menu(u8g2, GEM_POINTER_ROW, GEM_ITEMS_COUNT_AUTO);

Additional examples

Collection of additional examples and tricks:

Clone this wiki locally