<commit_before>define(function (require, exports, module) {

/**
 * 组件基类
 *
 * @module Widget
 */

'use strict';

var $ = require('$'),
  Class = require('class'),
  Events = require('events').prototype,

  Aspect = require('./aspect').prototype;

var DELEGATE_SPLITTER = /^(\S+)\s*(.*)$/,
  DELEGATE_NAMESPACE = '.delegate-widget-';

// TODO: 检查是否有内存泄漏发生

var defaults = {
  // TODO: ue-component 改成 pandora 之类的，以与旧版组件做区别？
  element: '

',
  container: 'body'
};

/**
 * 组件基类
 *
 * 扩展 Events 与 Aspect (AOP)
 *
 * @class Widget
 * @constructor
 * @requires Events
 * @requires Aspect
 *
 * @example
 * ```
 * // 创建子类
 * var PersonWidget = Widget.extend({
 *   setup: function () {
 *     // 通知事件 `setup`
 *     this.fire('setup', this.option('name'), this.option('age'));
 *   }
 * });
 * // 创建子类实例
 * var tom = new PersonWidget({
 *   name: 'Tom',
 *   age: 21,
 *   // 订阅事件
 *   events: {
 *     // `setup`
 *     setup: function (e, name, age) {
 *       // e === 'setup'
 *       // name === 'Tom'
 *       // age === 21
 *       // this === tom
 *     },
 *     // AOP `before:setup`
 *     'before:setup': function (e) {
 *       // 执行 `setup` 方法前执行
 *       // 此处返回 `false` 将阻止 `setup` 方法执行
 *     },
 *     // AOP `before:setup`
 *     'before:setup': function (e) {
 *       // 执行 `setup` 方法后执行
 *       // 如果 `setup` 方法被阻止，就不会执行到这里
 *     }
 *   },
 *   // 代理事件
 *   delegates: {
 *     'click': function (e) {
 *       // e.target === this.element[0]
 *       // this === tom
 *     },
 *     'mouseover .avatar': function (e) {
 *       // 鼠标悬停在 element 里的 `.avatar` 元素
 *     }
 *   }
 * });
 * ```
 */
var Widget = Class.create(Events, Aspect, {

  /**
   * 初始化函数，将自动执行；实现事件自动订阅与初始化组件参数
   *
   * @method initialize
   * @param {Object} options 组件参数
   */
  initialize: function (options) {
    options || (options = {});

    if (options.events) {

      // 初始化 events
      $.each(options.events, $.proxy(function (event, callback) {
        var match = /^(before|after):(\w+)$/.exec(event);
        if (match) {
          // AOP
          this[match[1]](match[2], callback);
        } else {
          // Subscriber
          this.on(event, callback);
        }
      }, this));
    }

    // 初始化组件参数，只读
    this.__options = $.extend(true, {}, defaults, this.defaults, options);

    // this.UI = {
    //   'default': {}
    // };

    this.uniqueId = uniqueId();

    this.setup();

    // this.fire('init');
  },

  /**
   * 默认参数，预留用于子类覆盖
   *
   * @property {Object} defaults
   * @type {Object}
   */
  defaults: {},

  /**
   * 寻找 element 后代
   *
   * @method $
   * @param {Mixed} selector 选择符
   * @return {Object} jQuery 包装的 DOM 节点
   */
  $: function (selector) {
    return this.element.find(selector);
  },

  /**
   * 自动执行的设置函数，预留用于子类覆盖
   *
   * @method setup
   */
  setup: function () {
  },

  /**
   * 存取组件状态
   *
   * @method state
   * @param {Number} [state] 状态值
   * @return {Mixed} 当前状态值或当前实例
   */
  state: function (state) {
    if (state === undefined) {
      return this.__state;
    }

    this.__state = state;
    return this;
  },

  /**
   * 获取初始化后的组件参数
   *
   * @method option
   * @param {String} [key] 键
   * @return {Mixed} 整个参数列表或指定参数值
   */
  option: function (key) {
    var options = this.__options;
    return ((key === undefined) ? options :
      (options.hasOwnProperty(key) ? options[key] : undefined));
  },

  /**
   * 存取组件数据；用于管理动态生成的数据，如服务端返回
   *
   * @method data
   * @param {String} [key] 键
   * @param {Mixed} [value] 值
   * @return {Mixed} 整个数据、指定键值或当前实例
   */
  data: function (key, value) {
    var datas = this.__datas || (this.__datas = {});

    if (key === undefined) {
      return datas;
    }

    if ($.isPlainObject(key)) {
      $.extend(true, datas, key);
    } else {

      if (value === undefined) {
        return (datas.hasOwnProperty(key) ? datas[key] : undefined);
      } else {
        datas[key] = value;
      }
    }

    return this;
  },

  /**
   * 事件代理，绑定在 element 上
   *
   * @method delegate
   * @param {Object|Function} [delegates] 代理事件列表
   * @return {Object} 当前实例
   */
  delegate: function (delegates) {
    var self = this;

    delegates || (delegates = this.option('delegates'));

    if (!delegates) {
      return this;
    }

    if (typeof delegates === 'function') {
      delegates = delegates.call(this);
    }

    $.each(delegates, function (key, callback) {
      var match = key.match(DELEGATE_SPLITTER),
        event = match[1] + DELEGATE_NAMESPACE + self.uniqueId;

      if (match[2]) {
        self.element.on(event, match[2], $.proxy(callback, self));
      } else {
        self.element.on(event, $.proxy(callback, self));
      }
    });

    return this;
  },

  /**
   * 插入elemnt到container
   * @method render
   */
  render: function () {
    if (!this.rendered) {
      this.container = $(this.option('container'));
      this.element = $(this.option('element'));

      // element 事件代理
      this.delegate();

      this.rendered = true;
    }

    this.container.append(this.element);
  },

  /**
   * 销毁当前组件对象
   * @method destroy
   */
  destroy: function () {
    var prop;

    // 移除事件订阅
    this.off();

    // 移除 element 事件代理
    this.element.off(DELEGATE_NAMESPACE + this.uniqueId);

    // 从DOM中移除element
    this.element.remove();

    // 移除属性
    for (prop in this) {
      if (this.hasOwnProperty(prop)) {
        delete this[prop];
      }
    }

    this.destroy = function() { };
  }

});

var uniqueId = (function () {
  var ids = {};
  return function () {
    var id = Math.random().toString(36).substr(2);
    if (ids[id]) {
      return uniqueId();
    }
    ids[id] = true;
    return id;
  };
})();

module.exports = Widget;

});
<commit_msg>增加实例属性：`document` 与 `viewport`，分别对应于 `element` 所在的 `document` 与 `window`<commit_after>define(function (require, exports, module) {

/**
 * 组件基类
 *
 * @module Widget
 */

'use strict';

var $ = require('$'),
  Class = require('class'),
  Events = require('events').prototype,

  Aspect = require('./aspect').prototype;

var DELEGATE_SPLITTER = /^(\S+)\s*(.*)$/,
  DELEGATE_NAMESPACE = '.delegate-widget-';

// TODO: 检查是否有内存泄漏发生

var defaults = {
  // TODO: ue-component 改成 pandora 之类的，以与旧版组件做区别？
  element: '

',
  container: 'body'
};

/**
 * 组件基类
 *
 * 扩展 Events 与 Aspect (AOP)
 *
 * @class Widget
 * @constructor
 * @requires Events
 * @requires Aspect
 *
 * @example
 * ```
 * // 创建子类
 * var PersonWidget = Widget.extend({
 *   setup: function () {
 *     // 通知事件 `setup`
 *     this.fire('setup', this.option('name'), this.option('age'));
 *   }
 * });
 * // 创建子类实例
 * var tom = new PersonWidget({
 *   name: 'Tom',
 *   age: 21,
 *   // 订阅事件
 *   events: {
 *     // `setup`
 *     setup: function (e, name, age) {
 *       // e === 'setup'
 *       // name === 'Tom'
 *       // age === 21
 *       // this === tom
 *     },
 *     // AOP `before:setup`
 *     'before:setup': function (e) {
 *       // 执行 `setup` 方法前执行
 *       // 此处返回 `false` 将阻止 `setup` 方法执行
 *     },
 *     // AOP `before:setup`
 *     'before:setup': function (e) {
 *       // 执行 `setup` 方法后执行
 *       // 如果 `setup` 方法被阻止，就不会执行到这里
 *     }
 *   },
 *   // 代理事件
 *   delegates: {
 *     'click': function (e) {
 *       // e.target === this.element[0]
 *       // this === tom
 *     },
 *     'mouseover .avatar': function (e) {
 *       // 鼠标悬停在 element 里的 `.avatar` 元素
 *     }
 *   }
 * });
 * ```
 */
var Widget = Class.create(Events, Aspect, {

  /**
   * 初始化函数，将自动执行；实现事件自动订阅与初始化组件参数
   *
   * @method initialize
   * @param {Object} options 组件参数
   */
  initialize: function (options) {
    options || (options = {});

    if (options.events) {

      // 初始化 events
      $.each(options.events, $.proxy(function (event, callback) {
        var match = /^(before|after):(\w+)$/.exec(event);
        if (match) {
          // AOP
          this[match[1]](match[2], callback);
        } else {
          // Subscriber
          this.on(event, callback);
        }
      }, this));
    }

    // 初始化组件参数，只读
    this.__options = $.extend(true, {}, defaults, this.defaults, options);

    // this.UI = {
    //   'default': {}
    // };

    // 容器与元素
    this.container = $(this.option('container'));
    this.element = $(this.option('element'));

    // 初始化事件代理
    this.delegate();

    // 实例唯一ID
    this.uniqueId = uniqueId();

    this.setup();
  },

  /**
   * 默认参数，预留用于子类覆盖
   *
   * @property {Object} defaults
   * @type {Object}
   */
  defaults: {},

  /**
   * 寻找 element 后代
   *
   * @method $
   * @param {Mixed} selector 选择符
   * @return {Object} jQuery 包装的 DOM 节点
   */
  $: function (selector) {
    return this.element.find(selector);
  },

  /**
   * 自动执行的设置函数，预留用于子类覆盖
   *
   * @method setup
   */
  setup: function () {
  },

  /**
   * 存取组件状态
   *
   * @method state
   * @param {Number} [state] 状态值
   * @return {Mixed} 当前状态值或当前实例
   */
  state: function (state) {
    if (state === undefined) {
      return this.__state;
    }

    this.__state = state;
    return this;
  },

  /**
   * 获取初始化后的组件参数
   *
   * @method option
   * @param {String} [key] 键
   * @return {Mixed} 整个参数列表或指定参数值
   */
  option: function (key) {
    var options = this.__options;
    return ((key === undefined) ? options :
      (options.hasOwnProperty(key) ? options[key] : undefined));
  },

  /**
   * 存取组件数据；用于管理动态生成的数据，如服务端返回
   *
   * @method data
   * @param {String} [key] 键
   * @param {Mixed} [value] 值
   * @return {Mixed} 整个数据、指定键值或当前实例
   */
  data: function (key, value) {
    var datas = this.__datas || (this.__datas = {});

    if (key === undefined) {
      return datas;
    }

    if ($.isPlainObject(key)) {
      $.extend(true, datas, key);
    } else {

      if (value === undefined) {
        return (datas.hasOwnProperty(key) ? datas[key] : undefined);
      } else {
        datas[key] = value;
      }
    }

    return this;
  },

  /**
   * 事件代理，绑定在 element 上
   *
   * @method delegate
   * @param {Object|Function} [delegates] 代理事件列表
   * @return {Object} 当前实例
   */
  delegate: function (delegates) {
    var self = this;

    delegates || (delegates = this.option('delegates'));

    if (!delegates) {
      return this;
    }

    if (typeof delegates === 'function') {
      delegates = delegates.call(this);
    }

    $.each(delegates, function (key, callback) {
      var match = key.match(DELEGATE_SPLITTER),
        event = match[1] + DELEGATE_NAMESPACE + self.uniqueId;

      if (match[2]) {
        self.element.on(event, match[2], $.proxy(callback, self));
      } else {
        self.element.on(event, $.proxy(callback, self));
      }
    });

    return this;
  },

  /**
   * 插入elemnt到container
   * @method render
   */
  render: function () {
    if (!this.rendered) {

      // 插入到容器中
      this.container.append(this.element);

      // document
      this.document = this.element.prop('ownerDocument');

      // viewport
      this.viewport = (function (doc) {
        return doc.defaultView || doc.parentWindow;
      })(this.document);

      this.rendered = true;
    }
  },

  /**
   * 销毁当前组件对象
   * @method destroy
   */
  destroy: function () {
    var prop;

    // 移除事件订阅
    this.off();

    // 移除 element 事件代理
    this.element.off(DELEGATE_NAMESPACE + this.uniqueId);

    // 从DOM中移除element
    this.element.remove();

    // 移除属性
    for (prop in this) {
      if (this.hasOwnProperty(prop)) {
        delete this[prop];
      }
    }

    this.destroy = function() { };
  }

});

var uniqueId = (function () {
  var ids = {};
  return function () {
    var id = Math.random().toString(36).substr(2);
    if (ids[id]) {
      return uniqueId();
    }
    ids[id] = true;
    return id;
  };
})();

module.exports = Widget;

});
<|endoftext|>


