Home Reference Source

scripts/widgets/ui_menu.js

"use strict";

import * as util from '../path-controller/util/util.js';
import cconst from '../config/const.js';
import * as ui_base from '../core/ui_base.js';
import * as toolprop from '../path-controller/toolsys/toolprop.js';
import {OldButton} from "./ui_button.js";
import {DomEventTypes} from '../path-controller/util/events.js';

import {HotKey, keymap} from '../path-controller/util/simple_events.js';

let EnumProperty = toolprop.EnumProperty,
    PropTypes    = toolprop.PropTypes;

let UIBase     = ui_base.UIBase,
    PackFlags  = ui_base.PackFlags,
    IconSheets = ui_base.IconSheets;

function getpx(css) {
  return parseFloat(css.trim().replace("px", ""))
}

function debugmenu() {
  if (window.DEBUG && window.DEBUG.menu) {
    console.warn("%cmenu:", "color:blue", ...arguments);
  }
}


export class Menu extends UIBase {
  constructor() {
    super();

    this.parentMenu = undefined;

    this._was_clicked = false;

    this.items = [];
    this.autoSearchMode = true;

    this._ignoreFocusEvents = false;
    this.closeOnMouseUp = true;

    this._submenu = undefined;

    this.ignoreFirstClick = false;

    this.itemindex = 0;
    this.closed = false;
    this.started = false;
    this.activeItem = undefined;

    this.overrideDefault("DefaultText", this.getDefault("MenuText"));

    //we have to make a container for any submenus to
    this.container = document.createElement("span");
    this.container.style["display"] = "flex";
    this.container.style["color"] = this.getDefault("MenuText").color;

    //this.container.style["background-color"] = "red";
    this.container.setAttribute("class", "menucon");

    this.dom = document.createElement("ul");
    this.dom.setAttribute("class", "menu");
    /*
              place-items: start start;
              flex-wrap : nowrap;
              align-content : start;
              place-content : start;
              justify-content : start;

              align-items : start;
              place-items : start;
              justify-items : start;
    */

    let style = this.menustyle = document.createElement("style");
    this.buildStyle();

    this.dom.setAttribute("tabindex", -1);

    //the menu wrangler handles key events

    this.shadow.appendChild(style);
    this.shadow.appendChild(this.container);
  }

  static define() {
    return {
      tagname: "menu-x",
      style  : "menu"
    };
  }

  float(x, y, zindex = undefined) {
    let dpi = this.getDPI();
    let rect = this.dom.getClientRects();
    let maxx = this.getWinWidth() - 10;
    let maxy = this.getWinHeight() - 10;

    if (rect.length > 0) {
      rect = rect[0];
      if (y + rect.height > maxy) {
        y = maxy - rect.height - 1;
      }

      if (x + rect.width > maxx) {
        x = maxx - rect.width - 1;
      }
    }

    super.float(x, y, 50);
  }

  click() {
    if (this._was_clicked) {
      return;
    }

    if (this.ignoreFirstClick) {
      this.ignoreFirstClick = Math.max(this.ignoreFirstClick - 1, 0);
      return;
    }

    if (!this.activeItem || this.activeItem._isMenu) {
      return;
    }

    this._was_clicked = true;

    if (this.onselect) {
      try {
        this.onselect(this.activeItem._id);
      } catch (error) {
        util.print_stack(error);
        console.log("Error in menu callback");
      }
    }

    this.close();
  }

  _ondestroy() {
    if (this.started) {
      menuWrangler.popMenu(this);

      if (this.onclose) {
        this.onclose();
      }
    }
  }

  init() {
    super.init();
    this.setCSS();
  }

  close() {
    if (this.closed) {
      return;
    }

    this.closed = true;

    if (this.started) {
      menuWrangler.popMenu(this);
    }

    this.started = false;

    if (this._popup) {
      this._popup.end();
      this._popup = undefined;
    }

    this.remove();
    this.dom.remove();

    if (this.onclose) {
      this.onclose(this);
    }
  }

  _select(dir, focus = true) {
    if (this.activeItem === undefined) {
      for (let item of this.items) {
        if (!item.hidden) {
          this.setActive(item, focus);
          break;
        }
      }
    } else {
      let i = this.items.indexOf(this.activeItem);
      let item = this.activeItem;

      do {
        i = (i + dir + this.items.length)%this.items.length;
        item = this.items[i];

        if (!item.hidden) {
          break;
        }
      } while (item !== this.activeItem);

      this.setActive(item, focus);
    }

    if (this.hasSearchBox) {
      this.activeItem.scrollIntoView();
    }
  }

  selectPrev(focus = true) {
    return this._select(-1, focus);
  }

  selectNext(focus = true) {
    return this._select(1, focus);
  }

  start_fancy(prepend, setActive = true) {
    return this.startFancy(prepend, setActive);
  }

  setActive(item, focus = true) {
    if (this.activeItem === item) {
      return;
    }

    if (this.activeItem) {
      this.activeItem.style["background-color"] = this.getDefault("MenuBG");

      if (focus) {
        this.activeItem.blur();
      }
    }

    if (item) {
      item.style["background-color"] = this.getDefault("MenuHighlight");

      if (focus) {
        item.focus();
      }
    }

    this.activeItem = item;
  }

