My Custom Stopwatch App - Questions on LONG KeyPress & triangle charmap

Hi - I'm making my own (VERY simple) customised stopwatch app (for me to track Sets and RestTime)
Couple of questions.

1) http://developer.garmin.com/downloads/connect-iq/monkey-c/doc/Toybox/WatchUi.html
based on the above link - how does one get a LONG Press of a key? eg: I want to do a Reset of the timer (the native timer does a LONG keypress of the DN key)

2) in the native timer, there is a little triangle beside the Reset word, how do I get this little triangle?


Thanks
  • Sigh.. after I post this, I figured out the answer to my own #1 question.
    I just have to simulate it on the simulator.
    Click the Up Button = KEY_UP 13 The up key
    Click and HOLD the Up Button = KEY_MENU 7 The menu key

    but question #2 would still be valid.
  • Hey, how did you implement a stopwatch feature? I looked in Toybox and it seemed only a timer was available.

  • You want to be really careful supporting your own long press code, as long press can be used for things the FW handles, and that varies by garmin device.  Then mix in hotkeys, when a user can assign different things

  • Hey, how did you implement a stopwatch feature?

    There's more than one way, but the way I did it was to use the confusingly named System.getTimer() function (which actually returns the system uptime in milliseconds), and to track two variables in my stopwatch code:

    - A: the uptime at which the stopwatch timer was last unpaused/started (initialized/set using System.getTimer())

    - B: the amount of stopwatch time that elapsed before the last unpause event (initialized to 0)

    When the user starts the stopwatch, A is initialized to System.getTimer() and B is initialized to 0.

    The current value of the stopwatch timer (C) is System.getTimer() - A + B. C is what's displayed at any given moment in time, when the stopwatch timer is visible on the screen.

    When the user pauses the stopwatch, B receives the value of C. When the user unpauses the stopwatch, A is updated with System.getTimer().

    The only thing to watch out for is that the value of System.getTimer() will overflow after ~25 days.

    I like this implementation because the user can exit and re-open the stopwatch app without losing the correct value of the stopwatch timer, assuming A and B are persisted, and it's not affected by any possible changes to the date/time ("wall clock") on the device.

    It's one way to implement long key press.

    I think the latest question (by XiaoZhZi) in this thread is asking about implementing a literal stopwatch timer (i.e. the part which keeps time), not about long press. Thank you for sharing your code! I have similar code - which uses a timer - to detect long press.

    But in general, as jim_m_58 pointed out, it's not a good idea to rely on long press as the only/default method of input to perform a specific action because:

    - some very old watches (FR235 era) won't allow CIQ to detect long press at all (and this isn't reflected in the sim)

    - even for non-ancient watches, CIQ can't detect long press of some buttons (like the LIGHT button on 5 button watches) even if the simulator allows it, since long press for those buttons is reserved for a system action

    - Many watches allow for user-defined hotkeys (including long-press of a button.) If a user-defined long-press hotkey is set for a button, then CIQ won't detect long-press of that button

    I support long press in my stopwatch app, but only as an option.

  • I think another factor (maybe just me) is I want a consistency.  Doing "z" in app "A" is similar to dong "z" in app "B". Adding a new key sequence in your app could be confusing for users that know the sequence from any other apps..

    On two button devices, both short and long presses are used for common functions already.

  • Hmm I'm having a little trouble understanding this. Aren't B and A effectively stopwatches? When the user starts the stopwatch everything is at 0, and then B starts increasing? And does System.getTime() remain the same value throughout the whole stopwatch session?

  • No, A and B are Number variables, they don't increase, they only hold the value you assign to them. The only thing that increases is System.getTimer().

    I'll add that most stop watches I used only display less than a second accuracy for the first 10 seconds or maybe minute, but beyond that they usually display only seconds. Though it is possible that it still tracks milliseconds and does display it when you pause or stop.

  • I'll add that most stop watches I used only display less than a second accuracy for the first 10 seconds or maybe minute, but beyond that they usually display only seconds. Though it is possible that it still tracks milliseconds and does display it when you pause or stop.

    Yeah, that's how my stopwatch works. 100ths of seconds are shown for the first 60 seconds, and for a certain amount of time after each lap button press - the screen updates every 50 ms during this period. Afterwards, only seconds are shown, and the screen updates every second. The primary motivation is to save battery life. (The user can opt to always hide 100ths of seconds, to save more battery.)

    Aren't B and A effectively stopwatches? When the user starts the stopwatch everything is at 0, and then B starts increasing?
    And does System.getTime() remain the same value throughout the whole stopwatch session?

    No, A and B are bookkeeping variables which allow you to implement a stopwatch, with the help of System.getTimer(). System.getTimer() returns the system uptime in milliseconds. For example, if it's been exactly 5 minutes since your watch last rebooted/powered on, then it will return 5 * 60 * 1000 = 300,000.

    Again:

    A = the system uptime at the time the stopwatch was last started

    B = the amount of stopwatch time accumulated before the stopwatch was last stopped

    A is necessary to handle the simple case of starting the stopwatch and displaying the stopwatch time at any given moment

    B is necessary to handle the additional case of starting and stopping the stopwatch multiple times without resetting it

    C is the calculation for determining the current stopwatch time: System.getTimer() - A  + B

    The stopwatch has 3 states:
    - reset (stopwatch is not running and has value of 0)
    - started (stopwatch is running)
    - stopped (stopwatch is not running and has non-zero value)

    Whenever the user starts the stopwatch, A receives the value of System.getTimer()

    Whenever the user stops the stopwatch, B receives the current value of the stopwatch (System.getTimer() - A + B)

    Concrete example:

    - Stopwatch is initially reset and the timer is not running. In this special case, the app displays 0:00. A is initialized to null and B is initialized to 0.

    - [watch uptime == 60,000 ms] User presses START to start the stopwatch, so A is assigned a value of 60,000. The stopwatch timer (C) is calculated as follows: System.getTimer() - A + B = 60,000 - 60,000 + 0 = 0. The app displays 0:00

    - [70,000 ms] User looks at display. The stopwatch timer (C) is calculated as follows: System.getTimer() - A + B = 70,000 - 60,000 + 0 = 10,000 ms (10 seconds). The app displays 0:10

    - [80,000 ms] User presses START to stop the stopwatch, so B is assigned the value of the stopwatch = System.getTimer() - A + B = 80,000 - 60,000 + 0 = 20,000. In other words, the stopwatch accumulated 20 seconds before it was stopped. The app displays 0:20

    - [90,000 ms] User presses START to start the stopwatch, so A is assigned a value of 90,000. The stopwatch timer (C) is calculated as follows: System.getTimer() - A + B = 90,000 - 90,000 + 20,000 = 20,000 (20 seconds). The app displays 0:20

    - [100,000 ms] User looks at display. The stopwatch timer (C) is calculated as follows: System.getTimer() - A + B = 100,000 - 90,000 + 20,000 = 30,000 (30 seconds). The app displays 0:30

    No, A and B are Number variables, they don't increase, they only hold the value you assign to them. The only thing that increases is System.getTimer().

    Correct. Here's an example stopwatch class to illustrate the concept. (It's up to the consumer of the class to handle input, schedule display updates, call the appropriate class methods, and format the stopwatch time appropriately)

    import Toybox.Lang;
    
    class Stopwatch {
        enum StopwatchState {
            STOPWATCH_STATE_RESET = 0,   // initial state
            STOPWATCH_STATE_STARTED = 1, // stopwatch is running
            STOPWATCH_STATE_STOPPED = 2  // stopwatch is paused after being started
        }
    
        // The system uptime (in ms) when the stopwatch was last started
        protected var uptimeAtStart as Number or Null = null;
        // The stopwatch time (in ms) accumulated before the stopwatch was last stopped
        // (this is required to handle multiple starts/stops)
        protected var stopwatchTimeBeforeStop as Number = 0;
        protected var state as StopwatchState = STOPWATCH_STATE_RESET;
    
        public function initialize() {
            onReset();
        }
    
        // reset the stopwatch (e.g. in response to the press of the LAP button while the stopwatch is stopped)
        public function onReset() as Void {
            uptimeAtStart = null;
            stopwatchTimeBeforeStop = 0;
            state = STOPWATCH_STATE_RESET;
        }
    
        // start or stop the stopwatch (e.g. in response to the press of the START button)
        public function onStartOrStop() as Void {
            if (state != STOPWATCH_STATE_STARTED) {
                uptimeAtStart = System.getTimer();
                state = STOPWATCH_STATE_STARTED;
            } else {
                stopwatchTimeBeforeStop = getStopwatchTime();
                state = STOPWATCH_STATE_STOPPED;
            }
        }
    
        public function getState() as StopwatchState {
            return state;
        }
    
        // get stopwatch time in milliseconds
        // (this would be typically called from a timer - e.g. 50 ms for fast updates, 1 second for slow updates)
        public function getStopwatchTime() as Number {
            var start = uptimeAtStart;
            var timeSinceStart = 0;
            if (start != null) {
                timeSinceStart = System.getTimer() - start;
            }
            return timeSinceStart + stopwatchTimeBeforeStop;
        }
    }

    EDIT: fixed various issues in Stopwatch class, ensure it compiles with strict type checking

  • Aren't B and A effectively stopwatches? When the user starts the stopwatch everything is at 0, and then B starts increasing?

    To answer the implicit question here - "why didn't I implement this by having a literal stopwatch variable that increases when a timer expires?" - there's a couple of reasons.

    - it's a lot simpler and more flexible for the stopwatch code to be able to calculate the answer to the question "what is the stopwatch timer value" at any given moment on demand, as opposed to constantly updating a stopwatch variable. For example, you could try to have a variable that literally keeps stopwatch time, and update that when a 50 ms timer expires, but you might find that the timer doesn't expire at *exactly* every 50 ms, and you'd have to use something like System.getTimer() anyway.

    - As I said above, by simply tracking the values A (uptime at last start) and B (stopwatch time before last stop) which don't change as long the stopwatch is running, the app can persist these values on shutdown and restore them on startup, making it very easy preserve the stopwatch time even if the app is closed and reopened. So for example, the stopwatch app can be implemented as a widget or glance where the stopwatch timer appears to keep running even after the user exits the app.

    With this implementation, the state of the stopwatch class itself (as opposed to the UI code) only changes when the user starts, stops or resets the stopwatch, as opposed to changing every time the display changes.