<commit_before>define(
	[
		"klassified",
		"./widget-extensions",
		"./router",
		"./events",
		"./htmlCanvas",
		"jquery"
	],

	function(klassified, ext, router, events, htmlCanvas, jQuery) {

		/**
		 * Creates unique ids used by widgets to identify their root div.
		 */
		var idGenerator = (function() {
			var that = {};
			var id = 0;

			that.newId = function() {
				id += 1;
				return id.toString();
			};

			return that;
		})();

		/**
		 * Helpers for keeping track of the currently rendered widget.
		 */
		var currentWidget = (function() {
			var current;
			return {
				get: function() {
					return current;
				},
				set: function(widget) {
					current = widget;
				}
			};
		})();

		/**
		 * Base for all widgets. A widget can keep state in variables, contain logic and
		 * render itself using `renderOn()`.
		 *
		 * @example
		 *
		 *		var titleWidget = function(spec) {
		 *			var that = widget(spec);
		 *
		 *			var title = spec.title || "Hello World";
		 *
		 *			that.renderContentOn = function(html) {
		 *				html.h1(title)
		 *			}
		 *
		 *			return that;
		 *		};
		 *
		 *		var helloWorldWidget = titleWidget({title: "Hello Widget!"});
		 *
		 *		$(document).ready(function() {
		 *			helloWorldWidget.appendTo("BODY");
		 *		});
		 *
		 * Widgets can also be rendered on a HTML canvas (since widget implements `appendToBrush()`). Eg.
		 *
		 *		html.div(helloWorldWidget)
		 *
		 * It is therefor easy to compose widgets from other widgets.
		 *
		 * @virtual
		 *
		 * @param {Object} spec
		 * @param {String} [spec.id] Unique id for widget. Also used for root element when attached/rendered to DOM.
		 *                           If not provided an ID will automatically be generated and assigned.
		 * @param {Object} [my]
		 *
		 * @returns {widget}
		 */
		var widget = klassified.object.abstractSubclass(function(that, my) {

			/**
			 * Keep track of the rendered subwidgets
			 */
			var children;
			var id;

			my.initialize = function(spec) {
				my.super(spec);
				id = spec.id || idGenerator.newId();
				// When within an update transaction, do not update the widget
				my.inUpdateTransaction = false;
				children = [];
				my.initializeSubwidgets(spec);
			};

			/**
			 * Hook evaluated at the end of widget initialization and
			 * before any rendering.
			 */
			my.initializeSubwidgets = function(spec) {};

			/** Events for widget */
			my.events = events.eventCategory();

			that.onAttach = my.events.createEvent();
			that.onDetach = my.events.createEvent();

			//
			// Public
			//

			/**
			 * Returns a unique id for the widget
			 *
			 * @returns {String}
			 */
			that.getId = function() {
				return id;
			};

			that.id = that.getId; //TODO: deprecated

			/**
			 * Performance tasks need for freeing/releasing/cleaning-up resources used by widget.
			 *
			 * Should always be executed before a widget is disposed. Especially
			 * if the widget register events to avoid memory leaks.
			 *
			 * Most widgets should override `my.dispose` instead of overriding
			 * this function.
			 *
			 */
			that.dispose = function() {
				children.forEach(function(child) {
					child.dispose();
				});
				my.dispose();

				my.events.dispose();
			};

			/**
			 * Method to be performed when a root widget is detached from the
			 * DOM. The widegt and all its children will call `my.willDetach` in
			 * turn.
			 */
			that.willDetach = function() {
				children.forEach(function(child) {
					child.willDetach();
				});
				my.willDetach();
				that.onDetach.trigger();
			};

			/**
			 * Renders the widget on a JQuery / DOM
			 *
			 * @example
			 * widget.appendTo("BODY");
			 *
			 * @param aJQuery
			 */
			that.appendTo = function(aJQuery) {
				my.withAttachHooks(function() {
					renderBasicOn(htmlCanvas(aJQuery));
				});
			};

			/**
			 * Does the same as appendTo except it first clear the
			 * elements matched by aJQuery
			 *
			 * @param aJQuery
			 */
			that.replace = function(aJQuery) {
				my.withAttachHooks(function() {
					var canvas = htmlCanvas(aJQuery);
					canvas.root.asJQuery().empty();
					renderBasicOn(canvas);
				});
			};

			/**
			 * Answers a jQuery that match the root DOM element. By default
			 * by selecting an element that have the same ID as the widget.
			 *
			 * See "renderOn"
			 *
			 * @returns {*}
			 */
			that.asJQuery = function() {
				return jQuery("#" + that.getId());
			};

			/**
			 * Answers true if if widget have rendered content in DOM
			 *
			 * @returns {boolean}
			 */
			that.isRendered = function() {
				return that.asJQuery().length > 0;
			};

			/**
			 * Implementation for `appendToBrush()` to allow a widget to be
			 * appended to a brush. See "htmlCanvas".
			 *
			 * Basically it allows us to do:
			 *		html.div(widget);
			 *
			 * @param aTagBrush
			 */
			that.appendToBrush = function(aTagBrush) {
				my.withAttachHooks(function() {
					renderBasicOn(htmlCanvas(aTagBrush.asJQuery()));
				});
			};

			/**
			 * Trigger the `willAttach` event on the receiver and all
			 * rendered subwidgets.
			 */
			that.triggerWillAttach = function() {
				my.willAttach();
				children.forEach(function(widget) {
					widget.triggerWillAttach();
				});
			};

			/**
			 * Trigger the `didAttach` event on the receiver and all
			 * rendered subwidgets.
			 */
			that.triggerDidAttach = function() {
				my.didAttach();
				children.forEach(function(widget) {
					widget.triggerDidAttach();
				});
				that.onAttach.trigger();
			};

			/**
			 * Evaluate `fn`, calling `willAttach` before and `didAttach` after
			 * the evaluation.
			 */
			my.withAttachHooks = function(fn) {
				var inRendering = inRenderingLoop();
				if(!inRendering) {
					that.triggerWillAttach();
				}
				fn();
				if(!inRendering) {
					that.triggerDidAttach();
				}
			};

			/**
			 * Create and expose an event named `name`.
			 */
			my.createEvent = function(name) {
				that[name] = my.events.createEvent();
			};

			/**
			 * Create and expose one event per string argument.
			 */
			my.createEvents = function() {
				var names = Array.prototype.slice.apply(arguments);
				names.forEach(my.createEvent);
			};

			// Expose events
			that.on = my.events.on;
			that.onceOn = my.events.onceOn;
			that.off = my.events.off;
			that.trigger = my.events.trigger;

			//
			// Protected
			//

			/**
			 * Exposes the internal ID generator. Generates new unique IDs to be used
			 * for sub-widgets, etc.
			 *
			 * @returns {String}
			 */
			my.nextId = function() {
				return idGenerator.newId();
			};

			/**
			 * Widget specific dispose.
			 */
			my.dispose = function() {};

			// Route / Controller extensions

			my.router = router.getRouter();

			my.linkTo = my.router.linkTo;
			my.linkToPath = my.router.linkToPath;
			my.linkToUrl = my.router.linkToUrl;

			my.redirectTo = my.router.redirectTo;
			my.redirectToPath = my.router.redirectToPath;
			my.redirectToUrl = my.router.redirectToUrl;

			my.getParameters = my.router.getParameters;
			my.getParameter = my.router.getParameter;
			my.setParameters = my.router.setParameters;

			//
			// Render
			//

			/**
			 * Private rendering function.	This is the function
			 * internally called each time the widget is rendered, in
			 * `appendTo`, `replace` and `update`.
			 *
			 */
			function renderBasicOn(html) {
				my.withChildrenRegistration(function() {
					that.renderOn(html);
				});
			}

			/**
			 * Main entry point for rendering. For convenience "renderOn" will	wrap the content
			 * rendered by "renderContentOn" in a root element (renderRootOn) that will be matched
			 * by asJQuery.
			 *
			 * Usually concrete widgets override "renderContentOn" to render it content. Widgets
			 * can override "renderOn" but must then make sure that it can be matched by "asJQuery".
			 *
			 * One way to do that is to make sure to have only one root element and setting the ID of
			 * that element to the ID of the widget.
			 *
			 * @example
			 *
			 *		that.renderOn = function(html) {
			 *			html.ul({id: that.getId()}
			 *				html.li("BMW"),
			 *				html.li("Toyota")
			 *			);
			 *		};
			 *
			 *
			 * @param html
			 */
			that.renderOn = function(html) {
				// Renders widget by wrapping `renderContentOn()` in a root element.
				my.renderRootOn(html).render(that.renderContentOn);
			};

			my.withChildrenRegistration = function(fn) {
				var parent = currentWidget.get();
				if(parent) {
					parent.registerChild(that);
				}
				withCurrentWidget(function() {
					children = [];
					fn();
				}, that);
			};

			that.registerChild = function(widget) {
				children.push(widget);
			};

			/**
			 * Renders a wrapper element (by default a "widgetjs-widget" tag) and
			 * set the element ID to the ID of the widget so that it can be found by
			 * "asJQuery" eg. when we re-render using "update".
			 *
			 * @param html
			 * @returns {htmlBrush}
			 */
			my.renderRootOn = function(html) {
				return html.tag("widgetjs-widget").id(id);
			};

			/**
			 * Overridden in concrete widgets to render widget to canvas/DOM.
			 *
			 * @example
			 *
			 *		that.renderContentOn = function(html) {
			 *			html.ul(
			 *				html.li("BMW"),
			 *				html.li("Toyota")
			 *			);
			 *		};
			 *
			 * @param {htmlCanvas} html
			 */
			that.renderContentOn = function(html) {};

			/**
			 * Hook evaluated before the widget is attached (or reattached due
			 * to an update of rendering) to the DOM.
			 */
			my.willAttach = function() {};

			/**
			 * Hook evaluated each time the widget is attached (or
			 * reattached due to an update of rendering) to the DOM.
			 */
			my.didAttach = function() {};

			/**
			 * Hook evaluated when a widget is detached from the DOM.
			 */
			my.willDetach = function() {};

			/**
			 * Hook evaluated before widget update.
			 */
			my.willUpdate = function() {};

			/**
			 * Re-renders the widget and replace it in the DOM
			 *
			 * Content is first re-rendered on a document fragment. Update then replace the element matched
			 * by "asJQuery" with the new content.
			 *
			 */
			that.update = function() {
				if (my.inUpdateTransaction || !that.isRendered()) {
					return;
				}

				my.willUpdate();
				my.withAttachHooks(function() {
					// Re-render
					var html = htmlCanvas();
					renderBasicOn(html);

					// Replace our self
					that.asJQuery().replaceWith(html.root.element);
				});
			};

			that.withinTransaction = function(fn, onDone) {
				if(my.inUpdateTransaction) {
					fn();
				} else {
					try {
						my.inUpdateTransaction = true;
						fn();
					}
					finally {
						my.inUpdateTransaction = false;
						if(onDone) {
							onDone();
						}
					}
				}
			};

			/**
			 * Evaluate `fn`, ensuring that an update will be
			 * performed after evaluating the function. Nested calls
			 * to `withUpdate` or `update` will result in updating the
			 * widget only once.
			 */
			that.withUpdate = function(fn) {
				that.withinTransaction(fn, that.update);
			};

			that.withNoUpdate = function(fn) {
				that.withinTransaction(fn);
			};

			// Third party protected extensions** added to `my`.
			// See widget-extensions.js
			for (var extProperty in ext) {
				if (ext.hasOwnProperty(extProperty)) {
					my[extProperty] = ext[extProperty];
				}
			}

			return that;
		});

		/**
		 * Return true if the parent widget is rendering the receiver.
		 */
		function inRenderingLoop() {
			return !!currentWidget.get();
		}

		/**
		 * Set `widget` as the current widget while evaluating `fn`.
		 */
		function withCurrentWidget(fn, widget) {
			var current = currentWidget.get();
			try {
				currentWidget.set(widget);
				fn();
			} finally {
				currentWidget.set(current);
			}
		}

		return widget;
	}
);
<commit_msg>Make renderContentOn abstract<commit_after>define(
	[
		"klassified",
		"./widget-extensions",
		"./router",
		"./events",
		"./htmlCanvas",
		"jquery"
	],

	function(klassified, ext, router, events, htmlCanvas, jQuery) {

		/**
		 * Creates unique ids used by widgets to identify their root div.
		 */
		var idGenerator = (function() {
			var that = {};
			var id = 0;

			that.newId = function() {
				id += 1;
				return id.toString();
			};

			return that;
		})();

		/**
		 * Helpers for keeping track of the currently rendered widget.
		 */
		var currentWidget = (function() {
			var current;
			return {
				get: function() {
					return current;
				},
				set: function(widget) {
					current = widget;
				}
			};
		})();

		/**
		 * Base for all widgets. A widget can keep state in variables, contain logic and
		 * render itself using `renderOn()`.
		 *
		 * @example
		 *
		 *		var titleWidget = function(spec) {
		 *			var that = widget(spec);
		 *
		 *			var title = spec.title || "Hello World";
		 *
		 *			that.renderContentOn = function(html) {
		 *				html.h1(title)
		 *			}
		 *
		 *			return that;
		 *		};
		 *
		 *		var helloWorldWidget = titleWidget({title: "Hello Widget!"});
		 *
		 *		$(document).ready(function() {
		 *			helloWorldWidget.appendTo("BODY");
		 *		});
		 *
		 * Widgets can also be rendered on a HTML canvas (since widget implements `appendToBrush()`). Eg.
		 *
		 *		html.div(helloWorldWidget)
		 *
		 * It is therefor easy to compose widgets from other widgets.
		 *
		 * @virtual
		 *
		 * @param {Object} spec
		 * @param {String} [spec.id] Unique id for widget. Also used for root element when attached/rendered to DOM.
		 *                           If not provided an ID will automatically be generated and assigned.
		 * @param {Object} [my]
		 *
		 * @returns {widget}
		 */
		var widget = klassified.object.abstractSubclass(function(that, my) {

			/**
			 * Keep track of the rendered subwidgets
			 */
			var children;
			var id;

			my.initialize = function(spec) {
				my.super(spec);
				id = spec.id || idGenerator.newId();
				// When within an update transaction, do not update the widget
				my.inUpdateTransaction = false;
				children = [];
				my.initializeSubwidgets(spec);
			};

			/**
			 * Hook evaluated at the end of widget initialization and
			 * before any rendering.
			 */
			my.initializeSubwidgets = function(spec) {};

			/** Events for widget */
			my.events = events.eventCategory();

			that.onAttach = my.events.createEvent();
			that.onDetach = my.events.createEvent();

			//
			// Public
			//

			/**
			 * Returns a unique id for the widget
			 *
			 * @returns {String}
			 */
			that.getId = function() {
				return id;
			};

			that.id = that.getId; //TODO: deprecated

			/**
			 * Performance tasks need for freeing/releasing/cleaning-up resources used by widget.
			 *
			 * Should always be executed before a widget is disposed. Especially
			 * if the widget register events to avoid memory leaks.
			 *
			 * Most widgets should override `my.dispose` instead of overriding
			 * this function.
			 *
			 */
			that.dispose = function() {
				children.forEach(function(child) {
					child.dispose();
				});
				my.dispose();

				my.events.dispose();
			};

			/**
			 * Method to be performed when a root widget is detached from the
			 * DOM. The widegt and all its children will call `my.willDetach` in
			 * turn.
			 */
			that.willDetach = function() {
				children.forEach(function(child) {
					child.willDetach();
				});
				my.willDetach();
				that.onDetach.trigger();
			};

			/**
			 * Renders the widget on a JQuery / DOM
			 *
			 * @example
			 * widget.appendTo("BODY");
			 *
			 * @param aJQuery
			 */
			that.appendTo = function(aJQuery) {
				my.withAttachHooks(function() {
					renderBasicOn(htmlCanvas(aJQuery));
				});
			};

			/**
			 * Does the same as appendTo except it first clear the
			 * elements matched by aJQuery
			 *
			 * @param aJQuery
			 */
			that.replace = function(aJQuery) {
				my.withAttachHooks(function() {
					var canvas = htmlCanvas(aJQuery);
					canvas.root.asJQuery().empty();
					renderBasicOn(canvas);
				});
			};

			/**
			 * Answers a jQuery that match the root DOM element. By default
			 * by selecting an element that have the same ID as the widget.
			 *
			 * See "renderOn"
			 *
			 * @returns {*}
			 */
			that.asJQuery = function() {
				return jQuery("#" + that.getId());
			};

			/**
			 * Answers true if if widget have rendered content in DOM
			 *
			 * @returns {boolean}
			 */
			that.isRendered = function() {
				return that.asJQuery().length > 0;
			};

			/**
			 * Implementation for `appendToBrush()` to allow a widget to be
			 * appended to a brush. See "htmlCanvas".
			 *
			 * Basically it allows us to do:
			 *		html.div(widget);
			 *
			 * @param aTagBrush
			 */
			that.appendToBrush = function(aTagBrush) {
				my.withAttachHooks(function() {
					renderBasicOn(htmlCanvas(aTagBrush.asJQuery()));
				});
			};

			/**
			 * Trigger the `willAttach` event on the receiver and all
			 * rendered subwidgets.
			 */
			that.triggerWillAttach = function() {
				my.willAttach();
				children.forEach(function(widget) {
					widget.triggerWillAttach();
				});
			};

			/**
			 * Trigger the `didAttach` event on the receiver and all
			 * rendered subwidgets.
			 */
			that.triggerDidAttach = function() {
				my.didAttach();
				children.forEach(function(widget) {
					widget.triggerDidAttach();
				});
				that.onAttach.trigger();
			};

			/**
			 * Evaluate `fn`, calling `willAttach` before and `didAttach` after
			 * the evaluation.
			 */
			my.withAttachHooks = function(fn) {
				var inRendering = inRenderingLoop();
				if(!inRendering) {
					that.triggerWillAttach();
				}
				fn();
				if(!inRendering) {
					that.triggerDidAttach();
				}
			};

			/**
			 * Create and expose an event named `name`.
			 */
			my.createEvent = function(name) {
				that[name] = my.events.createEvent();
			};

			/**
			 * Create and expose one event per string argument.
			 */
			my.createEvents = function() {
				var names = Array.prototype.slice.apply(arguments);
				names.forEach(my.createEvent);
			};

			// Expose events
			that.on = my.events.on;
			that.onceOn = my.events.onceOn;
			that.off = my.events.off;
			that.trigger = my.events.trigger;

			//
			// Protected
			//

			/**
			 * Exposes the internal ID generator. Generates new unique IDs to be used
			 * for sub-widgets, etc.
			 *
			 * @returns {String}
			 */
			my.nextId = function() {
				return idGenerator.newId();
			};

			/**
			 * Widget specific dispose.
			 */
			my.dispose = function() {};

			// Route / Controller extensions

			my.router = router.getRouter();

			my.linkTo = my.router.linkTo;
			my.linkToPath = my.router.linkToPath;
			my.linkToUrl = my.router.linkToUrl;

			my.redirectTo = my.router.redirectTo;
			my.redirectToPath = my.router.redirectToPath;
			my.redirectToUrl = my.router.redirectToUrl;

			my.getParameters = my.router.getParameters;
			my.getParameter = my.router.getParameter;
			my.setParameters = my.router.setParameters;

			//
			// Render
			//

			/**
			 * Private rendering function.	This is the function
			 * internally called each time the widget is rendered, in
			 * `appendTo`, `replace` and `update`.
			 *
			 */
			function renderBasicOn(html) {
				my.withChildrenRegistration(function() {
					that.renderOn(html);
				});
			}

			/**
			 * Main entry point for rendering. For convenience "renderOn" will	wrap the content
			 * rendered by "renderContentOn" in a root element (renderRootOn) that will be matched
			 * by asJQuery.
			 *
			 * Usually concrete widgets override "renderContentOn" to render it content. Widgets
			 * can override "renderOn" but must then make sure that it can be matched by "asJQuery".
			 *
			 * One way to do that is to make sure to have only one root element and setting the ID of
			 * that element to the ID of the widget.
			 *
			 * @example
			 *
			 *		that.renderOn = function(html) {
			 *			html.ul({id: that.getId()}
			 *				html.li("BMW"),
			 *				html.li("Toyota")
			 *			);
			 *		};
			 *
			 *
			 * @param html
			 */
			that.renderOn = function(html) {
				// Renders widget by wrapping `renderContentOn()` in a root element.
				my.renderRootOn(html).render(that.renderContentOn);
			};

			my.withChildrenRegistration = function(fn) {
				var parent = currentWidget.get();
				if(parent) {
					parent.registerChild(that);
				}
				withCurrentWidget(function() {
					children = [];
					fn();
				}, that);
			};

			that.registerChild = function(widget) {
				children.push(widget);
			};

			/**
			 * Renders a wrapper element (by default a "widgetjs-widget" tag) and
			 * set the element ID to the ID of the widget so that it can be found by
			 * "asJQuery" eg. when we re-render using "update".
			 *
			 * @param html
			 * @returns {htmlBrush}
			 */
			my.renderRootOn = function(html) {
				return html.tag("widgetjs-widget").id(id);
			};

			/**
			 * Overridden in concrete widgets to render widget to canvas/DOM.
			 *
			 * @example
			 *
			 *		that.renderContentOn = function(html) {
			 *			html.ul(
			 *				html.li("BMW"),
			 *				html.li("Toyota")
			 *			);
			 *		};
			 *
			 * @param {htmlCanvas} html
			 */
			that.renderContentOn = function(html) {
				return my.subclassResponsibility();
			};

			/**
			 * Hook evaluated before the widget is attached (or reattached due
			 * to an update of rendering) to the DOM.
			 */
			my.willAttach = function() {};

			/**
			 * Hook evaluated each time the widget is attached (or
			 * reattached due to an update of rendering) to the DOM.
			 */
			my.didAttach = function() {};

			/**
			 * Hook evaluated when a widget is detached from the DOM.
			 */
			my.willDetach = function() {};

			/**
			 * Hook evaluated before widget update.
			 */
			my.willUpdate = function() {};

			/**
			 * Re-renders the widget and replace it in the DOM
			 *
			 * Content is first re-rendered on a document fragment. Update then replace the element matched
			 * by "asJQuery" with the new content.
			 *
			 */
			that.update = function() {
				if (my.inUpdateTransaction || !that.isRendered()) {
					return;
				}

				my.willUpdate();
				my.withAttachHooks(function() {
					// Re-render
					var html = htmlCanvas();
					renderBasicOn(html);

					// Replace our self
					that.asJQuery().replaceWith(html.root.element);
				});
			};

			that.withinTransaction = function(fn, onDone) {
				if(my.inUpdateTransaction) {
					fn();
				} else {
					try {
						my.inUpdateTransaction = true;
						fn();
					}
					finally {
						my.inUpdateTransaction = false;
						if(onDone) {
							onDone();
						}
					}
				}
			};

			/**
			 * Evaluate `fn`, ensuring that an update will be
			 * performed after evaluating the function. Nested calls
			 * to `withUpdate` or `update` will result in updating the
			 * widget only once.
			 */
			that.withUpdate = function(fn) {
				that.withinTransaction(fn, that.update);
			};

			that.withNoUpdate = function(fn) {
				that.withinTransaction(fn);
			};

			// Third party protected extensions** added to `my`.
			// See widget-extensions.js
			for (var extProperty in ext) {
				if (ext.hasOwnProperty(extProperty)) {
					my[extProperty] = ext[extProperty];
				}
			}

			return that;
		});

		/**
		 * Return true if the parent widget is rendering the receiver.
		 */
		function inRenderingLoop() {
			return !!currentWidget.get();
		}

		/**
		 * Set `widget` as the current widget while evaluating `fn`.
		 */
		function withCurrentWidget(fn, widget) {
			var current = currentWidget.get();
			try {
				currentWidget.set(widget);
				fn();
			} finally {
				currentWidget.set(current);
			}
		}

		return widget;
	}
);
<|endoftext|>


