Pebble SDK 2.0 Tutorial #7: MenuLayers

Required Reading

Pebble SDK 2.0 Tutorial #1: Your First Watchapp

Pebble SDK 2.0 Tutorial #2: Telling the Time

Pebble SDK 2.0 Tutorial #3: Images and Fonts

Pebble SDK 2.0 Tutorial #4: Animations and Timers

Pebble SDK 2.0 Tutorial #5: Buttons and Vibrations

Pebble SDK 2.0 Tutorial #6: AppMessage for PebbleKit JS

Introduction

After a few requests, in this section we will look at using MenuLayers in a Pebble watchapp. If you pick up your Pebble now and press the select button from the watch face, what you see is a MenuLayer. It has rows, icons and actions. Let’s build one of those!

pebble-screenshot_2014-03-13_00-22-47

Setup

The first step as usual is to start a new CloudPebble project with the basic app template. Here’s that again, for convenience:

#include <pebble.h>

Window* window;

void window_load(Window *window)
{

}

void window_unload(Window *window)
{

}

void init()
{
	window = window_create();
	WindowHandlers handlers = {
		.load = window_load,
		.unload = window_unload
	};
	window_set_window_handlers(window, (WindowHandlers) handlers);
	window_stack_push(window, true);
}

void deinit()
{
	window_destroy(window);
}

int main(void)
{
	init();
	app_event_loop();
	deinit();
}

Now that’s out the way, declare a global pointer to a MenuLayer at the top of the file below the pre-processor directives.

MenuLayer *menu_layer;

This Layer type is a bit more complex to set up than the other Layers, in that it requires a large amount of information about how it will look and behave before it can be instantiated. This information is given to the MenuLayer via the use of a number of callbacks. When the MenuLayer is redrawn or reloaded, it calls these functions to get the relevant data. The advantage of this approach is that the MenuLayer rows can be filled with data that can be changed at any time, such as with Wristponder or Pebble Tube Status (shameless plugs!)

The API documentation describes all the possible MenuLayerCallbacks that can be associated with a MenuLayer, but the ones we will be using for a simple example will be:

  • .draw_row – This is used to draw the layout inside a menu item
  • .get_num_rows – This is used to feedback the total number of rows in the MenuLayer. This can be a #defined value, or an int, and so variable
  • .select_click – This is used to decide what happens when the select button is pressed, which will vary depending on which row is currently selected

Let’s define these callbacks using the signatures provided by the API documentation linked previously. These must be above window_load() as is now the norm (hopefully!):

void draw_row_callback(GContext *ctx, Layer *cell_layer, MenuIndex *cell_index, void *callback_context)
{

}

uint16_t num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *callback_context)
{

}

void select_click_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context)
{

}

Now those are in place, let’s add code to have them do something we’d find more useful than blank callbacks. The example we are going to use is a list of fruits (boring, I know!). The list will be of seven fruits, and brief descriptions. Thus, the num_rows_callback() function becomes simply:

uint16_t num_rows_callback(MenuLayer *menu_layer, uint16_t section_index, void *callback_context)
{
	return 7;
}

For the draw_row_handler(), we will need to be able to alter what is drawn in the row depending on which row it is. This can be done by switching the cell_index->row property. You can use the presented GContext however you like for any of the SDK drawing functions, but to keep things simple we will use the pre-made drawing functions provided by the SDK. With these two last points combined, the draw_row_callback() function transforms into this beast:

void draw_row_callback(GContext *ctx, Layer *cell_layer, MenuIndex *cell_index, void *callback_context)
{
	//Which row is it?
	switch(cell_index->row)
	{
	case 0:
		menu_cell_basic_draw(ctx, cell_layer, "1. Apple", "Green and crispy!", NULL);
		break;
	case 1:
		menu_cell_basic_draw(ctx, cell_layer, "2. Orange", "Peel first!", NULL);
		break;
	case 2:
		menu_cell_basic_draw(ctx, cell_layer, "3. Pear", "Teardrop shaped!", NULL);
		break;
	case 3:
		menu_cell_basic_draw(ctx, cell_layer, "4. Banana", "Can be a gun!", NULL);
		break;
	case 4:
		menu_cell_basic_draw(ctx, cell_layer, "5. Tomato", "Extremely versatile!", NULL);
		break;
	case 5:
		menu_cell_basic_draw(ctx, cell_layer, "6. Grape", "Bunches of 'em!", NULL);
		break;
	case 6:
		menu_cell_basic_draw(ctx, cell_layer, "7. Melon", "Only three left!", NULL);
		break;
	}
}

