Dynamic menus

I am loading a bunch of yacht racing mark details: name, lat, lon, from my web site and want user to select one from a list.
I am able to dynamically load them into a menu, but can't fathom how to determine which one they've selected.
The onMenuItem(Item) faithfully returns the symbol, but I can't see a way to dynamically generate the item's id when loading the menu.

The web site returns a JSON array of dictionaries in loadedMarks which I use to build the menu:
var marksMenu=new Menu();
for (var i = 0; i < loadedMarks.size(); i++) {
marksMenu.addItem(loadedMarks["name"],:selected);
}
Ui.pushView(marksMenu, new MenuTestMenuDelegate(), Ui.SLIDE_UP);
[/code]
This of course doesn't work because it allocates the same id (symbol :selected) to each item. I'm looking for a way to identify which item he selected in my onMenuItem(Item) callback.


I note that the spec says "...This class should be extended to get the chosen Menu item." and wonder if the documentation guy had my problem in mind!
I also noted somewhere in the programmers guide that menus should not be created programatically, but what else is a poor boy to do?
  • I've always done this...

    // for use with dynamic menus. limited to 16 entries because of Menu.MAX_SIZE
    const _symbols = [
    :symbol0,
    :symbol1,
    :symbol2,
    :symbol3,
    :symbol4,
    :symbol5,
    :symbol6,
    :symbol7,
    :symbol8,
    :symbol9,
    :symbol10,
    :symbol11,
    :symbol12,
    :symbol13,
    :symbol14,
    :symbol15
    ];

    class XMarksMenu extends Ui.Menu
    {
    function initialize(marks) {
    Menu.initialize();

    var n = marks.size();
    if (Menu.MAX_SIZE < n) {
    n = Menu.MAX_SIZE;
    }

    for (var i = 0; i < n; ++i) {
    Menu.addItem(marks["name"], _symbols);
    }
    }
    }

    class XMarksMenuDelegate extends Ui.MenuInputDelegate
    {
    hidden var _M_marks;
    hidden var _M_callback;

    function initialize(marks, callback) {
    MenuInputDelegate.initialize();
    _M_marks = marks;
    _M_callback = callback;
    }

    function onMenuItem(item) {
    for (var i = 0; i < _symbols.size(); ++i) {
    if (symbols== item) {
    return _M_callback.invoke(_M_marks);
    }
    }

    return false;
    }
    }

    //
    // var menu = new XMarksMenu(marks);
    // var delegate = new XMarksMenuDelegate(marks, self.method(:onMarkSelected));
    // Ui.pushView(menu, delegate, Ui.SLIDE_UP);
    //
    [/code]

    I don't really like it because it relies on a global, but I don't really see any other way to do it. Maybe someone else will have a better suggestion..

    Travis
  • Former Member
    Former Member over 8 years ago
    I'm pretty sure you can use anything for the id of a menu. I glanced at the API code and don't see any type enforcement on that parameter. You will just get passed back what you provide, so you can do this:

    for (var i = 0; i < n; ++i) {
    Menu.addItem(marks["name"],i);
    }[/CODE]

    or I suppose probably even this:

    for (var i = 0; i < n; ++i) {
    Menu.addItem(marks["name"],marks["name"]);
    }[/CODE]
  • I'm pretty sure you can use anything for the id of a menu.

    It is weird. The issue is that the value passed to onMenuItem() is always a Lang.Symbol regardless of what is passed into the addItem() call. The type is completely lost. Interestingly enough, the symbol it can successfully be used in comparisons using the equals operator (as opposed to the equals() method) to find the matching object...

    using Toybox.Application as App;
    using Toybox.Lang as Lang;
    using Toybox.System as Sys;
    using Toybox.WatchUi as Ui;

    class XMenu extends Ui.Menu
    {
    function initialize() {
    Menu.initialize();
    }
    }

    class XMenuInputDelegate extends Ui.MenuInputDelegate
    {
    hidden var _M_callback;

    function initialize(callback) {
    MenuInputDelegate.initialize();
    _M_callback = callback;
    }

    function onMenuItem(item) {
    return _M_callback.invoke(item);
    }
    }

    var _symbol = :symbol;
    var _number = 1;
    var _long = 37000l;
    var _float = 8.0;
    var _double = 10.0d;
    var _string = "abc";
    var _array = [ 1, "2" ];
    var _dict = { 1 => "2" };

    class XBehaviorDelegate extends Ui.BehaviorDelegate
    {
    function initialize() {
    BehaviorDelegate.initialize();
    }

    function onMenu() {
    var menu = new XMenu();

    menu.addItem("Symbol", _symbol);
    menu.addItem("Number", _number);
    menu.addItem("Long", _long);
    menu.addItem("Float", _float);
    menu.addItem("Double", _double);
    menu.addItem("String", _string);
    menu.addItem("Array", _array);
    menu.addItem("Dictionary", _dict);

    var delegate = new XMenuInputDelegate(self.method(:onMenuItem));

    Ui.pushView(menu, delegate, Ui.SLIDE_UP);

    return true;
    }

    function onMenuItem(item) {

    if (item instanceof Lang.Symbol) {
    Sys.print(item);
    }
    else if (item instanceof Lang.Number) {
    Sys.print(Lang.format("Number: $1$", [ item ]));
    }
    else if (item instanceof Lang.Long) {
    Sys.print(Lang.format("Long: $1$", [ item ]));
    }
    else if (item instanceof Lang.Float) {
    Sys.print(Lang.format("Float: $1$", [ item ]));
    }
    else if (item instanceof Lang.Double) {
    Sys.print(Lang.format("Double: $1$", [ item ]));
    }
    else if (item instanceof Lang.String) {
    Sys.print(Lang.format("String: $1$", [ item ]));
    }
    else if (item instanceof Lang.String) {
    Sys.print(Lang.format("String: $1$", [ item ]));
    }
    else if (item instanceof Lang.Array) {
    Sys.print(Lang.format("Array: $1$", [ item ]));
    }
    else if (item instanceof Lang.Dictionary) {
    Sys.print(Lang.format("Dictionary: $1$", [ item ]));
    }
    else {
    Sys.print(Lang.format("Unknown: $1$", [ item ]));
    }

    if (_symbol.equals(item)) {
    Sys.println(" match for symbol");
    }
    else if (_number.equals(item)) {
    Sys.println(" _number.equals(item)");
    }
    else if (_number == item) {
    Sys.println(" _number == item");
    }
    else if (_long.equals(item)) {
    Sys.println(" _long.equals(item)");
    }
    else if (_long == item) {
    Sys.println(" _long == item");
    }
    else if (_float.equals(item)) {
    Sys.println(" _float.equals(item)");
    }
    else if (_float == item) {
    Sys.println(" _float == item");
    }
    else if (_double.equals(item)) {
    Sys.println(" _double.equals(item)");
    }
    else if (_double == item) {
    Sys.println(" _double == item");
    }
    else if (_string.equals(item)) {
    Sys.println(" _string.equals(item)");
    }
    else if (_string == item) {
    Sys.println(" _string == item");
    }
    else if (_array.equals(item)) {
    Sys.println(" _array.equals(item)");
    }
    else if (_array == item) {
    Sys.println(" _array == item");
    }
    else if (_dict.equals(item)) {
    Sys.println(" _dict.equals(item)");
    }
    else if (_dict == item) {
    Sys.println(" _dict == item");
    }
    else {
    Sys.println(" unknown value");
    }
    }
    }

    class XView extends Ui.View
    {
    function initialize() {
    View.initialize();
    }
    }

    class XApp extends App.AppBase
    {
    function initialize() {
    AppBase.initialize();
    }

    function getInitialView() {
    return [ new XView(), new XBehaviorDelegate() ];
    }
    }


    If I run that testcase and select each menu item in order, I see the following output...

    Device Version 0.1.0
    Device id 1 name "A garmin device"
    Shell Version 0.1.0
    symbol (8389891) match for symbol
    symbol (1) _number == item
    symbol (16) _long == item
    symbol (1090519040) _float == item
    symbol (17) _double == item
    symbol (15) _string == item
    symbol (18) _array == item
    symbol (20) _dict == item
    Complete
    Connection Finished
    Closing shell and port


    If you have a situation like the initial post and you make a dynamic menu, it gets weird.

    using Toybox.Application as App;
    using Toybox.Lang as Lang;
    using Toybox.System as Sys;
    using Toybox.WatchUi as Ui;

    class XMenu extends Ui.Menu
    {
    function initialize() {
    Menu.initialize();
    }
    }

    class XMenuInputDelegate extends Ui.MenuInputDelegate
    {
    hidden var _M_callback;

    function initialize(callback) {
    MenuInputDelegate.initialize();
    _M_callback = callback;
    }

    function onMenuItem(item) {
    return _M_callback.invoke(item);
    }
    }

    var _array = [
    { "name" => "A", "lat" => 1.0, "lon" => 1.0 },
    { "name" => "B", "lat" => 2.0, "lon" => 2.0 },
    { "name" => "C", "lat" => 3.0, "lon" => 3.0 }
    ];

    class XBehaviorDelegate extends Ui.BehaviorDelegate
    {
    function initialize() {
    BehaviorDelegate.initialize();
    }

    function onMenu() {
    var menu = new XMenu();

    for (var i = 0; i < _array.size(); ++i) {
    menu.addItem(_array["name"], _array);
    }

    var delegate = new XMenuInputDelegate(self.method(:onMenuItem));

    Ui.pushView(menu, delegate, Ui.SLIDE_UP);

    return true;
    }

    function onMenuItem(item) {
    Sys.println(item["name"]);
    }
    }

    class XView extends Ui.View
    {
    function initialize() {
    View.initialize();
    }
    }

    class XApp extends App.AppBase
    {
    function initialize() {
    AppBase.initialize();
    }

    function getInitialView() {
    return [ new XView(), new XBehaviorDelegate() ];
    }
    }
    [/code]

    The problem is that you can't treat item as if it were a Lang.Dictionary (the type of the object passed in) inside onMenuItem(), because the type has been lost. You can work around this by searching the original array for the matching item (using the equals operator), and when you find a match you can access the item from the original array...

    function onMenuItem(item) {
    // Sys.println(item["name"]); // UnexpectedTypeException: Expected Object/Array/Dictionary, given Symbol

    // this works...
    for (var i = 0; i < _array.size(); ++i) {
    if (_array== item) {
    Sys.println(_array["name"]);
    }
    }
    }
    [/code]

    This is just as ugly as my original suggestion (using a lookup table of symbols) because the data has to be shared between the view and the delegate.

    As you've pointed out, you can pass a Lang.Number as the second parameter to addItem(), but that still has the same problem... you need to do the same lookup trick to avoid the UnexpectedTypeException you get from passing a Lang.Symbol to the array index operator.

    Travis
  • Former Member
    Former Member over 8 years ago
    There must be something in the code I missed when I looked through that is converting the parameter to a symbol.

    Sorry for the misinformation.
  • Thanks for the work guys. Travis, you have done a much more thorough exploration of the issue than I could, and have hit the nail on the head. Incidentally, I opted for your array of symbols solution while waiting for an answer. You have also alerted me to a potential user challenge with the Menu.MAX_SIZE which I hadn't spotted.
    Some thoughts on the Menu:
    • The emulator displays a menu header, but the VA-HR doesn't.
    • The emulator doesn't respond reliably to a menu click.
    • It would be a great enhancement if the onMenuItem could return the passed object, not limited to a symbol.
  • The menu is the simulator is just a generic menu, as the menu on the actual devices is device dependant. On the real vivoactive, for example, each menu item is a full screen and you left/right swipe to switch between screens, but looks like the menu on other devices in the sim.
    The menu title not being shown on the va-hr native menu, is another example of a generic vs device specific thing.

    As far as #2, with the generic menu with the va-hr, the current item is the one in the middle, and if you tap on that (for touch devices), it's picked. If you tap on the one under the middle one, it moves up to be the middle one. That's just the generic menu thing again.

    You'll also see a "generic" version of things like the NumberPicker and TextPicker in the sim.
  • Former Member
    Former Member over 8 years ago
    It would be a great enhancement if the onMenuItem could return the passed object, not limited to a symbol.


    I've created a request ticket for this.
  • Copied all that. Thanks for the great feedback and explanations.
  • It would be a great enhancement if the onMenuItem could return the passed object, not limited to a symbol.

    Agreed. In the meantime, you could use this. It is a generic menu implementation that keeps all of the menu logic in the menu itself, and calls back to your code with an object reference. You supply an array (or a dictionary with integer keys from 0) of objects, and the name of the field you want to use for display purposes.

    // for use with dynamic menus. limited to 16 entries because of Menu.MAX_SIZE
    const _symbols = [
    :symbol0,
    :symbol1,
    :symbol2,
    :symbol3,
    :symbol4,
    :symbol5,
    :symbol6,
    :symbol7,
    :symbol8,
    :symbol9,
    :symbol10,
    :symbol11,
    :symbol12,
    :symbol13,
    :symbol14,
    :symbol15
    ];

    class XMenuOverflowException extends Lang.Exception
    {
    function initialize() {
    Exception.initialize();
    }

    function getErrorMessage() {
    return "Too many menu items";
    }
    }

    class XMenu extends Ui.Menu
    {
    // could also use a callback function to get the display string
    // given an object, but this seems simpler and covers the bases..

    function initialize(objects, field) {
    Menu.initialize();

    var n = objects.size();
    if (MAX_SIZE < n) {
    throw new XMenuOverflowException();
    }

    for (var i = 0; i < n; ++i) {
    Menu.addItem(objects[field], _symbols);
    }
    }
    }

    class XMenuInputDelegate extends Ui.MenuInputDelegate
    {
    hidden var _M_objects;
    hidden var _M_callback;

    function initialize(objects, callback) {
    MenuInputDelegate.initialize();
    _M_objects = objects;
    _M_callback = callback;
    }

    function onMenuItem(item) {
    for (var i = 0; i < _symbols.size(); ++i) {
    if (_symbols== item) {
    return _M_callback.invoke(_M_objects);
    }
    }

    return false;
    }
    }
    [/code]

    In your case, where you want to show a menu that uses the name in each map entry, you'd use this...

    // assume this is the input data
    var _array = [
    { "name" => "A", "lat" => 1.0, "lon" => 1.0 },
    { "name" => "B", "lat" => 2.0, "lon" => 2.0 },
    { "name" => "C", "lat" => 3.0, "lon" => 3.0 }
    ];

    // show field by the name of the object
    var menu = new XMenu(_array, "name");
    var delegate = new XMenuInputDelegate(_array, self.method(:onMenuItem));
    Ui.pushView(menu, delegate, Ui.SLIDE_UP);


    This trick can work with arrays of objects as well as arrays of dictionaries. You'd just have to specify :name as the field parameter so the XMenu would access the name member of the object. You could do arrays of arrays as well by specifying the index.

    Travis
  • At least in the simulator, if you pass a number for the key, it does get converted in to a Symbol. But if you get the hashCode from that symbol, it is the original number. Not sure if you can really guarantee this, or if it works with any/all the devices. I'll do some checking.

    //Delegate
    function onMenu()
    {
    ...
    menu.addItem("item",45)
    ...
    }

    //MenuDelegate
    function onMenuItem(item)
    {
    var iItem = item.hashCode(); //gives 45.
    ...
    }