/* wmDrawer (c) 2002-2004 Valery Febvre <vfebvre@vfebvre.lautre.net>
 *
 * wmDrawer is a dock application (dockapp) which provides a
 * drawer (button bar) to launch applications from.
 *
 * This program 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 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <signal.h>

#if defined SOLARIS
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#endif

#include "types_defs.h"

#include <X11/extensions/shape.h>
#include <X11/cursorfont.h>

#include "config.h"
#include "graphics.h"
#include "images.h"

#include "pixmaps/defaultDockIcon.xpm"
#include "pixmaps/defaultIconsBg.xpm"
#include "pixmaps/defaultIcon.xpm"
#include "pixmaps/defaultHighlightImg.xpm"

static void signalHandler (int signum);
static void quit (int status);
static void xfreeAllMemory (void);
static void buildDock (int argc, char *argv[]);
static void setDockIcon (void);
static void getDockPosition (void);
static void getDrawerPosition (int *x, int *y);
static void showDrawer (void);
static void hideDrawer (void);
static void buildDrawer (void);
static void buildTooltip (void);
static void highlightBtn (int col, int row);
static void highlightBtnShading (int col, int row);
static void highlightBtnTarget (int col, int row);
static void unhighlightBtn (Window btnWin);
static void rebuildApp (void);
static void execCmd (const char *cmd);
static void mySleep (unsigned long msec);
static int  xEnterEventOrTimeout (XEvent *e, unsigned long msec);
static void convertMCoordIntoBCoord (int mcol, int mrow, int *bcol, int *brow);
static void eventLoop (void);

Display *display;
static Window dockWin, dockIconWin;
static drawingArea drawer, subDrawer, tooltip;
static XFontStruct *font;
drawerButton **btns;
static Cursor cursor;
static Pixmap highlightImgPix, highlightImgMask;

static unsigned int drawerOpened = 0;
static unsigned int dpyWidth, dpyHeight;
static unsigned int dockX, dockY;
static unsigned int drawerW, drawerH;
static unsigned int btnDim = 0;
static int highlightCol, highlightRow;

extern drawerConfig config;
extern unsigned int nbRows, nbCols, drawerOK;
extern unsigned int useDefaultIconsBg, useDefaultDockIcon, useDefaultHighlightImg;

int main (int argc, char **argv) {
  struct sigaction action;
  
  parseOptions (argc, argv);
  parseConfig ();
  buildDock (argc, argv);
  setDockIcon ();
  buildDrawer ();
  buildTooltip ();
  action.sa_handler = signalHandler;
  sigemptyset (&action.sa_mask);
  action.sa_flags = SA_NOCLDSTOP | SA_NODEFER; 
  sigaction (SIGTERM, &action, NULL);
  sigaction (SIGINT,  &action, NULL);
  sigaction (SIGCHLD, &action, NULL);

  eventLoop ();
  return 0;
}

static void signalHandler (int signum) {
  switch (signum) {
  case SIGINT:
  case SIGTERM:
    quit (EXIT_SUCCESS);
    break;
  case SIGCHLD:
    while (waitpid (-1, NULL, WNOHANG | WUNTRACED) > 0);
    break;
  default:
    quit (EXIT_FAILURE);
  }
}

static void quit (int status) {
  printf ("Bye bye, thx to use %s\n", PACKAGE);
  xfreeAllMemory ();
  freeAllMemory ();
  XCloseDisplay (display);
  exit (status);
}

void xfreeAllMemory (void) {
  unsigned int col, row;

  /* free buttons' pixmap and GC */
  for (col=0; col<nbCols; col++) {
    for (row=0; row<nbRows; row++) {
      XFreeGC (display, btns[col][row].da.gc);
      if (btns[col][row].da.pixmap != None) {
	XFreePixmap (display, btns[col][row].da.pixmap);
      }
    }
  }
  /* free tooltip GC and font */
  XFreeGC (display, tooltip.gc);
  XFreeFont (display, font);
  /* destroy all windows and sub-windows */
  if (drawerOK == 1) {
    XDestroyWindow (display, drawer.win);
    XDestroyWindow (display, tooltip.win);
  }
}

