diff --git a/examples/Basics/Buttons_and_Events/Buttons_and_Events.ino b/examples/Basics/Buttons_and_Events/Buttons_and_Events.ino new file mode 100644 index 00000000..558a23d0 --- /dev/null +++ b/examples/Basics/Buttons_and_Events/Buttons_and_Events.ino @@ -0,0 +1,21 @@ +#include + +void setup() { + M5.begin(); + M5.BtnA.setLabel("Test"); + M5.BtnB.setLabel("Wow !"); + M5.BtnC.setLabel("Amazing !"); + M5.BtnA.off = M5.BtnB.off = M5.BtnC.off = {BLUE, WHITE, NODRAW}; + M5.BtnA.on = M5.BtnB.on = M5.BtnC.on = {RED, WHITE, NODRAW}; + M5.Buttons.addHandler(eventDisplay); + M5.Buttons.draw(); +} + +void loop() { + M5.update(); +} + +void eventDisplay(Event& e) { + Serial.printf("\n%-12s %-18s", e.typeName(), e.objName()); + if (e.type == E_RELEASE || e.type == E_PRESSED) Serial.printf("%5d ms", e.duration); +} diff --git a/examples/M5Button/Combi_Buttons_AB_BC/Combi_Buttons_AB_BC.ino b/examples/M5Button/Combi_Buttons_AB_BC/Combi_Buttons_AB_BC.ino new file mode 100644 index 00000000..aad1826e --- /dev/null +++ b/examples/M5Button/Combi_Buttons_AB_BC/Combi_Buttons_AB_BC.ino @@ -0,0 +1,60 @@ +/* + + This example will show that the original buttons will not fire any + high-level events after the combinations of buttons are detected, and their + buttons will generate these events instead. Notice how it is not printing + the low-level (E_TOUCH and E_RELEASE) events to not clutter the display. + + See https://github.com/m5stack/M5Stack/blob/master/src/utility/M5Button.h + for complete documentation of buttons and events. + +*/ + +#include + +// Shortcuts: the & means it's a reference to the same object +Button& A = M5.BtnA; +Button& B = M5.BtnB; +Button& C = M5.BtnC; + +// New buttons, not tied to hardware pins +Button AB = Button(55, 193, 102, 21, true, "BtnAB"); +Button BC = Button(161, 193, 102, 21, true, "BtnBC"); + +void setup() { + M5.begin(); + A.off = B.off = C.off = AB.off = BC.off = {BLUE, WHITE, NODRAW}; + A.on = B.on = C.on = AB.on = BC.on = {RED, WHITE, NODRAW}; + M5.Buttons.draw(); + M5.Buttons.addHandler(eventDisplay, E_ALL - E_TOUCH - E_RELEASE); + M5.Buttons.addHandler(buttonDown, E_TOUCH); + M5.Buttons.addHandler(buttonUp, E_RELEASE); +} + +void loop() { + M5.update(); +} + +void buttonDown(Event& e) { + if (A && B && !AB) { + A.cancel(); + B.cancel(); + AB.fingerDown(); + } + if (B && C && !BC) { + B.cancel(); + C.cancel(); + BC.fingerDown(); + } +} + +void buttonUp(Event& e) { + if (AB && !(A && B)) AB.fingerUp(); + if (BC && !(B && C)) BC.fingerUp(); +} + + +void eventDisplay(Event& e) { + Serial.printf("%-14s %-18s %5d ms\n", e.typeName(), e.objName(), + e.duration); +} diff --git a/examples/M5Button/Responsive_Labels/Responsive_Labels.ino b/examples/M5Button/Responsive_Labels/Responsive_Labels.ino new file mode 100644 index 00000000..b0faa992 --- /dev/null +++ b/examples/M5Button/Responsive_Labels/Responsive_Labels.ino @@ -0,0 +1,12 @@ +#include + +void setup() { + M5.BtnA.off = M5.BtnB.off = M5.BtnC.off = {BLUE, WHITE, NODRAW}; + M5.BtnA.on = M5.BtnB.on = M5.BtnC.on = {RED, WHITE, NODRAW}; + M5.begin(); + M5.Buttons.draw(); +} + +void loop() { + M5.update(); +} diff --git a/keywords.txt b/keywords.txt index 68a18694..8716f7c8 100644 --- a/keywords.txt +++ b/keywords.txt @@ -27,14 +27,104 @@ BtnC KEYWORD1 begin KEYWORD2 update KEYWORD2 Power KEYWORD2 + + +# Zone +Zone KEYWORD1 +rot1 KEYWORD2 +set KEYWORD2 +contains KEYWORD2 +rotate KEYWORD2 + +# Point +Point KEYWORD1 +in KEYWORD2 +Equals KEYWORD2 +distanceTo KEYWORD2 +directionTo KEYWORD2 +isDirectionTo KEYWORD2 +valid KEYWORD2 + +ButtonColors KEYWORD1 +bg KEYWORD2 +text KEYWORD2 +outline KEYWORD2 + +Event KEYWORD1 +TFT_eSPI_Button KEYWORD1 + +instance KEYWORD2 +instances KEYWORD2 + +#Events +fireEvent KEYWORD2 +typeName KEYWORD2 +objName KEYWORD2 +finger KEYWORD2 +type KEYWORD2 +from KEYWORD2 +to KEYWORD2 +duration KEYWORD2 +distance KEYWORD2 +direction KEYWORD2 +isDirection KEYWORD2 +button KEYWORD2 +gesture KEYWORD2 + +E_TOUCH LITERAL1 +E_RELEASE LITERAL1 +E_MOVE LITERAL1 +E_GESTURE LITERAL1 +E_TAP LITERAL1 +E_DBLTAP LITERAL1 +E_DRAGGED LITERAL1 +E_PRESSED LITERAL1 +E_PRESSING LITERAL1 +E_LONGPRESSED LITERAL1 +E_LONGPRESSING LITERAL1 + +UP LITERAL1 +DOWN LITERAL1 +LEFT LITERAL1 +RIGHT LITERAL1 + +# Button +Button KEYWORD1 isPressed KEYWORD2 wasPressed KEYWORD2 pressedFor KEYWORD2 - isReleased KEYWORD2 wasReleased KEYWORD2 releasedFor KEYWORD2 wasReleasefor KEYWORD2 +addHandler KEYWORD2 +delHandlers KEYWORD2 +lastChange KEYWORD2 +cancel KEYWORD2 +fingerDown KEYWORD2 +fingerUp KEYWORD2 +fingerMove KEYWORD2 +name KEYWORD2 +setLabel KEYWORD2 +event KEYWORD2 +tapTime KEYWORD2 +dbltapTime KEYWORD2 +longpressTime KEYWORD2 +instanceIndex KEYWORD2 +drawFn KEYWORD2 +repeatDelay KEYWORD2 +repeatinterval KEYWORD2 +on KEYWORD2 +off KEYWORD2 +dx KEYWORD2 +dy KEYWORD2 + + +#Buttons +Buttons KEYWORD1 +draw KEYWORD2 +which KEYWORD2 +fireEvent KEYWORD2 held KEYWORD2 repeat KEYWORD2 @@ -72,8 +162,12 @@ textWrap KEYWORD2 fontWidth KEYWORD2 fontHeight KEYWORD2 setFont KEYWORD2 +setTextFont KEYWORD2 +setFreeFont KEYWORD2 setTextColor KEYWORD2 setTextSize KEYWORD2 +pushState KEYWORD2 +popState KEYWORD2 WHITE LITERAL1 BLACK LITERAL1 @@ -82,3 +176,4 @@ GRAY LITERAL1 RED LITERAL1 BLUE LITERAL1 GREEN LITERAL1 +NODRAW LITERAL1 diff --git a/src/M5Display.cpp b/src/M5Display.cpp index 1d920a85..e86e07f7 100644 --- a/src/M5Display.cpp +++ b/src/M5Display.cpp @@ -1,8 +1,17 @@ #include "M5Display.h" +#ifdef M5Stack_M5Core2 +#include +#endif /* M5Stack_M5Core2 */ + #define BLK_PWM_CHANNEL 7 // LEDC_CHANNEL_7 -M5Display::M5Display() : TFT_eSPI() {} +// So we can use this instance without including all of M5Core2 / M5Stack +M5Display* M5Display::instance; + +M5Display::M5Display() : TFT_eSPI() { + if (!instance) instance = this; +} void M5Display::begin() { TFT_eSPI::begin(); @@ -39,7 +48,7 @@ void M5Display::drawBitmap(int16_t x0, int16_t y0, int16_t w, int16_t h, const u } void M5Display::drawBitmap(int16_t x0, int16_t y0, int16_t w, int16_t h, uint16_t *data) { - bool swap = getSwapBytes(); + bool swap = getSwapBytes(); setSwapBytes(true); pushImage((int32_t)x0, (int32_t)y0, (uint32_t)w, (uint32_t)h, data); setSwapBytes(swap); @@ -607,3 +616,71 @@ void M5Display::drawPngUrl(const char *url, uint16_t x, uint16_t y, pngle_destroy(pngle); http.end(); } + + +// Saves and restores font properties, datum, cursor, colors + +void M5Display::pushState() { + DisplayState s; + s.textfont = textfont; + s.textsize = textsize; + s.textcolor = textcolor; + s.textbgcolor = textbgcolor; + s.cursor_x = cursor_x; + s.cursor_y = cursor_y; + s.padX = padX; + s.gfxFont = gfxFont; + _displayStateStack.push_back(s); +} + +void M5Display::popState() { + if (_displayStateStack.empty()) return; + DisplayState s = _displayStateStack.back(); + _displayStateStack.pop_back(); + textfont = s.textfont; + textsize = s.textsize; + textcolor = s.textcolor; + textbgcolor = s.textbgcolor; + cursor_x = s.cursor_x; + cursor_y = s.cursor_y; + padX = s.padX; + if (s.gfxFont && s.gfxFont != gfxFont) setFreeFont(s.gfxFont); +} + +#ifdef M5Stack_M5Core2 + +#ifdef TFT_eSPI_TOUCH_EMULATION + +// Emulates the native (resistive) TFT_eSPI touch interface using M5.Touch + +uint8_t M5Display::getTouchRaw(uint16_t *x, uint16_t *y) { + return getTouch(x, y); +} + +uint16_t M5Display::getTouchRawZ(void) { + return (TOUCH->ispressed()) ? 1000 : 0; +} + +void M5Display::convertRawXY(uint16_t *x, uint16_t *y) { return; } + +uint8_t M5Display::getTouch(uint16_t *x, uint16_t *y, + uint16_t threshold /* = 600 */) { + TOUCH->read(); + if (TOUCH->points) { + *x = TOUCH->point[0].x; + *y = TOUCH->point[0].y; + return true; + } + return false; +} + +void M5Display::calibrateTouch(uint16_t *data, uint32_t color_fg, + uint32_t color_bg, uint8_t size) { + return; +} + +void M5Display::setTouch(uint16_t *data) { return; } + +#endif /* TFT_eSPI_TOUCH_EMULATION */ + +#endif /* M5Stack_M5Core2 */ diff --git a/src/M5Display.h b/src/M5Display.h index b5818a45..3a1b2fbe 100644 --- a/src/M5Display.h +++ b/src/M5Display.h @@ -4,6 +4,8 @@ #include #include #include + + #include "utility/Config.h" // This is where Core2 defines would be #include "utility/In_eSPI.h" #include "utility/Sprite.h" @@ -15,8 +17,16 @@ JPEG_DIV_MAX } jpeg_div_t; + struct DisplayState { + uint8_t textfont, textsize, datum; + const GFXfont *gfxFont; + uint32_t textcolor, textbgcolor; + int32_t cursor_x, cursor_y, padX; + }; + class M5Display : public TFT_eSPI { public: + static M5Display* instance; M5Display(); void begin(); void sleep(); @@ -38,7 +48,7 @@ #if defined (SPI_HAS_TRANSACTION) && defined (SUPPORT_TRANSACTIONS) && !defined(ESP32_PARALLEL) if(!inTransaction) { if (!locked) { - locked = true; + locked = true; SPI.endTransaction(); } } @@ -92,6 +102,34 @@ uint16_t offX = 0, uint16_t offY = 0, double scale = 1.0, uint8_t alphaThreshold = 127); - private: - }; -#endif + // Saves and restores font properties, datum, cursor and colors so + // code can be non-invasive. Just make sure that every push is also + // popped when you're done to prevent stack from growing. + // + // (User code can never do this completely because the gfxFont + // class variable of TFT_eSPI is protected.) + #define M5DISPLAY_HAS_PUSH_POP + public: + void pushState(); + void popState(); + + private: + std::vector _displayStateStack; + + #ifdef M5Stack_M5Core2 + + #ifdef TFT_eSPI_TOUCH_EMULATION + // Emulates the TFT_eSPI touch interface using M5.Touch + public: + uint8_t getTouchRaw(uint16_t *x, uint16_t *y); + uint16_t getTouchRawZ(void); + void convertRawXY(uint16_t *x, uint16_t *y); + uint8_t getTouch(uint16_t *x, uint16_t *y, uint16_t threshold = 600); + void calibrateTouch(uint16_t *data, uint32_t color_fg, uint32_t color_bg, + uint8_t size); + void setTouch(uint16_t *data); + #endif /* TFT_eSPI_TOUCH_EMULATION */ + + #endif /* M5Stack_M5Core2 */ +}; +#endif /* _M5DISPLAY_H_ */ diff --git a/src/M5Stack.cpp b/src/M5Stack.cpp index 94729c5f..20b696f2 100644 --- a/src/M5Stack.cpp +++ b/src/M5Stack.cpp @@ -47,15 +47,13 @@ void M5Stack::begin(bool LCDEnable, bool SDEnable, bool SerialEnable, bool I2CEn Serial.println("OK"); } - // if use M5GO button, need set gpio15 OD or PP mode to avoid affecting the wifi signal + // if use M5GO button, need set gpio15 OD or PP mode to avoid affecting the wifi signal pinMode(15, OUTPUT_OPEN_DRAIN); } void M5Stack::update() { //Button update - BtnA.read(); - BtnB.read(); - BtnC.read(); + Buttons.update(); //Speaker update Speaker.update(); diff --git a/src/M5Stack.h b/src/M5Stack.h index f0f00f85..6d6a5d7c 100644 --- a/src/M5Stack.h +++ b/src/M5Stack.h @@ -95,7 +95,7 @@ #ifndef _M5STACK_H_ #define _M5STACK_H_ - + #if defined(ESP32) #include "gitTagVersion.h" @@ -107,7 +107,7 @@ #include "M5Display.h" #include "utility/Config.h" - #include "utility/Button.h" + #include "utility/M5Button.h" #include "utility/Speaker.h" #include "utility/Power.h" #include "utility/CommUtil.h" @@ -125,17 +125,23 @@ void begin(bool LCDEnable = true, bool SDEnable = true, bool SerialEnable = true, bool I2CEnable = false); void update(); + // Buttons (for things that involve all buttons) + M5Buttons Buttons; + // Button API #define DEBOUNCE_MS 10 - Button BtnA = Button(BUTTON_A_PIN, true, DEBOUNCE_MS); - Button BtnB = Button(BUTTON_B_PIN, true, DEBOUNCE_MS); - Button BtnC = Button(BUTTON_C_PIN, true, DEBOUNCE_MS); + Button BtnA = Button(BUTTON_A_PIN, true, DEBOUNCE_MS, "hw", + 3, 218, 102, 21, true, "BtnA"); + Button BtnB = Button(BUTTON_B_PIN, true, DEBOUNCE_MS, "hw", + 109, 218, 102, 21, true, "BtnB"); + Button BtnC = Button(BUTTON_C_PIN, true, DEBOUNCE_MS, "hw", + 215, 218, 102, 21, true, "BtnC"); // SPEAKER SPEAKER Speaker; // LCD - M5Display Lcd = M5Display(); + M5Display Lcd; //Power POWER Power; @@ -151,8 +157,8 @@ #endif // I2C - CommUtil I2C = CommUtil(); - + CommUtil I2C; + /** * Function has been move to Power class.(for compatibility) * This name will be removed in a future release. @@ -160,11 +166,11 @@ void setPowerBoostKeepOn(bool en) __attribute__((deprecated)); void setWakeupButton(uint8_t button) __attribute__((deprecated)); void powerOFF() __attribute__((deprecated)); - + private: bool isInited; }; - + extern M5Stack M5; #define m5 M5 #define lcd Lcd diff --git a/src/utility/Button.cpp b/src/utility/Button.cpp deleted file mode 100644 index 9077399d..00000000 --- a/src/utility/Button.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/*----------------------------------------------------------------------* - * Arduino Button Library v1.0 * - * Jack Christensen May 2011, published Mar 2012 * - * * - * Library for reading momentary contact switches like tactile button * - * switches. Intended for use in state machine constructs. * - * Use the read() function to read all buttons in the main loop, * - * which should execute as fast as possible. * - * * - * This work is licensed under the Creative Commons Attribution- * - * ShareAlike 3.0 Unported License. To view a copy of this license, * - * visit http://creativecommons.org/licenses/by-sa/3.0/ or send a * - * letter to Creative Commons, 171 Second Street, Suite 300, * - * San Francisco, California, 94105, USA. * - *----------------------------------------------------------------------*/ - -#include "Button.h" - -/*----------------------------------------------------------------------* - * Button(pin, puEnable, invert, dbTime) instantiates a button object. * - * pin Is the Arduino pin the button is connected to. * - * puEnable Enables the AVR internal pullup resistor if != 0 (can also * - * use true or false). * - * invert If invert == 0, interprets a high state as pressed, low as * - * released. If invert != 0, interprets a high state as * - * released, low as pressed (can also use true or false). * - * dbTime Is the debounce time in milliseconds. * - * * - * (Note that invert cannot be implied from puEnable since an external * - * pullup could be used.) * - *----------------------------------------------------------------------*/ -Button::Button(uint8_t pin, uint8_t invert, uint32_t dbTime) { - _pin = pin; - _invert = invert; - _dbTime = dbTime; - pinMode(_pin, INPUT_PULLUP); - _state = digitalRead(_pin); - if (_invert != 0) _state = !_state; - _time = millis(); - _lastState = _state; - _changed = 0; - _hold_time = -1; - _lastTime = _time; - _lastChange = _time; - _pressTime = _time; -} - -/*----------------------------------------------------------------------* - * read() returns the state of the button, 1==pressed, 0==released, * - * does debouncing, captures and maintains times, previous states, etc. * - *----------------------------------------------------------------------*/ -uint8_t Button::read(void) { - static uint32_t ms; - static uint8_t pinVal; - - ms = millis(); - pinVal = digitalRead(_pin); - if (_invert != 0) pinVal = !pinVal; - if (ms - _lastChange < _dbTime) { - _lastTime = _time; - _time = ms; - _changed = 0; - return _state; - } - else { - _lastTime = _time; - _time = ms; - _lastState = _state; - _state = pinVal; - if (_state != _lastState) { - _lastChange = ms; - _changed = 1; - if (_state) { _pressTime = _time; } - } - else { - _changed = 0; - } - return _state; - } -} - -/*----------------------------------------------------------------------* - * isPressed() and isReleased() check the button state when it was last * - * read, and return false (0) or true (!=0) accordingly. * - * These functions do not cause the button to be read. * - *----------------------------------------------------------------------*/ -uint8_t Button::isPressed(void) { - return _state == 0 ? 0 : 1; -} - -uint8_t Button::isReleased(void) { - return _state == 0 ? 1 : 0; -} - -/*----------------------------------------------------------------------* - * wasPressed() and wasReleased() check the button state to see if it * - * changed between the last two reads and return false (0) or * - * true (!=0) accordingly. * - * These functions do not cause the button to be read. * - *----------------------------------------------------------------------*/ -uint8_t Button::wasPressed(void) { - return _state && _changed; -} - -uint8_t Button::wasReleased(void) { - return !_state && _changed && millis() - _pressTime < _hold_time; -} - -uint8_t Button::wasReleasefor(uint32_t ms) { - _hold_time = ms; - return !_state && _changed && millis() - _pressTime >= ms; -} -/*----------------------------------------------------------------------* - * pressedFor(ms) and releasedFor(ms) check to see if the button is * - * pressed (or released), and has been in that state for the specified * - * time in milliseconds. Returns false (0) or true (1) accordingly. * - * These functions do not cause the button to be read. * - *----------------------------------------------------------------------*/ -uint8_t Button::pressedFor(uint32_t ms) { - return (_state == 1 && _time - _lastChange >= ms) ? 1 : 0; -} - -uint8_t Button::pressedFor(uint32_t ms, uint32_t continuous_time) { - if (_state == 1 && _time - _lastChange >= ms && _time - _lastLongPress >= continuous_time) { - _lastLongPress = _time; - return 1; - } - return 0; -} - -uint8_t Button::releasedFor(uint32_t ms) { - return (_state == 0 && _time - _lastChange >= ms) ? 1 : 0; -} -/*----------------------------------------------------------------------* - * lastChange() returns the time the button last changed state, * - * in milliseconds. * - *----------------------------------------------------------------------*/ -uint32_t Button::lastChange(void) { - return _lastChange; -} diff --git a/src/utility/Button.h b/src/utility/Button.h deleted file mode 100644 index 13298040..00000000 --- a/src/utility/Button.h +++ /dev/null @@ -1,47 +0,0 @@ -/*----------------------------------------------------------------------* - * Arduino Button Library v1.0 * - * Jack Christensen Mar 2012 * - * * - * This work is licensed under the Creative Commons Attribution- * - * ShareAlike 3.0 Unported License. To view a copy of this license, * - * visit http://creativecommons.org/licenses/by-sa/3.0/ or send a * - * letter to Creative Commons, 171 Second Street, Suite 300, * - * San Francisco, California, 94105, USA. * - *----------------------------------------------------------------------*/ -#ifndef Button_h -#define Button_h -// #if ARDUINO >= 100 -#include -// #else -// #include -// #endif -class Button { - public: - Button(uint8_t pin, uint8_t invert, uint32_t dbTime); - uint8_t read(); - uint8_t isPressed(); - uint8_t isReleased(); - uint8_t wasPressed(); - uint8_t wasReleased(); - uint8_t pressedFor(uint32_t ms); - uint8_t pressedFor(uint32_t ms, uint32_t continuous_time); - uint8_t releasedFor(uint32_t ms); - uint8_t wasReleasefor(uint32_t ms); - uint32_t lastChange(); - - private: - uint8_t _pin; //arduino pin number - uint8_t _puEnable; //internal pullup resistor enabled - uint8_t _invert; //if 0, interpret high state as pressed, else interpret low state as pressed - uint8_t _state; //current button state - uint8_t _lastState; //previous button state - uint8_t _changed; //state changed since last read - uint32_t _time; //time of current state (all times are in ms) - uint32_t _lastTime; //time of previous state - uint32_t _lastChange; //time of last state change - uint32_t _lastLongPress; //time of last state change - uint32_t _dbTime; //debounce time - uint32_t _pressTime; //press time - uint32_t _hold_time; //hold time call wasreleasefor -}; -#endif diff --git a/src/utility/Config.h b/src/utility/Config.h index 7b390837..9996b3a0 100644 --- a/src/utility/Config.h +++ b/src/utility/Config.h @@ -1,6 +1,9 @@ #ifndef _CONFIG_H_ #define _CONFIG_H_ + #define TFT M5Display::instance + #define BUTTONS M5Buttons::instance + // Screen #define TFT_LED_PIN 32 #define TFT_DC_PIN 27 diff --git a/src/utility/M5Button.cpp b/src/utility/M5Button.cpp new file mode 100644 index 00000000..9e8f2cd5 --- /dev/null +++ b/src/utility/M5Button.cpp @@ -0,0 +1,722 @@ +#include "M5Button.h" + +// Button class + +/* static */ std::vector Button::instances; + +Button::Button(int16_t x_, int16_t y_, int16_t w_, int16_t h_, + bool rot1_ /* = false */, const char* name_ /* = "" */, + ButtonColors off_ /*= {NODRAW, NODRAW, NODRAW} */, + ButtonColors on_ /* = {NODRAW, NODRAW, NODRAW} */, + uint8_t datum_ /* = BUTTON_DATUM */, int16_t dx_ /* = 0 */, + int16_t dy_ /* = 0 */, uint8_t r_ /* = 0xFF */ + ) + : Zone(x_, y_, w_, h_, rot1_) { + _pin = 0xFF; + _invert = false; + _dbTime = 0; + strncpy(_name, name_, 15); + off = off_; + on = on_; + datum = datum_; + dx = dx_; + dy = dy_; + r = r_; + init(); +} + +Button::Button(uint8_t pin_, uint8_t invert_, uint32_t dbTime_, + String hw_ /* = "hw" */, int16_t x_ /* = 0 */, + int16_t y_ /* = 0 */, int16_t w_ /* = 0 */, int16_t h_ /* = 0 */, + bool rot1_ /* = false */, const char* name_ /* = "" */, + ButtonColors off_ /*= {NODRAW, NODRAW, NODRAW} */, + ButtonColors on_ /* = {NODRAW, NODRAW, NODRAW} */, + uint8_t datum_ /* = BUTTON_DATUM */, int16_t dx_ /* = 0 */, + int16_t dy_ /* = 0 */, uint8_t r_ /* = 0xFF */ + ) + : Zone(x_, y_, w_, h_, rot1_) { + _pin = pin_; + _invert = invert_; + _dbTime = dbTime_; + strncpy(_name, name_, 15); + off = off_; + on = on_; + datum = datum_; + dx = dx_; + dy = dy_; + r = r_; + init(); +} + +Button::~Button() { + for (int i = 0; i < instances.size(); ++i) { + if (instances[i] == this) { + BUTTONS->delHandlers(nullptr, this, nullptr); + instances.erase(instances.begin() + i); + return; + } + } +} + +Button::operator bool() { return _state; } + +bool Button::operator==(const Button& b) { return (this == &b); } +bool Button::operator!=(const Button& b) { return (this != &b); } +bool Button::operator==(Button* b) { return (this == b); } +bool Button::operator!=(Button* b) { return (this != b); } + +void Button::init() { + _state = _tapWait = _pressing = _manuallyRead = false; + _time = _lastChange = _pressTime = millis(); + _hold_time = -1; + _textFont = _textSize = 0; + _freeFont = nullptr; + drawFn = nullptr; + _compat = 0; + drawZone = Zone(); + tapTime = TAP_TIME; + dbltapTime = DBLTAP_TIME; + longPressTime = LONGPRESS_TIME; + repeatDelay = REPEAT_DELAY; + repeatInterval = REPEAT_INTERVAL; + strncpy(_label, _name, 16); + if (_pin != 0xFF) pinMode(_pin, INPUT_PULLUP); + instances.push_back(this); + draw(); +} + +int16_t Button::instanceIndex() { + for (int16_t i = 0; i < instances.size(); ++i) { + if (instances[i] == this) return i; + } + return -1; +} + +bool Button::read(bool manualRead /* = true */) { + if (manualRead) _manuallyRead = true; + event = Event(); + if (_changed) { + _changed = false; + _lastChange = _time; + if (!_state && !_cancelled && postReleaseEvents()) return _state; + } else { + if (!_cancelled && timeoutEvents()) return _state; + if (!_state) _cancelled = false; + } + // Do actual read from the pin if this is a hardware button + _time = millis(); + uint8_t newState = false; + if (_pin != 0xFF) { + newState = (digitalRead(_pin)); + newState = _invert ? !newState : newState; + if (newState != _state && _time - _lastChange >= _dbTime) { + if (newState) fingerDown(); + if (!newState) fingerUp(); + } + } + return _state; +} + +void Button::fingerDown(Point pos /* = Point() */, + uint8_t finger /* = 0 */) { + _finger = finger; + _currentPt[finger] = _fromPt[finger] = pos; + if (!_state && !_currentPt[1 - finger]) { + // other finger not here + _state = true; + _changed = true; + _pressTime = _time; + draw(); + } + BUTTONS->fireEvent(finger, E_TOUCH, pos, pos, 0, this, nullptr); +} + +void Button::fingerUp(uint8_t finger /* = 0 */) { + uint32_t duration = _time - _pressTime; + _finger = finger; + _toPt[finger] = _currentPt[finger]; + _currentPt[finger] = Point(); + if (_state && !_currentPt[1 - finger]) { + // other finger not here + _state = false; + _changed = true; + draw(); + } + BUTTONS->fireEvent(finger, E_RELEASE, _fromPt[finger], _toPt[finger], + duration, this, nullptr); +} + +void Button::fingerMove(Point pos, uint8_t finger) { + BUTTONS->fireEvent(finger, E_MOVE, _currentPt[finger], pos, + _time - _lastChange, this, nullptr); + _currentPt[finger] = pos; +} + +bool Button::postReleaseEvents() { + uint32_t duration = _time - _pressTime; + if (_toPt[_finger] && !contains(_toPt[_finger])) { + BUTTONS->fireEvent(_finger, E_DRAGGED, _fromPt[_finger], _toPt[_finger], + duration, this, nullptr); + _tapWait = false; + _pressing = false; + _longPressing = false; + return true; + } + if (duration <= tapTime) { + if (_tapWait) { + BUTTONS->fireEvent(_finger, E_DBLTAP, _fromPt[_finger], _toPt[_finger], + duration, this, nullptr); + _tapWait = false; + _pressing = false; + _longPressing = false; + return true; + } + _tapWait = true; + } else if (_pressing) { + BUTTONS->fireEvent(_finger, _longPressing ? E_LONGPRESSED : E_PRESSED, + _fromPt[_finger], _toPt[_finger], duration, this, + nullptr); + _pressing = false; + _longPressing = false; + return true; + } + return false; +} + +bool Button::timeoutEvents() { + uint32_t duration = _time - _pressTime; + if (_tapWait && duration >= dbltapTime) { + BUTTONS->fireEvent(_finger, E_TAP, _fromPt[_finger], _toPt[_finger], + duration, this, nullptr); + _tapWait = false; + _pressing = false; + return true; + } + if (!_state) return false; + if ((!_pressing && duration > tapTime) || + (repeatDelay && duration > repeatDelay && + _time - _lastRepeat > repeatInterval)) { + BUTTONS->fireEvent(_finger, E_PRESSING, _fromPt[_finger], + _currentPt[_finger], duration, this, nullptr); + _lastRepeat = _time; + _pressing = true; + return true; + } + if (longPressTime && !_longPressing && duration > longPressTime) { + BUTTONS->fireEvent(_finger, E_LONGPRESSING, _fromPt[_finger], + _currentPt[_finger], duration, this, nullptr); + _longPressing = true; + return true; + } + return false; +} + +void Button::cancel() { + _cancelled = true; + _tapWait = false; + draw(off); +} + +char* Button::getName() { return _name; } + +bool Button::isPressed() { return _state; } + +bool Button::isReleased() { return !_state; } + +bool Button::wasPressed() { return _state && _changed; } + +bool Button::wasReleased() { + return (!_state && _changed && millis() - _pressTime < _hold_time); +} + +bool Button::wasReleasefor(uint32_t ms) { + _hold_time = ms; + return (!_state && _changed && millis() - _pressTime >= ms); +} + +bool Button::pressedFor(uint32_t ms) { + return (_state && _time - _lastChange >= ms); +} + +bool Button::pressedFor(uint32_t ms, uint32_t continuous_time) { + if (_state && _time - _lastChange >= ms && + _time - _lastLongPress >= continuous_time) { + _lastLongPress = _time; + return true; + } + return false; +} + +bool Button::releasedFor(uint32_t ms) { + return (!_state && _time - _lastChange >= ms); +} + +uint32_t Button::lastChange() { return (_lastChange); } + +void Button::addHandler(void (*fn)(Event&), uint16_t eventMask /* = E_ALL */) { + BUTTONS->addHandler(fn, eventMask, this, nullptr); +} + +void Button::delHandlers(void (*fn)(Event&) /* = nullptr */) { + BUTTONS->delHandlers(fn, this, nullptr); +} + +// visual things for Button + +void Button::draw() { + if (_state) + draw(on); + else + draw(off); +} + +void Button::erase(uint16_t color /* = BLACK */) { + draw({color, NODRAW, NODRAW}); +} + +void Button::draw(ButtonColors bc) { + _hidden = false; + // use locally set draw function if aplicable, global one otherwise + if (drawFn) { + drawFn(*this, bc); + } else if (BUTTONS->drawFn) { + BUTTONS->drawFn(*this, bc); + } +} + +void Button::hide(uint16_t color /* = NODRAW */) { + _hidden = true; + if (color != NODRAW) erase(color); +} + +char* Button::label() { return _label; } + +void Button::setLabel(const char* label_) { strncpy(_label, label_, 50); } + +void Button::setFont(const GFXfont* freeFont_) { + _freeFont = freeFont_; + _textFont = 1; +} + +void Button::setFont(uint8_t textFont_ /* = 0 */) { + _freeFont = nullptr; + _textFont = textFont_; +} + +void Button::setTextSize(uint8_t textSize_ /* = 0 */) { _textSize = textSize_; } + + +// M5Buttons class + +/* static */ M5Buttons* M5Buttons::instance; + +/* static */ void M5Buttons::drawFunction(Button& b, ButtonColors bc) { + if (bc.bg == NODRAW && bc.outline == NODRAW && bc.text == NODRAW) return; + Zone z = (b.drawZone) ? b.drawZone : b; + if (z.rot1) z.rotate(TFT->rotation); + + uint8_t r = (b.r == 0xFF) ? min(z.w, z.h) / 4 : b.r; + + if (bc.bg != NODRAW) { + if (r >= 2) { + TFT->fillRoundRect(z.x, z.y, z.w, z.h, r, bc.bg); + } else { + TFT->fillRect(z.x, z.y, z.w, z.h, bc.bg); + } + } + + if (bc.outline != NODRAW) { + if (r >= 2) { + TFT->drawRoundRect(z.x, z.y, z.w, z.h, r, bc.outline); + } else { + TFT->drawRect(z.x, z.y, z.w, z.h, bc.outline); + } + } + + if (bc.text != NODRAW && bc.text != bc.bg && strlen(b._label)) { + // figure out where to put the text + uint16_t tx, ty; + tx = z.x + (z.w / 2); + ty = z.y + (z.h / 2); + + if (!b._compat) { + uint8_t margin = max(r / 2, 6); + switch (b.datum) { + case TL_DATUM: + case ML_DATUM: + case BL_DATUM: + tx = z.x + margin; + break; + case TR_DATUM: + case MR_DATUM: + case BR_DATUM: + tx = z.x + z.w - margin; + break; + } + switch (b.datum) { + case TL_DATUM: + case TC_DATUM: + case TR_DATUM: + ty = z.y + margin; + break; + case BL_DATUM: + case BC_DATUM: + case BR_DATUM: + ty = z.y + z.h - margin; + break; + } + } + + // Save state + uint8_t tempdatum = TFT->getTextDatum(); + uint16_t tempPadding = TFT->padX; + if (!b._compat) TFT->pushState(); + + // Actual drawing of text + TFT->setTextColor(bc.text); + if (b._textSize) + TFT->setTextSize(b._textSize); + else + TFT->setTextSize(BUTTONS->_textSize); + if (b._textFont) { + if (b._freeFont) + TFT->setFreeFont(b._freeFont); + else + TFT->setTextFont(b._textFont); + } else { + if (BUTTONS->_freeFont) + TFT->setFreeFont(BUTTONS->_freeFont); + else + TFT->setTextFont(BUTTONS->_textFont); + } + TFT->setTextDatum(b.datum); + TFT->setTextPadding(0); + TFT->drawString(b._label, tx + b.dx, ty + b.dy); + // Set state back + if (!b._compat) { + TFT->popState(); + } else { + TFT->setTextDatum(tempdatum); + TFT->setTextPadding(tempPadding); + } + } +} + +M5Buttons::M5Buttons() { + if (!instance) instance = this; + drawFn = drawFunction; + _freeFont = BUTTON_FREEFONT; + _textFont = BUTTON_TEXTFONT; + _textSize = BUTTON_TEXTSIZE; +} + +Button* M5Buttons::which(Point& p) { + if (!Button::instances.size()) return nullptr; + for (int i = Button::instances.size() - 1; i >= 0; --i) { + Button* b = Button::instances[i]; + // Always return button when i == 0 --> background + if (!i || (b->_pin == 0xFF && !b->_hidden && b->contains(p))) return b; + } + return nullptr; +} + +void M5Buttons::draw() { + for (auto button : Button::instances) button->draw(); +} + +void M5Buttons::update() { +#ifdef _M5TOUCH_H_ + for (auto gesture : Gesture::instances) gesture->_detected = false; + BUTTONS->event = Event(); + if (TOUCH->wasRead || _leftovers) { + _finger[TOUCH->point0finger].current = TOUCH->point[0]; + _finger[1 - TOUCH->point0finger].current = TOUCH->point[1]; + _leftovers = true; + for (uint8_t i = 0; i < 2; i++) { + if (i == 1) _leftovers = false; + Finger& fi = _finger[i]; + Point& curr = fi.current; + Point prev = fi.previous; + fi.previous = fi.current; + if (curr == prev) continue; + if (!prev && curr) { + // A new touch happened + fi.startTime = millis(); + fi.startPoint = curr; + fi.button = BUTTONS->which(curr); + if (fi.button) { + fi.button->fingerDown(curr, i); + return; + } + } else if (prev && !curr) { + // Finger removed + uint16_t duration = millis() - fi.startTime; + for (auto gesture : Gesture::instances) { + if (gesture->test(fi.startPoint, prev, duration)) { + BUTTONS->fireEvent(i, E_GESTURE, fi.startPoint, prev, duration, + nullptr, gesture); + if (fi.button) fi.button->cancel(); + break; + } + } + if (fi.button) { + fi.button->fingerUp(i); + return; + } + } else { + // Finger moved + if (fi.button) { + fi.button->fingerMove(curr, i); + return; + } + } + } + } +#endif /* _M5TOUCH_H_ */ + + for (auto button : Button::instances) { + if (!button->_manuallyRead) button->read(false); + } +} + +void M5Buttons::setFont(const GFXfont* freeFont_) { + _freeFont = freeFont_; + _textFont = 1; +} + +void M5Buttons::setFont(uint8_t textFont_) { + _freeFont = nullptr; + _textFont = textFont_; +} + +void M5Buttons::setTextSize(uint8_t textSize_) { _textSize = textSize_; } + +void M5Buttons::fireEvent(uint8_t finger, uint16_t type, Point& from, + Point& to, uint16_t duration, Button* button, + Gesture* gesture) { + Event e; + e.finger = finger; + e.type = type; + e.from = from; + e.to = to; + e.duration = duration; + e.button = button; + e.gesture = gesture; + if (button) button->event = e; + event = e; + for (auto h : _eventHandlers) { + if (!(h.eventMask & e.type)) continue; + if (h.button && h.button != e.button) continue; + if (h.gesture && h.gesture != e.gesture) continue; + h.fn(e); + } +} + +void M5Buttons::addHandler(void (*fn)(Event&), uint16_t eventMask /* = E_ALL */, + Button* button /* = nullptr */, + Gesture* gesture /* = nullptr */ +) { + EventHandler handler; + handler.fn = fn; + handler.eventMask = eventMask; + handler.button = button; + handler.gesture = gesture; + _eventHandlers.push_back(handler); +} + +void M5Buttons::delHandlers(void (*fn)(Event&) /* = nullptr */, + Button* button /* = nullptr */, + Gesture* gesture /* = nullptr */ +) { + for (int i = _eventHandlers.size() - 1; i >= 0; --i) { + if (fn && fn != _eventHandlers[i].fn) continue; + if (button && _eventHandlers[i].button != button) continue; + if (gesture && _eventHandlers[i].gesture != gesture) continue; + _eventHandlers.erase(_eventHandlers.begin() + i); + } +} + +// Gesture class + +std::vector Gesture::instances; + +Gesture::Gesture(Zone fromZone_, Zone toZone_, const char* name_ /* = "" */, + uint16_t minDistance_ /* = GESTURE_MINDIST */, + int16_t direction_ /* = INVALID_VALUE */, + uint8_t plusminus_ /* = PLUSMINUS */, bool rot1_ /* = false */, + uint16_t maxTime_ /* = GESTURE_MAXTIME */ +) { + fromZone = fromZone_; + toZone = toZone_; + strncpy(_name, name_, 15); + minDistance = minDistance_; + direction = direction_; + plusminus = plusminus_; + rot1 = rot1_; + maxTime = maxTime_; + _detected = false; + instances.push_back(this); +} + +Gesture::Gesture(const char* name_ /* = "" */, + uint16_t minDistance_ /* = GESTURE_MINDIST */, + int16_t direction_ /* = INVALID_VALUE */, + uint8_t plusminus_ /* = PLUSMINUS */, bool rot1_ /* = false */, + uint16_t maxTime_ /* = GESTURE_MAXTIME */ +) { + fromZone = ANYWHERE; + toZone = ANYWHERE; + strncpy(_name, name_, 15); + minDistance = minDistance_; + direction = direction_; + plusminus = plusminus_; + rot1 = rot1_; + maxTime = maxTime_; + _detected = false; + instances.push_back(this); +} + +Gesture::~Gesture() { + for (int i = 0; i < instances.size(); ++i) { + if (instances[i] == this) { + instances.erase(instances.begin() + i); + BUTTONS->delHandlers(nullptr, nullptr, this); + return; + } + } +} + +Gesture::operator bool() { return _detected; } + +int16_t Gesture::instanceIndex() { + for (int16_t i = 0; i < instances.size(); ++i) { + if (instances[i] == this) return i; + } + return -1; +} + +char* Gesture::getName() { return _name; } + +bool Gesture::test(Point& from, Point& to, uint16_t duration) { + if (from.distanceTo(to) < minDistance) return false; + if (fromZone && !fromZone.contains(from)) return false; + if (toZone && !toZone.contains(to)) return false; + if (direction != INVALID_VALUE && + !from.isDirectionTo(to, direction, plusminus, rot1)) + return false; + if (duration > maxTime) return false; + _detected = true; + return true; +} + +bool Gesture::wasDetected() { return _detected; } + +void Gesture::addHandler(void (*fn)(Event&), uint16_t eventMask /* = E_ALL */) { + BUTTONS->addHandler(fn, eventMask, nullptr, this); +} + +void Gesture::delHandlers(void (*fn)(Event&) /* = nullptr */) { + BUTTONS->delHandlers(fn, nullptr, this); +} + +// Event class + +Event::Event() { + finger = type = duration = 0; + from = to = Point(); + button = nullptr; + gesture = nullptr; +} + +Event::operator uint16_t() { return type; } + +const char* Event::typeName() { + const char* unknown = "E_UNKNOWN"; + const char* none = "E_NONE"; + const char* eventNames[NUM_EVENTS] = { + "E_TOUCH", "E_RELEASE", "E_MOVE", "E_GESTURE", + "E_TAP", "E_DBLTAP", "E_DRAGGED", "E_PRESSED", + "E_PRESSING", "E_LONGPRESSED", "E_LONGPRESSING"}; + if (!type) return none; + for (uint8_t i = 0; i < NUM_EVENTS; i++) { + if ((type >> i) & 1) return eventNames[i]; + } + return unknown; +} + +const char* Event::objName() { + const char* empty = ""; + if (gesture) return gesture->getName(); + if (button) return button->getName(); + return empty; +}; + +uint16_t Event::direction(bool rot1 /* = false */) { + return from.directionTo(to, rot1); +} + +bool Event::isDirection(int16_t wanted, uint8_t plusminus /* = PLUSMINUS */, + bool rot1 /* = false */) { + return from.isDirectionTo(to, wanted, plusminus, rot1); +} + +uint16_t Event::distance() { return from.distanceTo(to); } + +// TFT_eSPI_Button compatibility mode + +TFT_eSPI_Button::TFT_eSPI_Button() : Button(0, 0, 0, 0) { _compat = true; } + +void TFT_eSPI_Button::initButton(TFT_eSPI* gfx, int16_t x, int16_t y, + uint16_t w, uint16_t h, uint16_t outline, + uint16_t fill, uint16_t textcolor, + char* label_, uint8_t textsize) { + initButtonUL(gfx, x - (w / 2), y - (h / 2), w, h, outline, fill, textcolor, + label_, textsize); +} + +void TFT_eSPI_Button::initButtonUL(TFT_eSPI* gfx, int16_t x_, int16_t y_, + uint16_t w_, uint16_t h_, uint16_t outline, + uint16_t fill, uint16_t textcolor, + char* label_, uint8_t textsize_) { + x = x_; + y = y_; + w = w_; + h = h_; + off = {fill, textcolor, outline}; + on = {textcolor, fill, outline}; + setTextSize(textsize_); + strncpy(_label, label_, 9); +} + +void TFT_eSPI_Button::setLabelDatum(int16_t dx_, int16_t dy_, + uint8_t datum_ /* = MC_DATUM */) { + dx = dx_; + dy = dy_; + datum = datum_; +} + +void TFT_eSPI_Button::drawButton(bool inverted /* = false */, + String long_name /* = "" */) { + char oldLabel[51]; + if (long_name != "") { + strncpy(oldLabel, _label, 50); + strncpy(_label, long_name.c_str(), 50); + } + draw(inverted ? on : off); + if (long_name != "") strncpy(_label, oldLabel, 50); +} + +bool TFT_eSPI_Button::contains(int16_t x, int16_t y) { + return Button::contains(x, y); +} + +void TFT_eSPI_Button::press(bool p) { + if (p) + fingerDown(); + else + fingerUp(); +} + +bool TFT_eSPI_Button::justPressed() { return wasPressed(); } + +bool TFT_eSPI_Button::justReleased() { return wasReleased(); } diff --git a/src/utility/M5Button.h b/src/utility/M5Button.h new file mode 100644 index 00000000..427bf8ac --- /dev/null +++ b/src/utility/M5Button.h @@ -0,0 +1,982 @@ +/* + +== M5Button: Buttons, Gestures and Events == + + * Hardware button support that is 100% Arduino Button Library compatible. + + * Buttons on the screen, either as labels above the original M5Stack's + hardware buttons or anywhere on the touch screen of the Core2. + + * Zone and Point objects to work with screen locations and areas. Functions + for distance, direction and more. + + * Touch gestures that are processed before the buttons, so you can still + use gestures when the screen is full of buttons. + + * Buttons and gestures send events that you can attach handler routines to, + or poll in a loop. Events include tap, doubletap, pressed, dragged and + more. Support for key repeat. + + * Extensive screen rotation support, including support for buttons and + gestures that stay referenced to the physical screen regardless of rotation. + + * Intuitive, consistent and well-documented API. + + * Emulation of the (much less feature-rich) TFT_eSPI_Button class. This + goes together well with M5Touch's emulation of the TFT_eSPI resistive + touch screen interface to run a lot of existing programs without + modification. + + This library was written for the M5Stack series of devices, but was made to + be general enough to be produce pretty visual buttons with any TFT_eSPI + display. Its more advanced features need the M5Touch interface, although + other input methods could be implemented. + + +== Point and Zone: Describing Points and Areas on the Screen == + + The Point and Zone classes allow you to create variables that hold a point + or an area on the screen. + + Point(x, y) + + Holds a point on the screen. Has members x and y that hold the + coordinates of a touch. Values -1 for x and y indicate an invalid value, + and that's what a point starts out with if you declare it without + parameters. The 'valid()' method tests if a point is valid. If you + explicitly evaluate a Point as a boolean ("if (p) ..."), you also get + whether the point is valid, so this is equivalent to writing "if + (p.valid()) ...". + + Zone(x, y, w, h) + + Holds a rectangular area on the screen. Members x, y, w and h are for the + x and y coordinate of the top-left corner and the width and height of the + rectangle. + + The 'set' method allows you to change the properties of an existing Point + or Zone. Using the 'in' or 'contains' method you can test if a point lies + in a zone. + + The PointAndZone library also provides the low-level support for direction + from one point to another and for screen rotation translations. + + The documentation in src/utility/PointAndZone.h provides more details about + rotation and examples covering most of the above. + + +== Buttons == + + You can create classic Arduino buttons that act on the voltage on a pin of + the controller. On the M5Stack Core2, you can also create buttons that act + on touch within a given rectangle on the screen. If you want, that same + rectangle will also be used for a graphical representation of the button + that will show a text label in a colored background with a colored outline. + The colors of background, text, and outline can be configured, both for the + 'off' and the 'on' state of the button. + + Whether on the M5Stack with hardware buttons or on the Core2 with a touch + screen, buttons are special forms of the 'Zone' object, meaning that all + functions that apply to 'Zone' objects also work on buttons. On the M5Stack + with buttons, while this zone cannot be used for touch input, it can still + be used to display a button that responds to the button state. + + +== Hardware Buttons == + + For hardware buttons, use the classic Arduino way of setting up the button, + by providing the pin, whether the pin is inverted and the debounce time. + + + #include + + Button myButton(39, true, 10); + + void setup() { + M5.begin(); + } + + void loop() { + M5.update(); + if (myButton.wasPressed()) Serial.print("* "); + } + + + This would set up 'myButton' with the inverse state of pin 39, and a + debounce time of 10 milliseconds. Because pin 39 is the left button on an + M5Stack with buttons, this sketch will output a star to the serial port + every time you release the button. (And because pin 39 is the interrupt + wire for the touch screen on the Core2, it also happens to output a star on + that device every time you touch the screen.) + + Note that the sketch uses 'M5.update()' instead of 'myButton.read()'. You + don't need to read() your buttons explicitly anymore. All buttons created + with M5Button are automatically read by 'M5.update()'. (Unless you read + them with 'myButton.read()', in which case 'M5.update()' stops doing that + to avoid you missing things.) + + The next sections will describe buttons and gestures on the touch screen, + but if you have an M5Stack device without a touch screen: keep reading + because many events work on hardware buttons too. Hardware buttons can have + responsive representation on the screen, we'll get to that also. + + +== Buttons Using the Touch Screen == + + Note: It may make sense to also read the documentation in the M5Touch.h + file, as tells you about the touch sensor and the lower-level touch + interface that is underneath the M5Button library. + + To have a button that reacts to the touch sensor, all you need to do is + create a variable for the Button and provide the coordinates (x, y, width + and height). These buttons can be used in two ways. You can either use them + the way you would a normal Arduino button, or you can provide handler + functions to process various events for the button. We'll talk about the + events later, but here's the same simple sketch from above again but now it + defines a 100x50 pixel touch button near the top-right of the screen. Note + that this button does not show anything on the sreen just yet. + + + #include + + Button myButton(10, 10, 200, 100); + + void setup() { + M5.begin(); + } + + void loop() { + M5.update(); + if (myButton.wasPressed()) Serial.print("* "); + } + + + 'wasPressed()' will only be true once when you press the button. You can + also use the other Arduino button functions such as 'isPressed()' that is + true as soon and as long as the button is touched. Note that the buttons + only become pressed if the touch starts within the button, not if you swipe + over it, and that they will stay pressed as long as the finger touches, + even if it leaves the button area. You may want read about the events + further down to distinguish between different kinds of button-presses. + + On the Core2 the three buttons M5.BtnA, M5.BtnB and M5.BtnC from the older + M5Stack units come already implemented as touch buttons that lie just below + the screen where the three circles are. + + +== Buttons with visual appearance == + + If you want you button to show on the screen, all you need to do is provide + a set of three colors for the background of the button, the text printed on + it and the outline of the button. Using yet the same skech again: + + + #include + + Button myButton(10, 10, 200, 100, false, "I'm a button !", + {BLACK, WHITE, WHITE}); + + void setup() { + M5.begin(); + } + + void loop() { + M5.update(); + if (myButton.wasPressed()) Serial.print("* "); + } + + + As you can see the colors are provided in {curly braces}, that's because + they are one variable, of the 'ButtonColors' type. Especialy if you're + going to define a bunch of buttons, you're better off replacing the button + line by: + + + ButtonColors col = {BLACK, WHITE, WHITE}; + Button myButton(10, 10, 200, 100, false, "I'm a button !", col); + + + The order there is background, text, outline. If you do not want any of + these components drawn, simply put NODRAW in that position. The thing we are + defining here is what the button draws in its 'off' state. Since we haven + specified anything to draw in the 'on' state, the button just stays like it + is, regardless of whether it's pressed. Thus, if we say + + + ButtonColors onCol = {BLACK, WHITE, WHITE}; + ButtonColors offCol = {RED, WHITE, WHITE}; + Button myButton(10, 10, 200, 100, false, "I'm a button !", onCol, offCol); + + + the button background would turn red if the button was pressed. The button + colors can also be addressed directly. "myButton.on.bg = BLUE;" will turn + the background blue in the on state. The other two properties of the + ButtonColors variable are predicatably called 'text' and 'outline'. + + If you run the sketches you will see the text is placed in the center of + the button and the buttons have visually please round edges. The corner + radius defaults to a quarter of the shortest side of the button. You can + change all this with the remaining parameters when setting up the button: + + + Button myButton(10, 10, 200, 100, false, "I'm a button !", onCol, offCol, + TL_DATUM, 0, 10, 0); + + + These last parameters indicate where to put the label in the TFT_eSPI + standard datum values, top-left in this case. The values after that are the + dx and dy, meaning the offsets from the default position. In this case + that's no left-right offset and 10 pixels down. Negative values move the + other way. The last value is the corner radius. In this case it would draw + an ugly 1980's rectangular button. + + You can make a button draw its current state with "myButton.draw()", or all + buttons do that with "M5.Buttons.draw()". You can also call draw with a + ButtonColors variable so "myButton.draw({BLUE, WHITE, WHITE})" draws it + with those colors. Until the next state-change comes along that is, if you + have colors for the new state defined. + + Note that the text provided here is the name of the buttton. A button + always keeps the same name, but the label (that which is shown) can change, + but initialises to the name. Use 'myButton.setLabel("New text!")' to change + it. + + With "myButton.hide()" you can make a button temporarily invisible to the + touch sensor. You can specify an optional color value to draw over the + button if you want to make it visually disappear also. myButton.draw() makes + it visible to the touch sensor again, even if you have no colors defined, so + nothing shows on the screen. "MyButton.erase()" only paints over the button, + in a color you can specify (default black). + + +== Visual Buttons (Labels) with Hardware Buttons == + + You can have a visual representation of the state of a hardware button on + the screen, for example right above the hardware buttons of the original + M5Stack. We'll call these buttons "labels", but they're regular buttons + that just respond to a physical button insetad of the touch sensor. If you + want to display a label on the screen that responds to the state of a + hardware button, just set up a hardware button up as usual, but then follow + the parameter list with "hw" (in quotes), followed by the parameters of the + touch button below. + + The hardware buttons in the older M5Stack devices are already set up to + display labels: all you need is supply colors. Their initialization (in + M5Stack.h in this library) looks like this: + + + Button BtnA = Button(BUTTON_A_PIN, true, DEBOUNCE_MS, + "hw", 3, 218, 102, 21, true, "BtnA"); + Button BtnB = Button(BUTTON_B_PIN, true, DEBOUNCE_MS, + "hw", 109, 218, 102, 21, true, "BtnB"); + Button BtnC = Button(BUTTON_C_PIN, true, DEBOUNCE_MS, + "hw", 215, 218, 102, 21, true, "BtnC"); + + + As you can see: its just a hardware button that has a zone to display the + label. So the sketch below is all that is needed to show repsonsive labels + on the M5Stack: + + + #include + + void setup() { + M5.BtnA.off = M5.BtnB.off = M5.BtnC.off = {BLUE, WHITE, NODRAW}; + M5.BtnA.on = M5.BtnB.on = M5.BtnC.on = {RED, WHITE, NODRAW}; + M5.begin(); + M5.Buttons.draw(); + } + + void loop() { + M5.update(); + } + + + If you looked closely you might have noticed that the mysterious fifth + argument has changed from 'false' to 'true'. This argument is called + 'rot1', and it determines that the location of this Zone or Button is + specified in rotation one, i.e. the normal default screen rotation. What + that means is that no matter what rotation you set the display to, these + button will always stay in the same place. The documentation in + src/utility/PointAndZone.h has more details if you want to know more about + this. You will only ever need rot1 if you need multiple screen rotations + AND you want objects to stay in the same physical place regardless. + + +== M5.Buttons == + + Apart from the class "Button" that you use to create buttons of your own, + there is an instance called "M5.Buttons" (plural), that is used to talk to + the M5Button library for things that involve all buttons. For instance: + "M5.Buttons.setFont" sets a font for all buttons, and you can use + "M5.Buttons.addHandler" to add a handler that gets events for all buttons + (and gestures). + + +== Events == + + Buttons (and gestures, but we'll get to those later) have a set of simple + functions to see if they are pressed or not. These Arduino-compatible + functions work fine for that purpose. But if you want to detect whether a + button received a short tap, or even a double-tap on many buttons + simultaneously, you find yourself writing quite a bit of code for that. + + Events are M5Button's way of making it easier to react to events on + hardware buttons or the touch screen. For this you need to define one or + more event handler functions. This is done like this: + + void myHandler(Event& e) { ... } + + It's important to do it exactly this way, only changing the name of the + function. You can then set things up so that this function receives events. + + Here's an events-based sketch for the Core2. We'll base it on the same + buton we've seen before. + + + #include + + ButtonColors onCol = {BLACK, WHITE, WHITE}; + ButtonColors offCol = {RED, WHITE, WHITE}; + Button myButton(10, 10, 200, 100, false, "I'm a button !", onCol, offCol); + + void setup() { + M5.begin(); + myButton.addHandler(touched, E_TOUCH); + myButton.addHandler(released, E_RELEASE); + } + + void loop() { + M5.update(); + } + + void touched(Event& e) { + Serial.println("Touched!"); + } + + void released(Event& e) { + Serial.println("Released!"); + } + + + Note that the function names "touched" and "released" are provided to + addHandler without the parenthesis. Here's two ways you can set up a handler + function to receive events: + + M5.Buttons.addHandler(myHandler); + + - or - + + myButton.addHandler(myHandler); + + The first form receives all the events relating to all buttons and gestures, + the second form only receives the events for that specific button. After the + name of the function, without the brackets, you can specify which events the + function needs to receive. You can add together (or "bitwise or") the names + of the events if you want a handler function to reive multiple events. + + The Event object that is passed to the handler function contains all sorts + of information about the event: where on the screen it started, where it + ended, the duration, etc. etc. + + Let's first look at all the possible events and when they are fired. The + first three events always happen when a finger touches the display. + + E_TOUCH, E_MOVE and E_RELEASE + + The E_TOUCH and E_RELEASE events fire when a button is pressed and + released. On a touch sensor, E_MOVE will fire every time it detects the + finger has moved. These events cannot be prevented from firing, like most + of the other ones. So every time your finger touches the display it will + fire E_TOUCH and then E_MOVEs until finally, when you release your + finger, an E_RELEASE. + + E_PRESSING and E_LONGPRESSING + + There are also events that happen while the button is still pressed. + These are E_PRESSING and E_LONGPRESSING. E_PRESSING happens as soon as + M5Button is sure that's not just a short tap (more later). The maximum + time for a tap is settable, but defaults to 150 ms. So if the button is + still held 150 ms after E_TOUCH, E_PRESSING fires. Just once, unless you + have set up a key repeat, more about that later too. Then at some point + you might get a E_LONGPRESSING, if you have set up a duration for that to + happen. + + E_TAP, E_DBLTAP, E_PRESSED, E_LONGPRESSED and E_DRAGGED + + Unless the keypress is cancelled (more later), exactly one of these events + will fire after the button has been released, after E_RELEASE has fired. + Think of these as final decisions on what kind of keypress this was. + (E_TAP takes a tiny bit longer before it fires because M5Button needs to + make sure it wasn't a doubletap, in that case E_DBLTAP wil fire instead.) + + So tap and doubletap are sort of obvious, E_LONGPRESSED fires if the key + was pressed more that the set time in ms. E_DRAGGED fires if the finger + has moved outside of the button area when it was released. E_PRESSED is + fires in all other cases. + + E_GESTURE + + Doesn't really fit in with the others, but is the event that gets fired + when a gesture is detected. + + + If at any point after the pressing of a button, "myButton.cancel()" is + called, no further high-level events for that button will fire. What that + means is nothing other than possible E_MOVEs and one E_RELEASE event will + fire for that button until it is released and then pressed again. This is + used internally when a gesture is detected, so that when a touch gesture + starts on a button, there won't be an E_PRESSED, or any of the others. + + The second thing to look at more closely is the 'Event' object itself. When + you set up a handler function like this + + void myhandler(Event& e) { + + what that means is you're creating a function that recives a (reference to) + an event. That event has all sorts of properties that we can look at. + + + e.type + + The type of event, such as E_TOUCH or E_TAP from above. The event + itself, when you evaluate it, also returns the type. What that means is + that "if (e.type == E_TAP) .." is equivalent with "if (e == E_TAP) .." + + e.finger + + 0 or 1, whether this is the first or second finger detected on the + touch screen. Left at zero on the M5Stack with buttons. + + e.from and e.to + + Points that say from where to where this event happened. Left at + invalid for the M5Stack with buttons. + + e.duration + + Duration of the event in milliseconds. + + e.button + + Pointer to the button attached to the event. What that means is that you + can use all the methods for button as long as you precede them with + "e.button->". Note the '->' there because this is a pointer to an + object. + + e.gesture + + e.gesture is a pointer to the gesture attached to the event, and may be + null if the event is not a gesture. So unless you know for sure this + event is a gesture (because handler attached to that gesture or because + you asked for E_GESTURE events only), this pointer needs to be tested + using "if (e.gesture)" before using -> methods on it, oterwise your + program will crash. + + other methods + + Additionally, you can ask for the name of the event as text by using + "e.typeName()" and get the name of the gesture or button with + "e.objName()". "e.direction()" gives the direction, for instance of a + gesture or of an E_RELEASE event, where it gives direction between + E_TOUCH and E_RELEASE. "e.isDirectionTo(0,30)" will output true if the + swipe was upwards, plus or minus 30 degrees. + + + When you add a handler function you can also specify what events it should + receive by supplying it as the second argument after the handler function. + If you want to register multiple events for the same function, don't + register the handler twice, but simply add (or bitwise or) the event + values. The default value there is the pseudo-event E_ALL, which is simply + a value with all the event bits turned on. You can also subtract event type + values from E_ALL to exclude them. + + Here are some examples of ways to add a handler function: + + + button1.addHandler(b1Function, E_TOUCH + E_RELEASE); + + b1Function only get these two events for button1. + + + M5.Buttons.addHandler(btnHandle, E_ALL - E_MOVE); + + btnHandle gets all events, except E_MOVE. + + + swipeUp.addHandler(nextPage); + + Handler nextPage is called when swipeUp gesture detected. + + + Note that all handler functions must be of the "void someName(Event& e)" + type, even if they plan to completely ignore the event that is passed to + them. + + + If your event reads data or calls functions in e.button or e.gesture, + remember that these are pointers. Without going into too much detail, it + means it must do so with the -> notation, so to read the button x position, + you would say "e.button->x". + + Please have a look at the example sketch (see below) to understand how this + all works and run the sketch to see all the events printed to the serial + port. + + +== Taps, Doubletaps, Longpresses and Key Repeat == + + Some features are best explained with some examples: + + myButton.tapTime = 0; + + Turns off detection of taps and doubletaps, the button will fire + E_PRESSING immediately when pressed. Any other value makes that the + maximum time a tap can take in milliseconds, and thus the wait tme + before "E_PRESSING" fires. + + mybutton.tapWait = 0; + + Turns off detection of doubletaps only. Any other value makes that the + wait before an E_TAP fires, because M5Button is still waiting to see if + it's maybe a doubletap. + + mybutton.longPressTime = 700; + + Sets up the button to fire an E_LONGPRESSING after 700 ms, and then fire + E_LONGPRESSED instead of E_PRESSED when the button is released. By + default this is set to zero, meaning longpress detection is off. + + myButton.repeatDelay = 500; + myButton.repeatInterval = 250; + + Makes the button repeat the sending of its E_PRESSING event every 250 + milliseconds if key is held for 500 ms. + + +== In Loop vs. Event Handlers == + + Button and Gesture objects have an 'event' method that returns the event + that was detected this time around by 'M5.update()'. Each event comes in + it's own rotation of 'M5.update()', so if you prefer to detect events this + way and not with handler routines that's fine too. + + If nothing was detected, the event type will be set to E_NONE with a value + of 0, so you can do "if (myButton.event) ...". 'M5.Buttons.event' has the + event detected this time around, regardless of what button or gesture it was + attached to. This example prints a star to serial if it is doubletapped. + + #include + + Button myButton(50,70,220, 100, false, "Button", + {YELLOW, BLACK, NODRAW}, + {RED, BLACK, NODRAW} ); + + void setup() { + M5.begin(); + M5.Buttons.setFont(FSS18); + M5.Buttons.draw(); + } + + void loop() { + M5.update(); + if (myButton.event == E_DBLTAP) Serial.print("* "); + } + + +== M5.background == + + Only one button can become pressed for any spot on the touch screen. If you + define overlapping buttons, the first defined button for the overlap become + pressed and gets all subsequent events. + + One special button, "M5.background", was defined before any others, and it + has the size of the entire touch sensor. This means it gets all events + where the first touch was not within any of the defined buttons. + + +== Gestures on the Touch Screen == + + Whenever a finger is released from the touch screen and before any + higher-level button events are fired, the library first checks whether this + was perhaps a gesture. When you define gestures, you can optionally specify + the zone in which the gesture must start, the zone in which it must end, the + minimum distance the finger must have travelled, the direction it has + travelled in and the maximum time the gesture may take. + + Gesture exampleGesture(fromZone, toZone, "exampleName", minimumDistance, + direction, plusminus, ro1, maxTime) + + Where fromZone and toZone can be valid zones or the word "ANYWHERE". If you + want to specify neither fromZone nor toZone, you can also leave them off + completely. The minimum distance defaults to 75 pixels. The direction + (default: don't care) is in compass degrees (so 180 is down), but the + compiler defines DIR_UP, DIR_DOWN, DIR_LEFT and DIR_RIGHT are provided for + convenience. The plusminus deines how many degress off-course the gesture + may be, and the rot1 flag defines whether this direction is relative to the + current rotation, or as seen in rotation 1. maxTime is in milliseconds as + usual and defaults to 500 ms. DIR_ANY can be used for direction if you need + to specify it in order provide a rot1 or maximum time value. + + here are a few examples of valid gesture definitions: + + + Gesture swipeDown("swipe down", 100, DIR_DOWN, 30); + + Down (plus or minus 30 degrees) for at least 100 pixels within 500 ms + + + Gesture fromTop(Zone(0, 0, 360, 30), ANYWHERE, "from top", 100, DIR_DOWN, 30); + + The same but starting from within the top 30 pixels. (If you make that + too narrow you may miss the swipe because the sensor 'sees' only once + every 13 ms or so. + + + (Note that if you defined both these gestures in this order the second one + would never fire because any swipe that matched number two would first match + number one and fire that one instead.) + + Gestures have a 'wasDetected()' method if you want to detect them in the + main loop, or you attach a handler the same way you would for a button, + with "myGesture.addhandler(myHandler)" + + + #include + + Gesture swipeDown("swipe down", DIR_DOWN, 30); + + void setup() { + M5.begin(); + } + + void loop() { + M5.update(); + if (swipeDown.wasDetected()) Serial.println("Swiped down!"); + } + + +== Advanced Hints and Tricks + + ## drawFn + + If you look at the source code for the library you will see that the + drawing of the button is done by a static function in the M5Buttons + object. It's defined as + + void M5Buttons::drawFunction(Button& b, ButtonColors bc) + + If you make your own function that takes the same arguments but that does + something different, you can make the library use it by saying + "M5.Buttons.drawFn = myFunction". You can even do that on a per-button + basis with "myButton.drawFn = myFunction". + + + ## drawZone + + A Button instance _is_ also a Zone object, in that it descends from it. + Which means a Button has all the methods of a Zone object, as well as its + own. But it contains another zone, called drawZone. This allows you to + have the visual representation happen somewhere else than where the + button is on the touch sensor. Normally this is set to "invalid zone", + but if you set it to a valid screen area, the button will be drawn there. + This is used internally to put the optional labels for the off-screen + buttons on the Core2 on the screen just above their touch areas. + + + ## Drawing is Non-Invasive + + This library uses a brand-new feature of the M5Display object -- + M5.Lcd.popState() and M5.lcd.pushState() -- that allows it to save and + reload the complete display driver state before and after drawing a + button. What that means is that you can draw to the display without + worrying that the button drawing will mess with your font setting, cursor + position or anything else that is display-related. + + + ## TFT_ePI_Button Emulation + + This libary also defines an object called TFT_eSPI_Button, which is the + old way of doing buttons that comes as an optional extra with the display + library. Together with M5Touch's emulation of the TFT_eSPI touch + interface (written for the older resistive touch-screens), you can use it + to run software made for those APIs. Do not use either for new code: the + native interfaces are much more powerful. + + + ## Buttons and Variable Scope + + Buttons come into existence and are drawn in their initial state when + their variables are defined and are not detected anymore when their + variables are removed from memory when the function they were defined in + returns. Except for global buttons - defined outside any functions: their + variables always exist. The programmer has to take responsability for + erasing expired buttons off the screen because Button doesnt know what is + supposed to be in the background. If you're not clearing the entire + screen anyway, this can be done with "myButton.erase(BLACK)" if the + background is to be black. + +*/ + +#ifndef _M5BUTTON_H_ +#define _M5BUTTON_H_ + +class Gesture; + +#include +#include +#include + +#include "PointAndZone.h" +#include "utility/Config.h" + +#ifdef M5Stack_M5Core2 +#include +#endif /* M5Stack_M5Core2 */ + +#define BUTTON_FREEFONT FSS9 +#define BUTTON_TEXTFONT 1 +#define BUTTON_TEXTSIZE 1 +#define BUTTON_DATUM MC_DATUM + +#define TAP_TIME 150 +#define DBLTAP_TIME 300 +#define LONGPRESS_TIME 0 +#define REPEAT_DELAY 0 +#define REPEAT_INTERVAL 200 + +#define GESTURE_MAXTIME 500 +#define GESTURE_MINDIST 75 +#define ANYWHERE Zone() + +#define NUM_EVENTS 11 +#define E_TOUCH 0x0001 +#define E_RELEASE 0x0002 +#define E_MOVE 0x0004 +#define E_GESTURE 0x0008 +#define E_TAP 0x0010 +#define E_DBLTAP 0x0020 +#define E_DRAGGED 0x0040 +#define E_PRESSED 0x0080 +#define E_PRESSING 0x0100 +#define E_LONGPRESSED 0x0200 +#define E_LONGPRESSING 0x0400 + +#define E_ALL 0x0FFF + +#define NODRAW 0x0120 // Special color value: transparent + +struct ButtonColors { + uint16_t bg; + uint16_t text; + uint16_t outline; +}; + +class Button; +class Event; + +#ifdef _M5TOUCH_H_ +struct Finger { + Point current, previous, startPoint, tapPoint; + uint32_t startTime, tapTime; + Button* button; +}; +#endif + +class Event { + public: + Event(); + operator uint16_t(); + const char* typeName(); + const char* objName(); + uint16_t direction(bool rot1 = false); + bool isDirection(int16_t wanted, uint8_t plusminus = PLUSMINUS, + bool rot1 = false); + uint16_t distance(); + uint8_t finger; + uint16_t type; + Point from, to; + uint16_t duration; + Button* button; + Gesture* gesture; +}; + +class Button : public Zone { + public: + static std::vector instances; + Button(int16_t x_, int16_t y_, int16_t w_, int16_t h_, bool rot1_ = false, + const char* name_ = "", ButtonColors off_ = {NODRAW, NODRAW, NODRAW}, + ButtonColors on_ = {NODRAW, NODRAW, NODRAW}, + uint8_t datum_ = BUTTON_DATUM, int16_t dx_ = 0, int16_t dy_ = 0, + uint8_t r_ = 0xFF); + Button(uint8_t pin_, uint8_t invert_, uint32_t dbTime_, String hw = "hw", + int16_t x_ = 0, int16_t y_ = 0, int16_t w_ = 0, int16_t h_ = 0, + bool rot1_ = false, const char* name_ = "", + ButtonColors off_ = {NODRAW, NODRAW, NODRAW}, + ButtonColors on_ = {NODRAW, NODRAW, NODRAW}, + uint8_t datum_ = BUTTON_DATUM, int16_t dx_ = 0, int16_t dy_ = 0, + uint8_t r_ = 0xFF); + ~Button(); + operator bool(); + bool operator==(const Button& b); + bool operator!=(const Button& b); + bool operator==(Button* b); + bool operator!=(Button* b); + int16_t instanceIndex(); + bool read(bool manualRead = true); + void fingerDown(Point pos = Point(), uint8_t finger = 0); + void fingerUp(uint8_t finger = 0); + void fingerMove(Point pos, uint8_t finger); + void cancel(); + bool isPressed(); + bool isReleased(); + bool wasPressed(); + bool wasReleased(); + bool pressedFor(uint32_t ms); + bool pressedFor(uint32_t ms, uint32_t continuous_time); + bool releasedFor(uint32_t ms); + bool wasReleasefor(uint32_t ms); + void addHandler(void (*fn)(Event&), uint16_t eventMask = E_ALL); + void delHandlers(void (*fn)(Event&) = nullptr); + char* getName(); + uint32_t lastChange(); + Event event; + uint16_t userData; + uint16_t tapTime, dbltapTime, longPressTime; + uint16_t repeatDelay, repeatInterval; + + protected: + void init(); + bool postReleaseEvents(); + bool timeoutEvents(); + friend class M5Buttons; + char _name[16]; + uint8_t _pin; + uint16_t _dbTime; + bool _invert; + bool _changed, _state, _tapWait, _pressing; + bool _longPressing, _cancelled, _manuallyRead; + uint8_t _setState; + uint32_t _time, _lastRepeat; + uint32_t _lastChange, _lastLongPress, _pressTime, _hold_time; + uint8_t _finger; + Point _fromPt[2], _toPt[2], _currentPt[2]; + + // visual stuff + public: + void draw(ButtonColors bc); + void draw(); + void hide(uint16_t color = NODRAW); + void erase(uint16_t color = BLACK); + void setLabel(const char* label_); + void setFont(const GFXfont* freeFont_); + void setFont(uint8_t textFont_ = 0); + void setTextSize(uint8_t textSize_ = 0); + char* label(); + ButtonColors off, on; + Zone drawZone; + uint8_t datum, r; + int16_t dx, dy; + void (*drawFn)(Button& b, ButtonColors bc); + + protected: + bool _hidden; + bool _compat; // For TFT_eSPI_Button emulation + char _label[51]; + uint8_t _textFont; + const GFXfont* _freeFont; + uint8_t _textSize; +}; + +class Gesture { + public: + static std::vector instances; + Gesture(Zone fromZone_, Zone toZone_, const char* name_ = "", + uint16_t minDistance_ = GESTURE_MINDIST, + int16_t direction_ = INVALID_VALUE, uint8_t plusminus_ = PLUSMINUS, + bool rot1_ = false, uint16_t maxTime_ = GESTURE_MAXTIME); + Gesture(const char* name_ = "", uint16_t minDistance_ = GESTURE_MINDIST, + int16_t direction_ = INVALID_VALUE, uint8_t plusminus_ = PLUSMINUS, + bool rot1_ = false, uint16_t maxTime_ = GESTURE_MAXTIME); + ~Gesture(); + operator bool(); + int16_t instanceIndex(); + bool test(Point& from, Point& to, uint16_t duration); + bool wasDetected(); + void addHandler(void (*fn)(Event&), uint16_t eventMask = E_ALL); + void delHandlers(void (*fn)(Event&) = nullptr); + char* getName(); + Zone fromZone; + Zone toZone; + Event event; + int16_t direction; + uint8_t plusminus; + bool rot1; + uint16_t maxTime, minDistance; + + protected: + friend class M5Buttons; + bool _detected; + char _name[16]; +}; + +struct EventHandler { + uint16_t eventMask; + Button* button; + Gesture* gesture; + void (*fn)(Event&); +}; + +class M5Buttons { + public: + static M5Buttons* instance; + static void drawFunction(Button& b, ButtonColors bc); + M5Buttons(); + Button* which(Point& p); + void draw(); + void update(); + void setFont(const GFXfont* freeFont_); + void setFont(uint8_t textFont_); + void setTextSize(uint8_t textSize_); + void (*drawFn)(Button& b, ButtonColors bc); + void fireEvent(uint8_t finger, uint16_t type, Point& from, Point& to, + uint16_t duration, Button* button, Gesture* gesture); + void addHandler(void (*fn)(Event&), uint16_t eventMask = E_ALL, + Button* button = nullptr, Gesture* gesture = nullptr); + void delHandlers(void (*fn)(Event&), Button* button, Gesture* gesture); + Event event; + + protected: + std::vector _eventHandlers; + uint8_t _textFont; + const GFXfont* _freeFont; + uint8_t _textSize; + bool _leftovers; + +#ifdef _M5TOUCH_H_ + protected: + Finger _finger[2]; +#endif +}; + +// TFT_eSPI_Button compatibility emulation +class TFT_eSPI_Button : public Button { + public: + TFT_eSPI_Button(); + void initButton(TFT_eSPI* gfx, int16_t x, int16_t y, uint16_t w, uint16_t h, + uint16_t outline, uint16_t fill, uint16_t textcolor, + char* label_, uint8_t textsize_); + void initButtonUL(TFT_eSPI* gfx, int16_t x_, int16_t y_, uint16_t w_, + uint16_t h_, uint16_t outline, uint16_t fill, + uint16_t textcolor, char* label_, uint8_t textsize_); + void setLabelDatum(int16_t x_delta, int16_t y_delta, + uint8_t datum = MC_DATUM); + void drawButton(bool inverted = false, String long_name = ""); + bool contains(int16_t x, int16_t y); + void press(bool p); + bool isPressed(); + bool justPressed(); + bool justReleased(); +}; + +#endif /* _M5BUTTON_H_ */ diff --git a/src/utility/PointAndZone.cpp b/src/utility/PointAndZone.cpp new file mode 100644 index 00000000..09a84baa --- /dev/null +++ b/src/utility/PointAndZone.cpp @@ -0,0 +1,183 @@ +#include "PointAndZone.h" + +// Point class + +Point::Point(int16_t x_ /* = INVALID_VALUE */, + int16_t y_ /* = INVALID_VALUE */) { + x = x_; + y = y_; +} + +bool Point::operator==(const Point& p) { return (Equals(p)); } + +bool Point::operator!=(const Point& p) { return (!Equals(p)); } + +Point::operator char*() { + if (valid()) { + sprintf(_text, "(%d, %d)", x, y); + } else { + strncpy(_text, "(invalid)", 12); + } + return _text; +} + +Point::operator bool() { return !(x == INVALID_VALUE && y == INVALID_VALUE); } + +void Point::set(int16_t x_ /* = INVALID_VALUE */, + int16_t y_ /* = INVALID_VALUE */) { + x = x_; + y = y_; +} + +bool Point::Equals(const Point& p) { return (x == p.x && y == p.y); } + +bool Point::in(Zone& z) { return (z.contains(x, y)); } + +bool Point::valid() { return !(x == INVALID_VALUE && y == INVALID_VALUE); } + +uint16_t Point::distanceTo(const Point& p) { + int16_t dx = x - p.x; + int16_t dy = y - p.y; + return sqrt(dx * dx + dy * dy) + 0.5; +} + +uint16_t Point::directionTo(const Point& p, bool rot1 /* = false */) { + // 57.29578 =~ 180/pi, see https://stackoverflow.com/a/62486304 + uint16_t a = (uint16_t)(450.5 + (atan2(p.y - y, p.x - x) * 57.29578)); +#ifdef TFT + if (rot1) { + if (TFT->rotation < 4) { + a = (((TFT->rotation + 3) % 4) * 90) + a; + } else { + a = (((TFT->rotation + 1) % 4) * 90) - a; + } + } +#endif /* TFT */ + a = (360 + a) % 360; + return a; +} + +bool Point::isDirectionTo(const Point& p, int16_t wanted, + uint8_t plusminus /* = PLUSMINUS */, + bool rot1 /* = false */) { + uint16_t a = directionTo(p, rot1); + return (min(abs(wanted - a), 360 - abs(wanted - a)) <= plusminus); +} + +void Point::rotate(uint8_t m) { + if (m == 1 || !valid()) return; + int16_t normal_x = x; + int16_t normal_y = y; + int16_t inv_x = HIGHEST_X - x; + int16_t inv_y = HIGHEST_Y - y; + // inv_y can be negative for area below screen of M5Core2 + switch (m) { + case 0: + x = inv_y; + y = normal_x; + break; + case 2: + x = normal_y; + y = inv_x; + break; + case 3: + x = inv_x; + y = inv_y; + break; + // rotations 4-7 are mirrored + case 4: + x = inv_y; + y = inv_x; + break; + case 5: + x = normal_x; + y = inv_y; + break; + case 6: + x = normal_y; + y = normal_x; + break; + case 7: + x = inv_x; + y = normal_y; + break; + } +} + +// Zone class + +Zone::Zone(int16_t x_ /* = INVALID_VALUE */, int16_t y_ /* = INVALID_VALUE */, + int16_t w_ /* = 0 */, int16_t h_ /* = 0 */, bool rot1_ /* = false */ +) { + set(x_, y_, w_, h_, rot1_); +} + +Zone::operator bool() { return !(x == INVALID_VALUE && y == INVALID_VALUE); } + +void Zone::set(int16_t x_ /* = INVALID_VALUE */, + int16_t y_ /* = INVALID_VALUE */, + int16_t w_ /* = 0 */, int16_t h_ /* = 0 */, + bool rot1_ /* = false */) { + x = x_; + y = y_; + w = w_; + h = h_; + rot1 = rot1_; +} + +bool Zone::valid() { return !(x == INVALID_VALUE && y == INVALID_VALUE); } + +bool Zone::contains(const Point& p) { return contains(p.x, p.y); } + +bool Zone::contains(int16_t x_, int16_t y_) { + +#ifdef TFT + if (rot1 && TFT->rotation != 1) { + Zone t = *this; + t.rotate(TFT->rotation); + return (y_ >= t.y && y_ <= t.y + t.h && x_ >= t.x && x_ <= t.x + t.w); + } +#endif /* TFT */ + + return (y_ >= y && y_ <= y + h && x_ >= x && x_ <= x + w); +} + +void Zone::rotate(uint8_t m) { + if (m == 1) return; + int16_t normal_x = x; + int16_t normal_y = y; + int16_t inv_x = TFT_WIDTH - 1 - x - w; + int16_t inv_y = TFT_HEIGHT - 1 - y - h; // negative for area below screen + switch (m) { + case 0: + x = inv_y; + y = normal_x; + break; + case 2: + x = normal_y; + y = inv_x; + break; + case 3: + x = inv_x; + y = inv_y; + break; + // rotations 4-7 are mirrored + case 4: + x = inv_y; + y = inv_x; + break; + case 5: + x = normal_x; + y = inv_y; + break; + case 6: + x = normal_y; + y = normal_x; + break; + case 7: + x = inv_x; + y = normal_y; + break; + } + if (!(m % 2)) std::swap(w, h); +} diff --git a/src/utility/PointAndZone.h b/src/utility/PointAndZone.h new file mode 100644 index 00000000..bed059b3 --- /dev/null +++ b/src/utility/PointAndZone.h @@ -0,0 +1,191 @@ +/* + +== The PointAndZone Library == + + This library was written to supply Point and Zone, common primitives for + M5Display and M5Button, libraries originally written for the M5Stack series + of devices. They could be useful for many other applications, especially + anything based on a TFT_eSPI display driver. + + +== Point and Zone: Describing Points and Areas on the Screen == + + The Point and Zone classes allow you to create variables that hold a point + or an area on the screen. + + Point(x, y) + + Holds a point on the screen. Has members x and y that hold the coordinates + of a touch. Values INVALID_VALUE for x and y indicate an invalid value, + and that's what a point starts out with if you declare it without + parameters. The 'valid()' method tests if a point is valid. If you + explicitly evaluate a Point as a boolean ("if (p) ..."), you also get + whether the point is valid, so this is equivalent to writing "if + (p.valid()) ...". + + Zone(x, y, w, h) + + Holds a rectangular area on the screen. Members x, y, w and h are for the + x and y coordinate of the top-left corner and the width and height of the + rectangle. + + The 'set' method allows you to change the properties of an existing Point + or Zone. Using the 'in' or 'contains' method you can test if a point lies + in a zone. + + The PointAndZone library also provides the low-level support for direction + from one point to another and for screen rotation translations. + + +== Looking for Directions? == + + This library allows you to find the distance in pixels between two Point + objects with "A.distanceTo(B)". Using "A.directionTo(B)" will output a + compass course in degrees from A to B. That is, if A lies directly above A + on the screen the output will be 0, if B lies to the left of A, the output + will be 270. You can also test whether a direction matches some other + direction by using "A.isDirectionTo(B, 0, 30)". What this does is take the + direction from A to B and output 'true' if it is 0, plus or minus 30 + degrees. (So either between 330 and 359 or between 0 and 30). + + Do note that unlike in math, on a display the Y-axis points downwards. So + pixel coordinates (10, 10) are at direction 135 deg from (0, 0). + + As a last argument to the direction functions, you can supply 'rot1' again + (default is 'false'). Just like in zones and buttons, what that means is + that the direction is output as if the rotation 1 coordinate system was + used. + + Distance and direction functionality is used in Gesture recognition in the + M5Button highler level library. Its 'Event' objects have methods that look + very much like these, except the 'To' in the name is missing because Events + have a starting and ending point so you can just print + "myEvent.direction()" or state "if (myEvent.isDirection(0,30) ..." + + +== Some Examples == + + Point a; + Point b(50, 120); + Serial.println(a.valid()); // 0 + Serial.println(a); // (invalid) + a.set(10, 30); + Serial.println(a); // (10,30) + Serial.println(a.valid()); // 1 + Serial.println(b.y); // 120 + Serial.println(a.distanceTo(b)); // 98 + Serial.println(a.directionTo(b)); // 156 + Serial.println(a.isDirectionTo(b, 0, 30)); // 0 + Zone z(0,0,100, 100); + Serial.println(z.w); // 100 + Serial.println(z.contains(a)); // 1 + Serial.println(b.in(z)); // 0 + + +== Screen Rotation and the 'rot1' Property == + + TL;DR: just set it to 'false' if you don't need anything special, + otherwise read the rest of this section. + + The TFT_eSPI library allows you to rotate the screen. On M5Stack devices, + you do this with "M5.Lcd.SetRotation", supplying a number between 0 and 7. + Numbers 0-3 are the obvious rotations, with 1 being the default. 4-7 are + the other 4 mirrored, so you would not be using these unless you want to + look at the display through a mirror for a homebrew heads-up display or + mini-teleprompter. + + The M5Touch library for the Core2 device rotates the data it reads from the + sensor to match the display using the 'rotate' function on the Point + objects it reads. So if the display is upside down (rotation 3), the (0, + 0)-point is diagonally accross from where it is in rotation 1. (And the + area that was below the screen at y 240-279 now has negative y-values.) + + Zones (and buttons, because they are extensions of zones) have a 'rot1' + property. This would normally set to false, meaning the coordinates are + treated as specified in the current rotation. If it is set to 'true' + however, the library treats the cordinates as if they are specified in + rotation 1 and internally rotates them before evaluating whether a Point is + in a Zone. The Button object also rotates the coordinates before drawing + the button. + + So by setting 'rot1' to true, you can create zones and buttons that stay in + the same place even if the screen is rotated. On the Core2, this is used to + define the BtnA through BtnC virtual below-screen buttons, which should + always be in the area below the screen where the circles are printed, + regardless of rotation. + + +== Porting == + + To use this library on other devices, simply replace these two lines + + #include // so that we can get the rotation + #include "utility/Config.h" // Defines 'TFT', a pointer to the display + + by whatever you need to do to make the symbol 'TFT' be a pointer to a + TFT_eSPI-derived display device that has a 'rotation' variable. If you + don't need rotation just delete the lines: the direction functions and the + 'contains' function will now simply ignore their 'rot1' parameters. + +*/ + +#ifndef _POINTANDZONE_H_ +#define _POINTANDZONE_H_ + +#include +#include // so that we can get the rotation +#include "utility/Config.h" // Defines 'TFT', a pointer to the display + +#define INVALID_VALUE -32768 +#define PLUSMINUS 45 // default value for isDirectionTo + +#define DIR_UP 0 +#define DIR_RIGHT 90 +#define DIR_DOWN 180 +#define DIR_LEFT 270 +#define DIR_ANY INVALID_VALUE + +#define HIGHEST_X 319 // Can't trust TFT_WIDTH, driver is portrait +#define HIGHEST_Y 239 + + +class Zone; + +class Point { + public: + Point(int16_t x_ = INVALID_VALUE, int16_t y_ = INVALID_VALUE); + bool operator==(const Point& p); + bool operator!=(const Point& p); + explicit operator bool(); + operator char*(); + void set(int16_t x_ = INVALID_VALUE, int16_t y_ = INVALID_VALUE); + bool valid(); + bool in(Zone& z); + bool Equals(const Point& p); + uint16_t distanceTo(const Point& p); + uint16_t directionTo(const Point& p, bool rot1 = false); + bool isDirectionTo(const Point& p, int16_t wanted, + uint8_t plusminus = PLUSMINUS, bool rot1 = false); + void rotate(uint8_t m); + int16_t x, y; + + private: + char _text[12]; +}; + +class Zone { + public: + Zone(int16_t x_ = INVALID_VALUE, int16_t y_ = INVALID_VALUE, int16_t w_ = 0, + int16_t h_ = 0, bool rot1_ = false); + explicit operator bool(); + bool valid(); + void set(int16_t x_ = INVALID_VALUE, int16_t y_ = INVALID_VALUE, + int16_t w_ = 0 , int16_t h_ = 0, bool rot1_ = false); + bool contains(const Point& p); + bool contains(int16_t x, int16_t y); + void rotate(uint8_t m); + int16_t x, y, w, h; + bool rot1; +}; + +#endif /* _POINTANDZONE_H_ */