TFT_eSPI icon indicating copy to clipboard operation
TFT_eSPI copied to clipboard

Add "actions" to buttons, allowing custom callbacks to be attached to buttons

Open jack828 opened this issue 2 years ago • 21 comments

Hey, great library! Again, thank you for making it!

I am currently writing another application using it, and I will have a series of buttons with various actions for each button. I believe that attaching a function to the button object itself results in good application of DRY techniques and makes adding more buttons a breeze - once the core is in place.

Now the specifics are not too important, but to demonstrate the usage of this PR please see below:

Note, the code is an incomplete example - setting up the screen etc are assumed to be complete already.

TFT_eSPI_Button btnL, btnR;
TFT_eSPI_Button *buttons[] = { &btnL, &btnR };
uint8_t buttonCount = sizeof(buttons) / sizeof(buttons[0]);

void initButtons() {
  btnL.initButtonUL(&tft, tft.width() - 105, tft.height() - 30, 50, 30, TFT_WHITE, TFT_WHITE, TFT_BLACK, const_cast<char *>("<-"), 1);

  btnL.setButtonAction([]() {
    Serial.println("BUTTON LEFT PRESSED");
  });
  btnL.drawButton();

  btnR.initButtonUL(&tft, tft.width() - 50, tft.height() - 30, 50, 30, TFT_WHITE, TFT_WHITE, TFT_BLACK, const_cast<char *>("->"), 1);
  btnR.setButtonAction([]() {
    Serial.println("BUTTON RIGHT PRESSED");
  });
  btnR.drawButton();
}

// Then later on, in your UI loop with touch handler:
void loop() {
  tft.setCursor(0, 30, 2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);

  int pos[2] = { 0, 0 };
  ft6236_pos(pos);
  tft.print("Touch X: ");tft.print(pos[0]);tft.print(" Y: ");tft.println(pos[1]);tft.print("    ");

  for (uint8_t buttonIndex = 0; buttonIndex < buttonCount; buttonIndex++) {
    TFT_eSPI_Button *btn = buttons[buttonIndex];

    if (pos[0] != -1 && pos[1] != -1) {
      btn->press(btn->contains(tft.width() - pos[0], pos[1]));
    } else {
      btn->press(false);
    }

    // In this example, i invert buttons when pressed and trigger the "action"
    if (btn->justReleased()) {
      btn->drawButton(false);
    } else if (btn->justPressed()) {
      btn->action();
      btn->drawButton(true);
    }
  }
}

Please let me know what else you wish to see implemented or changed in the PR - it's your project after all! 😄

jack828 avatar Mar 13 '22 15:03 jack828

The button class is a bit too basic at the moment so extra functionality would be of benefit and simplify sketches.

