Source: widgets/chem/viewer/kekule.chemWidget.viewers.js

/**
 * @fileoverview
 * Related types and classes of chem viewer.
 * Viewer is a widget to show chem objects on HTML page.
 * @author Partridge Jiang
 */

/*
 * requires /lan/classes.js
 * requires /utils/kekule.utils.js
 * requires /xbrowsers/kekule.x.js
 * requires /core/kekule.common.js
 * requires /widgets/kekule.widget.base.js
 * requires /widgets/kekule.widget.menus.js
 * requires /widgets/kekule.widget.dialogs.js
 * requires /widgets/kekule.widget.helpers.js
 * requires /widgets/chem/kekule.chemWidget.base.js
 * requires /widgets/chem/kekule.chemWidget.chemObjDisplayers.js
 * requires /widgets/operation/kekule.actions.js
 * requires /widgets/commonCtrls/kekule.widget.buttons.js
 * requires /widgets/commonCtrls/kekule.widget.containers.js
 *
 * requires /localization/kekule.localize.widget.js
 */

(function(){

"use strict";

var PS = Class.PropertyScope;
var AU = Kekule.ArrayUtils;
var ZU = Kekule.ZoomUtils;
var BNS = Kekule.ChemWidget.ComponentWidgetNames;
var CW = Kekule.ChemWidget;
var EM = Kekule.Widget.EvokeMode;

/** @ignore */
Kekule.globalOptions.add('chemWidget.viewer', {
	toolButtons: [
		//BNS.loadFile,
		BNS.loadData,
		BNS.saveData,
		//BNS.clearObjs,
		BNS.molDisplayType,
		BNS.molHideHydrogens,
		BNS.zoomIn, BNS.zoomOut,
		BNS.rotateX, BNS.rotateY, BNS.rotateZ,
		BNS.rotateLeft, BNS.rotateRight,
		BNS.reset,
		BNS.openEditor
	],
	menuItems: [
		BNS.loadData,
		BNS.saveData,
		Kekule.Widget.MenuItem.SEPARATOR_TEXT,
		BNS.molDisplayType,
		BNS.molHideHydrogens,
		BNS.zoomIn, BNS.zoomOut,
		{
			'text': Kekule.$L('ChemWidgetTexts.CAPTION_ROTATE'),
			'hint': Kekule.$L('ChemWidgetTexts.HINT_ROTATE'),
			'children': [
				BNS.rotateLeft, BNS.rotateRight,
				BNS.rotateX, BNS.rotateY, BNS.rotateZ
			]
		},
		BNS.reset,
		Kekule.Widget.MenuItem.SEPARATOR_TEXT,
		BNS.openEditor,
		BNS.config
	]
});

/** @ignore */
Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, {
	VIEWER: 'K-Chem-Viewer',
	VIEWER2D: 'K-Chem-Viewer2D',
	VIEWER3D: 'K-Chem-Viewer3D',
	VIEWER_CAPTION: 'K-Chem-Viewer-Caption',
	VIEWER_EMBEDDED_TOOLBAR: 'K-Chem-Viewer-Embedded-Toolbar',

	VIEWER_MENU_BUTTON: 'K-Chem-Viewer-Menu-Button',

	VIEWER_EDITOR_FULLCLIENT: 'K-Chem-Viewer-Editor-FullClient',

	// predefined actions
	ACTION_ROTATE_LEFT: 'K-Chem-RotateLeft',
	ACTION_ROTATE_RIGHT: 'K-Chem-RotateRight',
	ACTION_ROTATE_X: 'K-Chem-RotateX',
	ACTION_ROTATE_Y: 'K-Chem-RotateY',
	ACTION_ROTATE_Z: 'K-Chem-RotateZ',
	ACTION_VIEWER_EDIT: 'K-Chem-Viewer-Edit'
});

var CNS = Kekule.Widget.HtmlClassNames;
var CCNS = Kekule.ChemWidget.HtmlClassNames;

/**
 * An universal viewer widget for chem objects (especially molecules).
 * @class
 * @augments Kekule.ChemWidget.ChemObjDisplayer
 *
 * @param {Variant} parentOrElementOrDocument
 * @param {Kekule.ChemObject} chemObj
 * @param {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}.
 * @param {Kekule.ChemWidget.ChemObjDisplayerConfigs} viewerConfigs
 *
 * @property {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}. Read only.
 * @property {Kekule.ChemObject} chemObj Object to be drawn. Set this property will repaint the client.
 * @property {Bool} chemObjLoaded Whether the chemObj is successful loaded and drawn in viewer.
 * //@property {Object} renderConfigs Configuration for rendering.
 * // This property should be an instance of {@link Kekule.Render.Render2DConfigs} or {@link Kekule.Render.Render3DConfigs}
 * //@property {Hash} drawOptions Options to draw object.
 * //@property {Float} zoom Zoom ratio to draw chem object. Note this setting will overwrite drawOptions.zoom.
 * //@property {Bool} autoSize Whether the widget change its size to fit the dimension of chem object.
 * //@property {Int} padding Padding between chem object and edge of widget, in px. Only works when autoSize is true.
 *
 * @property {String} caption Caption of viewer.
 * @property {Bool} showCaption Whether show caption below or above viewer.
 * @property {Int} captionPos Value from {@link Kekule.Widget.Position}, now only TOP and BOTTOM are usable.
 * @property {Bool} autoCaption Set caption automatically by chemObj info.
 *
 * //@property {Bool} liveUpdate Whether viewer repaint itself automatically when containing chem object changed.
 *
 * @property {Bool} enableDirectInteraction Whether interact without tool button is allowed (e.g., zoom/rotate by mouse).
 * @property {Bool} enableTouchInteraction Whether touch interaction is allowed. Note if enableDirectInteraction is false, touch interaction will also be disabled.
 * @property {Bool} enableRestraintRotation3D Set to true to rotate only on one axis of X/Y/Z when the starting point is near edge of viewer.
 * @property {Float} restraintRotation3DEdgeRatio
 * @property {Bool} enableEdit Whether a edit button is shown in toolbar to edit object in viewer. Works only in 2D mode.
 * @property {Bool} modalEdit Whether opens a modal dialog when editting object in viewer.
 * @property {Bool} enableEditFromVoid Whether editor can be launched even if viewer is empty.
 * @property {Hash} editorProperties Hash object to set properties of popup editor.
 * @property {Bool} restrainEditorWithCurrObj If true, the editor popuped can only edit current object in viewer (and add new
 *   objects is disabled). If false, the editor can do everything like a normal composer, viewer will load objects in composer
 *   after editting (and will not keep the original object in viewer).
 * @property {Bool} shareEditorInstance If true, all viewers in one document will shares one editor.
 *   This setting may reduce the cost of creating many composer widgets.
 *
 * @property {Array} toolButtons buttons in interaction tool bar. This is a array of predefined strings, e.g.: ['zoomIn', 'zoomOut', 'resetZoom', 'molDisplayType', ...]. <br />
 *   In the array, complex hash can also be used to add custom buttons, e.g.: <br />
 *     [ <br />
 *       'zoomIn', 'zoomOut',<br />
 *       {'name': 'myCustomButton1', 'widgetClass': 'Kekule.Widget.Button', 'action': actionClass},<br />
 *       {'name': 'myCustomButton2', 'htmlClass': 'MyClass' 'caption': 'My Button', 'hint': 'My Hint', '#execute': function(){ ... }},<br />
 *     ]<br />
 *   most hash fields are similar to the param of {@link Kekule.Widget.Utils.createFromHash}.<br />
 *   If this property is not set, default buttons will be used.
 * @property {Bool} enableToolbar Whether show tool bar in viewer.
 * @property {Int} toolbarPos Value from {@link Kekule.Widget.Position}, position of toolbar in viewer.
 *   For example, set this property to TOP will make toolbar shows in the center below the top edge of viewer,
 *   TOP_RIGHT will make the toolbar shows at the top right corner. Default value is BOTTOM_RIGHT.
 *   Set this property to AUTO, viewer will set toolbar position (including margin) automatically.
 * @property {Int} toolbarMarginHorizontal Horizontal margin of toolbar to viewer edge, in px.
 *   Negative value means toolbar outside viewer.
 * @property {Int} toolbarMarginVertical Vertical margin of toolbar to viewer edge, in px.
 *   Negative value means toolbar outside viewer.
 * //@property {Array} toolbarShowEvents Events to cause the display of toolbar. If set to null, the toolbar will always be visible.
 * @property {Array} toolbarEvokeModes Interaction modes to show the toolbar. Array item values should from {@link Kekule.Widget.EvokeMode}.
 *   Set enableToolbar to true and include {@link Kekule.Widget.EvokeMode.ALWAYS} will always show the toolbar.
 * @property {Array} toolbarRevokeModes Interaction modes to hide the toolbar. Array item values should from {@link Kekule.Widget.EvokeMode}.
 * @property {Int} toolbarRevokeTimeout Toolbar should be hidden after how many milliseconds after shown.
 *   Only available when {@link Kekule.Widget.EvokeMode.EVOKEE_TIMEOUT} or {@link Kekule.Widget.EvokeMode.EVOKER_TIMEOUT} in toolbarRevokeModes.
 *
 * @property {Array} allowedMolDisplayTypes Molecule types can be changed in tool bar.
 */
Kekule.ChemWidget.Viewer = Class.create(Kekule.ChemWidget.ChemObjDisplayer,
/** @lends Kekule.ChemWidget.Viewer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.Viewer',
	/** @private */
	BINDABLE_TAG_NAMES: ['div', 'span', 'img'],
	/** @private */
	DEF_BGCOLOR_2D: null,
	/** @private */
	DEF_BGCOLOR_3D: '#000000',
	/** @private */
	DEF_TOOLBAR_EVOKE_MODES: [/*EM.ALWAYS,*/ EM.EVOKEE_CLICK, EM.EVOKEE_MOUSE_ENTER, EM.EVOKEE_TOUCH],
	/** @private */
	DEF_TOOLBAR_REVOKE_MODES: [/*EM.ALWAYS,*/ /*EM.EVOKEE_CLICK,*/ EM.EVOKEE_MOUSE_LEAVE, EM.EVOKER_TIMEOUT],
	/** @construct */
	initialize: function($super, parentOrElementOrDocument, chemObj, renderType, viewerConfigs)
	{
		//this._errorReportElem = null;  // use internally
		this.setPropStoreFieldValue('renderType', renderType || Kekule.Render.RendererType.R2D); // must set this value first
		this.setPropStoreFieldValue('enableToolbar', false);
		this.setPropStoreFieldValue('toolbarEvokeModes', this.DEF_TOOLBAR_EVOKE_MODES);
		this.setPropStoreFieldValue('toolbarRevokeModes', this.DEF_TOOLBAR_REVOKE_MODES);
		this.setPropStoreFieldValue('enableDirectInteraction', true);
		this.setPropStoreFieldValue('toolbarPos', Kekule.Widget.Position.AUTO);
		this.setPropStoreFieldValue('toolbarMarginHorizontal', 10);
		this.setPropStoreFieldValue('toolbarMarginVertical', 10);
		this.setPropStoreFieldValue('showCaption', false);
		this.setPropStoreFieldValue('useCornerDecoration', true);
		//this.setUseCornerDecoration(true);
		$super(parentOrElementOrDocument, chemObj, renderType, viewerConfigs);
		this.setPadding(this.getRenderConfigs().getLengthConfigs().getActualLength('autofitContextPadding'));
		/*
		if (chemObj)
		{
			this.setChemObj(chemObj);
		}
		*/

		this._isContinuousRepainting = false;  // flag, use internally
		//this._lastRotate3DMatrix = null;  // store the last 3D rotation information

		var RT = Kekule.Render.RendererType;
		var color2D = (this.getRenderType() === RT.R2D)? (this.getBackgroundColor() || this.DEF_BGCOLOR_2D): this.DEF_BGCOLOR_2D;
		var color3D = (this.getRenderType() === RT.R3D)? (this.getBackgroundColor() || this.DEF_BGCOLOR_3D): this.DEF_BGCOLOR_3D;
		this.setBackgroundColorOfType(color2D, RT.R2D);
		this.setBackgroundColorOfType(color3D, RT.R3D);

		this.useCornerDecorationChanged();
		this.doResize();  // adjust caption and drawParent size

		this.addIaController('default', new Kekule.ChemWidget.ViewerBasicInteractionController(this), true);
	},
	/** @private */
	doFinalize: function($super)
	{
		//this.getPainter().finalize();
		var toolBar = this.getToolbar();
		$super();
		if (toolBar)
			toolBar.finalize();
		if (this._composerDialog)
			this._composerDialog.finalize();
		if (this._composerPanel)
			this._composerPanel.finalize();
	},
	/** @private */
	initProperties: function()
	{
		/*
		this.defineProp('chemObj', {'dataType': 'Kekule.ChemObject', 'serializable': false,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('chemObj', value);
				this.chemObjChanged(value);
			}
		});
		this.defineProp('chemObjLoaded', {'dataType': DataType.BOOL, 'serializable': false, 'setter': null,
			'getter': function() { return this.getChemObj() && this.getPropStoreFieldValue('chemObjLoaded'); }
		});
		this.defineProp('renderType', {'dataType': DataType.INT, 'serializable': false, 'setter': null});

		this.defineProp('renderConfigs', {'dataType': DataType.OBJECT, 'serializable': false});
		this.defineProp('drawOptions', {'dataType': DataType.HASH, 'serializable': false,
			'getter': function()
			{
				var result = this.getPropStoreFieldValue('drawOptions');
				if (!result)
				{
					result = {};
					this.setPropStoreFieldValue('drawOptions', result);
				}
				return result;
			}
		});
		*/

		//this.defineProp('zoom', {'dataType': DataType.FLOAT, 'serializable': false});

		this.defineProp('viewerConfigs', {'dataType': 'Kekule.ChemWidget.ChemObjDisplayerConfigs', 'serializable': false,
			'getter': function() { return this.getDisplayerConfigs(); },
			'setter': function(value) { return this.setDisplayerConfigs(value); }
		});

		this.defineProp('allowedMolDisplayTypes', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('allowedMolDisplayTypes', value);
				//this.updateToolbar();
				this.updateUiComps();
			}
		});

		this.defineProp('enableRestraintRotation3D', {'dataType': DataType.BOOL});
		this.defineProp('restraintRotation3DEdgeRatio', {'dataType': DataType.FLOAT});
		//this.defineProp('liveUpdate', {'dataType': DataType.BOOL});

		this.defineProp('enableEdit', {'dataType': DataType.BOOL,
			'getter': function()
			{
				// TODO: now only allows 2D editing
				return this.getPropStoreFieldValue('enableEdit') && (this.getCoordMode() !== Kekule.CoordMode.COORD3D);
			}
		});
		this.defineProp('shareEditorInstance', {'dataType': DataType.BOOL});
		this.defineProp('enableEditFromVoid', {'dataType': DataType.BOOL});
		this.defineProp('restrainEditorWithCurrObj', {'dataType': DataType.BOOL});
		this.defineProp('modalEdit', {'dataType': DataType.BOOL});
		this.defineProp('editorProperties', {'dataType': DataType.HASH});

		this.defineProp('toolButtons', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC,
			'getter': function()
			{
				var result = this.getPropStoreFieldValue('toolButtons');
				/*
				if (!result)  // create default one
				{
					result = this.getDefaultToolBarButtons();
					this.setPropStoreFieldValue('toolButtons', result);
				}
				*/
				return result;
			},
			'setter': function(value)
			{
				this.setPropStoreFieldValue('toolButtons', value);
				this.updateToolbar();
			}
		});
		this.defineProp('menuItems', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('menuItems', value);
				this.updateMenu();
			}
		});
		/*
		// private
		this.defineProp('toolButtonNameMapping', {'dataType': DataType.HASH, 'serializable': false, 'scope': PS.PRIVATE,
			'setter': null,
			'getter': function()
			{
				var result = this.getPropStoreFieldValue('toolButtonNameMapping');
				if (!result)  // create default one
				{
					result = this.createDefaultToolButtonNameMapping();
					this.setPropStoreFieldValue('toolButtonNameMapping', result);
				}
				return result;
			}
		});
		*/
		// private
		this.defineProp('menu', {'dataType': 'Kekule.Widget.Menu', 'serializable': false, 'scope': PS.PRIVATE,
			'setter': function(value)
			{
				var old = this.getMenu();
				if (value !== old)
				{
					if (old)
					{
						old.finalize();
						old = null;
					}
					this.setPropStoreFieldValue('menu', value);
				}
			}
		});

		this.defineProp('toolbar', {'dataType': 'Kekule.Widget.ButtonGroup', 'serializable': false, 'scope': PS.PRIVATE,
			'setter': function(value)
			{
				var old = this.getToolbar();
				var evokeHelper = this.getToolbarEvokeHelper();
				if (value !== old)
				{
					if (old)
					{
						old.finalize();
						var helper = this.getToolbarEvokeHelper();
						if (helper)
							helper.finalize();
						old = null;
					}
					if (evokeHelper)
						evokeHelper.finalize();
					this.setPropStoreFieldValue('toolbar', value);
					// hide the new toolbar and wait for the evoke helper to display it
					//value.setDisplayed(false);
					if (value)
					{
						this.setPropStoreFieldValue('toolbarEvokeHelper',
							new Kekule.Widget.DynamicEvokeHelper(this, value, this.getToolbarEvokeModes(), this.getToolbarRevokeModes()));
					}
				}
			}
		});

		this.defineProp('enableToolbar', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('enableToolbar', value);
				this.updateToolbar();
			}
		});

		this.defineProp('toolbarPos', {'dataType': DataType.INT, 'enumSource': Kekule.Widget.Position,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('toolbarPos', value);
				this.adjustToolbarPos();
			}
		});
		this.defineProp('toolbarMarginVertical', {'dataType': DataType.INT,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('toolbarMarginVertical', value);
				this.adjustToolbarPos();
			}
		});
		this.defineProp('toolbarMarginHorizontal', {'dataType': DataType.INT,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('toolbarMarginHorizontal', value);
				this.adjustToolbarPos();
			}
		});
		/*
		this.defineProp('toolbarShowEvents', {'dataType': DataType.ARRAY});
		this.defineProp('toolbarAlwaysShow', {'dataType': DataType.BOOL, 'serializable': false,
			'getter': function() { return !!this.getToolbarShowEvents(); },
			'setter': null
		});
		*/

		this.defineProp('toolbarEvokeHelper', {'dataType': 'Kekule.Widget.DynamicEvokeHelper',
			'serializable': false, 'setter': null, 'scope': PS.PRIVATE}); // private
		this.defineProp('toolbarEvokeModes', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('toolbarEvokeModes', value || []);
				if (this.getToolbarEvokeHelper())
					this.getToolbarEvokeHelper().setEvokeModes(value || []);
			}
		});
		this.defineProp('toolbarRevokeModes', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('toolbarRevokeModes', value || []);
				if (this.getToolbarEvokeHelper())
					this.getToolbarEvokeHelper().setRevokeModes(value || []);
			}
		});
		this.defineProp('toolbarRevokeTimeout', {'dataType': DataType.INT,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('toolbarRevokeTimeout', value);
				if (this.getToolbarEvokeHelper())
					this.getToolbarEvokeHelper().setTimeout(value);
			}
		});

		this.defineProp('toolbarParentElem', {'dataType': DataType.OBJECT, 'serializable': false,
			'setter': function(value)
			{
				if (this.getToolbarParentElem() !== value)
				{
					this.setPropStoreFieldValue('toolbarParentElem', value);
					this.updateToolbar();
				}
			}
		});

		this.defineProp('caption', {'dataType': DataType.STRING,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('caption', value);
				Kekule.DomUtils.setElementText(this.getCaptionElem(), value || '');
				this.captionChanged();
			}
		});
		this.defineProp('showCaption', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('showCaption', value);
				this.captionChanged();
			}
		});
		this.defineProp('captionPos', {'dataType': DataType.INT, 'enumSource': Kekule.Widget.Position,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('captionPos', value);
				this.captionChanged();
			}
		});
		this.defineProp('autoCaption', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('autoCaption', value);
				if (value)
					this.autoDetectCaption();
			}
		});
		this.defineProp('captionElem', {'dataType': DataType.OBJECT, 'scope': PS.PRIVATE,
			'setter': null,
			'getter': function(doNotAutoCreate)
			{
				var result = this.getPropStoreFieldValue('captionElem');
				if (!result && !doNotAutoCreate)  // create new
				{
					result = this.doCreateCaptionElem();
					this.setPropStoreFieldValue('captionElem', result);
				}
				return result;
			}
		});

		this.defineProp('actions', {'dataType': 'Kekule.ActionList', 'serializable': false, 'scope': PS.PUBLIC,
			'setter': null,
			'getter': function()
			{
				var result = this.getPropStoreFieldValue('actions');
				if (!result)
				{
					result = new Kekule.ActionList();
					this.setPropStoreFieldValue('actions', result);
				}
				return result;
			}
		});
		this.defineProp('actionMap', {'dataType': 'Kekule.MapEx', 'serializable': false, 'scope': PS.PRIVATE,
			'setter': null,
			'getter': function()
			{
				var result = this.getPropStoreFieldValue('actionMap');
				if (!result)
				{
					result = new Kekule.MapEx(true);
					this.setPropStoreFieldValue('actionMap', result);
				}
				return result;
			}
		});

		this.defineProp('enableDirectInteraction', {'dataType': DataType.BOOL});
		this.defineProp('enableTouchInteraction', {'dataType': DataType.BOOL,
			'setter': function(value)
			{
				this.setPropStoreFieldValue('enableTouchInteraction', !!value);
				this.setTouchAction(value? 'none': null);
			}
		});
	},
	/** @ignore */
	initPropValues: function($super)
	{
		// debug
		/*
		this.setEnableEdit(true);
		*/
		this.setUseNormalBackground(false);
		this.setModalEdit(true);
		this.setRestrainEditorWithCurrObj(true);
		this.setRestraintRotation3DEdgeRatio(0.18);
		this.setEnableRestraintRotation3D(true);
		this.setShareEditorInstance(true);
		this.setEnableTouchInteraction(!true);
	},

	/** @ignore */
	canUsePlaceHolderOnElem: function(elem)
	{
		// When using a img element with src image, it may contains the figure of chem object
		var imgSrc = elem.getAttribute('src');
		return (elem.tagName.toLowerCase() === 'img') && (!!imgSrc);
	},

	/** @ignore */
	doObjectChange: function($super, modifiedPropNames)
	{
		$super(modifiedPropNames);
		this.updateActions();
	},

	/** @ignore */
	doSetElement: function($super, element)
	{
		var elem = element;
		if (elem)
		{
			var tagName = elem.tagName.toLowerCase();
			if (tagName === 'img')  // is an image element, need to use span to replace it
			{
				elem = Kekule.DomUtils.replaceTagName(elem, 'span');
				//this.setElement(elem);
				//console.log('replace img to span');
			}
		}
		return $super(elem);
	},
	/** @ignore */
	doUnbindElement: function($super, element)
	{
		// unbind old element, the context parent element should be set to null
		if (this._drawContextParentElem && this._drawContextParentElem.parentNode)
		{
			this._drawContextParentElem.parentNode.removeChild(this._drawContextParentElem);
			this._drawContextParentElem = null;
		}
		return $super(element);
	},

	/** @ignore */
	doCreateRootElement: function(doc)
	{
		var result = doc.createElement('div');
		return result;
	},
	/** @ignore */
	doGetWidgetClassName: function($super)
	{
		var result = $super() + ' ' + CCNS.VIEWER;
		try  // may raise exception when called with class prototype (required by placeholder related methods)
		{
			var renderType = this.getRenderType();
			var additional = this._getRenderTypeSpecifiedHtmlClassName(renderType);
			result += ' ' + additional;
		}
		catch(e)
		{

		}
		return result;
	},
	/** @private */
	_getRenderTypeSpecifiedHtmlClassName: function(renderType)
	{
		return (renderType === Kekule.Render.RendererType.R3D)?
			CCNS.VIEWER3D: CCNS.VIEWER2D;
	},

	/** @ignore */
	getResizerElement: function()
	{
		return this.getDrawContextParentElem();
	},

	/** @ignore */
	doResize: function($super)
	{
		//$super();
		this.adjustDrawParentDim();
		this.adjustToolbarPos();
		$super();
	},
	/** @ignore */
	doWidgetShowStateChanged: function(isShown)
	{
		if (isShown)
		{
			//console.log('update toolbar');
			//this.updateToolbar();
			this.updateActions();
		}
	},
	/** @ignore */
	refitDrawContext: function($super, doNotRepaint)
	{
		// resize context, means client size changed, so toolbar should also be adjusted.
		$super(doNotRepaint);
		this.adjustToolbarPos();
	},

	/** @ignore */
	getAllowRenderTypeChange: function()
	{
		return true;
	},
	/** @ignore */
	resetRenderType: function($super, oldType, newType)
	{
		$super(oldType, newType);
		// classname
		var oldHtmlClassName = this._getRenderTypeSpecifiedHtmlClassName(oldType);
		var newHtmlClassName = this._getRenderTypeSpecifiedHtmlClassName(newType);
		this.removeClassName(oldHtmlClassName);
		this.addClassName(newHtmlClassName);
		// toolbar
		//this.updateToolbar();
		this.updateUiComps();
	},

	/** @private */
	doLoadEnd: function(chemObj)
	{
		this.updateActions();
		this.autoDetectCaption();
	},

	/** @private */
	doSetUseCornerDecoration: function($super, value)
	{
		$super(value);
		this.useCornerDecorationChanged();
	},
	/** @private */
	useCornerDecorationChanged: function()
	{
		var elem = this.getDrawContextParentElem();  // do not auto create element
		if (elem)
		{
			var v = this.getUseCornerDecoration();
			if (v)
				Kekule.HtmlElementUtils.addClass(elem, CNS.CORNER_ALL);
			else
				Kekule.HtmlElementUtils.removeClass(elem, CNS.CORNER_ALL);
		}
	},

	/** @private */
	adjustDrawParentDim: function()
	{
		var drawParentElem = this.getDrawContextParentElem();
		var parentElem = drawParentElem.parentNode;
		var captionElem = this.getCaptionElem(true);  // do not auto create
		var dimParent = Kekule.HtmlElementUtils.getElemClientDimension(parentElem);
		var t, h;
		if (captionElem && this.captionIsShown() && captionElem.parentNode === parentElem)
		{
			var dimCaption = Kekule.HtmlElementUtils.getElemClientDimension(captionElem);
			h = Math.max(dimParent.height - dimCaption.height, 0);  // avoid value < 0
			t = (this.getCaptionPos() & Kekule.Widget.Position.TOP)? dimCaption.height: 0;

			drawParentElem.style.top = t + 'px';
			drawParentElem.style.height = h + 'px';
		}
		else
		{
			/*
			t = 0;
			h = dimParent.height;
			*/
			// restore 100% height setting
			Kekule.StyleUtils.removeStyleProperty(drawParentElem.style, 'top');
			//Kekule.StyleUtils.removeStyleProperty(drawParentElem.style, 'height');
			drawParentElem.style.height = dimParent.height + 'px';  // explicit set height, or the height may not be updated in some mobile browsers
		}

		//this.refitDrawContext();
	},

	/** @private */
	getInteractionReceiverElem: function()
	{
		return this.getDrawContextParentElem();
	},

	/** @ignore */
	setDrawDimension: function($super, width, height)
	{
		var newHeight = height;
		if (this.captionIsShown())  // height need add the height of caption
		{
			var dimCaption = Kekule.HtmlElementUtils.getElemClientDimension(this.getCaptionElem());
			newHeight += dimCaption.height || 0;
		}
		$super(width, newHeight);
	},

	/// Methods about caption: currently not used ///////////
	/* @private */
	doCreateCaptionElem: function()
	{
		var result = this.getDocument().createElement('span');
		result.className = CNS.DYN_CREATED + ' ' + ' ' + CNS.SELECTABLE + ' ' + CCNS.VIEWER_CAPTION;
		this.getElement().appendChild(result);
		return result;
	},
	/**
	 * Called when caption or showCaption or captionPos property changes.
	 * @private
	 */
	captionChanged: function()
	{
		if (this.captionIsShown())
		{
			var elem = this.getCaptionElem();
			var style = elem.style;
			var pos = this.getCaptionPos();
			if (pos & Kekule.Widget.Position.TOP)
			{
				style.top = 0;
				style.bottom = 'auto';
			}
			else
			{
				style.bottom = 0;
				style.top = 'auto';
			}
			style.display = 'block';
		}
		else  // caption need to be hidden
		{
			var elem = this.getCaptionElem(true);  // do not auto create
			if (elem)
				elem.style.display = 'none';
		}
		//this.adjustDrawParentDim();
		this.doResize();
	},
	/**
	 * Returns whether the caption is actually displayed.
	 */
	captionIsShown: function()
	{
		return this.getCaption() && this.getShowCaption();
	},

	/*
	 * Called when caption or showCaption property has been changed.
	 * @private
	 */
	/*
	captionChanged: function()
	{
		var displayCaption = this.getShowCaption() && this.getCaption();
		var elem = this.getCaptionElem();
		Kekule.DomUtils.setElementText(elem, this.getCaption());
		elem.style.display = displayCaption? 'inherit': 'none';
	},
	*/

	/// Methods about popup editing ////////////////
	/**
	 * Returns whether editor can be lauched in current viewer.
	 */
	getAllowEditing: function()
	{
		return (this.getCoordMode() !== Kekule.CoordMode.COORD3D) &&
			this.getEnableEdit() && (this.getChemObj() || this.getEnableEditFromVoid());
	},
	/** @private */
	getComposerDialog: function()
	{
		var result;
		if (this.getShareEditorInstance())
			result = Kekule.ChemWidget.Viewer._composerDialog;
		else
			result = this._composerDialog;
		if (!result)
		{
			if (Kekule.Editor.ComposerDialog)
			{
				result = new Kekule.Editor.ComposerDialog(this.getDocument(), Kekule.$L('ChemWidgetTexts.CAPTION_EDIT_OBJ'), //CWT.CAPTION_EDIT_OBJ,
						[Kekule.Widget.DialogButtons.OK, Kekule.Widget.DialogButtons.CANCEL]);
			}
		}
		if (this.getShareEditorInstance())
			Kekule.ChemWidget.Viewer._composerDialog = result;
		else
			this._composerDialog = result;
		return result;
	},
	/** @private */
	getComposerPanel: function()
	{
		var result;
		if (this.getShareEditorInstance())
			result = Kekule.ChemWidget.Viewer._composerPanel;
		else
			result = this._composerPanel;
		if (!result)  // create new
		{
			if (Kekule.Editor.Composer/* && Kekule.Editor.ComposerFrame*/)
			{
				result = new Kekule.Editor.Composer(this.getDocument());
				//result = new Kekule.Editor.ComposerFrame(this.getDocument());
				result.addClassName(CCNS.VIEWER_EDITOR_FULLCLIENT);
				result.setUseNormalBackground(true);
				//result.setAutoAdjustSizeOnPopup(true);
				var minDim = Kekule.Editor.Composer.getMinPreferredDimension();
				result.setMinDimension(minDim);
				//result.setAutoSetMinDimension(true);
				result.setEnableDimensionTransform(true);
				result.setAutoResizeConstraints({'width': 1, 'height': 1});
				// set toolbar buttons, remove config and inspector to save place
				var btns = Kekule.globalOptions.chemWidget.composer.commonToolButtons;
				btns = AU.exclude(btns, [BNS.cut, BNS.config, BNS.objInspector]);
				result.setCommonToolButtons(btns);

				// two custom buttons to save or discard edits
				var customButtons = [
					{
						'text': Kekule.$L('ChemWidgetTexts.CAPTION_EDITOR_DONE'),
						'hint': Kekule.$L('ChemWidgetTexts.HINT_EDITOR_DONE'),
						'htmlClass': 'K-Res-Button-YesOk',
						'showText': true,
						'#execute': function() { result._doneEditCallback(); result.hide(); }
					},
					{
						'text': Kekule.$L('ChemWidgetTexts.CAPTION_EDITOR_CANCEL'),
						'hint': Kekule.$L('ChemWidgetTexts.HINT_EDITOR_CANCEL'),
						'htmlClass': 'K-Res-Button-NoCancel',
						'showText': true,
						'#execute': function() { result.hide(); }
					}
				];
				result._customEndEditButtons = customButtons;  // use a special field to store in composer
			}
		}
		if (this.getShareEditorInstance())
			Kekule.ChemWidget.Viewer._composerPanel = result;
		else
			this._composerPanel = result;
		result._invokerViewer = this;
		result._doneEditCallback = null;
		return result;
	},
	/**
	 * Open a popup editor to modify displayed object.
	 * @param {Kekule.Widget.BaseWidget} callerWidget Who invokes edit action, default is the viewer itself.
	 */
	openEditor: function(callerWidget)
	{
		//if (this.getEnableEdit() && this.getChemObj())
		if (this.getAllowEditing())
		{
			// load object in editor
			var chemObj = this.getChemObj();
			var editFromVoid = !chemObj;
			var editFromEmpty =  chemObj && chemObj.isEmpty && chemObj.isEmpty(); // has chem object but obj is empty (e.g., mol with no atom and bond)
			var restrainObj = this.getRestrainEditorWithCurrObj();

			var clientDim = Kekule.DocumentUtils.getClientDimension(this.getDocument());
			//var clientDim = Kekule.DocumentUtils.getClientVisibleBox(this.getDocument());
			//console.log(clientDim);
			var minComposerDim = Kekule.Editor.Composer.getMinPreferredDimension();
			if (clientDim.width <= minComposerDim.width + 50 || clientDim.height < minComposerDim.height + 100)
			{
				this._openEditComposer(callerWidget, chemObj, restrainObj, editFromVoid, editFromEmpty);
			}
			else
			{
				this._openEditComposerDialog(callerWidget, chemObj, restrainObj, editFromVoid, editFromEmpty);
			}
		}
	},
	/** @private */
	_prepareEditComposer: function(composer, restrainObj, editFromVoid, editFromEmpty)
	{
		composer.setEnableCreateNewDoc(editFromVoid || !restrainObj);
		composer.setEnableLoadNewFile(editFromVoid || !restrainObj);
		composer.setAllowCreateNewChild(editFromVoid || !restrainObj);

		var editorProperties = this.getEditorProperties();
		if (editorProperties)
			composer.setPropValues(editorProperties);

		//composer.updateAllActions();
		//console.log(composer.getEnableLoadNewFile(), editFromVoid);
	},
	/** @private */
	_feedbackEditResult: function(composer, chemObj, editFromVoid)
	{
		if (!composer.isDirty())
			return;
		var newObj = composer.getSavingTargetObj();
		if (editFromVoid)
		{
			this.setChemObj(newObj.clone());
		}
		else
		{
			if (this.getRestrainEditorWithCurrObj())
			{
				if (chemObj.getClass() === newObj.getClass())  // same type of object in editor
					chemObj.assign(newObj.clone());
				else  // preserve old object type in viewer
					chemObj.assign(cloneObj);
				// clear src info data
				chemObj.setSrcInfo(null);
				//self.repaint();
				this.setChemObj(chemObj); // force repaint, as repaint() will not reflect to object changes
			}
			else // not restrain, load object in composer directy into viewer
			{
				//console.log(newObj);
				this.setChemObj(newObj);
			}
		}
	},
	/** @private */
	_openEditComposerDialog: function(callerWidget, chemObj, restrainObj, editFromVoid, editFromEmpty)
	{
		var dialog = this.getComposerDialog();
		if (!dialog)  // can not invoke composer dialog
		{
			Kekule.error(Kekule.$L('ErrorMsg.CAN_NOT_CREATE_EDITOR'));
			return;
		}

		var composer = dialog.getComposer();
		this._prepareEditComposer(composer, restrainObj, editFromVoid, editFromEmpty);

		var cloneObj;
		if (!editFromVoid && !editFromEmpty)
		{
			cloneObj = chemObj.clone();  // edit this cloned one, avoid affect chemObj directly
			dialog.setChemObj(cloneObj);
		}
		else
		{
			//dialog.setChemObj(null);
			dialog.getComposer().newDoc();
		}

		var self = this;
		var callback = function(dialogResult)
		{
			if (dialogResult === Kekule.Widget.DialogButtons.OK && dialog.getComposer().isDirty())  // feedback result
			{
				/*
				var newObj = dialog.getSavingTargetObj();
				if (editFromVoid)
				{
					self.setChemObj(newObj.clone());
				}
				else
				{
					if (self.getRestrainEditorWithCurrObj())
					{
						if (chemObj.getClass() === newObj.getClass())  // same type of object in editor
							chemObj.assign(newObj.clone());
						else  // preserve old object type in viewer
							chemObj.assign(cloneObj);
						// clear src info data
						chemObj.setSrcInfo(null);
						//self.repaint();
						self.setChemObj(chemObj); // force repaint, as repaint() will not reflect to object changes
					}
					else // not restrain, load object in composer directy into viewer
					{
						//console.log(newObj);
						self.setChemObj(newObj);
					}
				}
				*/
				self._feedbackEditResult(dialog.getComposer(), chemObj, editFromVoid);
			}
			//dialog.finalize();
		};
		if (this.getModalEdit())
			dialog.openModal(callback, callerWidget || this);
		else
			dialog.openPopup(callback, callerWidget || this);
	},
	/** @private */
	_openEditComposer: function(callerWidget, chemObj, restrainObj, editFromVoid, editFromEmpty)
	{
		var composer = this.getComposerPanel();
		if (!composer)  // can not invoke composer
		{
			Kekule.error(Kekule.$L('ErrorMsg.CAN_NOT_CREATE_EDITOR'));
			return;
		}

		//var composer = composerFrame.getComposer();
		this._prepareEditComposer(composer, restrainObj, editFromVoid, editFromEmpty);
		//composer.newDoc();

		// ensure save & cancel buttons are in toolbar
		var customButtons = composer._customEndEditButtons;
		var toolbtns = composer.getCommonToolButtons() || [];
		var btnModified = false;
		for (var i = 0, l = customButtons.length; i < l; ++i)
		{
			var btn = customButtons[i];
			if (toolbtns.indexOf(btn) < 0)
			{
				toolbtns.push(btn);
				btnModified = true;
			}
		}
		if (btnModified)
			composer.setCommonToolButtons(toolbtns);


		if (!editFromVoid && !editFromEmpty)
		{
			//composer.updateDimensionTransform();
			var cloneObj = chemObj.clone();  // edit this cloned one, avoid affect chemObj directly
			composer.setChemObj(cloneObj);
		}
		else
		{
			composer.newDoc();
		}

		var self = this;
		composer._doneEditCallback = function(){
			self._feedbackEditResult(composer, chemObj, editFromVoid);
		};
		composer.show(callerWidget, function(){
			//var cloneObj;
			if (!editFromVoid && !editFromEmpty)
			{
				//composer.updateDimensionTransform();
				//cloneObj = chemObj.clone();  // edit this cloned one, avoid affect chemObj directly
				composer.setChemObj(cloneObj); // set chemObj again, or it will not be displayed in some mobile browsers
			}
			else
			{
				//dialog.setChemObj(null);
				//composer.newDoc();
			}
		}, Kekule.Widget.ShowHideType.POPUP);
	},
	/*
	 * Returns a new widget to edit object in viewer.
	 * @private
	 */
	/*
	createEditorWidget: function()
	{
		var result = new Kekule.Editor.Composer(this.getDocument());
		var editor = result.getEditor();
		editor.setEnableCreateNewDoc(false);
		editor.setEnableLoadNewFile(false);
		editor.setAllowCreateNewChild(false);
		editor.addClassName(CNS.DYN_CREATED);
		return result;
	},
	*/


	////////////////////////////////////////////////
	/**
	 * Reset viewer to initial state (no zoom, rotation and so on).
	 */
	resetView: function()
	{
		return this.resetDisplay();
	},


	/**
	 * Returns current 2D rotation angle (in arc).
	 * @returns {Float}
	 */
	getCurr2DRotationAngle: function()
	{
		return this.getDrawOptions().rotateAngle || 0;
	},
	/**
	 * Do a 2D rotation base on delta.
 	 * @param {Float} delta In arc.
	 * @param {Bool} suspendRendering Set this to true if a immediate repaint is not needed.
	 */
	rotate2DBy: function(delta, suspendRendering)
	{
		return this.rotate2DTo(this.getCurr2DRotationAngle() + delta, suspendRendering);
	},
	/**
	 * Do a 2D rotation to angle.
	 * @param {Float} angle In arc.
	 * @param {Bool} suspendRendering Set this to true if a immediate repaint is not needed.
	 */
	rotate2DTo: function(angle, suspendRendering)
	{
		this.getDrawOptions().rotateAngle = angle;
		//this.drawOptionChanged();
		if (!suspendRendering)
			this.geometryOptionChanged();
		return this;
	},

	/**
	 * Returns current 3D rotation info.
	 * @returns {Hash} {rotateMatrix, rotateX, rotateY, rotateZ, rotateAngle, rotateAxisVector}
	 */
	getCurr3DRotationInfo: function()
	{
		var result = {};
		var fields = ['rotateMatrix', 'rotateX', 'rotateY', 'rotateZ', 'rotateAngle'];
		var ops = this.getDrawOptions();
		for (var i = 0, l = fields.length; i < l; ++i)
		{
			var field = fields[i];
			result[field] = ops[field] || 0;
		}
		// rotateAxisVector
		result.rotateAxisVector = ops.rotateAxisVector || null;
		return result;
	},
	/**
	 * Set 3D rotation matrix.
	 * @param {Array} matrix A 4X4 rotation matrix.
	 * @param {Bool} suspendRendering Set this to true if a immediate repaint is not needed.
	 */
	setRotate3DMatrix: function(matrix, suspendRendering)
	{
		this.getDrawOptions().rotateMatrix = matrix;
		//this.drawOptionChanged();
		if (!suspendRendering)
			this.geometryOptionChanged();
		return this;
	},

	/**
	 * Do a 3D rotation base on delta.
	 * @param {Float} deltaX In arc.
	 * @param {Float} deltaY In arc.
	 * @param {Float} deltaZ In arc.
	 * @param {Bool} suspendRendering Set this to true if a immediate repaint is not needed.
	 */
	rotate3DBy: function(deltaX, deltaY, deltaZ, suspendRendering)
	{
		var lastInfo = this.getCurr3DRotationInfo();
		var lastMatrix = lastInfo.rotateMatrix || Kekule.MatrixUtils.createIdentity(4);
		//console.log('lastMatrix', lastMatrix);
		var currMatrix = Kekule.CoordUtils.calcRotate3DMatrix({
			'rotateX': deltaX,
			'rotateY': deltaY,
			'rotateZ': deltaZ
		});
		//var matrix = Kekule.MatrixUtils.multiply(lastMatrix, currMatrix);
		var matrix = Kekule.MatrixUtils.multiply(currMatrix, lastMatrix);   // x/y/z system changes also after each rotation
		//console.log('nowMatrix', matrix);
		this.setRotate3DMatrix(matrix, suspendRendering);
		/*
		angles.rotateX += deltaX || 0;
		angles.rotateY += deltaY || 0;
		angles.rotateZ += deltaZ || 0;
		return this.rotate3DTo(angles.rotateX, angles.rotateY, angles.rotateZ);
		*/
		return this;
	},
	/**
	 * Do a 3D rotation around axis.
	 * @param {Float} angle In arc.
	 * @param {Hash} axisVector Axis vector coord.
	 * @param {Bool} suspendRendering Set this to true if a immediate repaint is not needed.
	 */
	rotate3DByAxis: function(angle, axisVector, suspendRendering)
	{
		var lastInfo = this.getCurr3DRotationInfo();
		var lastMatrix = lastInfo.rotateMatrix || Kekule.MatrixUtils.createIdentity(4);
		var currMatrix = Kekule.CoordUtils.calcRotate3DMatrix({
			'rotateAngle': angle,
			'rotateAxisVector': axisVector
		});
		var matrix = Kekule.MatrixUtils.multiply(currMatrix, lastMatrix);   // sequence is IMPORTANT!
		//var matrix = Kekule.MatrixUtils.multiply(lastMatrix, currMatrix);
		this.setRotate3DMatrix(matrix, suspendRendering);
		return this;
	},
	/*
	 * Do a 3D rotation to angle on x/y/z axis
	 * @param {Float} x Rotation on X axis
	 * @param {Float} y Rotation on Y axis
	 * @param {Float} z Rotation on Z axis
	 */
	/*
	rotate3DTo: function(x, y, z)
	{
		var ops = this.getDrawOptions();
		ops.rotateX = x || 0;
		ops.rotateY = y || 0;
		ops.rotateZ = z || 0;

		this.drawOptionChanged();
		return this;
	},
	*/

	// methods about tool buttons
	/** @private */
	getDefaultToolBarButtons: function()
	{
		return Kekule.globalOptions.chemWidget.viewer.toolButtons;
		/*
		var buttons = [
			//BNS.loadFile,
			BNS.loadData,
			BNS.saveData,
			//BNS.clearObjs,
			BNS.molDisplayType,
			BNS.molHideHydrogens,
			BNS.zoomIn, BNS.zoomOut
		];
		// rotate
		//if (this.getRenderType() === Kekule.Render.RendererType.R3D)
		{
			buttons = buttons.concat([BNS.rotateX, BNS.rotateY, BNS.rotateZ]);
		}
		//else
		{
			buttons = buttons.concat([BNS.rotateLeft, BNS.rotateRight]);
		}
		buttons.push(BNS.reset);
		// debug
		buttons.push(BNS.openEditor);
		// config
		buttons.push(BNS.config);
		// debug
		//buttons.push(BNS.menu);

		return buttons;
		*/
	},

	/* @private */
	/*
	createDefaultToolButtonNameMapping: function()
	{
		var result = {};
		result[BNS.loadFile] = CW.ActionDisplayerLoadFile;
		result[BNS.loadData] = CW.ActionDisplayerLoadData;
		result[BNS.saveData] = CW.ActionDisplayerSaveFile;
		result[BNS.clearObjs] = CW.ActionDisplayerClear;
		result[BNS.zoomIn] = CW.ActionDisplayerZoomIn;
		result[BNS.zoomOut] = CW.ActionDisplayerZoomOut;
		result[BNS.rotateLeft] = CW.ActionViewerRotateLeft;
		result[BNS.rotateRight] = CW.ActionViewerRotateRight;
		result[BNS.rotateX] = CW.ActionViewerRotateX;
		result[BNS.rotateY] = CW.ActionViewerRotateY;
		result[BNS.rotateZ] = CW.ActionViewerRotateZ;
		result[BNS.reset] = CW.ActionDisplayerReset;
		result[BNS.molHideHydrogens] = CW.ActionDisplayerHideHydrogens;
		result[BNS.molDisplayType] = CW.ActionViewerChangeMolDisplayTypeStub;

		result[BNS.openEditor] = CW.ActionViewerEdit;
		result[BNS.config] = Kekule.Widget.ActionOpenConfigWidget;

		return result;
	},
	*/

	/**
	 * Return whether toolbarParentElem is not set the the toolbar is directly embedded in viewer itself.
	 * @returns {Bool}
	 */
	isToolbarEmbedded: function()
	{
		return !this.getToolbarParentElem();
	},

	/**
	 * Recalc and set toolbar position.
	 * @private
	 */
	adjustToolbarPos: function()
	{
		var toolbar = this.getToolbar();
		if (!toolbar)
			return;
		if (this.isToolbarEmbedded())
		{
			//if (toolbar)
			{
				var WP = Kekule.Widget.Position;
				//var viewerClientRect = Kekule.HtmlElementUtils.getElemBoundingClientRect(this.getDrawContextParentElem()); //this.getBoundingClientRect();
				var viewerClientRect = Kekule.HtmlElementUtils.getElemPageRect(this.getDrawContextParentElem());
				//var toolbarClientRect = toolbar.getBoundingClientRect();
				var toolbarClientRect = Kekule.HtmlElementUtils.getElemPageRect(toolbar);
				var pos = this.getToolbarPos();
				var hMargin = this.getToolbarMarginHorizontal();
				var vMargin = this.getToolbarMarginVertical();

				// default
				var hPosProp = 'left', vPosProp = 'top';
				var hPosPropUnused = 'right', vPosPropUnused = 'bottom';
				var hPosValue = (viewerClientRect.width - toolbarClientRect.width) / 2;
				var vPosValue = (viewerClientRect.height - toolbarClientRect.height) / 2;

				if (pos === WP.AUTO || !pos)  // auto decide position, including margin
				{
					var toolbarTotalW = ((hMargin > 0) ? hMargin : 0) * 2 + toolbarClientRect.width;
					var toolbarTotalH = ((vMargin > 0) ? vMargin : 0) * 2 + toolbarClientRect.height;
					if (toolbarTotalW > viewerClientRect.width)  // can not fit in viewer
					{
						pos |= WP.LEFT;
						hMargin = 0;
					}
					else
						pos |= WP.RIGHT;
					if (toolbarTotalH > viewerClientRect.height / 2)  // can not fit in viewer
					{
						pos |= WP.BOTTOM;
						vMargin = -1;
					}
					else
						pos |= WP.BOTTOM;
				}

				// horizontal direction
				if (pos & WP.LEFT)
				{
					hPosProp = 'left';
					hPosPropUnused = 'right';
					hPosValue = (hMargin >= 0) ? hMargin : hMargin - toolbarClientRect.width;
				}
				else if (pos & WP.RIGHT)
				{
					hPosProp = 'right';
					hPosPropUnused = 'left';
					hPosValue = (hMargin >= 0) ? hMargin : hMargin - toolbarClientRect.width;
				}
				// vertical direction
				if (pos & WP.TOP)
				{
					vPosProp = 'top';
					vPosPropUnused = 'bottom';
					vPosValue = (vMargin >= 0) ? vMargin : vMargin - toolbarClientRect.height;
				}
				else if (pos & WP.BOTTOM)
				{
					vPosProp = 'bottom';
					vPosPropUnused = 'top';
					vPosValue = (vMargin >= 0) ? vMargin : vMargin - toolbarClientRect.height;
				}
				toolbar.removeStyleProperty(hPosPropUnused);
				toolbar.removeStyleProperty(vPosPropUnused);
				toolbar.setStyleProperty(hPosProp, hPosValue + 'px');
				toolbar.setStyleProperty(vPosProp, vPosValue + 'px');
			}
		}
		else  // toolbar parent appointed
		{
			toolbar.removeStyleProperty('left');
			toolbar.removeStyleProperty('top');
			toolbar.removeStyleProperty('width');
			toolbar.removeStyleProperty('height');
		}
	},
	/**
	 * Update toolbar in viewer.
	 */
	updateToolbar: function()
	{
		if (this.getEnableToolbar())
		{
			this.createToolbar();
			this.adjustToolbarPos();
		}
		else
			this.setToolbar(null);
	},
	/**
	 * Update menu in viewer.
	 */
	updateMenu: function()
	{
		this.createMenu();
	},
	/**
	 * Update toolbar and menu in viewer.
	 */
	updateUiComps: function()
	{
		this.updateToolbar();
		this.updateMenu();
	},

	/**
	 * Update toolbar actions.
	 * @private
	 */
	updateActions: function()
	{
		if (this.getActions())
			this.getActions().updateAll();
	},

	/** @private */
	clearActions: function()
	{
		this.getActions().clear();
		this.getActionMap().clear();
	},

	/** @private */
	getCompActionClass: function(compName)
	{
		//return this.getToolButtonNameMapping()[compName];
		return this.getChildActionClass(compName, true);
	},

	/** @private */
	_getActionOfComp: function(compNameOrComp, canCreate, defActionClass)
	{
		var map = this.getActionMap();
		var result = map.get(compNameOrComp);
		if (!result && canCreate)
		{
			var c = this.getCompActionClass(compNameOrComp) || defActionClass;
			if (c)
			{
				result = new c(this);
				map.set(compNameOrComp, result);
				this.getActions().add(result);
			}
		}
		return result;
	},

	/** @private */
	createToolbar: function()
	{
		this.clearActions();
		var toolBar = new Kekule.Widget.ButtonGroup(this);
		toolBar.addClassName(CNS.DYN_CREATED);
		toolBar.setDisplayed(false);  // hide at first, evokeHelper controls its visibility
		//console.log('After create, display to: ', toolBar.getDisplayed());
		//toolBar.show();
		// add buttons
		//var settings = this.getToolButtonSettings();
		toolBar.setShowText(false);
		toolBar.doSetShowGlyph(true);

		var btns = this.getToolButtons() || this.getDefaultToolBarButtons(); //settings.buttons;
		for (var i = 0, l = btns.length; i < l; ++i)
		{
			var name = btns[i];
			var btn = this.createToolButton(name, toolBar);
		}
		toolBar.addClassName(CCNS.INNER_TOOLBAR);
		if (this.isToolbarEmbedded())
			toolBar.addClassName(CCNS.VIEWER_EMBEDDED_TOOLBAR);

		toolBar.appendToElem(this.getToolbarParentElem() || this.getElement()/*this.getDrawContextParentElem()*/);
			// IMPORTANT, must append to widget before setToolbar,
			// otherwise in Chrome the tool bar may be hidden at first even if we set it to always show
		//console.log('After append to widget: ', toolBar.getDisplayed());
		this.setToolbar(toolBar);
		//console.log('After set tool bar, display to: ', toolBar.getDisplayed());
		//this.updateActions();

		return toolBar;
	},
	/** @private */
	createToolButton: function(btnName, parentGroup)
	{
		var result = null;
		var beginContinuousRepaintingBind = this.beginContinuousRepainting.bind(this);
		var endContinuousRepaintingBind = this.endContinuousRepainting.bind(this);

		var rotateBtnNames = [BNS.rotateX, BNS.rotateY, BNS.rotateZ, BNS.rotateLeft, BNS.rotateRight];

		if (DataType.isObjectValue(btnName))  // custom button
		{
			var objDefHash = Object.extend({'widget': Kekule.Widget.Button}, btnName);
			result = Kekule.Widget.Utils.createFromHash(parentGroup, objDefHash);
			var actionClass = objDefHash.actionClass;
			if (actionClass)  // create action
			{
				if (typeof(actionClass) === 'string')
					actionClass = ClassEx.findClass(objDefHash.actionClass);
			}
			if (actionClass)
			{
				//var action = new actionClass(this);
				//this.getActions().add(action);
				var action = this._getActionOfComp(result, true, actionClass);
				if (action)
					result.setAction(action);
			}
		}
		else
		{
			if (btnName === BNS.molDisplayType)  // in 2D or 3D mode, type differs a lot, need handle separately
			{
				return this.createMolDisplayTypeButton(parentGroup);
			}
			else if (btnName === BNS.menu)  // menu button
			{
				return this.createMenuButton(parentGroup);
			}

			var actionClass = this.getCompActionClass(btnName);
			var btnClass = Kekule.Widget.Button;
			if (btnName === BNS.molHideHydrogens)
				btnClass = Kekule.Widget.CheckButton;
			//if (actionClass)
			{
				result = new btnClass(parentGroup);
				//result.addClassName(CCNS.PREFIX + btnName);
				//var action = new actionClass(this);
				var action = this._getActionOfComp(btnName, true);
				//this.getActions().add(action);
				if (action)
					result.setAction(action);
				if (rotateBtnNames.indexOf(btnName) >= 0)
				{
					result.setPeriodicalExecInterval(20);
					result.setEnablePeriodicalExec(true);
					result.addEventListener('activate', beginContinuousRepaintingBind);
					result.addEventListener('deactivate', endContinuousRepaintingBind);
				}
			}
		}
		return result;
	},
	/** @private */
	createMenuButton: function(parentGroup)
	{
		var result = new Kekule.Widget.DropDownButton(parentGroup);
		result.setText(Kekule.$L('WidgetTexts.CAPTION_MENU')).setHint(Kekule.$L('WidgetTexts.HINT_MENU'));
		result.addClassName(CCNS.VIEWER_MENU_BUTTON);
		// create menu if essential
		if (!this.getMenu())
			this.createMenu();
		result.setDropDownWidget(this.getMenu());

		return result;
	},
	/** @private */
	createMolDisplayTypeButton: function(parentGroup)
	{
		var result = new Kekule.Widget.CompactButtonSet(parentGroup);
		result.getButtonSet().addClassName(CCNS.INNER_TOOLBAR);
		  // IMPORTANT: buttonSet may be popup and moved in DOM tree before showing,
		  // without this, the button size setting in CSS may be lost
		  // TODO: may find a better solution to solve popup widget style lost problem
		result.setShowText(false);
		//result.setHint(CWT.HINT_MOL_DISPLAY_TYPE);
		//var action = new Kekule.ChemWidget.ActionViewerChangeMolDisplayTypeStub(this);
		var action = this._getActionOfComp(BNS.molDisplayType, true);
		result.setAction(action);
		this.getActions().add(action);

		// DONE: Now we fix the drop down direction
		//result.setDropPosition(Kekule.Widget.Position.TOP);

		var doc = this.getDocument();
		var allowedType = this.getAllowedMolDisplayTypes();
		var actionClasses = this._getUsableMolDisplayActionClasses(allowedType);

		for (var i = 0, l = actionClasses.length; i < l; ++i)
		{
			var displayType = actionClasses[i].TYPE;
			/*
			if (allowedType && (allowedType.indexOf(displayType) < 0) && (displayType !== this.getCurrMoleculeDisplayType()))  // not in allowed type
				continue;
			*/

			var btn = new Kekule.Widget.RadioButton(doc);
			//action = new actionClasses[i](this);
			var action = this._getActionOfComp(BNS.molDisplayType + displayType, true, actionClasses[i]);
			//this.getActions().add(action);
			btn.setAction(action);
			result.append(btn, displayType === this.getCurrMoleculeDisplayType());
		}
		return result;
	},

	/* @private */
	_getUsableMolDisplayActionClasses: function(allowedTypes)
	{
		var result = [];
		var actionClasses = (this.getRenderType() === Kekule.Render.RendererType.R3D)?
			CW.Viewer.molDisplayType3DActionClasses: CW.Viewer.molDisplayType2DActionClasses;
		for (var i = 0, l = actionClasses.length; i < l; ++i)
		{
			var displayType = actionClasses[i].TYPE;
			if (allowedTypes && (allowedTypes.indexOf(displayType) < 0) && (displayType !== this.getCurrMoleculeDisplayType()))  // not in allowed type
				continue;
			result.push(actionClasses[i]);
		}
		return result;
	},

	// abount menu
	/** @private */
	getDefaultMenuItems: function()
	{
		return Kekule.globalOptions.chemWidget.viewer.menuItems;
		/*
		var sSeparator = Kekule.Widget.MenuItem.SEPARATOR_TEXT;
		var items = [
			BNS.loadData,
			BNS.saveData,
			sSeparator,
			BNS.molDisplayType,
			BNS.molHideHydrogens,
			BNS.zoomIn, BNS.zoomOut
		];
		// rotate
		items.push({
			'text': Kekule.$L('ChemWidgetTexts.CAPTION_ROTATE'),
			'hint': Kekule.$L('ChemWidgetTexts.HINT_ROTATE'),
			'children': [
				BNS.rotateLeft, BNS.rotateRight,
				BNS.rotateX, BNS.rotateY, BNS.rotateZ
			]
		});
		items.push(BNS.reset);
		items.push(sSeparator);
		items.push(BNS.openEditor);
		// config
		items.push(BNS.config);
		return items;
		*/
	},
	/** @private */
	prepareMenuItems: function(itemDefs)
	{
		var items = [];
		var sSeparator = Kekule.Widget.MenuItem.SEPARATOR_TEXT;
		for (var i = 0, l = itemDefs.length; i < l; ++i)
		{
			var itemDef = itemDefs[i];
			if (typeof(itemDef) === 'string')  // not hash, but a predefined comp name or separator
			{
				if (itemDef !== sSeparator)
				{
					var defHash = this.createPredefinedMenuItemDefHash(itemDef);
					if (defHash)
						items.push(defHash);
				}
			}
			else  // hash definition
			{
				var newItem = Object.extend({}, itemDef);
				if (!itemDef.widget && !itemDef.widgetClass)
				{
					newItem.widget = Kekule.Widget.MenuItem;
				}
				if (itemDef.children && itemDef.children.length)
				{
					var childItems = this.prepareMenuItems(itemDef.children);
					newItem.children = childItems;
				}
				items.push(newItem);
			}
		}
		return items;
	},
	/** @private */
	createPredefinedMenuItemDefHash: function(compName)
	{
		if (compName === BNS.molDisplayType)  // in 2D or 3D mode, type differs a lot, need handle separately
		{
			return this.createMolDisplayMenuDefHash();
		}
		var rotateCompNames = [BNS.rotateX, BNS.rotateY, BNS.rotateZ, BNS.rotateLeft, BNS.rotateRight];
		var beginContinuousRepaintingBind = this.beginContinuousRepainting.bind(this);
		var endContinuousRepaintingBind = this.endContinuousRepainting.bind(this);

		var actionClass = this.getCompActionClass(compName);
		var itemClass = Kekule.Widget.MenuItem;

		var result = null;

		var action = this._getActionOfComp(compName, true);
		if (action)
		{
			//console.log('menu item', action.getClassName(), action.getDisplayer? action.getDisplayer().getElement().id: '-', this.getElement().id);
			result = {
				'widget': itemClass,
				'action': action
			};
			if (rotateCompNames.indexOf(compName) >= 0)
			{
				result = Object.extend(result, {
					'periodicalExecInterval': 20,
					'enablePeriodicalExec': true,
					'#activate': beginContinuousRepaintingBind,
					'@deactivate': endContinuousRepaintingBind
				});
			}
		}
		return result;
	},
	/** @private */
	createMolDisplayMenuDefHash: function()
	{
		var action = this._getActionOfComp(BNS.molDisplayType, true);
		var result = {
			'widget': Kekule.Widget.MenuItem,
			'action': action
		};

		var children = [];
		var allowedType = this.getAllowedMolDisplayTypes();
		var actionClasses = this._getUsableMolDisplayActionClasses(allowedType);

		for (var i = 0, l = actionClasses.length; i < l; ++i)
		{
			var displayType = actionClasses[i].TYPE;
			var action = this._getActionOfComp(BNS.molDisplayType + displayType, true, actionClasses[i]);
			children.push({
				'widget': Kekule.Widget.MenuItem,
				'action': action
			});
		}
		if (children.length)
		{
			result.children = [{
				'widget': Kekule.Widget.PopupMenu,
				'children': children
			}];
		}

		return result;
	},
	/** @private */
	createMenu: function()
	{
		var result = this.getMenu();
		if (!result)
		{
			result = new Kekule.Widget.PopupMenu(this);
			result.addClassName(CNS.DYN_CREATED);
			result.setDisplayed(false);  // hide at first;
		}
		else  // clear old first
		{
			result.clearMenuItems();
		}
		var items = this.getMenuItems() || this.getDefaultMenuItems();
		//console.log('create menu', this.getId(), items);
		items = this.prepareMenuItems(items);
		/*
		for (var i = 0, l = items.length; i < l; ++i)
		{
			var menuItem = Kekule.Widget.Utils.createFromHash(result, items[i]);
			result.appendMenuItem(menuItem);
		}
		*/
		result.createChildrenByDefs(items);
		this.setMenu(result);
		//this.updateActions();
		return result;
	},

	/** @private */
	autoDetectCaption: function()
	{
		if (this.getAutoCaption())
		{
			var obj = this.getChemObj();
			if (obj)
			{
				var info = obj && obj.getInfo();
				var srcInfo = obj && obj.getSrcInfo();
				if (info && srcInfo)
				{
					var caption = info.title || info.caption || obj.getName() || srcInfo.fileName;
					if (caption)
						this.setCaption(caption);
				}
			}
		}
	}
});