<commit_before>// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//   * Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//   * Neither the name of the GNOME Foundation nor the
//     names of its contributors may be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Params = imports.params;
const CategoryList = imports.categoryList;
const Character = imports.character;
const CharacterList = imports.characterList;
const Pango = imports.gi.Pango;
const Gc = imports.gi.Gc;
const Gettext = imports.gettext;

const Util = imports.util;

const MAX_SEARCH_RESULTS = 100;

const MainWindow = new Lang.Class({
    Name: 'MainWindow',
    Extends: Gtk.ApplicationWindow,
    Properties: {
        'search-active': GObject.ParamSpec.boolean(
            'search-active', '', '',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, false)
    },

    _init: function(params) {
        params = Params.fill(params, { title: GLib.get_application_name(),
                                       default_width: 640,
                                       default_height: 480 });
        this.parent(params);

        this._searchActive = false;
        this._searchKeywords = [];

        Util.initActions(this,
                         [{ name: 'about',
                            activate: this._about },
                          { name: 'search-active',
                            activate: this._toggleSearch,
                            parameter_type: new GLib.VariantType('b'),
                            state: new GLib.Variant('b', false) },
                          { name: 'category',
                            activate: this._category,
                            parameter_type: new GLib.VariantType('s'),
                            state: new GLib.Variant('s', 'punctuation') },
                          { name: 'character',
                            activate: this._character,
                            parameter_type: new GLib.VariantType('s') }]);

        let builder = new Gtk.Builder();
        builder.add_from_resource('/org/gnome/Characters/main.ui');
        builder.add_from_resource('/org/gnome/Characters/sidebar.ui');

        this._headerBar = builder.get_object('main-header');
        this.set_titlebar(this._headerBar);

        let searchBtn = builder.get_object('search-active-button');
        this.bind_property('search-active', searchBtn, 'active',
                           GObject.BindingFlags.SYNC_CREATE |
                           GObject.BindingFlags.BIDIRECTIONAL);
        this._searchBar = builder.get_object('search-bar');
        this.bind_property('search-active',
                           this._searchBar,
                           'search-mode-enabled',
                           GObject.BindingFlags.SYNC_CREATE |
                           GObject.BindingFlags.BIDIRECTIONAL);
        let searchEntry = builder.get_object('search-entry');
        this._searchBar.connect_entry(searchEntry);
        searchEntry.connect('search-changed',
                            Lang.bind(this, this._handleSearchChanged));

        let grid = builder.get_object('main-grid');
        let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL });

        let sidebarGrid = builder.get_object('sidebar-grid');
        this._categoryList = new CategoryList.CategoryListWidget();
        sidebarGrid.add(this._categoryList);
        hbox.pack_start(sidebarGrid, false, false, 2);

        this._mainView = new MainView();
        hbox.pack_start(this._mainView, true, true, 0);

        grid.add(hbox);

        this.add(grid);
        grid.show_all();

        // Due to limitations of gobject-introspection wrt GdkEvent
        // and GdkEventKey, this needs to be a signal handler
        this.connect('key-press-event', Lang.bind(this, this._handleKeyPress));
    },

    get search_active() {
        return this._searchActive;
    },

    set search_active(v) {
        if (this._searchActive == v)
            return;

        this._searchActive = v;
        this.notify('search-active');
    },

    _handleSearchChanged: function(entry) {
        let text = entry.get_text().replace(/^\s+|\s+$/g, '');
        let keywords = text == '' ? [] : text.split(/\s+/);
        keywords = keywords.map(String.toUpperCase);
        if (keywords != this._searchKeywords) {
            this._searchKeywords = keywords;
            if (this._searchKeywords.length > 0)
                this._mainView.startSearch(this._searchKeywords);
            else
                this._mainView.cancelSearch();
        }
        return true;
    },

    _handleKeyPress: function(self, event) {
        return this._searchBar.handle_event(event);
    },

    _about: function() {
        let aboutDialog = new Gtk.AboutDialog(
            { authors: [ 'Daiki Ueno <dueno@src.gnome.org>' ],
              translator_credits: _("translator-credits"),
              program_name: _("GNOME Characters"),
              comments: _("Character Map"),
              copyright: 'Copyright 2014 Daiki Ueno',
              license_type: Gtk.License.GPL_2_0,
              logo_icon_name: 'org.gnome.Characters',
              version: pkg.version,
              website: 'http://www.example.com/gnome-characters/',
              wrap_license: true,
              modal: true,
              transient_for: this
            });

        aboutDialog.show();
        aboutDialog.connect('response', function() {
            aboutDialog.destroy();
        });
    },

    _category: function(action, v) {
        let [name, length] = v.get_string()
        let category = null;
        for (let index in CategoryList.Category) {
            category = CategoryList.Category[index];
            if (category.name == name)
                break;
        }

        Util.assertNotEqual(category, null);
        this._mainView.setPage(category.name);
        this._headerBar.title = Gettext.gettext(category.label);
    },

    _character: function(action, v) {
        let [uc, length] = v.get_string()
        this._mainView.selectCharacter(uc);
    },
});

