Blob Blame Raw

#include "app.h"

#include <math.h>

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

#include <X11/Xutil.h>
#include <X11/Xatom.h>
#include <X11/extensions/Xrandr.h>


int appInit(App *app) {
  LOGDBG("app: init");
  
  CLEARFROM(app, dpy);
  
  LOGDBG("app: init: connect to xcb");
  app->dpy = XOpenDisplay(NULL);
  if (!app->dpy)
    return LOGERR("app: init: cannot connect to xcb");
  
  LOGDBG("app: init: get screen");
  app->screen = DefaultScreen(app->dpy);
  app->sw = DisplayWidth(app->dpy, app->screen);
  app->sh = DisplayHeight(app->dpy, app->screen);
  LOGDBG("app: init: screen=%d sw=%d sh=%d", app->screen, app->sw, app->sh);
  
  LOGDBG("app: init: get root window");
  app->root = RootWindow(app->dpy, app->screen);
  
  LOGDBG("app: init: create window");
  
  app->x = 0;
  app->w = app->sw * WIDTH_SCALE;
  app->h = app->sh * HEIGHT_SCALE;

  #if defined(WIDTH_SCALE_P) && defined(HEIGHT_SCALE_P)
  if (app->sw < app->sh) {
    app->w = app->sw * WIDTH_SCALE_P;
    app->h = app->sh * HEIGHT_SCALE_P;
  }
  #endif

  #ifdef TOP
  app->y = 0;
  #else
  app->y = app->sh - app->h;
  #endif
  
  XSetWindowAttributes attr = {};
  #ifdef NOBORDER
  attr.override_redirect = 1;
  #endif

  app->win = XCreateWindow(
    app->dpy, app->root,               // display and parent window
    app->x, app->y, app->w, app->h, 0, // position, size and border width
    CopyFromParent, CopyFromParent, CopyFromParent, // depth, class and visual
    CWOverrideRedirect, &attr);        // mask with attributes

  XSelectInput(
    app->dpy, app->win,
      StructureNotifyMask
    | ExposureMask
    | ButtonPressMask
    | ButtonReleaseMask
    | Button1MotionMask );

  LOGDBG("app: init: set window title");
  #ifndef NOBORDER
  char *title = TITLE;
  XTextProperty wtitle;
  if (!XStringListToTextProperty(&title, 1, &wtitle)) {
    LOGWRN("app: init: XStringListToTextProperty error for title: %s", title);
  } else {
    XSetWMName(app->dpy, app->win, &wtitle);
    XFree(wtitle.value);
  }
  #ifdef DOCK
  Atom wt = XInternAtom(app->dpy, "_NET_WM_WINDOW_TYPE", False);
  //Atom wtval = XInternAtom(app->dpy, "_HILDON_WM_WINDOW_TYPE_REMOTE_TEXTURE", False);
  Atom wtval = XInternAtom(app->dpy, "_NET_WM_WINDOW_TYPE_TOOLBAR", False);
  if (wt != None && wtval != None)
    XChangeProperty(app->dpy, app->win, wt, XA_ATOM, 32, PropModeReplace, (unsigned char*)&wtval, 1);
  Atom pm = XInternAtom(app->dpy, "_HILDON_PORTRAIT_MODE_SUPPORT", False);
  unsigned int pmval = 1;
  if (pm)
    XChangeProperty(app->dpy, app->win, pm, XA_CARDINAL, 32, PropModeReplace, (unsigned char*)&pmval, 1);
  #endif
  #endif

  LOGDBG("app: init: set window minimum size");
  XSizeHints *wsh = XAllocSizeHints();
  if (!wsh) {
    LOGWRN("app: init: XAllocSizeHints error");
  } else {
    #ifdef LOCK_SIZE
    wsh->flags      = PMinSize | PMaxSize;
    wsh->min_width  = wsh->max_width  = app->w;
    wsh->min_height = wsh->max_height = app->h;
    #else
    wsh->flags      = PMinSize;
    wsh->min_width  = MIN_WIDTH;
    wsh->min_height = MIN_HEIGHT;
    #endif
    XSetSizeHints(app->dpy, app->win, wsh, XA_WM_NORMAL_HINTS);
    XFree(wsh);
  }
  
  // window must not grab the focus
  LOGDBG("app: init: set window input hint");
  XWMHints *wmh = XAllocWMHints();
  if (!wmh) {
    LOGWRN("app: init: XAllocWMHints error");
  } else {
    wmh->flags = InputHint;
    wmh->input = False;
    XSetWMHints(app->dpy, app->win, wmh);
    XFree(wmh);
  }
  
  // subscribe to windows delete message from window manager
  app->aWmDel = XInternAtom(app->dpy, "WM_DELETE_WINDOW", False);
  XSetWMProtocols(app->dpy, app->win, &app->aWmDel, 1);
  
  // subscribe to keyboard state and layout changes
  XkbSelectEvents(app->dpy, XkbUseCoreKbd, XkbMapNotifyMask | XkbStateNotifyMask, XkbMapNotifyMask | XkbStateNotifyMask);
  
  #ifdef SCREEN_EVENTS
  // subscribe to xrandr events
  if (!XRRQueryExtension(app->dpy, &app->xrev, &app->xrerr)) {
    LOGWRN("app: init: XRRQueryExtension error");
  } else {
    app->xron = 1;
    XRRSelectInput(app->dpy, app->win, RRScreenChangeNotifyMask);
  }
  #endif

  // init submodules
  if (graphInit(&app->graph, app)) {
    graphResize(&app->graph);
    if (inputInit(&app->input, app)) {
      if (keyboardInit(&app->keyboard, app)) {
        return 1;
      }
      inputDeinit(&app->input);
    }
    graphDeinit(&app->graph);
  } else {
    XCloseDisplay(app->dpy);
  }
  
  return 0;
}