var XEvent = Kekule.X.Event;

/**
 * Basic Interaction controller for general viewers, can do zoomIn/out job.
 * @class
 * @augments Kekule.Widget.InteractionController
 *
 * @param {Kekule.ChemWidget.Viewer} viewer Viewer of current object being installed to.
 */
Kekule.ChemWidget.ViewerBasicInteractionController = Class.create(Kekule.Widget.InteractionController,
/** @lends Kekule.ChemWidget.ViewerBasicInteractionController# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ViewerBasicInteractionController',
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer);
		this._enableMouseRotate = true;  // private
		this._transformInfo = {
			'isTransforming': false,
			//'isRotating': false,
			'lastCoord': null,
			'lastZoom': null
		};
		this._restraintCoord = null;
		this._doInteractiveTransformStepBind = this._doInteractiveTransformStep.bind(this);
		/*
		this._zoomInfo = {
			'isTransforming': false,
			'lastZoom': null
		}
		*/
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('viewer', {'dataType': 'Kekule.ChemWidget.Viewer', 'serializable': false,
			'getter': function() { return this.getWidget(); }, 'setter': function(value) { this.setWidget(value); } });
	},
	/** @private */
	getViewerRenderType: function()
	{
		return this.getViewer().getRenderType();
	},
	/** @private */
	getEnableInteraction: function()
	{
		var v = this.getViewer();
		var result = !!(v && v.getEnableDirectInteraction() && v.getChemObjLoaded());
		//console.log('enabledInteraction', result);
		return result;
	},
	/** @private */
	getEnableTouchInteraction: function()
	{
		var v = this.getViewer();
		var result = !!(v && v.getEnableDirectInteraction() && v.getEnableTouchInteraction() && v.getChemObjLoaded());
		return result;
	},
	/** @private */
	_initTransform: function()
	{
		var viewer = this.getViewer();
		var w = viewer.getOffsetWidth();
		var h = viewer.getOffsetHeight();
		var refLength;
		var info = this._transformInfo;
		var rc = this._restraintCoord;
		if (rc)
		{
			refLength = (rc === 'x')? h: w;
			info.angleRatio = 1 / refLength * Math.PI * 2;
		}
		else
		{
			refLength = Math.min(w, h);
			info.angleRatio = 1 / refLength * Math.PI;
		}
		//info.lastRotateXYZ = {'x': 0, 'y': 0, 'z': 0};
	},
	/** @private */
	zoomViewer: function(delta)
	{
		var v = this.getViewer();
		if (!v || !v.getChemObj())
			return;
		if (delta > 0)
		{
			if (v.zoomIn)
				v.zoomIn(delta);
		}
		else if (delta < 0)
		{
			if (v.zoomOut)
				v.zoomOut(-delta);
		}
	},
	/** @private */
	isEventFromInteractionArea: function(e)
	{
		var target = e.getTarget();
		var interactionElem = this.getViewer().getInteractionReceiverElem();
		return (target === interactionElem) || Kekule.DomUtils.isDescendantOf(target, interactionElem);
	},
	/** @private */
	_beginInteractTransformAtCoord: function(screenX, screenY, clientX, clientY)
	{
		var viewer = this.getViewer();
		if (viewer && viewer.getChemObj())
		{
			//if (viewer.getRenderType() === Kekule.Render.RendererType.R3D)
			{
				var info = this._transformInfo;
				info.isTransforming = true;
				info.lastCoord = {'x': screenX, 'y': screenY};

				/*
				 var minLength = Math.min(viewer.getOffsetWidth(), viewer.getOffsetHeight());
				 info.angleRatio = 1 / minLength * Math.PI;
				 */
				this._restraintCoord = this._calcRestraintRotateCoord(clientX, clientY);

				this._initTransform();
				this.getViewer().setTouchAction('none');

				this._requestInteractiveTransform(screenX, screenY);
			}
		}
	},
	/** @private */
	_endInteractTransform: function()
	{
		this._transformInfo.isTransforming = false;
		this.getViewer().setTouchAction(null);
		this._doInteractiveTransformEnd();
	},
	/** @private */
	_calcRestraintRotateCoord: function(clientX, clientY)
	{
		var viewer = this.getViewer();
		var result = null;
		// check if ned restraint rotate
		var restraintRotateEdgeSize = this._getRestraintRotate3DEdgeSize();
		if (restraintRotateEdgeSize > 0)
		{
			var elem = viewer.getInteractionReceiverElem();
			//var rect = Kekule.HtmlElementUtils.getElemBoundingClientRect(elem, false);
			var rect = Kekule.HtmlElementUtils.getElemPageRect(elem, true);
			var x1 = clientX - rect.left;
			var y1 = clientY - rect.top;
			var x2 = rect.right - clientX; //rect.right - screenX;
			var y2 = rect.bottom - clientY; //rect.bottom - screenY;
			var minX, minY, flagX, flagY;
			if (x1 <= x2)
			{
				flagX = 1;
				minX = x1;
			}
			else
			{
				flagX = -1;
				minX = x2;
			}
			if (y1 <= y2)
			{
				flagY = 1;
				minY = y1;
			}
			else
			{
				flagY = -1;
				minY = y2;
			}
			var minOffset = Math.min(minX, minY);
			if (minOffset > restraintRotateEdgeSize)  // no restraint
				return null;
			else  // calc restraint coord
			{
				if (minY > minOffset)  // more near to left or right edge
				{
					if (flagX < 0)  // on right edge
						result = 'x';
				}
				else  // more near to top/bottom edge
				{
					if (flagY > 0)  // on top edge, rotate on z axis
						result = 'z';
					else  // on bottom edge
						result = 'y';
				}
			}
		}
		//console.log('rotate restraint coord', result);
		return result;
	},
	/** @private */
	_interactTransformAtCoord: function(screenX, screenY)
	{
		var lastCoord = this._transformInfo.lastCoord;
		if (lastCoord)
		{
			var currCoord = {'x': screenX, 'y': screenY};
			var distance = Kekule.CoordUtils.getDistance(lastCoord, currCoord);
			if (distance < 5)  // moves too little to react
				return;
		}
		this._requestInteractiveTransform(screenX, screenY);
	},
	/** @private */
	_requestInteractiveTransform: function(screenX, screenY)
	{
		/*
		if (!this._transformInfo)
		{
			this._transformInfo = {};
		}
		*/
		this._transformInfo.interactScreenCoord = {x: screenX, y: screenY};

		if (!this._interactiveTransformStepId)
			this._interactiveTransformStepId = window.requestAnimationFrame(this._doInteractiveTransformStepBind);
	},
	/** @private */
	_doInteractiveTransformStep: function()
	{
		if (this._transformInfo && this._transformInfo.isTransforming)
		{
			var screenCoord = this._transformInfo.interactScreenCoord;
			if (this.getViewerRenderType() === Kekule.Render.RendererType.R3D)
				this.rotateByXYDistance(screenCoord.x, screenCoord.y);
			else
				this.moveByXYDistance(screenCoord.x, screenCoord.y);

			this._interactiveTransformStepId = window.requestAnimationFrame(this._doInteractiveTransformStepBind);
		}
	},
	/** @private */
	_doInteractiveTransformEnd: function()
	{
		if (this._interactiveTransformStepId)
		{
			window.cancelAnimationFrame(this._interactiveTransformStepId);
			this._interactiveTransformStepId = null;
		}
	},
	/** @private */
	_getRestraintRotate3DEdgeSize: function()
	{
		var viewer = this.getViewer();
		if (viewer.getEnableRestraintRotation3D() && viewer.getRenderType() === Kekule.Render.RendererType.R3D)
		{
			var dim = viewer.getDimension();
			var length = Math.min(dim.width, dim.height);
			return length * (viewer.getRestraintRotation3DEdgeRatio() || 0);
		}
		else
			return 0;
	},
	/** @private */
	needReactEvent: function(e)
	{
		return this.getEnableInteraction() && this.isEventFromInteractionArea(e);
	},
	/* @private */
	/*
	needReactToTouchEvent: function(e)
	{
		var touches = e.getTouches();
		return this.getEnableTouchInteraction() && touches && touches.length > 1;
	},
	*/
	/** @private */
	react_dblclick: function(e)
	{
		if (this.needReactEvent(e))
		{
			this.getViewer().resetDisplay();
		}
	},
	/** @private */
	react_mousewheel: function(e)
	{
		if (this.needReactEvent(e))
		{
			var delta = e.wheelDeltaY || e.wheelDelta;
			if (delta)
				delta /= 120;
			this.zoomViewer(delta);
			e.preventDefault();
			return true;
		}
	},
	/** @private */
	react_pointerdown: function(e)
	{
		if (!this.needReactEvent(e))
			return;
		if (e.getButton() === XEvent.MouseButton.LEFT)
		{
			if (e.getPointerType() !== XEvent.PointerType.TOUCH || this.getEnableTouchInteraction())
			{
				// start mouse drag rotation in 3D render mode
				this._beginInteractTransformAtCoord(e.getScreenX(), e.getScreenY(), e.getClientX(), e.getClientY());
			}
		}
	},
	/** @private */
	react_pointerhold: function(e)
	{
		if (!this.needReactEvent(e))
			return;
		if (e.getPointerType() === XEvent.PointerType.TOUCH && e.getButton() === XEvent.MouseButton.LEFT)
		{
			this.getViewer().setEnableTouchInteraction(!this.getViewer().getEnableTouchInteraction());
			/*
			if (!this._transformInfo.isTransforming)
			{
				// start mouse drag rotation in 3D render mode
				this._beginInteractTransformAtCoord(e.getScreenX(), e.getScreenY(), e.getClientX(), e.getClientY());
				e.preventDefault();
			}
      */
		}
	},
	/** @private */
	/*
	react_touchstart: function(e)
	{
		if (!this.needReactEvent(e) || !this.needReactToTouchEvent(e))
			return;
		var touchInfo = e.getTouches()[0];
		var notUnset = Kekule.ObjUtils.notUnset;
		if (touchInfo && notUnset(touchInfo.screenX) && notUnset(touchInfo.screenY))
		{
			this._beginInteractTransformAtCoord(touchInfo.screenX, touchInfo.screenY, touchInfo.clientX, touchInfo.clientY);
			e.stopPropagation();
			e.preventDefault();
		}
	},
	*/
	/** @private */
	react_pointerleave: function(e)
	{
		//this._transformInfo.isTransforming = false;
		this._endInteractTransform();
	},
	/** @private */
	/*
	react_touchleave: function(e)
	{
		this._transformInfo.isTransforming = false;
	},
	*/
	/** @private */
	/*
	react_touchcancel: function(e)
	{
		this._transformInfo.isTransforming = false;
	},
	*/
	/** @private */
	react_pointerup: function(e)
	{
		if (e.getButton() === XEvent.MouseButton.LEFT)
		{
			//this._transformInfo.isTransforming = false;
			this._endInteractTransform();
		}
	},
	/** @private */
	/*
	react_touchend: function(e)
	{
		this._transformInfo.isTransforming = false;
	},
	*/
	/** @private */
	react_pointermove: function(e)
	{
		if (!this.needReactEvent(e))
			return;

		/*
		if (this.getViewerRenderType() === Kekule.Render.RendererType.R3D)
			this.rotateByXYDistance(e.getScreenX(), e.getScreenY());
		else
			this.moveByXYDistance(e.getScreenX(), e.getScreenY());
		*/
		if (this._transformInfo.isTransforming)
		{
			this._interactTransformAtCoord(e.getScreenX(), e.getScreenY());
			e.preventDefault();
		}
	},
	/** @private */
	/*
	react_touchmove: function(e)
	{
		if (!this.needReactEvent(e) || !this.needReactToTouchEvent(e))
			return;
		//console.log(e.touches);
		var touchInfo = e.getTouches()[0];
		var notUnset = Kekule.ObjUtils.notUnset;
		if (touchInfo && notUnset(touchInfo.screenX) && notUnset(touchInfo.screenY))
		{
			this._interactTransformAtCoord(touchInfo.screenX, touchInfo.screenY);
			e.stopPropagation();
			e.preventDefault();
		}
	},
	*/
	/** @private */
	moveByXYDistance: function(currX, currY)
	{
		var info = this._transformInfo;
		if (info && info.isTransforming && (!info.calculating))
		{
			var viewer = this.getViewer();
			if (viewer.getRenderType() === Kekule.Render.RendererType.R3D)
				return;
			info.calculating = true;
			try
			{
				var currCoord = {'x': currX, 'y': currY};
				var delta = Kekule.CoordUtils.substract(currCoord, info.lastCoord);
				var baseCoordOffset = viewer.getBaseCoordOffset() || {};
				baseCoordOffset = Kekule.CoordUtils.add(baseCoordOffset, delta);
				viewer.setBaseCoordOffset(baseCoordOffset);
				info.lastCoord = currCoord;
			}
			finally
			{
				info.calculating = false;
			}
		}
	},
	/** @private */
	rotateByXYDistance: function(currX, currY)
	{
		var info = this._transformInfo;
		if (info && info.isTransforming && (!info.calculating))
		{
			var viewer = this.getViewer();
			if (viewer.getRenderType() !== Kekule.Render.RendererType.R3D)
				return;
			info.calculating = true;
			try
			{
				var currCoord = {'x': currX, 'y': currY};
				var delta = Kekule.CoordUtils.substract(currCoord, info.lastCoord);
				delta.y = -delta.y;

				var dis, rotateAngle, axisVector;
				if (this._restraintCoord)  // restraint rotation on one axis
				{
					var rc = this._restraintCoord;

					if (rc === 'x')
					{
						dis = delta.y;
						axisVector = {'x': 1, 'y': 0, 'z': 0};
					}
					else
					{
						dis = delta.x;
						if (rc === 'y')
							axisVector = {'x': 0, 'y': -1, 'z': 0};
						else
							axisVector = {'x': 0, 'y': 0, 'z': 1};
					}
					rotateAngle = -dis * info.angleRatio;
				}
				else  // normal rotation
				{
					dis = Kekule.CoordUtils.getDistance({'x': 0, 'y': 0}, delta);
					rotateAngle = dis * info.angleRatio;
					axisVector = {'x': -delta.y, 'y': delta.x, 'z': 0};
				}

				viewer.rotate3DByAxis(rotateAngle, axisVector);

				info.lastCoord = currCoord;
				info.calculating = false;
			}
			catch(e) {}   // fix IE finally bug
			finally
			{
				info.calculating = false;
			}
		}
	},

	/* @private */
	/*
	react_transformstart: function(e)
	{
		if (!this.getEnableTouchInteraction() || !this.needReactEvent(e))
			return;
		var viewer = this.getViewer();
		if (viewer)
		{
			var info = this._transformInfo;
			info.isTransforming = true;
			info.lastZoom = viewer.getCurrZoom();
			info.lastCoord = {'x': 0, 'y': 0};
			this._initTransform();
		}
	},
	*/
	/* @private */
	/*
	react_transformend: function(e)
	{
		if (!this.getEnableTouchInteraction() || !this.needReactEvent(e))
			return;
		this._transformInfo.isTransforming = false;
	},
	*/
	/* @private */
	/*
	react_transform: function(e)
	{
		if (!this.getEnableTouchInteraction() || !this.needReactEvent(e))
			return;
		var viewer = this.getViewer();
		var info = this._transformInfo;
		if (viewer && info.isTransforming && (!info.calculating))
		{
			try
			{
				// TODO: the event detail data is binded to hammer.js, may need to change later
				var gesture = e.gesture;
				if (gesture)
				{
					// zoom

					var scale = e.gesture.scale;
					viewer.zoomTo((info.lastZoom || 1) * scale, true);  // suspend render, as we will rotate further

					//console.log('new scale', scale);


					// rotate
					var dx = gesture.deltaX;
					var dy = gesture.deltaY;

					//console.log(dx, dy, info.lastCoord.x, info.lastCoord.y);
					this.rotateByXYDistance(dx, dy);

					e.gesture.preventDefault();
					e.gesture.stopPropagation();
				}
			}
			finally
			{
				//info.calculating = false;
			}
		}
		//console.log(e, e.gesture);
	},
  */

	/** @private */
	doTestMouseCursor: function(coord, e)
	{
		var result = '';
		var info = this._transformInfo;
		if (info.isTransforming)
			result = 'move';  //'grabbing';
		return result;
	}
});

