// Gmsh - Copyright (C) 1997-2019 C. Geuzaine, J.-F. Remacle
//
// See the LICENSE.txt file for license information. Please report all
// issues on https://gitlab.onelab.info/gmsh/gmsh/issues.

#include <stdio.h>
#include <string.h>
#include <FL/Fl_Tooltip.H>
#include "openglWindow.h"
#include "graphicWindow.h"
#include "manipWindow.h"
#include "contextWindow.h"
#include "visibilityWindow.h"
#include "GmshDefines.h"
#include "GmshMessage.h"
#include "GModel.h"
#include "MElement.h"
#include "PView.h"
#include "PViewOptions.h"
#include "Numeric.h"
#include "FlGui.h"
#include "OpenFile.h"
#include "drawContext.h"
#include "Context.h"
#include "Trackball.h"
#include "GamePad.h"

// Navigator handler (read gamepad event if gamepad exists or question presence
// of gamepad)
static void navigator_handler(void *data)
{
  openglWindow *gl_win = (openglWindow *)data;
  if(CTX::instance()->gamepad && CTX::instance()->gamepad->active) {
    if(gl_win->Nautilus == 0) {
      gl_win->Nautilus = new Navigator(CTX::instance()->gamepad->frequency,
                                       gl_win->getDrawContext());
    }
    gl_win->moveWithGamepad();
    Fl::add_timeout(CTX::instance()->gamepad->frequency, navigator_handler,
                    data);
  }
  else {
    if(gl_win->Nautilus) {
      delete gl_win->Nautilus;
      gl_win->Nautilus = 0;
    }
    Fl::add_timeout(3., navigator_handler, data);
  }
}

static void lassoZoom(drawContext *ctx, mousePosition &click1,
                      mousePosition &click2)
{
  if(click1.win[0] == click2.win[0] || click1.win[1] == click2.win[1]) return;

  ctx->s[0] *= (double)ctx->viewport[2] / (click2.win[0] - click1.win[0]);
  ctx->s[1] *= (double)ctx->viewport[3] / (click2.win[1] - click1.win[1]);
  ctx->s[2] = std::min(ctx->s[0], ctx->s[1]); // bof...

  // recenter around the center of the lasso rectangle
  mousePosition tmp(click1);
  tmp.wnr[0] = 0.5 * (click1.wnr[0] + click2.wnr[0]);
  tmp.wnr[1] = 0.5 * (click1.wnr[1] + click2.wnr[1]);
  tmp.recenter(ctx);

  ctx->initPosition(false);
  drawContext::global()->draw();
  FlGui::instance()->manip->update();
}

openglWindow::openglWindow(int x, int y, int w, int h)
  : Fl_Gl_Window(x, y, w, h, "gl"), _lock(false), _drawn(false),
    _selection(ENT_NONE), _trySelection(0), Nautilus(0)
{
  _ctx = new drawContext(this);

  for(int i = 0; i < 3; i++) _point[i] = 0.;
  for(int i = 0; i < 4; i++) _trySelectionXYWH[i] = 0;
  _lassoXY[0] = _lassoXY[1] = 0;

  addPointMode = 0;
  lassoMode = selectionMode = false;
  endSelection = undoSelection = invertSelection = quitSelection = 0;
  changeSelection = 0;

  if(CTX::instance()->gamepad)
    Fl::add_timeout(.5, navigator_handler, (void *)this);
}

openglWindow::~openglWindow()
{
  delete _ctx;
  if(Nautilus) delete Nautilus;
}

void openglWindow::show()
{
  Fl_Gl_Window::show();
  /* You can uncomment this if you cannot use the very latest FLTK 1.4 version
     patched for macOS mojave

#if defined(__APPLE__) && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14)
  Msg::Info("OpenGL hack for macOS 10.14: see http://www.fltk.org/str.php?L3496");
  resize(x(), y(), w()+1, h());
  resize(x(), y(), w()-1, h());
#endif

  */
}

void openglWindow::_drawScreenMessage()
{
  if(screenMessage[0].empty() && screenMessage[1].empty()) return;

  glColor4ubv((GLubyte *)&CTX::instance()->color.text);
  drawContext::global()->setFont(CTX::instance()->glFontEnum,
                                 CTX::instance()->glFontSize);
  double h = drawContext::global()->getStringHeight();

  if(screenMessage[0].size()) {
    const char *txt = screenMessage[0].c_str();
    double w = drawContext::global()->getStringWidth(txt);
    glRasterPos2d(_ctx->viewport[2] / 2. - w / 2., _ctx->viewport[3] - 1.2 * h);
    drawContext::global()->drawString(txt);
  }
  if(screenMessage[1].size()) {
    const char *txt = screenMessage[1].c_str();
    double w = drawContext::global()->getStringWidth(txt);
    glRasterPos2d(_ctx->viewport[2] / 2. - w / 2., _ctx->viewport[3] - 2.4 * h);
    drawContext::global()->drawString(txt);
  }
}

