/***********************************************************************************************************************
PicoMite MMBasic

GUI.c

<COPYRIGHT HOLDERS>  Geoff Graham, Peter Mather
Copyright (c) 2021, <COPYRIGHT HOLDERS> All rights reserved. 
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 
1.	Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2.	Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
    in the documentation and/or other materials provided with the distribution.
3.	The name MMBasic be used when referring to the interpreter in any documentation and promotional material and the original copyright message be displayed 
    on the console at startup (additional copyright messages may be added).
4.	All advertising materials mentioning features or use of this software must display the following acknowledgement: This product includes software developed 
    by the <copyright holder>.
5.	Neither the name of the <copyright holder> nor the names of its contributors may be used to endorse or promote products derived from this software 
    without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY <COPYRIGHT HOLDERS> AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDERS> BE LIABLE FOR ANY DIRECT, 
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

************************************************************************************************************************/
/**
* @file GUI.c
* @author Geoff Graham, Peter Mather
* @brief Source for GUI MMBasic commands and functions
*/
/**
 * @cond
 * The following section will be excluded from the documentation.
 */


#include "MMBasic_Includes.h"
#include "Hardware_Includes.h"
#include "float.h"
#define BTN_SIDE_BRIGHT     -25
#define BTN_SIDE_DULL       -50
#define BTN_SIDE_WIDTH      4
#define BTN_CAPTION_SHIFT   2

#define CLICK_DURATION      3           // the duration of a "click" in mSec

#define CTRL_NORMAL         0b0000000   // the control should be displayed as normal
#define CTRL_DISABLED       0b0000001   // the control is disabled and displayed in dull colours
#define CTRL_DISABLED2      0b0000010   // as above but only used when a keyboard is active
#define CTRL_HIDDEN         0b0000100   // the control is hidden
#define CTRL_HIDDEN2        0b0001000   // only used when setting a control to hidden
#define CTRL_SPINUP         0b0010000   // the spinbox up arrow is touched
#define CTRL_SPINDOWN       0b0100000   // ditto down
#define CTRL_SELECTED       0b1000000   // for a textbox or numberbox indicated that the box is selected

#define BTN_DISABLED        -55

// define what the function DrawKeyboard() will do
#define KEY_OPEN            1
#define KEY_DRAW_ALL        2
#define KEY_KEY_DWN         3
#define KEY_KEY_UP          4
#define KEY_KEY_CANCEL      5

#define CTRL_BUTTON         1
#define CTRL_SWITCH         2
#define CTRL_RADIOBTN       3
#define CTRL_CHECKBOX       4
#define CTRL_LED            5
#define CTRL_SPINNER        6
#define CTRL_FRAME          7
#define CTRL_NBRBOX         8
#define CTRL_TEXTBOX        9
#define CTRL_FMTBOX         10
#define CTRL_DISPLAYBOX     11
#define CTRL_CAPTION        12
#define CTRL_AREA           13
#define CTRL_GAUGE          14
#define CTRL_BARGAUGE       15

#define MAX_PAGES           32          // the number of pages that can be specifies (this must not exceed 32)
extern void cmd_guiMX170(void);


// used by the gauge control to store extra data in the allocated string space
#define GAUGE_UNITS_SIZE 32
struct s_GaugeS {
    char units[GAUGE_UNITS_SIZE];
    MMFLOAT ta, tb, tc;
    int c1, c2, c3, c4;
    int laststrlen, cval, csaved, lastfc, lastbc;
};


int SetupPage;
unsigned int CurrentPages;

short gui_font_width, gui_font_height;

int display_backlight;                  // the brightness of the backlight (1 to 100)

int gui_click_pin = 0;                  // the sound pin

volatile int CursorTimer;               // used to time the flashing cursor
volatile int ClickTimer = 0;            // used to time the click when touch occurs
volatile int TouchTimer;                // used to time the response to touch
int CheckGuiFlag = 0;                   // used to tell the mSec timer to call CheckGui()

short CurrentRef;                         // if the pen is down this is the control (or zero if not on a control)
short LastRef;                            // this is the last control touched
short LastX;                              // this is the x coord when the pen was lifted
short LastY;                              // ditto for y

MMFLOAT CtrlSavedVal;                   // a temporary place to save a control's value

int TouchX, TouchY;
volatile bool TouchDown = false;
volatile bool TouchUp = false;
volatile bool TouchState = false;


int last_x2, last_y2;                   // defaults used when creating controls
MMFLOAT last_inc, last_min, last_max;
int last_fcolour, last_bcolour;
MMFLOAT last_ta, last_tb, last_tc;
int last_c1 = -1, last_c2, last_c3, last_c4;
char last_units[32];

// used for keypads
int GUIKeyDown = 0;                        // true if a key is down
bool KeyAltShift = false;                    // true if in alt keypad layout
int InvokingCtrl = 0;                   // the number of the control that invoked the keypad
//int InCallback = 0;                     // true if we are running MM.KEYPRESS
//int InPause = 0;                        // true if we are inside a PAUSE command (used to suppress calling MM.KEYPRESS)
char CancelValue[256];                  // save the value of the control in case the user cancels

bool gui_int_down=false;                       // true if the touch has triggered an interrupt
char *GuiIntDownVector = NULL;          // address of the interrupt routine or NULL if no interrupt
bool gui_int_up=false;                         // true if the release of the touch has triggered an interrupt
char *GuiIntUpVector = NULL;            // address of the interrupt routine or NULL if no interrupt
volatile bool DelayedDrawKeyboard = false;        // a flag to indicate that the pop-up keyboard should be drawn AFTER the pen down interrupt
volatile bool DelayedDrawFmtBox = false;          // a flag to indicate that the pop-up formatted keyboard should be drawn AFTER the pen down interrupt

//struct s_ctrl *Ctrl;                    // list of the controls


int ChangeBright(int c, int pct);
void SpecialWritePixel(int x, int y, unsigned int tc, int status);
void SpecialDrawLine(int x1, int y1, int x2, int y2, int w, int tc, int status);
void SpecialDrawBox(int x1, int y1, int x2, int y2, int w, int c, int fill, int status);
void SpecialDrawRBox(int x1, int y1, int x2, int y2, int radius, int c, int fill, int status);
void SpecialPrintString(int x, int y, int jh, int jv, int jo, int fc, int bc, char *str, int status);
void SpecialDrawCircle(int x, int y, int radius, int w, int c, int fill, MMFLOAT aspect, int status);
void SpecialDrawTriangle(int x0, int y0, int x1, int y1, int x2, int y2, int c, int fill, int status);
void DrawBorder(int x1, int y1, int x2, int y2, int w, int tc, int bc, int status);
void DrawBasicButton(int x1, int y1, int x2, int y2, int w, int up, int c, int status);
void DrawButton(int r);
void DrawSwitch(int r);
void DrawCheckBox(int r);
void DrawRadioBtn(int r);
void DrawLED(int r);
void DrawSpinner(int r);
void DrawFrame(int r);
void DrawCaption(int r);
void DrawGauge(int r);
void DrawBarGauge(int r);
char *GetCaption(int k, int is_alpha, int alt);
void GetSingleKeyCoord(int k, int is_alpha, int *x1, int *y1, int *x2, int *y2);
void DrawDisplayBox(int r);
void PopUpRedrawAll(int r, int disabled);
void KeyPadErase(int is_alpha);
void DrawSingleKey(int is_alpha, int x1, int y1, int x2, int y2, char *s, int fc, int bc);
void DrawKeyboard(int mode);
void DrawControl(int r);
void UpdateControl(int r);
void SetCtrlState(int r, int state, int err);
void DoCallback(int InvokingCtrl, char *key);
void DrawFmtBox(int mode);


////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// The GUI command is the base of all the sophisticated GUI drawing features in the Micromite Plus
//
////////////////////////////////////////////////////////////////////////////////////////////////////////



/*********************************************************************************************
 Most controls have similar argument lists, so this function is a "universal" argument
 collector for all controls.
 It returns with the control's reference number in case the caller wants to make some
 more adjustments.
 ********************************************************************************************/