static void buildDock (int argc, char *argv[]) {
  XClassHint *classhint = NULL;
  XWMHints *wmhints = NULL;
  XSizeHints *sizehints = NULL;
  int tmp;

  display = XOpenDisplay ("");
  if (!display) {
    printf ("%s error: couldn't open display\n", PACKAGE);
    exit (EXIT_FAILURE);
  }

  /* query SHAPE extension */
  if (!XShapeQueryExtension (display, &tmp, &tmp) && config.transparency > 0) {
    printf ("%s warning: SHAPE extension not supported\n", PACKAGE);
    config.transparency = 0;
  }

  // create dock wins
  dockWin = XCreateSimpleWindow (display, DefaultRootWindow (display),
				 0, 0, config.dockW, config.dockH, 0, 0, 0);
  dockIconWin = XCreateSimpleWindow (display, DefaultRootWindow (display),
				     0, 0, config.dockW, config.dockH,
				     0, 0, 0);

  /* set ClassHint */
  classhint = XAllocClassHint ();
  if (!classhint) {
    printf ("%s error: can't allocate memory for class hints!\n", PACKAGE);
    exit (EXIT_FAILURE);
  }
  classhint->res_class = PACKAGE;
  /* Ed Goforth <e.goforth@computer.org> */
  /* Get the instanceName from the config file or command-line */
  /* to set the client instance name (application name) */
  /* so the window manager can differentiate between multiple instances. */
  classhint->res_name = config.instanceName;
  XSetClassHint (display, dockWin, classhint);
  XSetClassHint (display, dockIconWin, classhint);
  XFree (classhint);

  // set name and icon name for dock wins
  XStoreName (display, dockWin, PACKAGE);
  XStoreName (display, dockIconWin, PACKAGE);
  XSetIconName (display, dockWin, PACKAGE);
  XSetIconName (display, dockIconWin, PACKAGE);

  /* set WMHints */
  wmhints = XAllocWMHints ();
  if (!wmhints) {
    printf ("%s error: can't allocate memory for wm hints!\n", PACKAGE);
    exit (EXIT_FAILURE);
  }
  wmhints->flags = IconWindowHint | WindowGroupHint;
  if (config.windowedMode == 0) {
    wmhints->flags |= StateHint;
    wmhints->initial_state = WithdrawnState;
  }
  wmhints->window_group = dockWin;
  wmhints->icon_window = dockIconWin;
  XSetWMHints (display, dockWin, wmhints);
  XFree (wmhints);

  /* set SizeHints */
  sizehints = XAllocSizeHints ();
  sizehints->flags = USSize;
  if (config.windowedMode == 0) {
    sizehints->flags |= USPosition;
    sizehints->x = sizehints->y = 0;
  }
  else {
    sizehints->flags |= PMinSize | PMaxSize;
    sizehints->min_width  = sizehints->max_width  = config.dockW;
    sizehints->min_height = sizehints->max_height = config.dockH;
  }
  sizehints->width = config.dockW;
  sizehints->height = config.dockH;
  XSetWMNormalHints (display, dockWin, sizehints);
  XFree (sizehints);

  /* Ed Goforth <e.goforth@computer.org> */
  /* This allows window managers (such as WindowMaker) to pick up the */
  /* command-line arguments that were used when the app was launched. */
  /* This is particularly useful when docking. */
  XSetCommand (display, dockWin, argv, argc);
  XSetCommand (display, dockIconWin, argv, argc);

#define INPUT_MASK ButtonPressMask | ButtonReleaseMask | ExposureMask | EnterWindowMask | LeaveWindowMask | StructureNotifyMask | SubstructureNotifyMask
  XSelectInput (display, dockWin, INPUT_MASK);
  XSelectInput (display, dockIconWin, INPUT_MASK);
#undef INPUT_MASK
}

static void setDockIcon (void) {
  Pixmap dockIconPix, dockIconMask, icon;
  int dockIconW, dockIconH;
  unsigned int dx, dy, res = 0;
  GC gc;
  XGCValues xgcv;

  if (useDefaultDockIcon == 0)
    res = createPixmapFromFile (config.dockIcon, &dockIconPix, &dockIconMask,
				&dockIconW, &dockIconH, config.dockW, config.dockH,
				RESIZE_SMALLER);
  if (useDefaultDockIcon == 1 || res == 0)
    createPixmapFromData ((const char **)defaultDockIcon_xpm, &dockIconPix,
			  &dockIconMask, &dockIconW, &dockIconH,
			  config.dockW, config.dockH, RESIZE_SMALLER);

  dx = (config.dockW - dockIconW) / 2;
  dy = (config.dockH - dockIconH) / 2;

  XShapeCombineMask (display, dockIconWin,
		     ShapeBounding, dx, dy, dockIconMask, ShapeSet);
  XShapeCombineMask (display, dockWin,
		     ShapeBounding, dx, dy, dockIconMask, ShapeSet);

  icon = XCreatePixmap (display, dockWin, config.dockW, config.dockH,
			(unsigned int)DefaultDepth (display, 0));
  xgcv.clip_x_origin = dx;
  xgcv.clip_y_origin = dy;
  xgcv.clip_mask = dockIconMask; /* could be omitted */
  xgcv.graphics_exposures = False;
  gc = XCreateGC (display, dockWin,
		  GCClipMask|GCClipXOrigin|GCClipYOrigin|GCGraphicsExposures,
		  &xgcv);
  XCopyArea (display, dockIconPix, icon, gc, 0, 0,
	     dockIconW, dockIconH, dx, dy);

  XSetWindowBackgroundPixmap (display, dockWin, icon);
  XSetWindowBackgroundPixmap (display, dockIconWin, icon);
  if (icon != None) XFreePixmap (display, icon);
  XFreeGC (display, gc);

  if (dockIconPix != None) XFreePixmap (display, dockIconPix);
  if (dockIconMask != None) XFreePixmap (display, dockIconMask);

  XMapWindow (display, dockWin);
  XClearWindow (display, dockWin);
  XClearWindow (display, dockIconWin);

  XFlush (display);
}

