Home Reference Source

scripts/path-controller/util/simple_events.js

import * as util from "./util.js";
import cconst from '../config/config.js';
import {Vector2} from './vectormath.js';

export let modalstack = [];
let singleMouseCBs = {};

function debugDomEvents() {
  let cbsymbol = Symbol("event-callback");
  let thsymbol = Symbol("debug-info");
  
  let idgen = 0;
  
  function init(et) {
    if (!et[thsymbol]) {
      et[thsymbol] = idgen++;
    }
  }
  
  function getkey(et, type, options) {
    init(et);
    return "" + et[thsymbol] + ":" + type + ":" + JSON.stringify(options);
  }
  
  let addEventListener = EventTarget.prototype.addEventListener;
  let removeEventListener = EventTarget.prototype.removeEventListener;
  
  EventTarget.prototype.addEventListener = function(type, cb, options) {
    init(this);
    
    if (!cb[cbsymbol]) {
      cb[cbsymbol] = new Set();
    }
    
    let key = getkey(this, type, options);
    cb[cbsymbol].add(key);
    
    return addEventListener.call(this, type, cb, options);
  }
  
  EventTarget.prototype.removeEventListener = function(type, cb, options) {
    init(this);
    
    if (!cb[cbsymbol]) {
      console.error("Invalid callback in removeEventListener for", type, this, cb);
      return;
    }
    
    let key = getkey(this, type, options);
    
    if (!cb[cbsymbol].has(key)) {
      console.error("Callback not in removeEventListener;", type, this, cb);
      return;
    }
    
    cb[cbsymbol].delete(key);
    
    return removeEventListener.call(this, type, cb, options);
  }
}

//debugDomEvents();

function singletonMouseEvents() {
  let keys = ["mousedown", "mouseup", "mousemove"];
  for (let k of keys) {
    singleMouseCBs[k] = new Set();
  }

  let ddd = -1.0;
  window.testSingleMouseUpEvent = (type = "mousedown") => {
    let id = ddd++;
    singleMouseEvent(() => {
      console.log("mouse event", id);
    }, type)
  };

  let _mpos = new Vector2();

  function doSingleCbs(e, type) {
    let list = singleMouseCBs[type];
    singleMouseCBs[type] = new Set();

    if (e.type !== "touchend" && e.type !== "touchcancel") {
      _mpos[0] = e.touches && e.touches.length > 0 ? e.touches[0].pageX : e.x;
      _mpos[1] = e.touches && e.touches.length > 0 ? e.touches[0].pageY : e.y;
    }

    if (e.touches) {
      e = copyEvent(e);

      e.type = type;
      if (e.touches.length > 0) {
        e.x = e.pageX = e.touches[0].pageX;
        e.y = e.pageY = e.touches[0].pageY;
      } else {
        e.x = _mpos[0];
        e.y = _mpos[1];
      }
    }

    for (let cb of list) {
      try {
        cb(e);
      } catch (error) {
        util.print_stack(error);
        console.warn("Error in event callback");
      }
    }
  }

  window.addEventListener("mouseup", (e) => {
    doSingleCbs(e, "mouseup");
  }, {capture: true});
  window.addEventListener("touchcancel", (e) => {
    doSingleCbs(e, "mouseup");
  }, {capture: true});
  document.addEventListener("touchend", (e) => {
    doSingleCbs(e, "mouseup");
  }, {capture: true});

  document.addEventListener("mousedown", (e) => {
    doSingleCbs(e, "mousedown");
  }, {capture: true});
  document.addEventListener("touchstart", (e) => {
    doSingleCbs(e, "mousedown");
  }, {capture: true});

  document.addEventListener("mousemove", (e) => {
    doSingleCbs(e, "mousemove");
  }, {capture: true});
  document.addEventListener("touchmove", (e) => {
    doSingleCbs(e, "mousemove");
  }, {capture: true});

  return {
    singleMouseEvent(cb, type) {
      if (!(type in singleMouseCBs)) {
        throw new Error("not a mouse event");
      }

      singleMouseCBs[type].add(cb);
    }
  };
};

singletonMouseEvents = singletonMouseEvents();

/**
 * adds a mouse event callback that only gets called once
 * */
export function singleMouseEvent(cb, type) {
  return singletonMouseEvents.singleMouseEvent(cb, type);
}


/*tests if either the left mouse button is down,
* or a touch event has happened and e.touches.length == 1*/
export function isLeftClick(e) {
  if (e.touches !== undefined) {
    return e.touches.length === 1;
  }

  return e.button === 0;
}