/**
 * An 2D viewer widget for chem objects, actually a specialization of {@link Kekule.ChemWidget.Viewer}.
 * @class
 * @augments Kekule.ChemWidget.Viewer
 *
 * @param {Variant} parentOrElementOrDocument
 * @param {Kekule.ChemObject} chemObj
 */
Kekule.ChemWidget.Viewer2D = Class.create(Kekule.ChemWidget.Viewer,
/** @lends Kekule.ChemWidget.Viewer2D# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.Viewer2D',
	/** @construct */
	initialize: function($super, parentOrElementOrDocument, chemObj)
	{
		$super(parentOrElementOrDocument, chemObj, Kekule.Render.RendererType.R2D);
	},
	/** @ignore */
	getAllowRenderTypeChange: function()
	{
		return false;
	}
});

/**
 * An 3D viewer widget for chem objects, actually a specialization of {@link Kekule.ChemWidget.Viewer}.
 * @class
 * @augments Kekule.ChemWidget.Viewer
 *
 * @param {Variant} parentOrElementOrDocument
 * @param {Kekule.ChemObject} chemObj
 */
Kekule.ChemWidget.Viewer3D = Class.create(Kekule.ChemWidget.Viewer,
/** @lends Kekule.ChemWidget.Viewer3D# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.Viewer3D',
	/** @construct */
	initialize: function($super, parentOrElementOrDocument, chemObj)
	{
		$super(parentOrElementOrDocument, chemObj, Kekule.Render.RendererType.R3D);
	},
	/** @ignore */
	getAllowRenderTypeChange: function()
	{
		return false;
	}
});