static void getDockPosition (void) {
  Window win, frame, parent, root, *children;
  unsigned int nchildren;
  XWindowAttributes waP, waC;

  dpyWidth =  DisplayWidth  (display, XDefaultScreen (display));
  dpyHeight = DisplayHeight (display, XDefaultScreen (display));

  if (config.windowedMode == 1) {
    win = dockWin;
  }
  else {
    win = dockIconWin;
  }
  /* get frame win */
  root = 0;
  frame = parent = win;
  while (parent != root) {
    frame = parent;
    XQueryTree (display, frame, &root, &parent, &children, &nchildren);
    dbg_msg (1, "Parent win: 0x%x\n", parent);
    if (children != None) XFree (children);
  }

  XGetWindowAttributes (display, frame, &waP);
  XGetWindowAttributes (display, win, &waC);
  dockX = waP.x + waC.x;
  dockY = waP.y + waC.y;
  dbg_msg (1, "Dock position: %d, %d\n", dockX, dockY);
}

static void getDrawerPosition (int *x, int *y) {
  getDockPosition ();
  switch (config.direction) {
  case topToBottom:
    *x = dockX - (int) (drawerW + config.borderSize*2 - config.dockW) / 2;
    *y = dockY + config.dockH;
    break;
  case rightToLeft:
    *x = dockX - (drawerW + config.borderSize*2);
    *y = dockY - (int) (drawerH + config.borderSize*2 - config.dockH) / 2;
    break;
  case bottomToTop:
    *x = dockX - (int) (drawerW + config.borderSize*2 - config.dockW) / 2;
    *y = dockY - (drawerH + config.borderSize*2);
    break;
  case leftToRight:
    *x = dockX + config.dockW;
    *y = dockY - (int) (drawerH + config.borderSize*2 - config.dockH) / 2;
    break;
  }
  if (*x < 0) *x = 0;
  if (*x + drawerW + config.borderSize*2 > dpyWidth - 1) {
    *x = dpyWidth - (drawerW + config.borderSize*2);
  }
  if (*y < 0) *y = 0;
  if (*y + drawerH + config.borderSize*2 > dpyHeight - 1) {
    *y = dpyHeight - (drawerH + config.borderSize*2);
  }
}

static void showDrawer (void) {
  int x, y;
  unsigned int dw, dh, inc;
  
  if (!drawerOK)
    return;
  drawerOpened = 1;
  highlightCol = highlightRow = -1;
  getDrawerPosition(&x, &y);

  // if no animation for drawer
  if (config.animationSpeed == 0) {
    XMoveResizeWindow (display, drawer.win, x, y, drawerW, drawerH);
    XMapRaised (display, drawer.win);
    return;
  }

  inc = (nbRows * btnDim) / ((5 - config.animationSpeed) * 4);
  switch (config.direction) {
  case topToBottom:
    for (dh = inc; dh < drawerH+inc; dh = dh+inc) {
      if (dh >= drawerH) {
	XMoveResizeWindow (display, drawer.win, x, y, drawerW, drawerH);
      }
      else {
	XMoveResizeWindow (display, drawer.win, x, y, drawerW, dh);
      }
      XMapRaised (display, drawer.win);
      XClearWindow (display, subDrawer.win);
      XSync (display, 0);
      mySleep (15);
    }
    break;
  case rightToLeft:
    for (dw = inc; dw < drawerW+inc; dw = dw+inc) {
      if (dw >= drawerW) {
	XMoveResizeWindow (display, drawer.win, x, y, drawerW, drawerH);
      }
      else {
	XMoveResizeWindow (display, drawer.win,
			   dockX - dw - config.borderSize*2, y, dw, drawerH);
      }
      XMapRaised (display, drawer.win);
      XClearWindow (display, subDrawer.win);
      XSync (display, 0);
      mySleep (15);
    }
    break;
  case bottomToTop:
    for (dh = inc; dh < drawerH+inc; dh = dh+inc) {
      if (dh >= drawerH) {
	XMoveResizeWindow (display, drawer.win, x, y, drawerW, drawerH);
      }
      else {
	XMoveResizeWindow (display, drawer.win,
			   x, dockY - dh - config.borderSize*2, drawerW, dh);
      }
      XMapRaised (display, drawer.win);
      XClearWindow (display, subDrawer.win);
      XSync (display, 0);
      mySleep (15);
    }
    break;
  case leftToRight:
    for (dw = inc; dw < drawerW+inc; dw = dw+inc) {
      if (dw >= drawerW) {
	XMoveResizeWindow (display, drawer.win, x, y, drawerW, drawerH);
      }
      else {
	XMoveResizeWindow (display, drawer.win, x, y, dw, drawerH);
      }
      XMapRaised (display, drawer.win);
      XClearWindow (display, subDrawer.win);
      XSync (display, 0);
      mySleep (15);
    }
    break;
  default:
    XMapWindow (display, drawer.win);
  }      
}