export class DoubleClickHandler {
  constructor() {
    this.down = 0;
    this.last = 0;
    this.dblEvent = undefined;

    this.start_mpos = new Vector2();

    this._on_mouseup = this._on_mouseup.bind(this);
    this._on_mousemove = this._on_mousemove.bind(this);
  }

  _on_mouseup(e) {
    //console.log("mup", e);
    this.mdown = false;
  }

  _on_mousemove(e) {
    let mpos = new Vector2();
    mpos[0] = e.x;
    mpos[1] = e.y;

    let dist = mpos.vectorDistance(this.start_mpos)*devicePixelRatio;

    if (dist > 11) {
      //console.log("cancel", dist);
      this.mdown = false;
    }

    if (this.mdown) {
      singleMouseEvent(this._on_mousemove, "mousemove");
    }

    this.update();
  }

  mousedown(e) {
    //console.log("mdown", e.x, e.y);

    if (!this.last) {
      this.last = 0;
    }
    if (!this.down) {
      this.down = 0;
    }
    if (!this.up) {
      this.up = 0;
    }

    if (isMouseDown(e)) {
      this.mdown = true;

      let cpy = Object.assign({}, e);

      this.start_mpos[0] = e.x;
      this.start_mpos[1] = e.y;

      singleMouseEvent(this._on_mousemove, "mousemove");

      if (e.type.search("touch") >= 0 && e.touches.length > 0) {
        cpy.x = cpy.pageX = e.touches[0].pageX;
        cpy.y = cpy.pageY = e.touches[1].pageY;
      } else {
        cpy.x = cpy.pageX = e.x;
        cpy.y = cpy.pageY = e.y;
      }

      //stupid real MouseEvent class zeros .x/.y
      //continue using hackish copyEvent for now...

      this.dblEvent = copyEvent(e);
      this.dblEvent.type = "dblclick";

      this.last = this.down;
      this.down = util.time_ms();

      if (this.down - this.last < cconst.doubleClickTime) {
        this.mdown = false;
        this.ondblclick(this.dblEvent);

        this.down = this.last = 0.0;
      } else {
        singleMouseEvent(this._on_mouseup, "mouseup");
      }
    } else {
      this.mdown = false;
    }
  }

  //you may override this
  ondblclick(e) {

  }

  update() {
    if (modalstack.length > 0) {
      //cancel double click requests
      this.mdown = false;
    }

    if (this.mdown && util.time_ms() - this.down > cconst.doubleClickHoldTime) {
      this.mdown = false;
      this.ondblclick(this.dblEvent);
    }
  }

  abort() {
    this.last = this.down = 0;
  }
}

export function isMouseDown(e) {
  let mdown = 0;

  if (e.touches !== undefined) {
    mdown = e.touches.length > 0;
  } else {
    mdown = e.buttons;
  }

  mdown = mdown & 1;

  return mdown;
}

export function pathDebugEvent(e, extra) {
  e.__prevdef = e.preventDefault;
  e.__stopprop = e.stopPropagation;

  e.preventDefault = function () {
    console.warn("preventDefault", extra);
    return this.__prevdef();
  };

  e.stopPropagation = function () {
    console.warn("stopPropagation", extra);
    return this.__stopprop();
  }
}

/** Returns true if event came from a touchscreen or pen device */
export function eventWasTouch(e) {
  let ret = e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents;
  ret = ret || e.was_touch;
  ret = ret || e instanceof TouchEvent;
  ret = ret || e.touches !== undefined;

  if (e instanceof PointerEvent) {
    ret = ret || (e.pointerType === "pen" || e.pointerType === "touch");
  }

  return ret;
}

export function copyEvent(e) {
  let ret = {};
  let keys = [];

  for (let k in e) {
    keys.push(k);
  }

  keys = keys.concat(Object.getOwnPropertySymbols(e));
  keys = keys.concat(Object.getOwnPropertyNames(e));

  for (let k of keys) {
    let v;

    try {
      v = e[k];
    } catch (error) {
      console.warn("read error for event key", k);
      continue;
    }

    if (typeof v == "function") {
      ret[k] = v.bind(e);
    } else {
      ret[k] = v;
    }
  }

  ret.original = e;

  return ret;
}

let Screen;

export function _setScreenClass(cls) {
  Screen = cls;
}

function findScreen() {
  let rec = (n) => {
    for (let n2 of n.childNodes) {
      if (n2 && typeof n2 === "object" && n2 instanceof Screen) {
        return n2;
      }
    }

    for (let n2 of n.childNodes) {
      let ret = rec(n2);
      if (ret) {
        return ret;
      }
    }
  };

  return rec(document.body);
}