int GetCtrlParams(int type, unsigned char *p) {
    int r, a;
    struct s_GaugeS *GaugeS;                                        // in case we are dealing with a GAUGE control
    if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
    getargs(&p, 40, (unsigned char *)",");
    if((argc & 1) != 1) error("Argument count");
    if(*argv[0] == '#') argv[0]++;
    r = getint(argv[0], 1, Option.MaxCtrls - 1);
    if(Ctrl[r].type) error("GUI reference number #% is in use", r);
    if(argc < 5) error("Argument count");
    a = 0;

    // setup the defaults
    Ctrl[r].x2 = last_x2; Ctrl[r].y2 = last_y2;
    if(type == CTRL_CAPTION) {
        Ctrl[r].fc = gui_fcolour;                                   // a caption defaults to the system colours set by the COLOUR command
        Ctrl[r].bc = gui_bcolour;
    } else {
        Ctrl[r].fc = last_fcolour;                                  // the others take the last colours used in the last command
        Ctrl[r].bc = last_bcolour;
    }
    Ctrl[r].inc = last_inc; Ctrl[r].min = last_min; Ctrl[r].max = last_max;

    // save the current font, needed if we redraw the control
    Ctrl[r].font = gui_font;

    // get string space
    Ctrl[r].s = GetMemory(MAXSTRLEN);

    // and the caption if the control needs it
    switch(type) {                  // these need a caption
        case CTRL_CAPTION:
        case CTRL_BUTTON:
        case CTRL_LED:
        case CTRL_SWITCH:
        case CTRL_FRAME:
        case CTRL_RADIOBTN:
        case CTRL_CHECKBOX:         strcpy((char *)Ctrl[r].s, (char *)getCstring(argv[a += 2]));
    }

    if(type == CTRL_FMTBOX) {
        // get string space
        a += 2;
        if(*argv[a] == '"') {
            Ctrl[r].fmt = GetMemory(MAXSTRLEN);
            strcpy((char *)Ctrl[r].fmt, (char *)getCstring(argv[a]));
        } else {
            // Format String:
            // xc where x is the maximum value digit and c is the ghost char
            // these are concatenated to make a string of digits (eg, seconds = 5s9s)
            // Special constructs: 1 to 32 = 3dDd       1 to 12 = 1mMm
            //                     1 to 24 = 2hHh       0 to 179 = 1dLd9d
            // A separator is in brackets.  Eg (:)   or   (' )
            // Special chars: A = AM/PM    N = N/S    E = E/W
            if(checkstring(argv[a], (unsigned char *)"DATE1")) Ctrl[r].fmt = (unsigned char *)"3dDd(/)1mMm(/)9y9y";
            if(checkstring(argv[a], (unsigned char *)"DATE2")) Ctrl[r].fmt = (unsigned char *)"1mMm(/)3dDd(/)9y9y";
            if(checkstring(argv[a], (unsigned char *)"DATE3")) Ctrl[r].fmt = (unsigned char *)"9y9y9y9y(/)1mMm(/)3dDd";
            if(checkstring(argv[a], (unsigned char *)"TIME1")) Ctrl[r].fmt = (unsigned char *)"2hHh(:)5m9m";
            if(checkstring(argv[a], (unsigned char *)"TIME2")) Ctrl[r].fmt = (unsigned char *)"2hHh(:)5m9m(:)5s9s";
            if(checkstring(argv[a], (unsigned char *)"TIME3")) Ctrl[r].fmt = (unsigned char *)"1hMh(:)5m9m( )A";
            if(checkstring(argv[a], (unsigned char *)"TIME4")) Ctrl[r].fmt = (unsigned char *)"1hMh(:)5m9m(:)5s9s( )A";
            if(checkstring(argv[a], (unsigned char *)"DATETIME1")) Ctrl[r].fmt = (unsigned char *)"3dDd(/)1mMm(/)9y9y( )1hMh(:)5m9m( )A";
            if(checkstring(argv[a], (unsigned char *)"DATETIME2")) Ctrl[r].fmt = (unsigned char *)"3dDd(/)1mMm(/)9y9y( )2hHh(:)5m9m";
            if(checkstring(argv[a], (unsigned char *)"DATETIME3")) Ctrl[r].fmt = (unsigned char *)"1mMm(/)3dDd(/)9y9y( )1hMh(:)5m9m( )A";
            if(checkstring(argv[a], (unsigned char *)"DATETIME4")) Ctrl[r].fmt = (unsigned char *)"1mMm(/)3dDd(/)9y9y( )2hHh(:)5m9m";
            if(checkstring(argv[a], (unsigned char *)"LAT1")) Ctrl[r].fmt = (unsigned char *)"8d9d(` )5m9m(' )5s9s(\" )N";
            if(checkstring(argv[a], (unsigned char *)"LAT2")) Ctrl[r].fmt = (unsigned char *)"8d9d(` )5m9m(' )5s9s(.)9s(\" )N";
            if(checkstring(argv[a], (unsigned char *)"LONG1")) Ctrl[r].fmt = (unsigned char *)"1dLd9d(` )5m9m(' )5s9s(\" )E";
            if(checkstring(argv[a], (unsigned char *)"LONG2")) Ctrl[r].fmt = (unsigned char *)"1dLd9d(` )5m9m(' )5s9s(.)9s(\" )E";
            if(checkstring(argv[a], (unsigned char *)"ANGLE1")) Ctrl[r].fmt = (unsigned char *)"9d9d9d(` )5m9m(')";
        }
    }


    // get x1 and y1 - all controls require these
    Ctrl[r].x1 = getint(argv[a += 2], 0, HRes);
    if(argc < a) error("Argument count");
    Ctrl[r].y1 = getint(argv[a += 2], 0, VRes);

    // the fourth argument in GUI CAPTION is the justification
    if(type == CTRL_CAPTION) {
        a += 2;
        if(!(argc < a || *argv[a] == 0)) {                          // if justification is specified
            int jh = 0, jv = 0, jo = 0;
            if(!GetJustification((char *)argv[a], &jh, &jv, &jo))
                if(!GetJustification((char *)getCstring(argv[a]), &jh, &jv, &jo))
                    error("Justification");
            Ctrl[r].x2 = jh | jv << 2 | jo << 4;                    // stuff the justification parameters into the short int
        }
        else
            Ctrl[r].x2 = 0;
    } else {
        // get the width - all controls except CAPTION need this
        // for GAUGE, LED, etc this is the radius
        if(argc > a + 2) if(*argv[a += 2] != 0) last_x2 = Ctrl[r].x2 = getint(argv[a], BTN_SIDE_WIDTH, HRes);

        // now get or set the height
        switch(type) {
            case CTRL_NBRBOX:          *Ctrl[r].s = '0';            // this needs to be set, then fall thru
            case CTRL_BUTTON:
            case CTRL_SWITCH:
            case CTRL_FRAME:
            case CTRL_TEXTBOX:
            case CTRL_FMTBOX:
            case CTRL_DISPLAYBOX:
            case CTRL_SPINNER:
            case CTRL_AREA:
            case CTRL_BARGAUGE:         // these all need the height in addition to the width
                                        if(argc > a + 2) if(*argv[a += 2] != 0) last_y2 = Ctrl[r].y2 = getint(argv[a], BTN_SIDE_WIDTH, VRes);
                                        if(type != CTRL_BARGAUGE) { Ctrl[r].x2 +=  Ctrl[r].x1; Ctrl[r].y2 +=  Ctrl[r].y1; }
                                        break;

            case CTRL_CHECKBOX:         // the check box does not need the height and is a special case
                                        // its touch sensitive area is different from its drawing parameters
                                        // we have the width/height in x2 and it is saved in Ctrl[r].inc
                                        Ctrl[r].inc = Ctrl[r].x2;
                                        Ctrl[r].y2 =  Ctrl[r].y1 + Ctrl[r].x2;  // calculate the touch sensitive area
                                        Ctrl[r].x2 = Ctrl[r].x1 + Ctrl[r].x2 + (gui_font_width * (strlen((char *)Ctrl[r].s) + 1));  // calculate the touch sensitive area
                                        break;

            case CTRL_LED:
            case CTRL_RADIOBTN:         // the LED and radio button also do not need the height and are a special case
                                        // their touch sensitive area is stored in x1, y1, x2 and y2
                                        // the radius is stored in Ctrl[r].max and the X and Y centre of the button is calculated when drawing the control
                                        Ctrl[r].max = Ctrl[r].x2;
                                        Ctrl[r].x2 = Ctrl[r].x1 + Ctrl[r].max + (gui_font_width * (strlen((char *)Ctrl[r].s) + 1));  // calculate the touch sensitive area
                                        Ctrl[r].x1 -= Ctrl[r].max;
                                        Ctrl[r].y2 = Ctrl[r].y1 + Ctrl[r].max;
                                        Ctrl[r].y1 -= Ctrl[r].max;
                                        break;

        }
    }

    // get the foreground colour - all controls except the area control need this
    if(type != CTRL_AREA && argc > a + 2) if(*argv[a += 2] != 0) last_fcolour = Ctrl[r].fc = getint(argv[a], BLACK, WHITE);

    switch(type) {                  // these all need the background colour
        case CTRL_SPINNER:
        case CTRL_BUTTON:
        case CTRL_SWITCH:
        case CTRL_TEXTBOX:
        case CTRL_FMTBOX:
        case CTRL_NBRBOX:
        case CTRL_DISPLAYBOX:       if(argc > a + 2) if(*argv[a += 2] != 0) last_bcolour = Ctrl[r].bc = getint(argv[a], 0, WHITE);
                                    break;
        case CTRL_CAPTION:
        case CTRL_GAUGE:
        case CTRL_BARGAUGE:         if(argc > a + 2) if(*argv[a += 2] != 0) last_bcolour = Ctrl[r].bc = getint(argv[a], -1, WHITE);
                                    break;
    }

    if(type == CTRL_SPINNER) {
        if(argc > a + 2) if(*argv[a += 2] != 0) last_inc = Ctrl[r].inc = getnumber(argv[a]);
        if(argc > a + 2) if(*argv[a += 2] != 0) last_min = Ctrl[r].min = getnumber(argv[a]);
        if(argc > a + 2) last_max = Ctrl[r].max = getnumber(argv[a += 2]);
    }

    if(type == CTRL_GAUGE || type == CTRL_BARGAUGE) {
        // special processing for a gauge
        // Note that a GAUGE uses the allocated string memory (Ctrl[r].s) for also storing other data
        if(argc > a + 2) if(*argv[a += 2] != 0) last_min = Ctrl[r].min = getnumber(argv[a]);
        if(argc > a + 2) if(*argv[a += 2] != 0) last_max = Ctrl[r].max = getnumber(argv[a]);
        if(type == CTRL_GAUGE) { if(argc > a + 2) if(*argv[a += 2] != 0) last_inc = Ctrl[r].inc = getnumber(argv[a]); }

        strcpy((char *)Ctrl[r].s, last_units);                              // setup the default default
        if(type == CTRL_GAUGE && argc > a + 2) {
            if(*argv[a += 2] != 0) {
                strcpy((char *)Ctrl[r].s, (char *)getCstring(argv[a]));             // get the units caption
                Ctrl[r].s[GAUGE_UNITS_SIZE - 1] = 0;                // truncate to max size
               strcpy(last_units, (char *)Ctrl[r].s);                       // save as default
            } else
                *last_units = *Ctrl[r].s = 0;
        }
        GaugeS = (void *)(Ctrl[r].s);
        GaugeS->c1 = -1; GaugeS->c2 = GaugeS->c3 = GaugeS->c4 = gui_fcolour;
        GaugeS->ta = GaugeS->tb = GaugeS->tc = Ctrl[r].max;
        if(argc > a + 2) if(*argv[a += 2] != 0) GaugeS->c1 = getint(argv[a], 0, WHITE);
        if(argc > a + 2) if(*argv[a += 2] != 0) GaugeS->ta = getnumber(argv[a]);
        if(argc > a + 2) if(*argv[a += 2] != 0) GaugeS->c2 = getint(argv[a], 0, WHITE);
        if(argc > a + 2) if(*argv[a += 2] != 0) GaugeS->tb = getnumber(argv[a]);
        if(argc > a + 2) if(*argv[a += 2] != 0) GaugeS->c3 = getint(argv[a], 0, WHITE);
        if(argc > a + 2) if(*argv[a += 2] != 0) GaugeS->tc = getnumber(argv[a]);
        if(argc > a + 2) if(*argv[a += 2] != 0) GaugeS->c4 = getint(argv[a], 0, WHITE);
        if(type == CTRL_GAUGE) Ctrl[r].y2 = -1;                     // on first use draw the full gauge
    }

    if(argc > a + 1) error("Argument count");
    Ctrl[r].type = type;
    if(type == CTRL_GAUGE || type == CTRL_BARGAUGE) {
        Ctrl[r].value = Ctrl[r].min;
    } else {
        Ctrl[r].value = 0;
    }
    Ctrl[r].page = SetupPage;
    Ctrl[r].fcc = gui_fcolour;
    SetCtrlState(r, CTRL_NORMAL, false);
    return r;
}

/*  @endcond */

void cmd_gui(void) {
    int r;
    unsigned char *p;

    if(!Option.DISPLAY_TYPE) error("Display not configured");

    if((p = checkstring(cmdline, (unsigned char *)"PAGE"))) {
    	cmd_GUIpage(p);
    	return;
    }
    if((p = checkstring(cmdline, (unsigned char *)"BUTTON"))) {
        r = GetCtrlParams(CTRL_BUTTON, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"SWITCH"))) {
        r = GetCtrlParams(CTRL_SWITCH, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"CHECKBOX"))) {
        r = GetCtrlParams(CTRL_CHECKBOX, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"RADIO"))) {
        r = GetCtrlParams(CTRL_RADIOBTN, p);

        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"LED"))) {
        r = GetCtrlParams(CTRL_LED, p);
        Ctrl[r].inc = 0;
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"FRAME"))) {
        r = GetCtrlParams(CTRL_FRAME, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"NUMBERBOX"))) {
         if((checkstring(p, (unsigned char *)"CANCEL"))) {
            if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
            if(!InvokingCtrl) return;
            DrawKeyboard(KEY_KEY_CANCEL);
        } else
            r = GetCtrlParams(CTRL_NBRBOX, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"TEXTBOX"))) {
        if((checkstring(p, (unsigned char *)"CANCEL"))) {
            if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
            if(!InvokingCtrl) return;
            DrawKeyboard(KEY_KEY_CANCEL);

        } else
            r = GetCtrlParams(CTRL_TEXTBOX, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"FORMATBOX"))) {
        if((checkstring(p, (unsigned char *)"CANCEL"))) {
            if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
            if(!InvokingCtrl) return;
            DrawFmtBox(KEY_KEY_CANCEL);
        } else
            r = GetCtrlParams(CTRL_FMTBOX, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"SPINBOX"))) {
        r = GetCtrlParams(CTRL_SPINNER, p);
        Ctrl[r].value = 0;
        if(Ctrl[r].value < Ctrl[r].min) Ctrl[r].value = Ctrl[r].min;
        if(Ctrl[r].value > Ctrl[r].max) Ctrl[r].value = Ctrl[r].max;
        FloatToStr((char *)Ctrl[r].s, Ctrl[r].value, 0, STR_AUTO_PRECISION, ' ');
        UpdateControl(r);                                           // update the displayed string
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"DISPLAYBOX"))) {
        r = GetCtrlParams(CTRL_DISPLAYBOX, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"CAPTION"))) {
        r = GetCtrlParams(CTRL_CAPTION, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"GAUGE"))) {
        r = GetCtrlParams(CTRL_GAUGE, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"BARGAUGE"))) {
        r = GetCtrlParams(CTRL_BARGAUGE, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"AREA"))) {
        r = GetCtrlParams(CTRL_AREA, p);
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"DELETE"))) {
        int i, r;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!(argc & 1)) error("Argument count");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(checkstring(argv[0], (unsigned char *)"ALL")) {
            for(r = 1; r < Option.MaxCtrls; r++)
                if(Ctrl[r].type != 0) {
                    SetCtrlState(r, CTRL_HIDDEN, true);
                    FreeMemorySafe((void **)&Ctrl[r].s);
                    if(Ctrl[r].fmt) FreeMemorySafe((void **)&Ctrl[r].fmt);
                    memset(&Ctrl[r],0,sizeof(struct s_ctrl));
                }
            return;
        } else {
            for(i = 0; i < argc; i += 2) {
                if(*argv[i] == '#') argv[i]++;
                r = getint(argv[i], 1, Option.MaxCtrls - 1);
                SetCtrlState(r, CTRL_HIDDEN, true);
                FreeMemorySafe((void **)&Ctrl[r].s);
                if(Ctrl[r].fmt) FreeMemorySafe((void **)&Ctrl[r].fmt);
                memset(&Ctrl[r],0,sizeof(struct s_ctrl));
            }
        }
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"DISABLE"))) {
        int i, r;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!(argc & 1)) error("Argument count");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(checkstring(argv[0], (unsigned char *)"ALL")) {
            for(r = 1; r < Option.MaxCtrls; r++)
                if(CurrentPages & (1 << Ctrl[r].page))
                    SetCtrlState(r, CTRL_DISABLED, false);
            return;
        }
        for(i = 0; i < argc; i += 2) {
            if(*argv[i] == '#') argv[i]++;
            r = getint(argv[i], 1, Option.MaxCtrls - 1);
            SetCtrlState(r, CTRL_DISABLED, true);
        }
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"HIDE"))) {
        int i, r;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(!(argc & 1)) error("Argument count");
        if(checkstring(argv[0], (unsigned char *)"ALL")) {
            for(r = 1; r < Option.MaxCtrls; r++)
                if(CurrentPages & (1 << Ctrl[r].page))
                    SetCtrlState(r, CTRL_HIDDEN, false);
            return;
        }
        for(i = 0; i < argc; i += 2) {
            if(*argv[i] == '#') argv[i]++;
            r = getint(argv[i], 1, Option.MaxCtrls - 1);
            SetCtrlState(r, CTRL_HIDDEN, true);
        }
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"ENABLE"))) {
        int i, r;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!(argc & 1)) error("Argument count");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(checkstring(argv[0], (unsigned char *)"ALL")) {
            for(r = 1; r < Option.MaxCtrls; r++) {
                if(CurrentPages & (1 << Ctrl[r].page)) {
                    Ctrl[r].state &= ~(CTRL_DISABLED | CTRL_DISABLED2);
                    SetCtrlState(r, CTRL_NORMAL, false);
                }
            }
            return;
        }
        for(i = 0; i < argc; i += 2) {
            if(*argv[i] == '#') argv[i]++;
            r = getint(argv[i], 1, Option.MaxCtrls - 1);
            Ctrl[r].state &= ~(CTRL_DISABLED | CTRL_DISABLED2);
            SetCtrlState(r, CTRL_NORMAL, true);
        }
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"SHOW"))) {
        int i, r;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!(argc & 1)) error("Argument count");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(checkstring(argv[0], (unsigned char *)"ALL")) {
            for(r = 1; r < Option.MaxCtrls; r++) {
                if(CurrentPages & (1 << Ctrl[r].page)) {
                    Ctrl[r].state &= ~CTRL_HIDDEN;
                    SetCtrlState(r, CTRL_NORMAL, false);
                }
            }
            return;
        }
        for(i = 0; i < argc; i += 2) {
            if(*argv[i] == '#') argv[i]++;
            r = getint(argv[i], 1, Option.MaxCtrls - 1);
            Ctrl[r].state &= ~CTRL_HIDDEN;
            SetCtrlState(r, CTRL_NORMAL, true);
        }
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"RESTORE"))) {
        int i, r;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(!(argc & 1)) error("Argument count");
        if(checkstring(argv[0], (unsigned char *)"ALL")) {
            for(r = 1; r < Option.MaxCtrls; r++) {
                    if(CurrentPages & (1 << Ctrl[r].page)) {
                    Ctrl[r].state &= ~(CTRL_DISABLED | CTRL_DISABLED2 | CTRL_HIDDEN);
                    SetCtrlState(r, CTRL_NORMAL, false);
                }
            }
            return;
        }
        for(i = 0; i < argc; i += 2) {
            if(*argv[i] == '#') argv[i]++;
            r = getint(argv[i], 1, Option.MaxCtrls - 1);
            Ctrl[r].state &= ~(CTRL_DISABLED | CTRL_DISABLED2 | CTRL_HIDDEN);
            SetCtrlState(r, CTRL_NORMAL, true);
        }
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"REDRAW"))) {
        int i, r;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!(argc & 1)) error("Argument count");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(checkstring(argv[0], (unsigned char *)"ALL")) {
            ClearScreen(gui_bcolour);
            for(r = 1; r < Option.MaxCtrls; r++)
                if(CurrentPages & (1 << Ctrl[r].page)) {
                    if(Ctrl[r].type == CTRL_GAUGE) Ctrl[r].y2 = -1; // this will force a full redraw of the gauge
                    UpdateControl(r);
                }
            return;
        }
        for(i = 0; i < argc; i += 2) {
            if(*argv[i] == '#') argv[i]++;
            r = getint(argv[i], 1, Option.MaxCtrls - 1);
            if(Ctrl[r].type == CTRL_GAUGE) Ctrl[r].y2 = -1;         // this will force a full redraw of the gauge
            UpdateControl(r);
        }
        return;
    }


    if((p = checkstring(cmdline, (unsigned char *)"FCOLOUR")) || (p = checkstring(cmdline, (unsigned char *)"BCOLOUR"))) {
        int i, r, c;
        getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        if(!(argc & 1) || argc < 3) error("Argument count");
        c = getint(argv[0], BLACK, WHITE);
        for(i = 2; i < argc; i += 2) {
            if(*argv[i] == '#') argv[i]++;
            r = getint(argv[i], 1, Option.MaxCtrls - 1);
            if(Ctrl[r].type == 0) error("Control #% does not exist", r);
            if(checkstring(cmdline, (unsigned char *)"FCOLOUR"))
                Ctrl[r].fc = c;
            else
                Ctrl[r].bc = c;
            UpdateControl(r);
        }
        return;
    }


/*    if((p = checkstring(cmdline, "BEEP"))) {
        if(Option.TOUCH_Click == 0) error("Click option not set");
        ClickTimer = getint(p, 0, INT_MAX) + 1;
      return;
  }*/


    if((p = checkstring(cmdline, (unsigned char *)"INTERRUPT"))) {
        getargs(&p, 3, (unsigned char *)",");
        if(Option.TOUCH_CS == 0) error("Touch option not set");
        if(!Option.TOUCH_XZERO && !Option.TOUCH_YZERO) error("Touch not calibrated");
      if(*argv[0] == '0' && !isdigit(*(argv[0]+1)))
            GuiIntDownVector = GuiIntUpVector = NULL;
        else {
            GuiIntDownVector = (char *)GetIntAddress(argv[0]);              // get a pointer to the down interrupt routine
            if(argc == 3)
                GuiIntUpVector = (char *)GetIntAddress(argv[2]);            // and for the up routine
            else
                GuiIntUpVector = NULL;
            InterruptUsed = true;
        }
        gui_int_down = gui_int_up = false;
      return;
  }


    if((p = checkstring(cmdline, (unsigned char *)"SETUP"))) {
        if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
        SetupPage = getint(p, 1, MAX_PAGES) - 1;
    return;
  }

    // the final few commands are common to the MX170 and MX470 so execute them in Draw.c
    cmd_guiMX170();
}




void cmd_GUIpage(unsigned char *p) {
    int i, r, OldPages;

    getargs(&p, MAX_ARG_COUNT, (unsigned char *)",");
    if(!(argc & 1)) error("Argument count");
    if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
    OldPages = CurrentPages;
    CurrentPages = 0;
    for(i = 0; i < argc; i += 2) {                                  // get the new set of pages
        if(*argv[i] == '#') argv[i]++;
        CurrentPages |= (1 << (getint(argv[i], 1, MAX_PAGES) - 1));
    }

    // hide any that are showing but not on the new pages
    for(r = 0; r < Option.MaxCtrls; r++) {                          // step thru the controls
        if(Ctrl[r].type != 0) {                                     // if the control is active
            if((!(CurrentPages & (1 << Ctrl[r].page))) && (OldPages & (1 << Ctrl[r].page)) && (!(Ctrl[r].state & CTRL_HIDDEN))) {
                Ctrl[r].state |= CTRL_HIDDEN;
                if(r == CurrentRef) {
                    if(Ctrl[r].type == CTRL_BUTTON) Ctrl[r].value = 0;
                    if(Ctrl[r].type == CTRL_SPINNER) Ctrl[r].state &= ~(CTRL_SPINUP | CTRL_SPINDOWN);
                    LastRef = CurrentRef = 0;
                    gui_int_down = gui_int_up = false;
                }
                DrawControl(r);
                Ctrl[r].state &= ~CTRL_HIDDEN;
            }
        }
    }

    // show any that are on the new pages but not currently showing and are not hidden
    for(r = 0; r < Option.MaxCtrls; r++) {                          // step thru the controls
        if(Ctrl[r].type != 0) {                                     // if the control is active
            if((!(OldPages & (1 << Ctrl[r].page))) && (CurrentPages & (1 << Ctrl[r].page)) && (!(Ctrl[r].state & CTRL_HIDDEN))) {
                DrawControl(r);
            }
        }
    }
}

/* 
 * @cond
 * The following section will be excluded from the documentation.
 */


////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// This is the detailed implementation of the GUI controls in the Micromite Plus
//
////////////////////////////////////////////////////////////////////////////////////////////////////////




int ChangeBright(int c, int pct) {
    int r, g, b;
    if(pct == 0 || c <= 0) return c;
    r = (c >> 16) & 0xff;
    g = (c >> 8) & 0xff;
    b = c & 0xff;
    r += (r * pct) / 100; if(r > 255) r = 255; if(r < 0) r = 0;
    g += (g * pct) / 100; if(g > 255) g = 255; if(g < 0) g = 0;
    b += (b * pct) / 100; if(b > 255) b = 255; if(b < 0) b = 0;
    return RGB(r, g, b);
}


void SpecialWritePixel(int x, int y, unsigned int tc, int status) {
    if(status & CTRL_HIDDEN) {
        tc = gui_bcolour;
    } else if(status & (CTRL_DISABLED | CTRL_DISABLED2)) {
        tc = ChangeBright(tc, BTN_DISABLED);
    }
    DrawPixel(x, y, tc);
}


void SpecialDrawLine(int x1, int y1, int x2, int y2, int w, int tc, int status){
    if(status & CTRL_HIDDEN) {
        tc = gui_bcolour;
    } else if(status & (CTRL_DISABLED | CTRL_DISABLED2)) {
        tc = ChangeBright(tc, BTN_DISABLED);
    }
    DrawLine(x1, y1, x2, y2, w, tc);
}


void SpecialDrawBox(int x1, int y1, int x2, int y2, int w, int c, int fill, int status) {
    if(status & CTRL_HIDDEN) {
        c = gui_bcolour;
        fill = gui_bcolour;
    } else if(status & (CTRL_DISABLED | CTRL_DISABLED2)) {
        c = ChangeBright(c, BTN_DISABLED);
        if(fill != gui_bcolour) fill = ChangeBright(fill, BTN_DISABLED);
    }
    DrawBox(x1, y1, x2, y2, w, c, fill);
}


void SpecialDrawRBox(int x1, int y1, int x2, int y2, int radius, int c, int fill, int status) {
    if(status & CTRL_HIDDEN) {
        c = gui_bcolour;
        fill = gui_bcolour;
    } else if(status & (CTRL_DISABLED | CTRL_DISABLED2)) {
        c = ChangeBright(c, BTN_DISABLED);
        if(fill != gui_bcolour) fill = ChangeBright(fill, BTN_DISABLED);
    }
    DrawRBox(x1, y1, x2, y2, radius, c, fill);
}


void SpecialPrintString(int x, int y, int jh, int jv, int jo, int fc, int bc, char *str, int status) {
    int lines, i, y2;
    char t, *p, *idx[MAX_CAPTION_LINES + 1];

    // first check if the string contains one or more line split chars ('~')
    // while we are doing this save their addresses in idx[]
    idx[0] = p = str;
    lines = 1;
    while((p = strchr((char *)p, '~')) != NULL && lines < MAX_CAPTION_LINES) {
        idx[lines] = p++;
        lines++;
    }

    if(lines > 1) {
        // this is two or more lines
        // recursively call this function to print each line
        idx[lines] = str + strlen(str);
        y2 = y - (gui_font_height / 2) * (lines - 1);
        for(i = 0; i < lines; i++) {
            t = *idx[i + 1];
            *idx[i + 1] = 0;
            SpecialPrintString(x, y2, jh, jv, jo, fc, bc, idx[i], status);
            *idx[i + 1] = t;
            if(t == '~') idx[i + 1]++;
            y2 += gui_font_height;
        }
    } else {
        // this is just a single line, so print it
        if(status & CTRL_HIDDEN) {
            fc = gui_bcolour;
            bc = gui_bcolour;
        } else if(status & (CTRL_DISABLED | CTRL_DISABLED2)) {
            fc = ChangeBright(fc, BTN_DISABLED);
            if(bc != gui_bcolour) bc = ChangeBright(bc, BTN_DISABLED);
        }
        GUIPrintString(x, y, gui_font, jh, jv, jo, fc, bc, str);
    }
}



void SpecialDrawCircle(int x, int y, int radius, int w, int c, int fill, MMFLOAT aspect, int status) {
    if(status & CTRL_HIDDEN) {
        c = gui_bcolour;
        fill = gui_bcolour;
    } else if(status & (CTRL_DISABLED | CTRL_DISABLED2)) {
        c = ChangeBright(c, BTN_DISABLED);
        if(fill != gui_bcolour) fill = ChangeBright(fill, BTN_DISABLED);
    }
    DrawCircle(x, y, radius, w, c, fill, aspect);
}



void SpecialDrawTriangle(int x0, int y0, int x1, int y1, int x2, int y2, int c, int fill, int status) {
    if(status & CTRL_HIDDEN) {
        c = gui_bcolour;
        fill = gui_bcolour;
    } else if(status & (CTRL_DISABLED | CTRL_DISABLED2)) {
        c = ChangeBright(c, BTN_DISABLED);
        if(fill != gui_bcolour) fill = ChangeBright(fill, BTN_DISABLED);
    }
    DrawTriangle(x0, y0, x1, y1, x2, y2, c, fill);
    DrawTriangle(x0, y0, x1, y1, x2, y2, c, -1);
}



void DrawBorder(int x1, int y1, int x2, int y2, int w, int tc, int bc, int status) {
    int i;
    for(i = 0; i < w; i++) {
        SpecialDrawLine(x1 + i, y1 + i, x2 - i, y1 + i, 1, tc, status);     // top border
        SpecialDrawLine(x1 + i, y1 + i, x1 + i, y2 - i, 1, tc, status);     // left border
        SpecialDrawLine(x1 + i, y2 - i, x2 - i, y2 - i, 1, bc, status);     // bottom
        SpecialDrawLine(x2 - i, y1 + i, x2 - i, y2 - i, 1, bc, status);     // and right
    }
}



void DrawBasicButton(int x1, int y1, int x2, int y2, int w, int up, int c, int status) {
    DrawBorder(x1, y1, x2, y2, w, ChangeBright(c, up ? BTN_SIDE_BRIGHT:BTN_SIDE_DULL), ChangeBright(c, BTN_SIDE_DULL), status);
    SpecialDrawBox(x1 + w, y1 + w, x2 - w, y2 - w, 0, 0, c, status);    // fill in the face of the button
}



void DrawButton(int r) {
    unsigned char *p, *pp, s[MAXSTRLEN];
    int bs = 0;

    DrawBasicButton(Ctrl[r].x1, Ctrl[r].y1, Ctrl[r].x2, Ctrl[r].y2, BTN_SIDE_WIDTH, (Ctrl[r].value == 0), Ctrl[r].bc, Ctrl[r].state);

    // extract the up/down strings if necessary
    for(p = Ctrl[r].s, pp = s; *p != '|';) {
        if(*p == 0) {
            p = s - 1;
            bs = BTN_CAPTION_SHIFT;
            break;
        }
        *pp++ = *p++;
    }
    p++;
    *pp = 0;

    // draw the caption
    if(Ctrl[r].value == 0)
        SpecialPrintString(Ctrl[r].x1 + (Ctrl[r].x2 - Ctrl[r].x1)/2, Ctrl[r].y1 + (Ctrl[r].y2 - Ctrl[r].y1)/2, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, Ctrl[r].fc, Ctrl[r].bc, (char *)s, Ctrl[r].state);
    else
        SpecialPrintString(Ctrl[r].x1 + (Ctrl[r].x2 - Ctrl[r].x1)/2 + bs, Ctrl[r].y1 + (Ctrl[r].y2 - Ctrl[r].y1)/2 + bs, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, Ctrl[r].fc, Ctrl[r].bc, (char *)p, Ctrl[r].state);
}



void DrawSwitch(int r) {
    unsigned char *p, *pp, s[MAXSTRLEN];
    int half, on, twobtn, shift;

    // extract the up/down strings if necessary
    twobtn = true;
    for(p = Ctrl[r].s, pp = s; *p != '|';) {
        if(*p == 0) {
            p = s - 1;
            twobtn = false;
            break;
        }
        *pp++ = *p++;
    }
    p++;
    *pp = 0;

    on = (Ctrl[r].value == 0);
    if(on) shift = 0; else shift = BTN_CAPTION_SHIFT;
    if(twobtn) {
        half = Ctrl[r].x1 + (Ctrl[r].x2 - Ctrl[r].x1)/2;
        if(!(on)) {                                                    // use the min, max elements to store the active x area of the button
            Ctrl[r].min = half;
            Ctrl[r].max = Ctrl[r].x2;
        } else {
            Ctrl[r].min = Ctrl[r].x1;
            Ctrl[r].max = half;
        }          
    } else {
        Ctrl[r].min = Ctrl[r].x1;
        Ctrl[r].max = half = Ctrl[r].x2;
    }
    
    DrawBasicButton(Ctrl[r].x1, Ctrl[r].y1, half, Ctrl[r].y2, BTN_SIDE_WIDTH, on, ChangeBright(Ctrl[r].bc, on ? 0 : -25), Ctrl[r].state);
    if(twobtn) DrawBasicButton(half, Ctrl[r].y1, Ctrl[r].x2, Ctrl[r].y2, BTN_SIDE_WIDTH, !on, ChangeBright(Ctrl[r].bc, on ? -25 : 0), Ctrl[r].state);

    // draw the captions
    if(on) shift = 0; else shift = BTN_CAPTION_SHIFT;
    SpecialPrintString(Ctrl[r].x1 + (half - Ctrl[r].x1)/2 + shift, Ctrl[r].y1 + (Ctrl[r].y2 - Ctrl[r].y1)/2 + shift,      JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, ChangeBright(Ctrl[r].fc, (on || !twobtn) ? 0:-25), ChangeBright(Ctrl[r].bc, on ? 0:-25), (char *)s, Ctrl[r].state);
    if(!on) shift = 0; else shift = BTN_CAPTION_SHIFT;
    if(twobtn) SpecialPrintString(half + (Ctrl[r].x2 - half)/2 + shift, Ctrl[r].y1 + (Ctrl[r].y2 - Ctrl[r].y1)/2 + shift, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, ChangeBright(Ctrl[r].fc, on ? -25:0),  ChangeBright(Ctrl[r].bc, on ? -25:0),  (char *)p, Ctrl[r].state);
}



// for the checkbox the width/height is saved in Ctrl[r].inc
void DrawCheckBox(int r) {
    int i, w;

    SpecialDrawBox(Ctrl[r].x1, Ctrl[r].y1, Ctrl[r].x1 + Ctrl[r].inc, Ctrl[r].y1 + Ctrl[r].inc, BTN_SIDE_WIDTH, ChangeBright(Ctrl[r].fc, -30), gui_bcolour, Ctrl[r].state) ;
    SpecialPrintString(Ctrl[r].x1 + Ctrl[r].inc + Ctrl[r].inc/2, Ctrl[r].y1 + Ctrl[r].inc/2, JUSTIFY_LEFT, JUSTIFY_MIDDLE, ORIENT_NORMAL, Ctrl[r].fcc, gui_bcolour, (char *)Ctrl[r].s, Ctrl[r].state);

    // draw the tick
    if(Ctrl[r].value != 0) {
        w = (Ctrl[r].inc / 28) + 1;                                     // vary the tick size according to the box size
        for(i = -w; i <= w; i++) {
            SpecialDrawLine(Ctrl[r].x1 + i + BTN_SIDE_WIDTH*2, Ctrl[r].y1 + BTN_SIDE_WIDTH*2, Ctrl[r].x1 + Ctrl[r].inc + i - BTN_SIDE_WIDTH*2, Ctrl[r].y2 - BTN_SIDE_WIDTH*2, 1, Ctrl[r].fc, Ctrl[r].state);
            SpecialDrawLine(Ctrl[r].x1 + i + BTN_SIDE_WIDTH*2, Ctrl[r].y2 - BTN_SIDE_WIDTH*2, Ctrl[r].x1 + Ctrl[r].inc + i - BTN_SIDE_WIDTH*2, Ctrl[r].y1 + BTN_SIDE_WIDTH*2, 1, Ctrl[r].fc, Ctrl[r].state);
        }
    }
}



// the radio button is a special case, its touch sensitive area is different from its drawing parameters
// the touch sensitive area is stored in x1, y1 to x2, y2
// the buttons radius is stored in Ctrl[r].max and the X and Y centre of the button (xc and yc) are calculated using that
void DrawRadioBtn(int r) {
    int i, frame;
    int xc = Ctrl[r].x1 + Ctrl[r].max;
    int yc = Ctrl[r].y1 + Ctrl[r].max;

    SpecialDrawCircle(xc, yc, Ctrl[r].max, BTN_SIDE_WIDTH, ChangeBright(Ctrl[r].fc, -30), gui_bcolour, 1.0, Ctrl[r].state);
    SpecialPrintString(xc + Ctrl[r].max + gui_font_width - (gui_font_width/4), yc, JUSTIFY_LEFT, JUSTIFY_MIDDLE, ORIENT_NORMAL, Ctrl[r].fcc, gui_bcolour, (char *)Ctrl[r].s, Ctrl[r].state);

    if(Ctrl[r].value != 0) {
        // draw the button if this control has been selected
        SpecialDrawCircle(xc, yc, Ctrl[r].max - ((BTN_SIDE_WIDTH * 3) / 2), 0, 0, Ctrl[r].fc, 1.0, Ctrl[r].state);

        // make sure that all the other radio buttons are in the up state
        // first find the frame (if any) that the current button is in
        for(frame = 1; frame < Option.MaxCtrls; frame++)
            if(Ctrl[frame].type == CTRL_FRAME && Ctrl[r].page == Ctrl[frame].page && xc > Ctrl[frame].x1 && xc < Ctrl[frame].x2 && yc > Ctrl[frame].y1 && yc < Ctrl[frame].y2)
                break;
        // next look for any other radio buttons that are not the current one and is down and is in the same frame... and set it up
        for(i = 1; i < Option.MaxCtrls; i++) {
            if(Ctrl[i].type == CTRL_RADIOBTN && Ctrl[i].page == Ctrl[r].page && i != r && Ctrl[r].value != 0) {
                // if frame == MAX_CTRL this means that there is no frame and the whole screen can be considered the frame
                if(frame == Option.MaxCtrls || (Ctrl[i].x1 + Ctrl[i].max > Ctrl[frame].x1 && Ctrl[i].x1 + Ctrl[i].max < Ctrl[frame].x2 && Ctrl[i].y1 + Ctrl[i].max > Ctrl[frame].y1 && Ctrl[i].y1 + Ctrl[i].max < Ctrl[frame].y2)) {
                    Ctrl[i].value = 0;
                    UpdateControl(i);
                }
            }
        }
    }
}



// the LED is a special case, its touch sensitive area is different from its drawing parameters
// the touch sensitive area is stored in x1, y1 to x2, y2
// the LED's radius is stored in Ctrl[r].max and the X and Y centre of the button (xc and yc) are calculated using that
void DrawLED(int r) {
    int xc = Ctrl[r].x1 + Ctrl[r].max;
    int yc = Ctrl[r].y1 + Ctrl[r].max;

    SpecialDrawCircle(xc, yc, Ctrl[r].max, 0, 0, RGB(160,160,160), 1.0, Ctrl[r].state);
    SpecialDrawCircle(xc, yc, Ctrl[r].max - BTN_SIDE_WIDTH/2, 0, 0, ChangeBright(Ctrl[r].fc, (Ctrl[r].value == 0) ? -65:0), 1.0, Ctrl[r].state);
    SpecialPrintString(xc + Ctrl[r].max + gui_font_width - (gui_font_width/4), yc, JUSTIFY_LEFT, JUSTIFY_MIDDLE, ORIENT_NORMAL, Ctrl[r].fcc, gui_bcolour, (char *)Ctrl[r].s, Ctrl[r].state);
}


void DrawSpinner(int r) {
    int x1, x2, y1, y2;
    int h, z;

    h = (Ctrl[r].y2 - Ctrl[r].y1);
    z = h/6;
    x1 = Ctrl[r].x1; x2 = Ctrl[r].x2;
    y1 = Ctrl[r].y1; y2 = Ctrl[r].y2;
    SpecialDrawRBox(x1 + h, y1, x2 - h, y2, 10,  Ctrl[r].fc, Ctrl[r].bc, Ctrl[r].state);
    FloatToStr((char *)Ctrl[r].s, Ctrl[r].value, 0, STR_AUTO_PRECISION, ' ');               // convert the value to a string for display
    Ctrl[r].s[(x2 - x1 - h * 2) / gui_font_width] = 0;              // truncate to the display width
    SpecialPrintString(x1 + (x2 - x1)/2, y1 + h/2, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, Ctrl[r].fc, Ctrl[r].bc, (char *)Ctrl[r].s, Ctrl[r].state);
    if(Ctrl[r].state & CTRL_SPINUP)
        SpecialDrawTriangle(x2 - h/2, y1 + z, x2 - z, y1 + h - z, x2 - h + z, y1 + h - z, Ctrl[r].bc, Ctrl[r].fc, Ctrl[r].state);  // touched up pointing spin button
    else
        SpecialDrawTriangle(x2 - h/2, y1 + z, x2 - z, y1 + h - z, x2 - h + z, y1 + h - z, Ctrl[r].fc, Ctrl[r].bc, Ctrl[r].state);  // up pointing spin button
    if(Ctrl[r].state & CTRL_SPINDOWN)
        SpecialDrawTriangle(x1 + h/2, y1 + h - z, x1 + z, y1 + z, x1 + h - z, y1 + z, Ctrl[r].bc, Ctrl[r].fc, Ctrl[r].state);      // touched down pointing spin button
    else
        SpecialDrawTriangle(x1 + h/2, y1 + h - z, x1 + z, y1 + z, x1 + h - z, y1 + z, Ctrl[r].fc, Ctrl[r].bc, Ctrl[r].state);      // down pointing spin button
}



void DrawFrame(int r) {
    SpecialDrawRBox(Ctrl[r].x1, Ctrl[r].y1, Ctrl[r].x2, Ctrl[r].y2, 10, Ctrl[r].fc, -1, Ctrl[r].state);     // the border
    SpecialPrintString(Ctrl[r].x1 + 15, Ctrl[r].y1, JUSTIFY_LEFT, JUSTIFY_MIDDLE, ORIENT_NORMAL, Ctrl[r].fc, gui_bcolour, (char *)Ctrl[r].s, Ctrl[r].state);
}


void DrawCaption(int r) {
    SpecialPrintString(Ctrl[r].x1, Ctrl[r].y1, Ctrl[r].x2 & 0b0000011, (Ctrl[r].x2 >> 2) & 0b0000011, (Ctrl[r].x2 >> 4) & 0b0000111, Ctrl[r].fc, Ctrl[r].bc, (char *)Ctrl[r].s, Ctrl[r].state);
}