  startFancy(prepend, setActive = true) {
    this.hasSearchBox = true;
    this.started = true;
    menuWrangler.pushMenu(this);

    let dom2 = document.createElement("div");
    //let dom2 = document.createElement("div");

    this.dom.setAttribute("class", "menu");
    dom2.setAttribute("class", "menu");

    let sbox = this.textbox = UIBase.createElement("textbox-x");
    this.textbox.parentWidget = this;

    dom2.appendChild(sbox);
    dom2.appendChild(this.dom);

    dom2.style["height"] = "300px";
    this.dom.style["height"] = "300px";
    this.dom.style["overflow"] = "scroll";

    if (prepend) {
      this.container.prepend(dom2);
    } else {
      this.container.appendChild(dom2);
    }

    dom2.parentWidget = this.container;

    sbox.focus();
    sbox.onchange = () => {
      let t = sbox.text.trim().toLowerCase();

      for (let item of this.items) {
        item.hidden = true;
        item.remove();
      }

      for (let item of this.items) {
        let ok = t == "";
        ok = ok || item.innerHTML.toLowerCase().search(t) >= 0;

        if (ok) {
          item.hidden = false;
          this.dom.appendChild(item);
        } else if (item === this.activeItem) {
          this.selectNext(false);
        }
        //item.hidden = !ok;
      }
    }

    sbox.addEventListener("keydown", (e) => {
      switch (e.keyCode) {
        case 27: //escape key
          this.close();
          break;
        case 13: //enter key
          this.click(this.activeItem);
          this.close();
          break;
      }
    });
  }

  start(prepend = false, setActive = true) {
    this.closed = false;

    this.started = true;
    this.focus();

    menuWrangler.pushMenu(this);

    let dokey = (key) => {
      let val = this.getDefault(key);
      if (typeof val === "number") {
        val = "" + val + "px";
      }

      if (val !== undefined) {
        this.dom.style[key] = val;
      }
    }

    dokey("padding");
    dokey("padding-top");
    dokey("padding-left");
    dokey("padding-right");
    dokey("padding-bottom");

    if (this.items.length > 15 && this.autoSearchMode) {
      return this.start_fancy(prepend, setActive);
    }

    if (prepend) {
      this.container.prepend(this.dom);
    } else {
      this.container.appendChild(this.dom);
    }

    if (!setActive)
      return;

    this.setCSS();
    this.flushUpdate();

    window.setTimeout(() => {
      this.flushUpdate();

      //select first child
      //TODO: cache last child entry

      if (this.activeItem === undefined) {
        this.activeItem = this.dom.childNodes[0];
      }

      if (this.activeItem === undefined) {
        return;
      }

      this.activeItem.focus();
    }, 0);
  }

  addItemExtra(text, id = undefined, hotkey, icon = -1, add = true, tooltip = undefined) {
    let dom = document.createElement("span");

    dom.style["display"] = "inline-flex";

    dom.hotkey = hotkey;
    dom.icon = icon;

    let icon_div;

    if (1) { //icon >= 0) {
      icon_div = ui_base.makeIconDiv(icon, IconSheets.SMALL);
    } else {
      let tilesize = ui_base.iconmanager.getTileSize(IconSheets.SMALL);

      //tilesize *= window.devicePixelRatio;

      icon_div = document.createElement("span");
      icon_div.style["padding"] = icon_div.style["margin"] = "0px";
      icon_div.style["width"] = tilesize + "px";
      icon_div.style["height"] = tilesize + "px";
    }

    icon_div.style["display"] = "inline-flex";
    icon_div.style["margin-right"] = "1px";
    icon_div.style["align"] = "left";

    let span = document.createElement("span");

    //stupid css doesn't get width right. . .
    span.style["font"] = ui_base.getFont(this, undefined, "MenuText");

    let dpi = this.getDPI();
    let tsize = this.getDefault("MenuText").size;
    //XXX proportional font fail

    //XXX stupid!
    let canvas = document.createElement("canvas");
    let g = canvas.getContext("2d");

    g.font = span.style["font"];

    let rect = span.getClientRects();

    let twid = Math.ceil(g.measureText(text).width);
    let hwid;
    if (hotkey) {
      dom.hotkey = hotkey;
      g.font = ui_base.getFont(this, undefined, "HotkeyText");
      hwid = Math.ceil(g.measureText(hotkey).width/UIBase.getDPI());
      twid += hwid + 8;
    }

    //let twid = Math.ceil(text.trim().length * tsize / dpi);

    span.innerText = text;

    span.style["word-wrap"] = "none";
    span.style["white-space"] = "pre";
    span.style["overflow"] = "hidden";
    span.style["text-overflow"] = "clip";

    span.style["width"] = ~~(twid) + "px";
    span.style["padding"] = "0px";
    span.style["margin"] = "0px";

    dom.style["width"] = "100%";

    dom.appendChild(icon_div);
    dom.appendChild(span);

    if (hotkey) {
      let hotkey_span = document.createElement("span");
      hotkey_span.innerText = hotkey;
      hotkey_span.style["display"] = "inline-flex";

      hotkey_span.style["margin"] = "0px";
      hotkey_span.style["margin-left"] = "auto";
      hotkey_span.style["margin-right"] = "0px";
      hotkey_span.style["padding"] = "0px";

      hotkey_span.style["font"] = ui_base.getFont(this, undefined, "HotkeyText");
      hotkey_span.style["color"] = this.getDefault("HotkeyTextColor");

      //hotkey_span.style["width"] = ~~((hwid + 7)) + "px";
      hotkey_span.style["width"] = "max-content";

      //hotkey_span.style["background-color"] = "rgba(0,0,0,0)";

      hotkey_span.style["text-align"] = "right";
      hotkey_span.style["justify-content"] = "right";
      hotkey_span["flex-wrap"] = "nowrap";
      hotkey_span["text-wrap"] = "nowrap";

      //hotkey_span.style["border"] = "1px solid red";

      //hotkey_span.style["display"] = "inline";
      //hotkey_span.style["float"] = "right";

      dom.appendChild(hotkey_span);
    }

    let ret = this.addItem(dom, id, add);

    ret.hotkey = hotkey;
    ret.icon = icon;
    ret.label = text ? text : ret.innerText;

    if (tooltip) {
      ret.title = tooltip;
    }

    return ret;
  }