static void hideDrawer (void) {
  int x, y;
  unsigned int dw, dh, inc;

  if (!drawerOK)
    return;
  drawerOpened = 0;
  XUnmapWindow (display, tooltip.win);

  // if no animation for drawer
  if (config.animationSpeed == 0) {
    XUnmapWindow (display, drawer.win);
    return;
  }

  inc = (nbRows * btnDim) / ((5 - config.animationSpeed) * 4);
  switch (config.direction) {
  case topToBottom:
    x = dockX - (int) (drawerW + config.borderSize*2 - config.dockW) / 2;
    if (x + drawerW + config.borderSize*2 > dpyWidth - 1) {
      x = dpyWidth - (drawerW + config.borderSize*2);
    }
    if (x < 0) x = 0;
    for (dh = drawerH - inc; dh >= inc; dh=dh-inc) {
      XMoveResizeWindow (display, drawer.win,
			 x, dockY + config.dockH, drawerW, dh);
      XMapRaised (display, drawer.win);
      XSync (display, 0);
      mySleep (15);
   }
    break;
  case rightToLeft:
    y = dockY - (int) (drawerH + config.borderSize*2 - config.dockH) / 2;
    if (y + drawerH + config.borderSize*2 > dpyHeight - 1) {
      y = dpyHeight - (drawerH + config.borderSize*2);
    }
    if (y < 0) y = 0;
    for (dw = drawerW - inc; dw >= inc; dw=dw-inc) {
      XMoveResizeWindow (display, drawer.win,
			 dockX - dw - config.borderSize*2, y, dw, drawerH);
      XMapRaised (display, drawer.win);
      XSync (display, 0);
      mySleep (15);
    }
    break;
  case bottomToTop:
    x = dockX - (int) (drawerW + config.borderSize*2 - config.dockW) / 2;
    if (x + drawerW + config.borderSize*2 > dpyWidth - 1) {
      x = dpyWidth - (drawerW + config.borderSize*2);
    }
    if (x < 0) x = 0;
    for (dh = drawerH - inc; dh >= inc; dh = dh-inc) {
      XMoveResizeWindow (display, drawer.win,
			 x, dockY - dh - config.borderSize*2, drawerW, dh);
      XMapRaised (display, drawer.win);
      XSync (display, 0);
      mySleep (15);
    }
    break;
  case leftToRight:
    y = dockY - (int) (drawerH + config.borderSize*2 - config.dockH) / 2;
    if (y + drawerH + config.borderSize*2 > dpyHeight - 1) {
      y = dpyHeight - (drawerH + config.borderSize*2);
    }
    if (y < 0) y = 0;
    for (dw = drawerW - inc; dw >= inc; dw=dw-inc) {
      XMoveResizeWindow (display, drawer.win,
			 dockX + config.dockW, y, dw, drawerH);
      XMapRaised (display, drawer.win);
      XSync (display, 0);
      mySleep (15);
    }
    break;
  }
  XUnmapWindow (display, drawer.win);
}