void openglWindow::_drawBorder()
{
  if(!parent()) return;
  // draw thin border if the parent group has more than 2 opengl windows
  int numgl = 0;
  for(int i = 0; i < parent()->children(); i++) {
    if(parent()->child(i)->label() &&
       !strcmp(parent()->child(i)->label(), label()))
      numgl++;
  }
  if(numgl < 2) return;

  unsigned char r, g, b;
  Fl::get_color(color(), r, g, b);
  /* would need to redraw all gl's when _lastHandled is changed
   if(_lastHandled == this)
     Fl::get_color(FL_SELECTION_COLOR, r, g, b);
   else
     Fl::get_color(FL_BACKGROUND_COLOR, r, g, b);
  */
  glColor3ub(r, g, b);
  glLineWidth(1.0F);
  glBegin(GL_LINE_LOOP);
  glVertex2d(_ctx->viewport[0], _ctx->viewport[1]);
  glVertex2d(_ctx->viewport[2], _ctx->viewport[1]);
  glVertex2d(_ctx->viewport[2], _ctx->viewport[3]);
  glVertex2d(_ctx->viewport[0], _ctx->viewport[3]);
  glEnd();
}

void openglWindow::draw()
{
  // some drawing routines can create data (STL triangulations, etc.): make sure
  // that we don't fire draw() while we are already drawing, e.g. due to an
  // impromptu Fl::check(). The same lock is also used in _select to guarantee
  // that we don't mix GL_RENDER and GL_SELECT rendering passes.
  _drawn = true;
  if(_lock) return;
  _lock = true;

  Msg::Debug("openglWindow::draw()");

  if(!context_valid()) {
    _ctx->invalidateQuadricsAndDisplayLists();
  }

  _ctx->viewport[0] = 0;
  _ctx->viewport[1] = 0;
  _ctx->viewport[2] = w();
  _ctx->viewport[3] = h();
  glViewport(0, 0, pixel_w(), pixel_h());

  if(lassoMode) {
    // draw the zoom or selection lasso on top of the current scene (without
    // using overlays!)
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho((double)_ctx->viewport[0], (double)_ctx->viewport[2],
            (double)_ctx->viewport[1], (double)_ctx->viewport[3], -1., 1.);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glColor3d(1., 1., 1.);
    glDisable(GL_DEPTH_TEST);
    glDrawBuffer(GL_FRONT_AND_BACK);
    if(selectionMode && CTX::instance()->mouseSelection) {
      glEnable(GL_LINE_STIPPLE);
      glLineStipple(1, 0x0F0F);
    }
    // glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO);
    glEnable(GL_BLEND);
    glLineWidth(0.2F);
    glBegin(GL_LINE_LOOP);
    glVertex2d(_click.win[0], _ctx->viewport[3] - _click.win[1]);
    glVertex2d(_lassoXY[0], _ctx->viewport[3] - _click.win[1]);
    glVertex2d(_lassoXY[0], _ctx->viewport[3] - _lassoXY[1]);
    glVertex2d(_click.win[0], _ctx->viewport[3] - _lassoXY[1]);
    glEnd();
    glBegin(GL_LINE_LOOP);
    glVertex2d(_click.win[0], _ctx->viewport[3] - _click.win[1]);
    glVertex2d(_curr.win[0], _ctx->viewport[3] - _click.win[1]);
    glVertex2d(_curr.win[0], _ctx->viewport[3] - _curr.win[1]);
    glVertex2d(_click.win[0], _ctx->viewport[3] - _curr.win[1]);
    glEnd();
    _lassoXY[0] = _curr.win[0];
    _lassoXY[1] = _curr.win[1];
    glDisable(GL_BLEND);
    glDisable(GL_LINE_STIPPLE);
    glEnable(GL_DEPTH_TEST);
    glDrawBuffer(GL_BACK);
  }
  else if(addPointMode) {
    // draw the whole scene and the point to add
    if(CTX::instance()->fastRedraw) {
      CTX::instance()->mesh.draw = 0;
      CTX::instance()->post.draw = 0;
    }

    glClearColor(
      (GLclampf)(CTX::instance()->unpackRed(CTX::instance()->color.bg) / 255.),
      (GLclampf)(CTX::instance()->unpackGreen(CTX::instance()->color.bg) /
                 255.),
      (GLclampf)(CTX::instance()->unpackBlue(CTX::instance()->color.bg) / 255.),
      0.0F);

    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

    _ctx->draw3d();
    glColor4ubv((GLubyte *)&CTX::instance()->color.geom.highlight[0]);
    float ps = CTX::instance()->geom.pointSize;
    if(_ctx->isHighResolution())
      ps *= CTX::instance()->highResolutionPointSizeFactor;
    glPointSize(ps);
    glBegin(GL_POINTS);
    glVertex3d(_point[0], _point[1], _point[2]);
    glEnd();
    _ctx->draw2d();
    _drawScreenMessage();
    _drawBorder();
    CTX::instance()->mesh.draw = 1;
    CTX::instance()->post.draw = 1;
  }
  else {
    // draw the whole scene
    if(CTX::instance()->printing && !CTX::instance()->print.background)
      glClearColor(1.0F, 1.0F, 1.0F, 0.0F);
    else
      glClearColor(
        (GLclampf)(CTX::instance()->unpackRed(CTX::instance()->color.bg) /
                   255.),
        (GLclampf)(CTX::instance()->unpackGreen(CTX::instance()->color.bg) /
                   255.),
        (GLclampf)(CTX::instance()->unpackBlue(CTX::instance()->color.bg) /
                   255.),
        0.0F);
    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

    if(CTX::instance()->camera && !CTX::instance()->stereo) {
      Camera *cam = &(_ctx->camera);
      if(!cam->on) cam->init();
      cam->giveViewportDimension(_ctx->viewport[2], _ctx->viewport[3]);
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();

      glFrustum(cam->glFleft, cam->glFright, cam->glFbottom, cam->glFtop,
                cam->glFnear, cam->glFfar * cam->Lc);

      glMatrixMode(GL_MODELVIEW);
      glLoadIdentity();
      glDrawBuffer(GL_BACK);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      glLoadIdentity();
      gluLookAt(cam->position.x, cam->position.y, cam->position.z,
                cam->target.x, cam->target.y, cam->target.z, cam->up.x,
                cam->up.y, cam->up.z);
      _ctx->draw3d();
      _ctx->draw2d();
      if(CTX::instance()->gamepad && CTX::instance()->gamepad->active &&
         Nautilus)
        Nautilus->drawIcons();
      _drawScreenMessage();
      _drawBorder();
    }
    else if(CTX::instance()->stereo) {
      Camera *cam = &(_ctx->camera);
      if(!cam->on) cam->init();
      cam->giveViewportDimension(_ctx->viewport[2], _ctx->viewport[3]);
      XYZ eye = cam->eyesep / 2.0 * cam->right;
      // right eye
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      double left =
        -cam->screenratio * cam->wd2 - 0.5 * cam->eyesep * cam->ndfl;
      double right =
        cam->screenratio * cam->wd2 - 0.5 * cam->eyesep * cam->ndfl;
      double top = cam->wd2;
      double bottom = -cam->wd2;
      glFrustum(left, right, bottom, top, cam->glFnear, cam->glFfar * cam->Lc);
      glMatrixMode(GL_MODELVIEW);
      glDrawBuffer(GL_BACK_RIGHT);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      glLoadIdentity();
      gluLookAt(cam->position.x + eye.x, cam->position.y + eye.y,
                cam->position.z + eye.z, cam->target.x + eye.x,
                cam->target.y + eye.y, cam->target.z + eye.z, cam->up.x,
                cam->up.y, cam->up.z);
      _ctx->draw3d();
      _ctx->draw2d();
      _drawScreenMessage();
      _drawBorder();
      // left eye
      glMatrixMode(GL_PROJECTION);
      glLoadIdentity();
      left = -cam->screenratio * cam->wd2 + 0.5 * cam->eyesep * cam->ndfl;
      right = cam->screenratio * cam->wd2 + 0.5 * cam->eyesep * cam->ndfl;
      top = cam->wd2;
      bottom = -cam->wd2;
      glFrustum(left, right, bottom, top, cam->glFnear, cam->glFfar * cam->Lc);

      glMatrixMode(GL_MODELVIEW);
      glDrawBuffer(GL_BACK_LEFT);
      glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      glLoadIdentity();
      gluLookAt(cam->position.x - eye.x, cam->position.y - eye.y,
                cam->position.z - eye.z, cam->target.x - eye.x,
                cam->target.y - eye.y, cam->target.z - eye.z, cam->up.x,
                cam->up.y, cam->up.z);
      _ctx->draw3d();
      _ctx->draw2d();
      _drawScreenMessage();
      _drawBorder();
    }
    else {
      _ctx->draw3d();
      _ctx->draw2d();
      _drawScreenMessage();
      _drawBorder();
    }
  }
  drawContext::global()->flushString();
  _lock = false;
}