  //item can be menu or text
  addItem(item, id, add = true, tooltip = undefined) {
    id = id === undefined ? item : id;
    let text = item;

    if (typeof item === "string" || item instanceof String) {
      let dom = document.createElement("dom");
      dom.style["text-align"] = "left";

      dom.textContent = item;
      item = dom;
      //return this.addItemExtra(item, id);
    } else {
      text = item.textContent;
    }

    let li = document.createElement("li");

    li.setAttribute("tabindex", this.itemindex++);
    li.setAttribute("class", "menuitem");

    if (tooltip !== undefined) {
      li.title = tooltip;
    }

    if (item instanceof Menu) {
      //let dom = this.addItemExtra(""+item.title, id, "", -1, false);
      let dom = document.createElement("span");
      dom.innerHTML = "" + item.title;
      dom._id = dom.id = id;
      dom.setAttribute("class", "menu")

      //dom = document.createElement("div");
      //dom.innerText = ""+item.title;

      //dom.style["display"] = "inline-block";
      li.style["width"] = "100%"
      li.appendChild(dom);

      li._isMenu = true;
      li._menu = item;
      item.parentMenu = this;

      item.hidden = false;
      item.container = this.container;
    } else {
      li._isMenu = false;
      li.appendChild(item);
    }

    li._id = id;

    this.items.push(li);

    li.label = text ? text : li.innerText.trim();

    if (add) {
      li.addEventListener("blur", (e) => {
        if (this._ignoreFocusEvents) {
          return;
        }

        if (this.activeItem && !this.activeItem._isMenu) {
          this.setActive(undefined, false);
        }
      });

      let onfocus = (e) => {
        if (this._ignoreFocusEvents) {
          return;
        }

        let active = this.activeItem;

        if (this._submenu) {
          this._submenu.close();
          this._submenu = undefined;
        }

        if (li._isMenu) {
          li._menu.onselect = (item) => {
            this.onselect(item);
            li._menu.close();
            this.close();
          };

          li._menu.start(false, false);
          this._submenu = li._menu;
        }

        this.setActive(li, false);
      };

      let onclick = (e) => {
        onfocus(e);

        e.stopPropagation();
        e.preventDefault();

        if (this.activeItem !== undefined && this.activeItem._isMenu) {
          //ignore
          return;
        }

        this.click();
      }

      li.addEventListener("contextmenu", e => e.preventDefault());
      this.addEventListener("contextmenu", e => e.preventDefault());

      li.addEventListener("pointerup", onclick, {capture: true});
      li.addEventListener("click", onclick, {capture: true});
      li.addEventListener("pointerdown", onclick, {capture: true});

      li.addEventListener("focus", (e) => {
        onfocus(e);
        onfocus(e);
      })

      li.addEventListener("pointermove", (e) => {
        onfocus(e);
        li.focus();
      });
      li.addEventListener("mouseover", (e) => {
        onfocus(e);
        li.focus();
      });
      li.addEventListener("mouseenter", (e) => {
        onfocus(e);
        li.focus();
      });

      li.addEventListener("pointerover", (e) => {
        onfocus(e);
        li.focus();
      });

      this.dom.appendChild(li);
    }

    return li;
  }

  _getBorderStyle() {
    let r = this.getDefault("border-width");
    let s = this.getDefault("border-style");
    let c = this.getDefault("border-color");

    return `${r}px ${s} ${c}`;
  }

