diff options
Diffstat (limited to 'drv_X11.c')
-rw-r--r-- | drv_X11.c | 525 |
1 files changed, 479 insertions, 46 deletions
@@ -1,10 +1,10 @@ -/* $Id: drv_X11.c 773 2007-02-25 12:39:09Z michael $ - * $URL: https://ssl.bulix.org/svn/lcd4linux/branches/0.10.1/drv_X11.c $ +/* $Id: drv_X11.c 1125 2010-07-11 11:17:41Z mzuther $ + * $URL: https://ssl.bulix.org/svn/lcd4linux/trunk/drv_X11.c $ * * new style X11 Driver for LCD4Linux * - * Copyright (C) 2003 Michael Reinelt <reinelt@eunet.at> - * Copyright (C) 2004 The LCD4Linux Team <lcd4linux-devel@users.sourceforge.net> + * Copyright (C) 2003 Michael Reinelt <michael@reinelt.co.at> + * Copyright (C) 2004, 2008 The LCD4Linux Team <lcd4linux-devel@users.sourceforge.net> * * based on the old XWindow.c which is * Copyright (C) 2000 Herbert Rosmanith <herp@wildsau.idv.uni-linz.ac.at> @@ -46,16 +46,22 @@ #include <termios.h> #include <fcntl.h> #include <sys/time.h> +#include <signal.h> #include <X11/Xlib.h> #include <X11/Xutil.h> +#include <X11/Xresource.h> #include "debug.h" #include "cfg.h" #include "qprintf.h" #include "timer.h" #include "plugin.h" +#include "rgb.h" #include "drv.h" +#include "widget.h" +#include "widget_keypad.h" #include "drv_generic_graphic.h" +#include "drv_generic_keypad.h" #ifdef WITH_DMALLOC #include <dmalloc.h> @@ -68,10 +74,17 @@ static int pgap = 0; /* gap between points */ static int rgap = 0; /* row gap between lines */ static int cgap = 0; /* column gap between characters */ static int border = 0; /* window border */ +static int buttons = 0; /* number of keypad buttons */ +static int btnwidth = 0; +static int btnheight = 0; static int dimx, dimy; /* total window dimension in pixel */ +static Atom wmDeleteMessage; -static RGBA *drv_X11_FB = NULL; +static RGBA *drv_X11_FB = NULL; /* framebuffer */ + +static RGBA BP_COL = {.R = 0xff,.G = 0xff,.B = 0xff,.A = 0x00 }; /* pixel background color */ +static RGBA BR_COL = {.R = 0xff,.G = 0xff,.B = 0xff,.A = 0x00 }; /* border color */ static Display *dp; static int sc, dd; @@ -79,33 +92,61 @@ static Window w, rw; static Visual *vi; static GC gc; static Colormap cm; -static XColor xc; static Pixmap pm; +static int allow_autorepeat = 1; /* consider auto-repeated KeyPress events? */ + +static char myDisplayName[256] = ""; +static int opTableEntries = 2; +static XrmOptionDescRec opTable[] = { + {"-display", ".display", XrmoptionSepArg, NULL}, + {"-synchronous", "*synchronous", XrmoptionNoArg, "on"}, +}; + +static XrmDatabase commandlineDB; + /****************************************/ /*** hardware dependant functions ***/ /****************************************/ -static void drv_X11_color(RGBA c) +static XColor drv_X11_color(RGBA c, int brightness) { - xc.red = 255 * c.R; - xc.green = 255 * c.G; - xc.blue = 255 * c.B; - xc.flags = DoRed | DoGreen | DoBlue; - if (XAllocColor(dp, cm, &xc) == False) { - error("%s: XAllocColor(%02x%02x%02x) failed!", Name, c.R, c.G, c.B); + static XColor col[64]; + static unsigned char alloced[64] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + XColor xc; + int key; + + xc.red = brightness * c.R; + xc.green = brightness * c.G; + xc.blue = brightness * c.B; + /* 16bits per color, compressed to 6 bits (2 each color) */ + key = (xc.red & 0xc000) >> 10 | (xc.green & 0xc000) >> 12 | (xc.blue & 0xc000) >> 14; + /* todo: support more than 64 colors: check if allocated color is exactly the requested */ + if (alloced[key]) { + xc = col[key]; + } else { + xc.flags = DoRed | DoGreen | DoBlue; + if (XAllocColor(dp, cm, &xc) == False) { + error("%s: XAllocColor(%02x%02x%02x) failed!", Name, c.R, c.G, c.B); + } + col[key] = xc; + alloced[key] = 1; } + XSetForeground(dp, gc, xc.pixel); - XSetBackground(dp, gc, xc.pixel); + return (xc); } static void drv_X11_blit(const int row, const int col, const int height, const int width) { int r, c; - int dirty = 0; for (r = row; r < row + height; r++) { int y = border + (r / YRES) * rgap + r * (pixel + pgap); @@ -114,16 +155,89 @@ static void drv_X11_blit(const int row, const int col, const int height, const i RGBA p1 = drv_X11_FB[r * DCOLS + c]; RGBA p2 = drv_generic_graphic_rgb(r, c); if (p1.R != p2.R || p1.G != p2.G || p1.B != p2.B) { - drv_X11_color(p2); - XFillRectangle(dp, w, gc, x, y, pixel, pixel); + XClearArea(dp, w, x, y, pixel, pixel, 1); drv_X11_FB[r * DCOLS + c] = p2; - dirty = 1; } } } - if (dirty) { - XSync(dp, False); +} + + +static int drv_X11_brightness(int brightness) +{ + static int Brightness = 255; + int i; + + /* -1 is used to query the current brightness */ + if (brightness == -1) + return Brightness; + + if (brightness < 0) + brightness = 0; + if (brightness > 255) + brightness = 255; + + if (Brightness != brightness) { + + float dim = brightness / 255.0; + + debug("%s: set brightness to %d%%", Name, (int) (dim * 100)); + + /* set new background */ + XSetWindowBackground(dp, w, drv_X11_color(BR_COL, brightness).pixel); + + /* redraw every LCD pixel */ + XClearWindow(dp, w); + for (i = 0; i < DROWS * DCOLS; i++) { + drv_X11_FB[i] = NO_COL; + } + drv_X11_blit(0, 0, LROWS, LCOLS); + + /* remember new brightness */ + Brightness = brightness; } + + return Brightness; +} + + +static int drv_X11_keypad(const int num) +{ + int val; + int new_num = num; + + if (new_num > 0) + val = WIDGET_KEY_PRESSED; + else { + /* negative values mark a key release */ + new_num = -num; + val = WIDGET_KEY_RELEASED; + } + + switch (new_num) { + case 1: + val += WIDGET_KEY_UP; + break; + case 2: + val += WIDGET_KEY_DOWN; + break; + case 3: + val += WIDGET_KEY_LEFT; + break; + case 4: + val += WIDGET_KEY_RIGHT; + break; + case 5: + val += WIDGET_KEY_CONFIRM; + break; + case 6: + val += WIDGET_KEY_CANCEL; + break; + default: + error("%s: unknown keypad value %d", Name, num); + } + + return val; } @@ -140,6 +254,18 @@ static void drv_X11_expose(const int x, const int y, const int width, const int int r, c; int x0, y0; int x1, y1; + XFontStruct *xfs; + int xoffset = border + (DCOLS / XRES) * cgap + DCOLS * (pixel + pgap); + int yoffset = border + (DROWS / YRES) * rgap; + int yk; + char *s; + char unknownTxt[10]; + XRectangle rect[DROWS * DCOLS]; + int nrect = 0; + RGBA col; + RGBA lastCol = { 0, 0, 0, 0 }; + int hasLastCol = 0; + int brightness = drv_X11_brightness(-1); x0 = x - pixel; x1 = x + pixel + width; @@ -154,11 +280,72 @@ static void drv_X11_expose(const int x, const int y, const int width, const int int xc = border + (c / XRES) * cgap + c * (pixel + pgap); if (xc < x0 || xc > x1) continue; - drv_X11_color(drv_generic_graphic_rgb(r, c)); - XFillRectangle(dp, w, gc, xc, yc, pixel, pixel); + col = drv_generic_graphic_rgb(r, c); + if (hasLastCol) { + /* if the color of this pixel is different to the last pixels draw the old ones */ + if (col.R != lastCol.R || col.G != lastCol.G || col.B != lastCol.B) { + drv_X11_color(lastCol, brightness); + XFillRectangles(dp, w, gc, rect, nrect); + nrect = 0; + lastCol = col; + } + rect[nrect].x = xc; + rect[nrect].y = yc; + rect[nrect].width = pixel; + rect[nrect].height = pixel; + nrect++; + } else { + /* 1st shot: no old color */ + drv_X11_color(col, brightness); + XFillRectangle(dp, w, gc, xc, yc, pixel, pixel); + lastCol = col; + hasLastCol = 1; + } + } + } + /* draw the last block of rectangles */ + drv_X11_color(lastCol, brightness); + XFillRectangles(dp, w, gc, rect, nrect); + + /* Keypad on the right side */ + if (x1 >= xoffset) { + xfs = XQueryFont(dp, XGContextFromGC(DefaultGC(dp, 0))); + if (drv_X11_brightness(-1) > 127) { + drv_X11_color(FG_COL, 255); + } else { + drv_X11_color(BG_COL, 255); + } + for (r = 0; r < buttons; r++) { + yk = yoffset + r * (btnheight + pgap); + switch (r) { + case 0: + s = "Up"; + break; + case 1: + s = "Down"; + break; + case 2: + s = "Left"; + break; + case 3: + s = "Right"; + break; + case 4: + s = "Confirm"; + break; + case 5: + s = "Cancel"; + break; + default: + snprintf(unknownTxt, sizeof(unknownTxt), "#%d??", r); + s = unknownTxt; + } + XDrawRectangle(dp, w, gc, xoffset, yk, btnwidth, btnheight - 2); + XDrawString(dp, w, gc, + xoffset + btnwidth / 2 - (xfs->max_bounds.width * strlen(s)) / 2, + yk + btnheight / 2 + xfs->max_bounds.ascent / 2, s, strlen(s)); } } - XSync(dp, False); } @@ -166,11 +353,171 @@ static void drv_X11_timer( __attribute__ ((unused)) void *notused) { XEvent ev; - - if (XCheckWindowEvent(dp, w, ExposureMask, &ev) == 0) + XRectangle exp; + KeySym key; + int xoffset = border + (DCOLS / XRES) * cgap + DCOLS * (pixel + pgap); + int yoffset = border + (DROWS / YRES) * rgap; + static int btn = 0; + + if (XCheckWindowEvent + (dp, w, ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask, &ev) == 0 + /* there is no ClientMessageMask, so this will be checked separately */ + && XCheckTypedWindowEvent(dp, w, ClientMessage, &ev) == 0) return; - if (ev.type == Expose) { - drv_X11_expose(ev.xexpose.x, ev.xexpose.y, ev.xexpose.width, ev.xexpose.height); + + /* check whether key has been retriggered by "auto repeat" */ + unsigned short is_retriggered = 0; + + switch (ev.type) { + + case Expose: + /* collect all expose events in eventqueue */ + exp.x = ev.xexpose.x; + exp.y = ev.xexpose.y; + exp.width = ev.xexpose.width; + exp.height = ev.xexpose.height; + while (XCheckWindowEvent(dp, w, ExposureMask, &ev)) { + if (ev.xexpose.x < exp.x) { + exp.width += exp.x - ev.xexpose.x; + exp.x -= exp.x - ev.xexpose.x; + } + if (ev.xexpose.y < exp.y) { + exp.height += exp.y - ev.xexpose.y; + exp.y -= exp.y - ev.xexpose.y; + } + if (ev.xexpose.x + ev.xexpose.width > exp.x + exp.width) { + exp.width += ev.xexpose.x + ev.xexpose.width - (exp.x + exp.width); + } + if (ev.xexpose.y + ev.xexpose.height > exp.y + exp.height) { + exp.height += ev.xexpose.y + ev.xexpose.height - (exp.y + exp.height); + } + } + drv_X11_expose(exp.x, exp.y, exp.width, exp.height); + break; + + case KeyPress: + key = XLookupKeysym(&ev.xkey, 0); + switch (key) { + case XK_Up: + btn = 1; + break; + case XK_Down: + btn = 2; + break; + case XK_Left: + btn = 3; + break; + case XK_Right: + btn = 4; + break; + case XK_Return: + btn = 5; + break; + case XK_Escape: + btn = 6; + break; + default: + btn = 0; + } + /* only register key press if button is defined on GUI */ + if (btn > 0) { + if (btn <= buttons) { + debug("key for button %i pressed", btn); + drv_X11_color(BG_COL, 255); + XFillRectangle(dp, w, gc, xoffset + 1, yoffset + (btn - 1) * (btnheight + pgap) + 1, btnwidth - 1, + btnheight - 2 - 1); + drv_generic_keypad_press(btn); + } else { + debug("key press for button %i ignored", btn); + } + } + break; + + case KeyRelease: + /* check whether key has been retriggered by "auto repeat" */ + if (!allow_autorepeat && XEventsQueued(dp, QueuedAfterReading)) { + XEvent nev; + XPeekEvent(dp, &nev); + + if (nev.type == KeyPress && nev.xkey.time == ev.xkey.time && nev.xkey.keycode == ev.xkey.keycode) { + is_retriggered = 1; + + /* delete retriggered KeyPress event */ + XNextEvent(dp, &ev); + } + } + + key = XLookupKeysym(&ev.xkey, 0); + switch (key) { + case XK_Up: + btn = 1; + break; + case XK_Down: + btn = 2; + break; + case XK_Left: + btn = 3; + break; + case XK_Right: + btn = 4; + break; + case XK_Return: + btn = 5; + break; + case XK_Escape: + btn = 6; + break; + } + /* only register key release if button is defined on GUI */ + if (!is_retriggered && (btn > 0)) { + if (btn <= buttons) { + debug("key for button %i released", btn); + XClearArea(dp, w, xoffset, yoffset + (btn - 1) * (btnheight + pgap), btnwidth, btnheight - 2, + 1 /* true */ ); + /* negative values mark a key release */ + drv_generic_keypad_press(-btn); + } else { + debug("key release for button %i ignored", btn); + } + } + break; + + case ButtonPress: + if (ev.xbutton.x >= xoffset && ev.xbutton.x <= xoffset + btnwidth + && ev.xbutton.y >= yoffset && ev.xbutton.y <= yoffset + buttons * btnheight + (buttons - 1) * pgap) { + btn = (ev.xbutton.y - yoffset) / (btnheight + pgap) + 1; /* btn 0 is unused */ + debug("button %d pressed", btn); + drv_X11_color(BG_COL, 255); + XFillRectangle(dp, w, gc, xoffset + 1, yoffset + (btn - 1) * (btnheight + pgap) + 1, btnwidth - 1, + btnheight - 2 - 1); + drv_generic_keypad_press(btn); + } + break; + + case ButtonRelease: + if (ev.xbutton.x >= xoffset && ev.xbutton.x <= xoffset + btnwidth + && ev.xbutton.y >= yoffset && ev.xbutton.y <= yoffset + buttons * btnheight + (buttons - 1) * pgap) { + btn = (ev.xbutton.y - yoffset) / (btnheight + pgap) + 1; /* btn 0 is unused */ + debug("button %d released", btn); + XClearArea(dp, w, xoffset, yoffset + (btn - 1) * (btnheight + pgap), btnwidth, btnheight - 2, + 1 /* true */ ); + } + break; + + case ClientMessage: + if ((Atom) (ev.xclient.data.l[0]) == wmDeleteMessage) { + info("%s: Window closed by WindowManager, quit.", Name); + if (raise(SIGTERM) != 0) { + error("%s: Error raising SIGTERM: exit!", Name); + exit(1); + } + } else { + debug("%s: Got XClient message 0x%lx %lx %lx %lx %lx", Name, ev.xclient.data.l[0], + ev.xclient.data.l[1], ev.xclient.data.l[2], ev.xclient.data.l[3], ev.xclient.data.l[4]); + } + + default: + debug("%s: unknown XEvent %d", Name, ev.type); } } @@ -179,7 +526,8 @@ static int drv_X11_start(const char *section) { int i; char *s; - RGBA BC; + XrmValue value; + char *str_type[20]; XSetWindowAttributes wa; XSizeHints sh; XEvent ev; @@ -221,11 +569,33 @@ static int drv_X11_start(const char *section) if (cfg_number(section, "border", 0, 0, -1, &border) < 0) return -1; + /* special case for the X11 driver: + * the border color may be different from the backlight color + * the backlight color is the color of an inactive pixel + * the border color is the color of the border and gaps between pixels + * for the brightness pugin we need a copy of BL_COL, we call it BP_COL + */ s = cfg_get(section, "basecolor", "000000ff"); - if (color2RGBA(s, &BC) < 0) { + if (color2RGBA(s, &BP_COL) < 0) { error("%s: ignoring illegal color '%s'", Name, s); } free(s); + BL_COL = BP_COL; + + BR_COL = BP_COL; + if ((s = cfg_get(section, "bordercolor", NULL)) != NULL) { + if (color2RGBA(s, &BR_COL) < 0) { + error("%s: ignoring illegal color '%s'", Name, s); + } + free(s); + } + + /* consider auto-repeated KeyPress events? */ + cfg_number(section, "autorepeat", 1, 0, 1, &allow_autorepeat); + + /* virtual keyboard: number of buttons (0..6) */ + if (cfg_number(section, "buttons", 0, 0, 6, &buttons) < 0) + return -1; drv_X11_FB = malloc(DCOLS * DROWS * sizeof(*drv_X11_FB)); if (drv_X11_FB == NULL) { @@ -234,14 +604,24 @@ static int drv_X11_start(const char *section) } for (i = 0; i < DCOLS * DROWS; i++) { - drv_X11_FB[i] = BC; + drv_X11_FB[i] = NO_COL; + } + + if (XrmGetResource(commandlineDB, "lcd4linux.display", "Lcd4linux.Display", str_type, &value)) { + strncpy(myDisplayName, value.addr, value.size); + debug("%s: X11 display name from command line: %s", Name, myDisplayName); } - if ((dp = XOpenDisplay(NULL)) == NULL) { - error("%s: can't open display", Name); + if ((dp = XOpenDisplay(strlen(myDisplayName) > 0 ? myDisplayName : NULL)) == NULL) { + error("%s: can't open display %s", Name, XDisplayName(strlen(myDisplayName) > 0 ? myDisplayName : NULL)); return -1; } + if (XrmGetResource(commandlineDB, "lcd4linux*synchronous", "Lcd4linux*Synchronous", str_type, &value)) { + debug("%s: X synchronize on", Name); + XSynchronize(dp, 1 /* true */ ); + } + sc = DefaultScreen(dp); gc = DefaultGC(dp, sc); vi = DefaultVisual(dp, sc); @@ -251,25 +631,41 @@ static int drv_X11_start(const char *section) dimx = DCOLS * pixel + (DCOLS - 1) * pgap + (DCOLS / XRES - 1) * cgap; dimy = DROWS * pixel + (DROWS - 1) * pgap + (DROWS / YRES - 1) * rgap; + if (buttons != 0) { + btnwidth = (DCOLS * pixel + (DCOLS - 1) * pgap) / 10; + btnheight = (DROWS * pixel + (DROWS - 1) * pgap) / buttons; + } - wa.event_mask = ExposureMask; - - w = XCreateWindow(dp, rw, 0, 0, dimx + 2 * border, dimy + 2 * border, 0, 0, InputOutput, vi, CWEventMask, &wa); - - pm = XCreatePixmap(dp, w, dimx, dimy, dd); + wa.event_mask = ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask; - sh.min_width = sh.max_width = dimx + 2 * border; + sh.min_width = sh.max_width = dimx + 2 * border + btnwidth; sh.min_height = sh.max_height = dimy + 2 * border; sh.flags = PPosition | PSize | PMinSize | PMaxSize; - XSetWMProperties(dp, w, NULL, NULL, NULL, 0, &sh, NULL, NULL); + if (sh.min_width > DisplayWidth(dp, sc) || sh.min_height > DisplayHeight(dp, sc)) { + error("%s: Warning: X11-Window with dimensions (%d,%d) is greater than display (%d,%d)!", + Name, sh.min_width, sh.min_height, DisplayWidth(dp, sc), DisplayHeight(dp, sc)); + if (sh.min_width > 32767 || sh.min_height > 32676) { + /* XProtocol data size exceeded */ + exit(1); + } + } + w = XCreateWindow(dp, rw, 0, 0, sh.min_width, sh.min_height, 0, 0, InputOutput, vi, CWEventMask, &wa); + + pm = XCreatePixmap(dp, w, dimx, dimy, dd); - drv_X11_color(BC); + XSetWMProperties(dp, w, NULL, NULL, NULL, 0, &sh, NULL, NULL); + wmDeleteMessage = XInternAtom(dp, "WM_DELETE_WINDOW", False); + XSetWMProtocols(dp, w, &wmDeleteMessage, 1); - XFillRectangle(dp, pm, gc, 0, 0, dimx + 2 * border, dimy + 2 * border); - XSetWindowBackground(dp, w, xc.pixel); + XSetWindowBackground(dp, w, drv_X11_color(BR_COL, 255).pixel); XClearWindow(dp, w); + /* set brightness (after first background painting) */ + if (cfg_number(section, "Brightness", 255, 0, 255, &i) > 0) { + drv_X11_brightness(i); + } + XStoreName(dp, w, "LCD4Linux"); XMapWindow(dp, w); @@ -294,7 +690,24 @@ static int drv_X11_start(const char *section) /*** plugins ***/ /****************************************/ -/* none at the moment... */ +static void plugin_brightness(RESULT * result, const int argc, RESULT * argv[]) +{ + double brightness; + + switch (argc) { + case 0: + brightness = drv_X11_brightness(-1); + SetResult(&result, R_NUMBER, &brightness); + break; + case 1: + brightness = drv_X11_brightness(R2N(argv[0])); + SetResult(&result, R_NUMBER, &brightness); + break; + default: + error("%s.brightness(): wrong number of parameters", Name); + SetResult(&result, R_STRING, ""); + } +} /****************************************/ @@ -305,17 +718,27 @@ static int drv_X11_start(const char *section) /* list models */ int drv_X11_list(void) { - printf("any"); + printf("any X11 server"); return 0; } +/* read X11 specific command line arguments */ +/* it is defined in drv.h */ +void drv_X11_parseArgs(int *argc, char *argv[]) +{ + XrmInitialize(); + XrmParseCommand(&commandlineDB, opTable, opTableEntries, "lcd4linux", argc, argv); +} + + /* initialize driver & display */ int drv_X11_init(const char *section, const int quiet) { + RGBA bl_col; int ret; - info("%s: %s", Name, "$Rev: 773 $"); + info("%s: %s", Name, "$Rev: 1125 $"); /* start display */ if ((ret = drv_X11_start(section)) != 0) @@ -323,10 +746,18 @@ int drv_X11_init(const char *section, const int quiet) /* real worker functions */ drv_generic_graphic_real_blit = drv_X11_blit; + drv_generic_keypad_real_press = drv_X11_keypad; /* initialize generic graphic driver */ + /* save BL_COL which may already be dimmed */ + bl_col = BL_COL; if ((ret = drv_generic_graphic_init(section, Name)) != 0) return ret; + BL_COL = bl_col; + + /* initialize generic key pad driver */ + if ((ret = drv_generic_keypad_init(section, Name)) != 0) + return ret; drv_generic_graphic_clear(); @@ -337,13 +768,14 @@ int drv_X11_init(const char *section, const int quiet) char buffer[40]; qprintf(buffer, sizeof(buffer), "%s %dx%d", Name, DCOLS, DROWS); if (drv_generic_graphic_greet(buffer, NULL)) { + drv_X11_expose(0, 0, dimx + 2 * border, dimy + 2 * border); sleep(3); drv_generic_graphic_clear(); } } /* register plugins */ - /* none at the moment... */ + AddFunction("LCD::brightness", -1, plugin_brightness); return 0; @@ -357,6 +789,7 @@ int drv_X11_quit(const __attribute__ ((unused)) info("%s: shutting down.", Name); drv_generic_graphic_quit(); + drv_generic_keypad_quit(); if (drv_X11_FB) { free(drv_X11_FB); |