window._findScreen = findScreen;

let ContextAreaClass;

export function _setModalAreaClass(cls) {
  ContextAreaClass = cls;
}

export function pushPointerModal(obj, elem, pointerId, autoStopPropagation = true) {
  return pushModalLight(obj, autoStopPropagation, elem, pointerId);
}

export function pushModalLight(obj, autoStopPropagation = true, elem, pointerId) {
  let keys;

  if (pointerId === undefined) {
    keys = new Set([
      "keydown", "keyup", "keypress", "mousedown", "mouseup", "touchstart", "touchend",
      "touchcancel", "mousewheel", "mousemove", "mouseover", "mouseout", "mouseenter",
      "mouseleave", "dragstart", "drag", "dragend", "dragexit", "dragleave", "dragover",
      "dragenter", "drop", "pointerdown", "pointermove", "pointerup", "pointercancel",
      "pointerstart", "pointerend", "pointerleave", "pointerexit", "pointerenter",
      "pointerover"
    ]);
  } else {
    keys = new Set([
      "keydown", "keyup", "keypress", "mousewheel"
    ]);
  }

  let ret = {
    keys     : keys,
    handlers : {},
    last_mpos: [0, 0]
  };

  let touchmap = {
    "touchstart" : "mousedown",
    "touchmove"  : "mousemove",
    "touchend"   : "mouseup",
    "touchcancel": "mouseup"
  };

  let mpos = [0, 0];

  let screen = findScreen();
  if (screen) {
    mpos[0] = screen.mpos[0];
    mpos[1] = screen.mpos[1];
    screen = undefined;
  }

  function handleAreaContext() {
    let screen = findScreen();
    if (screen) {
      let sarea = screen.findScreenArea(mpos[0], mpos[1]);
      if (sarea && sarea.area) {
        sarea.area.push_ctx_active();
        sarea.area.pop_ctx_active();
      }
    }
  }

  function make_default_touchhandler(type, state) {
    return function (e) {
      //console.warn("touch event!", type, touchmap[type], e.touches.length);
      if (cconst.DEBUG.domEvents) {
        pathDebugEvent(e);
      }

      if (touchmap[type] in ret.handlers) {
        let type2 = touchmap[type];

        let e2 = copyEvent(e);

        e2.was_touch = true;
        e2.type = type2;
        e2.button = type == "touchcancel" ? 1 : 0;
        e2.touches = e.touches;

        if (e.touches.length > 0) {
          let dpi = window.devicePixelRatio; //UIBase.getDPI();
          let t = e.touches[0];

          mpos[0] = t.pageX;
          mpos[1] = t.pageY;

          e2.pageX = e2.x = t.pageX;// * dpi;
          e2.pageY = e2.y = t.pageY;// * dpi;
          e2.clientX = t.clientX;// * dpi;
          e2.clientY = t.clientY;// * dpi;
          e2.x = t.clientX;// * dpi;
          e2.y = t.clientY;// * dpi;

          ret.last_mpos[0] = e2.x;
          ret.last_mpos[1] = e2.y;
        } else {
          e2.x = e2.clientX = e2.pageX = e2.screenX = ret.last_mpos[0];
          e2.y = e2.clientY = e2.pageY = e2.screenY = ret.last_mpos[1];
        }

        e2.was_touch = true;

        handleAreaContext();
        //console.log(e2.x, e2.y);
        ret.handlers[type2](e2);
      }

      if (autoStopPropagation) {
        e.preventDefault();
        e.stopPropagation();
      }
    }
  }

  function make_handler(type, key) {
    return function (e) {
      if (cconst.DEBUG.domEvents) {
        pathDebugEvent(e);
      }

      if (typeof key !== "string") {
        //console.warn("key was undefined", key, type);
        return;
      }

      if (key.startsWith("mouse")) {
        mpos[0] = e.pageX;
        mpos[1] = e.pageY;
      }

      handleAreaContext();

      if (key !== undefined)
        obj[key](e);

      if (autoStopPropagation) {
        e.preventDefault();
        e.stopPropagation();
      }
    }
  }

  let found = {};

  for (let k of keys) {
    let key;

    if (obj[k])
      key = k;
    else if (obj["on" + k])
      key = "on" + k;
    else if (obj["on_" + k])
      key = "on_" + k;
    else if (k in touchmap)
      continue; //default touch event handlers will be done seperately
    else
      key = undefined; //make handler that still blocks events

    //check we don't override other mouse pointer event handlers
    if (key === undefined && k.search("pointer") === 0) {
      continue;
    }

    if (key !== undefined) {
      found[k] = 1;
    }

    let handler = make_handler(k, key);
    ret.handlers[k] = handler;

    let settings = handler.settings = {passive: false, capture: true};
    window.addEventListener(k, handler, settings);
  }

  for (let k in touchmap) {
    if (!(k in found)) {
      //console.log("making touch handler for", '"' + k + '"', ret.handlers[k]);

      ret.handlers[k] = make_default_touchhandler(k, ret);

      let settings = ret.handlers[k].settings = {passive: false, capture: true};
      window.addEventListener(k, ret.handlers[k], settings);
    }
  }

  if (pointerId !== undefined) {
    ret.pointer = {
      elem, pointerId
    };

    function make_pointer(k) {
      let k2 = "on_" + k;

      ret.pointer[k] = function (e) {
        if (obj[k2] !== undefined) {
          obj[k2](e);
        }

        if (autoStopPropagation) {
          e.stopPropagation();
          e.preventDefault();
        }
      }
    }

    make_pointer("pointerdown");
    make_pointer("pointermove");
    make_pointer("pointerup");
    make_pointer("pointerstart");
    make_pointer("pointerend");
    make_pointer("pointerleave");
    make_pointer("pointerenter");
    make_pointer("pointerout");
    make_pointer("pointerover");
    make_pointer("pointerexit");
    make_pointer("pointercancel");

    for (let k in ret.pointer) {
      if (k !== "elem" && k !== "pointerId") {
        elem.addEventListener(k, ret.pointer[k]);
      }
    }

    try {
      elem.setPointerCapture(pointerId)
    } catch (error) {
      util.print_stack(error);

      console.log("attempting fallback");
      
      for (let k in ret.pointer) {
        if (k !== "elem" && k !== "pointerId") {
          elem.removeEventListener(k, ret.pointer[k]);
        }
      }

      delete ret.pointer;

      modalstack.push(ret);
      popModalLight(ret);

      for (let k in obj) {
        if (k === "pointercancel" || k === "pointerend" || k === "pointerstart") {
          continue;
        }

        if (k.startsWith("pointer")) {
          let k2 = k.replace(/pointer/, "mouse");
          if (k2 in obj) {
            console.warn("warning, existing mouse handler", k2);
            continue;
          }

          let v = obj[k];
          obj[k] = undefined;

          obj[k2] = v;
        }
      }

      console.log(obj);

      return pushModalLight(obj, autoStopPropagation);
    }
  }

  modalstack.push(ret);
  ContextAreaClass.lock();

  if (cconst.DEBUG.modalEvents) {
    console.warn("pushModalLight", ret.pointer ? "(pointer events)" : "");
  }

  return ret;
}