const MainView = new Lang.Class({
    Name: 'MainView',
    Extends: Gtk.Stack,

    _init: function(params) {
        params = Params.fill(params, { hexpand: true, vexpand: true });
        this.parent(params);

        this._characterListWidgets = {};

        let characterList;
        for (let index in CategoryList.Category) {
            let category = CategoryList.Category[index];
            characterList = this._createCharacterList();
            characterList.get_accessible().accessible_name =
                _('%s Character List').format(category.label);
            this._characterListWidgets[category.name] = characterList;
            this.add_named(this._createScrolledWindow(characterList),
                           category.name);
        }

        characterList = this._createCharacterList();
        characterList.get_accessible().accessible_name =
            _('Search Result Character List');
        this.add_named(this._createScrolledWindow(characterList),
                       'search-result');
        this._characterListWidgets['search-result'] = characterList;

        let builder = new Gtk.Builder();
        builder.add_from_resource('/org/gnome/Characters/main.ui');
        let searchBannerGrid = builder.get_object('search-banner-grid');
        this.add_named(searchBannerGrid, 'search-banner');
        let loadingBannerGrid = builder.get_object('loading-banner-grid');
        this._spinner = builder.get_object('loading-banner-spinner');
        this.add_named(loadingBannerGrid, 'loading-banner');

        this._recentCharacters = [];
        this._cancellable = new Gio.Cancellable();
    },

    _createCharacterList: function() {
        let widget = new CharacterList.CharacterListWidget({ hexpand: true,
                                                             vexpand: true });
        widget.connect('character-selected',
                       Lang.bind(this, this._handleCharacterSelected));
        return widget;
    },

    _createScrolledWindow: function(widget) {
        let viewport = new Gtk.Viewport({});
        viewport.add(widget);

        let scroll = new Gtk.ScrolledWindow({
            hscrollbar_policy: Gtk.PolicyType.NEVER
        });
        scroll.add(viewport);
        return scroll;
    },

    _startSearch: function() {
        this._cancellable.cancel();
        this._cancellable.reset();

        if (this.visible_child_name != 'search-banner' &&
            this.visible_child_name != 'search-result')
            this._lastPage = this.visible_child_name;

        this._spinnerTimeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000,
                             Lang.bind(this, function () {
                                 this._spinnerTimeoutId = 0;
                                 this._spinner.start();
                                 this.visible_child_name = 'loading-banner';
                                 this.show_all();
                             }));
    },

    _finishSearch: function(name, result) {
        if (this._spinnerTimeoutId > 0) {
            GLib.source_remove(this._spinnerTimeoutId);
            this._spinner.stop();
        }

        let characters = [];
        for (let index = 0; index < result.len; index++) {
            characters.push(Gc.search_result_get(result, index));
        }

        let characterList = this._characterListWidgets[name];
        characterList.setCharacters(characters);
        if (result.length == 0) {
            if (this._lastPage)
                this.visible_child_name = this._lastPage;
            else
                this.visible_child_name = 'search-banner';
        } else {
            this.visible_child_name = name;
        }
        this.show_all();
    },

    startSearch: function(keywords) {
        this._startSearch();
        Gc.search_by_keywords(
            keywords,
            MAX_SEARCH_RESULTS,
            this._cancellable,
            Lang.bind(this, function(source_object, res, user_data) {
                try {
                    let result = Gc.search_finish(res);
                    this._finishSearch('search-result', result);
                } catch (e) {
                    log("Failed to search by keywords: " + e);
                }
            }));
    },

    cancelSearch: function() {
        this._cancellable.cancel();
        this._finishSearch('search-result', []);
    },

    setPage: function(name) {
        if (!(name in this._characterListWidgets))
            return;

        let characterList = this._characterListWidgets[name];
        if (name == 'recent') {
            if (this._recentCharacters.length == 0)
                this.visible_child_name = 'search-banner';
            else {
                characterList.setCharacters(this._recentCharacters);
                characterList.show_all();
                this.visible_child_name = name;
            }
            this.show_all();
        } else {
            let category = null;
            for (let index in CategoryList.Category) {
                category = CategoryList.Category[index];
                if (category.name == name)
                    break;
            }

            Util.assertNotEqual(category, null);
            this._startSearch();
            Gc.search_by_category(
                category.category,
                -1,
                this._cancellable,
                Lang.bind(this,
                          function(source_object, res, user_data) {
                              try {
                                  let result = Gc.search_finish(res);
                                  this._finishSearch(name, result);
                              } catch (e) {
                                  log("Failed to search by category: " + e);
                              }
                          }));
        }
    },

    selectCharacter: function(uc) {
        if (this._recentCharacters.indexOf(uc) < 0) {
            this._recentCharacters.push(uc);

            if (this.visible_child_name == 'recent')
                this.setPage('recent');
        }
    },

    _handleCharacterSelected: function(widget, uc) {
        this.selectCharacter(uc);

        let dialog = new Character.CharacterDialog({
            character: uc,
            modal: true,
            transient_for: this.get_toplevel()
        });

        dialog.show();
        dialog.connect('response', function(self, response_id) {
            if (response_id == Gtk.ResponseType.CLOSE)
                dialog.destroy();
        });
    }
});
<commit_msg>Fix spinner timeout handling<commit_after>// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
//
// Copyright (c) 2013 Giovanni Campagna <scampa.giovanni@gmail.com>
//
// Redistribution and use in source and binary forms, with or without
//  modification, are permitted provided that the following conditions are met:
//   * Redistributions of source code must retain the above copyright
//     notice, this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//   * Neither the name of the GNOME Foundation nor the
//     names of its contributors may be used to endorse or promote products
//     derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Params = imports.params;
const CategoryList = imports.categoryList;
const Character = imports.character;
const CharacterList = imports.characterList;
const Pango = imports.gi.Pango;
const Gc = imports.gi.Gc;
const Gettext = imports.gettext;