static void buildDrawer (void) {
  /* XColor cursor_color; */
  unsigned int res = 0, x, y, xr, yr, row, col;
  int pixmapW, pixmapH;
  unsigned long resizeMask;

  Pixmap bgImgPix, bgImgMask;
  Pixmap iconPix, iconMask;

  if (!drawerOK)
    return;
  btns = config.entries;

  resizeMask = RESIZE_SMALLER;
  if (config.iconsExpand == 1)
    resizeMask |= RESIZE_GREATER;

  if (config.btnsSize == 0) {
    btnDim = config.dockW / nbCols;
    if (btnDim < MIN_ICON_SIZE) btnDim = MIN_ICON_SIZE;
  }
  else {
    btnDim = config.btnsSize;
  }

  if (config.direction == leftToRight || config.direction == rightToLeft) {
    drawerW = nbRows * btnDim;
    drawerH = nbCols * btnDim;
  }
  else {
    drawerW = nbCols * btnDim;
    drawerH = nbRows * btnDim;
  }
  dbg_msg(1, "drawerW = %d, drawerH = %d, btn dim = %d\n",
	  drawerW, drawerH, btnDim);

  /* create drawer's root window */
  drawer.win = XCreateSimpleWindow (display, DefaultRootWindow (display),
				   0, 0, drawerW, drawerH,
				   config.borderSize, 0, 0);
  drawer.xswa.event_mask  = EnterWindowMask | LeaveWindowMask;
  drawer.xswa.override_redirect = True;
  XChangeWindowAttributes (display, drawer.win,
			   CWEventMask | CWOverrideRedirect, &(drawer.xswa));
  if (config.transparency == 1) {
    XSetWindowBackgroundPixmap (display, drawer.win, ParentRelative);
  }

  /* create drawer's subwindow */
  subDrawer.win = XCreateSimpleWindow (display, drawer.win,
				       0, 0, drawerW, drawerH,
				       0, 0, 0);
  subDrawer.xswa.event_mask = ButtonPressMask|ButtonReleaseMask|PointerMotionMask;
  subDrawer.xswa.override_redirect = False;
  XChangeWindowAttributes (display, subDrawer.win,
			   CWEventMask | CWOverrideRedirect, &(subDrawer.xswa));
  if (config.transparency == 1) {
    XSetWindowBackgroundPixmap (display, subDrawer.win, ParentRelative);
  }
  XMapWindow (display, subDrawer.win);

  /* highlight mask pixmap */
  if (useDefaultHighlightImg == 0) {
    res = createPixmapFromFile (config.highlightImg,
				&highlightImgPix, &highlightImgMask,
				&pixmapW, &pixmapH, btnDim, btnDim, resizeMask);
  }
  if (useDefaultHighlightImg == 1 || res == 0) {
    createPixmapFromData ((const char **)defaultHighlightImg_xpm,
			  &highlightImgPix, &highlightImgMask,
			  &pixmapW, &pixmapH, btnDim, btnDim, resizeMask);
  }

  /* btn's background pixmap */
  if (useDefaultIconsBg == 0) {
    res = createPixmapFromFile (config.iconsBg, &bgImgPix, &bgImgMask,
				&pixmapW, &pixmapH, btnDim, btnDim, resizeMask);
  }
  if (useDefaultIconsBg == 1 || res == 0) {
    createPixmapFromData ((const char **)defaultIconsBg_xpm,
			  &bgImgPix, &bgImgMask,
			  &pixmapW, &pixmapH, btnDim, btnDim, resizeMask);
  }

  /* Create boutons windows and set bg */
  for (col=0; col<nbCols; col++) {
    for (row=0; row<nbRows; row++) {
      if (config.direction == leftToRight || config.direction == rightToLeft) {
	x = row * btnDim;
	y = col * btnDim;
      }
      else {
	x = col * btnDim;
	y = row * btnDim;
      }
      btns[col][row].da.win = XCreateSimpleWindow (display, subDrawer.win,
						   x, y, btnDim, btnDim,
						   0, 0, 0);
      btns[col][row].da.pixmap = XCreatePixmap (display, btns[col][row].da.win,
						btnDim, btnDim,
						DefaultDepth (display, 0));
      btns[col][row].da.gc = XCreateGC (display, btns[col][row].da.pixmap, 0,
					&(btns[col][row].da.xgcv));
      if (config.transparency == 0) {
	XCopyArea (display, bgImgPix, btns[col][row].da.pixmap,
		   btns[col][row].da.gc,
		   0, 0, btnDim, btnDim, 0, 0);
      }
    }
  }
  if (bgImgPix != None) XFreePixmap (display, bgImgPix);
  if (bgImgMask != None) XFreePixmap (display, bgImgMask);

  /* Draw icons */
  for (col=0; col<nbCols; col++) {
    for (row=0; row<nbRows; row++) {
      if (btns[col][row].isEmpty == 1) continue;
      res = createPixmapFromFile (btns[col][row].image, &iconPix, &iconMask,
				  &pixmapW, &pixmapH, btnDim, btnDim,
				  resizeMask);
      if (res == 0)
	createPixmapFromData ((const char **)defaultIcon_xpm,
			      &iconPix, &iconMask,
			      &pixmapW, &pixmapH, btnDim, btnDim, resizeMask);
      xr = (btnDim - pixmapW) / 2;
      yr = (btnDim - pixmapH) / 2;
      if (config.transparency == 1) {
	XShapeCombineMask (display, btns[col][row].da.win, ShapeBounding, xr, yr,
			   iconMask, ShapeSet);
      }
      btns[col][row].da.xgcv.clip_x_origin = xr;
      btns[col][row].da.xgcv.clip_y_origin = yr;
      btns[col][row].da.xgcv.clip_mask = iconMask;
      XChangeGC (display, btns[col][row].da.gc,
		 GCClipXOrigin | GCClipYOrigin | GCClipMask,
		 &(btns[col][row].da.xgcv));
      XCopyArea (display, iconPix, btns[col][row].da.pixmap,
		 btns[col][row].da.gc, 0, 0, btnDim, btnDim, xr, yr);
    }
  }
  if (iconPix != None) XFreePixmap (display, iconPix);
  if (iconMask != None) XFreePixmap (display, iconMask);
  
  for (col=0; col<nbCols; col++) {
    for (row=0; row<nbRows; row++) {
      if (btns[col][row].isEmpty == 1 && config.transparency == 1) continue;
      btns[col][row].da.xswa.event_mask = 0;
      btns[col][row].da.xswa.background_pixmap = btns[col][row].da.pixmap;
      btns[col][row].da.xswa.override_redirect = False;
      XChangeWindowAttributes (display, btns[col][row].da.win,
			       CWEventMask | CWBackPixmap | CWOverrideRedirect,
			       &(btns[col][row].da.xswa));
      XMapWindow (display, btns[col][row].da.win);
    }
  }

  /* Cursor */
  cursor = XCreateFontCursor (display, config.cursor * 2);
  /* Red cursor, just for fun ? */
  /*   cursor_color.red = 65535; */
  /*   cursor_color.green = 0; */
  /*   cursor_color.blue = 0; */
  /*   XRecolorCursor(display, cursor, &cursor_color, */
  /*     &WhitePixel (display, DefaultScreen (display))); */
  XDefineCursor (display, drawer.win, cursor);
  XFreeCursor (display, cursor);
}