void appDeinit(App *app) {
  LOGDBG("app: deinit");
  keyboardDeinit(&app->keyboard);
  inputDeinit(&app->input);
  graphDeinit(&app->graph);
  XCloseDisplay(app->dpy);
}


int appRun(App *app) {
  LOGDBG("app: run");

  if (app->run)
    return LOGERR("app: run: seems already run");
  app->run = 1;
  
  LOGDBG("app: run: show window");
  XMapWindow(app->dpy, app->win);

  LOGDBG("app: run: get connection file descriptor");
  int fd = ConnectionNumber(app->dpy);
  if (fd < 0)
    LOGERR("app: run: cannot get XLib file descruptor");
    
  LOGDBG("app: run: event loop");
  int buttonDown = 0;
  unsigned int buttonDownMs = 0;
  fd_set fds = {};
  int maxFd = fd > app->stopFd ? fd : app->stopFd;
  XFlush(app->dpy);
  while(1) {
    int hasEvent = 0;
    XEvent event = {};
    
    FD_ZERO(&fds);
    FD_SET(fd, &fds);
    FD_SET(app->stopFd, &fds);
    
    // wait for next event
    if (XPending(app->dpy)) {
      XNextEvent(app->dpy, &event);
      hasEvent = 1;
    } else
    if (buttonDown != 1) {
      // just wait for fds
      select(maxFd + 1, &fds, NULL, NULL, NULL);
    } else {
      // manually call select for the X-socket to use timeout
      unsigned int dt = monotonicMs() - buttonDownMs;
      if (dt < LONGPRESS_MS) {
        dt = LONGPRESS_MS - dt;
        struct timeval t = {};
        t.tv_sec = dt/1000;
        t.tv_usec = dt%1000*1000;
        
        LOGDBG("app: run: use select fd=%d, timeout=%ums, sec=%ld, usec=%ld", fd, dt, t.tv_sec, t.tv_usec);
        
        XFlush(app->dpy);
        select(maxFd + 1, &fds, NULL, NULL, &t);
      }
    }
    
    if (!hasEvent && XPending(app->dpy)) {
      XNextEvent(app->dpy, &event);
      hasEvent = 1;
    }
    
    if (hasEvent) {
      switch(event.type) {
      case Expose: {
        LOGDBG( "app: expose: x=%d, y=%d, w=%d, h=%d",
                event.xexpose.x, event.xexpose.y, event.xexpose.width, event.xexpose.height );
        appInvalidateRect(app, event.xexpose.x, event.xexpose.y, event.xexpose.width, event.xexpose.height);
        break;
      }
      case ConfigureNotify: {
        if ( event.xconfigure.width
          && event.xconfigure.height
          && ( app->x != event.xconfigure.x
            || app->y != event.xconfigure.y
            || app->w != event.xconfigure.width
            || app->h != event.xconfigure.height ))
        {
          LOGDBG( "app: moved: x=%d y=%d w=%d, h=%d",
                  event.xconfigure.x, event.xconfigure.y, event.xconfigure.width, event.xconfigure.height );
          int resized = app->w != event.xconfigure.width || app->h != event.xconfigure.height;
          app->x = event.xconfigure.x;
          app->y = event.xconfigure.y;
          app->w = event.xconfigure.width;
          app->h = event.xconfigure.height;
          if (resized) graphResize(&app->graph);
        }
        break;
      }
      case ButtonPress: {
        if (event.xbutton.button == 1) {
          LOGDBG("app: mouse down: x=%d, y=%d", event.xbutton.x, event.xbutton.y);
          if (!buttonDown) { buttonDown = 1; buttonDownMs = monotonicMs(); }
          keyboardMouseDown(&app->keyboard, event.xbutton.x, event.xbutton.y);
        }
        break;
      }
      case ButtonRelease: {
        if (event.xbutton.button == 1) {
          LOGDBG("app: mouse up");
          buttonDown = 0;
          keyboardMouseUp(&app->keyboard);
        }
        break;
      }
      case MotionNotify: {
        if (event.xmotion.state & Button1Mask) {
          LOGDBG("app: mouse motion: x=%d, y=%d", event.xmotion.x, event.xmotion.y);
          keyboardMouseMotion(&app->keyboard, event.xmotion.x, event.xmotion.y);
        }
        break;
      }
      case MappingNotify: {
        if ( event.xmapping.request == MappingModifier
          || event.xmapping.request == MappingKeyboard )
        {
          LOGDBG("app: kayboard layout changed");
          XRefreshKeyboardMapping(&event.xmapping);
          inputUpdateLayout(
            &app->input,
            event.xmapping.first_keycode,
            event.xmapping.first_keycode + event.xmapping.count );
        }
        break;
      }
      case ClientMessage: {
        if (event.xclient.data.l[0] == app->aWmDel) {
          LOGDBG("app: delete window event");
          appStop(app, 0);
        }
        break;
      }
      default:
        if (app->xron && event.type == app->xrev + RRScreenChangeNotify) {
          LOGDBG("app: screen change event");
          XRRScreenChangeNotifyEvent *ev = (XRRScreenChangeNotifyEvent*)&event;
          appUpdateScreenSize(app, ev->width, ev->height);
        }
        break;
      }
    }
    
    if (buttonDown == 1 && monotonicMs() - buttonDownMs >= LONGPRESS_MS) {
        LOGDBG("app: long press");
        buttonDown = 2;
        keyboardMouseLongDown(&app->keyboard);
    }
    
    if (app->run != 1) break;
    
    inputUpdateModifiers(&app->input);
    keyboardResize(&app->keyboard);
    keyboardUpdateModifiers(&app->keyboard);
    if (app->irw > 0 && app->irh > 0) {
      LOGDBG("app: draw: x=%d, y=%d, w=%d, h=%d", app->irx, app->iry, app->irw, app->irh);
      graphDrawBackgound(&app->graph, app->irx, app->iry, app->irw, app->irh);
      keyboardDraw(&app->keyboard, app->irx, app->iry, app->irw, app->irh);
      app->irw = 0;
    }
  }
  
  int err = app->run == -2;
  app->run = 0;
  LOGDBG("app: run: done err=%d", err);
  return !err;
}


