How to get out of nested menus

I'm trying to use the built-in menus and confirmation dialog to implement resume/save/discard functionality. I have this working for the most part, but I do have one issue. If, when the resume/save/discard menu is presented, the user selects Discard, and when the confirmation dialog is presented, they select Yes, I want to get out of the menu completely. Technically, I want to exit the application.

In the simulator, calling Ui.popView(Ui.SLIDE_IMMEDIATE) for every menu in the stack seems to work just fine. On the device (a fr920xt) the additional popView() calls don't appear to do anything (when confirming the discard you end up at the resume/save/discard menu).

Does anyone know how to handle this? Am I trying to work around a ConnectIQ bug specific to the 920? Is what I'm trying to do just not supported by the menus?

Travis
  • In case it helps to see actual code, here is a pseudo-activity recording app that has been reduced down to a simple view and the resume/save/discard flow.

    Travis

    using Toybox.WatchUi as Ui;
    using Toybox.Graphics as Gfx;
    using Toybox.Timer as Timer;
    using Toybox.System as Sys;
    using Toybox.WatchUi as Ui;

    var state = "Press Enter";

    class ProgressView extends Ui.ProgressBar
    {
    hidden var _timer;
    hidden var _ticks;

    hidden var _complete;

    function initialize(start, complete) {
    Sys.println("DiscardProgressView.onTimer");

    Ui.ProgressBar.initialize(start, null);
    _complete = complete;

    _timer = new Timer.Timer();
    _timer.start(self.method(:onTimer), 1000, true);

    _ticks = 0;
    }

    function onTimer() {
    Sys.println("DiscardProgressView.onTimer");

    _ticks += 1;
    if (_ticks == 2) {
    setDisplayString(_complete);
    setProgress(100);
    }
    else if (_ticks == 3) {
    _timer.stop();

    // pop the progress bar
    Ui.popView(Ui.SLIDE_IMMEDIATE);

    // pop the save menu
    Ui.popView(Ui.SLIDE_IMMEDIATE);

    // pop the main view
    Ui.popView(Ui.SLIDE_IMMEDIATE);
    }
    }
    }

    class DiscardConfirmationDelegate extends Ui.ConfirmationDelegate
    {
    function onResponse(response) {
    Sys.println("DiscardConfirmationDelegate.initialize");

    if (response == CONFIRM_YES) {

    state = "Discarded";

    var view = new ProgressView("Discarding", "Activity Dicarded");
    Ui.pushView(view, null, Ui.SLIDE_IMMEDIATE);

    return true;
    }
    else {
    return true;
    }
    }
    }


    class SaveMenuDelegate extends Ui.MenuInputDelegate
    {
    function initialize() {
    Sys.println("SaveMenuDelegate.initialize");
    }

    function onMenuItem(item) {
    Sys.println("SaveMenuDelegate.onMenuItem");

    if (item == :save) {
    save();
    }
    else if (item == :discard) {
    discard();
    }
    else if (item == :resume) {
    resume();
    }

    return true;
    }

    function resume() {
    Sys.println("SaveMenuDelegate.resume");

    state = "Recording";

    Ui.requestUpdate();
    }

    function save() {
    Sys.println("SaveMenuDelegate.save");

    state = "Saved";

    var view = new ProgressView("Saving", "Activity Saved");
    Ui.pushView(view, null, Ui.SLIDE_IMMEDIATE);
    }

    function discard() {
    Sys.println("SaveMenuDelegate.discard");

    var view = new Ui.Confirmation("Discard?");
    var delegate = new DiscardConfirmationDelegate();

    Ui.pushView(view, delegate, Ui.SLIDE_IMMEDIATE);
    }
    }


    class TestDelegate extends Ui.InputDelegate
    {
    hidden var _timer;

    function initialize() {
    _timer = null;
    }

    function onKey(evt) {
    var key = evt.getKey();
    if (key == Ui.KEY_ENTER) {

    if (state == null) {
    state = "Paused";
    }

    if (!"Recording".equals(state)) {
    state = "Recording";

    stopTimer();
    }
    else {

    state = "Paused";

    startTimer();
    }

    Ui.requestUpdate();
    }
    else if (key == Ui.KEY_ESC) {

    if (!"Recording".equals(state)) {

    stopTimer();

    Ui.popView(Ui.SLIDE_IMMEDIATE);
    }
    }

    return true;
    }

    hidden function startTimer() {
    _timer = new Timer.Timer();
    _timer.start(self.method(:onTimer), 3000, false);
    }

    hidden function stopTimer() {
    if (_timer != null) {
    _timer.stop();
    _timer = null;
    }
    }

    function onTimer() {
    _timer.stop();
    _timer = null;

    var menu = new Ui.Menu();
    menu.addItem("Resume" , :resume);
    menu.addItem("Save" , :save);
    menu.addItem("Discard", :discard);

    var delegate = new SaveMenuDelegate();
    Ui.pushView(menu, delegate, Ui.SLIDE_UP);
    }
    }

    class TestView extends Ui.View {

    hidden var _timer;

    function initialize() {
    _timer = new Timer.Timer();
    }

    function onShow() {
    _timer.start(self.method(:onTimer), 1000, true);
    }

    function onHide() {
    _timer.stop();
    }

    function onTimer() {
    Ui.requestUpdate();
    }

    //! Update the view
    function onUpdate(dc) {
    dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_BLACK);
    dc.clear();

    var cx = dc.getWidth() / 2;
    var cy = dc.getHeight() / 2;

    dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT);

    dc.drawText(cx, cy, Gfx.FONT_LARGE, state,
    Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);

    dc.drawText(cx, cy - 25, Gfx.FONT_TINY, Time.now().value().toString(),
    Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);
    }
    }
  • Hi Travis,

    I'm not much of a programmer, but I spent some time playing with this issue.

    It seems to me that the ProgressView might not actually get popped until after the onTimer function is done executing. So the save menu and main menu can't get popped because they're not in the foreground.

    I ran the following extra printlns in your code to slow things down a bit. It did all of the println commands so I had 0 to 24 printed into the log file 2 times and I had all 3 of the popped view text lines in the log file. However, the view on the watch stayed stuck on the Progress view until after ALL of the println's were executed. The "Activity Discarded" message stayed on the watch screen for the several seconds it took to execute all of the println's.

    else if (_ticks == 3) {
    _timer.stop();

    // pop the progress bar
    Ui.popView(Ui.SLIDE_IMMEDIATE);
    for(var i = 0; i < 25; i ++)
    {
    Sys.println(i);
    Sys.println(" ");
    Ui.requestUpdate();
    }
    Sys.println("popped progress bar");

    // pop the save menu
    Ui.popView(Ui.SLIDE_IMMEDIATE);
    for(var j = 0; j < 25; j ++)
    {
    Sys.println(j);
    Sys.println(" ");
    Ui.requestUpdate();
    }
    Sys.println("popped save menu");

    // pop the main view
    Ui.popView(Ui.SLIDE_IMMEDIATE);
    Sys.println("popped main view");
    }


    Bottom line is that I don't know if this approach can work because current view can't pop itself and then proceed to pop the other views on the stack.

    Here's the log file.

    SaveMenuDelegate.initialize
    SaveMenuDelegate.onMenuItem
    SaveMenuDelegate.discard
    DiscardConfirmationDelegate.initialize
    DiscardProgressView.onTimer
    DiscardProgressView.onTimer
    DiscardProgressView.onTimer
    DiscardProgressView.onTimer
    0

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    popped progress bar
    0

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    popped save menu
    popped main view
  • Hi Travis,

    One additional note. I not positive that your code works in the simulator either, but I haven't figured out exactly what its doing there. Typically, when I pop the last view on the simulator, it crashes with an error on the console. You will see this if you just run your code and hit the back button to exit.

    I don't get that error on the console when I run your code and go to the discard menu so I'm not sure it's actually popping all of the views in the simulator either.
  • Former Member
    Former Member over 10 years ago
    Off the top of my head... I would try create an empty view that pops itself, then try switching to that view from the menu.
  • Well, that would almost work. The problem is that I don't know if I need to pop multiple menus until I've got the confirmation three levels deep. I'll keep thinking on this.

    Maybe one of the Garmin gurus will post something.
  • Former Member
    Former Member over 10 years ago
    I don't think you understood my meaning, or am I not understanding you? When you get that confirmation, switch to the view that closes itself.

    class DiscardConfirmationDelegate extends Ui.ConfirmationDelegate
    {
    function onResponse(response) {
    Sys.println("DiscardConfirmationDelegate.initialize");


    if (response == CONFIRM_YES) {


    state = "Discarded";


    //var view = new ProgressView("Discarding", "Activity Dicarded");
    var view = new ViewThatHandlesProgressThenExits("Discarding", "Activity Discarded");
    Ui.SwitchToView(view, null, Ui.SLIDE_IMMEDIATE);


    return true;
    }
    else {
    return true;
    }
    }
    }


    That may disrecard your other views ;)
  • I think I'm the one who is confused..

    I think you're suggesting I write a special class that does nothing other than push a progress bar and pop itself. I would use switchToView() to get this view into the view stack, which allow me to eliminate the menu below before pushing the progress bar. When the progress bar pops itself, this view would still have to pop itself. This trick would allow me to eliminate one menu/confirmation from the view stack. I'd likely have to repeat a similar hack for every level of menu nesting that I want to skip back over.

    Travis
  • Former Member
    Former Member over 10 years ago
    Lol, I have that effect. I'm not really focused on this, sorry. If the idea had any credence at all then it wasn't helped by my placing change in the wrong place. And then my attempt at sly humor went awry, which was meant to highlight your "Activity Dicarded" typo.

    The basis of my thinking was that you can't SwitchTo a confirmation dialog, and that maybe a progress bar was the same. By not pushing new views all the time you can easily remain at the bottom of the stack. By overriding the ESC button you can easily decide if you want it to pop the view or navigate to a previous view.
  • Former Member
    Former Member over 10 years ago
    I don't know if I followed everything being discussed here, but I can address the original question about differing behavior observed when popping multiple views in the simulator and on device.

    The was a bug in the implementation on devices that caused multiple successive view controls to fail which has been fixed. I believe the fix is integrated in the recent VivoActive and Fenix 3 releases, and there should be a new Forerunner 920XT release that also incorporates this fix soon.

    There are some additional improvements that I am hoping to see with this functionality in the future because view animations don't really work when executing multiple view controls. I recommend only using SLIDE_IMMEDIATE for this type of behavior.

    Edit: The VivoActive does not have this fix as of firmware version 2.60. The Fenix 3 and FR920XT have corrected this issue.
  • Hi Brian,

    Thanks for the update on this.

    Can you clarify a few things for me on how the view stack works? I've been running a bunch of tests with Travis's test code and I've made myself pretty confused.

    1) If you look at Travis's SaveMenuDelegate, there are resume, save, and discard functions. The resume function doesn't pop any views or push any new views, yet the Menu view seems to be popped after the resume function is run. However, if discard is selected, it pushes the Confirmation view on top of the stack and it seems to leave the save menu on the stack. Is it correct to think about a menu and a confirmation as being on the page stack like below?

    DiscardConfirmation with DiscardConfirmationDelegate
    SaveMenu with SaveMenu Delegate
    TestView with Test Delegate

    If this stack is the right way to think about it, why didn't the SaveMenu need to be popped after the resume function, but it did need to be popped after the discard function? Is pushing a new view what changed this behaviour?

    2) When the Confirmation selection is made, there is nothing to pop the confirmation view. Does it just pop itself after onResponse returns? This seems reasonable to me, but I couldn't find anywhere in the documentation that it is intended to self-pop from the page stack.

    3) Is the intended functionality that calling multiple popViews in a current view will pop the current view as well as queue up popping the next views down on the page stack? If I had 3 views on a page stack with view1 on the bottom view2 in the middle and view3 on the top and I call Ui.popView three times in View3 or its delegate, is the intended functionality what's below or something different?
    • Finish executing the onUpdate, onKey, onMenuItem, or onResponse function in View3 or its delegate
    • run onHide for View3
    • remove View3 from the stack
    • run onShow for View2???
    • run onUpdate for View2???
    • run onHide for View2
    • remove View2 from the stack
    • run onShow for View1???
    • run onUpdate for View1???
    • run onHide for View1
    • exit the app