openglWindow *openglWindow::_lastHandled = 0;

void openglWindow::_setLastHandled(openglWindow *w)
{
  _lastHandled = w;
  FlGui::instance()->visibility->updatePerWindow();
}

int openglWindow::handle(int event)
{
  switch(event) {
  case FL_FOCUS: // accept the focus when I'm asked if I want it
  case FL_UNFOCUS: return 1;

  case FL_SHORTCUT:
  case FL_KEYBOARD:
    // override the default widget arrow-key-navigation
    if(FlGui::instance()->testArrowShortcuts()) return 1;
    return Fl_Gl_Window::handle(event);

  case FL_PUSH:
    if(Fl::event_clicks() == 1 && !selectionMode) {
      // double-click and not in selection mode
      std::vector<GVertex *> vertices;
      std::vector<GEdge *> edges;
      std::vector<GFace *> faces;
      std::vector<GRegion *> regions;
      std::vector<MElement *> elements;
      std::vector<SPoint2> points;
      std::vector<PView *> views;
      _select(ENT_ALL, false, false, true, Fl::event_x(), Fl::event_y(), 5, 5,
              vertices, edges, faces, regions, elements, points, views);
      if(vertices.size() &&
         CTX::instance()->geom.doubleClickedPointCommand.size()) {
        CTX::instance()->geom.doubleClickedEntityTag = vertices[0]->tag();
        ParseString(CTX::instance()->geom.doubleClickedPointCommand, true);
      }
      else if(edges.size() &&
              CTX::instance()->geom.doubleClickedCurveCommand.size()) {
        CTX::instance()->geom.doubleClickedEntityTag = edges[0]->tag();
        ParseString(CTX::instance()->geom.doubleClickedCurveCommand, true);
      }
      else if(faces.size() &&
              CTX::instance()->geom.doubleClickedSurfaceCommand.size()) {
        CTX::instance()->geom.doubleClickedEntityTag = faces[0]->tag();
        ParseString(CTX::instance()->geom.doubleClickedSurfaceCommand, true);
      }
      else if(regions.size() &&
              CTX::instance()->geom.doubleClickedVolumeCommand.size()) {
        CTX::instance()->geom.doubleClickedEntityTag = regions[0]->tag();
        ParseString(CTX::instance()->geom.doubleClickedVolumeCommand, true);
      }
      else if(views.size() &&
              views[0]->getOptions()->doubleClickedCommand.size()) {
        CTX::instance()->post.doubleClickedView = views[0]->getIndex();
        ParseString(views[0]->getOptions()->doubleClickedCommand, true);
      }
      else if(points.size() &&
              CTX::instance()->post.doubleClickedGraphPointCommand.size()) {
        CTX::instance()->post.doubleClickedGraphPointX = points[0].x();
        CTX::instance()->post.doubleClickedGraphPointY = points[0].y();
        ParseString(CTX::instance()->post.doubleClickedGraphPointCommand, true);
      }
      else { // popup quick access menu
        status_options_cb(0, (void *)"quick_access");
      }
      Fl::event_clicks(-1);
      return 1;
    }
    _setLastHandled(this);
    take_focus(); // force keyboard focus when we click in the window
    _curr.set(_ctx, Fl::event_x(), Fl::event_y());
    if(Fl::event_button() == 1 && !Fl::event_state(FL_SHIFT) &&
       !Fl::event_state(FL_ALT)) {
      if(!lassoMode && Fl::event_state(FL_CTRL)) {
        lassoMode = true;
        _lassoXY[0] = _curr.win[0];
        _lassoXY[1] = _curr.win[1];
      }
      else if(lassoMode) {
        lassoMode = false;
        if(selectionMode && CTX::instance()->mouseSelection) {
          // will try to select multiple entities
          _trySelection = 2;
          _trySelectionXYWH[0] = (int)(_click.win[0] + _curr.win[0]) / 2;
          _trySelectionXYWH[1] = (int)(_click.win[1] + _curr.win[1]) / 2;
          _trySelectionXYWH[2] = (int)fabs(_click.win[0] - _curr.win[0]);
          _trySelectionXYWH[3] = (int)fabs(_click.win[1] - _curr.win[1]);
        }
        else {
          if(CTX::instance()->camera) {
            Camera *cam = &(_ctx->camera);
            double dy = fabs(-_click.win[1] + _curr.win[1]);
            double dx = fabs(-_click.win[0] + _curr.win[0]);
            double factx = w() / fabs(dx);
            double facty = h() / fabs(dy);
            double fact = .8 * std::min(factx, facty);
            double x_med = (_click.win[0] + _curr.win[0]) / 2.;
            double y_med = (_click.win[1] + _curr.win[1]) / 2.;
            double theta_x = .96 * cam->radians * (w() / 2 - x_med) * 2. / h();
            double theta_y = .96 * cam->radians * (h() / 2 - y_med) * 2. / h();
            cam->moveRight(theta_x);
            cam->moveUp(theta_y);
            _ctx->camera.zoom(fact);
            _ctx->camera.update();
            redraw();
          }
          else {
            lassoZoom(_ctx, _click, _curr);
          }
        }
      }
      else if(CTX::instance()->mouseSelection) {
        // will try to select clicked entity
        _trySelection = 1;
        _trySelectionXYWH[0] = (int)_curr.win[0];
        _trySelectionXYWH[1] = (int)_curr.win[1];
        _trySelectionXYWH[2] = 5;
        _trySelectionXYWH[3] = 5;
      }
    }
    else if(Fl::event_button() == 2 ||
            (Fl::event_button() == 1 && Fl::event_state(FL_SHIFT))) {
      if(!lassoMode && Fl::event_state(FL_CTRL)) {
        // make zoom isotropic
        _ctx->s[1] = _ctx->s[0];
        _ctx->s[2] = _ctx->s[0];
        redraw();
      }
      else if(lassoMode) {
        lassoMode = false;
        if(selectionMode && CTX::instance()->mouseSelection) {
          // will try to unselect multiple entities
          _trySelection = -2;
          _trySelectionXYWH[0] = (int)(_click.win[0] + _curr.win[0]) / 2;
          _trySelectionXYWH[1] = (int)(_click.win[1] + _curr.win[1]) / 2;
          _trySelectionXYWH[2] = (int)fabs(_click.win[0] - _curr.win[0]);
          _trySelectionXYWH[3] = (int)fabs(_click.win[1] - _curr.win[1]);
        }
        else {
          lassoZoom(_ctx, _click, _curr);
        }
      }
      else if(CTX::instance()->mouseSelection) {
        // will try to unselect clicked entity
        _trySelection = -1;
        _trySelectionXYWH[0] = (int)_curr.win[0];
        _trySelectionXYWH[1] = (int)_curr.win[1];
        _trySelectionXYWH[2] = 5;
        _trySelectionXYWH[3] = 5;
      }
    }
    else {
      if(Fl::event_state(FL_CTRL) && !lassoMode) {
        if(CTX::instance()->useTrackball)
          _ctx->setQuaternion(0., 0., 0., 1.);
        else
          _ctx->r[0] = _ctx->r[1] = _ctx->r[2] = 0.;
        _ctx->t[0] = _ctx->t[1] = _ctx->t[2] = 0.;
        _ctx->s[0] = _ctx->s[1] = _ctx->s[2] = 1.;
        redraw();
      }
      else {
        lassoMode = false;
      }
    }
    _click.set(_ctx, Fl::event_x(), Fl::event_y());
    _prev.set(_ctx, Fl::event_x(), Fl::event_y());
    FlGui::instance()->manip->update();
    return 1;

  case FL_RELEASE:
    _curr.set(_ctx, Fl::event_x(), Fl::event_y());
    CTX::instance()->drawRotationCenter = 0;
    if(!lassoMode) {
      CTX::instance()->mesh.draw = 1;
      CTX::instance()->post.draw = 1;
      redraw();
    }
    _prev.set(_ctx, Fl::event_x(), Fl::event_y());
    return 1;

  case FL_MOUSEWHEEL: {
    _prev.set(_ctx, Fl::event_x(), Fl::event_y());
    double dy = Fl::event_dy();
    double fact =
      (5. * CTX::instance()->zoomFactor * fabs(dy) + h()) / (double)h();
    bool direction = (CTX::instance()->mouseInvertZoom) ? (dy <= 0) : (dy > 0);
    if(CTX::instance()->camera) {
      fact = (direction ? fact : 1. / fact);
      _ctx->camera.zoom(fact);
      _ctx->camera.update();
      redraw();
    }
    else {
      _ctx->s[0] *= (direction ? fact : 1. / fact);
      _ctx->s[1] = _ctx->s[0];
      _ctx->s[2] = _ctx->s[0];
      _prev.recenter(_ctx);
      redraw();
    }
  }
    FlGui::instance()->manip->update();
    return 1;

  case FL_DRAG:
    _curr.set(_ctx, Fl::event_x(), Fl::event_y());
    {
      double dx = _curr.win[0] - _prev.win[0];
      double dy = _curr.win[1] - _prev.win[1];
      if(lassoMode) {
        redraw();
      }
      else {
        if(Fl::event_state(FL_META)) {
          // will try to select or unselect entities on the fly
          _trySelection = Fl::event_state(FL_SHIFT) ? -1 : 1;
          _trySelectionXYWH[0] = (int)_curr.win[0];
          _trySelectionXYWH[1] = (int)_curr.win[1];
          _trySelectionXYWH[2] = 5;
          _trySelectionXYWH[3] = 5;
        }
        // (m1) and (!shift) and (!alt)  => rotation
        else if(Fl::event_button() == 1 && !Fl::event_state(FL_SHIFT) &&
                !Fl::event_state(FL_ALT)) {
          if(CTX::instance()->useTrackball)
            _ctx->addQuaternion(
              (2. * _prev.win[0] - w()) / w(), (h() - 2. * _prev.win[1]) / h(),
              (2. * _curr.win[0] - w()) / w(), (h() - 2. * _curr.win[1]) / h());
          else {
            _ctx->r[1] +=
              ((fabs(dx) > fabs(dy)) ? 180. * dx / (double)w() : 0.);
            _ctx->r[0] +=
              ((fabs(dx) > fabs(dy)) ? 0. : 180. * dy / (double)h());
          }
        }
        // m2 or (m1 and shift) => zoom (only move in y is used)
        // but start point is the center of the homothety
        else if(Fl::event_button() == 2 ||
                (Fl::event_button() == 1 && Fl::event_state(FL_SHIFT))) {
          // isotrop zoom in camera mode
          if(CTX::instance()->camera) {
            double dy = (int)_curr.win[1] - (int)_prev.win[1];
            double fact =
              (CTX::instance()->zoomFactor * fabs(dy) + (double)h()) /
              (double)h();
            fact = ((dy > 0) ? fact : 1. / fact);
            _ctx->camera.zoom(fact);
            _ctx->camera.update();
            redraw();
          }
          else {
            // move in y greater than move in x
            if(fabs(dy) > fabs(dx)) {
              double fact =
                (CTX::instance()->zoomFactor * fabs(dy) + h()) / (double)h();
              _ctx->s[0] *= ((dy > 0) ? fact : 1. / fact);
              _ctx->s[1] = _ctx->s[0];
              _ctx->s[2] = _ctx->s[0];
              _click.recenter(_ctx);
            }
            else if(!CTX::instance()->useTrackball)
              _ctx->r[2] += -180. * dx / (double)w();
          }
        }
        // other case => translation
        else {
          if(CTX::instance()->camera) {
            Camera *cam = &(_ctx->camera);
            double theta_x = cam->radians *
                             (-(double)_prev.win[0] + (double)_curr.win[0]) *
                             2. / h();
            double theta_y = cam->radians *
                             (-(double)_prev.win[1] + (double)_curr.win[1]) *
                             2. / h();
            cam->moveRight(theta_x);
            cam->moveUp(theta_y);
          }
          else {
            _ctx->t[0] += (_curr.wnr[0] - _click.wnr[0]);
            _ctx->t[1] += (_curr.wnr[1] - _click.wnr[1]);
            _ctx->t[2] = 0.;
          }
        }
        CTX::instance()->drawRotationCenter = 1;
        if(CTX::instance()->fastRedraw) {
          CTX::instance()->mesh.draw = 0;
          CTX::instance()->post.draw = 0;
        }
        redraw();
      }
    }
    _prev.set(_ctx, Fl::event_x(), Fl::event_y());
    FlGui::instance()->manip->update();
    return 1;

  case FL_MOVE:
    _curr.set(_ctx, Fl::event_x(), Fl::event_y());
    if(lassoMode) {
      redraw();
    }
    else if(addPointMode && !Fl::event_state(FL_SHIFT)) {
      cursor(FL_CURSOR_CROSS, FL_BLACK, FL_WHITE);
      // find line in real space corresponding to current cursor position
      double p[3], d[3];
      _ctx->unproject(_curr.win[0], _curr.win[1], p, d);
      // find closest point to the center of gravity
      double r[3] = {CTX::instance()->cg[0] - p[0],
                     CTX::instance()->cg[1] - p[1],
                     CTX::instance()->cg[2] - p[2]},
             t;
      t = prosca(r, d);
      for(int i = 0; i < 3; i++) {
        if(!FlGui::instance()->elementaryContext->frozenPointCoord(i)) {
          _point[i] = p[i] + t * d[i];
          if(CTX::instance()->geom.snap[i]) {
            double d = _point[i] / CTX::instance()->geom.snap[i];
            double f = floor(d);
            double c = ceil(d);
            double n = (d - f < c - d) ? f : c;
            _point[i] = n * CTX::instance()->geom.snap[i];
          }
        }
      }
      FlGui::instance()->elementaryContext->updatePoint(_point, addPointMode);
      redraw();
    }
    else { // hover mode
      if(_curr.win[0] != _prev.win[0] || _curr.win[1] != _prev.win[1]) {
        std::vector<GVertex *> vertices;
        std::vector<GEdge *> edges;
        std::vector<GFace *> faces;
        std::vector<GRegion *> regions;
        std::vector<MElement *> elements;
        std::vector<SPoint2> points;
        std::vector<PView *> views;
        bool res = _select(_selection, false, CTX::instance()->mouseHoverMeshes,
                           CTX::instance()->mouseHoverMeshes, (int)_curr.win[0],
                           (int)_curr.win[1], 5, 5, vertices, edges, faces,
                           regions, elements, points, views);
        if((_selection == ENT_ALL && res) ||
           (_selection == ENT_POINT && vertices.size()) ||
           (_selection == ENT_CURVE && edges.size()) ||
           (_selection == ENT_SURFACE && faces.size()) ||
           (_selection == ENT_VOLUME && regions.size()))
          cursor(FL_CURSOR_CROSS, FL_BLACK, FL_WHITE);
        else
          cursor(FL_CURSOR_DEFAULT, FL_BLACK, FL_WHITE);
        std::string text, cmd;
        bool multiline = CTX::instance()->tooltips;
        if(vertices.size()) {
          text = vertices[0]->getInfoString(true, multiline);
          cmd = CTX::instance()->geom.doubleClickedPointCommand;
        }
        else if(edges.size()) {
          text = edges[0]->getInfoString(true, multiline);
          cmd = CTX::instance()->geom.doubleClickedCurveCommand;
        }
        else if(faces.size()) {
          text = faces[0]->getInfoString(true, multiline);
          cmd = CTX::instance()->geom.doubleClickedSurfaceCommand;
        }
        else if(regions.size()) {
          text = regions[0]->getInfoString(true, multiline);
          cmd = CTX::instance()->geom.doubleClickedVolumeCommand;
        }
        else if(elements.size()) {
          text = elements[0]->getInfoString(multiline);
        }
        else if(points.size()) {
          char tmp[256];
          sprintf(tmp, "Point (%g, %g)", points[0].x(), points[0].y());
          text = tmp;
          cmd = CTX::instance()->post.doubleClickedGraphPointCommand;
        }
        else if(views.size()) {
          char tmp[256];
          sprintf(tmp, "View[%d]", views[0]->getIndex());
          text = tmp;
          cmd = views[0]->getOptions()->doubleClickedCommand;
        }
        if(cmd.size()) {
          text += std::string(" - Double-click to execute\n\n");
          std::replace(cmd.begin(), cmd.end(), '\r', ' ');
          text += cmd;
        }
        if(CTX::instance()->tooltips)
          drawTooltip(text);
        else
          Msg::StatusBar(false, text.c_str());
      }
    }
    _prev.set(_ctx, Fl::event_x(), Fl::event_y());
    return 1;

  default: return Fl_Gl_Window::handle(event);
  }
}