const Util = imports.util;

const MAX_SEARCH_RESULTS = 100;

const MainWindow = new Lang.Class({
    Name: 'MainWindow',
    Extends: Gtk.ApplicationWindow,
    Properties: {
        'search-active': GObject.ParamSpec.boolean(
            'search-active', '', '',
            GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE, false)
    },

    _init: function(params) {
        params = Params.fill(params, { title: GLib.get_application_name(),
                                       default_width: 640,
                                       default_height: 480 });
        this.parent(params);

        this._searchActive = false;
        this._searchKeywords = [];

        Util.initActions(this,
                         [{ name: 'about',
                            activate: this._about },
                          { name: 'search-active',
                            activate: this._toggleSearch,
                            parameter_type: new GLib.VariantType('b'),
                            state: new GLib.Variant('b', false) },
                          { name: 'category',
                            activate: this._category,
                            parameter_type: new GLib.VariantType('s'),
                            state: new GLib.Variant('s', 'punctuation') },
                          { name: 'character',
                            activate: this._character,
                            parameter_type: new GLib.VariantType('s') }]);

        let builder = new Gtk.Builder();
        builder.add_from_resource('/org/gnome/Characters/main.ui');
        builder.add_from_resource('/org/gnome/Characters/sidebar.ui');

        this._headerBar = builder.get_object('main-header');
        this.set_titlebar(this._headerBar);

        let searchBtn = builder.get_object('search-active-button');
        this.bind_property('search-active', searchBtn, 'active',
                           GObject.BindingFlags.SYNC_CREATE |
                           GObject.BindingFlags.BIDIRECTIONAL);
        this._searchBar = builder.get_object('search-bar');
        this.bind_property('search-active',
                           this._searchBar,
                           'search-mode-enabled',
                           GObject.BindingFlags.SYNC_CREATE |
                           GObject.BindingFlags.BIDIRECTIONAL);
        let searchEntry = builder.get_object('search-entry');
        this._searchBar.connect_entry(searchEntry);
        searchEntry.connect('search-changed',
                            Lang.bind(this, this._handleSearchChanged));

        let grid = builder.get_object('main-grid');
        let hbox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL });

        let sidebarGrid = builder.get_object('sidebar-grid');
        this._categoryList = new CategoryList.CategoryListWidget();
        sidebarGrid.add(this._categoryList);
        hbox.pack_start(sidebarGrid, false, false, 2);

        this._mainView = new MainView();
        hbox.pack_start(this._mainView, true, true, 0);

        grid.add(hbox);

        this.add(grid);
        grid.show_all();

        // Due to limitations of gobject-introspection wrt GdkEvent
        // and GdkEventKey, this needs to be a signal handler
        this.connect('key-press-event', Lang.bind(this, this._handleKeyPress));
    },

    get search_active() {
        return this._searchActive;
    },

    set search_active(v) {
        if (this._searchActive == v)
            return;

        this._searchActive = v;
        this.notify('search-active');
    },

    _handleSearchChanged: function(entry) {
        let text = entry.get_text().replace(/^\s+|\s+$/g, '');
        let keywords = text == '' ? [] : text.split(/\s+/);
        keywords = keywords.map(String.toUpperCase);
        if (keywords != this._searchKeywords) {
            this._searchKeywords = keywords;
            if (this._searchKeywords.length > 0)
                this._mainView.startSearch(this._searchKeywords);
            else
                this._mainView.cancelSearch();
        }
        return true;
    },

    _handleKeyPress: function(self, event) {
        return this._searchBar.handle_event(event);
    },

    _about: function() {
        let aboutDialog = new Gtk.AboutDialog(
            { authors: [ 'Daiki Ueno <dueno@src.gnome.org>' ],
              translator_credits: _("translator-credits"),
              program_name: _("GNOME Characters"),
              comments: _("Character Map"),
              copyright: 'Copyright 2014 Daiki Ueno',
              license_type: Gtk.License.GPL_2_0,
              logo_icon_name: 'org.gnome.Characters',
              version: pkg.version,
              website: 'http://www.example.com/gnome-characters/',
              wrap_license: true,
              modal: true,
              transient_for: this
            });

        aboutDialog.show();
        aboutDialog.connect('response', function() {
            aboutDialog.destroy();
        });
    },

    _category: function(action, v) {
        let [name, length] = v.get_string()
        let category = null;
        for (let index in CategoryList.Category) {
            category = CategoryList.Category[index];
            if (category.name == name)
                break;
        }

        Util.assertNotEqual(category, null);
        this._mainView.setPage(category.name);
        this._headerBar.title = Gettext.gettext(category.label);
    },

    _character: function(action, v) {
        let [uc, length] = v.get_string()
        this._mainView.selectCharacter(uc);
    },
});