  buildStyle() {
    let pad1 = util.isMobile() ? 2 : 0;
    pad1 += this.getDefault("MenuSpacing");

    let boxShadow = "";
    if (this.hasDefault("box-shadow")) {
      boxShadow = "box-shadow: " + this.getDefault("box-shadow") + ';';
    }

    let sepcss = this.getDefault("MenuSeparator");
    if (typeof sepcss === "object") {
      let s = '';

      for (let k in sepcss) {
        let v = sepcss[k];

        if (typeof v === "number") {
          v = v.toFixed(4) + "px";
        }

        s += `    ${k}: ${v};\n`;
      }

      sepcss = s;
    }

    let itemRadius = 0;

    if (this.hasDefault("item-radius")) {
      itemRadius = this.getDefault("item-radius");
    } else {
      itemRadius = this.getDefault("border-radius");
    }

    this.menustyle.textContent = `
        .menucon {
          position:fixed;
          float:left;
          
          border-radius : ${this.getDefault("border-radius")}px;

          display: block;
          -moz-user-focus: normal;
          ${boxShadow}
        }
        
        ul.menu {
          display        : flex;
          flex-direction : column;
          flex-wrap      : nowrap;
          width          : max-content;
          
          margin : 0px;
          padding : 0px;
          border : ${this._getBorderStyle()};
          border-radius : ${this.getDefault("border-radius")}px;
          -moz-user-focus: normal;
          background-color: ${this.getDefault("MenuBG")};
          color : ${this.getDefault("MenuText").color};
        }
        
        .menuitem {
          display : flex;
          flex-wrap : nowrap;
          flex-direction : row;          
          
          list-style-type:none;
          -moz-user-focus: normal;
          
          margin : 0;
          padding : 0px;
          padding-right: 16px;
          padding-left: 16px;
          padding-top : ${pad1}px;
          padding-bottom : ${pad1}px;
          
          border-radius : ${itemRadius}px;
          
          color : ${this.getDefault("MenuText").color};
          font : ${this.getDefault("MenuText").genCSS()};
          background-color: ${this.getDefault("MenuBG")};
        }
        
        .menuseparator {
          ${sepcss}
        }
        
        .menuitem:focus {
          display : flex;
          text-align: left;
          
          border : none;
          outline : none;
          border-radius : ${itemRadius}px;
          
          background-color: ${this.getDefault("MenuHighlight")};
          color : ${this.getDefault("MenuText").color};
          -moz-user-focus: normal;
        }
      `;
  }

  setCSS() {
    super.setCSS();

    this.buildStyle();

    this.container.style["color"] = this.getDefault("MenuText").color;
    this.style["color"] = this.getDefault("MenuText").color;
  }

  seperator() {
    let bar = document.createElement("div");
    bar.setAttribute("class", "menuseparator");

    this.dom.appendChild(bar);

    return this;
  }

  menu(title) {
    let ret = UIBase.createElement("menu-x");

    ret.setAttribute("name", title);
    this.addItem(ret);

    return ret;
  }

  calcSize() {

  }
}

Menu.SEP = Symbol("menu seperator");
UIBase.internalRegister(Menu);

export class DropBox extends OldButton {
  constructor() {
    super();

    this.lockTimer = 0;

    this._template = undefined;

    this._searchMenuMode = false;
    this.altKey = undefined;

    this._value = 0;

    this._last_datapath = undefined;

    this.r = 5;
    this._menu = undefined;
    this._auto_depress = false;
    //this.prop = new ui_base.EnumProperty(undefined, {}, "", "", 0);

    this._onpress = this._onpress.bind(this);
  }

  get searchMenuMode() {
    return this._searchMenuMode;
  }

  set searchMenuMode(v) {
    this._searchMenuMode = v;
  }

  get template() {
    return this._template;
  }

  set template(v) {
    this._template = v;
  }

  get value() {
    return this._value;
  }

  set value(v) {
    this.setValue(v);
  }

  get menu() {
    return this._menu;
  }

  set menu(val) {
    this._menu = val;

    if (val !== undefined) {
      this._name = val.title;
      this.updateName();
    }
  }

  static define() {
    return {
      tagname: "dropbox-x",
      style  : "dropbox"
    };
  }

  init() {
    super.init();

    this.setAttribute("menu-button", "true");

    this.updateWidth();
  }

  setCSS() {
    //do not call parent classes's setCSS here

    this.style["user-select"] = "none";
    this.dom.style["user-select"] = "none";

    let keys;
    if (this.getAttribute("simple")) {
      keys = ["margin-left", "margin-right", "padding-left", "padding-right"];
    } else {
      keys = [
        "margin", "margin-left", "margin-right",
        "margin-top", "margin-bottom", "padding",
        "padding-left", "padding-right", "padding-top",
        "padding-bottom"];
    }

    let setDefault = (key) => {
      if (this.hasDefault(key)) {
        this.dom.style[key] = this.getDefault(key, undefined, 0) + "px";
      }
    }

    for (let k of keys) {
      setDefault(k);
    }
  }

  _genLabel() {
    let s = super._genLabel();
    let ret = "";

    if (s.length === 0) {
      s = "(error)";
    }

    this.altKey = s[0].toUpperCase().charCodeAt(0);

    for (let i = 0; i < s.length; i++) {
      if (s[i] === "&" && i < s.length - 1 && s[i + 1] !== "&") {
        this.altKey = s[i + 1].toUpperCase().charCodeAt(0);
      } else if (s[i] === "&" && i < s.length - 1 && s[i + 1] === "&") {
        continue;
      } else {
        ret += s[i];
      }
    }

    return ret;
  }

  updateWidth() {
    //let ret = super.updateWidth(10);
    let dpi = this.getDPI();

    let ts = this.getDefault("DefaultText").size;
    let tw = this.g.measureText(this._genLabel()).width/dpi;
    //let tw = ui_base.measureText(this, this._genLabel(), undefined, undefined, ts).width + 8;
    tw = ~~tw;

    tw += 15;

    if (!this.getAttribute("simple")) {
      tw += 35;
    }

    if (tw !== this._last_w) {
      this._last_w = tw;
      this.dom.style["width"] = tw + "px";
      this.style["width"] = tw + "px";
      this.width = tw;

      this.overrideDefault("width", tw);
      this._repos_canvas();
      this._redraw();
    }

    return 0;
  }

