I thought Datafields could not receive input but...

I thought Datafields could not receive input and I saw threads in the forum confirming that, but the Specialized Turbo Levo Data Field can indeed handle user inputs.

In the UI of the datafield there are plus and minus buttons that can be tapped to change the level of assistance from the ebike. Someone knows how that has been achieved?

Camillo
  • I had never noticed this and nobody ever mentioned it up to now. It seems that either this data field is special in some way, or someone forgot to mention that data fields on the edge devices can accept input.

    I'll have to do some tinkering and report back.

    Travis
  • Is this a native field on the device or one you downloaded from the app store? If it a native one, it might do things that can't be done in Connect IQ. Native stuff doesn't use CIQ.
  • Travis - that's interesting!

    Maybe something in a upcoming SDK we don"t know about?
  • It is not mentioned in the release notes as far as I can see, but it does appear to work with full-screen data fields in the simulator for the edge_1000 device profile only. It appears to have started working in the simulator with the 1.2.3 SDK, and it continues to work with the 1.2.6 SDK. It also works with data fields of various sizes on the actual device. Here is my test code.

    I'm guessing that this is something that Garmin enabled for the folks at Specialized. They haven't mentioned it because it doesn't appear to work correctly in the simulator for data fields that are not full-screen. It is possible that they had intended to tell us when the simulator functionality was complete, or maybe it is something that they wanted to keep secret. I'm not sure.

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

    function assert(cond, reason) {
    if (!cond) {
    Sys.error(reason);
    }
    }

    class MyButton extends Ui.Drawable
    {
    hidden var mFillColor;
    hidden var mTextColor;
    hidden var mFont;
    hidden var mText;

    function initialize(params) {
    Drawable.initialize(params);

    if (params != null) {
    mFillColor = params[:fillColor];
    mTextColor = params[:textColor];
    mFont = params[:font];
    mText = params[:text];
    }

    if (mFillColor == null) {
    mFillColor = Gfx.COLOR_DK_GRAY;
    }

    if (mTextColor == null) {
    mTextColor = Gfx.COLOR_WHITE;
    }

    if (mText == null) {
    mText = "";
    }

    if (mFont == null) {
    mFont = Gfx.FONT_LARGE;
    }
    }

    function setFillColor(color) {
    mFillColor = color;
    }

    function setTextColor(color) {
    mTextColor = color;
    }

    function setText(text) {
    mText = text;
    }

    function setFont(font) {
    mFont = font;
    }

    function draw(dc) {
    var radius = (width < height ? width : height) / 10;

    dc.setColor(mFillColor, mFillColor);
    dc.fillRoundedRectangle(locX, locY, width, height, radius);

    dc.setColor(mTextColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(locX + width / 2,
    locY + height / 2,
    mFont,
    mText,
    Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);
    }

    function isPointInside(coord) {
    if (coord[0] < locX) {
    return false; // too far left
    }
    else if (coord[1] < locY) {
    return false; // too far above
    }
    else if ((locX + width) < coord[0]) {
    return false; // too far right
    }
    else if ((locY + height) < coord[1]) {
    return false; // too far below
    }

    return true;
    }

    hidden var mTapCallback;

    function setTapCallback(callback) {
    mTapCallback = callback;
    }

    function onTap() {
    if (mTapCallback != null) {
    return mTapCallback.invoke(self);
    }

    return false;
    }
    }

    class MyButtonRegistry
    {
    // i'm using a map for simplicity. an array that is dynamically
    // resized would be more efficient in terms of memory use.
    hidden var mButtons;

    function initialize() {
    mButtons = {};
    }

    function appendButton(button) {
    mButtons.put(button, button);
    }

    function removeButton(button) {
    mButtons.remove(button);
    }

    function removeAll() {
    mButtons.clear();
    }

    function onTap(coord) {

    var buttons = mButtons.keys();
    for (var i = 0; i < buttons.size(); ++i) {
    var button = buttons;

    if (button.isPointInside(coord)) {
    return button.onTap();
    }
    }

    return false;
    }
    }

    // singleton-like data and accessor
    /* hidden */ var buttonRegistry;

    function getButtonRegistry() {
    return buttonRegistry;
    }

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

    function onStart() {
    buttonRegistry = new MyButtonRegistry();
    }

    function getInitialView() {
    return [ new MyView(), new MyViewDelegate() ];
    }

    function onStop() {
    buttonRegistry = null;
    }
    }

    class MyViewDelegate extends Ui.InputDelegate
    {
    function initialize() {
    InputDelegate.initialize();
    }

    function onTap(evt) {
    var coords = evt.getCoordinates();
    return getButtonRegistry().onTap(coords);
    }
    }

    class MyView extends Ui.DataField
    {
    hidden var decrementButton;
    hidden var incrementButton;
    hidden var currentHeading;

    function initialize() {
    DataField.initialize();

    currentHeading = 0;
    }

    function onLayout(dc) {

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

    var params = {
    :width => cx - 4,
    :height => cy - 4,
    :textColor => Gfx.COLOR_WHITE
    };

    params.put(:locX, 0 + 2);
    params.put(:locY, cy + 2);
    params.put(:text, "-");
    params.put(:fillColor, Gfx.COLOR_RED);

    decrementButton = new MyButton(params);

    params.put(:locX, cx + 2);
    params.put(:locY, cy + 2);
    params.put(:text, "+");
    params.put(:fillColor, Gfx.COLOR_GREEN);

    incrementButton = new MyButton(params);
    }

    function onShow() {
    var registry = getButtonRegistry();
    assert (registry != null);

    incrementButton.setTapCallback(self.method(:onIncrement));
    registry.appendButton(decrementButton);

    decrementButton.setTapCallback(self.method(:onDecrement));
    registry.appendButton(incrementButton);
    }

    function onUpdate(dc) {
    var bgcolor = self.getBackgroundColor();
    dc.setColor(bgcolor, bgcolor);
    dc.clear();

    decrementButton.draw(dc);
    incrementButton.draw(dc);

    if (currentHeading != null) {
    var fgcolor = ~bgcolor & 0xFFFFFF;
    dc.setColor(fgcolor, Gfx.COLOR_TRANSPARENT);

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

    dc.drawText(cx, cy,
    Gfx.FONT_LARGE,
    currentHeading.toString(),
    Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);
    }
    }

    function onHide() {
    var registry = getButtonRegistry();
    assert (registry != null);

    registry.removeButton(decrementButton);
    decrementButton.setTapCallback(null);

    registry.removeButton(incrementButton);
    decrementButton.setTapCallback(null);
    }

    function onIncrement(button) {
    currentHeading += 1;
    currentHeading %= 360;
    return true;
    }

    function onDecrement(button) {
    currentHeading += 359;
    currentHeading %= 360;
    return true;
    }
    }
    [/code]
  • It is not mentioned in the release notes as far as I can see, but it does appear to work with full-screen data fields in the simulator for the edge_1000 device profile only. It appears to have started working in the simulator with the 1.2.3 SDK, and it continues to work with the 1.2.6 SDK. It also works with data fields of various sizes on the actual device. Here is my test code.


    Thanks Travis, I was tinkering with similar code but couldn't make it work! It was because I was using a simpleDataField instead of a DataField
  • So, upon further investigation, it doesn't work in the simulator when used with a multi-field layout because of bugs in the my code. The big problem is that onLayout() is called after onShow(), so the buttons that are created after onShow() aren't registered with my button registry.

    Travis
  • Additionally, only the onTap() events are passed to the input delegate. Neither onHold() or onRelease() are called.
  • Here is improved code that behaves correctly.

    using Toybox.Application as App;
    using Toybox.Graphics as Gfx;
    using Toybox.WatchUi as Ui;

    class MyButton extends Ui.Drawable
    {
    hidden var mFillColor;
    hidden var mTextColor;
    hidden var mFont;
    hidden var mText;

    function initialize(params) {
    Drawable.initialize(params);

    if (params != null) {
    mFillColor = params[:fillColor];
    mTextColor = params[:textColor];
    mFont = params[:font];
    mText = params[:text];
    }

    if (mFillColor == null) {
    mFillColor = Gfx.COLOR_DK_GRAY;
    }

    if (mTextColor == null) {
    mTextColor = Gfx.COLOR_WHITE;
    }

    if (mText == null) {
    mText = "";
    }

    if (mFont == null) {
    mFont = Gfx.FONT_LARGE;
    }
    }

    function setFillColor(color) {
    mFillColor = color;
    }

    function setTextColor(color) {
    mTextColor = color;
    }

    function setText(text) {
    mText = text;
    }

    function setFont(font) {
    mFont = font;
    }

    function draw(dc) {
    var radius = (width < height ? width : height) / 10;

    dc.setColor(mFillColor, mFillColor);
    dc.fillRoundedRectangle(locX, locY, width, height, radius);

    dc.setColor(mTextColor, Gfx.COLOR_TRANSPARENT);
    dc.drawText(locX + width / 2,
    locY + height / 2,
    mFont,
    mText,
    Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);
    }

    function isPointInside(x, y) {
    if (x < locX) {
    return false; // too far left
    }
    else if (y < locY) {
    return false; // too far above
    }
    else if ((locX + width) < x) {
    return false; // too far right
    }
    else if ((locY + height) < y) {
    return false; // too far below
    }

    return true;
    }
    }

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

    function getInitialView() {
    var view = new MyView();
    return [ view, new MyViewDelegate(view) ];
    }
    }

    class MyViewDelegate extends Ui.InputDelegate
    {
    hidden var mView;

    function initialize(view) {
    InputDelegate.initialize();
    mView = view;
    }

    function onTap(evt) {

    var coords = evt.getCoordinates();
    var x = coords[0];
    var y = coords[1];

    var button;

    button = mView.getDecrementButton();
    if (button.isPointInside(x, y)) {
    return mView.offset(-1);
    }

    button = mView.getIncrementButton();
    if (button.isPointInside(x, y)) {
    return mView.offset(+1);
    }

    return false;
    }
    }

    class MyView extends Ui.DataField
    {
    hidden var mDecrementButton;
    hidden var mIncrementButton;
    hidden var mValue;

    function initialize() {
    DataField.initialize();

    mValue = 0;
    }

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

    var params = {
    :width => cx - 4,
    :height => cy - 4,
    :textColor => Gfx.COLOR_WHITE
    };

    params.put(:locX, 0 + 2);
    params.put(:locY, cy + 2);
    params.put(:text, "-");
    params.put(:fillColor, Gfx.COLOR_RED);

    mDecrementButton = new MyButton(params);

    params.put(:locX, cx + 2);
    params.put(:locY, cy + 2);
    params.put(:text, "+");
    params.put(:fillColor, Gfx.COLOR_GREEN);

    mIncrementButton = new MyButton(params);
    }

    function getDecrementButton() {
    return mDecrementButton;
    }

    function getIncrementButton() {
    return mIncrementButton;
    }

    function onUpdate(dc) {
    var bgcolor = self.getBackgroundColor();
    dc.setColor(bgcolor, bgcolor);
    dc.clear();

    mDecrementButton.draw(dc);
    mIncrementButton.draw(dc);

    if (mValue != null) {
    var fgcolor = ~bgcolor & 0xFFFFFF;
    dc.setColor(fgcolor, Gfx.COLOR_TRANSPARENT);

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

    dc.drawText(cx, cy,
    Gfx.FONT_LARGE,
    mValue.toString(),
    Gfx.TEXT_JUSTIFY_CENTER | Gfx.TEXT_JUSTIFY_VCENTER);
    }
    }

    function offset(value) {
    mValue += value;
    return true;
    }
    }
  • When you tap the screen of the Garmin Edge 1030 when the timer is running, the timer overlay shows. (See https://www8.garmin.com/manuals/webhelp/edge1030/EN-US/GUID-FA15D546-45B9-405A-BE90-BED318105645.html)

    The onTap() event is still passed to the input delegate. But I cannot find a way to prevent the timer overlay from showing. Is there a way to accomplish this?