const MainView = new Lang.Class({
    Name: 'MainView',
    Extends: Gtk.Stack,

    _init: function(params) {
        params = Params.fill(params, { hexpand: true, vexpand: true });
        this.parent(params);

        this._characterListWidgets = {};

        let characterList;
        for (let index in CategoryList.Category) {
            let category = CategoryList.Category[index];
            characterList = this._createCharacterList();
            characterList.get_accessible().accessible_name =
                _('%s Character List').format(category.label);
            this._characterListWidgets[category.name] = characterList;
            this.add_named(this._createScrolledWindow(characterList),
                           category.name);
        }

        characterList = this._createCharacterList();
        characterList.get_accessible().accessible_name =
            _('Search Result Character List');
        this.add_named(this._createScrolledWindow(characterList),
                       'search-result');
        this._characterListWidgets['search-result'] = characterList;

        let builder = new Gtk.Builder();
        builder.add_from_resource('/org/gnome/Characters/main.ui');
        let searchBannerGrid = builder.get_object('search-banner-grid');
        this.add_named(searchBannerGrid, 'search-banner');
        let loadingBannerGrid = builder.get_object('loading-banner-grid');
        this._spinner = builder.get_object('loading-banner-spinner');
        this.add_named(loadingBannerGrid, 'loading-banner');
        this._spinnerTimeoutId = 0;

        this._recentCharacters = [];
        this._cancellable = new Gio.Cancellable();
    },

    _createCharacterList: function() {
        let widget = new CharacterList.CharacterListWidget({ hexpand: true,
                                                             vexpand: true });
        widget.connect('character-selected',
                       Lang.bind(this, this._handleCharacterSelected));
        return widget;
    },

    _createScrolledWindow: function(widget) {
        let viewport = new Gtk.Viewport({});
        viewport.add(widget);

        let scroll = new Gtk.ScrolledWindow({
            hscrollbar_policy: Gtk.PolicyType.NEVER
        });
        scroll.add(viewport);
        return scroll;
    },

    _startSearch: function() {
        this._cancellable.cancel();
        this._cancellable.reset();

        if (this.visible_child_name != 'search-banner' &&
            this.visible_child_name != 'search-result')
            this._lastPage = this.visible_child_name;

        this._spinnerTimeoutId =
            GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000,
                             Lang.bind(this, function () {
                                 this._spinner.start();
                                 this.visible_child_name = 'loading-banner';
                                 this.show_all();
                             }));
    },

    _finishSearch: function(name, result) {
        if (this._spinnerTimeoutId > 0) {
            GLib.source_remove(this._spinnerTimeoutId);
            this._spinnerTimeoutId = 0;
            this._spinner.stop();
        }

        let characters = [];
        for (let index = 0; index < result.len; index++) {
            characters.push(Gc.search_result_get(result, index));
        }

        let characterList = this._characterListWidgets[name];
        characterList.setCharacters(characters);
        if (result.length == 0) {
            if (this._lastPage)
                this.visible_child_name = this._lastPage;
            else
                this.visible_child_name = 'search-banner';
        } else {
            this.visible_child_name = name;
        }
        this.show_all();
    },

    startSearch: function(keywords) {
        this._startSearch();
        Gc.search_by_keywords(
            keywords,
            MAX_SEARCH_RESULTS,
            this._cancellable,
            Lang.bind(this, function(source_object, res, user_data) {
                try {
                    let result = Gc.search_finish(res);
                    this._finishSearch('search-result', result);
                } catch (e) {
                    log("Failed to search by keywords: " + e);
                }
            }));
    },

    cancelSearch: function() {
        this._cancellable.cancel();
        this._finishSearch('search-result', []);
    },

    setPage: function(name) {
        if (!(name in this._characterListWidgets))
            return;

        let characterList = this._characterListWidgets[name];
        if (name == 'recent') {
            if (this._recentCharacters.length == 0)
                this.visible_child_name = 'search-banner';
            else {
                characterList.setCharacters(this._recentCharacters);
                characterList.show_all();
                this.visible_child_name = name;
            }
            this.show_all();
        } else {
            let category = null;
            for (let index in CategoryList.Category) {
                category = CategoryList.Category[index];
                if (category.name == name)
                    break;
            }

            Util.assertNotEqual(category, null);
            this._startSearch();
            Gc.search_by_category(
                category.category,
                -1,
                this._cancellable,
                Lang.bind(this,
                          function(source_object, res, user_data) {
                              try {
                                  let result = Gc.search_finish(res);
                                  this._finishSearch(name, result);
                              } catch (e) {
                                  log("Failed to search by category: " + e);
                              }
                          }));
        }
    },

    selectCharacter: function(uc) {
        if (this._recentCharacters.indexOf(uc) < 0) {
            this._recentCharacters.push(uc);

            if (this.visible_child_name == 'recent')
                this.setPage('recent');
        }
    },

    _handleCharacterSelected: function(widget, uc) {
        this.selectCharacter(uc);

        let dialog = new Character.CharacterDialog({
            character: uc,
            modal: true,
            transient_for: this.get_toplevel()
        });

        dialog.show();
        dialog.connect('response', function(self, response_id) {
            if (response_id == Gtk.ResponseType.CLOSE)
                dialog.destroy();
        });
    }
});
<|endoftext|>


<commit_before>/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var createMatrix = require('regl-scattermatrix');
var arrayRange = require('array-range');

var Lib = require('../../lib');
var AxisIDs = require('../../plots/cartesian/axis_ids');

var subTypes = require('../scatter/subtypes');
var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
var calcColorscales = require('../scatter/colorscale_calc');
var convertMarkerSelection = require('../scattergl/convert').convertMarkerSelection;
var convertMarkerStyle = require('../scattergl/convert').convertMarkerStyle;
var calcHover = require('../scattergl').calcHover;

var BADNUM = require('../../constants/numerical').BADNUM;
var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS;

function calc(gd, trace) {
    var dimensions = trace.dimensions;
    var commonLength = trace._commonLength;
    var stash = {};
    var opts = {};
    // 'c' for calculated, 'l' for linear,
    // only differ here for log axes, pass ldata to createMatrix as 'data'
    var cdata = opts.cdata = [];
    var ldata = opts.data = [];
    var i, k, dim;

    for(i = 0; i < dimensions.length; i++) {
        dim = dimensions[i];

        if(dim.visible) {
            var axId = trace._diag[i][0] || trace._diag[i][1];
            var ax = AxisIDs.getFromId(gd, axId);
            if(ax) {
                var ccol = makeCalcdata(ax, trace, dim);
                var lcol = ax.type === 'log' ? Lib.simpleMap(ccol, ax.c2l) : ccol;
                cdata.push(ccol);
                ldata.push(lcol);
            }
        }
    }

    calcColorscales(trace);
    Lib.extendFlat(opts, convertMarkerStyle(trace));

    var visibleLength = cdata.length;
    var hasTooManyPoints = (visibleLength * commonLength) > TOO_MANY_POINTS;

    for(i = 0, k = 0; i < dimensions.length; i++) {
        dim = dimensions[i];

        if(dim.visible) {
            var xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {};
            var ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {};

            // Re-use SVG scatter axis expansion routine except
            // for graph with very large number of points where it
            // performs poorly.
            // In big data case, fake Axes.expand outputs with data bounds,
            // and an average size for array marker.size inputs.
            var ppad;
            if(hasTooManyPoints) {
                ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3));
            } else {
                ppad = calcMarkerSize(trace, commonLength);
            }

            calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad);
            k++;
        }
    }

    var scene = stash._scene = sceneUpdate(gd, stash);
    if(!scene.matrix) scene.matrix = true;
    scene.matrixOptions = opts;

    scene.selectedOptions = convertMarkerSelection(trace, trace.selected);
    scene.unselectedOptions = convertMarkerSelection(trace, trace.unselected);

    return [{x: false, y: false, t: stash, trace: trace}];
}

function makeCalcdata(ax, trace, dim) {
    // call makeCalcdata with fake input
    var ccol = ax.makeCalcdata({
        v: dim.values,
        vcalendar: trace.calendar
    }, 'v');

    for(var i = 0; i < ccol.length; i++) {
        ccol[i] = ccol[i] === BADNUM ? NaN : ccol[i];
    }

    return ccol;
}

function sceneUpdate(gd, stash) {
    var scene = stash._scene;

    var reset = {
        dirty: true
    };

    var first = {
        selectBatch: null,
        unselectBatch: null,
        matrix: false,
        select: null
    };

    if(!scene) {
        scene = stash._scene = Lib.extendFlat({}, reset, first);

        scene.draw = function draw() {
            // draw traces in selection mode
            if(scene.matrix && scene.selectBatch) {
                scene.matrix.draw(scene.unselectBatch, scene.selectBatch);
            }

            else if(scene.matrix) {
                scene.matrix.draw();
            }

            scene.dirty = false;
        };

        // remove scene resources
        scene.destroy = function destroy() {
            if(scene.matrix) scene.matrix.destroy();

            scene.matrixOptions = null;
            scene.selectBatch = null;
            scene.unselectBatch = null;

            stash._scene = null;
        };
    }

    // In case if we have scene from the last calc - reset data
    if(!scene.dirty) {
        Lib.extendFlat(scene, reset);
    }

    return scene;
}

function plot(gd, _, splomCalcData) {
    if(!splomCalcData.length) return;

    for(var i = 0; i < splomCalcData.length; i++) {
        plotOne(gd, splomCalcData[i][0]);
    }
}

function plotOne(gd, cd0) {
    var fullLayout = gd._fullLayout;
    var gs = fullLayout._size;
    var trace = cd0.trace;
    var stash = cd0.t;
    var scene = stash._scene;
    var matrixOpts = scene.matrixOptions;
    var cdata = matrixOpts.cdata;
    var regl = fullLayout._glcanvas.data()[0].regl;
    var dragmode = fullLayout.dragmode;
    var xa, ya;
    var i, j, k;

    if(cdata.length === 0) return;

    // augment options with proper upper/lower halves
    // regl-scattermatrix's default grid start from bottom-left
    matrixOpts.lower = trace.showupperhalf;
    matrixOpts.upper = trace.showlowerhalf;
    matrixOpts.diagonal = trace.diagonal.visible;

    var dimensions = trace.dimensions;
    var visibleLength = cdata.length;
    var viewOpts = {};
    viewOpts.ranges = new Array(visibleLength);
    viewOpts.domains = new Array(visibleLength);

    for(i = 0, k = 0; i < dimensions.length; i++) {
        if(trace.dimensions[i].visible) {
            var rng = viewOpts.ranges[k] = new Array(4);
            var dmn = viewOpts.domains[k] = new Array(4);

            xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
            if(xa) {
                rng[0] = xa._rl[0];
                rng[2] = xa._rl[1];
                dmn[0] = xa.domain[0];
                dmn[2] = xa.domain[1];
            }

            ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
            if(ya) {
                rng[1] = ya._rl[0];
                rng[3] = ya._rl[1];
                dmn[1] = ya.domain[0];
                dmn[3] = ya.domain[1];
            }

            k++;
        }
    }

    viewOpts.viewport = [gs.l, gs.b, gs.w + gs.l, gs.h + gs.b];

    if(scene.matrix === true) {
        scene.matrix = createMatrix(regl);
    }

    var selectMode = dragmode === 'lasso' || dragmode === 'select' || !!trace.selectedpoints;
    scene.selectBatch = null;
    scene.unselectBatch = null;

    if(selectMode) {
        var commonLength = trace._commonLength;

        if(!scene.selectBatch) {
            scene.selectBatch = [];
            scene.unselectBatch = [];
        }

        // regenerate scene batch, if traces number changed during selection
        if(trace.selectedpoints) {
            scene.selectBatch = trace.selectedpoints;

            var selPts = trace.selectedpoints;
            var selDict = {};
            for(i = 0; i < selPts.length; i++) {
                selDict[selPts[i]] = true;
            }
            var unselPts = [];
            for(i = 0; i < commonLength; i++) {
                if(!selDict[i]) unselPts.push(i);
            }
            scene.unselectBatch = unselPts;
        }

        // precalculate px coords since we are not going to pan during select
        var xpx = stash.xpx = new Array(visibleLength);
        var ypx = stash.ypx = new Array(visibleLength);

        for(i = 0, k = 0; i < dimensions.length; i++) {
            if(trace.dimensions[i].visible) {
                xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
                if(xa) {
                    xpx[k] = new Array(commonLength);
                    for(j = 0; j < commonLength; j++) {
                        xpx[k][j] = xa.c2p(cdata[k][j]);
                    }
                }

                ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
                if(ya) {
                    ypx[k] = new Array(commonLength);
                    for(j = 0; j < commonLength; j++) {
                        ypx[k][j] = ya.c2p(cdata[k][j]);
                    }
                }

                k++;
            }
        }

        if(scene.selectBatch) {
            scene.matrix.update(matrixOpts, matrixOpts);
            scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
            scene.matrix.update(viewOpts, viewOpts);
        }
        else {
            // delete selection pass
            scene.matrix.update(viewOpts, null);
        }
    }
    else {
        scene.matrix.update(matrixOpts);
        scene.matrix.update(viewOpts);
        stash.xpx = stash.ypx = null;
    }

    scene.draw();
}

// TODO splom 'needs' the grid component, register it here?

function hoverPoints(pointData, xval, yval) {
    var cd = pointData.cd;
    var trace = cd[0].trace;
    var stash = cd[0].t;
    var scene = stash._scene;
    var cdata = scene.matrixOptions.cdata;
    var xa = pointData.xa;
    var ya = pointData.ya;
    var xpx = xa.c2p(xval);
    var ypx = ya.c2p(yval);
    var maxDistance = pointData.distance;

    var xi = getDimIndex(trace, xa);
    var yi = getDimIndex(trace, ya);
    if(xi === false || yi === false) return [pointData];

    var x = cdata[xi];
    var y = cdata[yi];

    var id, dxy;
    var minDist = maxDistance;

    for(var i = 0; i < x.length; i++) {
        var ptx = x[i];
        var pty = y[i];
        var dx = xa.c2p(ptx) - xpx;
        var dy = ya.c2p(pty) - ypx;
        var dist = Math.sqrt(dx * dx + dy * dy);

        if(dist < minDist) {
            minDist = dxy = dist;
            id = i;
        }
    }

    pointData.index = id;
    pointData.distance = minDist;
    pointData.dxy = dxy;

    if(id === undefined) return [pointData];

    calcHover(pointData, x, y, trace);

    return [pointData];
}

function selectPoints(searchInfo, polygon) {
    var cd = searchInfo.cd;
    var trace = cd[0].trace;
    var stash = cd[0].t;
    var scene = stash._scene;
    var cdata = scene.matrixOptions.cdata;
    var xa = searchInfo.xaxis;
    var ya = searchInfo.yaxis;
    var selection = [];
    var i;

    if(!scene) return selection;

    var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace));
    if(trace.visible !== true || hasOnlyLines) return selection;

    var xi = getDimIndex(trace, xa);
    var yi = getDimIndex(trace, ya);
    if(xi === false || yi === false) return selection;

    var xpx = stash.xpx[xi];
    var ypx = stash.ypx[yi];
    var x = cdata[xi];
    var y = cdata[yi];

    // degenerate polygon does not enable selection
    // filter out points by visible scatter ones
    var els = null;
    var unels = null;
    if(polygon !== false && !polygon.degenerate) {
        els = [], unels = [];
        for(i = 0; i < x.length; i++) {
            if(polygon.contains([xpx[i], ypx[i]])) {
                els.push(i);
                selection.push({
                    pointNumber: i,
                    x: x[i],
                    y: y[i]
                });
            }
            else {
                unels.push(i);
            }
        }
    } else {
        unels = arrayRange(stash.count);
    }

    // make sure selectBatch is created
    if(!scene.selectBatch) {
        scene.selectBatch = [];
        scene.unselectBatch = [];
    }

    if(!scene.selectBatch) {
        // enter every trace select mode
        for(i = 0; i < scene.count; i++) {
            scene.selectBatch = [];
            scene.unselectBatch = [];
        }
        // we should turn scatter2d into unselected once we have any points selected
        scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
    }

    scene.selectBatch = els;
    scene.unselectBatch = unels;

    return selection;
}

function style(gd, cds) {
    if(!cds) return;

    var fullLayout = gd._fullLayout;
    var cd0 = cds[0];
    var scene0 = cd0[0].t._scene;
    scene0.matrix.regl.clear({color: true});

    if(fullLayout._splomGrid) {
        fullLayout._splomGrid.draw();
    }

    for(var i = 0; i < cds.length; i++) {
        var scene = cds[i][0].t._scene;
        scene.draw();
    }

    // redraw all subplot with scattergl traces,
    // all we cleared the whole canvas above
    if(fullLayout._has('cartesian')) {
        for(var k in fullLayout._plots) {
            var sp = fullLayout._plots[k];
            if(sp._scene) sp._scene.draw();
        }
    }
}

function getDimIndex(trace, ax) {
    var axId = ax._id;
    var axLetter = axId.charAt(0);
    var ind = {x: 0, y: 1}[axLetter];
    var dimensions = trace.dimensions;

    for(var i = 0, k = 0; i < dimensions.length; i++) {
        if(dimensions[i].visible) {
            if(trace._diag[i][ind] === axId) return k;
            k++;
        }
    }
    return false;
}

module.exports = {
    moduleType: 'trace',
    name: 'splom',

    basePlotModule: require('./base_plot'),
    categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'],

    attributes: require('./attributes'),
    supplyDefaults: require('./defaults'),

    calc: calc,
    plot: plot,
    hoverPoints: hoverPoints,
    selectPoints: selectPoints,
    style: style,

    meta: {
        description: [
            'SPLOM !!!'
        ].join(' ')
    }
};
<commit_msg>:pencil2: splom meta description + register grid in splom index<commit_after>/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var createMatrix = require('regl-scattermatrix');
var arrayRange = require('array-range');

var Registry = require('../../registry');
var Grid = require('../../components/grid');
var Lib = require('../../lib');
var AxisIDs = require('../../plots/cartesian/axis_ids');

var subTypes = require('../scatter/subtypes');
var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
var calcColorscales = require('../scatter/colorscale_calc');
var convertMarkerSelection = require('../scattergl/convert').convertMarkerSelection;
var convertMarkerStyle = require('../scattergl/convert').convertMarkerStyle;
var calcHover = require('../scattergl').calcHover;

var BADNUM = require('../../constants/numerical').BADNUM;
var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS;

function calc(gd, trace) {
    var dimensions = trace.dimensions;
    var commonLength = trace._commonLength;
    var stash = {};
    var opts = {};
    // 'c' for calculated, 'l' for linear,
    // only differ here for log axes, pass ldata to createMatrix as 'data'
    var cdata = opts.cdata = [];
    var ldata = opts.data = [];
    var i, k, dim;

    for(i = 0; i < dimensions.length; i++) {
        dim = dimensions[i];

        if(dim.visible) {
            var axId = trace._diag[i][0] || trace._diag[i][1];
            var ax = AxisIDs.getFromId(gd, axId);
            if(ax) {
                var ccol = makeCalcdata(ax, trace, dim);
                var lcol = ax.type === 'log' ? Lib.simpleMap(ccol, ax.c2l) : ccol;
                cdata.push(ccol);
                ldata.push(lcol);
            }
        }
    }

    calcColorscales(trace);
    Lib.extendFlat(opts, convertMarkerStyle(trace));

    var visibleLength = cdata.length;
    var hasTooManyPoints = (visibleLength * commonLength) > TOO_MANY_POINTS;

    for(i = 0, k = 0; i < dimensions.length; i++) {
        dim = dimensions[i];

        if(dim.visible) {
            var xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {};
            var ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {};

            // Re-use SVG scatter axis expansion routine except
            // for graph with very large number of points where it
            // performs poorly.
            // In big data case, fake Axes.expand outputs with data bounds,
            // and an average size for array marker.size inputs.
            var ppad;
            if(hasTooManyPoints) {
                ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3));
            } else {
                ppad = calcMarkerSize(trace, commonLength);
            }

            calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad);
            k++;
        }
    }

    var scene = stash._scene = sceneUpdate(gd, stash);
    if(!scene.matrix) scene.matrix = true;
    scene.matrixOptions = opts;

    scene.selectedOptions = convertMarkerSelection(trace, trace.selected);
    scene.unselectedOptions = convertMarkerSelection(trace, trace.unselected);

    return [{x: false, y: false, t: stash, trace: trace}];
}