  updateDataPath() {
    if (!this.ctx || !this.hasAttribute("datapath")) {
      return;
    }

    let wasError = false;
    let prop, val;

    try {
      this.pushReportContext(this._reportCtxName);
      prop = this.ctx.api.resolvePath(this.ctx, this.getAttribute("datapath")).prop;
      val = this.ctx.api.getValue(this.ctx, this.getAttribute("datapath"));
      this.popReportContext();
    } catch (error) {
      util.print_stack(error);
      wasError = true;
    }

    if (wasError) {
      this.disabled = true;
      this.setCSS();
      this._redraw();

      return;
    } else {
      this.disabled = false;
      this.setCSS();
      this._redraw();
    }

    if (!prop) {
      return;
    }

    if (this.prop === undefined) {
      this.prop = prop;
    }

    prop = this.prop;

    let name = this.getAttribute("name");

    if (prop.type & (PropTypes.ENUM | PropTypes.FLAG)) {
      name = prop.ui_value_names[prop.keys[val]];
    } else {
      name = "" + val;
    }

    if (name !== this.getAttribute("name")) {
      this.setAttribute("name", name);
      this.updateName();
    }
  }

  update() {
    let path = this.getAttribute("datapath");

    if (path && path !== this._last_datapath) {
      this._last_datapath = path;
      this.prop = undefined;

      this.updateDataPath();
    }

    super.update();

    let key = this.getDefault("dropTextBG");
    if (key !== this._last_dbox_key) {
      this._last_dbox_key = key;
      this.setCSS();
      this._redraw();
    }

    if (this.hasAttribute("datapath")) {
      this.updateDataPath();
    }
  }

  _build_menu_template() {
    if (this._menu !== undefined && this._menu.parentNode !== undefined) {
      this._menu.remove();
    }

    //let name = "" + this.getAttribute("name");
    let template = this._template;

    if (typeof template === "function") {
      template = template();
    }

    this._menu = createMenu(this.ctx, "", template);
    return this._menu;
  }

  _build_menu() {
    if (this._template) {
      this._build_menu_template();
      return;
    }

    let prop = this.prop;

    if (this.prop === undefined) {
      return;
    }

    if (this._menu !== undefined && this._menu.parentNode !== undefined) {
      this._menu.remove();
    }

    let menu = this._menu = UIBase.createElement("menu-x");

    //let name = "" + this.getAttribute("name");
    menu.setAttribute("name", "");
    menu._dropbox = this;

    let valmap = {};
    let enummap = prop.values;
    let iconmap = prop.iconmap;
    let uimap = prop.ui_value_names;
    let desr = prop.descriptions || {};

    for (let k in enummap) {
      let uk = k;

      valmap[enummap[k]] = k;

      if (uimap !== undefined && k in uimap) {
        uk = uimap[k];
      }

      let tooltip = desr[k];

      //menu.addItem(k, enummap[k], ":");
      if (iconmap && iconmap[k]) {
        menu.addItemExtra(uk, enummap[k], undefined, iconmap[k], undefined, tooltip);
      } else {
        menu.addItem(uk, enummap[k], undefined, tooltip);
      }
    }

    menu.onselect = (id) => {
      this._pressed = false;

      this._pressed = false;
      this._redraw();

      this._menu = undefined;

      //check if datapath system will be calling .prop.setValue instead of us
      let callProp = true;
      if (this.hasAttribute("datapath")) {
        let prop = this.getPathMeta(this.ctx, this.getAttribute("datapath"));
        callProp = !prop || prop !== this.prop;
      }

      this._value = this._convertVal(id);

      if (callProp) {
        this.prop.setValue(id);
      }

      this.setAttribute("name", this.prop.ui_value_names[valmap[id]]);
      if (this.onselect) {
        this.onselect(id);
      }

      if (this.hasAttribute("datapath") && this.ctx) {
        this.setPathValue(this.ctx, this.getAttribute("datapath"), id);
      }
    };
  }

  _onpress(e) {
    this.abortToolTips(1000);

    if (this._menu !== undefined) {
      this.lockTimer = util.time_ms();

      this._pressed = false;
      this._redraw();

      let menu = this._menu;
      this._menu = undefined;
      menu.close();
      return;
    }

    if (util.time_ms() - this.lockTimer < 200) {
      return;
    }

    this._build_menu();

    if (this._menu === undefined) {
      return;
    }

    this._menu.autoSearchMode = false;

    this._menu._dropbox = this;
    this.dom._background = this.getDefault("BoxDepressed");
    this._background = this.getDefault("BoxDepressed");
    this._redraw();
    this._pressed = true;
    this.setCSS();

    let onclose = this._menu.onclose;
    this._menu.onclose = () => {
      this.lockTimer = util.time_ms();

      this._pressed = false;
      this._redraw();

      let menu = this._menu;
      if (menu) {
        this._menu = undefined;
        menu._dropbox = undefined;
      }

      if (onclose) {
        onclose.call(menu);
      }
    }

    let menu = this._menu;
    let screen = this.getScreen();

    let dpi = this.getDPI();

    let x = e.x, y = e.y;
    let rects = this.dom.getBoundingClientRect(); //getClientRects();

    let rheight = rects.height;
    x = rects.x - window.scrollX;
    y = rects.y + rheight - window.scrollY; // + rects[0].height; // visualViewport.scale;

    if (!window.haveElectron) {
      //y -= 8;
    }

    /* need to figure out a better way to pop up a menu
    *  above a given y position */
    if (cconst.menusCanPopupAbove && y > screen.size[1]*0.5 && !this.searchMenuMode) {
      let con = screen.popup(this, 500, 400, false, 0);

      con.style["z-index"] = "-10000";
      con.style["position"] = UIBase.PositionKey;
      document.body.appendChild(con);

      con.style["visibility"] = "hidden";

      con.add(menu);
      menu.start();

      let time = util.time_ms();

      let timer = window.setInterval(() => {
        if (util.time_ms() - time > 1500) {
          window.clearInterval(timer);
          return;
        }

        let r = menu.dom.getBoundingClientRect();

        if (!r || r.height < 55) {
          return;
        }

        window.clearInterval(timer);

        y -= r.height + rheight;

        menu.dom.remove();
        con.remove();

        let popup = this._popup = menu._popup = screen.popup(this, x, y, false, 0);
        popup.noMarginsOrPadding();

        //popup.shadow.appendChild(menu.dom);
        popup.add(menu);
        menu.start();

        popup.style["left"] = x + "px";
        popup.style["top"] = y + "px";
        //menu.setCSS();
      }, 1);

      return;
    }

    /*
    let w = document.createElement("div");
    w.style["width"] = w.style["height"] = "15px";
    w.style["background-color"] = "red";
    w.style["z-index"] = "5000";
    w.style["position"] = UIBase.PositionKey;
    w.style["pointer-events"] = "none";
    w.style["left"] = x + "px";
    w.style["top"] = y + "px";

    document.body.appendChild(w);
    //*/

    let con = this._popup = menu._popup = screen.popup(this, x, y, false, 0);
    con.noMarginsOrPadding();

    con.add(menu);
    if (this.searchMenuMode) {
      menu.startFancy();
    } else {
      menu.start();
    }
  }