static void buildTooltip (void) {
  /* load font */
  if ((font = XLoadQueryFont (display, config.tooltipsFont)) == NULL) {
    printf("%s warning: cannot open font \"%s\" for tooltips\n",
	   PACKAGE, config.tooltipsFont);
    return;
  }
  /* build window */
  tooltip.win = XCreateSimpleWindow (display, RootWindow(display, 0),
				     0, 0, 1, 1, 1, 0,
				     WhitePixel (display,
						 DefaultScreen (display)));
  tooltip.gc = XCreateGC (display, tooltip.win, 0, &(tooltip.xgcv));
  XSetFont (display, tooltip.gc, font->fid);
  XSetForeground (display, tooltip.gc,
		  BlackPixel (display, DefaultScreen (display)));
  XSetBackground (display, tooltip.gc,
		  WhitePixel (display, DefaultScreen (display)));
  tooltip.xswa.event_mask = 0;
  tooltip.xswa.override_redirect = True;
  XChangeWindowAttributes (display, tooltip.win,
			   CWEventMask | CWOverrideRedirect, &(tooltip.xswa));
}

static void highlightBtn (int col, int row) {
  /* if transparency = 1 (pseudo transparency), only highlight shading works */
  if (config.transparency == 1 && config.highlight == 1) {
    config.highlight = 2;
  }
  switch (config.highlight) {
  case 1:
    highlightBtnTarget (col, row);
    break;
  case 2:
    highlightBtnShading (col, row);
    break;
  }
}

static void highlightBtnShading (int col, int row) {
  XImage *img;
  ShadingInfo shade;

  img = XGetImage (display, btns[col][row].da.win,
		   0, 0, btnDim, btnDim, AllPlanes, ZPixmap);
  shade.shading = config.highlightShading.shading;
  shade.tintColor.red   = config.highlightShading.tintColor.red * 256;
  shade.tintColor.green = config.highlightShading.tintColor.green * 256;
  shade.tintColor.blue  = config.highlightShading.tintColor.blue * 256;
  ShadeXImage (display, img, &shade);
  /* gc is OK, nothing to do */
  XPutImage (display, btns[col][row].da.win,
	     btns[col][row].da.gc, img, 0, 0, 0, 0, btnDim, btnDim);
  XDestroyImage (img);
  if( !XCopyArea (display, btns[col][row].da.win, btns[col][row].da.win,
		  btns[col][row].da.gc, 0, 0, btnDim, btnDim, 0, 0)) {
    XFillRectangle ( display, btns[col][row].da.win, btns[col][row].da.gc,
		     0, 0, btnDim, btnDim);
  }
}

static void highlightBtnTarget (int col, int row) {
  XGCValues xgcv;
  GC highlightGC;

  xgcv.clip_mask = highlightImgMask;
  highlightGC = XCreateGC (display, btns[col][row].da.win, GCClipMask, &xgcv);
  XCopyArea (display, highlightImgPix, btns[col][row].da.win,
	     highlightGC, 0, 0, btnDim, btnDim, 0, 0);
  XFreeGC (display, highlightGC);
}

static void unhighlightBtn (Window btnWin) {
  //XClearArea (display, btnWin, 0, 0, btnDim, btnDim, False);
  XClearWindow (display, btnWin);
}

static void rebuildApp (void) {
  if (configChanged ()) {
    if (drawerOpened == 1) hideDrawer ();
    /* restore mouse cursor */
    XUndefineCursor (display, drawer.win);
    xfreeAllMemory ();
    freeAllMemory ();

    parseConfig ();
    XResizeWindow(display, dockWin, config.dockW, config.dockH);
    XResizeWindow(display, dockIconWin, config.dockW, config.dockH);
    setDockIcon ();
    buildDrawer ();
    buildTooltip ();
  }
}