function makeCalcdata(ax, trace, dim) {
    // call makeCalcdata with fake input
    var ccol = ax.makeCalcdata({
        v: dim.values,
        vcalendar: trace.calendar
    }, 'v');

    for(var i = 0; i < ccol.length; i++) {
        ccol[i] = ccol[i] === BADNUM ? NaN : ccol[i];
    }

    return ccol;
}

function sceneUpdate(gd, stash) {
    var scene = stash._scene;

    var reset = {
        dirty: true
    };

    var first = {
        selectBatch: null,
        unselectBatch: null,
        matrix: false,
        select: null
    };

    if(!scene) {
        scene = stash._scene = Lib.extendFlat({}, reset, first);

        scene.draw = function draw() {
            // draw traces in selection mode
            if(scene.matrix && scene.selectBatch) {
                scene.matrix.draw(scene.unselectBatch, scene.selectBatch);
            }

            else if(scene.matrix) {
                scene.matrix.draw();
            }

            scene.dirty = false;
        };

        // remove scene resources
        scene.destroy = function destroy() {
            if(scene.matrix) scene.matrix.destroy();

            scene.matrixOptions = null;
            scene.selectBatch = null;
            scene.unselectBatch = null;

            stash._scene = null;
        };
    }

    // In case if we have scene from the last calc - reset data
    if(!scene.dirty) {
        Lib.extendFlat(scene, reset);
    }

    return scene;
}

function plot(gd, _, splomCalcData) {
    if(!splomCalcData.length) return;

    for(var i = 0; i < splomCalcData.length; i++) {
        plotOne(gd, splomCalcData[i][0]);
    }
}

function plotOne(gd, cd0) {
    var fullLayout = gd._fullLayout;
    var gs = fullLayout._size;
    var trace = cd0.trace;
    var stash = cd0.t;
    var scene = stash._scene;
    var matrixOpts = scene.matrixOptions;
    var cdata = matrixOpts.cdata;
    var regl = fullLayout._glcanvas.data()[0].regl;
    var dragmode = fullLayout.dragmode;
    var xa, ya;
    var i, j, k;

    if(cdata.length === 0) return;

    // augment options with proper upper/lower halves
    // regl-scattermatrix's default grid starts from bottom-left
    matrixOpts.lower = trace.showupperhalf;
    matrixOpts.upper = trace.showlowerhalf;
    matrixOpts.diagonal = trace.diagonal.visible;

    var dimensions = trace.dimensions;
    var visibleLength = cdata.length;
    var viewOpts = {};
    viewOpts.ranges = new Array(visibleLength);
    viewOpts.domains = new Array(visibleLength);

    for(i = 0, k = 0; i < dimensions.length; i++) {
        if(trace.dimensions[i].visible) {
            var rng = viewOpts.ranges[k] = new Array(4);
            var dmn = viewOpts.domains[k] = new Array(4);

            xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
            if(xa) {
                rng[0] = xa._rl[0];
                rng[2] = xa._rl[1];
                dmn[0] = xa.domain[0];
                dmn[2] = xa.domain[1];
            }

            ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
            if(ya) {
                rng[1] = ya._rl[0];
                rng[3] = ya._rl[1];
                dmn[1] = ya.domain[0];
                dmn[3] = ya.domain[1];
            }

            k++;
        }
    }

    viewOpts.viewport = [gs.l, gs.b, gs.w + gs.l, gs.h + gs.b];

    if(scene.matrix === true) {
        scene.matrix = createMatrix(regl);
    }

    var selectMode = dragmode === 'lasso' || dragmode === 'select' || !!trace.selectedpoints;
    scene.selectBatch = null;
    scene.unselectBatch = null;

    if(selectMode) {
        var commonLength = trace._commonLength;

        if(!scene.selectBatch) {
            scene.selectBatch = [];
            scene.unselectBatch = [];
        }

        // regenerate scene batch, if traces number changed during selection
        if(trace.selectedpoints) {
            scene.selectBatch = trace.selectedpoints;

            var selPts = trace.selectedpoints;
            var selDict = {};
            for(i = 0; i < selPts.length; i++) {
                selDict[selPts[i]] = true;
            }
            var unselPts = [];
            for(i = 0; i < commonLength; i++) {
                if(!selDict[i]) unselPts.push(i);
            }
            scene.unselectBatch = unselPts;
        }

        // precalculate px coords since we are not going to pan during select
        var xpx = stash.xpx = new Array(visibleLength);
        var ypx = stash.ypx = new Array(visibleLength);

        for(i = 0, k = 0; i < dimensions.length; i++) {
            if(trace.dimensions[i].visible) {
                xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
                if(xa) {
                    xpx[k] = new Array(commonLength);
                    for(j = 0; j < commonLength; j++) {
                        xpx[k][j] = xa.c2p(cdata[k][j]);
                    }
                }

                ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
                if(ya) {
                    ypx[k] = new Array(commonLength);
                    for(j = 0; j < commonLength; j++) {
                        ypx[k][j] = ya.c2p(cdata[k][j]);
                    }
                }

                k++;
            }
        }

        if(scene.selectBatch) {
            scene.matrix.update(matrixOpts, matrixOpts);
            scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
            scene.matrix.update(viewOpts, viewOpts);
        }
        else {
            // delete selection pass
            scene.matrix.update(viewOpts, null);
        }
    }
    else {
        scene.matrix.update(matrixOpts);
        scene.matrix.update(viewOpts);
        stash.xpx = stash.ypx = null;
    }

    scene.draw();
}

function hoverPoints(pointData, xval, yval) {
    var cd = pointData.cd;
    var trace = cd[0].trace;
    var stash = cd[0].t;
    var scene = stash._scene;
    var cdata = scene.matrixOptions.cdata;
    var xa = pointData.xa;
    var ya = pointData.ya;
    var xpx = xa.c2p(xval);
    var ypx = ya.c2p(yval);
    var maxDistance = pointData.distance;

    var xi = getDimIndex(trace, xa);
    var yi = getDimIndex(trace, ya);
    if(xi === false || yi === false) return [pointData];

    var x = cdata[xi];
    var y = cdata[yi];

    var id, dxy;
    var minDist = maxDistance;

    for(var i = 0; i < x.length; i++) {
        var ptx = x[i];
        var pty = y[i];
        var dx = xa.c2p(ptx) - xpx;
        var dy = ya.c2p(pty) - ypx;
        var dist = Math.sqrt(dx * dx + dy * dy);

        if(dist < minDist) {
            minDist = dxy = dist;
            id = i;
        }
    }

    pointData.index = id;
    pointData.distance = minDist;
    pointData.dxy = dxy;

    if(id === undefined) return [pointData];

    calcHover(pointData, x, y, trace);

    return [pointData];
}

function selectPoints(searchInfo, polygon) {
    var cd = searchInfo.cd;
    var trace = cd[0].trace;
    var stash = cd[0].t;
    var scene = stash._scene;
    var cdata = scene.matrixOptions.cdata;
    var xa = searchInfo.xaxis;
    var ya = searchInfo.yaxis;
    var selection = [];
    var i;

    if(!scene) return selection;

    var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace));
    if(trace.visible !== true || hasOnlyLines) return selection;

    var xi = getDimIndex(trace, xa);
    var yi = getDimIndex(trace, ya);
    if(xi === false || yi === false) return selection;

    var xpx = stash.xpx[xi];
    var ypx = stash.ypx[yi];
    var x = cdata[xi];
    var y = cdata[yi];

    // degenerate polygon does not enable selection
    // filter out points by visible scatter ones
    var els = null;
    var unels = null;
    if(polygon !== false && !polygon.degenerate) {
        els = [], unels = [];
        for(i = 0; i < x.length; i++) {
            if(polygon.contains([xpx[i], ypx[i]])) {
                els.push(i);
                selection.push({
                    pointNumber: i,
                    x: x[i],
                    y: y[i]
                });
            }
            else {
                unels.push(i);
            }
        }
    } else {
        unels = arrayRange(stash.count);
    }

    // make sure selectBatch is created
    if(!scene.selectBatch) {
        scene.selectBatch = [];
        scene.unselectBatch = [];
    }

    if(!scene.selectBatch) {
        // enter every trace select mode
        for(i = 0; i < scene.count; i++) {
            scene.selectBatch = [];
            scene.unselectBatch = [];
        }
        // we should turn scatter2d into unselected once we have any points selected
        scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
    }

    scene.selectBatch = els;
    scene.unselectBatch = unels;

    return selection;
}

function style(gd, cds) {
    if(!cds) return;

    var fullLayout = gd._fullLayout;
    var cd0 = cds[0];
    var scene0 = cd0[0].t._scene;
    scene0.matrix.regl.clear({color: true});

    if(fullLayout._splomGrid) {
        fullLayout._splomGrid.draw();
    }

    for(var i = 0; i < cds.length; i++) {
        var scene = cds[i][0].t._scene;
        scene.draw();
    }

    // redraw all subplot with scattergl traces,
    // all we cleared the whole canvas above
    if(fullLayout._has('cartesian')) {
        for(var k in fullLayout._plots) {
            var sp = fullLayout._plots[k];
            if(sp._scene) sp._scene.draw();
        }
    }
}

function getDimIndex(trace, ax) {
    var axId = ax._id;
    var axLetter = axId.charAt(0);
    var ind = {x: 0, y: 1}[axLetter];
    var dimensions = trace.dimensions;

    for(var i = 0, k = 0; i < dimensions.length; i++) {
        if(dimensions[i].visible) {
            if(trace._diag[i][ind] === axId) return k;
            k++;
        }
    }
    return false;
}

module.exports = {
    moduleType: 'trace',
    name: 'splom',

    basePlotModule: require('./base_plot'),
    categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'],

    attributes: require('./attributes'),
    supplyDefaults: require('./defaults'),

    calc: calc,
    plot: plot,
    hoverPoints: hoverPoints,
    selectPoints: selectPoints,
    style: style,

    meta: {
        description: [
            'Splom traces generate scatter plot matrix visualizations.',
            'Each splom `dimensions` items correspond to an generated axes.',
            'Values for each of those dimensions are set in `dimensions[i].values`.',
            'Splom traces support all `scattergl` marker style attributes.',
            'Specify `layout.grid` attributes and/or layout x-axis and y-axis attributes',
            'for control '
        ].join(' ')
    }
};

// splom traces use the 'grid' component to generate their axes,
// register it here
Registry.register(Grid);
<|endoftext|>