  _redraw() {
    if (this.getAttribute("simple")) {
      let color;

      this.g.clearRect(0, 0, this.dom.width, this.dom.height);
      
      if (this._highlight) {
        ui_base.drawRoundBox2(this, {canvas: this.dom, g: this.g, color: this.getDefault("BoxHighlight")});
      }

      if (this._focus) {
        ui_base.drawRoundBox2(this, {
          canvas: this.dom, g: this.g, color: this.getDefault("BoxHighlight"), op: "stroke", no_clear: true
        });
        ui_base.drawRoundBox(this, this.dom, this.g, undefined, undefined, 2, "stroke");
      }

      this._draw_text();
      return;
    }

    super._redraw(false);

    let g = this.g;
    let w = this.dom.width, h = this.dom.height;
    let dpi = this.getDPI();

    let p = 10*dpi;
    let p2 = dpi;

    //*
    let bg = this.getDefault("dropTextBG");
    if (bg !== undefined) {
      g.fillStyle = bg;

      g.beginPath();
      g.rect(p2, p2, this.dom.width - p2 - h, this.dom.height - p2*2);
      g.fill();
    }
    //*/

    g.fillStyle = "rgba(50, 50, 50, 0.2)";
    g.strokeStyle = "rgba(50, 50, 50, 0.8)";
    g.beginPath();
    /*
    g.moveTo(w-p, p);
    g.lineTo(w-(p+h*0.25), h-p);
    g.lineTo(w-(p+h*0.5), p);
    g.closePath();
    //*/

    let sz = 0.3;
    g.moveTo(w - h*0.5 - p, p);
    g.lineTo(w - p, p);
    g.moveTo(w - h*0.5 - p, p + sz*h/3);
    g.lineTo(w - p, p + sz*h/3);
    g.moveTo(w - h*0.5 - p, p + sz*h*2/3);
    g.lineTo(w - p, p + sz*h*2/3);

    g.lineWidth = 1;
    g.stroke();

    this._draw_text();
  }

  _convertVal(val) {
    if (typeof val === "string" && this.prop) {
      if (val in this.prop.values) {
        return this.prop.values[val];
      } else if (val in this.prop.keys) {
        return this.prop.keys[val];
      } else {
        return undefined;
      }
    }

    return val;
  }

  setValue(val, setLabelOnly = false) {
    if (val === undefined || val === this._value) {
      return;
    }

    val = this._convertVal(val);

    if (val === undefined) {
      console.warn("Bad val", arguments[0]);
      return;
    }

    this._value = val;

    if (this.prop !== undefined && !setLabelOnly) {
      this.prop.setValue(val);
      let val2 = val;

      if (val2 in this.prop.keys)
        val2 = this.prop.keys[val2];
      val2 = this.prop.ui_value_names[val2];

      this.setAttribute("name", "" + val2);
      this._name = "" + val2;
    } else {
      this.setAttribute("name", "" + val);
      this._name = "" + val;
    }

    if (this.onchange && !setLabelOnly) {
      this.onchange(val);
    }

    this.setCSS();
    this.updateDataPath();
    this._redraw();
  }
}

UIBase.internalRegister(DropBox);

export class MenuWrangler {
  constructor() {
    this.screen = undefined;
    this.menustack = [];

    this.lastPickElemTime = util.time_ms();

    this._closetimer = 0;
    this.closeOnMouseUp = undefined;
    this.closereq = undefined;

    this.timer = undefined;
  }

  get closetimer() {
    return this._closetimer;
  }

  set closetimer(v) {
    debugmenu("set closertime", v);
    this._closetimer = v;
  }

  get menu() {
    return this.menustack.length > 0 ? this.menustack[this.menustack.length - 1] : undefined;
  }