// register predefined settings of viewer
var SM = Kekule.ObjPropSettingManager;
SM.register('Kekule.ChemWidget.Viewer.fullFunc', {  // viewer with all functions
	enableToolbar: true,
	enableDirectInteraction: true,
	enableTouchInteraction: true,
	enableEdit: true,
	toolButtons: null   // create all default tool buttons
});
SM.register('Kekule.ChemWidget.Viewer.basic', {  // viewer with basic function, suitable for embedded chem object with limited size
	enableToolbar: true,
	enableDirectInteraction: true,
	enableTouchInteraction: false,
	toolButtons: [BNS.saveData, BNS.molDisplayType, BNS.zoomIn, BNS.zoomOut],
	menuItems: [BNS.saveData, '-', BNS.molDisplayType, BNS.zoomIn, BNS.zoomOut]
});
SM.register('Kekule.ChemWidget.Viewer.mini', {  // viewer with only one menu button
	enableToolbar: true,
	enableDirectInteraction: true,
	enableTouchInteraction: false,
	toolButtons: [BNS.menu],
	menuItems: [BNS.saveData, '-', BNS.molDisplayType, BNS.zoomIn, BNS.zoomOut]
});
SM.register('Kekule.ChemWidget.Viewer.static', {  // viewer with no interaction ability, suitable for static embedded chem object
	enableToolbar: false,
	enableDirectInteraction: false,
	enableTouchInteraction: false,
	toolButtons: [],
	menuItems: []
});
SM.register('Kekule.ChemWidget.Viewer.editOnly', {  // viewer can be editted
	enableToolbar: true,
	enableEdit: true,
	toolButtons: [BNS.openEditor],
	menuItems: [BNS.openEditor]
});