The NULL references are in the place that a row icon reference would be placed (if a GBitmap were to be shown). Thus, each layer will be drawn with its own unique message.

The final callback, select_click_callback() will do something different depending on which row is selected when the select button is pressed. To illustrate this, we will use a series of vibrations that signifies the numerical value of the row. Here’s how this is done (or Vibes 101!):

void select_click_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context)
{
	//Get which row
	int which = cell_index->row;

	//The array that will hold the on/off vibration times
	uint32_t segments[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

	//Build the pattern (milliseconds on and off in alternating positions)
	for(int i = 0; i < which + 1; i++)
	{
		segments[2 * i] = 200;
		segments[(2 * i) + 1] = 100;
	}

	//Create a VibePattern data structure
	VibePattern pattern = {
		.durations = segments,
		.num_segments = 16
	};

	//Do the vibration pattern!
	vibes_enqueue_custom_pattern(pattern);
}

With those three callbacks in place, we can actually create the MenuLayer and add it to the main Window. This is done in four stages:

  • Create the MenuLayer and assign it to the global pointer
  • Set it up to receive clicks from the Window
  • Set the callbacks we just wrote to give the MenuLayer the information it needs
  • Add the MenuLayer to the main Window

Here’s the code for that sequence, with annotations (Note the casts used in the MenuLayerCallbacks structure creation):

void window_load(Window *window)
{
	//Create it - 12 is approx height of the top bar
	menu_layer = menu_layer_create(GRect(0, 0, 144, 168 - 16));

	//Let it receive clicks
	menu_layer_set_click_config_onto_window(menu_layer, window);

	//Give it its callbacks
	MenuLayerCallbacks callbacks = {
		.draw_row = (MenuLayerDrawRowCallback) draw_row_callback,
		.get_num_rows = (MenuLayerGetNumberOfRowsInSectionsCallback) num_rows_callback,
		.select_click = (MenuLayerSelectCallback) select_click_callback
	};
	menu_layer_set_callbacks(menu_layer, NULL, callbacks);

	//Add to Window
	layer_add_child(window_get_root_layer(window), menu_layer_get_layer(menu_layer));
}

As always, de-init the MenuLayer:

void window_unload(Window *window)
{
	menu_layer_destroy(menu_layer);
}

If all has gone well, after compilation you should be greeted with the screen below, as well as the corresponding vibrations when each row is selected:

pebble-screenshot_2014-03-13_01-27-12

Conclusions
So that’s how to setup a basic MenuLayer. An extended application like those mentioned previously will use char[] buffers to store each row’s text, modified in a in_received signature AppMessage callback, and calling menu_layer_reload_data() in that AppMessage callback, thus updating the MenuLayer with the new data.

The source code can be found on GitHub HERE!

Let me know any queries you have. Enjoy!

Advertisements
25 comments
  1. Thanks again for your new Tutorial, it’s great and simple to understand.
    I am confused whether to use select_click_callback() or select_click_handler() to select an option. In this new tutorial it seems very clear, should I change to select_click_callback().
    And how is the process to send a index row to the Javascript framework?

    • bonsitm said:

      Thanks. It can be select_click_callback() or select_click_handler() or cheese_banana_car() as long as it is the same in both the declaration and the MenuLayerCallbacks structure. It’s a naming convention that I changed slightly to be more in line with the docs on this one.

      Re: sending a row number to the JS: The cell_index->row property is an integer, so setup AppMessage and use a function such as my send_int() as in Tutorial #6: AppMessage for PebbleKit JS.

      Good luck!

  2. Cris, I see that there has been a change with the void select_click_callback(MenuLayer *menu_layer, MenuIndex *cell_index, void *callback_context)
    apparently, is this the correct way? the select_click_handler(ClickRecognizerRef recognizer, void *context) has a very different structure.

    • bonsitm said:

      Either of these will work, however the correct one is the one containing the MenuIndex parameter.

  3. I want to know if I can send the option contained in the row rather than the number of the row.

    • bonsitm said:

      If you represent each option as an integer, sure!

    • Not as such, but usually you’re getting that data from somewhere, so you can query an array with the index, send an AppMessage and get the corresponding result, etc.

  4. And if I want to construct a dictionary (with the strings) to send back to the Pebblekit Framework, should I created the same way I did it for the reception?

  5. Cris, but is there a way to fetch the string contained in the subtitle of the menu selected to then pass into the tuple?

    • bonsitm said:

      Sure, if you know which buffer is drawn in each row in the draw_row_handler(), then simply send the same one back to the server. I can’t say any more without specialist knowledge of your system, though.

    • Better use SimpleMenuLayer for that use case. With a SimpleMenuItem array you can simply use my_menu_items[index].title resp. .text.
      (Hint: Don’t forget to free the strings when the menu/window is removed!)

  6. or can you give and suggestion about how to transform the integer row into the string option that I had contained in my json string to send back my selection to the server?

  7. A general hint: You wrote “These must be above window_load() as is now the norm (hopefully!)”.
    This can be avoided when using headers. C just needs to know the function signature before it’s invoked. This can be done by implementing the function before it’s invoked, but the more elegant way is to define it in a header and include it.
    This also allows to split your code into multiple files, keep together functions that belong together, and so on.
    See e.g. http://www.tutorialspoint.com/cprogramming/c_header_files.htm (just the first useful link from Google…).

    • bonsitm said:

      Great tips, thanks. I have done this in a couple of larger projects that became too unweildly to manage in one source file. Perhaps this would make a good item for a future ‘large project best practices’ tutorial section?

  8. Sijt said:

    Hey very helpful but how can make it go to a new screen when I click on a menu?

    • bonsitm said:

      Hi Sijt,

      Simply make and push a new Window object when the menu’s select click handler is invoked! Hope this helps.

  9. Michael said:

    Hey Chris!

    I’m loving your tutorials. I can code, but the documentation simply doesn’t cut it when it comes to figuring out what stuff does and how to use it (I’m not very experienced in C).

    And that’s the cause of my current problem – I’m trying to code an app for Basalt but I just can’t figure out how to set menu colours… The documentation says to call menu_layer_set_normal_colors explicitly on the menulayer… Whaa?

    If you could help it would be great. You’re an amazing dev, truly a gift to the community!

    • bonsitm said:

      Michael, thanks for the kind words.

      It looks like you’re almost there – after creating your MenuLayer:

      menu_layer = menu_layer_create(bounds);

      Use menu_layer_set_normal_colors or menu_layer_set_highlight_colors to choose the foreground (text) and background color in those situations. If you get compiler errors, check that you have this code disabled when building for Aplite, as these functions do not exist there. Use #ifdef PBL_SDK_3:

      #ifdef PBL_SDK_3
      menu_layer_set_normal_colors(menu_layer, GColorWhite, GColorBlack);
      #endif

      Re:documentation – I’m now somewhat responsible for maintaining the Pebble Developer docs, so let me know if you see anything that badly needs improving. If you want a more wordy explanation of key concepts, look at the Guides section!

      • Michael said:

        Thanks! There’s nothing wrong with the documentation, I’m just not used to C yet so it doesn’t make sense. It probably will once I get into it properly.

        Thanks for helping me there – I was already using #ifdef pbl_color (I got that) but I wasn’t using menu_layer_create(bounds). Your quick response has helped me a lot!

  10. John R said:

    How do I do a subtitle/header? I can’t figure out how to use the callback without it just adding a third blank box to the menu. I already tried doing the same thing as the normal rows. Thanks.

    • bonsitm said:

      Hi John,

      When using menu_cell_basic_draw, the second char* argument will be the subtitle for each item, if it is not NULL.

      As for headers, make sure you include a MenuLayerGetHeaderHeightCallback and a MenuLayerDrawHeaderCallback in your MenuLayerCallbacks. In these callbacks you can draw your header for each section, if the height returned by the former is > 0.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: