brewpanel/sdlgui.c

changeset 409
cdf68044adaf
child 410
e3f8a51b566a
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/brewpanel/sdlgui.c	Sat Nov 07 22:04:17 2015 +0100
@@ -0,0 +1,698 @@
+/*****************************************************************************
+ * Copyright (C) 2015
+ *   
+ * Michiel Broek <mbroek at mbse dot eu>
+ *
+ * This file is part of the mbsePi-apps emulator
+ *
+ * The gui code is based on the gui from the emulator ARAnyM,
+ * Copyright (c) 2004 Petr Stehlik of ARAnyM dev team
+ *
+ * This progrm is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2, or (at your option) any
+ * later version.
+ *
+ * mbsePi-apps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with mbsePi-apps; see the file COPYING.  If not, write to the Free
+ * Software Foundation, 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ *****************************************************************************/
+
+#include "brewpanel.h"
+#include "sdlgui.h"
+#include "lcdfont10x16.h"
+
+
+static SDL_Surface	*pSdlGuiScrn;            	/* Pointer to the actual main SDL screen surface 	*/
+static SDL_Surface	*pFontGfx = NULL;        	/* The LCD font graphics 				*/
+static int		fontwidth, fontheight;		/* Width & height of the actual font 			*/
+TTF_Font                *pFont = NULL;			/* TTF font for buttons etc.				*/
+
+extern int		my_shutdown;
+
+
+/*-----------------------------------------------------------------------*/
+/*
+ * Load an 1 plane XBM into a 8 planes SDL_Surface.
+ */
+static SDL_Surface *SDLGui_LoadXBM(int w, int h, const char *pXbmBits)
+{
+    SDL_Surface	*bitmap;
+    Uint8 	*dstbits;
+    const Uint8 *srcbits;
+    int		x, y, srcpitch, mask;
+
+    srcbits = (Uint8 *)pXbmBits;
+
+    /* Allocate the bitmap */
+    if ((bitmap = SDL_CreateRGBSurface(SDL_SWSURFACE, w, h, 8, 0, 0, 0, 0)) == NULL) {
+	syslog(LOG_NOTICE, "Failed to allocate bitmap: %s", SDL_GetError());
+	return NULL;
+    }
+
+    srcpitch = ((w + 7) / 8);
+    dstbits = (Uint8 *)bitmap->pixels;
+    mask = 1;
+
+    /* Copy the pixels */
+    for (y = 0 ; y < h ; y++) {
+	for (x = 0 ; x < w ; x++) {
+	    dstbits[x] = (srcbits[x / 8] & mask) ? 1 : 0;
+	    mask <<= 1;
+	    mask |= (mask >> 8);
+	    mask &= 0xFF;
+	}
+	dstbits += bitmap->pitch;
+	srcbits += srcpitch;
+    }
+
+    return bitmap;
+}
+
+
+
+/*
+ * Initialize the GUI.
+ */
+int SDLGui_Init(void)
+{
+    char	*Pt = NULL;
+
+    SDL_Color blackWhiteColors[2] = {{255, 255, 255, 0}, {0, 0, 0, 0}};
+
+    /* 
+     * Initialize the LCD font graphics: 
+     */
+    pFontGfx = SDLGui_LoadXBM(lcdfont10x16_width, lcdfont10x16_height, lcdfont10x16_bits);
+    if (pFontGfx == NULL) {
+	syslog(LOG_NOTICE, "Error: Can not init font graphics!");
+	return -1;
+    }
+
+    /* Set color palette of the LCD font graphics: */
+    SDL_SetColors(pFontGfx, blackWhiteColors, 0, 2);
+
+    /* Set font color 0 as transparent: */
+    SDL_SetColorKey(pFontGfx, (SDL_SRCCOLORKEY|SDL_RLEACCEL), 0);
+
+    if (TTF_Init() == -1) {
+	syslog(LOG_NOTICE, "Could not init SDL_ttf");
+	return -1;
+    }
+
+    /*
+     * Load TTF font for the dialogs
+     */
+    Pt = calloc(1024, sizeof(char));
+    sprintf(Pt, "%s", "/usr/share/fonts/TTF/DejaVuSans.ttf");
+    if ((pFont = TTF_OpenFont(Pt, 14 )) == NULL) {
+	sprintf(Pt, "%s", "/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf");
+	if ((pFont = TTF_OpenFont(Pt, 14 )) == NULL) {
+	    sprintf(Pt, "%s", "/usr/share/fonts/truetype/freefont/DejaVuSans.ttf");
+	    if ((pFont = TTF_OpenFont(Pt, 14 )) == NULL) {
+		syslog(LOG_NOTICE, "Could not load DejaVuSans.ttf");
+		return -1;
+	    }
+	}
+    }
+    syslog(LOG_NOTICE, "Using ttf font: %s\n", Pt);
+    free(Pt);
+    Pt = NULL;
+
+    return 0;
+}
+
+
+
+/*
+ * Uninitialize the GUI.
+ */
+int SDLGui_UnInit(void)
+{
+    if (pFont)
+    	TTF_CloseFont(pFont);
+    pFont = NULL;
+
+    if (pFontGfx)
+	SDL_FreeSurface(pFontGfx);
+    pFontGfx = NULL;
+
+    return 0;
+}
+
+
+
+/*
+ * Inform the SDL-GUI about the actual SDL_Surface screen pointer and
+ * prepare the font to suit the actual resolution.
+ */
+int SDLGui_SetScreen(SDL_Surface *pScrn)
+{
+    pSdlGuiScrn = pScrn;
+
+    if (pFontGfx == NULL) {
+	syslog(LOG_NOTICE, "Error: A problem with the font occured!");
+	return -1;
+    }
+
+    /* Get the font width and height: */
+    fontwidth = pFontGfx->w/16;
+    fontheight = pFontGfx->h/16;
+    
+    return 0;
+}
+
+
+
+/*
+ * Center a dialog so that it appears in the middle of the screen.
+ * Note: We only store the coordinates in the root box of the dialog,
+ * all other objects in the dialog are positioned relatively to this one.
+ */
+void SDLGui_CenterDlg(SGOBJ *dlg)
+{
+    dlg[0].x = (pSdlGuiScrn->w/1-dlg[0].w)/2;
+    dlg[0].y = (pSdlGuiScrn->h/1-dlg[0].h)/2;
+}
+
+
+
+/*
+ * Draw a text string using TTF
+ */
+static void SDLGui_TTF(int x, int y, const char *txt)
+{
+    SDL_Rect    	offset;
+    SDL_Color		textColor = { 0, 0, 0 };
+    SDL_Surface*	message = NULL;
+
+    message = TTF_RenderText_Solid(pFont, txt, textColor);
+    offset.x = x;
+    offset.y = y;
+    SDL_BlitSurface(message, NULL, pSdlGuiScrn, &offset);
+    SDL_FreeSurface(message);
+    message = NULL;
+}
+
+
+
+/*
+ * Draw a dialog TTF text object.
+ */
+static void SDLGui_DrawTTF(const SGOBJ *tdlg, int objnum)
+{
+    int         x, y;
+
+    x = (tdlg[0].x + tdlg[objnum].x);
+    y = (tdlg[0].y + tdlg[objnum].y);
+    SDLGui_TTF(x, y, tdlg[objnum].txt);
+}
+
+
+
+/*
+ * Draw a text string.
+ */
+static void SDLGui_Text(int x, int y, const char *txt)
+{
+    int		i;
+    char	c;
+    SDL_Rect	sr, dr;
+
+    for (i=0; txt[i]!=0; i++) {
+	c = txt[i];
+	sr.x=fontwidth*(c%16);
+	sr.y=fontheight*(c/16);
+	sr.w=fontwidth;
+	sr.h=fontheight;
+	dr.x=x+i*(fontwidth+2);
+	dr.y=y;
+	dr.w=fontwidth;
+	dr.h=fontheight;
+	SDL_BlitSurface(pFontGfx, &sr, pSdlGuiScrn, &dr);
+    }
+}
+
+
+
+/*
+ * Draw a dialog text object.
+ */
+static void SDLGui_DrawText(const SGOBJ *tdlg, int objnum)
+{
+    int		x, y;
+
+    x = (tdlg[0].x + tdlg[objnum].x);
+    y = (tdlg[0].y + tdlg[objnum].y);
+    SDLGui_Text(x, y, tdlg[objnum].txt);
+}
+
+
+
+/*
+ * Draw a dialog LCD object.
+ */
+static void SDLGui_DrawLCD(const SGOBJ *bdlg, int objnum)
+{
+    SDL_Rect    rect;
+    int         x, y, w, h, offset, border = 4;
+    Uint32      bg0 = SDL_MapRGB(pSdlGuiScrn->format, 94,147, 69);
+    Uint32	bg1 = SDL_MapRGB(pSdlGuiScrn->format,156,235,  4);
+    Uint32	bc  = SDL_MapRGB(pSdlGuiScrn->format, 32, 32, 32);
+    Uint32      bg;
+
+    /*
+     * Width and height are given in character columns and rows,
+     * so calculate the display size in pixels.
+     */
+    w = bdlg[objnum].w * (fontwidth + 2) + 10;
+    h = bdlg[objnum].h * (fontheight + 2) + 4;
+
+    if (bdlg[objnum].x == -1) {
+	/*
+	 * Auto center
+	 */
+	x = (bdlg[0].w - w) / 2;
+    } else {
+    	x = bdlg[objnum].x;
+    }
+    y = bdlg[objnum].y;
+    if (objnum > 0) {           /* Since the root object is a box, too, */
+	/* we have to look for it now here and only */
+	x += bdlg[0].x;         /* add its absolute coordinates if we need to */
+	y += bdlg[0].y;
+    }
+
+    if (bdlg[objnum].state & SG_SELECTED) {
+	bg = bg1;
+    } else {
+	bg = bg0;
+    }
+
+    /* The root box should be bigger than the screen, so we disable the offset there: */
+    if (objnum != 0)
+	offset = border;
+    else
+	offset = 0;
+
+    /* Draw background: */
+    rect.x = x;
+    rect.y = y;
+    rect.w = w;
+    rect.h = h;
+    SDL_FillRect(pSdlGuiScrn, &rect, bg);
+
+    /* Draw upper border: */
+    rect.x = x - offset;
+    rect.y = y - offset;
+    rect.w = w + offset + offset;
+    rect.h = border;
+    SDL_FillRect(pSdlGuiScrn, &rect, bc);
+
+    /* Draw left border: */
+    rect.x = x - offset;
+    rect.y = y;
+    rect.w = border;
+    rect.h = h;
+    SDL_FillRect(pSdlGuiScrn, &rect, bc);
+
+    /* Draw bottom border: */
+    rect.x = x - offset;
+    rect.y = y + h - border + offset;
+    rect.w = w + offset + offset;
+    rect.h = border;
+    SDL_FillRect(pSdlGuiScrn, &rect, bc);
+
+    /* Draw right border: */
+    rect.x = x + w - border + offset;
+    rect.y = y;
+    rect.w = border;
+    rect.h = h;
+    SDL_FillRect(pSdlGuiScrn, &rect, bc);
+}
+
+
+
+/*
+ * Draw a dialog box object.
+ */
+static void SDLGui_DrawBox(const SGOBJ *bdlg, int objnum)
+{
+    SDL_Rect	rect;
+    int		x, y, w, h, offset, shade = 2;
+    Uint32	grey = SDL_MapRGB(pSdlGuiScrn->format,192,192,192);
+    Uint32	upleftc, downrightc;
+
+    x = bdlg[objnum].x;
+    y = bdlg[objnum].y;
+    if (objnum > 0) {		/* Since the root object is a box, too, */
+	/* we have to look for it now here and only */
+	x += bdlg[0].x;		/* add its absolute coordinates if we need to */
+	y += bdlg[0].y;
+    }
+    w = bdlg[objnum].w;
+    h = bdlg[objnum].h;
+
+    if (bdlg[objnum].state & SG_SELECTED) {
+	upleftc = SDL_MapRGB(pSdlGuiScrn->format,128,128,128);
+	downrightc = SDL_MapRGB(pSdlGuiScrn->format,255,255,255);
+    } else {
+	upleftc = SDL_MapRGB(pSdlGuiScrn->format,255,255,255);
+	downrightc = SDL_MapRGB(pSdlGuiScrn->format,128,128,128);
+    }
+
+    /* The root box should be bigger than the screen, so we disable the offset there: */
+    if (objnum != 0)
+	offset = shade;
+    else
+	offset = 0;
+
+    /* Draw background: */
+    rect.x = x;
+    rect.y = y;
+    rect.w = w;
+    rect.h = h;
+    SDL_FillRect(pSdlGuiScrn, &rect, grey);
+
+    /* Draw upper border: */
+    rect.x = x;
+    rect.y = y - offset;
+    rect.w = w;
+    rect.h = shade;
+    SDL_FillRect(pSdlGuiScrn, &rect, upleftc);
+
+    /* Draw left border: */
+    rect.x = x - offset;
+    rect.y = y;
+    rect.w = shade;
+    rect.h = h;
+    SDL_FillRect(pSdlGuiScrn, &rect, upleftc);
+
+    /* Draw bottom border: */
+    rect.x = x;
+    rect.y = y + h - shade + offset;
+    rect.w = w;
+    rect.h = shade;
+    SDL_FillRect(pSdlGuiScrn, &rect, downrightc);
+
+    /* Draw right border: */
+    rect.x = x + w - shade + offset;
+    rect.y = y;
+    rect.w = shade;
+    rect.h = h;
+    SDL_FillRect(pSdlGuiScrn, &rect, downrightc);
+}
+
+
+
+/*
+ * Draw a normal button.
+ */
+static void SDLGui_DrawButton(const SGOBJ *bdlg, int objnum)
+{
+    int		x, y, w, h;
+
+    SDLGui_DrawBox(bdlg, objnum);
+    /*
+     * Use bold text and get outer dimensions of the text
+     */
+    TTF_SetFontStyle(pFont, TTF_STYLE_BOLD);
+    TTF_SizeText(pFont, bdlg[objnum].txt, &w, &h);
+    x = bdlg[0].x + bdlg[objnum].x + (bdlg[objnum].w - w) / 2;
+    y = bdlg[0].y + bdlg[objnum].y + (bdlg[objnum].h - h) / 2;
+
+    if (bdlg[objnum].state & SG_SELECTED) {
+	x += 1;
+	y += 1;
+    }
+
+    if ((bdlg[objnum].flags & SG_HIDE) == 0)
+	SDLGui_TTF(x, y, bdlg[objnum].txt);
+    TTF_SetFontStyle(pFont, TTF_STYLE_NORMAL);
+}
+
+
+
+/*
+ * Draw a whole dialog.
+ */
+void SDLGui_DrawDialog(const SGOBJ *dlg)
+{
+    int i;
+	
+    for (i = 0; dlg[i].type != -1; i++) {
+	switch (dlg[i].type) {
+	    case SGBOX:
+			SDLGui_DrawBox(dlg, i);
+			break;
+	    case SGLCD:
+			SDLGui_DrawLCD(dlg, i);
+			break;
+	    case SGTEXT:
+			SDLGui_DrawText(dlg, i);
+			break;
+	    case SGTTF:
+			SDLGui_DrawTTF(dlg, i);
+			break;
+	    case SGBUTTON:
+			SDLGui_DrawButton(dlg, i);
+			break;
+	}
+    }
+
+    SDL_UpdateRect(pSdlGuiScrn, 0,0,0,0);
+}
+
+
+
+/*
+ * Search an object at a certain position.
+ */
+static int SDLGui_FindObj(const SGOBJ *dlg, int fx, int fy)
+{
+    int		len, i, ob = -1, xpos, ypos;
+
+    len = 0;
+    while (dlg[len].type != -1)
+	len++;
+
+    xpos = fx;
+    ypos = fy;
+    /* Now search for the object: */
+    for (i = len; i >= 0; i--) {
+	if (xpos >= dlg[0].x + dlg[i].x && ypos >= dlg[0].y + dlg[i].y && 
+	    xpos < dlg[0].x + dlg[i].x + dlg[i].w && ypos < dlg[0].y + dlg[i].y + dlg[i].h) {
+	    ob = i;
+	    break;
+	}
+    }
+	
+    return ob;
+}
+
+
+
+/*
+ * Search a button with a special flag (e.g. SG_DEFAULT or SG_CANCEL).
+ */
+static int SDLGui_SearchFlaggedButton(const SGOBJ *dlg, int flag)
+{
+    int i = 0;
+
+    while (dlg[i].type != -1) {
+	if (dlg[i].flags & flag)
+	    return i;
+	i++;
+    }
+
+    return 0;
+}
+
+
+
+/*
+ * Show and process a dialog. Returns the button number that has been
+ * pressed or SDLGUI_UNKNOWNEVENT if an unsupported event occured (will be
+ * stored in parameter pEventOut).
+ */
+int SDLGui_DoDialog(SGOBJ *dlg, SDL_Event *pEventOut)
+{
+    int		obj = 0, oldbutton = 0, retbutton = 0, i, j, b;
+    SDL_Event	sdlEvent;
+    SDL_Surface	*pBgSurface;
+    SDL_Rect	dlgrect, bgrect;
+
+//	if (pSdlGuiScrn->h / fontheight < dlg[0].h)
+//	{
+//		syslog(LOG_NOTICE, "Screen size too small for dialog!");
+//		return SDLGUI_ERROR;
+//	}
+
+    dlgrect.x = dlg[0].x;
+    dlgrect.y = dlg[0].y;
+    dlgrect.w = dlg[0].w;
+    dlgrect.h = dlg[0].h;
+
+    bgrect.x = bgrect.y = 0;
+    bgrect.w = dlgrect.w;
+    bgrect.h = dlgrect.h;
+
+    /*
+     * Save background
+     */
+    pBgSurface = SDL_CreateRGBSurface(SDL_SWSURFACE, dlgrect.w, dlgrect.h, pSdlGuiScrn->format->BitsPerPixel,
+	                                  pSdlGuiScrn->format->Rmask, pSdlGuiScrn->format->Gmask, pSdlGuiScrn->format->Bmask, pSdlGuiScrn->format->Amask);
+    if (pSdlGuiScrn->format->palette != NULL) {
+	SDL_SetColors(pBgSurface, pSdlGuiScrn->format->palette->colors, 0, pSdlGuiScrn->format->palette->ncolors-1);
+    }
+
+    if (pBgSurface != NULL) {
+	SDL_BlitSurface(pSdlGuiScrn,  &dlgrect, pBgSurface, &bgrect);
+    } else {
+	syslog(LOG_NOTICE, "SDLGUI_DoDialog: CreateRGBSurface failed: %s", SDL_GetError());
+    }
+
+    /* (Re-)draw the dialog */
+    SDLGui_DrawDialog(dlg);
+
+    /* 
+     * Is the left mouse button still pressed? Yes -> Handle TOUCHEXIT objects here
+     */
+    SDL_PumpEvents();
+    b = SDL_GetMouseState(&i, &j);
+    obj = SDLGui_FindObj(dlg, i, j);
+    if (obj > 0 && (dlg[obj].flags & SG_TOUCHEXIT) ) {
+	oldbutton = obj;
+	if (b & SDL_BUTTON(1)) {
+	    dlg[obj].state |= SG_SELECTED;
+	    retbutton = obj;
+	}
+    }
+
+    /* The main loop */
+    while (retbutton == 0 && !my_shutdown) {
+	if (SDL_WaitEvent(&sdlEvent) == 1)  /* Wait for events */
+	    switch (sdlEvent.type) {
+		case SDL_QUIT:
+				retbutton = SDLGUI_QUIT;
+				break;
+
+		case SDL_MOUSEBUTTONDOWN:
+				if (sdlEvent.button.button != SDL_BUTTON_LEFT) {
+				    /* Not left mouse button -> unsupported event */
+				    if (pEventOut)
+					retbutton = SDLGUI_UNKNOWNEVENT;
+				    break;
+				}
+				/* It was the left button: Find the object under the mouse cursor */
+				obj = SDLGui_FindObj(dlg, sdlEvent.button.x, sdlEvent.button.y);
+				if (obj > 0) {
+				    if (dlg[obj].type == SGBUTTON) {
+					dlg[obj].state |= SG_SELECTED;
+					SDLGui_DrawButton(dlg, obj);
+					SDL_UpdateRect(pSdlGuiScrn, dlg[0].x + dlg[obj].x - 2, dlg[0].y + dlg[obj].y - 2, dlg[obj].w + 4, dlg[obj].h + 4);
+					oldbutton=obj;
+				    }
+				    if ( dlg[obj].flags & SG_TOUCHEXIT ) {
+					dlg[obj].state |= SG_SELECTED;
+					retbutton = obj;
+				    }
+				}
+				break;
+
+			 case SDL_MOUSEBUTTONUP:
+				if (sdlEvent.button.button != SDL_BUTTON_LEFT) {
+				    /* Not left mouse button -> unsupported event */
+				    if (pEventOut)
+					retbutton = SDLGUI_UNKNOWNEVENT;
+				    break;
+				}
+				/* It was the left button: Find the object under the mouse cursor */
+				obj = SDLGui_FindObj(dlg, sdlEvent.button.x, sdlEvent.button.y);
+				if (obj > 0) {
+				    switch (dlg[obj].type) {
+					case SGBUTTON:
+						if (oldbutton==obj)
+						    retbutton=obj;
+						break;
+				    }
+				}
+				if (oldbutton > 0) {
+				    dlg[oldbutton].state &= ~SG_SELECTED;
+				    SDLGui_DrawButton(dlg, oldbutton);
+				    SDL_UpdateRect(pSdlGuiScrn, (dlg[0].x+dlg[oldbutton].x)*fontwidth-2, (dlg[0].y+dlg[oldbutton].y)*fontheight-2,
+						dlg[oldbutton].w*fontwidth+4, dlg[oldbutton].h*fontheight+4);
+				    oldbutton = 0;
+				}
+				if (obj >= 0 && (dlg[obj].flags & SG_EXIT)) {
+				    retbutton = obj;
+				}
+				break;
+
+			 case SDL_MOUSEMOTION:
+				break;
+
+			 case SDL_KEYDOWN:                     /* Key pressed */
+				if (sdlEvent.key.keysym.sym == SDLK_RETURN || sdlEvent.key.keysym.sym == SDLK_KP_ENTER) {
+				    retbutton = SDLGui_SearchFlaggedButton(dlg, SG_DEFAULT);
+				} else if (sdlEvent.key.keysym.sym == SDLK_ESCAPE) {
+				    retbutton = SDLGui_SearchFlaggedButton(dlg, SG_CANCEL);
+				} else if (pEventOut) {
+				    retbutton = SDLGUI_UNKNOWNEVENT;
+				}
+				break;
+
+			 default:
+				if (pEventOut)
+				    retbutton = SDLGUI_UNKNOWNEVENT;
+				break;
+	    }
+    }
+
+    /* Restore background */
+    if (pBgSurface) {
+	SDL_BlitSurface(pBgSurface, &bgrect, pSdlGuiScrn,  &dlgrect);
+	SDL_FreeSurface(pBgSurface);
+    }
+
+    /* Copy event data of unsupported events if caller wants to have it */
+    if (retbutton == SDLGUI_UNKNOWNEVENT && pEventOut)
+	memcpy(pEventOut, &sdlEvent, sizeof(SDL_Event));
+
+    if (retbutton == SDLGUI_QUIT)
+	my_shutdown = TRUE;
+
+    return retbutton;
+}
+
+
+
+void SDLGui_LCDwrite(SGOBJ *dlg, int x, int y, Uint8 c, int lcdindex)
+{
+    int		i, index;
+
+    fprintf(stdout, "SDLGui_LCDwrite( , %d, %d, %c, %d)\n", x, y, c, lcdindex);
+
+    i = index = 0;
+    for (;;) {
+	if (dlg[i].type == -1) {
+	    syslog(LOG_NOTICE, "SDLGui_LCDwrite() lcdindex=%d not found", lcdindex);
+	    return;
+	}
+	if (dlg[i].type == SGLCD) {
+	    if (index == lcdindex)
+		break;
+	    index++;
+	}
+	i++;
+    }
+    fprintf(stdout, "SDLGui_LCDwrite i=%d LCD=%dx%d\n", i, dlg[i].w, dlg[i].h);
+
+}
+
+

mercurial