void appStopBySignal(App *app) {
  char x = 0;
  write(app->stopFd, &x, 1);
}


void appStop(App *app, int err) {
  LOGDBG("app: stop: err=%d", err);
  if (!app->run) {
    LOGERR("app: stop: seems not started");
    return;
  }
  if (app->run == 1) app->run = -1;
  if (err) app->run = -2;
}


void appUpdateScreenSize(App *app, int sw, int sh) {
  if (sw <= 0 || sh <= 0) return;
  if (sw == app->sw && sh == app->sh) return;
  LOGDBG("app: update screen size: w=%d, h=%d", sw, sh);

  #if defined(DOCK) || (defined(LOCK_SIZE) && defined(NOBORDER) && defined(NOTITLE))
  // static size case
  int w = sw * WIDTH_SCALE;
  int h = sh * HEIGHT_SCALE;
  #if defined(WIDTH_SCALE_P) && defined(HEIGHT_SCALE_P)
  if (sw < sh) {
    w = sw * WIDTH_SCALE_P;
    h = sh * HEIGHT_SCALE_P;
  }
  #endif

  int x0 = 0;
  #ifdef TOP
  int y0 = 0;
  #else
  int y0 = sh - h;
  #endif

  #else
  // dynamic size case
  double kx = sw/(double)app->sw;
  double ky = sh/(double)app->sh;

  #if defined(WIDTH_SCALE_P) && defined(HEIGHT_SCALE_P)
  // rescale for portrait/landscape switching
  if (app->sw < app->sh && sw >= sh) {
    // portrait -> landscape
    kx *= ((double)WIDTH_SCALE)/((double)WIDTH_SCALE_P);
    ky *= ((double)HEIGHT_SCALE)/((double)HEIGHT_SCALE_P);
  } else
  if (app->sw >= app->sh && sw < sh) {
    // landscape -> portrait
    kx *= ((double)WIDTH_SCALE_P)/((double)WIDTH_SCALE);
    ky *= ((double)HEIGHT_SCALE_P)/((double)HEIGHT_SCALE);
  }
  #endif

  // scale window corners
  int x0 = (int)round( app->x*kx );
  int y0 = (int)round( app->y*ky );
  int x1 = (int)round( (app->x + app->w)*kx );
  int y1 = (int)round( (app->y + app->h)*ky );
  int w = x1 - x0;
  int h = y1 - y0;
  #endif

  app->sw = sw;
  app->sh = sh;
  appMove(app, x0, y0, w, h);
}