My initial thoughts need more consideration but are:

  1. Three action callback types are needed to cover the majority of cases:

    • just pressed action, e.g. turn on an LED, invert the button and/or change the state of something
    • pressed and held action, e.g. to brighten an LED gradually depending on how long the button is pressed
    • just released action, to revert button inversion and/or revert to previous state of something
  2. New button functions are needed that make a GUI simpler to implement, the above actions can be optionally attached:

  • check boxes, e.g. blank or x in box (can be automated within library and getState used to retrieve state
  • push buttons, visually get inverted when first pressed, revert when released
  • toggle buttons, button invert state toggles on each press
  1. New button getState function added, needs thought, maybe with a bit field returned:
  • bit 0 = button in inverted/ not inverted
  • bit 1 = check box checked/not checked
  • bit 2 = just pressed/not just pressed
  • bit 3 = pressed and held/not pressed
  • bit 4 = just released/not just released

Examples would be needed that are easy to understand for a noob. Generally it is best not to use DRY philosophy in the basic examples so that the library functions are demonstrated as simply as possible for each use case, however for more sophisticated examples like a working calculator this would be OK.

Bodmer avatar Mar 14 '22 16:03 Bodmer

Have thought a bit more about this. It would be simpler to have one action callback as you suggest, then within that callback a user can call getState and decide what to do with the press state.

Also, at the moment the contains(x,y) function is a bit clumsy, so maybe wrap up this function within a checkButton(x, y) call, then the action gets called if the x,y are in the button box. The function should return a status flag (set in the callback function) so calling code knows if an action was taken.

Bodmer avatar Mar 14 '22 23:03 Bodmer

Hey, great ideas!

I might give this a go over the next few weeks if you don't beat me to it 😄 I'll probably have more thoughts as the implementation progresses.

jack828 avatar Mar 18 '22 10:03 jack828

I have thought further on this and concluded that an independant buttons support library is the way to go. This then reduces the maintenance, development and bug/question/documentation support load on myself.

I can quickly create a template library for you with all the existing functions if that helps.

Bodmer avatar Mar 18 '22 11:03 Bodmer

Sure, sounds good! Thanks!

jack828 avatar Mar 18 '22 11:03 jack828

I actually opened a discussion about buttons yesterday and hadn't seen this PR. I think this is an excellent path to go. I'm sure you've seen it but GUIslice has a button callback (see here for quick look)

@jack828 Nice to see someone else using a CTP and working on something similar. Let me know if there's anything I can do to help.

GeorgeIoak avatar Mar 19 '22 15:03 GeorgeIoak

While continuing my research I ran across this repo, https://github.com/davetcc/IoAbstraction, which handles touch events for both resistive and capacitive screens, as well as other devices, in an interesting way.

GeorgeIoak avatar Mar 19 '22 17:03 GeorgeIoak

I have created a draft library here: https://github.com/Bodmer/TFT_eButton

It is working but has a few rough edges. The example needs work but demonstrates:

  • A toggle button
  • A button that inverts every 1s, pressing the button stops the flashing for 10s
  • Pressing the buttons also produces various serial messages
  • Smooth anti-aliased rectangle use
  • Free font as label

The buttons can use the anti-aliased (smooth) rounded rectangle to give an improved appearance.

Clearly all this is too much for a beginner in one sketch, so I will break out some simpler examples.

Constructive comments welcome.

Bodmer avatar Mar 20 '22 03:03 Bodmer

I was just drafting up something similar so thanks for beating me! I just took a quick look and haven't had my coffee so don't judge me too harshly.

  • Is there a reason why one of the defined button states isn't HELD
  • I need to look at how you use setPressTime and setReleaseTime I was originally thinking of adding a parameter that lets you define how long before the button state is changed to HELD
  • Side note, do you think you'll ever include support for capacitive touch panels?

I'll experiment with your example a little later on and provide some feedback for you but thanks again for working on this.

GeorgeIoak avatar Mar 20 '22 15:03 GeorgeIoak

  • Is there a reason why one of the defined button states isn't HELD Nope, just a test example for a momentary, non-latching push button. This is not a library button operation mode, it is coded in the button action.
  • I need to look at how you use setPressTime and setReleaseTime I was originally thinking of adding a parameter that lets you define how long before the button state is changed to HELD That is all it is, it is for experimentation as the library could capture this time.
  • Side note, do you think you'll ever include support for capacitive touch panels? No, because I think a separate library is a better option to cope with CTP chip features like multi-touch, swipe, pinch etc. The touch library built into TFT_eSPI will be deprecated soon and eventually may be removed. I created one here for my SSD1963 TFT.

I'll experiment with your example a little later on and provide some feedback for you but thanks again for working on this.

Thanks. The original Adafruit button library was simplistic as it was for limited resource processors like the UNO, so could do with a revamp.

I have an unpublished graphical widgets library that contains sliders, battery charge indicators etc so maybe I ought to add the button capability to it as well. The smooth graphics functions were developed specifically for that, but it is a WIP.

Bodmer avatar Mar 20 '22 17:03 Bodmer

I have some questions on your example, would you rather I start a thread over there?

GeorgeIoak avatar Mar 20 '22 18:03 GeorgeIoak

@Bodmer I did a little playing around and it's almost embarrassing to share but take a look at https://github.com/GeorgeIoak/TFT_eButton. I added a new longPressAction and added my example sketch that uses it. It needs cleanup but basically my thoughts are with something like this you can add control for things that need to respond to long button holds. In my case it was temperature control, another case would be volume control, scrolling, etc.

...
btnUP.setLongPressAction(btnCommonAction);
...

for (uint8_t b = 0; b < buttonCount; b++) {
      if (pressed) {
        if (btn[b]->contains(t_x, t_y)) {
          btn[b]->press(true);
          btn[b]->longPressAction(b, 1000, 5);  // btn#, long press time, long press increment
        }
      }
      else {
        btn[b]->press(false);
        btn[b]->releaseAction();
      }
    }

and then the callback looks like this:

void btnCommonAction(uint8_t btnNum, uint8_t longPressTime, uint8_t longPressInc)
{
  if (btn[btnNum]->justPressed())  {
    btn[btnNum]->drawSmoothButton(true);
    btn[btnNum]->setPressTime(millis());
  }
  // if button pressed for more than 1 sec...
  if (millis() - btn[btnNum]->getPressTime() >= longPressTime) {
    Serial.println("Button pressed for 1 second.......");
    btn[btnNum]->setPressTime(millis());
    switch (btnNum)  {
      case 0: // UP Button
        if (desiredTemp < MAX_H2O_TEMP)
          desiredTemp += longPressInc;
        else
          desiredTemp = MAX_H2O_TEMP;
        break;
      case 1:  // DOWN Button
        if (desiredTemp > MIN_H2O_TEMP)
          desiredTemp -= longPressInc;
        else
          desiredTemp = MIN_H2O_TEMP;
        break;
      default:
        break;
    }
  }
  tft.setTextColor(TFT_BLUE, TFT_BLACK);
  tft.drawNumber(desiredTemp, tft.width()/2, ((tft.height()/2)-180), 8);
}

I just wanted to get something working at first but my thoughts were to add a button state of HELD and then allow the option of passing in the time required to be considered held and then a default increment to use when in the HELD state. There's many ways to handle this so it might get too complicated (for instance after you wait the allotted time to enter into the HELD state do you respond immediately or do you wait before incrementing. Maybe all that is handled in actual callback instead of baking in something)?

Just thinking out loud to see what others think...

GeorgeIoak avatar Mar 21 '22 17:03 GeorgeIoak

I will think on this further.

I am wondering if a new action is needed. Have you tried the original eBotton example sketch and looked at the serial outputs? The pressAction of the on/off button code does 3 things, the last one seems to be exactly what you need. Try "press and holding" the "on/off button", the 3 things the callback handles are:

  1. button toggle
  2. still pressing reports for 1 seconds
  3. after the 1 second timeout it reports "Stop pressing my buttton......." as long as it remains pressed

Is that what you need?

Bodmer avatar Mar 22 '22 01:03 Bodmer

I did play with your example and it's what I based my code on. It was actually close to what I wanted so maybe my thoughts are just not as mainstream as I thought they were. What I was thinking is that when you define a button you should be able to "just" be able to have a HELD state defined based on a parameter that lets you define how long the button must be touched before it is considered HELD.

Your callbacks didn't allow anything to passed in so that's why I created the new function. I wouldn't consider myself a programmer so I may have approached this in the wrong way or maybe you're right and what you've created handles 99% of the use cases and I'm just that odd one!

In my code I chose to wait for the held period before reacting to the continued hold, many times (like in IR remotes) once you hold the button the repeat just blasts out as fast as possible. I kind of thought of it like a rotary dial with acceleration capability so it starts slow and then speeds up the response (not implemented in my code because I have a small control window).

GeorgeIoak avatar Mar 22 '22 16:03 GeorgeIoak

Screen buttons share a lot of common action requirements to physical switch buttons, so I have been looking at this library which seems popular: https://github.com/LennartHennigs/Button2

I think screen and physical button functions can have a very similar API once the screen zone is specified. Then the button type is abstracted and both screen and physical buttons can be supported within the same library. There will be variations since I would like to specify the image icons for the buttons but the action handling would be very similar.

Bodmer avatar Mar 22 '22 17:03 Bodmer

1000% agree with you and that library looks exactly of what I was thinking. Yes, I also agree that in many projects you will have both physical and screen buttons (as well as possibly ESP capacitive buttons) and that library looks like it handles most use cases well.

I didn't look into how it was implemented but one of the attractions to GUIslice (for me) what that the builder allowed creating image buttons which makes perfect sense for a TFT.

GeorgeIoak avatar Mar 22 '22 17:03 GeorgeIoak

I've got an urgent (paid for) hardware project to do now so I will not be able to spend any time developing the TFT_eButton library further for quite a while. I think it addresses the original request from @jack828 for callbacks. The other features discussed would require significant effort. I will pop back to this topic now and then and if I have any "light bulb" moments!

Bodmer avatar Mar 22 '22 23:03 Bodmer

Thanks for letting up know and good luck on the project. I'm up to my eyeballs as well but if there's anything I can do to help feel free to ask. I'll be plugging away at this as well so if I make any great revolutions I'll post a comment.

GeorgeIoak avatar Mar 23 '22 00:03 GeorgeIoak

I have ported a library from Kris Kasprzak that seems to work quite well. Copy here: https://github.com/Bodmer/ILI9341_t3_controls It has a few quirks but it produces some nice looking functional screens. All the examples should work OK, if touch is required then your screen will need to be calibrated. The sketch for this is included as an example.

Bodmer avatar Apr 01 '22 00:04 Bodmer

You're supposed to be working on that hardware project!

I'm up to my eyeballs but I'll definitely give this a try when I get a chance as it looks interesting.

GeorgeIoak avatar Apr 01 '22 01:04 GeorgeIoak

Hey folks. Sorry for the radio silence, I've been someplace sunny. I'm also up to the eyeballs in stuff but I'm very glad to see all this progress on the idea. It's quite likely that I'll not have time to go back to personal projects for a few weeks, but, if I do then this'll take priority.

jack828 avatar Apr 04 '22 10:04 jack828

I think most of these ideas are now built into the TFT_eWidget library and the Button_demo example.

Bodmer avatar Nov 05 '22 22:11 Bodmer