<commit_before>var glu = require('pex-glu');
var geom = require('pex-geom');
var sys = require('pex-sys');
var color = require('pex-color');
var Platform = sys.Platform;
var GUIControl = require('./GUIControl');
var SkiaRenderer = require('./SkiaRenderer');
var HTMLCanvasRenderer = require('./HTMLCanvasRenderer');
var Context = glu.Context;
var ScreenImage = glu.ScreenImage;
var Vec2 = geom.Vec2;
var Vec3 = geom.Vec3;
var Rect = geom.Rect;
var Spline1D = geom.Spline1D;
var Spline2D = geom.Spline2D;
var IO = sys.IO;
var Color = color.Color;

//`window` - parent window
//`x` - gui x position
//`y` - gui y position
//`scale` - slider scale, usefull for touch
//do not mistake that for highdpi as his is handled automatically based on window.settings.highdpi
function GUI(window, x, y, scale) {
  this.gl = Context.currentContext;
  this.window = window;
  this.x = x === undefined ? 0 : x;
  this.y = y === undefined ? 0 : y;
  this.mousePos = Vec2.create();
  this.scale = scale || 1;
  this.highdpi = window.settings.highdpi || 1;
  if (Platform.isPlask) {
    this.renderer = new SkiaRenderer(window.width, window.height, this.highdpi);
  }
  else if (Platform.isBrowser) {
    this.renderer = new HTMLCanvasRenderer(window.width, window.height, this.highdpi);
  }
  this.screenBounds = new Rect(this.x, this.y, window.width, window.height);
  this.screenImage = new ScreenImage(this.renderer.getTexture(), this.x, this.y, window.width, window.height, window.width, window.height);
  this.items = [];
  this.bindEventListeners(window);
}

GUI.prototype.bindEventListeners = function (window) {
  var self = this;
  window.on('leftMouseDown', function (e) {
    self.onMouseDown(e);
  });
  window.on('mouseDragged', function (e) {
    self.onMouseDrag(e);
  });
  window.on('leftMouseUp', function (e) {
    self.onMouseUp(e);
  });
};

GUI.prototype.onMouseDown = function (e) {
  this.activeControl = null;
  this.mousePos.set(e.x / this.highdpi - this.x, e.y / this.highdpi - this.y);
  for (var i = 0; i < this.items.length; i++) {
    if (this.items[i].activeArea.contains(this.mousePos)) {
      this.activeControl = this.items[i];
      this.activeControl.active = true;
      this.activeControl.dirty = true;
      if (this.activeControl.type == 'button') {
        this.activeControl.contextObject[this.activeControl.methodName]();
      }
      else if (this.activeControl.type == 'toggle') {
        this.activeControl.contextObject[this.activeControl.attributeName] = !this.activeControl.contextObject[this.activeControl.attributeName];
        if (this.activeControl.onchange) {
          this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
        }
      }
      else if (this.activeControl.type == 'radiolist') {
        var hitY = this.mousePos.y - this.activeControl.activeArea.y;
        var hitItemIndex = Math.floor(this.activeControl.items.length * hitY / this.activeControl.activeArea.height);
        if (hitItemIndex < 0)
          continue;
        if (hitItemIndex >= this.activeControl.items.length)
          continue;
        this.activeControl.contextObject[this.activeControl.attributeName] = this.activeControl.items[hitItemIndex].value;
        if (this.activeControl.onchange) {
          this.activeControl.onchange(this.activeControl.items[hitItemIndex].value);
        }
      }
      e.handled = true;
      this.onMouseDrag(e);
      break;
    }
  }
};