if (0) {
  let addevent = EventTarget.prototype.addEventListener;
  let remevent = EventTarget.prototype.removeEventListener;

  const funckey = Symbol("eventfunc");

  EventTarget.prototype.addEventListener = function (name, func, args) {
    //if (name.startsWith("key")) {
    console.warn("listener added", name, func, args);
    //}

    let func2 = function (e) {
      let proxy = new Proxy(e, {
        get(target, p, receiver) {
          if (p === "preventDefault") {
            return function () {
              console.warn("preventDefault", name, arguments);
              return e.preventDefault(...arguments);
            }
          } else if (p === "stopPropagation") {
            return function () {
              console.warn("stopPropagation", name, arguments);
              return e.preventDefault(...arguments);
            }
          }

          return e[p];
        }
      });

      return func.call(this, proxy);
    }

    func[funckey] = func2;

    return addevent.call(this, name, func2, args);
  }

  EventTarget.prototype.removeEventListener = function (name, func, args) {
    //if (name.startsWith("key")) {
    console.warn("listener removed", name, func, args);
    //}

    func = func[funckey];

    return remevent.call(this, name, func, args);
  }
}

export function popModalLight(state) {
  if (state === undefined) {
    console.warn("Bad call to popModalLight: state was undefined");
    return;
  }

  if (state !== modalstack[modalstack.length - 1]) {
    if (modalstack.indexOf(state) < 0) {
      console.warn("Error in popModalLight; modal handler not found");
      return;
    } else {
      console.warn("Error in popModalLight; called in wrong order");
    }
  }

  for (let k in state.handlers) {
    //console.log(k);
    window.removeEventListener(k, state.handlers[k], state.handlers[k].settings);
  }

  state.handlers = {};
  modalstack.remove(state);
  ContextAreaClass.unlock();

  if (cconst.DEBUG.modalEvents) {
    console.warn("popModalLight", modalstack, state.pointer ? "(pointer events)" : "");
  }

  if (state.pointer) {
    let elem = state.pointer.elem;

    try {
      elem.releasePointerCapture(state.pointer.pointerId);
    } catch (error) {
      util.print_stack(error);
    }

    for (let k in state.pointer) {
      if (k !== "elem" && k !== "pointerId") {
        elem.removeEventListener(k, state.pointer[k]);
      }
    }
  }
}

