Need help making a button

I am new to programming and would like to make a Widget with a button that when pressed triggers an HTTP Get request. Could someone point me in the right direction as to what is needed to make this happen. My plan is to have it send an http get request to io.adafruit.com to change a value on my feed.

Thanks
  • I am new to programming and would like to make a Widget with a button that when pressed triggers an HTTP Get request. Could someone point me in the right direction as to what is needed to make this happen. My plan is to have it send an http get request to io.adafruit.com to change a value on my feed.

    Thanks


    You can use one of the standard buttons on the watch, but if you're device has a touch screen, you can do a screen button, where in the Delegate, you look for a tap at a certain spot. Drawing the button is up to you, and could be a bitmap or just a rectangle with text.

    What device are you using?
  • I have a Vivoactive. Do you have example code on how to register a tap in a certain spot? Thanks for the quick reply!
  • I have a Vivoactive. Do you have example code on how to register a tap in a certain spot? Thanks for the quick reply!



    I did this long ago and could be done a bit cleaner. On the va, there are 9 "tap regions" (3x3) on the screen. This code uses the top left region. Width and height are the screen dimensions. In the real va, you get the center of the region all the time, but in the simulator, it's where you actually click.

    class MyViewDelegate extends Ui.BehaviorDelegate {

    function onTap(evt) {
    var xy=evt.getCoordinates();
    if(xy[0]>0 && xy[0]<(width*.33) && xy[1]>0 && xy[1]<(height*.33)) {
    // do what you need to do here }
    }
    }
    }
  • On small devices like this it is often difficult to use buttons. It is common to just allow a tap anywhere on the screen to do something. To do this, you just need to make a class that inherits from InputDelegate and implements the onTap() method. You could also inherit from BehaviorDelegate and implement onSelect() which would work for devices don't have touch support.

    If you are certain you want to have buttons, then things get more complicated. You need something to represent the visual of the button, and then you need provide some mechanism for the tap event to get to that button. Additionally, if you want to support devices without touch screens, you have to come up with another way to get the user input.

    The first part (getting something to draw where you want) is pretty easy. Note that I have written this code from scratch in the web browser and I have not even attempted to compile it.

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

    class MyButton extends Ui.Drawable
    {
    hidden var mColor;
    hidden var mText; // or icon stuff
    hidden var mFont;

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

    mColor = params[:color];
    mText = params[:text];
    mFont = params[:font];
    }

    function setColor(color) {
    mColor = color;
    }

    function setText(text) {
    mText = text;
    }

    function setFont(font) {
    mFont = font;
    }

    function draw(dc) {
    dc.setColor(mColor, mColor);
    dc.fillRectangle(locX, locY, width, height);

    dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_BLACK);
    dc.drawRectangle(locX, locY, width, height);

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


    Now you have a custom drawable, and you toss it into your layout (if you're using layouts) or instantiate it directly in your code...

    <drawables>
    <drawable-list id="background">
    <shape type="rectangle" x="0" y="0" width="fill" height="fill" color="Gfx.COLOR_WHITE" />
    </drawable-list>
    </drawables>

    <layouts>
    <layout id="MyLayout">
    <drawable id="background"/>
    <drawable class="MyButton" id="button">
    <param name="locX">100</param>
    <param name="locY">100</param>
    <param name="width">100</param>
    <param name="height">100</param>
    <param name="font">Gfx.FONT_TINY</param>
    <param name="text">ABC</param>
    <param name="color">Gfx.COLOR_RED</param>
    </drawable>
    </layout>
    </layouts>


    Now you have a button that you can place in a location that is specified in the layout (device independent). All we need to do now is get the button to receive tap events. This requires some work....

    using Toybox.System as Sys;
    using Toybox.Lang as Lang;

    class MyButton extends Ui.Drawable
    {
    // code from above goes here

    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;
    }
    }


    Now we can ask a button if a tap is over it, and we can tell the button to take whatever action it should take in response to a tap event. We still need something to keep track of the active buttons, and to dispatch tap events to them...

    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 = {};
    }

    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;
    }
    }
    [/code]

    So now we have something to accept taps and maintain a collection of buttons. We need our application to create of these, and then we need to assemble it all...

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

    function getButtonRegistry() {
    return gButtonRegistry;
    }

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

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

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

    function onStop() {
    gButtonRegistry = null;
    }
    }

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

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

    class MyView extends Ui.View
    {
    hidden var mButton;

    function initialize() {
    View.initialize();
    }

    function onLayout(dc) {
    setLayout(Rez.Layouts.MyLayout(dc));

    // find the button
    mButton = findDrawableById("button");

    // give the button something to do when it is clicked
    mButton.setTapCallback(self.method(:onButtonTapped));
    }

    function onShow() {
    getButtonRegistry().appendButton(mButton);
    }

    function onUpdate(dc) {
    View.onUpdate(dc);
    }

    function onHide() {
    getButtonRegistry().removeButton(mButton);
    }

    function onButtonTapped(button) {
    Sys.println(Lang.format("Button '$1$' tapped.", [ button.identifier ]));
    return true;
    }
    }


    If all of that worked, you should see a red button and when you tap it in the simulator, you should see the message Button 'button' tapped. in the console. If you add another button with a unique name, you should be able to register and get events from that button as well. You could register different functions (you don't have to register a view function) with each button, and everything should just work out. It should handle pushing and popping views correctly and it should not leak any resources or leave memory in use at program termination.

    This is a lot more complex than Jim's suggestion, but it is easily reusable among different touch capable devices and projects. As Jim pointed out, you may need to place your buttons carefully for devices that have touch regions, but that shouldn't be too much of a problem.

    Travis
  • I did this long ago and could be done a bit cleaner. On the va, there are 9 "tap regions" (3x3) on the screen. This code uses the top left region. Width and height are the screen dimensions. In the real va, you get the center of the region all the time, but in the simulator, it's where you actually click.

    class MyViewDelegate extends Ui.BehaviorDelegate {

    function onTap(evt) {
    var xy=evt.getCoordinates();
    if(xy[0]>0 && xy[0]<(width*.33) && xy[1]>0 && xy[1]<(height*.33)) {
    // do what you need to do here }
    }
    }
    }



    This look great! I try it out tonight.
    If all goes well I'll be able to unlock my car door from my watch.

    Thanks for the help.
  • On small devices like this it is often difficult to use buttons. It is common to just allow a tap anywhere on the screen to do something. To do this, you just need to make a class that inherits from InputDelegate and implements the onTap() method. You could also inherit from BehaviorDelegate and implement onSelect() which would work for devices don't have touch support.

    If you are certain you want to have buttons, then things get more complicated. You need something to represent the visual of the button, and then you need provide some mechanism for the tap event to get to that button. Additionally, if you want to support devices without touch screens, you have to come up with another way to get the user input.

    The first part (getting something to draw where you want) is pretty easy. Note that I have written this code from scratch in the web browser and I have not even attempted to compile it.

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

    class MyButton extends Ui.Drawable
    {
    hidden var mColor;
    hidden var mText; // or icon stuff
    hidden var mFont;

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

    mColor = params[:color];
    mText = params[:text];
    mFont = params[:font];
    }

    function setColor(color) {
    mColor = color;
    }

    function setText(text) {
    mText = text;
    }

    function setFont(font) {
    mFont = font;
    }

    function draw(dc) {
    dc.setColor(mColor, mColor);
    dc.fillRectangle(locX, locY, width, height);

    dc.setColor(Gfx.COLOR_BLACK, Gfx.COLOR_BLACK);
    dc.drawRectangle(locX, locY, width, height);

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


    Now you have a custom drawable, and you toss it into your layout (if you're using layouts) or instantiate it directly in your code...

    <drawables>
    <drawable-list id="background">
    <shape type="rectangle" x="0" y="0" width="fill" height="fill" color="Gfx.COLOR_WHITE" />
    </drawable-list>
    </drawables>

    <layouts>
    <layout id="MyLayout">
    <drawable id="background"/>
    <drawable class="MyButton" id="button">
    <param name="locX">100</param>
    <param name="locY">100</param>
    <param name="width">100</param>
    <param name="height">100</param>
    <param name="font">Gfx.FONT_TINY</param>
    <param name="text">ABC</param>
    <param name="color">Gfx.COLOR_RED</param>
    </drawable>
    </layout>
    </layouts>


    Now you have a button that you can place in a location that is specified in the layout (device independent). All we need to do now is get the button to receive tap events. This requires some work....

    using Toybox.System as Sys;
    using Toybox.Lang as Lang;

    class MyButton extends Ui.Drawable
    {
    // code from above goes here

    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
    }
    }

    hidden var mTapCallback;

    function setTapCallback(callback) {
    mTapCallback = callback;
    }

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


    Now we can ask a button if a tap is over it, and we can tell the button to take whatever action it should take in response to a tap event. We still need something to keep track of the active buttons, and to dispatch tap events to them...

    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 = {};
    }

    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;
    }
    }
    [/code]

    So now we have something to accept taps and maintain a collection of buttons. We need our application to create of these, and then we need to assemble it all...

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

    function getButtonRegistry() {
    return gButtonRegistry;
    }

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

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

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

    function onStop() {
    gButtonRegistry = null;
    }
    }

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

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

    class MyView extends Ui.View
    {
    hidden var mButton;

    function initialize() {
    View.initialize();
    }

    function onLayout(dc) {
    setLayout(Rez.Layouts.MyLayout(dc));

    // find the button
    mButton = findDrawableById("button");

    // give the button something to do when it is clicked
    mButton.setTapCallback(self.method(:onButtonTapped));
    }

    function onShow() {
    getButtonRegistry().appendButton(mButton);
    }

    function onUpdate(dc) {
    View.onUpdate(dc);
    }

    function onHide() {
    getButtonRegistry().removeButton(mButton);
    }

    function onButtonTapped(button) {
    Sys.println(Lang.format("Button '$1$' tapped.", [ button.identifier ]));
    return true;
    }
    }


    If all of that worked, you should see a red button and when you tap it in the simulator, you should see the message Button 'button' tapped. in the console. If you add another button with a unique name, you should be able to register and get events from that button as well. You could register different functions (you don't have to register a view function) with each button, and everything should just work out. It should handle pushing and popping views correctly and it should not leak any resources or leave memory in use at program termination.

    This is a lot more complex than Jim's suggestion, but it is easily reusable among different touch capable devices and projects. As Jim pointed out, you may need to place your buttons carefully for devices that have touch regions, but that shouldn't be too much of a problem.

    Travis

    This looks good. I'll definitely need to try this when I have more time.
  • So far I haven't had any luck in getting the button to work. I'm trying to start by doing a simple clear screen, but nothing is happening.

    Here is my code. Am I missing something?

    using Toybox.WatchUi as Ui;
    using Toybox.Application as App;
    using Toybox.Graphics as Gfx;
    using Toybox.System as Sys;
    using Toybox.Attention as Attention;
    using Toybox.Communications as Comm;


    class HTTPView extends Ui.View {

    function initialize() {
    View.initialize();
    }

    //! Load your resources here
    function onLayout(dc) {
    setLayout(Rez.Layouts.MainLayout(dc));
    }

    //! Called when this View is brought to the foreground. Restore
    //! the state of this View and prepare it to be shown. This includes
    //! loading resources into memory.
    function onShow() {
    }

    //! Update the view
    function onUpdate(dc) {
    // Call the parent onUpdate function to redraw the layout
    dc.clear();
    dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT);
    dc.fillRectangle(0, 0, dc.getWidth(), dc.getHeight());


    dc.clear();
    dc.setColor(Gfx.COLOR_BLUE, Gfx.COLOR_WHITE);
    dc.fillRectangle(0, 0, 67, 48);


    }

    //! Called when this View is removed from the screen. Save the
    //! state of this View here. This includes freeing resources from
    //! memory.
    function onHide() {
    }

    }

    class HTTPViewDelegate extends Ui.BehaviorDelegate
    {

    function onTap(evt)
    {
    var xy = evt.getCoordinates();
    if(xy[0]>0 && xy[0]<(205*.33) && xy[1]>0 && xy[1]<(148*.33))
    {
    // do what you need to do here
    dc.clear();
    }
    }
    }
  • Former Member
    Former Member over 9 years ago
    onTap doesn't have access to the DrawingContext, so you can't call dc.clear(). If you want to update the screen from a delegate, you need to call Ui.requestUpdate(), to force a redraw of the screen. At that point the views onUpdate will be called to redraw the screen.

    That being said, I'd expect an error when you run your code. If no error occurred, the code probably did not run. Did you set the delegate to the view (in App.getInitialView)?
  • Teun's correct - this code shouldn't even compile with the dc in the delegate.

    As a first step, take out the dc.clear() and replace it with a Sys.println("got the tap") and see if the rest of your code works, and then use Ui.requestUpdate(). I often use a global variable that gets set in the delegate, acted upon in onUpdate(), and then reset for something like this.
  • So far I haven't had any luck in getting the button to work. I'm trying to start by doing a simple clear screen, but nothing is happening.

    //! Update the view
    function onUpdate(dc) {
    // Call the parent onUpdate function to redraw the layout
    dc.clear();
    dc.setColor(Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT);
    dc.fillRectangle(0, 0, dc.getWidth(), dc.getHeight());


    dc.clear();
    dc.setColor(Gfx.COLOR_BLUE, Gfx.COLOR_WHITE);
    dc.fillRectangle(0, 0, 67, 48);


    }


    This code is wonky. It clears the view three times and then draws the blue rectangle. You should be able to set the colors once, clear, and then set the colors again and draw the button rectangle.

    Teun's correct - this code shouldn't even compile with the dc in the delegate.

    This code will compile fine. It will crash at runtime because dc should not be in scope.

    As for why the code doesn't work, it could be that the delegate is not getting set. If it isn't that, it is because the onTap() function of the delegate isn't getting called because the onSelect() function of the behavior delegate isn't returning true... per the BehaviorDelegate documentation...

    If a BehaviorDelegate does return true for a function, indicating that the input was used, then the InputDelegate function that corresponds to the behavior will be called.


    You may see your onTap() function get called if you switch to using an InputDelegate. Since you are clearly writing device-specific code, using a BehaviorDelegate isn't all that useful.

    Travis