/**
 * A special class to give a setting facade for Chem Viewer.
 * Do not use this class alone.
 * @class
 * @augments Kekule.ChemWidget.ChemObjDisplayer.Settings
 * @ignore
 */
Kekule.ChemWidget.Viewer.Settings = Class.create(Kekule.ChemWidget.ChemObjDisplayer.Settings,
/** @lends Kekule.ChemWidget.Viewer.Settings# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.Viewer.Settings',
	/** @private */
	initProperties: function()
	{
		this.defineDelegatedProps([
			'enableDirectInteraction', 'enableTouchInteraction',
			'enableToolbar', 'toolbarPos', 'toolbarMarginHorizontal', 'toolbarMarginVertical',
			'enableEdit', 'modalEdit'
		]);
	},
	/** @private */
	getViewer: function()
	{
		return this.getWidget();
	}
});

/**
 * Base class for actions for chem viewer.
 * @class
 * @augments Kekule.ChemWidget.ActionOnDisplayer
 *
 * @param {Kekule.ChemWidget.Viewer} viewer Target viewer widget.
 * @param {String} caption
 * @param {String} hint
 */
Kekule.ChemWidget.ActionOnViewer = Class.create(Kekule.ChemWidget.ActionOnDisplayer,
/** @lends Kekule.ChemWidget.ActionOnViewer# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionOnViewer',
	/** @constructs */
	initialize: function($super, viewer, caption, hint)
	{
		$super(viewer, caption, hint);
	},
	/** @private */
	doUpdate: function()
	{
		var displayer = this.getDisplayer();
		this.setEnabled(displayer && displayer.getChemObj() && displayer.getChemObjLoaded() && displayer.getEnabled());
	},
	/**
	 * Returns target chem viewer.
	 * @returns {Kekule.ChemWidget.Viewer}
	 */
	getViewer: function()
	{
		var result = this.getDisplayer();
		return (result instanceof Kekule.ChemWidget.Viewer)? result: null;
	}
});

/**
 * Base action for make rotation in viewer.
 * @class
 * @augments Kekule.ChemWidget.ActionOnViewer
 *
 * @property {Float} delta The rotation angle.
 */
Kekule.ChemWidget.ActionViewerRotateBase = Class.create(Kekule.ChemWidget.ActionOnViewer,
/** @lends Kekule.ChemWidget.ActionViewerRotateBase# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateBase',
	/** @constructs */
	initialize: function($super, viewer, caption, hint)
	{
		$super(viewer, caption, hint);
		this.setDelta(2 * Math.PI / 180);  // TODO: this default value should be configurable
	},
	/** @private */
	initProperties: function()
	{
		this.defineProp('delta', {'dataType': DataType.FLOAT});
	},
	/** @private */
	isShiftModified: function(htmlEvent)
	{
		return htmlEvent && htmlEvent.getShiftKey();
	}
});
/**
 * Base action for make rotation in 2D viewer.
 * @class
 * @augments Kekule.ChemWidget.ActionViewerRotateBase
 *
 * @property {Float} delta The rotation angle.
 */
Kekule.ChemWidget.ActionViewerRotateBase2D = Class.create(Kekule.ChemWidget.ActionViewerRotateBase,
/** @lends Kekule.ChemWidget.ActionViewerRotateBase2D# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateBase2D',
	/** @private */
	doUpdate: function($super)
	{
		$super();
		var viewer = this.getViewer();
		var flag = viewer && (viewer.getRenderType() === Kekule.Render.RendererType.R2D);
		this.setDisplayed(/*this.getDisplayed() &&*/ flag).setEnabled(this.getEnabled() && flag);
	}
});
/**
 * Base action for make rotation in 3D viewer.
 * @class
 * @augments Kekule.ChemWidget.ActionViewerRotateBase
 *
 * @property {Float} delta The rotation angle.
 */