GUI.prototype.onMouseDrag = function (e) {
  if (this.activeControl) {
    var aa = this.activeControl.activeArea;
    if (this.activeControl.type == 'slider') {
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      this.activeControl.setNormalizedValue(val);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'multislider') {
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(this.activeControl.getValue().length * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'vec2') {
      var numSliders = 2;
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(numSliders * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'vec3') {
      var numSliders = 3;
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(numSliders * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'color') {
      var numSliders = this.activeControl.options.alpha ? 4 : 3;
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(numSliders * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    e.handled = true;
  }
};

GUI.prototype.onMouseUp = function (e) {
  if (this.activeControl) {
    this.activeControl.active = false;
    this.activeControl.dirty = true;
    this.activeControl.clickedSlider = undefined;
    this.activeControl = null;
  }
};

GUI.prototype.addHeader = function (title) {
  var ctrl = new GUIControl({
    type: 'header',
    title: title,
    dirty: true,
    activeArea: new Rect(0, 0, 0, 0),
    setTitle: function (title) {
      this.title = title;
      this.dirty = true;
    }
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addLabel = function (title) {
  var ctrl = new GUIControl({
    type: 'label',
    title: title,
    dirty: true,
    activeArea: new Rect(0, 0, 0, 0),
    setTitle: function (title) {
      this.title = title;
      this.dirty = true;
    }
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addParam = function (title, contextObject, attributeName, options, onchange) {
  options = options || {};
  if (typeof(options.min) == 'undefined') options.min = 0;
  if (typeof(options.max) == 'undefined') options.max = 1;
  if (contextObject[attributeName] instanceof Array) {
    var ctrl = new GUIControl({
      type: 'multislider',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Vec2) {
    var ctrl = new GUIControl({
      type: 'vec2',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Vec3) {
    var ctrl = new GUIControl({
      type: 'vec3',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Color) {
    var ctrl = new GUIControl({
      type: 'color',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Spline1D) {
    var ctrl = new GUIControl({
      type: 'spline1D',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Spline2D) {
    var ctrl = new GUIControl({
      type: 'spline2D',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] === false || contextObject[attributeName] === true) {
    var ctrl = new GUIControl({
      type: 'toggle',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else {
    var ctrl = new GUIControl({
      type: 'slider',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
};

GUI.prototype.addButton = function (title, contextObject, methodName, options) {
  var ctrl = new GUIControl({
    type: 'button',
    title: title,
    contextObject: contextObject,
    methodName: methodName,
    activeArea: new Rect(0, 0, 0, 0),
    dirty: true,
    options: options || {}
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addRadioList = function (title, contextObject, attributeName, items, onchange) {
  var ctrl = new GUIControl({
    type: 'radiolist',
    title: title,
    contextObject: contextObject,
    attributeName: attributeName,
    activeArea: new Rect(0, 0, 0, 0),
    items: items,
    onchange: onchange,
    dirty: true
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addTexture2D = function (title, texture) {
  var ctrl = new GUIControl({
    type: 'texture2D',
    title: title,
    texture: texture,
    activeArea: new Rect(0, 0, 0, 0),
    dirty: true
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.dispose = function () {
};

GUI.prototype.draw = function () {
  if (this.items.length === 0) {
    return;
  }
  this.renderer.draw(this.items, this.scale);

  glu.enableDepthReadAndWrite(false, false);

  var gl = Context.currentContext;
  gl.disable(gl.DEPTH_TEST);
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  this.screenImage.draw();
  gl.disable(gl.BLEND);
  gl.enable(gl.DEPTH_TEST);
  this.drawTextures();
};

GUI.prototype.drawTextures = function () {
  //for (var i = 0; i < this.items.length; i++) {
  //  var item = this.items[i];
  //  if (item.type == 'texture2D') {
  //    var bounds = new Rect(item.activeArea.x * this.scale, item.activeArea.y * this.scale, item.activeArea.width * this.scale, item.activeArea.height * this.scale);
  //    this.screenImage.setBounds(bounds);
  //    this.screenImage.setImage(item.texture);
  //    this.screenImage.draw();
  //  }
  //}
  //this.screenImage.setBounds(this.screenBounds);
  //this.screenImage.setImage(this.renderer.getTexture());
};

GUI.prototype.serialize = function () {
  var data = {};
  this.items.forEach(function (item, i) {
    data[item.title] = item.getSerializedValue();
  });
  return data;
};

GUI.prototype.deserialize = function (data) {
  this.items.forEach(function (item, i) {
    if (data[item.title] !== undefined) {
      item.setSerializedValue(data[item.title]);
      item.dirty = true;
    }
  });
};

GUI.prototype.save = function (path) {
  var data = this.serialize();
  IO.saveTextFile(path, JSON.stringify(data));
};

GUI.prototype.load = function (path, callback) {
  var self = this;
  IO.loadTextFile(path, function (dataStr) {
    var data = JSON.parse(dataStr);
    self.deserialize(data);
    if (callback) {
      callback();
    }
  });
};

module.exports = GUI;<commit_msg>Updated type checks<commit_after>var glu = require('pex-glu');
var geom = require('pex-geom');
var sys = require('pex-sys');
var color = require('pex-color');
var Platform = sys.Platform;
var GUIControl = require('./GUIControl');
var SkiaRenderer = require('./SkiaRenderer');
var HTMLCanvasRenderer = require('./HTMLCanvasRenderer');
var Context = glu.Context;
var ScreenImage = glu.ScreenImage;
var Vec2 = geom.Vec2;
var Vec3 = geom.Vec3;
var Rect = geom.Rect;
var Spline1D = geom.Spline1D;
var Spline2D = geom.Spline2D;
var IO = sys.IO;
var Color = color.Color;

//`window` - parent window
//`x` - gui x position
//`y` - gui y position
//`scale` - slider scale, usefull for touch
//do not mistake that for highdpi as his is handled automatically based on window.settings.highdpi
function GUI(window, x, y, scale) {
  this.gl = Context.currentContext;
  this.window = window;
  this.x = x === undefined ? 0 : x;
  this.y = y === undefined ? 0 : y;
  this.mousePos = Vec2.create();
  this.scale = scale || 1;
  this.highdpi = window.settings.highdpi || 1;
  if (Platform.isPlask) {
    this.renderer = new SkiaRenderer(window.width, window.height, this.highdpi);
  }
  else if (Platform.isBrowser) {
    this.renderer = new HTMLCanvasRenderer(window.width, window.height, this.highdpi);
  }
  this.screenBounds = new Rect(this.x, this.y, window.width, window.height);
  this.screenImage = new ScreenImage(this.renderer.getTexture(), this.x, this.y, window.width, window.height, window.width, window.height);
  this.items = [];
  this.bindEventListeners(window);
}

GUI.prototype.bindEventListeners = function (window) {
  var self = this;
  window.on('leftMouseDown', function (e) {
    self.onMouseDown(e);
  });
  window.on('mouseDragged', function (e) {
    self.onMouseDrag(e);
  });
  window.on('leftMouseUp', function (e) {
    self.onMouseUp(e);
  });
};

GUI.prototype.onMouseDown = function (e) {
  this.activeControl = null;
  this.mousePos.set(e.x / this.highdpi - this.x, e.y / this.highdpi - this.y);
  for (var i = 0; i < this.items.length; i++) {
    if (this.items[i].activeArea.contains(this.mousePos)) {
      this.activeControl = this.items[i];
      this.activeControl.active = true;
      this.activeControl.dirty = true;
      if (this.activeControl.type == 'button') {
        this.activeControl.contextObject[this.activeControl.methodName]();
      }
      else if (this.activeControl.type == 'toggle') {
        this.activeControl.contextObject[this.activeControl.attributeName] = !this.activeControl.contextObject[this.activeControl.attributeName];
        if (this.activeControl.onchange) {
          this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
        }
      }
      else if (this.activeControl.type == 'radiolist') {
        var hitY = this.mousePos.y - this.activeControl.activeArea.y;
        var hitItemIndex = Math.floor(this.activeControl.items.length * hitY / this.activeControl.activeArea.height);
        if (hitItemIndex < 0)
          continue;
        if (hitItemIndex >= this.activeControl.items.length)
          continue;
        this.activeControl.contextObject[this.activeControl.attributeName] = this.activeControl.items[hitItemIndex].value;
        if (this.activeControl.onchange) {
          this.activeControl.onchange(this.activeControl.items[hitItemIndex].value);
        }
      }
      e.handled = true;
      this.onMouseDrag(e);
      break;
    }
  }
};

GUI.prototype.onMouseDrag = function (e) {
  if (this.activeControl) {
    var aa = this.activeControl.activeArea;
    if (this.activeControl.type == 'slider') {
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      this.activeControl.setNormalizedValue(val);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'multislider') {
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(this.activeControl.getValue().length * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'vec2') {
      var numSliders = 2;
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(numSliders * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'vec3') {
      var numSliders = 3;
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(numSliders * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    else if (this.activeControl.type == 'color') {
      var numSliders = this.activeControl.options.alpha ? 4 : 3;
      var val = (e.x / this.highdpi - aa.x) / aa.width;
      val = Math.max(0, Math.min(val, 1));
      var idx = Math.floor(numSliders * (e.y / this.highdpi - aa.y) / aa.height);
      if (!isNaN(this.activeControl.clickedSlider)) {
        idx = this.activeControl.clickedSlider;
      }
      else {
        this.activeControl.clickedSlider = idx;
      }
      this.activeControl.setNormalizedValue(val, idx);
      if (this.activeControl.onchange) {
        this.activeControl.onchange(this.activeControl.contextObject[this.activeControl.attributeName]);
      }
      this.activeControl.dirty = true;
    }
    e.handled = true;
  }
};

GUI.prototype.onMouseUp = function (e) {
  if (this.activeControl) {
    this.activeControl.active = false;
    this.activeControl.dirty = true;
    this.activeControl.clickedSlider = undefined;
    this.activeControl = null;
  }
};

GUI.prototype.addHeader = function (title) {
  var ctrl = new GUIControl({
    type: 'header',
    title: title,
    dirty: true,
    activeArea: new Rect(0, 0, 0, 0),
    setTitle: function (title) {
      this.title = title;
      this.dirty = true;
    }
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addLabel = function (title) {
  var ctrl = new GUIControl({
    type: 'label',
    title: title,
    dirty: true,
    activeArea: new Rect(0, 0, 0, 0),
    setTitle: function (title) {
      this.title = title;
      this.dirty = true;
    }
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addParam = function (title, contextObject, attributeName, options, onchange) {
  options = options || {};
  if (typeof(options.min) == 'undefined') options.min = 0;
  if (typeof(options.max) == 'undefined') options.max = 1;
  if (!isNaN(contextObject[attributeName])) {
    var ctrl = new GUIControl({
      type: 'slider',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] === false || contextObject[attributeName] === true) {
    var ctrl = new GUIControl({
      type: 'toggle',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Array) {
    var ctrl = new GUIControl({
      type: 'multislider',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Vec2) {
    var ctrl = new GUIControl({
      type: 'vec2',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Vec3) {
    var ctrl = new GUIControl({
      type: 'vec3',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Color) {
    var ctrl = new GUIControl({
      type: 'color',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Spline1D) {
    var ctrl = new GUIControl({
      type: 'spline1D',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
  else if (contextObject[attributeName] instanceof Spline2D) {
    var ctrl = new GUIControl({
      type: 'spline2D',
      title: title,
      contextObject: contextObject,
      attributeName: attributeName,
      activeArea: new Rect(0, 0, 0, 0),
      options: options,
      onchange: onchange,
      dirty: true
    });
    this.items.push(ctrl);
    return ctrl;
  }
};

GUI.prototype.addButton = function (title, contextObject, methodName, options) {
  var ctrl = new GUIControl({
    type: 'button',
    title: title,
    contextObject: contextObject,
    methodName: methodName,
    activeArea: new Rect(0, 0, 0, 0),
    dirty: true,
    options: options || {}
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addRadioList = function (title, contextObject, attributeName, items, onchange) {
  var ctrl = new GUIControl({
    type: 'radiolist',
    title: title,
    contextObject: contextObject,
    attributeName: attributeName,
    activeArea: new Rect(0, 0, 0, 0),
    items: items,
    onchange: onchange,
    dirty: true
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.addTexture2D = function (title, texture) {
  var ctrl = new GUIControl({
    type: 'texture2D',
    title: title,
    texture: texture,
    activeArea: new Rect(0, 0, 0, 0),
    dirty: true
  });
  this.items.push(ctrl);
  return ctrl;
};

GUI.prototype.dispose = function () {
};

GUI.prototype.draw = function () {
  if (this.items.length === 0) {
    return;
  }
  this.renderer.draw(this.items, this.scale);

  glu.enableDepthReadAndWrite(false, false);

  var gl = Context.currentContext;
  gl.disable(gl.DEPTH_TEST);
  gl.enable(gl.BLEND);
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
  this.screenImage.draw();
  gl.disable(gl.BLEND);
  gl.enable(gl.DEPTH_TEST);
  this.drawTextures();
};

GUI.prototype.drawTextures = function () {
  //for (var i = 0; i < this.items.length; i++) {
  //  var item = this.items[i];
  //  if (item.type == 'texture2D') {
  //    var bounds = new Rect(item.activeArea.x * this.scale, item.activeArea.y * this.scale, item.activeArea.width * this.scale, item.activeArea.height * this.scale);
  //    this.screenImage.setBounds(bounds);
  //    this.screenImage.setImage(item.texture);
  //    this.screenImage.draw();
  //  }
  //}
  //this.screenImage.setBounds(this.screenBounds);
  //this.screenImage.setImage(this.renderer.getTexture());
};

GUI.prototype.serialize = function () {
  var data = {};
  this.items.forEach(function (item, i) {
    data[item.title] = item.getSerializedValue();
  });
  return data;
};

GUI.prototype.deserialize = function (data) {
  this.items.forEach(function (item, i) {
    if (data[item.title] !== undefined) {
      item.setSerializedValue(data[item.title]);
      item.dirty = true;
    }
  });
};

GUI.prototype.save = function (path) {
  var data = this.serialize();
  IO.saveTextFile(path, JSON.stringify(data));
};

GUI.prototype.load = function (path, callback) {
  var self = this;
  IO.loadTextFile(path, function (dataStr) {
    var data = JSON.parse(dataStr);
    self.deserialize(data);
    if (callback) {
      callback();
    }
  });
};

module.exports = GUI;<|endoftext|>


<commit_before>Package.describe({
  name: 'electronifiejs:meteor-ensure',
  summary: "ensure",
  version: "0.0.1",
  git: "https://github.com/electronifie/meteor-ensure.git"
});

Npm.depends({
  "stack-trace": "0.0.9"
});

Package.onUse(function(api) {
  api.versionsFrom('METEOR@0.9.0');
  api.addFiles('ensure.js', 'server');
});
<commit_msg>Remove stack-trace; bump version.<commit_after>Package.describe({
  name: 'electronifiejs:meteor-ensure',
  summary: "ensure",
  version: "0.0.2",
  git: "https://github.com/electronifie/meteor-ensure.git"
});

Package.onUse(function(api) {
  api.versionsFrom('METEOR@0.9.0');
  api.addFiles('ensure.js', 'server');
});
<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Popup menu that is displayed when the user clicks on the bars in the bottom right in the navigation bar.
 * Would be nice to have a balloon triangle dropdown shape like in a comic book dialog.
 *
 * @author Sam Reid
 */
define( function( require ) {
  "use strict";

  var Node = require( 'SCENERY/nodes/Node' );
  var Shape = require( 'KITE/Shape' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Text = require( 'SCENERY/nodes/Text' );
  var inherit = require( 'PHET_CORE/inherit' );
  var AboutDialog = require( 'JOIST/AboutDialog' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var log = require( 'AXON/log' );

  var createMenuItem = function( text, width, callback ) {

    var menuItem = new Node( { cursor: 'pointer' } );

    var label = new Text( text, { fontSize: '18px' } );
    var xMargin = 5;
    var yMargin = 3;
    var cornerRadius = 5;
    var highlight = new Rectangle( 0, 0, width + xMargin + xMargin, label.height + yMargin + yMargin, cornerRadius, cornerRadius );

    menuItem.addChild( highlight );
    menuItem.addChild( label );

    label.left = highlight.left + xMargin;
    label.centerY = highlight.centerY;

    menuItem.addInputListener( {enter: function() {
      highlight.fill = '#a6d2f4';
    }, exit: function() {
      highlight.fill = null;
    }} );

    menuItem.addInputListener( new ButtonListener( {fire: callback } ) );
    return menuItem;
  };

  //TODO: The popup menu should scale with the size of the screen
  function PhetMenu( sim, options ) {

    options = _.extend( {renderer: 'svg'}, options ); //TODO add default options

    var simPopupMenu = this;
    Node.call( this );

    var maxItemWidth = 200;//TODO compute
    var homePageItem = createMenuItem( 'PhET Homepage', maxItemWidth, function() {
      window.open( "http://phet.colorado.edu" );
      window.focus();
    } );

    var outputLogItem = createMenuItem( 'Output log', maxItemWidth, function() {
      console.log( JSON.stringify( log.log ) );
    } );

    var aboutItem = createMenuItem( 'About...', maxItemWidth, function() {
      var aboutDialog = new AboutDialog( sim );
      sim.addChild( aboutDialog );
      var aboutDialogListener = {down: function() {
        aboutDialog.removeInputListener( aboutDialogListener );
        aboutDialog.detach();
      }};
      aboutDialog.addInputListener( aboutDialogListener );
    } );

    var items = [ homePageItem ];
    if ( log.enabled ) { items.push( outputLogItem ); }
    items.push( aboutItem );

    //left align the items

    //Compute bounds
    var widestItem = _.max( items, function( item ) {return item.width;} );
    var tallestItem = _.max( items, function( item ) {return item.height;} );

    var itemHeight = tallestItem.height;

    var verticalSpacing = 0;
    var padding = 5;
    var bubbleWidth = widestItem.width + padding * 2;
    var bubbleHeight = itemHeight * items.length + padding * 2 + verticalSpacing * (items.length - 1);

    var bubble = new Rectangle( 0, 0, bubbleWidth, bubbleHeight, 8, 8, {fill: 'white', lineWidth: 1, stroke: 'black'} );

    var tail = new Shape();
    tail.moveTo( bubbleWidth - 20, bubbleHeight - 2 );
    tail.lineToRelative( 0, 20 );
    tail.lineToRelative( -20, -20 );
    tail.close();

    this.addChild( bubble );
    this.addChild( new Path( {shape: tail, fill: 'white'} ) );

    var tailOutline = new Shape();
    tailOutline.moveTo( bubbleWidth - 20, bubbleHeight );
    tailOutline.lineToRelative( 0, 20 - 2 );
    tailOutline.lineToRelative( -18, -18 );
    this.addChild( new Path( {shape: tailOutline, stroke: 'black', lineWidth: 1} ) );

    var y = padding;
    _.each( items, function( item ) {
      item.top = y;
      item.left = padding;
      simPopupMenu.addChild( item );

      // Put a separator before the last item.
      if ( item === items[items.length - 2] ) {
        simPopupMenu.addChild( new Path( {shape: Shape.lineSegment( 8, y + itemHeight + verticalSpacing / 2, bubbleWidth - 8, y + itemHeight + verticalSpacing / 2 ), stroke: 'gray', lineWidth: 1} ) );
      }
      y += itemHeight + verticalSpacing;
    } );

    this.mutate( options );
  }

  inherit( Node, PhetMenu );

  return PhetMenu;
} );<commit_msg>Fixed hit area for phet menu items, closes #16<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * Popup menu that is displayed when the user clicks on the bars in the bottom right in the navigation bar.
 * Would be nice to have a balloon triangle dropdown shape like in a comic book dialog.
 *
 * @author Sam Reid
 */
define( function( require ) {
  "use strict";

  var Node = require( 'SCENERY/nodes/Node' );
  var Shape = require( 'KITE/Shape' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Text = require( 'SCENERY/nodes/Text' );
  var inherit = require( 'PHET_CORE/inherit' );
  var AboutDialog = require( 'JOIST/AboutDialog' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var log = require( 'AXON/log' );

  var FONT_SIZE = '18px';

  var createMenuItem = function( text, width, height, callback ) {

    var menuItem = new Node( { cursor: 'pointer' } );

    var textNode = new Text( text, { fontSize: FONT_SIZE } );
    var xMargin = 5;
    var yMargin = 3;
    var cornerRadius = 5;
    var highlight = new Rectangle( 0, 0, width + xMargin + xMargin, height + yMargin + yMargin, cornerRadius, cornerRadius );

    menuItem.addChild( highlight );
    menuItem.addChild( textNode );

    textNode.left = highlight.left + xMargin;
    textNode.centerY = highlight.centerY;

    menuItem.addInputListener( {enter: function() {
      highlight.fill = '#a6d2f4';
    }, exit: function() {
      highlight.fill = null;
    }} );

    menuItem.addInputListener( new ButtonListener( {fire: callback } ) );
    return menuItem;
  };

  //TODO: The popup menu should scale with the size of the screen
  function PhetMenu( sim, options ) {

    options = _.extend( {renderer: 'svg'}, options ); //TODO add default options

    var simPopupMenu = this;
    Node.call( this );

    var itemDescriptors = [
      {
        text: 'PhET Homepage', present: true, callback: function() {
        window.open( "http://phet.colorado.edu" );
        window.focus();
      }},
      {
        text: 'Output Log', present: log.enabled ? true : false, callback: function() {
        console.log( JSON.stringify( log.log ) );
      }},
      {
        text: 'About...', present: true, callback: function() {
        var aboutDialog = new AboutDialog( sim );
        sim.addChild( aboutDialog );
        var aboutDialogListener = {down: function() {
          aboutDialog.removeInputListener( aboutDialogListener );
          aboutDialog.detach();
        }};
        aboutDialog.addInputListener( aboutDialogListener );
      }}
    ];

    var keepItemDescriptors = _.filter( itemDescriptors, function( itemDescriptor ) {return itemDescriptor.present;} );
    var textNodes = _.map( keepItemDescriptors, function( item ) {return new Text( item.text, {fontSize: FONT_SIZE} );} );

    //Compute bounds
    var widestText = _.max( textNodes, function( node ) {return node.width;} );
    var tallestText = _.max( textNodes, function( node ) {return node.height;} );

    var items = _.map( itemDescriptors, function( itemDescriptor ) {
      return createMenuItem( itemDescriptor.text, widestText.width, tallestText.height, itemDescriptor.callback );
    } );

    var itemWidth = _.max( items,function( item ) {return item.width;} ).width;
    var itemHeight = _.max( items,function( item ) {return item.height;} ).height;

    var verticalSpacing = 0;
    var padding = 5;
    var bubbleWidth = itemWidth + padding * 2;
    var bubbleHeight = itemHeight * items.length + padding * 2 + verticalSpacing * (items.length - 1);

    var bubble = new Rectangle( 0, 0, bubbleWidth, bubbleHeight, 8, 8, {fill: 'white', lineWidth: 1, stroke: 'black'} );

    var tail = new Shape();
    tail.moveTo( bubbleWidth - 20, bubbleHeight - 2 );
    tail.lineToRelative( 0, 20 );
    tail.lineToRelative( -20, -20 );
    tail.close();

    this.addChild( bubble );
    this.addChild( new Path( {shape: tail, fill: 'white'} ) );

    var tailOutline = new Shape();
    tailOutline.moveTo( bubbleWidth - 20, bubbleHeight );
    tailOutline.lineToRelative( 0, 20 - 2 );
    tailOutline.lineToRelative( -18, -18 );
    this.addChild( new Path( {shape: tailOutline, stroke: 'black', lineWidth: 1} ) );

    var y = padding;
    _.each( items, function( item ) {
      item.top = y;
      item.left = padding;
      simPopupMenu.addChild( item );

      // Put a separator before the last item.
      if ( item === items[items.length - 2] ) {
        simPopupMenu.addChild( new Path( {shape: Shape.lineSegment( 8, y + itemHeight + verticalSpacing / 2, bubbleWidth - 8, y + itemHeight + verticalSpacing / 2 ), stroke: 'gray', lineWidth: 1} ) );
      }
      y += itemHeight + verticalSpacing;
    } );

    this.mutate( options );
  }

  inherit( Node, PhetMenu );

  return PhetMenu;
} );<|endoftext|>


<commit_before>Package.describe({
  name: "brylie:api-umbrella",
  summary: "Wrapper for API Umbrella Admin API.",
  version: "0.1.1",
  git: "https://github.com/brylie/meteor-api-umbrella"
});

Package.onUse(function(api) {
  api.versionsFrom("1.0.1");
  api.use(["meteor"]);

  api.addFiles("server/web-admin-api.js", ["server"]);

  api.export("apiUmbrella", ["server"]);
});

Package.onTest(function (api) {
  api.use("tinytest");
  api.use("brylie:api-umbrella");

  api.addFiles("tests/server/web-admin-api.js", ["server"]);
});
<commit_msg>Bumped package version.<commit_after>Package.describe({
  name: "brylie:api-umbrella",
  summary: "Wrapper for API Umbrella Admin API.",
  version: "0.1.2",
  git: "https://github.com/brylie/meteor-api-umbrella"
});

Package.onUse(function(api) {
  api.versionsFrom("1.0.1");
  api.use(["meteor"]);

  api.addFiles("server/web-admin-api.js", ["server"]);

  api.export("apiUmbrella", ["server"]);
});

Package.onTest(function (api) {
  api.use("tinytest");
  api.use("brylie:api-umbrella");

  api.addFiles("tests/server/web-admin-api.js", ["server"]);
});
<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * The 'PhET' menu.
 *
 * @author Sam Reid
 */
define( function( require ) {
  'use strict';

  // imports
  var Node = require( 'SCENERY/nodes/Node' );
  var Shape = require( 'KITE/Shape' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Text = require( 'SCENERY/nodes/Text' );
  var inherit = require( 'PHET_CORE/inherit' );
  var AboutDialog = require( 'JOIST/AboutDialog' );
  var SettingsDialog = require( 'JOIST/SettingsDialog' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Plane = require( 'SCENERY/nodes/Plane' );
  var log = require( 'AXON/log' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );

  // constants
  var FONT_SIZE = 18;
  var HIGHLIGHT_COLOR = '#a6d2f4';

  // Creates a menu item that highlights and fires.
  var createMenuItem = function( text, width, height, callback, immediateCallback ) {

    var X_MARGIN = 5;
    var Y_MARGIN = 3;
    var CORNER_RADIUS = 5;

    var textNode = new Text( text, { font: new PhetFont( FONT_SIZE ) } );
    var highlight = new Rectangle( 0, 0, width + X_MARGIN + X_MARGIN, height + Y_MARGIN + Y_MARGIN, CORNER_RADIUS, CORNER_RADIUS );

    var menuItem = new Node( { cursor: 'pointer' } );
    menuItem.addChild( highlight );
    menuItem.addChild( textNode );

    textNode.left = highlight.left + X_MARGIN; // text is left aligned
    textNode.centerY = highlight.centerY;

    menuItem.addInputListener( {
      enter: function() { highlight.fill = HIGHLIGHT_COLOR; },
      exit: function() { highlight.fill = null; },
      upImmediate: function() { immediateCallback && immediateCallback(); }
    } );
    menuItem.addInputListener( new ButtonListener( {fire: callback } ) );

    return menuItem;
  };

  // Creates a comic-book style bubble.
  var createBubble = function( width, height ) {

    var rectangle = new Rectangle( 0, 0, width, height, 8, 8, {fill: 'white', lineWidth: 1, stroke: 'black'} );

    var tail = new Shape();
    tail.moveTo( width - 20, height - 2 );
    tail.lineToRelative( 0, 20 );
    tail.lineToRelative( -20, -20 );
    tail.close();

    var tailOutline = new Shape();
    tailOutline.moveTo( width - 20, height );
    tailOutline.lineToRelative( 0, 20 - 2 );
    tailOutline.lineToRelative( -18, -18 );

    var bubble = new Node();
    bubble.addChild( rectangle );
    bubble.addChild( new Path( tail, {fill: 'white'} ) );
    bubble.addChild( new Path( tailOutline, {stroke: 'black', lineWidth: 1} ) );

    return bubble;
  };

  //TODO: The popup menu should scale with the size of the screen
  function PhetMenu( sim, options ) {

    options = _.extend( {renderer: 'svg'}, options );

    var thisMenu = this;
    Node.call( thisMenu );

    /*
     * Description of the items in the menu. Each descriptor has these properties:
     * {String} text - the item's text
     * {Boolean} present - whether the item should be added to the menu
     * {Function} callback - called when the item fires
     */
    var itemDescriptors = [
      {
        text: 'PhET website',
        present: true,
        callback: function() {
        },
        immediateCallback: function() {
          var phetWindow = window.open( 'http://phet.colorado.edu', '_blank' );
          phetWindow.focus();
        } },
      {
        text: 'Output Log',
        present: log.enabled ? true : false, // because double-negation (!!) coercion doesn't look as cool?
        callback: function() {
          console.log( JSON.stringify( log.entries ) );
        }},
      {
        text: 'Output Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
          // prints the recorded input event log to the console
          console.log( sim.getRecordedInputEventLogString() );
        }},
      {
        text: 'Submit Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
          // submits a recorded event log to the same-origin server (run scenery/tests/event-logs/server/server.js with Node, from the same directory)
          sim.submitEventLog();
        }},
      {
        text: 'Mail Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
        },
        immediateCallback: function() {
          // mailto: link including the body to email
          sim.mailEventLog();
        }},
      {
        text: 'Settings',
        present: true,
        callback: function() {
          var settingsDialog = new SettingsDialog( sim );
          var plane = new Plane( {fill: 'black', opacity: 0.3} );
          sim.addChild( plane );
          sim.addChild( settingsDialog );
          settingsDialog.addDoneListener( function() {
            plane.detach();
            settingsDialog.detach();
          } );
        }
      },
      {
        text: 'About...',
        present: true,
        callback: function() {
          var aboutDialog = new AboutDialog( sim );
          var plane = new Plane( {fill: 'black', opacity: 0.3} );
          sim.addChild( plane );
          sim.addChild( aboutDialog );
          var aboutDialogListener = {up: function() {
            aboutDialog.removeInputListener( aboutDialogListener );
            plane.addInputListener( aboutDialogListener );
            aboutDialog.detach();
            plane.detach();
          }};
          aboutDialog.addInputListener( aboutDialogListener );
          plane.addInputListener( aboutDialogListener );
        }}
    ];

    // Menu items have uniform size, so compute the max text dimensions.
    var keepItemDescriptors = _.filter( itemDescriptors, function( itemDescriptor ) {return itemDescriptor.present;} );
    var textNodes = _.map( keepItemDescriptors, function( item ) {return new Text( item.text, {font: new PhetFont( FONT_SIZE )} );} );
    var maxTextWidth = _.max( textNodes,function( node ) {return node.width;} ).width;
    var maxTextHeight = _.max( textNodes,function( node ) {return node.height;} ).height;

    // Create the menu items.
    var items = _.map( keepItemDescriptors, function( itemDescriptor ) {
      return createMenuItem( itemDescriptor.text, maxTextWidth, maxTextHeight, itemDescriptor.callback, itemDescriptor.immediateCallback );
    } );

    // Create a comic-book-style bubble.
    var itemWidth = _.max( items,function( item ) {return item.width;} ).width;
    var itemHeight = _.max( items,function( item ) {return item.height;} ).height;
    var X_MARGIN = 5;
    var Y_MARGIN = 5;
    var bubbleWidth = itemWidth + X_MARGIN + X_MARGIN;
    var bubbleHeight = itemHeight * items.length + Y_MARGIN + Y_MARGIN;
    thisMenu.addChild( createBubble( bubbleWidth, bubbleHeight ) );

    // Populate the bubble with menu items.
    var y = Y_MARGIN;
    _.each( items, function( item ) {
      item.top = y;
      item.left = X_MARGIN;
      thisMenu.addChild( item );

      //TODO separators should be specified in itemDescriptors
      // Put a separator before the last item.
      if ( item === items[items.length - 2] ) {
        thisMenu.addChild( new Path( Shape.lineSegment( 8, y + itemHeight, bubbleWidth - 8, y + itemHeight ), {stroke: 'gray', lineWidth: 1} ) );
      }
      y += itemHeight;
    } );

    thisMenu.mutate( options );
  }

  inherit( Node, PhetMenu );

  return PhetMenu;
} );
<commit_msg>change "Settings" to "Settings...", because it opens a dialog<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * The 'PhET' menu.
 *
 * @author Sam Reid
 */
define( function( require ) {
  'use strict';

  // imports
  var Node = require( 'SCENERY/nodes/Node' );
  var Shape = require( 'KITE/Shape' );
  var Path = require( 'SCENERY/nodes/Path' );
  var Text = require( 'SCENERY/nodes/Text' );
  var inherit = require( 'PHET_CORE/inherit' );
  var AboutDialog = require( 'JOIST/AboutDialog' );
  var SettingsDialog = require( 'JOIST/SettingsDialog' );
  var Rectangle = require( 'SCENERY/nodes/Rectangle' );
  var ButtonListener = require( 'SCENERY/input/ButtonListener' );
  var Plane = require( 'SCENERY/nodes/Plane' );
  var log = require( 'AXON/log' );
  var PhetFont = require( 'SCENERY_PHET/PhetFont' );

  // constants
  var FONT_SIZE = 18;
  var HIGHLIGHT_COLOR = '#a6d2f4';

  // Creates a menu item that highlights and fires.
  var createMenuItem = function( text, width, height, callback, immediateCallback ) {

    var X_MARGIN = 5;
    var Y_MARGIN = 3;
    var CORNER_RADIUS = 5;

    var textNode = new Text( text, { font: new PhetFont( FONT_SIZE ) } );
    var highlight = new Rectangle( 0, 0, width + X_MARGIN + X_MARGIN, height + Y_MARGIN + Y_MARGIN, CORNER_RADIUS, CORNER_RADIUS );

    var menuItem = new Node( { cursor: 'pointer' } );
    menuItem.addChild( highlight );
    menuItem.addChild( textNode );

    textNode.left = highlight.left + X_MARGIN; // text is left aligned
    textNode.centerY = highlight.centerY;

    menuItem.addInputListener( {
      enter: function() { highlight.fill = HIGHLIGHT_COLOR; },
      exit: function() { highlight.fill = null; },
      upImmediate: function() { immediateCallback && immediateCallback(); }
    } );
    menuItem.addInputListener( new ButtonListener( {fire: callback } ) );

    return menuItem;
  };

  // Creates a comic-book style bubble.
  var createBubble = function( width, height ) {

    var rectangle = new Rectangle( 0, 0, width, height, 8, 8, {fill: 'white', lineWidth: 1, stroke: 'black'} );

    var tail = new Shape();
    tail.moveTo( width - 20, height - 2 );
    tail.lineToRelative( 0, 20 );
    tail.lineToRelative( -20, -20 );
    tail.close();

    var tailOutline = new Shape();
    tailOutline.moveTo( width - 20, height );
    tailOutline.lineToRelative( 0, 20 - 2 );
    tailOutline.lineToRelative( -18, -18 );

    var bubble = new Node();
    bubble.addChild( rectangle );
    bubble.addChild( new Path( tail, {fill: 'white'} ) );
    bubble.addChild( new Path( tailOutline, {stroke: 'black', lineWidth: 1} ) );

    return bubble;
  };

  //TODO: The popup menu should scale with the size of the screen
  function PhetMenu( sim, options ) {

    options = _.extend( {renderer: 'svg'}, options );

    var thisMenu = this;
    Node.call( thisMenu );

    /*
     * Description of the items in the menu. Each descriptor has these properties:
     * {String} text - the item's text
     * {Boolean} present - whether the item should be added to the menu
     * {Function} callback - called when the item fires
     */
    var itemDescriptors = [
      {
        text: 'PhET website',
        present: true,
        callback: function() {
        },
        immediateCallback: function() {
          var phetWindow = window.open( 'http://phet.colorado.edu', '_blank' );
          phetWindow.focus();
        } },
      {
        text: 'Output Log',
        present: log.enabled ? true : false, // because double-negation (!!) coercion doesn't look as cool?
        callback: function() {
          console.log( JSON.stringify( log.entries ) );
        }},
      {
        text: 'Output Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
          // prints the recorded input event log to the console
          console.log( sim.getRecordedInputEventLogString() );
        }},
      {
        text: 'Submit Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
          // submits a recorded event log to the same-origin server (run scenery/tests/event-logs/server/server.js with Node, from the same directory)
          sim.submitEventLog();
        }},
      {
        text: 'Mail Input Events Log',
        present: !!sim.options.recordInputEventLog,
        callback: function() {
        },
        immediateCallback: function() {
          // mailto: link including the body to email
          sim.mailEventLog();
        }},
      {
        text: 'Settings...',
        present: true,
        callback: function() {
          var settingsDialog = new SettingsDialog( sim );
          var plane = new Plane( {fill: 'black', opacity: 0.3} );
          sim.addChild( plane );
          sim.addChild( settingsDialog );
          settingsDialog.addDoneListener( function() {
            plane.detach();
            settingsDialog.detach();
          } );
        }
      },
      {
        text: 'About...',
        present: true,
        callback: function() {
          var aboutDialog = new AboutDialog( sim );
          var plane = new Plane( {fill: 'black', opacity: 0.3} );
          sim.addChild( plane );
          sim.addChild( aboutDialog );
          var aboutDialogListener = {up: function() {
            aboutDialog.removeInputListener( aboutDialogListener );
            plane.addInputListener( aboutDialogListener );
            aboutDialog.detach();
            plane.detach();
          }};
          aboutDialog.addInputListener( aboutDialogListener );
          plane.addInputListener( aboutDialogListener );
        }}
    ];

    // Menu items have uniform size, so compute the max text dimensions.
    var keepItemDescriptors = _.filter( itemDescriptors, function( itemDescriptor ) {return itemDescriptor.present;} );
    var textNodes = _.map( keepItemDescriptors, function( item ) {return new Text( item.text, {font: new PhetFont( FONT_SIZE )} );} );
    var maxTextWidth = _.max( textNodes,function( node ) {return node.width;} ).width;
    var maxTextHeight = _.max( textNodes,function( node ) {return node.height;} ).height;

    // Create the menu items.
    var items = _.map( keepItemDescriptors, function( itemDescriptor ) {
      return createMenuItem( itemDescriptor.text, maxTextWidth, maxTextHeight, itemDescriptor.callback, itemDescriptor.immediateCallback );
    } );

    // Create a comic-book-style bubble.
    var itemWidth = _.max( items,function( item ) {return item.width;} ).width;
    var itemHeight = _.max( items,function( item ) {return item.height;} ).height;
    var X_MARGIN = 5;
    var Y_MARGIN = 5;
    var bubbleWidth = itemWidth + X_MARGIN + X_MARGIN;
    var bubbleHeight = itemHeight * items.length + Y_MARGIN + Y_MARGIN;
    thisMenu.addChild( createBubble( bubbleWidth, bubbleHeight ) );

    // Populate the bubble with menu items.
    var y = Y_MARGIN;
    _.each( items, function( item ) {
      item.top = y;
      item.left = X_MARGIN;
      thisMenu.addChild( item );

      //TODO separators should be specified in itemDescriptors
      // Put a separator before the last item.
      if ( item === items[items.length - 2] ) {
        thisMenu.addChild( new Path( Shape.lineSegment( 8, y + itemHeight, bubbleWidth - 8, y + itemHeight ), {stroke: 'gray', lineWidth: 1} ) );
      }
      y += itemHeight;
    } );

    thisMenu.mutate( options );
  }

  inherit( Node, PhetMenu );

  return PhetMenu;
} );
<|endoftext|>


<commit_before>var auto = require('auto-package')
var content = auto.content
var policystat = require('policystat')

content.name = 'edited'
auto.versionFile()
content.description = 'Listens on editable elements and calls back on significant changes'
content.main = 'lib/Edited/index.js'
content.dependencies = {
  'add-event-handler': '^1.0.0',
  'dom-events': '^0.1.1',
  'keycode': '^2.0.0',
  'is_element': '^0.0.11'
}
content.devDependencies = {
  mightyiam: '^1.1.5',
  policystat: '^1.1.0',
  'auto-package': '^0.2.0',
  'es5-shim': '^4.1.0',
  'foreach': '^2.0.5',
  'jasmine-core': '^2.2.0',
  'karma': '^0.12.31',
  'karma-browserify': '^3.0.2',
  'karma-chrome-launcher': '^0.1.7',
  'karma-cli': '0.0.4',
  'karma-firefox-launcher': '^0.1.4',
  'karma-jasmine': '^0.3.5',
  'keysim': '^1.1.2',
  'randomatic': '^1.1.0',
  'selenium-server': '^2.44.0',
  'standard': '^2.4.4',
  'license-generator': '^0.0.13',
  'verb-cli': '^0.4.4'
}
content.repository = {
  'type': 'git',
  'url': 'http://github.com/mightyiam/edited.git'
}
content.scripts = {
  'license': 'license-generator install bsd-3-clause -n "' + policystat.name.pretty + '"',
  'docs': 'verb',
  'lint': 'standard',
  'test-browsers': 'karma start',
  'test': 'npm run license && npm run docs && npm run lint && npm run test-browsers'
}
content.keywords = [
  'contentEditable',
  'designMode',
  'undo',
  'redo'
]
content.author = require('mightyiam').authorStr
content.license = {
  type: 'MIT'
}
content.copyright = policystat.copyrightNotice
<commit_msg>package.js: fix repo url<commit_after>var auto = require('auto-package')
var content = auto.content
var policystat = require('policystat')

content.name = 'edited'
auto.versionFile()
content.description = 'Listens on editable elements and calls back on significant changes'
content.main = 'lib/Edited/index.js'
content.dependencies = {
  'add-event-handler': '^1.0.0',
  'dom-events': '^0.1.1',
  'keycode': '^2.0.0',
  'is_element': '^0.0.11'
}
content.devDependencies = {
  mightyiam: '^1.1.5',
  policystat: '^1.1.0',
  'auto-package': '^0.2.0',
  'es5-shim': '^4.1.0',
  'foreach': '^2.0.5',
  'jasmine-core': '^2.2.0',
  'karma': '^0.12.31',
  'karma-browserify': '^3.0.2',
  'karma-chrome-launcher': '^0.1.7',
  'karma-cli': '0.0.4',
  'karma-firefox-launcher': '^0.1.4',
  'karma-jasmine': '^0.3.5',
  'keysim': '^1.1.2',
  'randomatic': '^1.1.0',
  'selenium-server': '^2.44.0',
  'standard': '^2.4.4',
  'license-generator': '^0.0.13',
  'verb-cli': '^0.4.4'
}
content.repository = {
  'type': 'git',
  'url': 'http://github.com/PolicyStat/edited.git'
}
content.scripts = {
  'license': 'license-generator install bsd-3-clause -n "' + policystat.name.pretty + '"',
  'docs': 'verb',
  'lint': 'standard',
  'test-browsers': 'karma start',
  'test': 'npm run license && npm run docs && npm run lint && npm run test-browsers'
}
content.keywords = [
  'contentEditable',
  'designMode',
  'undo',
  'redo'
]
content.author = require('mightyiam').authorStr
content.license = {
  type: 'MIT'
}
content.copyright = policystat.copyrightNotice
<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/**
 * An observable property, notifies registered observers when the value changes.
 *
 * Uses the 'Constructor' pattern for object creation, which has the downside that
 * all properties are created once for each instance. It would be nice if our functions
 * were shared. But since the only way to create private fields is in the constructor,
 * and the functions need access to those private fields, there doesn't seem to be
 * any choice but to define the functions in the constructor.
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );

  // Also requires Multilink and DerivedProperty, but cannot reference them here or it will create a
  // circular dependency.  So they are loaded through axon.Multilink and axon.DerivedProperty.

  /**
   * @param {*} value - the initial value of the property
   * @param {Object} [options] - optional values for the property, see below
   * @constructor
   */
  axon.Property = function Property( value, options ) {

    options = _.extend( {
      componentID: null
    }, options );

    this.componentID = options.componentID;

    //Store the internal value and the initial value
    this.storeValue( value );        // typically sets this._value
    this.storeInitialValue( value ); // typically sets this._initialValue
    this._observers = [];

    // Many sim Properties are completely internal to the simulation, so only register with the together.js api
    // if the componentID is non-null
    if ( this.componentID ) {
      together && together.addComponent( this );
    }
  };

  return inherit( Object, axon.Property, {

      /**
       * Gets the value.  You can also use the es5 getter (property.value) but this means is provided for inner loops or internal code that must be fast.
       * @return {*}
       */
      get: function() {
        return this._value;
      },

      /**
       * Sets the value and notifies registered observers.  You can also use the es5 getter (property.value) but this means is provided for inner loops or internal code that must be fast.
       * If the value hasn't changed, this is a no-op.
       *
       * @param {*} value
       */
      set: function( value ) {
        if ( !this.equalsValue( value ) ) {
          this._setAndNotifyObservers( value );
        }
        return this;
      },

      // whether this property will not "change" when the passed-in value is set
      equalsValue: function( value ) {
        return value === this._value;
      },

      // store the current (new) value
      storeValue: function( value ) {
        this._value = value;
      },

      // store the initial value
      storeInitialValue: function( value ) {
        this._initialValue = value;
      },

      get initialValue() {
        return this._initialValue;
      },

      _setAndNotifyObservers: function( value ) {
        var oldValue = this.get();
        this.storeValue( value );
        this._notifyObservers( oldValue );
      },

      _notifyObservers: function( oldValue ) {

        // Note the current value, since it will be sent to possibly multiple observers.
        var value = this.get();

        // Format values for arch.  Otherwise things like Property(Solute) will output arch messages that are
        // a big JSON instance describing the entire Solute structure.  We would rather just receive an ID
        // or a nickname in cases like that.
        var oldValueForArch = null;
        var newValueForArch = null;

        if ( arch && this.componentID ) {
          var archValueType = together.getType( this.componentID ).valueType;
          if ( archValueType.formatForArch ) {
            oldValueForArch = archValueType.formatForArch( oldValue );
            newValueForArch = archValueType.formatForArch( value );
          }
          else {
            oldValueForArch = oldValue;
            newValueForArch = value;
          }
        }

        // If enabled, send a message to phet events.  Avoid as much work as possible if phet.arch is inactive.
        var messageIndex = arch && this.componentID && arch.start( 'model', this.componentID, 'changed', {
            oldValue: oldValueForArch,
            newValue: newValueForArch
          } );

        // TODO: JO: avoid slice() by storing observers array correctly
        var observersCopy = this._observers.slice(); // make a copy, in case notification results in removeObserver
        for ( var i = 0; i < observersCopy.length; i++ ) {
          observersCopy[ i ]( value, oldValue );
        }

        // Send the end message to phet.arch
        messageIndex && arch.end( messageIndex );
      },

      /**
       * Use this method when mutating a value (not replacing with a new instance) and you want to send notifications about the change.
       * This is different from the normal axon strategy, but may be necessary to prevent memory allocations.
       * This method is unsafe for removing observers because it assumes the observer list not modified, to save another allocation
       * Only provides the new reference as a callback (no oldvalue)
       * See https://github.com/phetsims/axon/issues/6
       */
      notifyObserversStatic: function() {
        var value = this.get();
        for ( var i = 0; i < this._observers.length; i++ ) {
          this._observers[ i ]( value );
        }
      },

      /**
       * Resets the value to the initial value.
       */
      reset: function() {
        this.set( this._initialValue );
      },

      /**
       * This function returns a bound function that sets the specified value.  For use in creating closures e.g. with gui classes.
       * For instance, to have a button that sets a property to true, instead of using
       * button.click(function(){property.set(true);});
       * you could use
       * button.click(property._set(true));
       * @param value the value to use when the setter is called.
       * @return a function that can be used to set the specified value.
       */
      _set: function( value ) {
        return this.set.bind( this, value );
      },

      get value() { return this.get(); },

      set value( newValue ) { this.set( newValue ); },

      /**
       * Adds an observer and notifies it immediately.
       * If observer is already registered, this is a no-op.
       * The initial notification provides the current value for newValue and null for oldValue.
       *
       * @param {function} observer a function of the form observer(newValue,oldValue)
       */
      link: function( observer ) {
        if ( this._observers.indexOf( observer ) === -1 ) {
          this._observers.push( observer );
          observer( this.get(), null ); // null should be used when an object is expected but unavailable
        }
      },

      /**
       * Add an observer to the Property, without calling it back right away.  This is used when you need to register a observer without an immediate callback.
       * @param {function} observer  a function with a single argument, which is the value of the property at the time the function is called.
       */
      lazyLink: function( observer ) {
        if ( this._observers.indexOf( observer ) === -1 ) {
          this._observers.push( observer );
        }
      },

      /**
       * Removes an observer.
       * If observer is not registered, this is a no-op.
       *
       * @param {function} observer
       */
      unlink: function( observer ) {
        var index = this._observers.indexOf( observer );
        if ( index !== -1 ) {
          this._observers.splice( index, 1 );
        }
      },

      /**
       * Links an object's named attribute to this property.  Returns a handle so it can be removed using Property.unlink();
       * Example: modelVisibleProperty.linkAttribute(view,'visible');
       *
       * @param object
       * @param attributeName
       */
      linkAttribute: function( object, attributeName ) {
        var handle = function( value ) {object[ attributeName ] = value;};
        this.link( handle );
        return handle;
      },

      /**
       * Unlink an observer added with linkAttribute.  Note: the args of linkAttribute do not match the args of
       * unlinkAttribute: here, you must pass the observer handle returned by linkAttribute rather than object and attributeName
       * @param observer
       */
      unlinkAttribute: function( observer ) {
        this.unlink( observer );
      },

      //Provide toString for console debugging, see http://stackoverflow.com/questions/2485632/valueof-vs-tostring-in-javascript
      toString: function() {return 'Property{' + this.get() + '}'; },
      valueOf: function() {return this.toString();},

      /**
       * Add an observer so that it will only fire once (and not on registration)
       *
       * I can see two ways to implement this:
       * (a) add a field to the observer so after notifications it can be checked and possibly removed. Disadvantage: will make everything slower even if not using 'once'
       * (b) wrap the observer in a new function which will call the observer and then remove itself.  Disadvantage: cannot remove an observer added using 'once'
       * To avoid possible performance problems, use a wrapper function, and return it as a handle in case the 'once' observer must be removed before it is called once
       *
       * @param observer the observer which should be called back only for one property change (and not on registration)
       * @returns {function} the wrapper handle in case the wrapped function needs to be removed with 'unlink' before it is called once
       */
      once: function( observer ) {
        var property = this;
        var wrapper = function( newValue, oldValue ) {
          property.unlink( wrapper );
          observer( newValue, oldValue );
        };
        this.lazyLink( wrapper );
        return wrapper;
      },

      /**
       * Convenience function for debugging a property values.  It prints the new value on registration and when changed.
       * @param name debug name to be printed on the console
       * @returns {function} the handle to the linked observer in case it needs to be removed later
       */
      debug: function( name ) {
        var observer = function( value ) { console.log( name, value ); };
        this.link( observer );
        return observer;
      },

      /**
       * Returns a function that can be used to toggle the property (using !)
       * @returns {function}
       */
      get toggleFunction() {
        return this.toggle.bind( this );
      },

      /**
       * Modifies the value of this Property with the ! operator.  Works for booleans and non-booleans.
       */
      toggle: function() {
        this.value = !this.value;
      },

      /**
       * Adds an observer that is fired when the property takes the specified value.  If the property has the value already,
       * the observer is called back immediately.  A reference to the observer is returned so that it can be removed.
       *
       * @param value the value to match
       * @param observer the observer that is called when this Property
       */
      onValue: function( value, observer ) {
        var onValueObserver = function( v ) {
          if ( v === value ) {
            observer();
          }
        };
        this.link( onValueObserver );
        return onValueObserver;
      },

      /**
       * When the Property is no longer used by the sim, it can be eliminated.  All listeners are removed
       * and it is unregistered from any pertinent modules.
       */
      dispose: function() {

        while ( this._observers.length > 0 ) {
          this.unlink( this._observers[ 0 ] );
        }

        if ( this.componentID ) {
          together && together.removeComponent( this );
        }
      }
    },

    //statics
    {

      /**
       * Registers an observer with multiple properties, then notifies the observer immediately.
       * @param {Property[]} properties
       * @param {function} observer no params, returns nothing
       * @static
       */
      multilink: function( properties, observer ) {
        return new axon.Multilink( properties, observer, false );
      },

      lazyMultilink: function( properties, observer ) {
        return new axon.Multilink( properties, observer, true );
      },

      /**
       * Removes the multilinked observer from this Property.
       * Same as calling detach() on the handle (which happens to be a DerivedProperty instance)
       * @param derivedProperty
       */
      unmultilink: function( derivedProperty ) {
        derivedProperty.detach();
      },

      /**
       * Set up a PropertySet-like property on any object (see https://github.com/phetsims/axon/issues/42).
       *
       * @param {Object} object - The object that the property will be placed on
       * @param {string} propertyName - Name of the property
       * @param {*} initialValue - The initial value of the property
       */
      addProperty: function( object, propertyName, initialValue ) {
        // defines the property
        var property = this[ propertyName + 'Property' ] = new axon.Property( initialValue );

        // defines ES5 getter/setter
        Object.defineProperty( this, propertyName, {
          get: function() { return property.get(); },
          set: function( value ) { property.set( value ); },

          // Make it configurable and enumerable so it's easy to override...
          configurable: true,
          enumerable: true
        } );
      }
    } );
} );
<commit_msg>Renamed formatForArch => formatForEventStream, see https://github.com/phetsims/concentration/issues/38<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/**
 * An observable property, notifies registered observers when the value changes.
 *
 * Uses the 'Constructor' pattern for object creation, which has the downside that
 * all properties are created once for each instance. It would be nice if our functions
 * were shared. But since the only way to create private fields is in the constructor,
 * and the functions need access to those private fields, there doesn't seem to be
 * any choice but to define the functions in the constructor.
 *
 * @author Sam Reid
 * @author Chris Malley (PixelZoom, Inc.)
 */
define( function( require ) {
  'use strict';

  // modules
  var axon = require( 'AXON/axon' );
  var inherit = require( 'PHET_CORE/inherit' );

  // Also requires Multilink and DerivedProperty, but cannot reference them here or it will create a
  // circular dependency.  So they are loaded through axon.Multilink and axon.DerivedProperty.

  /**
   * @param {*} value - the initial value of the property
   * @param {Object} [options] - optional values for the property, see below
   * @constructor
   */
  axon.Property = function Property( value, options ) {

    options = _.extend( {
      componentID: null
    }, options );

    this.componentID = options.componentID;

    //Store the internal value and the initial value
    this.storeValue( value );        // typically sets this._value
    this.storeInitialValue( value ); // typically sets this._initialValue
    this._observers = [];

    // Many sim Properties are completely internal to the simulation, so only register with the together.js api
    // if the componentID is non-null
    if ( this.componentID ) {
      together && together.addComponent( this );
    }
  };

  return inherit( Object, axon.Property, {

      /**
       * Gets the value.  You can also use the es5 getter (property.value) but this means is provided for inner loops or internal code that must be fast.
       * @return {*}
       */
      get: function() {
        return this._value;
      },

      /**
       * Sets the value and notifies registered observers.  You can also use the es5 getter (property.value) but this means is provided for inner loops or internal code that must be fast.
       * If the value hasn't changed, this is a no-op.
       *
       * @param {*} value
       */
      set: function( value ) {
        if ( !this.equalsValue( value ) ) {
          this._setAndNotifyObservers( value );
        }
        return this;
      },

      // whether this property will not "change" when the passed-in value is set
      equalsValue: function( value ) {
        return value === this._value;
      },

      // store the current (new) value
      storeValue: function( value ) {
        this._value = value;
      },

      // store the initial value
      storeInitialValue: function( value ) {
        this._initialValue = value;
      },

      get initialValue() {
        return this._initialValue;
      },

      _setAndNotifyObservers: function( value ) {
        var oldValue = this.get();
        this.storeValue( value );
        this._notifyObservers( oldValue );
      },

      _notifyObservers: function( oldValue ) {

        // Note the current value, since it will be sent to possibly multiple observers.
        var value = this.get();

        // Format values for arch.  Otherwise things like Property(Solute) will output arch messages that are
        // a big JSON instance describing the entire Solute structure.  We would rather just receive an ID
        // or a nickname in cases like that.
        var oldValueForArch = null;
        var newValueForArch = null;

        if ( arch && this.componentID ) {
          var archValueType = together.getType( this.componentID ).valueType;
          if ( archValueType.formatForEventStream ) {
            oldValueForArch = archValueType.formatForEventStream( oldValue );
            newValueForArch = archValueType.formatForEventStream( value );
          }
          else {
            oldValueForArch = oldValue;
            newValueForArch = value;
          }
        }

        // If enabled, send a message to phet events.  Avoid as much work as possible if phet.arch is inactive.
        var messageIndex = arch && this.componentID && arch.start( 'model', this.componentID, 'changed', {
            oldValue: oldValueForArch,
            newValue: newValueForArch
          } );

        // TODO: JO: avoid slice() by storing observers array correctly
        var observersCopy = this._observers.slice(); // make a copy, in case notification results in removeObserver
        for ( var i = 0; i < observersCopy.length; i++ ) {
          observersCopy[ i ]( value, oldValue );
        }

        // Send the end message to phet.arch
        messageIndex && arch.end( messageIndex );
      },

      /**
       * Use this method when mutating a value (not replacing with a new instance) and you want to send notifications about the change.
       * This is different from the normal axon strategy, but may be necessary to prevent memory allocations.
       * This method is unsafe for removing observers because it assumes the observer list not modified, to save another allocation
       * Only provides the new reference as a callback (no oldvalue)
       * See https://github.com/phetsims/axon/issues/6
       */
      notifyObserversStatic: function() {
        var value = this.get();
        for ( var i = 0; i < this._observers.length; i++ ) {
          this._observers[ i ]( value );
        }
      },

      /**
       * Resets the value to the initial value.
       */
      reset: function() {
        this.set( this._initialValue );
      },

      /**
       * This function returns a bound function that sets the specified value.  For use in creating closures e.g. with gui classes.
       * For instance, to have a button that sets a property to true, instead of using
       * button.click(function(){property.set(true);});
       * you could use
       * button.click(property._set(true));
       * @param value the value to use when the setter is called.
       * @return a function that can be used to set the specified value.
       */
      _set: function( value ) {
        return this.set.bind( this, value );
      },

      get value() { return this.get(); },

      set value( newValue ) { this.set( newValue ); },

      /**
       * Adds an observer and notifies it immediately.
       * If observer is already registered, this is a no-op.
       * The initial notification provides the current value for newValue and null for oldValue.
       *
       * @param {function} observer a function of the form observer(newValue,oldValue)
       */
      link: function( observer ) {
        if ( this._observers.indexOf( observer ) === -1 ) {
          this._observers.push( observer );
          observer( this.get(), null ); // null should be used when an object is expected but unavailable
        }
      },

      /**
       * Add an observer to the Property, without calling it back right away.  This is used when you need to register a observer without an immediate callback.
       * @param {function} observer  a function with a single argument, which is the value of the property at the time the function is called.
       */
      lazyLink: function( observer ) {
        if ( this._observers.indexOf( observer ) === -1 ) {
          this._observers.push( observer );
        }
      },

      /**
       * Removes an observer.
       * If observer is not registered, this is a no-op.
       *
       * @param {function} observer
       */
      unlink: function( observer ) {
        var index = this._observers.indexOf( observer );
        if ( index !== -1 ) {
          this._observers.splice( index, 1 );
        }
      },

      /**
       * Links an object's named attribute to this property.  Returns a handle so it can be removed using Property.unlink();
       * Example: modelVisibleProperty.linkAttribute(view,'visible');
       *
       * @param object
       * @param attributeName
       */
      linkAttribute: function( object, attributeName ) {
        var handle = function( value ) {object[ attributeName ] = value;};
        this.link( handle );
        return handle;
      },

      /**
       * Unlink an observer added with linkAttribute.  Note: the args of linkAttribute do not match the args of
       * unlinkAttribute: here, you must pass the observer handle returned by linkAttribute rather than object and attributeName
       * @param observer
       */
      unlinkAttribute: function( observer ) {
        this.unlink( observer );
      },

      //Provide toString for console debugging, see http://stackoverflow.com/questions/2485632/valueof-vs-tostring-in-javascript
      toString: function() {return 'Property{' + this.get() + '}'; },
      valueOf: function() {return this.toString();},

      /**
       * Add an observer so that it will only fire once (and not on registration)
       *
       * I can see two ways to implement this:
       * (a) add a field to the observer so after notifications it can be checked and possibly removed. Disadvantage: will make everything slower even if not using 'once'
       * (b) wrap the observer in a new function which will call the observer and then remove itself.  Disadvantage: cannot remove an observer added using 'once'
       * To avoid possible performance problems, use a wrapper function, and return it as a handle in case the 'once' observer must be removed before it is called once
       *
       * @param observer the observer which should be called back only for one property change (and not on registration)
       * @returns {function} the wrapper handle in case the wrapped function needs to be removed with 'unlink' before it is called once
       */
      once: function( observer ) {
        var property = this;
        var wrapper = function( newValue, oldValue ) {
          property.unlink( wrapper );
          observer( newValue, oldValue );
        };
        this.lazyLink( wrapper );
        return wrapper;
      },

      /**
       * Convenience function for debugging a property values.  It prints the new value on registration and when changed.
       * @param name debug name to be printed on the console
       * @returns {function} the handle to the linked observer in case it needs to be removed later
       */
      debug: function( name ) {
        var observer = function( value ) { console.log( name, value ); };
        this.link( observer );
        return observer;
      },

      /**
       * Returns a function that can be used to toggle the property (using !)
       * @returns {function}
       */
      get toggleFunction() {
        return this.toggle.bind( this );
      },

      /**
       * Modifies the value of this Property with the ! operator.  Works for booleans and non-booleans.
       */
      toggle: function() {
        this.value = !this.value;
      },

      /**
       * Adds an observer that is fired when the property takes the specified value.  If the property has the value already,
       * the observer is called back immediately.  A reference to the observer is returned so that it can be removed.
       *
       * @param value the value to match
       * @param observer the observer that is called when this Property
       */
      onValue: function( value, observer ) {
        var onValueObserver = function( v ) {
          if ( v === value ) {
            observer();
          }
        };
        this.link( onValueObserver );
        return onValueObserver;
      },

      /**
       * When the Property is no longer used by the sim, it can be eliminated.  All listeners are removed
       * and it is unregistered from any pertinent modules.
       */
      dispose: function() {

        while ( this._observers.length > 0 ) {
          this.unlink( this._observers[ 0 ] );
        }

        if ( this.componentID ) {
          together && together.removeComponent( this );
        }
      }
    },

    //statics
    {

      /**
       * Registers an observer with multiple properties, then notifies the observer immediately.
       * @param {Property[]} properties
       * @param {function} observer no params, returns nothing
       * @static
       */
      multilink: function( properties, observer ) {
        return new axon.Multilink( properties, observer, false );
      },

      lazyMultilink: function( properties, observer ) {
        return new axon.Multilink( properties, observer, true );
      },

      /**
       * Removes the multilinked observer from this Property.
       * Same as calling detach() on the handle (which happens to be a DerivedProperty instance)
       * @param derivedProperty
       */
      unmultilink: function( derivedProperty ) {
        derivedProperty.detach();
      },

      /**
       * Set up a PropertySet-like property on any object (see https://github.com/phetsims/axon/issues/42).
       *
       * @param {Object} object - The object that the property will be placed on
       * @param {string} propertyName - Name of the property
       * @param {*} initialValue - The initial value of the property
       */
      addProperty: function( object, propertyName, initialValue ) {
        // defines the property
        var property = this[ propertyName + 'Property' ] = new axon.Property( initialValue );

        // defines ES5 getter/setter
        Object.defineProperty( this, propertyName, {
          get: function() { return property.get(); },
          set: function( value ) { property.set( value ); },

          // Make it configurable and enumerable so it's easy to override...
          configurable: true,
          enumerable: true
        } );
      }
    } );
} );
<|endoftext|>