void execCmd (const char *cmd) {
  int cpid;
  
  cpid = fork ();
  if (cpid == -1) {
    printf ("%s error: can't fork\n", PACKAGE);
  }
  else if (cpid == 0) {
    setsid ();
    execl ("/bin/sh", "/bin/sh", "-c", cmd, NULL);
    exit (EXIT_SUCCESS);
  }
}

/* msec is miliseconds, msec <= 999 */
static void mySleep (unsigned long msec) {
  struct timespec req, rem;

  req.tv_sec = 0;
  req.tv_nsec = msec * 1000000;
  
  while (nanosleep (&req, &rem) != 0) {
    req = rem;
  }
}

static int xEnterEventOrTimeout (XEvent *e, unsigned long msec) {
  struct timeval timeout;
  fd_set rset;
  
  dbg_msg (1, "Enter xNextEventOrTimeout function\n");

  XSync (display, False);
  if (XPending (display)) {
    XNextEvent (display, e);
    dbg_msg (1, "Event type = %d\n", e->type);
    if (e->type == EnterNotify) return 1;
  }

  timeout.tv_sec  = msec / 1000;
  timeout.tv_usec = (msec % 1000) * 1000;
  FD_ZERO (&rset);
  FD_SET (ConnectionNumber (display), &rset);
  
  if (select (ConnectionNumber (display)+1, &rset, NULL, NULL, &timeout) > 0) {
    XNextEvent (display, e);
    return 1;
  }
  return 0;
}

/* Convert mouse coordinate into button coordinate */
static void convertMCoordIntoBCoord (int mcol, int mrow, int *bcol, int *brow) {
  switch (config.direction) {
  case topToBottom:
  case bottomToTop:
    *bcol = (int)((float)mcol / btnDim);
    *brow = (int)((float)mrow / btnDim);
    break;
  case rightToLeft:
  case leftToRight:
    *bcol = (int)((float)mrow / btnDim);
    *brow = (int)((float)mcol / btnDim);
    break;
  }
}