Kekule.ChemWidget.ActionViewerRotateBase3D = Class.create(Kekule.ChemWidget.ActionViewerRotateBase,
/** @lends Kekule.ChemWidget.ActionViewerRotateBase3D# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateBase3D',
	/** @private */
	doUpdate: function($super)
	{
		$super();
		var viewer = this.getViewer();
		var flag = viewer && (viewer.getRenderType() === Kekule.Render.RendererType.R3D);
		this.setDisplayed(/*this.getDisplayed() &&*/ flag).setEnabled(this.getEnabled() && flag);
	}
});

/**
 * Action for do anticlockwise rotation in 2D mode.
 * @class
 * @augments Kekule.ChemWidget.ActionViewerRotateBase2D
 */
Kekule.ChemWidget.ActionViewerRotateLeft = Class.create(Kekule.ChemWidget.ActionViewerRotateBase2D,
/** @lends Kekule.ChemWidget.ActionViewerRotateLeft# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateLeft',
	/** @private */
	HTML_CLASSNAME: CCNS.ACTION_ROTATE_LEFT,
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer, /*CWT.CAPTION_ROTATELEFT, CWT.HINT_ROTATELEFT*/Kekule.$L('ChemWidgetTexts.CAPTION_ROTATELEFT'), Kekule.$L('ChemWidgetTexts.HINT_ROTATELEFT'));
	},
	/** @private */
	doExecute: function(target, htmlEvent)
	{
		var delta = this.getDelta();
		this.getViewer().rotate2DBy(delta);
	}
});
/**
 * Action for do clockwise rotation in 2D mode.
 * @class
 * @augments Kekule.ChemWidget.ActionViewerRotateBase2D
 */