  pushMenu(menu) {
    debugmenu("pushMenu");

    this.spawnreq = undefined;

    if (this.menustack.length === 0 && menu.closeOnMouseUp) {
      this.closeOnMouseUp = true;
    }

    this.menustack.push(menu);
  }

  popMenu(menu) {
    debugmenu("popMenu");

    return this.menustack.pop();
  }

  endMenus() {
    debugmenu("endMenus");

    for (let menu of this.menustack) {
      menu.close();
    }

    this.menustack = [];
  }

  searchKeyDown(e) {
    let menu = this.menu;

    e.stopPropagation();
    menu._ignoreFocusEvents = true;
    menu.textbox.focus();
    menu._ignoreFocusEvents = false;

    //if (e.shiftKey || e.altKey || e.ctrlKey || e.commandKey) {
    //  return;
    //}

    switch (e.keyCode) {
      case keymap["Enter"]: //return key
        menu.click(menu.activeItem);
        break;
      case keymap["Escape"]: //escape key
        menu.close();
        break;
      case keymap["Up"]:
        menu.selectPrev(false);
        break;
      case keymap["Down"]:
        menu.selectNext(false);
        break;
    }
  }

  on_keydown(e) {
    window.menu = this.menu;

    if (this.menu === undefined) {
      return;
    }

    if (this.menu.hasSearchBox) {
      return this.searchKeyDown(e);
    }

    let menu = this.menu;

    switch (e.keyCode) {
      case keymap["Left"]: //left
      case keymap["Right"]: //right
        if (menu._dropbox) {
          let dropbox = menu._dropbox;

          if (e.keyCode === keymap["Left"]) {
            dropbox = dropbox.previousElementSibling;
          } else {
            dropbox = dropbox.nextElementSibling;
          }

          if (dropbox !== undefined && dropbox instanceof DropBox) {
            this.endMenus();
            dropbox._onpress(e);
          }
        }
        break;
      case keymap["Up"]: //up
        menu.selectPrev();
        break;
      case keymap["Down"]: //down
        menu.selectNext();
        break;
      /*
      let item = menu.activeItem;
      if (!item) {
        item = menu.items[0];
      }

      if (!item) {
        return;
      }

      let item2;
      let i = menu.items.indexOf(item);

      if (e.keyCode == 38) {
        i = (i - 1 + menu.items.length) % menu.items.length;
      } else {
        i = (i + 1) % menu.items.length;
      }

      item2 = menu.items[i];

      if (item2) {
        menu.setActive(item2);
      }
      break;//*/
      case 13: //return key
      case 32: //space key
        menu.click(menu.activeItem);
        break;
      case 27: //escape key
        menu.close();
        break;
    }
  }

  on_pointerdown(e) {
    if (this.menu === undefined || this.screen === undefined) {
      this.closetimer = util.time_ms();
      return;
    }

    let screen = this.screen;
    let x = e.pageX, y = e.pageY;

    let element = screen.pickElement(x, y);

    if (element !== undefined && (element instanceof DropBox || util.isMobile())) {
      this.endMenus();
      e.preventDefault();
      e.stopPropagation();
    }
  }

  on_pointerup(e) {
    if (this.menu === undefined || this.screen === undefined) {
      this.closetimer = util.time_ms();
      return;
    }

    let screen = this.screen;
    let x = e.pageX, y = e.pageY;

    let element = screen.pickElement(x, y, undefined, undefined, DropBox);
    if (element !== undefined) {
      this.closeOnMouseUp = false;
    } else {
      element = screen.pickElement(x, y, undefined, undefined, Menu);

      //closeOnMouseUp
      if (element && this.closeOnMouseUp) {
        element.click();
      }
    }

  }

  findMenu(x, y) {
    let screen = this.screen;

    let element = screen.pickElement(x, y);

    if (element === undefined) {
      return;
    }

    if (element instanceof Menu) {
      return element;
    }

    let w = element;

    while (w) {
      if (w instanceof Menu) {//w === this.menu) {
        return w;
        break;
      }

      w = w.parentWidget;
    }

    return undefined;
  }

  on_pointermove(e) {
    if (this.menu && this.menu.hasSearchBox) {
      this.closetimer = util.time_ms();
      this.closereq = undefined;
      return;
    }

    if (this.menu === undefined || this.screen === undefined) {
      this.closetimer = util.time_ms();
      this.closereq = undefined;
      return;
    }

    let screen = this.screen;
    let x = e.pageX, y = e.pageY;

    let element;
    let menu = this.menu;

    if (menu) {
      let r = menu.getBoundingClientRect();
      let pad = 15;

      if (r && x >= r.x - pad && y >= r.y - pad && x <= r.x + r.width + pad*2 && y <= r.y + r.height + pad*2) {
        element = menu;
      }
    }

    if (!element && util.time_ms() - this.lastPickElemTime > 250) {
      element = screen.pickElement(x, y);
      this.lastPickElemTime = util.time_ms();
    }

    if (element === undefined) {
      return;
    }

    if (element instanceof Menu) {
      this.closetimer = util.time_ms();
      this.closereq = undefined;
      return;
    }

    let destroy = element.hasAttribute("menu-button") && element.hasAttribute("simple");
    destroy = destroy && element.menu !== this.menu;

    if (destroy) {
      /* check that dropbox doesn't contain our parent menu either */

      let menu2 = this.menu;
      while (menu2 !== element.menu) {
        menu2 = menu2.parentMenu;
        destroy = destroy && menu2 !== element.menu;
      }
    }

    if (destroy) {
      //destroy entire menu stack
      this.endMenus();

      this.closetimer = util.time_ms();
      this.closereq = undefined;

      //start new menu
      element._onpress(e);
      return;
    }

    let ok = false;

    let w = element;
    while (w) {
      if (w instanceof Menu) {//w === this.menu) {
        ok = true;
        break;
      }

      if (w.hasAttribute("menu-button") && w.menu === this.menu) {
        ok = true;
        break;
      }

      w = w.parentWidget;
    }

    if (!ok) {
      this.closereq = this.menu;
    } else {
      this.closetimer = util.time_ms();
      this.closereq = undefined;
    }
  }

