Scrolling long Pebble menu items
This is a technical blog post. Warning: contains code.
We recently pushed version 1.2 of Evernote for the Pebble to the Pebble App Store. It is a minor release, with one bug fix, and one new feature.
The bug fix is related to support for the additional character sets that Pebble can now display.
The enhancement is what this blog post is about. Since we released the first version of the app, which was generally well received, we’ve received emails from people complaining that their note titles, notebook names, tag names etc. don’t fit on the Pebble screen. They are cut off, and hard to read. People asked if we could make menu items scroll horizontally if they didn’t fit.
My response was generally something along the lines of “sorry, but we use the Pebble’s built-in menuing system, and until they support scrolling menu items horizontally, we can’t do anything”. I never felt great about this response, but it was the genuine situation. However before I pushed the 1.2 release with the character-set bug-fix, I thought I’d take a look at scrolling the menu items. Turns out, it was surprisingly easy.
You can see what I’m talking about here:
The funny thing about the Evernote Pebble watch app is that it knows almost nothing about Evernote. The Evernote intelligence is all delegated to the companion app that runs on the Phone. The watch app knows how to display massive menus (paging items in and out as necessary), checkboxes, images, text etc.
When the user scrolls to a new menu item, we kick off a wait timer using app_timer_register waiting for one second. If the user scrolls to another menu item before the timer has expired, we wait for a new second, this time using app_timer_reschedule:
static void selection_changed_callback(Layer *cell_layer, MenuIndex new_index, MenuIndex old_index,
void *data) {
WindowData* window_data = (WindowData*)data;
window_data->moving_forwards_in_menu = new_index.row >= old_index.row;
if(!window_data->menu_reloading_to_scroll) {
initiate_menu_scroll_timer(window_data);
} else {
window_data->menu_reloading_to_scroll = false;
}
}
The above method is called by the Pebble framework when the user scrolls to a new menu item. The check for menu_reloading_to_scroll is called to work around some behavior I’ve seen. This callback invokes the following method:
static void initiate_menu_scroll_timer(WindowData* window_data) {
// If there is already a timer then reschedule it, otherwise create one
bool need_to_create_timer = true;
window_data->scrolling_still_required = true;
window_data->menu_scroll_offset = 0;
window_data->menu_reloading_to_scroll = false;
if(window_data->menu_scroll_timer) {
// APP_LOG(APP_LOG_LEVEL_DEBUG, "Rescheduling timer");
need_to_create_timer = !app_timer_reschedule(window_data->menu_scroll_timer,
SCROLL_MENU_ITEM_WAIT_TIMER);
}
if(need_to_create_timer) {
// APP_LOG(APP_LOG_LEVEL_DEBUG, "Creating timer");
window_data->menu_scroll_timer = app_timer_register(SCROLL_MENU_ITEM_WAIT_TIMER,
scroll_menu_callback, window_data);
}
}
As you can see it uses a WindowsData structure, which is a custom structure associated with the current window via window_set_user_data. Once the timer expires it calls scroll_menu_callback:
static void scroll_menu_callback(void* data) {
WindowData* window_data = (WindowData*)data;
if(!window_data->menu) {
return;
}
window_data->menu_scroll_timer = NULL;
window_data->menu_scroll_offset++;
if(!window_data->scrolling_still_required) {
return;
}
// Redraw the menu with this scroll offset
MenuIndex menuIndex = menu_layer_get_selected_index(window_data->menu);
if(menuIndex.row != 0) {
window_data->menu_reloading_to_scroll = true;
}
window_data->scrolling_still_required = false;
menu_layer_reload_data(window_data->menu);
window_data->menu_scroll_timer = app_timer_register(SCROLL_MENU_ITEM_TIMER, scroll_menu_callback,
window_data);
}
This code is called once when the timer initiated by initiate_scroll_menu_timer expires (after the one second delay), and then it invokes itself repeatedly using a shorter delay (a fifth of a second), until the menu item is fully scrolled. The call to menu_layer_reload_data is what causes the menu to be redrawn, using the menu_scroll_offset to indicate how much to scroll the text by.
This is the method that gets called by the draw_row_callback to get the text to be displayed for each menu item:
void get_menu_text(WindowData* window_data, int index, char** text, char** subtext) {
MenuItem* menu_item = getMenuItem(window_data, index);
*text = menu_item ? menu_item->text : NULL;
*subtext = menu_item && menu_item->flags & ITEM_FLAG_TWO_LINER ?
menu_item->text + strlen(menu_item->text) + 1 : NULL;
if(*subtext != NULL && strlen(*subtext) == 0) {
*subtext = NULL;
}
MenuIndex menuIndex = menu_layer_get_selected_index(window_data->menu);
if(*text && menuIndex.row == index) {
int len = strlen(*text);
if(len - MENU_CHARS_VISIBLE - window_data->menu_scroll_offset > 0) {
*text += window_data->menu_scroll_offset;
window_data->scrolling_still_required = true;
}
}
}
The last if
code “scrolls” the text if the row corresponds to the currently selected item by indexing into the text to be displayed, and indicating that scrolling is still required. I’m not happy with using the fixed size MENU_CHARS_VISIBLE to decide whether or not to scroll – it would be much nicer to measure the text and see if it fits. If you know of a simple way to do this please comment!
The final thing I needed to do was to actually send longer menu item text from the phone to the watch. Since Pebble now support sending more than 120 or so bytes this was much easier. I’m sending up to 32 characters now.
In summary I’m simply using a timer to redisplay the menu, each time scrolling the current menu item’s text by indexing into the character array, and I stop the timer once it has all been displayed.