void DrawGauge(int r) {
    int x, y, c;
    MMFLOAT radius;
    int xo1, yo1, xo2, yo2, xi1, yi1, xi2, yi2;
    int i, v1, v2, v, ta, tb, tc;
    char buf[GAUGE_UNITS_SIZE * 2];
    struct s_GaugeS *GaugeS;                                        // we store extra info in the string allocated to this control


    // these define the span of the gauge in degrees
    const int gstart = -225;
    const int gend = 45;

    // this defines the width of the gauge as a percentage of the radius
    const MMFLOAT width = 0.25;

    // this is the number of degrees resolution
    // smaller nbr gives a cleaner graph but is slower
    const int stepsize = 3;

    x = Ctrl[r].x1; y = Ctrl[r].y1; radius = Ctrl[r].x2;
    GaugeS = (void *)Ctrl[r].s;
    c = Ctrl[r].fc;

    // scale the value of the gauge and the thresholds to degrees within the span of the gauge
    v = (((Ctrl[r].value - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (gend - gstart)) + gstart;
    ta = (((GaugeS->ta - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (gend - gstart)) + gstart;
    tb = (((GaugeS->tb - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (gend - gstart)) + gstart;
    tc = (((GaugeS->tc - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (gend - gstart)) + gstart;

    // make sure that the value and the thresholds are within the range
    if(v < gstart) v = gstart;
    if(v > gend) v = gend;
    if(ta <= gstart) ta = gstart + 1;
    if(tb <= ta + 1) tb = ta + 2;
    if(tc <= tb + 1) tc = tb + 2;
    if(ta > gend) ta = gend;
    if(tb > gend) tb = gend;
    if(tc > gend) tc = gend;

    // set v1 and v2 so that we are drawing only the part that changed while making sure v1 < v2
    if(v > GaugeS->cval) {
      v1 = GaugeS->cval; v2 = v;
    } else {
      v2 = GaugeS->cval; v1 = v;
    }

    if(Ctrl[r].y2 != Ctrl[r].state || Ctrl[r].fc != GaugeS->lastfc || Ctrl[r].bc != GaugeS->lastbc ) {
        // if the state of this control has changed we then need to redraw the complete gauge rather than just update the content of the gauge graph
        v1 = gstart;
        v2 = gend;
        GaugeS->laststrlen = GAUGE_UNITS_SIZE;
        Ctrl[r].y2 = Ctrl[r].state;
        GaugeS->cval = gend + 1;
        GaugeS->lastfc = Ctrl[r].fc; GaugeS->lastbc = Ctrl[r].bc;
    }

    if(v != GaugeS->cval) {  // only draw the gauge part if something has changed
        // draw the gauge
        // this only draws the part changed since the previous call
        for(i = v1; i <= v2; i = i+stepsize) {
            xo1 = x + (cosf(Rad(i)) * radius);
            yo1 = y + (sinf(Rad(i)) * radius);
            xo2 = x + (cosf(Rad(i+stepsize)) * radius);
            yo2 = y + (sinf(Rad(i+stepsize)) * radius);
            xi1 = x + (cosf(Rad(i)) * (radius * (1 - width)));
            yi1 = y + (sinf(Rad(i)) * (radius * (1 - width)));
            xi2 = x + (cosf(Rad(i+stepsize)) * (radius * (1 - width)));
            yi2 = y + (sinf(Rad(i+stepsize)) * (radius * (1 - width)));

            c = Ctrl[r].fc;
            if(GaugeS->c1 >= 0) c = GaugeS->c1;
            if(i > ta) c = GaugeS->c2;
            if(i > tb) c = GaugeS->c3;
            if(i > tc) c = GaugeS->c4;

            if(v > i || v == gend) {
                // drawing a filled in part of the gauge
                SpecialDrawTriangle(xo1, yo1, xo2, yo2, xi1, yi1, c, c, Ctrl[r].state);
                SpecialDrawTriangle(xo2, yo2, xi1, yi1, xi2, yi2, c, c, Ctrl[r].state);
            } else {
                GaugeS->csaved = c;
                // erasing part of the gauge
                c = ChangeBright(c, -70);
                if(GaugeS->c1 < 0 || ta >= gend) {
                    c = Ctrl[r].bc;
                    GaugeS->csaved = Ctrl[r].fc;
                }
                SpecialDrawTriangle(xo1, yo1, xo2, yo2, xi1, yi1, c, c, Ctrl[r].state);
                SpecialDrawTriangle(xo2, yo2, xi1, yi1, xi2, yi2, c, c, Ctrl[r].state);
            }
            // draw the outside frame
            SpecialDrawLine(xo1, yo1, xo2, yo2, 1, Ctrl[r].fc, Ctrl[r].state);
            SpecialDrawLine(xi1, yi1, xi2, yi2, 1, Ctrl[r].fc, Ctrl[r].state);

            // draw the end stops
            if(i == gstart) SpecialDrawLine(xi1, yi1, xo1, yo1, 1, Ctrl[r].fc, Ctrl[r].state);
            if(i == gend) SpecialDrawLine(xi2, yi2, xo2, yo2, 1, Ctrl[r].fc, Ctrl[r].state);
        }
    }
    
    FloatToStr(buf, Ctrl[r].value, 0, Ctrl[r].inc, ' ');            // convert the value to a string for display
    if(strlen(buf) < GaugeS->laststrlen) {                          // erase the last text if the length of the text has shrunk
        SpecialDrawCircle(x, y, (radius * 0.75) - 2, 0, Ctrl[r].bc, Ctrl[r].bc, 1.0, Ctrl[r].state) ;
    }
    GaugeS->laststrlen = strlen(buf);

    // get the colour of the text for the current value
    c = Ctrl[r].fc;
    if(GaugeS->c1 >= 0) c = GaugeS->c1;
    if(v > ta) c = GaugeS->c2;
    if(v > tb) c = GaugeS->c3;
    if(v > tc) c = GaugeS->c4;
    
    // draw the text
    if(*Ctrl[r].s) { strcat(buf, "~"); strcat(buf, (char *)Ctrl[r].s); }    // append the units string
    SpecialPrintString(x, y, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, c, Ctrl[r].bc, buf, Ctrl[r].state);

    GaugeS->cval = v;
}



void DrawBarGauge(int r) {
    int vert, x, y, x2 = 0, y2 = 0, start, end, len;
    int v, ta, tb, tc;
    struct s_GaugeS *GaugeS;                                        // we store extra info in the string allocated to this control

    if(Ctrl[r].x2 < Ctrl[r].y2) {
        // vertical bar
        // x, y refer to the bottom left of the bar (not the outline)
        // x2 is the width of the bar and len the overall length
        vert = true;
        x = Ctrl[r].x1 + 1;
        y = Ctrl[r].y1 + Ctrl[r].y2 - 1;
        x2 = Ctrl[r].x2 + x - 2;
        len = Ctrl[r].y2 - 2;
    } else {
        // horizontal bar
        // x, y refer to the top left coord of the bar
        // x2 is the width of the bar and len the overall length
        vert = false;
        x = Ctrl[r].x1 + 1;
        y = Ctrl[r].y1 + 1;
        y2 = Ctrl[r].y2 + y - 2;
        len = Ctrl[r].x2 - 2;
    }

    GaugeS = (void *)Ctrl[r].s;

    // scale the value of the gauge and the thresholds to pixels within the span of the gauge
    v = ((Ctrl[r].value - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (MMFLOAT)len;
    ta = ((GaugeS->ta - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (MMFLOAT)len;
    tb = ((GaugeS->tb - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (MMFLOAT)len;
    tc = ((GaugeS->tc - Ctrl[r].min) / (Ctrl[r].max - Ctrl[r].min)) * (MMFLOAT)len;

    // make sure that the value and the thresholds are within the range
    if(v < 0) v = 0;
    if(v > len) v = len;
    if(ta <= 0) ta = 1;
    if(tb <= ta + 1) tb = ta + 2;
    if(tc <= tb + 1) tc = tb + 2;
    if(ta > len) ta = len;
    if(tb > len) tb = len;
    if(tc > len) tc = len;

    SpecialDrawLine(Ctrl[r].x1, Ctrl[r].y1, Ctrl[r].x1 + Ctrl[r].x2, Ctrl[r].y1, 1, Ctrl[r].fc, Ctrl[r].state);    // draw the top of the outline
    SpecialDrawLine(Ctrl[r].x1, Ctrl[r].y1, Ctrl[r].x1, Ctrl[r].y1 + Ctrl[r].y2, 1, Ctrl[r].fc, Ctrl[r].state);    // draw the left side of the outline
    SpecialDrawLine(Ctrl[r].x1, Ctrl[r].y1 + Ctrl[r].y2, Ctrl[r].x1 + Ctrl[r].x2, Ctrl[r].y1 + Ctrl[r].y2, 1, Ctrl[r].fc, Ctrl[r].state);    // draw the botton of the outline
    SpecialDrawLine(Ctrl[r].x1 + Ctrl[r].x2, Ctrl[r].y1, Ctrl[r].x1 + Ctrl[r].x2, Ctrl[r].y1 + Ctrl[r].y2, 1, Ctrl[r].fc, Ctrl[r].state);    // draw the right side of the outline

    if(vert) {
        if(GaugeS->c1 < 0) GaugeS->c1 = Ctrl[r].fc;
        // draw the first bar
        if(v > ta) end = ta; else end = v;
        if(end > 0) {
            SpecialDrawBox(x, y, x2, y - end, 0, GaugeS->c1, GaugeS->c1, Ctrl[r].state);
            // draw the second bar
            start = end + 1; if(v > tb) end = tb; else end = v;
            if(end > start) {
                SpecialDrawBox(x, y - start, x2, y - end, 0, GaugeS->c2, GaugeS->c2, Ctrl[r].state);
                // draw the third bar
                start = end + 1; if(v > tc) end = tc; else end = v;
                if(end > start) {
                    SpecialDrawBox(x, y - start, x2, y - end, 0, GaugeS->c3, GaugeS->c3, Ctrl[r].state);
                    // finally draw the fourth bar
                    start = end + 1; if(v > tc) end = v;
                    if(end > start) {
                        SpecialDrawBox(x, y - start, x2, y - end, 0, GaugeS->c4, GaugeS->c4, Ctrl[r].state);
                    }
                }
            }
        }
        // erase the remaining part of the bar
        if(end < len) SpecialDrawBox(x, y - end, x2, y - len, 0, Ctrl[r].bc, Ctrl[r].bc, Ctrl[r].state);
    } else {
        if(GaugeS->c1 < 0) GaugeS->c1 = Ctrl[r].fc;
        // draw the first bar
        if(v > ta) end = ta; else end = v;
        if(end > 0) {
            SpecialDrawBox(x, y, x + end, y2, 0, GaugeS->c1, GaugeS->c1, Ctrl[r].state);
            // draw the second bar
            start = end + 1; if(v > tb) end = tb; else end = v;
            if(end > start) {
                SpecialDrawBox(x + start, y, x + end, y2, 0, GaugeS->c2, GaugeS->c2, Ctrl[r].state);
                // draw the third bar
                start = end + 1; if(v > tc) end = tc; else end = v;
                if(end > start) {
                    SpecialDrawBox(x + start, y, x + end, y2, 0, GaugeS->c3, GaugeS->c3, Ctrl[r].state);
                    // finally draw the fourth bar
                    start = end + 1; if(v > tc) end = v;
                    if(end > start) {
                        SpecialDrawBox(x + start, y, x + end, y2, 0, GaugeS->c4, GaugeS->c4, Ctrl[r].state);
                    }
                }
            }
        }
        // erase the remaining part of the bar
        if(end < len) SpecialDrawBox(x + end, y, x + len, y2, 0, Ctrl[r].bc, Ctrl[r].bc, Ctrl[r].state);
    }
}



char *KeypadCaption[12][2] = {  {"Alt", "123"},
                                {"0", "Can"},
                                {"Ent", "Ent"},
                                {"1", "."},
                                {"2", ""},
                                {"3", ""},
                                {"4", "+"},
                                {"5", "-"},
                                {"6", "E"},
                                {"7", "Del"},
                                {"8", "CE"},
                                {"9", ""}
                              };


char *KeyboardCaption[33][4] = {
                                // this is the bottom row of keys
                                {"&12", "ABC", "&12", "ABC"},
                                {"Z", "+" , "z", "["},
                                {"X", "-", "x" , "]"},
                                {"C", "=", "c", "{" },
                                {"V", "?", "v", "}" },
                                {"B", ":", "b", "|" },
                                {"N", "/", "n", "\\" },
                                {"M", ",", "m", "," },
                                {".", ".", ".", "." },
                                {"[ ]", "[ ]", "[ ]", "[ ]" },
                                {"Ent", "Ent", "Ent", "Ent" },

                                // this is the middle row
                                {"Can", "Can", "Can", "Can" },
                                {"A", "!", "a", "_" },
                                {"S", "@", "s", "/" },
                                {"D", "#", "d", "<" },
                                {"F", "$", "f", ">" },
                                {"G", "%", "g", ":" },
                                {"H", "&", "h", ";" },
                                {"J", "*", "j", "^" },
                                {"K", "(", "k", "'" },
                                {"L", ")", "l", "-" },
                                {" ", " ", " ", " " },

                                // and this is the top row
                                {"Q", "1", "q", "1" },
                                {"W", "2", "w", "2" },
                                {"E", "3", "e", "3" },
                                {"R", "4", "r", "4" },
                                {"T", "5", "t", "5" },
                                {"Y", "6", "y", "6" },
                                {"U", "7", "u", "7" },
                                {"I", "8", "i", "8" },
                                {"O", "9", "o", "9" },
                                {"P", "0", "p", "0" },
                                {"Del", "Del", "Del", "Del" }
                              };



char *GetCaption(int k, int is_alpha, int alt) {
    if(is_alpha)
        return KeyboardCaption[k][alt];
    else
        return KeypadCaption[k][alt];
}


// returns the top left coord of a key (x1 & y1) and the bottom right (x2 & y2)
// these must be passed as pointers to ints
// k is the key number
// is_alpha is true if keypad is the text keyboard
void GetSingleKeyCoord(int k, int is_alpha, int *x1, int *y1, int *x2, int *y2) {
    int horiznbr, width, height, keygap, BaseHoriz, BaseVert;

    if(is_alpha) {                                                  // if this is the keyboard
        horiznbr = 11;                                              // number of horizontal buttons
        if(HRes == 800) {
            width = 69;
            height = 77;
            keygap = 4;
        } else if(HRes == 480) {
            width = 40;
            height = 41;
            keygap = 4;
        } else {
            keygap = 2;
            width = (HRes - keygap*10)/11;
            height = ((VRes/2) - keygap*2) / 3;
        }
        BaseVert = (Ctrl[InvokingCtrl].y2 > VRes/2) ? (height * 3) + (keygap * 3) : VRes + keygap;  // what half of the screen?
        BaseHoriz = HRes + keygap/2;
    } else {                                                        // if this is the number pad
        horiznbr = 3;                                               // number of horizontal buttons
        keygap = 5;
        height = VRes < HRes ? VRes : HRes;                         // find the smallest resolution
        height = width = ((height - keygap*3)/4 < 80) ? (height - keygap*3)/4 : 80;               // scale the button size to the smallest resolution
        if(Ctrl[InvokingCtrl].y1 + height/2 > VRes/2) {
            BaseVert = (width * 4) + (keygap * 4);                  // should it be in the top part of the screen?
        } else {
            BaseVert = VRes + keygap;                               // or in the bottom
        }
        if(Ctrl[InvokingCtrl].x1 + width/2 > HRes/2) {
            BaseHoriz = (width * 3) + (keygap * 3);
        } else {
            BaseHoriz = HRes + keygap;
        }
    }

    *x1 = BaseHoriz - (horiznbr - (k % horiznbr)) * (width + keygap);
    *y1 = BaseVert - ((k / horiznbr) + 1) * (height + keygap);
    *x2 = *x1 + width;
    *y2 = *y1 + height;
}


void DrawDisplayBox(int r) {
    int fc, bc;
    if(Ctrl[r].state & CTRL_SELECTED) {
        fc = Ctrl[r].bc;
        bc = Ctrl[r].fc;
    } else {
        fc = Ctrl[r].fc;
        bc = Ctrl[r].bc;
    }
    SpecialDrawRBox(Ctrl[r].x1, Ctrl[r].y1, Ctrl[r].x2, Ctrl[r].y2, 10,  fc, bc, Ctrl[r].state);
    if(strlen((char *)Ctrl[r].s) > 2 && Ctrl[r].s[0] == '#' && Ctrl[r].s[1] == '#')
        SpecialPrintString(Ctrl[r].x1 + (Ctrl[r].x2 - Ctrl[r].x1)/2, Ctrl[r].y1 + (Ctrl[r].y2 - Ctrl[r].y1)/2, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, ChangeBright(fc, BTN_DISABLED), bc, (char *)Ctrl[r].s + 2, Ctrl[r].state);
    else
        SpecialPrintString(Ctrl[r].x1 + (Ctrl[r].x2 - Ctrl[r].x1)/2, Ctrl[r].y1 + (Ctrl[r].y2 - Ctrl[r].y1)/2, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, fc, bc, (char *)Ctrl[r].s , Ctrl[r].state);
}


// this will set all controls (except the control r) to disabled or non disabled
// note: r must always be a reference to the text box (never the keypad)
void PopUpRedrawAll(int r, int disabled) {
    int i;
    for(i = 1; i < Option.MaxCtrls; i++) {                          // find all the other controls and set to disabled
        if(Ctrl[i].type && i != r) {
            if(disabled)
                Ctrl[i].state |= CTRL_DISABLED2;                    // set it to disable
            else
                Ctrl[i].state &= ~CTRL_DISABLED2;                   // set it to not disabled
            UpdateControl(i);
        }
    }
}



void KeyPadErase(int is_alpha) {
    int x1, y1, x2, y2;
    int j1, j2;

    GetSingleKeyCoord(is_alpha ? 22:9, is_alpha, &x1, &y1, &j1, &j2);// get the top left coordinate of the key that is top left
    GetSingleKeyCoord(is_alpha ? 10:2, is_alpha, &j1, &j2, &x2, &y2);// get the bottom right coordinate of the key which is bottom right
    DrawBox(x1, y1, x2, y2, 0, 0, gui_bcolour);                     // erase the whole keyboard

    GUIKeyDown = -1;
}


void DrawSingleKey(int is_alpha, int x1, int y1, int x2, int y2, char *s, int fc, int bc) {
    int fnt, i;
    fnt = gui_font;
    if(is_alpha) {
        if(HRes > 400)
            SetFont(0x31);                                          // set a readable font
        else if(HRes > 400)
            SetFont(0x11);
        else
             SetFont(0x01);
    } else {
        if(VRes > 240)
            SetFont(0x31);                                          // set a readable font
        else if(VRes > 160)
            SetFont(0x11);
        else
             SetFont(0x01);
    }
    SpecialDrawRBox(x1, y1, x2, y2, 10,  fc, bc, 0);     // the border
    SpecialPrintString(x1 + (x2 - x1)/2, y1 + (y2 - y1)/2, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, fc, bc, s, 0);
    if(is_alpha && *s == ' ') {                                                 // a space means that we should draw the shift symbol inside the button
        for(i = 0; i < ((x2 - x1) / 3); i++)
            SpecialDrawLine(x1 + (x2 - x1)/2 - i/2, y1 + i + (y2 - y1)/3, x1 + (x2 - x1)/2 + i/2, y1 + i + (y2 - y1)/3, 1, fc, 0);
    }
    SetFont(fnt);
}


// invoke the callback routine so that the programmer has the option of manipulating the result
//void DoCallback(int InvokingCtrl, char *key) {
//    char callstr[32];
//    char *nextstmtSaved = nextstmt;
//    if(!InPause && FindSubFun("MM.KEYPRESS", 0) >= 0) {
//        strcpy(callstr, "MM.KEYPRESS"); IntToStrPad(callstr + strlen(callstr), InvokingCtrl, ' ', 4, 10);
//        strcat(callstr, ", \""); strcat(callstr, key); strcat(callstr, "\"");
//        callstr[strlen(callstr)+1] = 0;                             // two NULL chars required to terminate the call
//        g_LocalIndex++;
//        InCallback = true;
//        ExecuteProgram(callstr);
//        InCallback = false;
//        g_LocalIndex--;
//        g_TempMemoryIsChanged = true;                                 // signal that temporary memory should be checked
//        nextstmt = nextstmtSaved;
//    }
//}


void DrawKeyboard(int mode) {
    int x1, y1, x2, y2;
    int is_alpha, nbr_buttons;
    int i;
    unsigned char *p;

    if(Ctrl[InvokingCtrl].type == CTRL_TEXTBOX) {
        nbr_buttons = 33;
        is_alpha = true;
    } else {
        nbr_buttons = 12;
        is_alpha = false;
    }

    if(mode == KEY_OPEN) {
//        DoCallback(InvokingCtrl, "Open");
        KeyPadErase(Ctrl[InvokingCtrl].type == CTRL_TEXTBOX);       // erase the background
        mode = KEY_DRAW_ALL;
    }

    // special processing for KEY_KEY_UP when the numeric keypad is in alt mode
    // this switches back to number mode for everything except for backspace when there are still chars in the buffer
    if(mode == KEY_KEY_UP && !is_alpha && KeyAltShift && GUIKeyDown > 0 && (GUIKeyDown != 9 || *Ctrl[InvokingCtrl].s == 0)) {
        KeyAltShift = false;                                        // redraw all keys in number mode
        mode = KEY_DRAW_ALL;                                        // drop thru to redraw all
    }

    if(mode == KEY_DRAW_ALL) {
        // just draw all the buttons
        for(i = 0; i < nbr_buttons; i++) {
            GetSingleKeyCoord(i, is_alpha, &x1, &y1, &x2, &y2);
            DrawSingleKey(is_alpha, x1, y1, x2, y2, GetCaption(i, is_alpha, KeyAltShift), Ctrl[InvokingCtrl].fc, Ctrl[InvokingCtrl].bc);
        }
        GUIKeyDown = -1;                                               // no key to be down
        return;
    }

    // if a key has been released simply redraw it in normal mode and return immediately
    if(mode == KEY_KEY_UP) {
        if(GUIKeyDown >= 0) {
            GetSingleKeyCoord(GUIKeyDown, is_alpha, &x1, &y1, &x2, &y2);
            DrawSingleKey(is_alpha, x1, y1, x2, y2, GetCaption(GUIKeyDown, is_alpha, KeyAltShift), Ctrl[InvokingCtrl].fc, Ctrl[InvokingCtrl].bc);
        }
        GUIKeyDown = -1;
        return;
    }

    if(mode != KEY_KEY_CANCEL) {
        for(i = 0; i < nbr_buttons; i++) {                                  // search through the keys
            GetSingleKeyCoord(i, is_alpha, &x1, &y1, &x2, &y2);             // find the coordinates for this key
            if(TouchX > x1 && TouchX < x2 && TouchY > y1 && TouchY < y2) {  // if the touch is on this key
                if(!is_alpha && KeyAltShift) {                              // if this is the number keypad
                    // this ignores the invalid keys on the number pad when in the alt mode
                    switch(i) {
                        case 4:
                        case 5:
                        case 11: return;
                        case 3:  if(*Ctrl[InvokingCtrl].s == 0) break; else if(strchr((char *)Ctrl[InvokingCtrl].s, '.') != NULL) return; break;
                        case 8:  if(*Ctrl[InvokingCtrl].s == 0) break; else if(strchr((char *)Ctrl[InvokingCtrl].s, 'E') != NULL) return; break;
                        case 9:
                        case 10: if(*Ctrl[InvokingCtrl].s == 0) return;
                    }
                }
                GUIKeyDown = i;                                        // record the key that has been touched
                ClickTimer += CLICK_DURATION;                       // make a click
                DrawSingleKey(is_alpha, x1, y1, x2, y2, GetCaption(i, is_alpha, KeyAltShift), Ctrl[InvokingCtrl].bc, Ctrl[InvokingCtrl].fc);
            }
        }

        if(GUIKeyDown == -1) return;

        if(GUIKeyDown == 0) {                                          // this is the alt key
            KeyAltShift = !(KeyAltShift & 1);                       // toggle alt/text bit, also resets shift to no shift
            DrawKeyboard(KEY_DRAW_ALL);                             // redraw the keyboard/keypad
            return;
        }

        if(GUIKeyDown == 21) {                                                         // if this is the shift key on a textbox
            KeyAltShift = ((!(KeyAltShift & 0b10) << 1) | (KeyAltShift & 0b01));    // toggle the shift key
            DrawKeyboard(KEY_DRAW_ALL);                                             // redraw the keyboard/keypad
            return;
        }
    }

    if(mode == KEY_KEY_CANCEL || (is_alpha && GUIKeyDown == 11) || (!is_alpha && KeyAltShift && GUIKeyDown == 1)) {        // the user has hit the cancel key
        strcpy((char *)Ctrl[InvokingCtrl].s, CancelValue);                  // restore the current value
        GUIKeyDown = (is_alpha ? 10 : 2);                              // fake the Enter key
    }

    if(GUIKeyDown == (is_alpha ? 10 : 2)) {                            // this is the enter key
//        DoCallback(InvokingCtrl, GetCaption(GUIKeyDown, is_alpha, KeyAltShift));
        if(!is_alpha) {
            int t = STR_FLOAT_PRECISION;
            // get the number of digits that have been entered after the decimal point and use that to determine the precision of the display
            if(strchr((char *)Ctrl[InvokingCtrl].s, 'e') == NULL && strchr((char *)Ctrl[InvokingCtrl].s, 'E') == NULL && strchr((char *)Ctrl[InvokingCtrl].s, '.') != NULL) {
                t = strlen(strchr((char *)Ctrl[InvokingCtrl].s, '.') + 1);
                if(t < 1 || t > 16) t = STR_FLOAT_PRECISION;
            }
            FloatToStr((char *)Ctrl[InvokingCtrl].s, Ctrl[InvokingCtrl].value = atof((char *)Ctrl[InvokingCtrl].s), 1, t, ' ');     // convert to a float and put back on the display
        }
        KeyPadErase(is_alpha);                                      // hide the keypad
        Ctrl[InvokingCtrl].state &= ~CTRL_SELECTED;                 // deselect the text box
        PopUpRedrawAll(0, false);                                   // redraw all the controls
        LastRef = InvokingCtrl;
        InvokingCtrl = 0;
        return;
    }

    p = Ctrl[InvokingCtrl].s + strlen((char *)Ctrl[InvokingCtrl].s);
    if(!is_alpha) {
        // this is the number pad
        if(KeyAltShift) {
            // the keypad is in the alt mode
            // process the special keys
            switch(GUIKeyDown) {
                case 3:  *p++ = '.'; break;
                case 6:  *p++ = '+'; break;
                case 7:  *p++ = '-'; break;
                case 8:  *p++ = 'E'; break;
                case 9:  *(--p) = 0; break;
                case 10: *Ctrl[InvokingCtrl].s = 0; break;
                default: return;
            }
        } else {
            *p++ = *GetCaption(GUIKeyDown, is_alpha, KeyAltShift);     // otherwise use the caption
        }
    } else {
        // this is the text keyboard
        if(GUIKeyDown == 32) {                                         // is this the delete key
            if(strlen((char *)Ctrl[InvokingCtrl].s)) *(--p) = 0;            // delete the char
        } else if(GUIKeyDown == 9) {                                   // if it is space
            *p++ = ' ';                                             // insert a space
        } else {
            *p++ = *GetCaption(GUIKeyDown, is_alpha, KeyAltShift);     // otherwise use the caption
        }
    }
    *p = 0;

//    DoCallback(InvokingCtrl, GetCaption(GUIKeyDown, is_alpha, KeyAltShift));
    DrawControl(InvokingCtrl);
}


char *FmtKeyCaption[12] = { "<<<", "0", "Can", "1", "2", "3", "4", "5", "6", "7", "8", "9" } ;
char FmtKeyEnabled[12];


void DrawFmtKeypad(char x, char prev, int GUIKeyDown) {
    int i, dimfc, x1, y1, x2, y2;
//    dp("1"); uSec(10000);

    for(i = 0; i < 12; i++) FmtKeyEnabled[i] = false;
    FmtKeyEnabled[0] = FmtKeyEnabled[2] = true;
    FmtKeyCaption[7] = "5"; FmtKeyCaption[10] = "8";

    if(!isdigit(x)) {
        switch(x) {
            case '+': FmtKeyCaption[10] = "+";  FmtKeyCaption[7] = "-"; x = 0; break;
            case 'A': FmtKeyCaption[10] = "AM"; FmtKeyCaption[7] = "PM"; x = 0; break;
            case 'N': FmtKeyCaption[10] = "N";  FmtKeyCaption[7] = "S"; x = 0; break;
            case 'E': FmtKeyCaption[10] = "E";  FmtKeyCaption[7] = "W"; x = 0; break;
            case 'M': if(prev == '0') {
                          x = '9';
                      } else {
                          x = '2';
                          FmtKeyEnabled[1] = true;
                      }
                      break;
            case 'D': if(prev == '0') {
                          x = '9';
                      } else {
                          FmtKeyEnabled[1] = true;
                          if(prev == '3')
                              x = '1';
                          else
                              x = '9';

                      }
                      break;
            case 'H': FmtKeyEnabled[1] = true;
                      if(prev == '0' || prev == '1')
                          x = '9';
                      else
                          x = '3';
                      break;
            case 'L': FmtKeyEnabled[1] = true;
                      if(prev == '0')
                          x = '9';
                      else
                          x = '7';
                      break;
        }
        if(x == 0)
            FmtKeyEnabled[10] = FmtKeyEnabled[7] = true;
        else
            for(i = 3 ; i < x - '0' + 3 ; i++)
                FmtKeyEnabled[i] = true;
    } else {
        FmtKeyEnabled[1] = true;
        for(i = 3 ; i < x - '0' + 3 ; i++)
            FmtKeyEnabled[i] = true;
    }

//    dp("1"); uSec(10000);
    dimfc = ChangeBright(Ctrl[InvokingCtrl].fc, BTN_DISABLED);
    for(i = 0; i < 12; i++){
//    dp("2"); uSec(10000);
        GetSingleKeyCoord(i, false, &x1, &y1, &x2, &y2);
//    dp("3"); uSec(10000);
        if(i == GUIKeyDown)
            DrawSingleKey(false, x1, y1, x2, y2, FmtKeyCaption[i], Ctrl[InvokingCtrl].bc, Ctrl[InvokingCtrl].fc);
        else
            DrawSingleKey(false, x1, y1, x2, y2, FmtKeyCaption[i], FmtKeyEnabled[i] ? Ctrl[InvokingCtrl].fc:dimfc, Ctrl[InvokingCtrl].bc);
    }
}


void DrawFmtControl(int r, char *pfmt) {
    unsigned char *p;
    p = Ctrl[r].s + strlen((char *)Ctrl[r].s);
    if(*pfmt) *p++ = 0xff;                                                    // marker to change the following text brightness to dim
    while(*pfmt) {
        if(*pfmt == '(') {
            pfmt++;
            while(*pfmt != ')') *p++ = *pfmt++;
            pfmt++;
        } else {
            if(*(pfmt + 1) == 0)
                *p++ = *pfmt++;
            else {
                *p++ = *(pfmt + 1);
                pfmt += 2;
            }
        }
    }
    *p = 0;
    DrawControl(r);
}


void DrawFmtBox(int mode) {
    int i, x1, y1, x2, y2;
    static unsigned char *pfmt, *p;

    if(mode == KEY_OPEN) {
        KeyPadErase(false);                                         // erase the background
        pfmt = Ctrl[InvokingCtrl].fmt;
        p = Ctrl[InvokingCtrl].s;
        DrawFmtKeypad(*pfmt, 0, 99);                                // just draw all the buttons
        DrawFmtControl(InvokingCtrl, (char *)pfmt);
        GUIKeyDown = -1;                                               // no key to be down
        return;
    }

    if(mode == KEY_KEY_UP) {
        if(GUIKeyDown < 0) return;
        DrawFmtKeypad(*pfmt, *(p - 1), 99);                         // just draw all the buttons to show that the key is up
        return;
    }

    if(mode == KEY_KEY_DWN) {
        // if we are here we have the simple case of a key down
        for(i = 0; i < 12; i++) {                                           // search through the keys
            GetSingleKeyCoord(i, false, &x1, &y1, &x2, &y2);                // find the coordinates for this key
            if(TouchX > x1 && TouchX < x2 && TouchY > y1 && TouchY < y2) {  // if the touch is on this key
                // get the key and do something with it
                if(FmtKeyEnabled[i] == false || (i == 0 && pfmt == Ctrl[InvokingCtrl].fmt)) return;                      // do nothing if key is disabled
                DrawFmtKeypad((char)*pfmt, *(p - 1), i);                      // show that the key is down
                ClickTimer += CLICK_DURATION;  // make a click

                if(i == 0) {
                    // it is the backspace key
                    if(*(pfmt - 1) == ')') {
                        pfmt -= 2;
                        while(*pfmt-- != '(') p--;
                        pfmt--; p--;
                    } else {
                        pfmt -= 2;
                        p--;
                    }
                    *p = 0;
                } else if(i == 2) {
                    mode = KEY_KEY_CANCEL;
                    break;
                } else {
                    // this is an ordinary key (eg, 0 to 9)
                    p += strlen(strcpy((char *)p, FmtKeyCaption[i]));          // add they key caption to the display box string
                    pfmt++;
                    if(*pfmt) pfmt++;
                    if(*pfmt) {
                        if(*pfmt == '(') {
                            pfmt++;
                            while(*pfmt && *pfmt != ')') *p++ = *pfmt++;
                            *p = 0;
                            if(*pfmt) pfmt++;
                        }
                    }
                    if(*pfmt == 0) break;                           // *pfmt == 0 means that we have reached the end of the format string
                }
                GUIKeyDown = i;
                DrawFmtControl(InvokingCtrl, (char *)pfmt);
                return;
            }
        }
    }


    if(*pfmt == 0 || mode == KEY_KEY_CANCEL) {
        if(mode == KEY_KEY_CANCEL) strcpy((char *)Ctrl[InvokingCtrl].s, CancelValue);       // if we are cancelling first restore the current value
        KeyPadErase(false);                                         // hide the keypad
        DrawControl(InvokingCtrl);
        Ctrl[InvokingCtrl].state &= ~CTRL_SELECTED;                 // deselect the text box
        PopUpRedrawAll(0, false);                                   // redraw all the controls
        LastRef = InvokingCtrl;
        InvokingCtrl = 0;
        return;
    }
}


// draw a control on the screen
void DrawControl(int r) {
    int fnt;
    fnt = gui_font;
    SetFont(Ctrl[r].font);
    switch(Ctrl[r].type) {
        case CTRL_BUTTON:       DrawButton(r);   break;
        case CTRL_SWITCH:       DrawSwitch(r);   break;
        case CTRL_CHECKBOX:     DrawCheckBox(r); break;
        case CTRL_RADIOBTN:     DrawRadioBtn(r); break;
        case CTRL_LED:          DrawLED(r);      break;
        case CTRL_SPINNER:      DrawSpinner(r);  break;
        case CTRL_FRAME:        DrawFrame(r);    break;
        case CTRL_TEXTBOX:
        case CTRL_FMTBOX:
        case CTRL_NBRBOX:
        case CTRL_DISPLAYBOX:   DrawDisplayBox(r);  break;
        case CTRL_CAPTION:      DrawCaption(r);  break;
        case CTRL_GAUGE:        DrawGauge(r);    break;
        case CTRL_BARGAUGE:     DrawBarGauge(r); break;
        default:                break;
    }
    SetFont(fnt);
}


// similar to DrawControl() but it will only redraw the control if it is in the current
// set of pages for display and if it is not hidden
void UpdateControl(int r) {
    if(Ctrl[r].type && (CurrentPages & (1 << Ctrl[r].page)) && !(Ctrl[r].state & CTRL_HIDDEN)) DrawControl(r);
}


// check if the pen has touched or been lifted and animate the GUI elements as required
// this is called after every command (from check_interrupt()), in the getchar() loop and repeatedly in a pause
// TouchDown and TouchUp are set in the Timer 4 interrupt
void ProcessTouch(void) {
    static int repeat = 0;
    static int waiting = false;
    int r, spinup;
//    if(!Option.MaxCtrls || !TOUCH_GETIRQTRIS)return;
    if(repeat) {
        if(TOUCH_DOWN)
            if(TouchTimer < repeat)
                return;
            else 
                TouchDown = true;
        else
            repeat = 0;
    } else {
        if(waiting) {
            if(TouchTimer < 50)                                     // wait 50mS before a processing touch (allows the touch to settle)
                return;
            else
                waiting = false;
        } else {
            waiting = true;
            TouchTimer = 0;                                         // TouchTimer is incremented every mS in the Timer 4 interrupt
        }
    }

    if(!(TouchUp || TouchDown)) return;                             // quick exit if there is nothing to do
    if(TouchDown) {
        // touch has just occurred
        TouchX = GetTouch(GET_X_AXIS);
        TouchY = GetTouch(GET_Y_AXIS);
        LastRef = CurrentRef = 0;
        TouchUp = TouchDown = false;
        if(TouchX == TOUCH_ERROR) return;                           // abort if the pen was lifted
        if(InvokingCtrl) {                                          // the keyboard/keypad takes complete control when activated
            if(Ctrl[InvokingCtrl].type == CTRL_FMTBOX)
                DrawFmtBox(KEY_KEY_DWN);
            else
                DrawKeyboard(KEY_KEY_DWN);
            gui_int_down = false;
            return;
        }

        gui_int_down = true;                                        // signal that a MMBasic interrupt is valid
        for(r = 1; r < Option.MaxCtrls; r++) {
            if(Ctrl[r].type && TouchX >= Ctrl[r].x1 && TouchY >= Ctrl[r].y1 && TouchX <= Ctrl[r].x2 && TouchY <= Ctrl[r].y2) {
                if(!(CurrentPages & (1 << Ctrl[r].page))) continue;                            // ignore if the page is not displayed
                if(Ctrl[r].state & (CTRL_DISABLED | CTRL_DISABLED2 | CTRL_HIDDEN)) continue;   // ignore if control is disabled
                switch(Ctrl[r].type) {
                    case CTRL_SWITCH:   if(!(TouchX >= Ctrl[r].min && TouchX <= Ctrl[r].max)) return;   // skip if it is not the touch sensitive area (depends on the switch state)
                                        Ctrl[r].value = !Ctrl[r].value;
                                        break;
                    
                    case CTRL_BUTTON:
                    case CTRL_RADIOBTN:;
                    case CTRL_AREA:     Ctrl[r].value = 1;
                                        break;

                    case CTRL_FRAME:    continue;                   // a frame does not respond to touch but it might contain controls that do

                    case CTRL_CAPTION:  continue;                   // a caption does not respond to touch but it might be overlayed by an AREA control

                    case CTRL_DISPLAYBOX:
                    case CTRL_LED:      CurrentRef = r;
                                        return;                     // these controls do not respond to touch

                    case CTRL_SPINNER:  if(TouchX < (Ctrl[r].x1 + (Ctrl[r].y2 - Ctrl[r].y1)))               // if it is in the left side
                                                spinup = false;                                             // it is spin down
                                         else if(TouchX > (Ctrl[r].x2 - (Ctrl[r].y2 - Ctrl[r].y1)))         // if it is in the right side
                                                spinup = true;                                              // it is spin up
                                        else
                                            return;
                                        if(spinup) {
                                            if(Ctrl[r].state & CTRL_SPINDOWN) return;
                                            if(Ctrl[r].value + Ctrl[r].inc > Ctrl[r].max) { repeat = 0; break; }
                                            Ctrl[r].state |= CTRL_SPINUP;
                                            Ctrl[r].value += Ctrl[r].inc;
                                        } else {
                                            if(Ctrl[r].state & CTRL_SPINUP) return;
                                            if(Ctrl[r].value - Ctrl[r].inc < Ctrl[r].min) { repeat = 0; break; }
                                            Ctrl[r].state |= CTRL_SPINDOWN;
                                            Ctrl[r].value -= Ctrl[r].inc;
                                        }
                                        if(Ctrl[r].value < Ctrl[r].inc &&  Ctrl[r].value > -Ctrl[r].inc) Ctrl[r].value = 0;
                                        if(repeat == 0)
                                            repeat = 500;
                                        else
                                            repeat = 100;
                                        TouchTimer = 0;
                                        break;

                    case CTRL_NBRBOX:
                                        FloatToStr((char *)Ctrl[r].s, Ctrl[r].value, 0, STR_AUTO_PRECISION, ' ');   // set the string value to be saved
                                        // fall thru

                    case CTRL_TEXTBOX:  strcpy(CancelValue, (char *)Ctrl[r].s);                                     // save the current value in case the user cancels
                                        *Ctrl[r].s = 0;                                                     // set it to an empty string
                                        Ctrl[r].state |= CTRL_SELECTED;                                     // select the number text/box
                                        PopUpRedrawAll(r, true);
                                        CurrentRef = InvokingCtrl = r;                                      // tell the keypad what text/number box it is servicing
                                        GUIKeyDown = -1;
                                        KeyAltShift = 0;
                                        DrawControl(r);
                                        ClickTimer += CLICK_DURATION;
                                        if(GuiIntDownVector == NULL)
                                            DrawKeyboard(KEY_OPEN);                                          // initial draw of the keypad
                                        else
                                            DelayedDrawKeyboard = true;  
                                        return;

                    case CTRL_FMTBOX:   strcpy(CancelValue, (char *)Ctrl[r].s);                                     // save the current value in case the user cancels
                                        *Ctrl[r].s = 0;                                                     // set it to an empty string
                                        Ctrl[r].state |= CTRL_SELECTED;                                     // select the number text/box
                                        PopUpRedrawAll(r, true);
                                        CurrentRef = InvokingCtrl = r;                                      // save the format box being serviced
                                        GUIKeyDown = -1;
                                        ClickTimer += CLICK_DURATION;
                                        if(GuiIntDownVector == NULL)
                                            DrawFmtBox(KEY_OPEN);                                           // initial draw of the keypad
                                        else
                                            DelayedDrawFmtBox = true;                                       // leave it until after the keyboard interrupt
                                        return;

                    default:            Ctrl[r].value = !Ctrl[r].value;
                                        break;
                }
                CurrentRef = r;
                DrawControl(r);
                ClickTimer += CLICK_DURATION;                       // sound a "click"
                return;
            }
        }
    }

    if(TouchUp) {
        // the touch has just been released
        TouchUp = TouchDown = false;
        LastX=TouchX;
        LastY=TouchY;
        if(InvokingCtrl) {                                          // the keyboard/keypad takes complete control when activated
            if(Ctrl[InvokingCtrl].type == CTRL_FMTBOX)
                DrawFmtBox(KEY_KEY_UP);
            else
                DrawKeyboard(KEY_KEY_UP);
            gui_int_down = false;
            return;
        }

        gui_int_up = true;
        if(CurrentRef) {
            if(Ctrl[CurrentRef].type) {
                if((CurrentPages & (1 << Ctrl[CurrentRef].page)) && !(Ctrl[CurrentRef].state & (CTRL_DISABLED | CTRL_DISABLED2 | CTRL_HIDDEN))) {   // ignore if control is disabled or the page is not displayed
                    if(CurrentRef) LastRef = CurrentRef;
                    switch(Ctrl[CurrentRef].type) {
                        case CTRL_AREA:
                        case CTRL_BUTTON:   Ctrl[CurrentRef].value = 0;
                                            DrawControl(CurrentRef);
                                            break;

                        case CTRL_SPINNER:  Ctrl[CurrentRef].state &= ~(CTRL_SPINUP | CTRL_SPINDOWN);
                                            DrawControl(CurrentRef);
                                            break;

                        default:            break;
                    }
                }
            }
        }
        CurrentRef = 0;
    }
}



void SetCtrlState(int r, int state, int err) {
    if(Ctrl[r].type == 0) {
        if(err) error("Control #% does not exist", r);
    } else {
        if(r == CurrentRef) {
            if(Ctrl[r].type == CTRL_BUTTON) {
                Ctrl[r].value = 0;
                UpdateControl(r);
            }
            if(Ctrl[r].type == CTRL_SPINNER) {
                Ctrl[r].state &= ~(CTRL_SPINUP | CTRL_SPINDOWN);
                UpdateControl(r);
            }
            LastRef = CurrentRef = 0;
            gui_int_down = gui_int_up = false;
        }
        Ctrl[r].state |= state;
        if(state & CTRL_HIDDEN)
            DrawControl(r);
        else
            UpdateControl(r);
    }
}



// this function is used by the CLS command
void HideAllControls(void) {
    int r;
    for(r = 1; r < Option.MaxCtrls; r++)
        if((CurrentPages & (1 << Ctrl[r].page)) && !(Ctrl[r].state & CTRL_HIDDEN))
            SetCtrlState(r, CTRL_HIDDEN, false);
}


// This will check if an interrupt is pending and will service if yes.
// Essentially this runs a short program which does nothing but it gives the
// interrupt checking routines in ExecuteProgram() a chance to do their job.
// This routine should be called repeatedly during long delays.
void ServiceInterrupts(void) {
    char *ttp, *s, tcmdtoken;
    char p[4]={0};

    CheckAbort();
    g_LocalIndex++;                                                   // preserve the current temporary string memory allocations
    ttp = (char *)nextstmt;                                                 // save the globals used by commands
    tcmdtoken = cmdtoken;
    s = (char *)cmdline;

    p[0] = (cmdEND_IF & 0x7f ) + C_BASETOKEN;
    p[1] = (cmdEND_IF >> 7) + C_BASETOKEN; //tokens can be 14-bit
    ExecuteProgram((unsigned char *)p);                                              // execute the program's code

    cmdline = (unsigned char *)s;                                                    // restore the globals
    cmdtoken = tcmdtoken;
    nextstmt = (unsigned char *)ttp;
    g_LocalIndex--;
    g_TempMemoryIsChanged = true;                                     // signal that temporary memory should be checked
}

/*  @endcond */

void fun_msgbox(void) {
    int i, j, k;
    int x, y, x1, y1, x2, y2, msgnbr, btnnbr, btnwidth;
    char *p, *msg;
    char *btn;
    int btnx1[4], btny1, btnx2[4], btny2;
    long long int timeout;
    if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
    getargs(&ep, 9, (unsigned char *)",");
    if(argc < 3) error("Argument count");
    msg = GetMemory(MAX_CAPTION_LINES * MAXSTRLEN);
    btn = GetMemory(4 * MAXSTRLEN);

    p = (char *)getCstring(argv[0]);
    i = msgnbr = 0;
    while(*p) {
        if(*p == '~') {
            if(msgnbr + 1 >= MAX_CAPTION_LINES) break;
            msg[msgnbr++ * MAXSTRLEN + i] = 0;
            i = 0; p++;
        } else {
            msg[msgnbr * MAXSTRLEN + i++] = *p++;
        }
    }
    msg[msgnbr++ * MAXSTRLEN + i]= 0;

    btnnbr = ((argc - 2) / 2) + 1;
    for(i = 0; i < btnnbr; i++)
        strcpy(&btn[i * MAXSTRLEN], (char *)getCstring(argv[(i * 2) + 2]));

    for(j = i = 0; i < msgnbr; i++)
        if(strlen(&msg[i * MAXSTRLEN]) > j) j = strlen(&msg[i * MAXSTRLEN]);
    j *= gui_font_width;

    for(k = i = 0; i < btnnbr; i++)
        if(strlen(&btn[i * MAXSTRLEN]) > k) k = strlen(&btn[i * MAXSTRLEN]);
    btnwidth = (k + 2) * gui_font_width;
    k = (btnwidth * btnnbr) + ((btnnbr - 1) * 2 * gui_font_width);
    if(k > j) j = k;
    j += 4 * gui_font_width;

    i = (((5 + msgnbr) * gui_font_height) / 2) + gui_font_height / 4;
    x1 = HRes/2 - j/2; y1 = VRes/2 - i; x2 = HRes/2 + j/2; y2 = VRes/2 + i;

    // wait for any pen touch to be lifted
    timeout = mSecTimer + 10000;
    do { ServiceInterrupts();
    } while(TouchState && mSecTimer < timeout) ;

    PopUpRedrawAll(0, true);
    SpecialDrawRBox(x1, y1, x2, y2, 25, gui_fcolour, gui_bcolour, CTRL_NORMAL);

    for(i = 0; i < msgnbr; i++)
      SpecialPrintString(HRes/2, y1 + gui_font_height + (gui_font_height * i), JUSTIFY_CENTER, JUSTIFY_TOP, ORIENT_NORMAL, gui_fcolour, gui_bcolour, &msg[i * MAXSTRLEN], CTRL_NORMAL);

    btny1 = y2 - (gui_font_height * 3);
    btny2 = y2 - gui_font_height;
    for(i = 0; i < btnnbr; i++) {
        btnx1[i] = HRes/2 - (k/2) + (btnwidth * i) + (i * 2 * gui_font_width);  btnx2[i] = btnx1[i] + btnwidth;
        DrawBasicButton(btnx1[i], btny1, btnx2[i], btny2, BTN_SIDE_WIDTH, 1, gui_fcolour, CTRL_NORMAL);
        SpecialPrintString(btnx1[i] + btnwidth/2, btny1 + gui_font_height, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, gui_bcolour, gui_fcolour, &btn[i * MAXSTRLEN], CTRL_NORMAL);
    }

    while(1) {
        ServiceInterrupts();
        x = GetTouch(GET_X_AXIS);  y = GetTouch(GET_Y_AXIS);
        for(i = 0; i < btnnbr; i++)
            if(x >= btnx1[i] && x <= btnx2[i] && y >= btny1 && y <= btny2)
                break;
        if(i < btnnbr) {
            DrawBasicButton(btnx1[i], btny1, btnx2[i], btny2, BTN_SIDE_WIDTH, 0, gui_fcolour, CTRL_NORMAL);
            SpecialPrintString(btnx1[i] + btnwidth/2 + BTN_CAPTION_SHIFT, btny1 + gui_font_height + BTN_CAPTION_SHIFT, JUSTIFY_CENTER, JUSTIFY_MIDDLE, ORIENT_NORMAL, gui_bcolour, gui_fcolour, &btn[i * MAXSTRLEN], CTRL_NORMAL);
            ClickTimer += CLICK_DURATION;                        // sound a "click"
            while(GetTouch(GET_X_AXIS) != TOUCH_ERROR) ServiceInterrupts();
            SpecialDrawRBox(x1, y1, x2, y2, 25, gui_bcolour, gui_bcolour, CTRL_NORMAL);
            PopUpRedrawAll(0, false);
            iret = i + 1;
            targ = T_INT;
            FreeMemory((unsigned char *)msg);  FreeMemory((unsigned char *)btn);
            return;
        }
    }
}


void fun_ctrlval(void) {
    int r;
    if(!Option.DISPLAY_TYPE) error("Display not configured");
    if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
    if(*ep == '#') ep++;
    r = getint(ep, 1, Option.MaxCtrls);
    if(Ctrl[r].type == 0) error("Control #% does not exist", r);
    if(Ctrl[r].type == CTRL_NBRBOX) {
        if(r == InvokingCtrl){                                       // is the keypad for the number box being displayed?
                fret = (MMFLOAT)atof(CancelValue);                           // if NOT in the call back just return the value saved in case of a cancel
        }
        else {
            char a[16];
            FloatToStr(a, Ctrl[r].value, 1, STR_FLOAT_PRECISION, ' ');     // convert to a float and put back on the display
            fret = atof(a);
            }
        targ = T_NBR; 
    } else if(Ctrl[r].type == CTRL_TEXTBOX || Ctrl[r].type == CTRL_FMTBOX) {
        sret = GetTempMemory(STRINGSIZE);                                  // this will last for the life of the command
        if(r == InvokingCtrl)                                       // is the keypad for the number box being displayed?
                strcpy((char *)sret, CancelValue);                          // just return the value saved in case of a cancel
          else
            strcpy((char *)sret, (char *)Ctrl[r].s);
        if(Ctrl[r].s[0] == '#' && Ctrl[r].s[1] == '#') *sret = 0;   // return a zero length string if it is just "ghost text"
        CtoM(sret);                                                 // convert to a MMBasic string
        targ = T_STR;
    } else if(Ctrl[r].type == CTRL_DISPLAYBOX || Ctrl[r].type == CTRL_CAPTION) {
        sret = GetTempMemory(STRINGSIZE);                                  // this will last for the life of the command
        strcpy((char *)sret, (char *)Ctrl[r].s);                                    // copy the string
        CtoM(sret);                                                 // convert to a MMBasic string
        targ = T_STR;
    } else {
        char a[16];
        FloatToStr(a, Ctrl[r].value, 1, STR_FLOAT_PRECISION, ' ');     // convert to a float and put back on the display
        fret = atof(a);
        targ = T_NBR;
    }
}


void cmd_ctrlval(void) {
    int r;
    MMFLOAT v;
    if(HRes == 0) error("LCD Panel not configured");
    if(!Option.MaxCtrls)error("No memory allocated for GUI controls");
    if(*cmdline == '#') cmdline++;
    r = getint(cmdline, 1, Option.MaxCtrls);
    if(Ctrl[r].type == 0) error("Control #% does not exist", r);
    while(*cmdline && tokenfunction(*cmdline) != op_equal) cmdline++; // search for the = symbol
    if(!*cmdline) error("Syntax");
    ++cmdline;                                                        // step over the = symbol
        skipspace(cmdline);
    if(!*cmdline || *cmdline == '\'') error("Syntax");
    switch(Ctrl[r].type) {
        case CTRL_BUTTON:
        case CTRL_SWITCH:
        case CTRL_CHECKBOX:
        case CTRL_RADIOBTN: v = (getnumber(cmdline) != 0);
                            if(Ctrl[r].value == v) return;          // don't update if no change
                            Ctrl[r].value = v;
                            break;

        case CTRL_LED:      v = getnumber(cmdline);
                            if(v > 1) {
                                Ctrl[r].inc = v;
                                Ctrl[r].value = 1;
                                CheckGuiFlag = true;
                            } else {
                                int i;
                                CheckGuiFlag = false;
                                Ctrl[r].inc = 0;
                                // scan through all controls to see if any others are in a flash
                                // if there is, set the CheckGuiFlag which indicates that flash timing should continue
                                for(i = 1; i < Option.MaxCtrls; i++) {
                                    if(Ctrl[i].type == CTRL_LED && Ctrl[i].inc != 0) {
                                        CheckGuiFlag = true;
                                        break;
                                    }
                                }
                                if(Ctrl[r].value == v) return;      // don't update if no change
                                Ctrl[r].value = v;
                            }
                            break;

        case CTRL_SPINNER:
        case CTRL_GAUGE:
        case CTRL_BARGAUGE: v = getnumber(cmdline);
                            if(v < Ctrl[r].min) v = Ctrl[r].min;
                            if(v > Ctrl[r].max) v = Ctrl[r].max;
                            if(Ctrl[r].value == v) return;          // don't update if no change
                            Ctrl[r].value = v;
                            if(Ctrl[r].type == CTRL_SPINNER) FloatToStr((char *)Ctrl[r].s, v, 0, STR_AUTO_PRECISION, ' ');   // update the displayed string
                            break;

        case CTRL_NBRBOX:   if(strlen((char *)cmdline) > 3 && cmdline[0] == '"' && cmdline[1] == '#' && cmdline[2] == '#') {
                                // the user wants ghost text
                                strcpy((char *)Ctrl[r].s, (char *)getCstring(cmdline));
                                Ctrl[r].value = 0;
                            } else {
                                // a normal number
                                v = getnumber(cmdline);
                                if(r != InvokingCtrl && Ctrl[r].value == v) return;     // don't update if no change
                                Ctrl[r].value = v;
                                FloatToStr((char *)Ctrl[r].s, v, 0, STR_AUTO_PRECISION, ' ');   // update the displayed string
                            }
                            break;

        case CTRL_FRAME:
        case CTRL_CAPTION:
                            memset(Ctrl[r].s, ' ', strlen((char *)Ctrl[r].s));  // set the caption to spaces
                            UpdateControl(r);                           // erase the existing text
                            // fall through

        case CTRL_DISPLAYBOX:
        case CTRL_TEXTBOX:
        case CTRL_FMTBOX:  strcpy((char *)Ctrl[r].s, (char *)getCstring(cmdline));  break;
        }

    if(!(Ctrl[r].state & CTRL_DISABLED2)) UpdateControl(r);         // don't update if the gauge is disabled by a keypad (updating may mess they keypad)
}
/* 
 * @cond
 * The following section will be excluded from the documentation.
 */


void fun_mmhpos(void) {
    if(!Option.DISPLAY_TYPE) error("Display not configured");
    iret = CurrentX;
    targ = T_INT;
}


void fun_mmvpos(void) {
    if(!Option.DISPLAY_TYPE) error("Display not configured");
    iret = CurrentY;
    targ = T_INT;
}



/*void cmd_backlight(void) {
    if(HRes != 0 && Option.DISPLAY_TYPE > SSD_PANEL) error("SSD1963 display only");
    SetBacklightSSD1963(getint(cmdline, 0, 100));
}*/

void ResetGUI(void) {
    int i;
    InvokingCtrl = CurrentRef = LastRef = 0;
    LastX = LastY = TOUCH_ERROR;
    gui_int_down = gui_int_up = false;
    GuiIntDownVector = GuiIntUpVector = NULL;
    GUIKeyDown = 0;
    KeyAltShift = 0;
    last_x2 = 100;
    last_y2 = 100;
    last_fcolour = gui_fcolour;
    last_bcolour = gui_bcolour;
    last_inc = 1;
    last_min = -FLT_MAX;
    last_max = FLT_MAX;

    SetupPage = 0;
    CurrentPages = 1;
    for(i = 1; i < Option.MaxCtrls; i++) {
        if(Ctrl[i].s) FreeMemorySafe((void **)&Ctrl[i].s);
        if(Ctrl[i].fmt) FreeMemorySafe((void **)&Ctrl[i].fmt);
        memset(&Ctrl[i],0,sizeof(struct s_ctrl));
    }
}


// This is called by Timer.c every mSec
// it scans through all controls looking for a LED with a timeout set (in Ctrl[r].inc) and decrements it
void CheckGuiTimeouts(void) {
    int r;
    for(r = 1; r < Option.MaxCtrls; r++) {
        if(Ctrl[r].type == CTRL_LED && Ctrl[r].inc > 1) {
            Ctrl[r].inc--;                                          // decrement the timeout
        }
    }
}


// This implements a LED flash
// it is called by check_interrupt()
// it scans through all controls looking for a LED with a timeout set (in Ctrl[r].inc)
void CheckGui(void) {
    int r;
    CheckGuiFlag = false;
    for(r = 1; r < Option.MaxCtrls; r++) {
        if(Ctrl[r].type == CTRL_LED) {
            if(Ctrl[r].inc > 1)
                CheckGuiFlag = true;                                // keep calling this function while LEDs have timeouts
            if(Ctrl[r].inc == 1) {                                  // if this one has timed out
                Ctrl[r].inc = 0;
                Ctrl[r].value = 0;                                  // turn off the LED
                UpdateControl(r);
            }
        }
    }
}
/*  @endcond */