  update() {
    let closetime = cconst.menu_close_time;
    closetime = closetime === undefined ? 50 : closetime;

    let close = this.closereq && this.closereq === this.menu;
    close = close && util.time_ms() - this.closetimer > closetime;

    if (close) {
      this.closereq = undefined;
      this.endMenus();
    }
  }

  startTimer() {
    if (this.timer) {
      this.stopTimer();
    }

    this.timer = setInterval(() => {
      debugmenu("start menu wrangler interval");

      this.update();
    }, 150);
  }

  stopTimer() {
    if (this.timer) {
      debugmenu("stop menu wrangler interval");

      clearInterval(this.timer);
      this.timer = undefined;
    }
  }
}

export let menuWrangler = new MenuWrangler();
let wrangerStarted = false;

export function startMenuEventWrangling(screen) {
  menuWrangler.screen = screen;

  if (wrangerStarted) {
    return;
  }

  wrangerStarted = true;

  for (let k in DomEventTypes) {
    if (menuWrangler[k] === undefined) {
      continue;
    }

    let dom = k.search("key") >= 0 ? window : document.body;
    dom = window;
    dom.addEventListener(DomEventTypes[k], menuWrangler[k].bind(menuWrangler), {passive: false, capture: true})
  }

  menuWrangler.screen = screen;
  menuWrangler.startTimer();
}

export function setWranglerScreen(screen) {
  startMenuEventWrangling(screen);
}

export function getWranglerScreen() {
  return menuWrangler.screen;
}

export function createMenu(ctx, title, templ) {
  let menu = UIBase.createElement("menu-x");
  menu.ctx = ctx;
  menu.setAttribute("name", title);

  let SEP = menu.constructor.SEP;
  let id = 0;
  let cbs = {};

  let doItem = (item) => {
    if (item !== undefined && item instanceof Menu) {
      menu.addItem(item);
    } else if (typeof item == "string") {
      let def, hotkey;

      try {
        def = ctx.api.getToolDef(item);
      } catch (error) {
        menu.addItem("(tool path error)", id++);
        return;
      }

      //3Extra(text, id=undefined, hotkey, icon=-1, add=true) {
      if (!def.hotkey) {
        try {
          hotkey = ctx.api.getToolPathHotkey(ctx, item);
        } catch (error) {
          util.print_stack(error);
          console.warn("error getting hotkey for tool " + item);
          hotkey = undefined;
        }
      } else {
        hotkey = def.hotkey;
      }

      menu.addItemExtra(def.uiname, id, hotkey, def.icon);

      cbs[id] = (function (toolpath) {
        return function () {
          ctx.api.execTool(ctx, toolpath);
        }
      })(item);

      id++;
    } else if (item === SEP) {
      menu.seperator();
    } else if (typeof item === "function" || item instanceof Function) {
      doItem(item());
    } else if (item instanceof Array) { //old array-based api for custom entries
      let hotkey = item.length > 2 ? item[2] : undefined;
      let icon = item.length > 3 ? item[3] : undefined;
      let tooltip = item.length > 4 ? item[4] : undefined;
      let id2 = item.length > 5 ? item[5] : id++;

      if (hotkey !== undefined && hotkey instanceof HotKey) {
        hotkey = hotkey.buildString();
      }

      menu.addItemExtra(item[0], id2, hotkey, icon, undefined, tooltip);

      cbs[id2] = (function (cbfunc, arg) {
        return function () {
          cbfunc(arg);
        }
      })(item[1], id2);
    } else if (typeof item === "object") { //new object-based api for custom entries
      let {name, callback, hotkey, icon, tooltip} = item;

      let id2 = item.id !== undefined ? item.id : id++;
      if (hotkey !== undefined && hotkey instanceof HotKey) {
        hotkey = hotkey.buildString();
      }

      menu.addItemExtra(name, id2, hotkey, icon, undefined, tooltip);

      cbs[id2] = (function (cbfunc, arg) {
        return function () {
          cbfunc(arg);
        }
      })(callback, id2);
    }
  }

  for (let item of templ) {
    doItem(item);
  }

  menu.onselect = (id) => {
    cbs[id]();
  }

  return menu;
}

export function startMenu(menu, x, y, searchMenuMode = false, safetyDelay = 55) {
  menuWrangler.endMenus();

  let screen = menu.ctx.screen;
  let con = menu._popup = screen.popup(undefined, x, y, false, safetyDelay);
  con.noMarginsOrPadding();

  con.add(menu);
  if (searchMenuMode) {
    menu.startFancy();
  } else {
    menu.start();
  }

  menu.flushUpdate();
  menu.flushSetCSS();

  menu._popup.flushUpdate();
  menu._popup.flushSetCSS();
}