export function haveModal() {
  return modalstack.length > 0;
}

window._haveModal = haveModal; //for debugging console

export var keymap_latin_1 = {
  "Space" : 32,
  "Escape": 27,
  "Enter" : 13,
  "Return": 13,
  "Up"    : 38,
  "Down"  : 40,
  "Left"  : 37,
  "Right" : 39,

  "Num0"     : 96,
  "Num1"     : 97,
  "Num2"     : 98,
  "Num3"     : 99,
  "Num4"     : 100,
  "Num5"     : 101,
  "Num6"     : 102,
  "Num7"     : 103,
  "Num8"     : 104,
  "Num9"     : 105,
  "Home"     : 36,
  "End"      : 35,
  "Delete"   : 46,
  "Backspace": 8,
  "Insert"   : 45,
  "PageUp"   : 33,
  "PageDown" : 34,
  "Tab"      : 9,
  "-"        : 189,
  "="        : 187,
  "."        : 190,
  "/"        : 191,
  ","        : 188,
  ";"        : 186,
  "'"        : 222,
  "["        : 219,
  "]"        : 221,
  "NumPlus"  : 107,
  "NumMinus" : 109,
  "Shift"    : 16,
  "Ctrl"     : 17,
  "Control"  : 17,
  "Alt"      : 18
}

for (var i = 0; i < 26; i++) {
  keymap_latin_1[String.fromCharCode(i + 65)] = i + 65
}
for (var i = 0; i < 10; i++) {
  keymap_latin_1[String.fromCharCode(i + 48)] = i + 48
}

for (var k in keymap_latin_1) {
  if (!(k in keymap_latin_1)) {
    keymap_latin_1[keymap_latin_1[k]] = k;
  }
}

var keymap_latin_1_rev = {}
for (var k in keymap_latin_1) {
  keymap_latin_1_rev[keymap_latin_1[k]] = k
}

export var keymap = keymap_latin_1;
export var reverse_keymap = keymap_latin_1_rev;

export class HotKey {
  /**action can be a callback or a toolpath string*/
  constructor(key, modifiers, action, uiname) {
    this.action = action;
    this.mods = modifiers;
    this.key = keymap[key];
    this.uiname = uiname;
  }

  exec(ctx) {
    if (typeof this.action == "string") {
      ctx.api.execTool(ctx, this.action);
    } else {
      this.action(ctx);
    }
  }

  buildString() {
    let s = "";

    for (let i = 0; i < this.mods.length; i++) {
      if (i > 0) {
        s += " + ";
      }

      let k = this.mods[i].toLowerCase();
      k = k[0].toUpperCase() + k.slice(1, k.length).toLowerCase();

      s += k;
    }

    if (this.mods.length > 0) {
      s += "+";
    }

    s += reverse_keymap[this.key];

    return s.trim();
  }
}

export class KeyMap extends Array {
  /**
   *
   * @param pathid{string} Id of keymap, used when patching hotkeys, when
   *                       that is implemented
   * */
  constructor(hotkeys = [], pathid = "undefined") {
    super();

    this.pathid = pathid;

    for (let hk of hotkeys) {
      this.add(hk);
    }
  }

  handle(ctx, e) {
    let mods = new util.set();
    if (e.shiftKey)
      mods.add("shift");
    if (e.altKey)
      mods.add("alt");
    if (e.ctrlKey) {
      mods.add("ctrl");
    }
    if (e.commandKey) {
      mods.add("command");
    }

    for (let hk of this) {
      let ok = e.keyCode === hk.key;
      if (!ok) continue;

      let count = 0;
      for (let m of hk.mods) {
        m = m.toLowerCase().trim();

        if (!mods.has(m)) {
          ok = false;
          break;
        }

        count++;
      }

      if (count !== mods.length) {
        ok = false;
      }

      if (ok) {
        try {
          hk.exec(ctx);
        } catch (error) {
          util.print_stack(error);
          console.log("failed to execute a hotkey", keymap[e.keyCode]);
        }
        return true;
      }
    }
  }

  add(hk) {
    this.push(hk);
  }

  push(hk) {
    super.push(hk);
  }
}