int openglWindow::pixel_w()
{
#if(FL_MAJOR_VERSION == 1) && (FL_MINOR_VERSION == 3) && (FL_PATCH_VERSION >= 4)
  return Fl_Gl_Window::pixel_w();
#elif(FL_MAJOR_VERSION == 1) && (FL_MINOR_VERSION >= 4)
  return Fl_Gl_Window::pixel_w();
#else
  return w();
#endif
}

int openglWindow::pixel_h()
{
#if(FL_MAJOR_VERSION == 1) && (FL_MINOR_VERSION == 3) && (FL_PATCH_VERSION >= 4)
  return Fl_Gl_Window::pixel_h();
#elif(FL_MAJOR_VERSION == 1) && (FL_MINOR_VERSION >= 4)
  return Fl_Gl_Window::pixel_h();
#else
  return h();
#endif
}

bool openglWindow::_select(
  int type, bool multiple, bool mesh, bool post, int x, int y, int w, int h,
  std::vector<GVertex *> &vertices, std::vector<GEdge *> &edges,
  std::vector<GFace *> &faces, std::vector<GRegion *> &regions,
  std::vector<MElement *> &elements, std::vector<SPoint2> &points,
  std::vector<PView *> &views)
{
  // same lock as in draw() to prevent firing up a GL_SELECT rendering pass
  // while a GL_RENDER pass is happening (due to the asynchronus nature of
  // Fl::check()s
  if(_lock) return false;
  _lock = true;
  make_current();
  bool ret = _ctx->select(type, multiple, mesh, post, x, y, w, h, vertices,
                          edges, faces, regions, elements, points, views);
  _lock = false;
  return ret;
}