void appMove(App *app, int x, int y, int w, int h) {
  LOGDBG("app: move: x=%d, y=%d, w=%d, h=%d", x, y, w, h);
  if (w < MIN_WIDTH)  w = MIN_WIDTH;
  if (h < MIN_HEIGHT) h = MIN_HEIGHT;
  
  unsigned int mask = 0;
  XWindowChanges ch = {};
  if (app->x != x) ch.x = x, mask |= CWX;
  if (app->y != y) ch.y = y, mask |= CWY;
  if (app->w != w) ch.width  = w, mask |= CWWidth;
  if (app->h != h) ch.height = h, mask |= CWHeight;
  if (!mask) return;
  
  XConfigureWindow(app->dpy, app->win, mask, &ch);
  XFlush(app->dpy);
}


void appInvalidateRect(App *app, int x, int y, int w, int h) {
  LOGDBG("app: invalidate rect: x=%d, y=%d, w=%d, h=%d", x, y, w, h);
  rectIntersect(&x, &y, &w, &h, 0, 0, app->w, app->h);
  rectMerge(&app->irx, &app->iry, &app->irw, &app->irh, x, y, w, h);
  LOGDBG("app: invalidate rect: summary x=%d, y=%d, w=%d, h=%d", app->irx, app->iry, app->irw, app->irh);
}