static void eventLoop (void) {
  XEvent e;
  int x, y, w ,h;
  /* coord of btn pressed */
  int prow = 0, pcol = 0;
  /* btn's coord during motion */
  int mrow = 0, mcol = 0; 
  /* coord when btn released */
  int rrow = 0, rcol = 0;
  /* X btn's coord */
  int xbtn = 0, ybtn = 0;
  unsigned int delta, btnIsPressed = 0;

  while (True) {
    XNextEvent (display, &e);
    rebuildApp ();
    /* look at X.h & Xlib.h in /usr/X11R6/include/X11/ */
    switch (e.type) {
    case Expose:
      dbg_msg (2, "Expose event: count = %d, win = 0x%x\n",
	       e.xexpose.count, e.xexpose.window);
      break;
    case ReparentNotify :
    case ConfigureNotify:
      dbg_msg (1, "ConfigureNotify event\n");
      if (drawerOpened == 1) {
	getDrawerPosition (&x, &y);
	XMoveWindow (display, drawer.win, x, y);
      }
      break;
    case MotionNotify:
      if (config.highlight == 0 || btnIsPressed == 1 || drawerOpened == 0) break;
      convertMCoordIntoBCoord(e.xmotion.x, e.xmotion.y, &mcol, &mrow);
      if (highlightCol == mcol && highlightRow == mrow) break;
      dbg_msg (1, "Motion event: win=0x%x, subwin=0x%x, (%d, %d) (%d, %d)\n",
	       e.xmotion.window, e.xmotion.subwindow,
	       highlightCol, highlightRow, mcol, mrow);
      if (mcol < 0 || mcol >= nbCols || mrow < 0 || mrow >= nbRows) break;
      /* Ok, first raise drawer */
      XRaiseWindow (display, drawer.win);
      /* tooltips */
      if (config.tooltips == 1) {
	if (!btns[mcol][mrow].isEmpty && strlen (btns[mcol][mrow].tooltip) > 0) {
	  getDrawerPosition (&x, &y);
	  w = XTextWidth (font, btns[mcol][mrow].tooltip,
			  strlen(btns[mcol][mrow].tooltip)) + 8;
	  h = font->max_bounds.ascent + font->max_bounds.descent + 4;
	  switch (config.direction) {
	  case rightToLeft:
	  case leftToRight:
	    x = x + mrow * btnDim + btnDim / 2;
	    y = y + mcol * btnDim - h - 4;
	    break;
	  case bottomToTop:
	  case topToBottom:
	    x = x + mcol * btnDim + btnDim / 2;
	    y = y + mrow * btnDim - h - 4;
	    break;
	  }
	  if (x < 0) x = 0;
	  if (x + w + 2 > dpyWidth - 1) x = dpyWidth - (w + 2);
	  XMoveResizeWindow (display, tooltip.win, x, y, w, h);
	  XMapRaised (display, tooltip.win);
	  XClearWindow (display, tooltip.win);
	  XDrawString (display, tooltip.win, tooltip.gc,
		       0 + 4, font->max_bounds.ascent + font->max_bounds.descent,
		       btns[mcol][mrow].tooltip,
		       strlen(btns[mcol][mrow].tooltip));
	}
	else {
	  XUnmapWindow (display, tooltip.win);
	}
      }
      /* highlight */
      if (highlightCol >= 0 && highlightRow >= 0) {
	if (btns[highlightCol][highlightRow].isEmpty == 0) { 
	  unhighlightBtn (btns[highlightCol][highlightRow].da.win);
	}
      }
      if (btns[mcol][mrow].isEmpty == 0) {
	  highlightBtn (mcol, mrow);
	  highlightCol = mcol;
	  highlightRow = mrow;
      }
      else highlightCol = highlightRow = -1;
      break;
    case ButtonPress:
      dbg_msg (1, "Button press event: win = 0x%x, cursor = (%d,%d)\n",
	       e.xbutton.window, e.xbutton.y, e.xbutton.y);
      if (drawerOpened == 0) {
	showDrawer ();
      }
      else if (drawerOpened == 1 && e.xbutton.window != subDrawer.win) {
	hideDrawer ();
      }
      else if (drawerOpened == 1 && e.xbutton.window == subDrawer.win) {
        xbtn = e.xbutton.x / btnDim;
        ybtn = e.xbutton.y / btnDim;
	convertMCoordIntoBCoord (e.xbutton.x, e.xbutton.y, &pcol, &prow);
	delta = 1;
	if (btns[pcol][prow].isEmpty == 0) {
	  btnIsPressed = 1;
	  XMoveWindow (display, btns[pcol][prow].da.win,
		       xbtn * btnDim + delta, ybtn * btnDim + delta);
	  XMapRaised (display, btns[pcol][prow].da.win);
	  XSync (display, 0);
	}
      }
      break;
    case ButtonRelease:
      dbg_msg (1, "Button release event: win = 0x%x, cursor = (%d,%d)\n",
	       e.xbutton.window, e.xbutton.y, e.xbutton.y);
      btnIsPressed = 0;
      if (drawerOpened == 1 && e.xbutton.window == subDrawer.win) {
	convertMCoordIntoBCoord(e.xbutton.x, e.xbutton.y, &rcol, &rrow);
	if (btns[pcol][prow].isEmpty == 0) {
	  XMoveWindow (display, btns[pcol][prow].da.win,
		       xbtn * btnDim, ybtn * btnDim);
	  XMapRaised (display, btns[pcol][prow].da.win);
	  XSync (display, False);
	}
	if (prow == rrow && pcol == rcol && btns[pcol][prow].isEmpty == 0) {
	  /* if release coord = press coord => OK exec cmd */
	  dbg_msg (1, "Command executed : %s (%d, %d)\n",
		   btns[pcol][prow].command, pcol, prow);
	  execCmd (btns[pcol][prow].command);
	  if ((e.xbutton.state & ShiftMask) != 0 || e.xbutton.button != 1) {
	    /* If Shift is down whilst selecting app keep the drawer open */
	    /* or mouse button != 1 => nothing */
	  }
	  else {
	    XUnmapWindow (display, tooltip.win);
	    mySleep (500);
	    hideDrawer ();
	  }
	}
      }
      break;
    case EnterNotify:
      dbg_msg (1, "Enter window: 0x%x 0x%x, mode=%d, detail=%d\n",
	       (unsigned int)e.xcrossing.window,
	       drawer.win, e.xcrossing.mode, e.xcrossing.detail);
      /* mode != 2 to avoid EnterNotify event emit after ButtonRelease
	 in windowed mode */
      if (drawerOpened == 0 && config.showOnHover == 1 && e.xcrossing.mode != 2) {
	showDrawer ();
      }
      break;
    case LeaveNotify:
      dbg_msg (1, "Leave window: win=0x%x, mode=%d, detail=%d\n",
	       (unsigned int)e.xcrossing.window, e.xcrossing.mode,
	       e.xcrossing.detail);
      /* if we leave drawer => unhighlight the last button highlighted and
	 unmap tooltip window */
      if (config.highlight > 0 && btnIsPressed == 0 && drawerOpened == 1 &&
	  e.xcrossing.window == drawer.win &&
	  highlightCol >= 0 && highlightRow >= 0) {
	XUnmapWindow (display, tooltip.win);
	unhighlightBtn (btns[highlightCol][highlightRow].da.win);
	highlightCol = highlightRow = -1;
      }
      /* mode = NotifyNormal:0, NotifyGrab:1, NotifyUngrab:2, NotifyWhileGrabbed:3
	 mode != 1 to avoid LeaveNotify event emit before ButtonPress
	 in windowed mode */
      if (drawerOpened == 1 && config.hideOnOut == 1 && e.xcrossing.mode != 1) {
	if (! xEnterEventOrTimeout (&e, config.hideTimeout)) {
	  hideDrawer ();
	}
      }
      break;
    case DestroyNotify:
      quit (EXIT_SUCCESS);
    }
  }
}