char openglWindow::selectEntity(int type, std::vector<GVertex *> &vertices,
                                std::vector<GEdge *> &edges,
                                std::vector<GFace *> &faces,
                                std::vector<GRegion *> &regions,
                                std::vector<MElement *> &elements,
                                std::vector<SPoint2> &points,
                                std::vector<PView *> &views)
{
  // force keyboard focus in GL window
  take_focus();
  _selection = type;
  _trySelection = 0;
  selectionMode = true;
  quitSelection = 0;
  changeSelection = 0;
  endSelection = 0;
  undoSelection = 0;
  invertSelection = 0;

  while(1) {
    vertices.clear();
    edges.clear();
    faces.clear();
    regions.clear();
    elements.clear();
    FlGui::instance()->wait();
    if(changeSelection) {
      Msg::Debug("Changing selection mode to %d", changeSelection);
      _selection = changeSelection;
      changeSelection = 0;
    }
    if(quitSelection) {
      _selection = ENT_NONE;
      selectionMode = false;
      lassoMode = false;
      addPointMode = 0;
      cursor(FL_CURSOR_DEFAULT, FL_BLACK, FL_WHITE);
      return 'q';
    }
    if(endSelection) {
      _selection = ENT_NONE;
      endSelection = 0;
      return 'e';
    }
    if(undoSelection) {
      undoSelection = 0;
      return 'u';
    }
    if(invertSelection) {
      invertSelection = 0;
      return 'i';
    }
    if(_trySelection) {
      bool add = (_trySelection > 0) ? true : false;
      bool multi = (abs(_trySelection) > 1) ? true : false;
      _trySelection = 0;
      if(_selection == ENT_NONE) { // just report the mouse click
        selectionMode = false;
        return 'c';
      }
      else if(_select(_selection, multi, true, true, _trySelectionXYWH[0],
                      _trySelectionXYWH[1], _trySelectionXYWH[2],
                      _trySelectionXYWH[3], vertices, edges, faces, regions,
                      elements, points, views)) {
        _selection = ENT_NONE;
        selectionMode = false;
        if(add)
          return 'l';
        else
          return 'r';
      }
    }
  }
}

void openglWindow::drawTooltip(const std::string &text)
{
  static char str[1024];
  strncpy(str, text.c_str(), sizeof(str));
  Fl_Tooltip::exit(0);
  bool enabled = Fl_Tooltip::enabled();
  if(!enabled) Fl_Tooltip::enable();
  double d1 = Fl_Tooltip::delay();
  double d2 = Fl_Tooltip::hoverdelay();
  Fl_Tooltip::delay(0);
  Fl_Tooltip::hoverdelay(0);
  Fl_Tooltip::enter_area(this, _curr.win[0], _curr.win[1], 100, 50, str);
  Fl_Tooltip::delay(d1);
  Fl_Tooltip::hoverdelay(d2);
  if(!enabled) Fl_Tooltip::disable();
}

void openglWindow::moveWithGamepad()
{
  if(CTX::instance()->gamepad && CTX::instance()->gamepad->active && Nautilus) {
    if(!(_ctx->camera.on)) _ctx->camera.init();
    if(_drawn && (_lastHandled == this || _lastHandled == 0)) {
      Nautilus->move();
      this->flush();
    }
  }
}