Kekule.ChemWidget.ActionViewerRotateRight = Class.create(Kekule.ChemWidget.ActionViewerRotateBase2D,
/** @lends Kekule.ChemWidget.ActionViewerRotateRight# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateRight',
	/** @private */
	HTML_CLASSNAME: CCNS.ACTION_ROTATE_RIGHT,
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer, /*CWT.CAPTION_ROTATERIGHT, CWT.HINT_ROTATERIGHT*/Kekule.$L('ChemWidgetTexts.CAPTION_ROTATERIGHT'), Kekule.$L('ChemWidgetTexts.HINT_ROTATERIGHT'));
	},
	/** @private */
	doExecute: function(target, htmlEvent)
	{
		var delta = -this.getDelta();
		this.getViewer().rotate2DBy(delta);
	}
});
/**
 * Action for do rotation around X axis in 3D mode.
 * @class
 * @augments Kekule.ChemWidget.ActionViewerRotateBase3D
 */
Kekule.ChemWidget.ActionViewerRotateX = Class.create(Kekule.ChemWidget.ActionViewerRotateBase3D,
/** @lends Kekule.ChemWidget.ActionViewerRotateX# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateX',
	/** @private */
	HTML_CLASSNAME: CCNS.ACTION_ROTATE_X,
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer, /*CWT.CAPTION_ROTATEX, CWT.HINT_ROTATEX*/Kekule.$L('ChemWidgetTexts.CAPTION_ROTATEX'), Kekule.$L('ChemWidgetTexts.HINT_ROTATEX'));
	},
	/** @private */
	doExecute: function(target, htmlEvent)
	{
		var rev = this.isShiftModified(htmlEvent);
		var delta = -this.getDelta();
		if (rev)
			delta = -delta;
		this.getViewer().rotate3DBy(delta, 0, 0);
	}
});
/**
 * Action for do rotation around Y axis in 3D mode.
 * @class
 * @augments Kekule.ChemWidget.ActionViewerRotateBase3D
 */
Kekule.ChemWidget.ActionViewerRotateY = Class.create(Kekule.ChemWidget.ActionViewerRotateBase3D,
/** @lends Kekule.ChemWidget.ActionViewerRotateY# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateY',
	/** @private */
	HTML_CLASSNAME: CCNS.ACTION_ROTATE_Y,
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer, /*CWT.CAPTION_ROTATEY, CWT.HINT_ROTATEY*/Kekule.$L('ChemWidgetTexts.CAPTION_ROTATEY'), Kekule.$L('ChemWidgetTexts.HINT_ROTATEY'));
	},
	/** @private */
	doExecute: function(target, htmlEvent)
	{
		var rev = this.isShiftModified(htmlEvent);
		var delta = -this.getDelta();
		if (rev)
			delta = -delta;
		this.getViewer().rotate3DBy(0, delta, 0);
	}
});
/**
 * Action for do rotation around Z axis in 3D mode.
 * @class
 * @augments Kekule.ChemWidget.ActionViewerRotateBase3D
 */
Kekule.ChemWidget.ActionViewerRotateZ = Class.create(Kekule.ChemWidget.ActionViewerRotateBase3D,
/** @lends Kekule.ChemWidget.ActionViewerRotateZ# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerRotateZ',
	/** @private */
	HTML_CLASSNAME: CCNS.ACTION_ROTATE_Z,
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer, /*CWT.CAPTION_ROTATEZ, CWT.HINT_ROTATEZ*/Kekule.$L('ChemWidgetTexts.CAPTION_ROTATEZ'), Kekule.$L('ChemWidgetTexts.HINT_ROTATEZ'));
	},
	/** @private */
	doExecute: function(target, htmlEvent)
	{
		var rev = this.isShiftModified(htmlEvent);
		var delta = -this.getDelta();
		if (rev)
			delta = -delta;
		this.getViewer().rotate3DBy(0, 0, delta);
	}
});

/**
 * Action used for molecule display type stub button of compact button set in viewer.
 * @class
 * @augments Kekule.ChemWidget.ActionViewer
 */
Kekule.ChemWidget.ActionViewerChangeMolDisplayTypeStub = Class.create(Kekule.ChemWidget.ActionOnDisplayer,
/** @lends Kekule.ChemWidget.ActionViewerChangeMolDisplayTypeStub# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerChangeMolDisplayTypeStub',
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer, /*CWT.CAPTION_MOL_DISPLAY_TYPE, CWT.HINT_MOL_DISPLAY_TYPE*/Kekule.$L('ChemWidgetTexts.CAPTION_MOL_DISPLAY_TYPE'), Kekule.$L('ChemWidgetTexts.HINT_MOL_DISPLAY_TYPE'));
	},
	/** @private */
	doExecute: function(target)
	{
		// do nothing
	}
});

/**
 * Action to edit object in viewer.
 * @class
 * @augments Kekule.ChemWidget.ActionOnViewer
 *
 * @property {Float} delta The rotation angle.
 */
Kekule.ChemWidget.ActionViewerEdit = Class.create(Kekule.ChemWidget.ActionOnViewer,
/** @lends Kekule.ChemWidget.ActionViewerEdit# */
{
	/** @private */
	CLASS_NAME: 'Kekule.ChemWidget.ActionViewerEdit',
	/** @private */
	HTML_CLASSNAME: CCNS.ACTION_VIEWER_EDIT,
	/** @constructs */
	initialize: function($super, viewer)
	{
		$super(viewer, /*CWT.CAPTION_OPENEDITOR, CWT.HINT_OPENEDITOR*/Kekule.$L('ChemWidgetTexts.CAPTION_OPENEDITOR'), Kekule.$L('ChemWidgetTexts.HINT_OPENEDITOR'));
	},
	/** @private */
	doUpdate: function($super)
	{
		$super();
		var viewer = this.getViewer();
		//this.setEnabled(this.getEnabled() && viewer.getChemObj() && viewer.getEnableEdit());
		//this.setEnabled(this.getEnabled() && viewer.getAllowEditing());
		this.setEnabled(viewer && viewer.getAllowEditing() && viewer.getEnabled());
		this.setDisplayed(viewer && viewer.getEnableEdit());
	},
	/** @private */
	doExecute: function(target)
	{
		var viewer = this.getViewer();
		viewer.openEditor(target);
	}
});

/** @private */
Kekule.ChemWidget.Viewer.rotate2DActionClasses = [
	CW.ActionViewerRotateLeft,
	CW.ActionViewerRotateRight
];
/** @private */
Kekule.ChemWidget.Viewer.rotate3DActionClasses = [
	CW.ActionViewerRotateX,
	CW.ActionViewerRotateY,
	CW.ActionViewerRotateZ
];
/** @private */
Kekule.ChemWidget.Viewer.molDisplayType2DActionClasses = [
	CW.ActionDisplayerChangeMolDisplayTypeSkeletal,
	CW.ActionDisplayerChangeMolDisplayTypeCondensed
];
/** @private */
Kekule.ChemWidget.Viewer.molDisplayType3DActionClasses = [
	CW.ActionDisplayerChangeMolDisplayTypeWire,
	CW.ActionDisplayerChangeMolDisplayTypeSticks,
	CW.ActionDisplayerChangeMolDisplayTypeBallStick,
	CW.ActionDisplayerChangeMolDisplayTypeSpaceFill
];

// register actions to viewer widget
Kekule._registerAfterLoadSysProc(function(){
	var AM = Kekule.ActionManager;
	var CW = Kekule.ChemWidget;
	var widgetClass = Kekule.ChemWidget.Viewer;
	var reg = AM.registerNamedActionClass;

	reg(BNS.loadFile, CW.ActionDisplayerLoadFile, widgetClass);
	reg(BNS.loadData, CW.ActionDisplayerLoadData, widgetClass);
	reg(BNS.saveData, CW.ActionDisplayerSaveFile, widgetClass);
	reg(BNS.clearObjs, CW.ActionDisplayerClear, widgetClass);
	reg(BNS.zoomIn, CW.ActionDisplayerZoomIn, widgetClass);
	reg(BNS.zoomOut, CW.ActionDisplayerZoomOut, widgetClass);
	reg(BNS.rotateLeft, CW.ActionViewerRotateLeft, widgetClass);
	reg(BNS.rotateRight, CW.ActionViewerRotateRight, widgetClass);
	reg(BNS.rotateX, CW.ActionViewerRotateX, widgetClass);
	reg(BNS.rotateY, CW.ActionViewerRotateY, widgetClass);
	reg(BNS.rotateZ, CW.ActionViewerRotateZ, widgetClass);
	reg(BNS.reset, CW.ActionDisplayerReset, widgetClass);
	reg(BNS.molHideHydrogens, CW.ActionDisplayerHideHydrogens, widgetClass);
	reg(BNS.molDisplayType, CW.ActionViewerChangeMolDisplayTypeStub, widgetClass);
	reg(BNS.openEditor, CW.ActionViewerEdit, widgetClass);
	reg(BNS.config, Kekule.Widget.ActionOpenConfigWidget, widgetClass);

	reg(BNS.molDisplayTypeCondensed, CW.ActionDisplayerChangeMolDisplayTypeCondensed, widgetClass);
	reg(BNS.molDisplayTypeSkeletal, CW.ActionDisplayerChangeMolDisplayTypeSkeletal, widgetClass);
	reg(BNS.molDisplayTypeWire, CW.ActionDisplayerChangeMolDisplayTypeWire, widgetClass);
	reg(BNS.molDisplayTypeSticks, CW.ActionDisplayerChangeMolDisplayTypeSticks, widgetClass);
	reg(BNS.molDisplayTypeBallStick, CW.ActionDisplayerChangeMolDisplayTypeBallStick, widgetClass);
	reg(BNS.molDisplayTypeSpaceFill, CW.ActionDisplayerChangeMolDisplayTypeSpaceFill, widgetClass);
});

})();