/**
* @fileoverview
* Editor for Kekule.ChemSpace.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /core/kekule.common.js
* requires /render/kekule.render.base.js
* requires /render/kekule.render.extensions.js
* requires /render/kekule.render.kekule.render.utils.js
* requires /widgets/operation/kekule.operations.js
* requires /widgets/commonCtrls/kekule.widget.formControls.js
* requires /widgets/commonCtrls/kekule.widget.dialogs.js
* requires /widgets/chem/periodicTable/kekule.chemWidget.periodicTables.js
* requires /widgets/chem/uiMarker/kekule.chemWidget.uiMarkers.js
* requires /widgets/chem/editor/kekule.chemEditor.extensions.js
* requires /widgets/chem/editor/kekule.chemEditor.editorUtils.js
* requires /widgets/chem/editor/kekule.chemEditor.configs.js
* requires /widgets/chem/editor/kekule.chemEditor.operations.js
* requires /widgets/chem/editor/kekule.chemEditor.baseEditors.js
* requires /widgets/chem/editor/kekule.chemEditor.repositories.js
* requires /widgets/chem/editor/kekule.chemEditor.utilWidgets.js
*/
(function(){
"use strict";
var AU = Kekule.ArrayUtils;
var CU = Kekule.CoordUtils;
var CCNS = Kekule.ChemWidget.HtmlClassNames;
//var CWT = Kekule.ChemWidgetTexts;
/** @ignore */
Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, {
CHEMSPACE_EDITOR: 'K-Chem-Space-Editor',
CHEMSPACE_EDITOR2D: 'K-Chem-Space-Editor2D',
CHEMSPACE_EDITOR3D: 'K-Chem-Space-Editor3D',
CHEMEDITOR_ATOM_SETTER: 'K-ChemEditor-Atom-Setter',
CHEMEDITOR_TEXT_SETTER: 'K-ChemEditor-Text-Setter',
CHEMEDITOR_FORMULA_SETTER: 'K-ChemEditor-Formula-Setter'
});
/**
* A chem editor to edit chemspace object and other chem objects.
* When load a chem object other than instance of Kekule.ChemSpace, an empty ChemSpace instance will
* be created and loaded object should be insert into it.
* @class
* @augments Kekule.Editor.BaseEditor
* @param {Variant} parentOrElementOrDocument
* @param {Kekule.ChemObject} chemObj initially loaded chemObj.
* @param {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}.
* @param {Kekule.Editor.BaseEditorConfigs} editorConfigs Configuration of this editor.
*
* @property {Kekule.ChemSpace} chemSpace ChemSpace loaded in this editor.
* @property {Float} defBondLength
* @property {Bool} allowCreateNewChild Whether new direct child of space can be created.
* Note: if the space is empty, one new child will always be allowed to create.
* @property {Bool} autoCreateNewStructFragment Whether new molecule object can be created in space.
* Note: if property {@link Kekule.Editor.ChemSpaceEditor.allowCreateNewChild} is false, this property
* will always be false.
*/
Kekule.Editor.ChemSpaceEditor = Class.create(Kekule.Editor.BaseEditor,
/** @lends Kekule.Editor.ChemSpaceEditor# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.ChemSpaceEditor',
/** @constructs */
initialize: function($super, parentOrElementOrDocument, chemObj, renderType, editorConfigs)
{
this.setPropStoreFieldValue('allowCreateNewChild', true);
this.setPropStoreFieldValue('autoCreateNewStructFragment', true);
$super(parentOrElementOrDocument, chemObj, renderType, editorConfigs);
this._containerChemSpace = null; // private field, used to mark that a extra chem space container is used
},
/** @private */
initProperties: function()
{
this.defineProp('chemSpace', {'dataType': 'Kekule.ChemSpace', 'serializable': false,
'getter': function() { return this.getChemObj(); },
'setter': function(value) { this.setChemObj(value); }
});
this.defineProp('autoCreateNewStructFragment', {'dataType': DataType.BOOL,
'getter': function()
{
return this.getPropStoreFieldValue('autoCreateNewStructFragment') && this.canCreateNewChild();
}
});
this.defineProp('allowCreateNewChild', {'dataType': DataType.BOOL});
},
/**
* Returns whether new direct child can be created in current space.
* This method will returns true of property {@link Kekule.Editor.ChemSpaceEditor.allowCreateNewChild} is true
* or the space is empty.
* @returns {Bool}
*/
canCreateNewChild: function()
{
return this.getChemSpace() && (this.getAllowCreateNewChild() || (this.getChemSpace().getChildCount() <= 0));
},
/** @ignore */
getActualDrawOptions: function($super)
{
var result = $super();
// a special field to ensure use explict size of space
// rather than calc size based on child objects
result.useExplicitSpaceSize = true;
return result;
},
/** @private */
doSetChemObj: function($super, value)
{
var old = this.getChemObj();
if (old !== value)
{
if (old && this._containerChemSpace)
{
old.finalize();
this._containerChemSpace = null;
}
if (value)
{
if (value instanceof Kekule.ChemSpace)
{
this._initChemSpaceDefProps(value);
return $super(value);
}
else
{
var space = this.createContainerChemSpace(null, value);
//this._initChemSpaceDefProps(space, value);
//space.appendChild(value);
this._containerChemSpace = space;
$super(space);
}
}
else
$super(value);
}
else
$super(value);
},
/** @ignore */
getExportableClasses: function($super)
{
var result = $super(); // now result includes chemSpace
// add child objects of chemspace to result
var space = this.getChemSpace();
if (space)
{
for (var i = 0, l = space.getChildCount(); i < l; ++i)
{
var obj = space.getChildAt(i);
if (obj && obj.getClass)
Kekule.ArrayUtils.pushUnique(result, obj.getClass());
}
}
return result;
},
/** @ignore */
exportObjs: function($super, objClass)
{
var result = $super(objClass);
if ((!result || !result.length) && objClass) // check child objects of chemSpace
{
result = [];
var space = this.getChemSpace();
if (space)
{
for (var i = 0, l = space.getChildCount(); i < l; ++i)
{
var obj = space.getChildAt(i);
if (obj && (obj instanceof objClass))
result.push(obj);
}
}
}
return result;
},
/** @ignore */
getSavingTargetObj: function($super)
{
// if only one child in chemspace, save this obj alone (rather than the space).
var space = this.getChemSpace();
var childCount = space.getChildCount();
if (childCount === 1)
{
return space.getChildAt(0);
}
else
return $super();
},
/** @ignore */
createDefaultConfigs: function()
{
//return new Kekule.Editor.ChemSpaceEditorConfigs();
return Kekule.Editor.ChemSpaceEditorConfigs.getInstance();
},
/** @private */
doCreateNewDocObj: function()
{
return this.createContainerChemSpace();
},
/** @private */
doLoad: function($super, chemObj)
{
// supply essential charge and radical markers
this._supplyChemMarkersOnObj(chemObj);
$super(chemObj);
},
/** @private */
doLoadEnd: function($super, chemObj)
{
var result = $super(chemObj);
// calc def bond length
var defBondLength = null;
if (chemObj)
{
var connectors = chemObj.getAllContainingConnectors();
if (connectors && connectors.length)
{
var coordMode = (this.getRenderType() === Kekule.Render.RendererType.R3D)?
Kekule.CoordMode.COORD3D: Kekule.CoordMode.COORD2D;
defBondLength = Kekule.ChemStructureUtils.getConnectorLengthMedian(connectors, coordMode, this.getAllowCoordBorrow());
}
}
this.setDefBondLength(defBondLength);
return result;
},
/** @ignore */
resetDisplay: function($super)
{
// called after loading a new chemObj, or creating a new doc
$super();
// adjust editor size
var space = this.getChemObj();
if (space)
{
//console.log('decide size', space, space.getScreenSize);
var screenSize = space.getScreenSize();
this.changeClientSize(screenSize.x, screenSize.y, this.getCurrZoom());
// scroll to top center
var elem = this.getEditClientElem().parentNode;
var visibleClientSize = Kekule.HtmlElementUtils.getElemClientDimension(elem);
this.scrollClientTo(0, (screenSize.x * this.getCurrZoom() - visibleClientSize.width) / 2);
}
},
/** @ignore */
zoomChanged: function($super, zoomLevel)
{
$super();
var space = this.getChemObj();
if (space)
{
var screenSize = space.getScreenSize();
this.changeClientSize(screenSize.x, screenSize.y, zoomLevel);
}
},
/** @ignore */
createNewBoundInfoRecorder: function($super, renderer)
{
$super(renderer);
var recorder = this.getBoundInfoRecorder();
if (recorder) // add event listener to update text box size
{
recorder.addEventListener('updateBasicDrawObject', this.reactUpdateBasicDrawObject, this);
}
},
/** @private */
reactUpdateBasicDrawObject: function(e)
{
var obj = e.obj;
// TODO: use such a method to set text block size may not be a good approach
if (obj && (obj instanceof Kekule.TextBlock))
{
var boundInfo = e.boundInfo;
this.updateTextBlockSize(obj, boundInfo);
}
},
/* @ignore */
objectChanged: function($super, obj, changedPropNames)
{
/*
if (this.getCoordMode() === Kekule.CoordMode.COORD2D) // only works in 2D mode
{
if (obj instanceof Kekule.TextBlock) // size need to be recalculated
{
//console.log('text box changed', obj.getId());
// must not use setSize2D, otherwise a new object change event will be triggered
//obj.setPropStoreFieldValue('size2D', {'x': null, 'y': null});
obj.__$needRecalcSize__ = true; // special flag, indicating to recalculate size
}
}
*/
return $super(obj, changedPropNames);
},
/** @private */
updateTextBlockSize: function(textBlock, boundInfo)
{
if (this.getCoordMode() !== Kekule.CoordMode.COORD2D) // only works in 2D mode
return;
/*
var oldSize = textBlock.getSize2D();
if (Kekule.ObjUtils.notUnset(oldSize.x) || Kekule.ObjUtils.notUnset(oldSize.y)) // size already set, by pass
return;
*/
//if (!textBlock.__$needRecalcSize__)
if (!textBlock.getNeedRecalcSize())
return;
var stype = boundInfo.shapeType;
if (stype === Kekule.Render.BoundShapeType.RECT)
{
/*
console.log('boundddddd', boundInfo);
var coords = boundInfo.coords; // context coords
var objCoord1 = this.contextCoordToObj(coords[0]);
var objCoord2 = this.contextCoordToObj(coords[1]);
var delta = Kekule.CoordUtils.substract(objCoord2, objCoord1);
// must not use setSize2D, otherwise a new object change event will be triggered and a new update process will be launched
textBlock.setPropStoreFieldValue('size2D', {'x': Math.abs(delta.x), 'y': Math.abs(delta.y)});
//textBlock.setSize2D({'x': Math.abs(delta.x), 'y': Math.abs(delta.y)});
//delete textBlock.__$needRecalcSize__;
textBlock.setNeedRecalcSize(false);
*/
}
},
/** @private */
createContainerChemSpace: function(id, containingChemObj)
{
//var result = new Kekule.ChemSpace(id);
var result = new Kekule.ChemDocument(id);
this._initChemSpaceDefProps(result, containingChemObj);
if (containingChemObj)
{
result.appendChild(containingChemObj);
// adjust child position
var spaceSize = result.getSizeOfMode(this.getCoordMode(), this.getAllowCoordBorrow());
var coord, ratio;
var objBox = Kekule.Render.ObjUtils.getContainerBox(containingChemObj, this.getCoordMode(), this.getAllowCoordBorrow());
if (this.getCoordMode() === Kekule.CoordMode.COORD2D)
{
/*
var clientElem = this.getEditClientElem();
var clientDim = Kekule.HtmlElementUtils.getElemClientDimension(clientElem);
var clientScrollX = clientDim.scrollTop;
var clientScrollY = clientDim.scrollLeft;
var padding = clientDim.height / 2;
*/
var padding = this.getEditorConfigs().getChemSpaceConfigs().getDefPadding();
var ratio = result.getObjScreenLengthRatio();
//console.log('adjust inside obj size', objBox, this.isShown());
}
/*
var oldObjCoord = containingChemObj.getCoordOfMode?
containingChemObj.getCoordOfMode(this.getCoordMode(), this.getAllowCoordBorrow()) || {}:
{};
*/
var oldObjCoord = containingChemObj.getAbsBaseCoord?
containingChemObj.getAbsBaseCoord(this.getCoordMode(), this.getAllowCoordBorrow()) || {}:
{};
if (ratio && objBox) // 2D and calc padding
{
/*
var oldObjCoord = containingChemObj.getCoordOfMode?
containingChemObj.getCoordOfMode(this.getCoordMode()) || {}:
{};
*/
coord = Kekule.CoordUtils.divide(spaceSize, 2);
//coord.y = spaceSize.y - /*(objBox.y2 - objBox.y1) / 2*/objBox.y2 - padding * ratio;
//coord.y = spaceSize.y - Math.abs(objBox.y2 - objBox.y1) / 2 - padding * ratio;
//coord.x -= (objBox.x2 + objBox.x1) / 2;
/*
coord.y = (oldObjCoord.y || 0) + spaceSize.y - padding * ratio - objBox.y2;
coord.x += (oldObjCoord.x || 0) - (objBox.x2 + objBox.x1) / 2;
*/
var objBoxCenter = {x: (objBox.x1 + objBox.x2) / 2, y: (objBox.y1 + objBox.y2) / 2};
var newObjCenter = {x: coord.x, y: spaceSize.y - padding * ratio - (objBox.y2 - objBox.y1) / 2};
var centerDelta = Kekule.CoordUtils.substract(newObjCenter, objBoxCenter);
coord = Kekule.CoordUtils.add(oldObjCoord, centerDelta);
/*
coord.y = spaceSize.y - padding * ratio - (objBox.y2 - objBox.y1) / 2 - objBoxCenter.y;
coord.x = coord.x - (objBox.x2 + objBox.x1) / 2;
*/
//console.log(spaceSize, coord, objBox);
}
else
{
//var oldObjCoord = containingChemObj.getCoordOfMode(this.getCoordMode()) || {};
coord = Kekule.CoordUtils.divide(spaceSize, 2);
/*
coord.x -= (objBox.x2 + objBox.x1) / 2;
coord.y -= (objBox.y2 + objBox.y1) / 2;
if (this.getCoordMode() === Kekule.CoordMode.COORD3D)
coord.z -= (objBox.z2 + objBox.z1) / 2;
coord = Kekule.CoordUtils.add(coord, oldObjCoord);
*/
}
/*
if (containingChemObj.setCoordOfMode)
containingChemObj.setCoordOfMode(coord, this.getCoordMode());
*/
if (containingChemObj.setAbsBaseCoord)
containingChemObj.setAbsBaseCoord(coord, this.getCoordMode());
//this.setObjCoord(containingChemObj, coord, Kekule.Render.CoordPos.CENTER);
}
return result;
},
/** @private */
_initChemSpaceDefProps: function(chemSpace, containingChemObj)
{
var configs = this.getEditorConfigs();
var chemSpaceConfigs = configs.getChemSpaceConfigs();
if (this.getCoordMode() === Kekule.CoordMode.COORD2D) // now only handles 2D size
{
var screenSize = chemSpace.getScreenSize();
if (!screenSize.x && !screenSize.y)
{
screenSize = chemSpaceConfigs.getDefScreenSize2D();
chemSpace.setScreenSize(screenSize);
}
if (!chemSpace.getDefAutoScaleRefLength())
{
var refLength;
if (containingChemObj && containingChemObj.getAllAutoScaleRefLengths)
{
var refLengths = containingChemObj.getAllAutoScaleRefLengths(this.getCoordMode(), this.getAllowCoordBorrow());
refLength = refLengths && refLengths.length? Kekule.ArrayUtils.getMedian(refLengths): null;
}
else
{
var refLengths = chemSpace.getAllAutoScaleRefLengths(this.getCoordMode(), this.getAllowCoordBorrow());
refLength = refLengths.length? Kekule.ArrayUtils.getMedian(refLengths): null;
}
if (!refLength)
refLength = configs.getStructureConfigs().getDefBondLength();
chemSpace.setDefAutoScaleRefLength(refLength);
}
if (!chemSpace.getSize2D())
{
var refScreenLength = this.getRenderConfigs().getLengthConfigs().getDefBondLength();
var ratio = chemSpace.getDefAutoScaleRefLength() / refScreenLength;
chemSpace.setObjScreenLengthRatio(ratio);
chemSpace.setSize2D({'x': screenSize.x * ratio, 'y': screenSize.y * ratio});
}
}
},
/**
* Create new molecule or structure fragment in chem space.
* @param {String} id
* @returns {Kekule.StructureFragment}
*/
createNewStructFragmentAnchor: function(id)
{
/*
if (!id) // debug, auto add
{
id = 'M' + this.getChemObj().getChildCount();
}
*/
if (this.getAutoCreateNewStructFragment())
{
var result = new Kekule.Molecule(id);
this.getChemSpace().appendChild(result);
return result;
}
else
return null;
},
/**
* Create a new atom in a blank parentMol to growing bonds.
* @param {Kekule.StructFragment} parentMol
* @param {String} id
* @param {Hash} absCoord
* @param {String} isotopeId Set null to create a default one
* @returns {Kekule.ChemStructureNode}
*/
createStructStartingAtom: function(parentMol, id, absCoord, isotopeId)
{
this.beginUpdateObject();
try
{
if (parentMol)
{
if (!isotopeId)
{
// create default one
isotopeId = this.getEditorConfigs().getStructureConfigs().getDefIsotopeId();
}
var initialNode = new Kekule.Atom(null, isotopeId);
parentMol.appendNode(initialNode);
initialNode.setAbsCoordOfMode(absCoord, this.getCoordMode());
return initialNode;
}
else
return null;
}
finally
{
this.endUpdateObject();
}
},
/**
* Create new molecule or structure fragment in chem space, the fragment containing a
* starting node (atom) to growing bond.
* @param {String} id
* @param {Hash} absCoord
* @param {String} isotopeId Set null to create a default one
* @returns {Kekule.ChemStructureNode}
*/
createNewStructFragmentAndStartingAtom: function(id, absCoord, isotopeId)
{
this.beginUpdateObject();
try
{
var struct;
/*
if (this.hasOnlyOneBlankStructFragment()) // only one blank molecule, use it as the new structure fragment's parent.
struct = this.getChemSpace().getChildAt(0);
else
*/
struct = this.createNewStructFragmentAnchor(id);
if (struct)
{
return this.createStructStartingAtom(struct, id, absCoord, isotopeId);
}
else
return null;
}
finally
{
this.endUpdateObject();
}
},
/**
* Check if there is only one blank structure fragment (with no node and connector or formula) in editor.
* @returns {Bool}
*/
hasOnlyOneBlankStructFragment: function()
{
var result = false;
if (this.getChemSpace().getChildCount() === 1)
{
var obj = this.getChemSpace().getChildAt(0);
if (obj instanceof Kekule.StructureFragment)
{
if (obj.getNodeCount() <= 0 && obj.getConnectorCount() <= 0 && !obj.hasFormula()) // empty molecule
result = true;
}
}
return result;
},
/**
* If there is only one blank structure fragment (with no node and connector or formula) in editor,
* returns it.
* @returns {Kekule.StructureFragment}
*/
getOnlyOneBlankStructFragment: function()
{
if (this.hasOnlyOneBlankStructFragment())
return this.getChemSpace().getChildAt(0);
},
/**
* Returns whether a new standalone object (e.g. molecule) can be added to editor.
* @returns {Bool}
*/
canAddNewStandaloneObject: function()
{
return this.getAllowCreateNewChild() || (this.getChemSpace().getChildCount() <= 0);
},
/**
* Returns whether a new structure fragment that doest not connected to any existing ones can be added to space.
* This function returns true if property autoCreateNewStructFragment is true or there is an empty molecule in space.
* @returns {Bool}
*/
canAddUnconnectedStructFragment: function()
{
return this.canAddNewStandaloneObject() || this.hasOnlyOneBlankStructFragment();
},
/**
* Returns whether current editor allows clone objects.
* @returns {Bool}
*/
canCloneObjects: function()
{
return this.getAllowCreateNewChild();
},
/**
* Clone objects in space.
* Note that this method only works when property allowCreateNewChild is true.
* @param {Array} objects
* @param {Hash} screenCoordOffset If this value is set, new cloned objects will be moved based on this coord.
* @param {Bool} addToSpace If true, the cloned objects will add to current space immediately.
* @returns {Array} Actually cloned objects.
*/
cloneObjects: function(objects, screenCoordOffset, addToSpace)
{
if (!this.getChemSpace())
return null;
/*
if (!this.getAllowCreateNewChild())
return null;
*/
var allowAddToSpace = this.getAllowCreateNewChild();
var isParentOfOneObj = function(obj, childObjs)
{
for (var i = 0, l = childObjs.length; i < l; ++i)
{
var childObj = childObjs[i];
if (/*(obj === childObj) ||*/ (childObj.isChildOf(obj)))
return true;
}
return false;
};
var removeUnessentialChildren = function(rootObj, refObj, reservedChildObjs)
{
if (reservedChildObjs.indexOf(refObj) >= 0)
{
AU.remove(reservedChildObjs, refObj);
return;
}
if (rootObj.getChildAt && refObj.getChildAt)
{
var refChildObjCount = refObj.getChildCount();
for (var i = refChildObjCount - 1; i >= 0; --i)
{
var o = rootObj.getChildAt(i);
if (!reservedChildObjs.length)
{
rootObj.removeChild(o);
continue;
}
var refChildObj = refObj.getChildAt(i);
if (reservedChildObjs.indexOf(refChildObj) >= 0)
{
AU.remove(reservedChildObjs, refChildObj);
}
else if (isParentOfOneObj(refChildObj, reservedChildObjs))
{
if (refChildObj.getChildObjs)
removeUnessentialChildren(o, refChildObj, reservedChildObjs);
}
else // can delete
{
rootObj.removeChild(o);
}
}
}
};
var targetObjs = Kekule.ArrayUtils.toArray(objects);
var standAloneObjs = [];
var childObjMap = new Kekule.MapEx();
for (var i = 0, l = targetObjs.length; i < l; ++i)
{
var obj = targetObjs[i];
var standAloneObj = obj.getStandaloneAncestor? obj.getStandaloneAncestor(): obj;
if (standAloneObj.clone) // object can be cloned
{
Kekule.ArrayUtils.pushUnique(standAloneObjs, standAloneObj);
var mapItem = childObjMap.get(standAloneObj);
if (!mapItem)
{
mapItem = [];
childObjMap.set(standAloneObj, mapItem);
}
mapItem.push(obj);
}
}
// start clone
var space = this.getChemSpace();
var clonedObjs = [];
var coordMode = this.getCoordMode();
var allowCoordBorrow = this.getAllowCoordBorrow();
var standAloneObjCount = standAloneObjs.length;
for (var i = 0; i < standAloneObjCount; ++i)
{
var obj = standAloneObjs[i];
var clonedObj = obj.clone();
// clear ids to avoid conflict
if (clonedObj.clearIds)
clonedObj.clearIds();
/*
if (coordOffset && clonedObj.getCoordOfMode && clonedObj.setCoordOfMode)
{
var coord = clonedObj.getCoordOfMode(coordMode, allowCoordBorrow);
var newCoord = Kekule.CoordUtils.add(coord, coordOffset);
clonedObj.setCoordOfMode(newCoord, coordMode);
}
*/
// remove unessential child objects of cloned object
removeUnessentialChildren(clonedObj, obj, childObjMap.get(obj));
if (addToSpace && allowAddToSpace)
{
space.appendChild(clonedObj);
if (screenCoordOffset)
{
var coord = this.getObjectScreenCoord(clonedObj);
var newCoord = Kekule.CoordUtils.add(coord, screenCoordOffset);
this.setObjectScreenCoord(clonedObj, newCoord);
}
}
clonedObjs.push(clonedObj);
}
childObjMap.finalize();
return clonedObjs;
},
/**
* Clone objects in editor's selection.
* @param {Hash} coordOffset New cloned objects will be moved based on this coord.
* If this value is not set, a default one will be used.
* @param {Bool} addToSpace If true, the objects cloned will be added to space immediately.
* @returns {Array} Actually cloned objects.
*/
cloneSelection: function(coordOffset, addToSpace)
{
if (coordOffset === undefined) // use default one
{
coordOffset = this.getDefaultCloneScreenCoordOffset();
}
var objs = this.getSelection();
var clonedObjs = this.cloneObjects(objs, coordOffset, addToSpace);
if (addToSpace)
this.setSelection(clonedObjs);
return clonedObjs;
},
/**
* Returns default coord offset when doing clone selection job in editor.
* @returns {Hash}
*/
getDefaultCloneScreenCoordOffset: function()
{
var screenOffset = this.getEditorConfigs().getInteractionConfigs().getClonedObjectScreenOffset() || 0;
var coordMode = this.getCoordMode();
var coordOffset = {'x': screenOffset, 'y': screenOffset};
if (coordMode === Kekule.CoordMode.COORD3D)
{
coordOffset.z = screenOffset;
}
//coordOffset = this.translateCoord(coordOffset, Kekule.Editor.CoordSys.SCREEN, Kekule.Editor.CoordSys.OBJ);
return coordOffset;
},
/**
* Supply essential charge and radical markers when loading a new chemObj.
* @private
*/
_supplyChemMarkersOnObj: function(chemObj)
{
if (chemObj)
{
var structFragments = Kekule.ChemStructureUtils.getAllStructFragments(chemObj, true);
if (structFragments || structFragments.length)
{
for (var i = 0, l = structFragments.length; i < l; ++i)
{
this._createLosingChemMarkerOnStructFragment(structFragments[i]);
}
}
}
},
/** @private */
_createLosingChemMarkerOnStructFragment: function(mol)
{
mol.beginUpdate();
try
{
if (mol.getCharge && mol.getCharge())
mol.fetchChargeMarker(true);
// then the children
var nodes = mol.getNodes();
for (var i = 0, l = mol.getNodeCount(); i < l; ++i)
{
var node = mol.getNodeAt(i);
node.beginUpdate();
try
{
if (node.getCharge())
node.fetchChargeMarker(true);
if (node.getRadical())
node.fetchRadicalMarker(true);
if (node.getNodeAt) // is sub fragment
this._createLosingChemMarkerOnStructFragment(node);
}
finally
{
node.endUpdate();
}
}
}
finally
{
mol.endUpdate();
}
},
/**
* A helper function returning the available non-atom settings used by atom setter widget.
* @returns {Array}
*/
getEnabledNonAtomInputData: function()
{
var result = [];
var labelConfigs = this.getRenderConfigs().getDisplayLabelConfigs();
var nonAtomSetting = this.getEditorConfigs().getStructureConfigs().getEnabledNonAtomNodeTypes();
// R group
if (nonAtomSetting.RGroup)
result.push({
'text': labelConfigs.getRgroup(), 'nodeClass': Kekule.RGroup,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_RGROUP') //Kekule.ChemWidgetTexts.CAPTION_RGROUP
});
// Kekule.Pseudoatom
if (nonAtomSetting.pseudoatomDummy)
result.push({
'text': labelConfigs.getDummyAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.DUMMY},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_DUMMY_ATOM') //Kekule.ChemWidgetTexts.CAPTION_DUMMY_ATOM
});
if (nonAtomSetting.pseudoatomHetero)
result.push({
'text': labelConfigs.getHeteroAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.HETERO},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_HETERO_ATOM') //Kekule.ChemWidgetTexts.CAPTION_HETERO_ATOM
});
if (nonAtomSetting.pseudoatomAny)
result.push({
'text': labelConfigs.getAnyAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.ANY},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_ANY_ATOM') //Kekule.ChemWidgetTexts.CAPTION_ANY_ATOM
});
// Kekule.VariableAtom List and Not List
if (nonAtomSetting.variableAtomList)
result.push({
'text': this._getVarAtomListLabel(), 'nodeClass': Kekule.VariableAtom,
'isVarList': true,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_VARIABLE_ATOM') //Kekule.ChemWidgetTexts.CAPTION_VARIABLE_ATOM
});
if (nonAtomSetting.variableAtomNotList)
result.push({
'text': this._getVarAtomNotListLabel(), 'nodeClass': Kekule.VariableAtom,
'isNotVarList': true,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_VARIABLE_NOT_ATOM') //Kekule.ChemWidgetTexts.CAPTION_VARIABLE_NOT_ATOM
});
return result;
},
/** @private */
_getVarAtomListLabel: function()
{
var labelConfigs = this.getRenderConfigs().getDisplayLabelConfigs();
return labelConfigs? labelConfigs.getVariableAtom(): Kekule.ChemStructureNodeLabels.VARIABLE_ATOM;
},
/** @private */
_getVarAtomNotListLabel: function()
{
var labelConfigs = this.getRenderConfigs().getDisplayLabelConfigs();
return '~' + (labelConfigs? labelConfigs.getVariableAtom(): Kekule.ChemStructureNodeLabels.VARIABLE_ATOM);
},
/**
* A helper function returning the available bond form data used by bond setter widget.
* @returns {Array}
*/
getEnabledBondFormData: function()
{
var BT = Kekule.BondType;
var BO = Kekule.BondOrder;
var BS = Kekule.BondStereo;
var $L = Kekule.$L;
var HTMLCLASS_PREFIX = 'K-Chem-MolBondIaController-';
var predefinedBondData = {
// covalent bond types
'single': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_SINGLE'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_SINGLE'),
'htmlClass': HTMLCLASS_PREFIX + 'Single',
'isDefault': true, // the default bond type
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.SINGLE, 'stereo': BS.NONE}
},
'double': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_DOUBLE'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_DOUBLE'),
'htmlClass': HTMLCLASS_PREFIX + 'Double',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.DOUBLE, 'stereo': BS.NONE}
},
'triple': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_TRIPLE'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_TRIPLE'),
'htmlClass': HTMLCLASS_PREFIX + 'Triple',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.TRIPLE, 'stereo': BS.NONE}
},
'quad': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_QUAD'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_QUAD'),
'htmlClass': HTMLCLASS_PREFIX + 'Quad',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.QUAD, 'stereo': BS.NONE}
},
'explicitAromatic': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_EXPLICIT_AROMATIC'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_EXPLICIT_AROMATIC'),
'htmlClass': HTMLCLASS_PREFIX + 'Aromatic',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.EXPLICIT_AROMATIC, 'stereo': BS.NONE}
},
// stereo bond types
'up': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_WEDGEUP'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_WEDGEUP'),
'htmlClass': HTMLCLASS_PREFIX + 'WedgeUp',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.SINGLE, 'stereo': BS.UP}
},
'upInverted': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_WEDGEUP_INVERTED'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_WEDGEUP_INVERTED'),
'htmlClass': HTMLCLASS_PREFIX + 'WedgeUpInverted',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.SINGLE, 'stereo': BS.UP_INVERTED}
},
'down': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_WEDGEDOWN'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_WEDGEDOWN'),
'htmlClass': HTMLCLASS_PREFIX + 'WedgeDown',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.SINGLE, 'stereo': BS.DOWN}
},
'downInverted': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_WEDGEDOWN_INVERTED'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_WEDGEDOWN_INVERTED'),
'htmlClass': HTMLCLASS_PREFIX + 'WedgeDownInverted',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.SINGLE, 'stereo': BS.DOWN_INVERTED}
},
'upOrDown': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_WAVY'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_WAVY'),
'htmlClass': HTMLCLASS_PREFIX + 'WedgeUpOrDown',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.SINGLE, 'stereo': BS.UP_OR_DOWN}
},
'closer': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_CLOSER'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_CLOSER'),
'htmlClass': HTMLCLASS_PREFIX + 'Closer',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.SINGLE, 'stereo': BS.CLOSER}
},
'eOrZ': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_DOUBLE_EITHER'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_DOUBLE_EITHER'),
'htmlClass': HTMLCLASS_PREFIX + 'Double-Either',
'bondProps': {'bondType': BT.COVALENT, 'bondOrder': BO.DOUBLE, 'stereo': BS.E_OR_Z}
},
// other types
'ionic': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_IONIC'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_IONIC'),
'htmlClass': HTMLCLASS_PREFIX + 'Ionic',
'bondProps': {'bondType': BT.IONIC}
},
'coordinate': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_COORDINATE'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_COORDINATE'),
'htmlClass': HTMLCLASS_PREFIX + 'Coordinate',
'bondProps': {'bondType': BT.COORDINATE}
},
'metallic': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_METALLIC'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_METALLIC'),
'htmlClass': HTMLCLASS_PREFIX + 'Metallic',
'bondProps': {'bondType': BT.METALLIC}
},
'hydrogen': {
'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_HYDROGEN'),
'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_HYDROGEN'),
'htmlClass': HTMLCLASS_PREFIX + 'Hydrogen',
'bondProps': {'bondType': BT.HYDROGEN}
}
};
var predefinedExtraData = {
'single': {},
'double': {'text': $L('ChemWidgetTexts.CAPTION_MOL_BOND_DOUBLE'), 'hint': $L('ChemWidgetTexts.HINT_MOL_BOND_DOUBLE')}
};
var bondForms = this.getEditorConfigs().getStructureConfigs().getEnabledBondForms();
var keys = Kekule.ObjUtils.getOwnedFieldNames(bondForms, false);
var result = [];
for (var i = 0, l = keys.length; i < l; ++i)
{
var key = keys[i];
if (bondForms[key]) // this form should be available
{
if (DataType.isObjectValue(bondForms[key])) // a custom bond form
result.push(bondForms[key]);
else if (predefinedBondData[key])
result.push(predefinedBondData[key]);
}
}
return result;
}
});
/**
* A special class to give a setting facade for ChemSpaceEditor.
* Do not use this class alone.
* @class
* @augments Kekule.Editor.BaseEditor.Settings
* @ignore
*/
Kekule.Editor.ChemSpaceEditor.Settings = Class.create(Kekule.Editor.BaseEditor.Settings,
/** @lends Kekule.Editor.ChemSpaceEditor.Settings# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.ChemSpaceEditor.Settings',
/** @private */
initProperties: function()
{
this.defineProp('allowCreateNewChild', {'dataType': DataType.BOOL, 'serializable': false,
'getter': function() { return this.getEditor().getAllowCreateNewChild(); },
'setter': function(value) { this.getEditor().setAllowCreateNewChild(value); }
});
}
});
/**
* Controller for deleting objects (including molecules) in chem space editor.
* @class
* @augments Kekule.Widget.BasicEraserIaController
*
* @param {Kekule.Editor.BaseEditor} widget Editor of current object being installed to.
*/
Kekule.Editor.BasicMolEraserIaController = Class.create(Kekule.Editor.BasicEraserIaController,
/** @lends Kekule.Editor.BasicMolEraserIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BasicMolEraserIaController',
/** @constructs */
initialize: function($super, widget)
{
$super(widget);
},
/*
* @private
*/
doGetActualRemovedObjs: function(objs)
{
var result = [];
Kekule.ArrayUtils.pushUnique(result, objs);
for (var i = 0, l = objs.length; i < l; ++i)
{
var delObjs = objs[i].getCascadeDeleteObjs? objs[i].getCascadeDeleteObjs(): [];
//Kekule.Editor.StructureUtils.getCascadeDeleteObjs(objs[i]);
if (delObjs)
Kekule.ArrayUtils.pushUnique(result, delObjs);
}
return result;
},
/** @private */
doRemoveObjs: function(objs)
{
if (!objs.length)
return;
var operGroup = new Kekule.MacroOperation();
var molParents = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var obj = objs[i];
var parent = obj.getParent();
var editor = this.getEditor();
var oper;
if (obj instanceof /*Kekule.ChemStructureNode*/Kekule.BaseStructureNode)
{
oper = new Kekule.ChemStructOperation.RemoveNode(obj, null, null, editor);
Kekule.ArrayUtils.pushUnique(molParents, obj.getParent());
}
else if (obj instanceof /*Kekule.ChemStructureConnector*/Kekule.BaseStructureConnector)
{
oper = new Kekule.ChemStructOperation.RemoveConnector(obj, null, null, editor);
Kekule.ArrayUtils.pushUnique(molParents, obj.getParent());
}
else
{
oper = new Kekule.ChemObjOperation.Remove(obj, null, null, editor);
}
if (oper)
operGroup.add(oper);
}
// check if molecules need to be splitted
for (var i = 0, l = molParents.length; i < l; ++i)
{
var mol = molParents[i];
if (mol && (mol instanceof Kekule.StructureFragment))
{
var standardizeOper = new Kekule.ChemStructOperation.StandardizeStructFragment(mol, editor);
standardizeOper.setEnableSplit(this.getEditor().canCreateNewChild()); // if can not create new child, split is disabled.
operGroup.add(standardizeOper);
}
}
//operGroup.execute();
editor.execOperation(operGroup);
/* Do not need to add to history, since it has been done in editor.execOperation
// add to history
if (editor && editor.getEnableOperHistory())
{
editor.pushOperation(operGroup);
}
*/
}
});
/** @ignore */
Kekule.Editor.IaControllerManager.register(Kekule.Editor.BasicMolEraserIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Advanced controller for selecting, moving or rotating chem objects in molecule (ctab) based editor.
* @class
* @augments Kekule.Editor.BasicManipulationIaController
*
* @property {Bool} enableMagneticMerge Whether nearing node will be merged when moving their position.
* @property {Bool} enableNodeMerge Whether node merging is allowed.
* @property {Bool} enableNeighborNodeMerge Whether neighboring node merging is allowed.
* @property {Bool} enableConnectorMerge Whether connector merging is allowed.
* @property {Bool} enableStructFragmentMerge Whether node or connector merging between different molecule is allowed.
* @property {Bool} enableConstrainedMove
* @property {Bool} enableConstrainedRotate
* @property {Bool} enableConstrainedResize
* @property {Bool} enableDirectedMove When true, pres shift key during moving will cause object moves only at X or Y direction.
*/
Kekule.Editor.BasicMolManipulationIaController = Class.create(Kekule.Editor.BasicManipulationIaController,
/** @lends Kekule.Editor.BasicMolManipulationIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BasicMolManipulationIaController',
/** @constructs */
initialize: function($super, editor)
{
$super(editor);
this.setEnableMagneticMerge(true);
this.setEnableNodeMerge(true);
this.setEnableNeighborNodeMerge(true);
this.setEnableConnectorMerge(true);
this.setEnableStructFragmentMerge(true);
this.setEnableConstrainedMove(true);
this.setEnableConstrainedRotate(true);
this.setEnableConstrainedResize(true);
this.setEnableDirectedMove(true);
this._suppressConstrainedMoving = false; // used internally
this._suppressConstrainedRotating = false; // used internally
this._isInDirectedMoving = false;
this._directedMovingDirection = null; // used internally
},
/** @private */
initProperties: function()
{
this.defineProp('enableMagneticMerge', {'dataType': DataType.BOOL});
this.defineProp('enableNodeMerge', {'dataType': DataType.BOOL});
this.defineProp('enableNeighborNodeMerge', {'dataType': DataType.BOOL});
this.defineProp('enableConnectorMerge', {'dataType': DataType.BOOL});
this.defineProp('enableStructFragmentMerge', {'dataType': DataType.BOOL});
//this.defineProp('mergeOperation', {'dataType': 'Kekule.MacroOperation', 'serializable': false}); // store operation of merging nodes
//this.defineProp('connectorMergeOperation', {'dataType': 'Kekule.Operation', 'serializable': false}); // store operation of merging
this.defineProp('allManipulateObjsMerged', {'dataType': DataType.BOOL, 'serializable': false}); // store whether a merge operation merges all current objects
this.defineProp('isMergeDone', {'dataType': DataType.BOOL, 'serializable': false}); // store whether a merge operation is done
this.defineProp('enableConstrainedMove', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableConstrainedRotate', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableConstrainedResize', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableDirectedMove', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('mergeOperations', {'dataType': DataType.ARRAY, 'serializable': false}); // store operations of merging
this.defineProp('mergePreviewOperations', {'dataType': DataType.ARRAY, 'serializable': false}); // store preview operations of merging
//this.defineProp('prevMergeOperations', {'dataType': DataType.ARRAY, 'serializable': false}); // store operations of merging of last phrase
this.defineProp('mergingDests', {'dataType': DataType.ARRAY, 'serializable': false}); // private
},
/** @private */
canInteractWithObj: function($super, obj)
{
return $super(obj) || (this._isMerging && obj);
},
/** @ignore */
doSetManipulateObjs: function($super, value)
{
// When merging just node to another, operating node will be removed from
// space temporily, add dest mol to operating objs avoid the disappearing of
// mol in editor when using operContext
var objs = value? AU.clone(value): [];
if (this.getMergingDests() && !this.useMergePreview())
{
AU.pushUnique(objs, this.getMergingDests());
}
this.setPropStoreFieldValue('manipulateObjs', value);
if (!value)
this.getEditor().endOperatingObjs();
else
this.getEditor().prepareOperatingObjs(objs);
},
/**
* Check if currently is in constrained rotate mode.
* In that mode, objects can only be rotate to some certain angle.
* @returns {Bool}
* @private
*/
isConstrainedRotate: function()
{
return this.getEnableConstrainedMove();
},
/**
* Check if currently is in constrained resize mode.
* In that mode, objects can only be rotate to some certain angle.
* @returns {Bool}
* @private
*/
isConstrainedResize: function()
{
return this.getEnableConstrainedResize();
},
/**
* Check if currently is in constrained move mode.
* That is to say, have not select any object, directly move a node and the node has only one connector connected.
* In that case, original connector length should be retained.
* @returns {Bool}
* @private
*/
isConstrainedMove: function()
{
if (!this.getEnableConstrainedMove())
return false;
/*
if (this._suppressConstrainedMoving)
return false;
*/
var objs = this.getManipulateObjs();
if (objs.length === 1)
{
var obj = objs[0];
var selection = this.getEditor().getSelection();
if (selection && (selection.indexOf(obj) < 0))
{
/*
if (obj instanceof Kekule.ChemStructureNode)
{
return (obj.getLinkedObjs().length === 1);
}
*/
return obj.getConstraintManipulationBaseObj && obj.getConstraintManipulationBaseObj();
}
}
return false;
},
/**
* Check if currently in real constrained rotation and the constrains are not suppressed (e.g., pressing the Alt key).
* @returns {Bool}
*/
isInActualConstrainedRotation: function()
{
return (this.isConstrainedRotate() && (!this._suppressConstrainedRotating));
},
/**
* Check if currently is in directed move mode.
* @returns {Bool}
* @private
*/
isDirectedMove: function()
{
return (this.getEnableDirectedMove() && (!this.isConstrainedMove()) && this._isInDirectedMoving);
},
/** @private */
useMergePreview: function()
{
return this._useMergePreview;
},
/** @private */
setUseMergePreview: function(value)
{
this._useMergePreview = value;
},
/** @private */
getMergeOperationsInManipulating: function()
{
return this._useMergePreview? this.getMergePreviewOperations(): this.getMergeOperations();
},
/** @private */
createManipulateObjInfo: function($super, obj, objIndex, startContextCoord)
{
var editor = this.getEditor();
var info = $super(obj, objIndex, startContextCoord);
var isConstrained = this.isConstrainedMove();
if (isConstrained) // constrained move, store connector length into info
{
/*
var connector = obj.getLinkedConnectors()[0];
var connectedNode = obj.getLinkedObjs()[0];
*/
var stubObj = obj.getConstraintManipulationBaseObj();
//if (connector && connectedNode)
if (stubObj)
{
info.isConstrained = true;
if (!info.hasNoCoord)
{
info.originScreenCoord = editor.getObjectScreenCoord(obj);
//info.refScreenCoord = editor.getObjectScreenCoord(connectedNode);
info.refScreenCoord = editor.getObjectScreenCoord(stubObj);
info.connectorScreenLength = Kekule.CoordUtils.getDistance(info.screenCoord, info.refScreenCoord);
//info.connectorObjLength = connector.getLength(this.getEditor().getCoordMode(), this.getEditor().getAllowCoordBorrow());
info.connectorObjLength = Kekule.CoordUtils.getDistance(editor.getObjCoord(obj), editor.getObjCoord(stubObj));
var delta = Kekule.CoordUtils.substract(info.originScreenCoord, info.refScreenCoord);
info.originBondDirectionAngle = Math.atan2(delta.y, delta.x);
//console.log('create Info', info.screenCoord, info.refScreenCoord, info.connectorScreenLength);
info.constrainedBondDirectionDelta = this.getEditorConfigs().getStructureConfigs().getBondConstrainedDirectionDelta();
info.constrainedBondDirectionAngles = this.getEditorConfigs().getStructureConfigs().getBondConstrainedDirectionAngles();
info.constrainedBondDirectionAngleThreshold = this.getEditorConfigs().getStructureConfigs().getBondConstrainedDirectionAngleThreshold();
}
}
}
return info;
},
/** @private */
_calcActualMovedScreenCoord: function($super, obj, info, newScreenCoord)
{
var C = Kekule.CoordUtils;
var result;
var screenCoord = newScreenCoord;
if (info.isConstrained && (!this._suppressConstrainedMoving)) // constrained, need to reserve bond length, recalc coord
{
var constrainedBondDirectionDelta = info.constrainedBondDirectionDelta || null;
var constrainedBondDirectionAngles = info.constrainedBondDirectionAngles || null;
var constrainedBondDirectionAngleThreshold = info.constrainedBondDirectionAngleThreshold || null;
var directionAngleResolved = false;
var directionAngle;
var currVector = C.substract(newScreenCoord, info.refScreenCoord);
var currDirection = Math.atan2(currVector.y, currVector.x);
if (constrainedBondDirectionAngles && constrainedBondDirectionAngleThreshold)
{
for (var i = 0, l = constrainedBondDirectionAngles.length; i < l; ++i)
{
var dAngle = constrainedBondDirectionAngles[i];
if (Math.abs(dAngle - currDirection) <= constrainedBondDirectionAngleThreshold)
{
directionAngleResolved = true;
directionAngle = dAngle;
}
}
}
if (!directionAngleResolved && constrainedBondDirectionDelta)
{
var step = Math.round((currDirection - info.originBondDirectionAngle) / constrainedBondDirectionDelta);
//console.log()
directionAngle = info.originBondDirectionAngle + step * constrainedBondDirectionDelta;
directionAngleResolved = true;
}
if (!directionAngleResolved)
directionAngle = currDirection;
result = C.add(info.refScreenCoord,
{'x': info.connectorScreenLength * Math.cos(directionAngle), 'y': info.connectorScreenLength * Math.sin(directionAngle)});
//console.log(directionAngle, result);
/*
var currLength = C.getDistance(screenCoord, info.refScreenCoord);
if (currLength !== info.connectorScreenLength)
{
var ratio = info.connectorScreenLength / currLength;
var delta = C.substract(screenCoord, info.refScreenCoord);
//console.log('constrained', info, ratio);
result = C.add(info.refScreenCoord, C.multiply(delta, ratio));
//console.log(result, info.connectorScreenLength, C.getDistance(result, info.refScreenCoord));
}
*/
}
else
result = screenCoord;
return result;
},
/** @private */
_calcActualRotateAngle: function($super, objs, newDeltaAngle, oldAbsAngle, newAbsAngle)
{
var isConstrained = (this.isConstrainedRotate() && (!this._suppressConstrainedRotating));
var angleStep = isConstrained?
this.getEditorConfigs().getInteractionConfigs().getConstrainedRotateStep(): null;
if (angleStep)
{
//var times = Math.floor(newDeltaAngle / angleStep);
var times = Math.round(newDeltaAngle / angleStep);
return times * angleStep;
}
else
return $super(objs, newDeltaAngle/*, oldAbsAngle, newAbsAngle*/);
},
/** @private */
_calcActualResizeScales: function(objs, newScales)
{
var isConstrained = (this.isConstrainedResize() && (!this._suppressConstrainedResize));
if (!isConstrained)
return newScales;
else // constrained scale calculation
{
var scaleStep = this.getEditorConfigs().getInteractionConfigs().getConstrainedResizeStep();
if (scaleStep)
{
var sx = (Math.round(newScales.scaleX / scaleStep) || 1) * scaleStep; // do not scale to 0
var sy = (Math.round(newScales.scaleY / scaleStep) || 1) * scaleStep;
var actualScales = {
'scaleX': sx,
'scaleY': sy
};
return actualScales;
}
else
return newScales;
}
},
/** @private */
prepareManipulating: function($super, manipulationType, manipulatingObjs, startScreenCoord, startBox, rotateCenter, rotateRefCoord)
{
/*
this.setIsMergeDone(false);
this.setMergeOperation(null);
*/
this.setUseMergePreview(this.getEditorConfigs().getInteractionConfigs().getEnableMergePreview());
this.setMergeOperations([]);
this.setMergePreviewOperations([]);
//this.setPrevMergeOperations([]);
$super(manipulationType, manipulatingObjs, startScreenCoord, startBox, rotateCenter, rotateRefCoord);
//this._mergeReversed = false; // internal flag
this._directedMovingDirection = null;
this.setManuallyHotTrack(true); // manully hot track
//console.log('start', this.getManuallyHotTrack());
},
/** @ignore */
manipulateEnd: function($super)
{
$super();
this.getEditor().hideHotTrack();
this.setManuallyHotTrack(false);
},
/** @ignore */
createManipulateOperation: function($super)
{
$super();
this.setMergeOperations([]);
this.setMergePreviewOperations([]);
},
/** @ignore */
getAllObjOperations: function($super, isTheFinalOperationToEditor)
{
/*
var moveOpers = this.getMoveOperations();
var mergeOpers = this.getMergeOperations();
var count = Math.max(moveOpers.length, mergeOpers.length);
var result = [];
for (var i = 0; i < count; ++i)
{
if (mergeOpers[i])
result.push(mergeOpers[i]);
else
result.push(moveOpers[i]);
}
return result;
*/
//console.log('getAllObjOperations', isTheFinalOperationToEditor, this.useMergePreview());
var mergeOpers;
// if use merge preview, we should do the actual merging when the manipulation is done
if (isTheFinalOperationToEditor && this.useMergePreview())
{
var previewOpers = this.getMergePreviewOperations();
if (previewOpers && previewOpers.length)
{
//console.log('preview opers', previewOpers);
var opers = this.getMergeOperations();
for (var i = 0, l = previewOpers.length; i < l; ++i)
{
var previewOper = previewOpers[i];
if (previewOper) // may be empty slot in operations
{
var mergeConnector = (previewOper instanceof Kekule.ChemStructOperation.MergeConnectorsBase);
/*
Kekule.ChemStructOperation.MergeConnectors:
Kekule.ChemStructOperation.MergeNodes;
*/
var oper = mergeConnector ?
this.createConnectorMergeOperation(previewOper.getTarget(), previewOper.getDest()) :
this.createNodeMergeOperation(previewOper.getTarget(), previewOper.getDest());
opers.push(oper);
}
}
this.executeMergeOpers(opers);
}
mergeOpers = opers; // this.getMergeOperations();
}
else
{
mergeOpers = this.getMergeOperationsInManipulating();
}
//console.log('merge operations', mergeOpers);
var result = $super() || [];
if (mergeOpers && mergeOpers.length)
Kekule.ArrayUtils.pushUnique(result, mergeOpers);
return result;
},
/** @private */
_canMergeNodes: function(targetNode, destNode)
{
var allowMolMerge = this.getEnableStructFragmentMerge();
var allowNeighborNodeMerge = this.getEnableNeighborNodeMerge();
if (this.getEnableNodeMerge())
{
/*
if (this.getEnableStructFragmentMerge())
result = true;
else
result = (targetNode.getParent() === destNode.getParent());
*/
return Kekule.ChemStructOperation.MergeNodes.canMerge(targetNode, destNode, allowMolMerge, allowNeighborNodeMerge);
}
else
return false;
},
/** @private */
_canMergeConnectors: function(targetConnector, destConnector)
{
var allowMolMerge = this.getEnableStructFragmentMerge();
if (this.getEnableConnectorMerge())
{
return Kekule.ChemStructOperation.MergeConnectors.canMerge(targetConnector, destConnector, allowMolMerge);
}
else
return false;
},
/** @private */
_findSuitableMergeTargetBoundInfo: function(boundInfos, excludedObjs, targetClass, checkFunc)
{
for (var i = boundInfos.length - 1; i >= 0; --i)
{
var info = boundInfos[i];
var obj = info.obj;
if (excludedObjs.indexOf(obj) >= 0)
continue;
if (!(obj instanceof targetClass))
continue;
if (checkFunc && !checkFunc(obj))
continue;
return info;
}
return null;
},
/** @private */
_getMagneticNodeMergeDest: function(node, nodeScreenCoord, excludedObjs)
{
var editor = this.getEditor();
var self = this;
var filterFunc = function(bound)
{
var obj = bound.obj;
return (node !== obj) && (excludedObjs.indexOf(obj) < 0)
&& (obj instanceof Kekule.ChemStructureNode)
&& self._canMergeNodes(node, obj);
};
if (nodeScreenCoord)
{
var boundInfos = editor.getBoundInfosAtCoord(nodeScreenCoord, filterFunc, this.getCurrBoundInflation());
//console.log('boundInfos', boundInfos);
/*
var overlapBoundInfo = this._findSuitableMergeTargetBoundInfo(boundInfos, excludedObjs, Kekule.ChemStructureNode,
function(destObj)
{
return self._canMergeNodes(node, destObj);
}
);
*/
var overlapBoundInfo = boundInfos.length? boundInfos[boundInfos.length - 1]: null;
return overlapBoundInfo? overlapBoundInfo.obj: null;
}
else
return null;
},
/** @ignore */
moveManipulatedObjs: function($super, endScreenCoord)
{
var actualEndCoord; // = Object.extend({}, endScreenCoord);
//console.log(this.isDirectedMove(), this._isInDirectedMoving);
if (this.isDirectedMove())
{
var C = Kekule.CoordUtils;
var startCoord = this.getStartCoord();
var distance = C.getDistance(startCoord, endScreenCoord);
var delta = C.substract(endScreenCoord, startCoord);
var directedDirection = this._directedMovingDirection;
if (this._directedMovingDirection) // already set direction
{
}
else // not set, need calculate
{
directedDirection = (Math.abs(delta.x) >= Math.abs(delta.y))? 'x': 'y';
if (distance < this.getEditor().getEditorConfigs().getInteractionConfigs().getDirectedMoveDistanceThreshold())
{
// do nothing
}
else // set _directedMovingDirection
{
this._directedMovingDirection = directedDirection;
}
}
actualEndCoord = Object.extend({}, startCoord);
actualEndCoord[directedDirection] = endScreenCoord[directedDirection];
}
else // normal move
actualEndCoord = endScreenCoord;
return $super(actualEndCoord);
},
/** @ignore */
applyManipulatingObjsInfo: function($super, endScreenCoord)
{
this.setAllManipulateObjsMerged(false);
var useMergePreview = this.useMergePreview();
var editor = this.getEditor();
editor.hotTrackOnObj(null); // clear old hot track objects
var MT = Kekule.Editor.BasicManipulationIaController.ManipulationType;
var manipulateType = this.getManipulationType();
var originManipulatedObjs = this.getManipulateOriginObjs();
var manipulatedObjs = this.getManipulateObjs();
var excludedObjs = [].concat(originManipulatedObjs);
Kekule.ArrayUtils.pushUnique(excludedObjs, manipulatedObjs);
//var oldMergeOpers = this.getMergeOperations();
var oldMergeOpers = this.getMergeOperationsInManipulating();
//console.log(this.getMergeOperationsInManipulating() === this.getMergeOperations());
//var oldMergeOpers = this.getMergeOperations();
//var allowMolMerge = this.getEnableStructFragmentMerge();
var self = this;
var objCanBeMerged = function(obj)
{
if (obj instanceof Kekule.StructureFragment)
return false;
else
return (obj instanceof Kekule.ChemStructureNode) || (obj instanceof Kekule.ChemStructureConnector);
} ;
// handle mouse position merge and magnetic merge here
var isMovingOneBond = (originManipulatedObjs.length === 1) && (originManipulatedObjs[0] instanceof Kekule.ChemStructureConnector);
var isMovingOneNode = (manipulatedObjs.length === 1) && (manipulatedObjs[0] instanceof Kekule.ChemStructureNode) && objCanBeMerged(manipulatedObjs[0]);
if (!isMovingOneBond && this.getEnableMagneticMerge())
{
var currManipulateInfoMap = this.getManipulateObjCurrInfoMap();
var manipulateInfoMap = this.getManipulateObjInfoMap();
var self = this;
var magneticMergeObjIndexes = [];
var magneticMergeObjs = [];
var magneticMergeDests = [];
// filter out all merge nodes
//console.log('manipulate objects count', manipulatedObjs.length);
for (var i = 0, l = manipulatedObjs.length; i < l; ++i)
{
var obj = manipulatedObjs[i];
if (!objCanBeMerged(obj))
continue;
var currInfo = currManipulateInfoMap.get(obj);
var currCoord = currInfo.screenCoord;
if (currCoord)
{
var boundInfos = editor.getBoundInfosAtCoord(currCoord, null, this.getCurrBoundInflation());
/*
var overlapBoundInfo = this._findSuitableMergeTargetBoundInfo(boundInfos, excludedObjs, Kekule.ChemStructureNode,
function(destObj)
{
return self._canMergeNodes(obj, destObj);
}
);
if (overlapBoundInfo && overlapBoundInfo.obj) // may merge, store info
*/
var mergeDest = this._getMagneticNodeMergeDest(obj, currCoord, excludedObjs);
if (mergeDest) // may merge, store info
{
magneticMergeObjIndexes.push(i);
magneticMergeObjs.push(obj);
magneticMergeDests.push(mergeDest);
//console.log('check merge ok on', i);
}
else
{
//console.log('check merge fail on', i, currCoord);
}
}
}
if (magneticMergeObjs.length) // has merge items
{
var mergedObjCount = magneticMergeObjs.length;
this.setAllManipulateObjsMerged(mergedObjCount === manipulatedObjs.length);
/*
if (this.getAllManipulateObjsMerged())
console.log('all merged!', mergedObjCount);
*/
var mergeSingleObj = (mergedObjCount <= 1); // merge only one node
/*
// If merge on only one node, other node position may also be changed
// e.g. add repository ring structure to another node
var needCreateNewMerge = (mergedObjCount <= 1); // false;
*/
var needCreateNewMerge = false;
// check if need create new merge operation
if (!needCreateNewMerge)
{
var sameMergeOpers = [];
//console.log('oldMergeOpers', oldMergeOpers, magneticMergeObjIndexes);
for (var i = 0, l = magneticMergeObjs.length; i < l; ++i)
{
var obj = magneticMergeObjs[i];
var dest = magneticMergeDests[i];
var index = magneticMergeObjIndexes[i];
var oldMergeOper = oldMergeOpers[index];
if (!oldMergeOper || !this.isSameNodeMerge(oldMergeOper, obj, dest))
{
//console.log('need new', oldMergeOper, obj.getId(), dest.getId(), index);
needCreateNewMerge = true;
break;
}
else
sameMergeOpers.push(oldMergeOper);
}
for (var i = 0, l = oldMergeOpers.length; i < l; ++i)
{
var oldMergeOper = oldMergeOpers[i];
if (oldMergeOper)
{
var index = sameMergeOpers.indexOf(oldMergeOper);
if (index < 0) // old merge has more nodes than current, need to recreate new merge
{
needCreateNewMerge = true;
break;
}
else
sameMergeOpers.splice(index, 1);
}
}
}
//console.log('need new', needCreateNewMerge, mergeSingleObj);
if (needCreateNewMerge || mergeSingleObj)
{
if (needCreateNewMerge)
{
//console.log('need new');
//this.reverseMergeOpers();
this.reverseMergeOpers(this.getMergeOperationsInManipulating());
}
// also need to adjust position of rest manipulatedObjs
var CU = Kekule.CoordUtils;
if ((mergedObjCount === 1) || (editor.getCoordMode() === Kekule.CoordMode.COORD3D))
{
var currInfo = currManipulateInfoMap.get(magneticMergeObjs[0]);
var currCoord = currInfo.screenCoord;
var destCoord = editor.getObjectScreenCoord(magneticMergeDests[0]);
var coordTranslate = CU.substract(destCoord, currCoord);
// change all currInfo coord, and redo apply job
var needReApply = false;
var fequal = Kekule.NumUtils.isFloatEqual;
var threshold = 1e-10; //{x: Math.abs(currCoord.x) * 1e-8, y: Math.abs(currCoord.y) * 1e-8}
if (!fequal(coordTranslate.x, 0, threshold) || !fequal(coordTranslate.y, 0, threshold)) // if transalte coord is {0, 0} (often ocurrs in ring / chain ia controller, no need to adjust coords)
{
//console.log('here', coordTranslate, currCoord, destCoord);
for (var i = 0, l = manipulatedObjs.length; i < l; ++i)
{
var obj = manipulatedObjs[i];
if (obj !== magneticMergeObjs[0])
{
var info = currManipulateInfoMap.get(obj);
if (info.screenCoord)
{
var newCoord = CU.add(info.screenCoord, coordTranslate);
info.screenCoord = newCoord;
if (this._getMagneticNodeMergeDest(obj, newCoord, excludedObjs)) // move position can do another magnetic merge
needReApply = true;
}
//this.applySingleManipulatingObjInfo(i, obj, info, endScreenCoord);
}
}
}
if (needReApply)
{
return this.applyManipulatingObjsInfo(endScreenCoord);
}
else
{
for (var i = 0, l = manipulatedObjs.length; i < l; ++i)
{
var obj = manipulatedObjs[i];
//if (obj !== magneticMergeObjs[0])
{
var info = currManipulateInfoMap.get(obj);
this.applySingleManipulatingObjInfo(i, obj, info, endScreenCoord);
}
}
}
}
else if ((mergedObjCount > 1) && (editor.getCoordMode() !== Kekule.CoordMode.COORD3D)) // 2 or more, first two one decide all others' position
{
var obj0 = magneticMergeObjs[0];
var obj1 = magneticMergeObjs[1];
var coordObj0 = manipulateInfoMap.get(obj0).screenCoord;
var coordObj1 = manipulateInfoMap.get(obj1).screenCoord;
//console.log(coordObj0, coordObj1, manipulateInfoMap.get(obj0));
/*
var distanceObj = CU.getDistance(coordObj0, coordObj1);
var deltaObj = CU.substract(coordObj1, coordObj0);
var angleObj = Math.atan2(deltaObj.y, deltaObj.x);
*/
var coordDest0 = editor.getObjectScreenCoord(magneticMergeDests[0]);
var coordDest1 = editor.getObjectScreenCoord(magneticMergeDests[1]);
/*
var distanceDest = CU.getDistance(coordDest0, coordDest1);
var deltaDest = CU.substract(coordDest1, coordDest0);
var angleDest = Math.atan2(deltaDest.y, deltaDest.x);
var coordDelta = CU.substract(coordDest0, coordObj0);
//console.log(coordDelta, coordDest0, coordObj0);
var transParam = {
'translateX': coordDelta.x,
'translateY': coordDelta.y,
'scale': distanceDest / distanceObj,
'rotateAngle': angleDest - angleObj,
'center': coordObj0 //coordDest0
}
*/
// TODO: currently only handle 2D situation
var transParam = CU.calcCoordGroup2DTransformParams(coordObj0, coordObj1, coordDest0, coordDest1);
//console.log(transParam, transParam.rotateAngle * 180 / Math.PI);
var matrix = CU.calcTransform2DMatrix(transParam);
// change all currInfo coord, and redo apply job
var needReApply = false;
for (var i = 0, l = manipulatedObjs.length; i < l; ++i)
{
var obj = manipulatedObjs[i];
if (magneticMergeObjs.indexOf(obj) < 0)
{
var info = manipulateInfoMap.get(obj);
var currInfo = currManipulateInfoMap.get(obj);
if (info.screenCoord)
{
var newCoord = CU.transform2DByMatrix(info.screenCoord, matrix);
currInfo.screenCoord = newCoord;
if (this._getMagneticNodeMergeDest(obj, newCoord, excludedObjs)) // move position can do another magnetic merge
needReApply = true;
}
if (info.size)
currInfo.size = CU.multiply(info.size, transParam.scale);
//this.applySingleManipulatingObjInfo(i, obj, currInfo, endScreenCoord);
}
}
if (needReApply)
return this.applyManipulatingObjsInfo(endScreenCoord);
else
{
for (var i = 0, l = manipulatedObjs.length; i < l; ++i)
{
var obj = manipulatedObjs[i];
if (magneticMergeObjs.indexOf(obj) < 0)
{
var currInfo = currManipulateInfoMap.get(obj);
this.applySingleManipulatingObjInfo(i, obj, currInfo, endScreenCoord);
}
}
}
}
if (needCreateNewMerge)
{
//console.log('here');
for (var i = 0, l = mergedObjCount; i < l; ++i)
{
var obj = magneticMergeObjs[i];
var dest = magneticMergeDests[i];
var index = magneticMergeObjIndexes[i];
if (useMergePreview)
{
var mergePreviewOper = this.createNodeMergeOperation(obj, dest, true);
this.getMergePreviewOperations()[index] = mergePreviewOper;
}
else
{
var mergeOper = this.createNodeMergeOperation(obj, dest);
this.getMergeOperations()[index] = mergeOper;
}
}
//console.log('execute merge on', mergedObjCount);
//console.log('create new', magneticMergeObjIndexes, this.getMergeOperationsInManipulating());
//this.executeMergeOpers();
this.executeMergeOpers(this.getMergeOperationsInManipulating());
}
}
//console.log('hot track on', magneticMergeDests.length, mergedObjCount, magneticMergeObjs.length);
editor.hotTrackOnObj(magneticMergeDests);
return;
}
}
// check if do magnetic merge
// then check if mouse position merge
if (manipulateType === MT.MOVE)
{
var doMousePosMerge = false;
if ((isMovingOneBond && this.getEnableConnectorMerge()) || (isMovingOneNode && this.getEnableNodeMerge()))
{
// check if endScreenCoord (mouse position) overlap with an existing object
var overlapedObj;
var boundInfos = editor.getBoundInfosAtCoord(endScreenCoord, null, this.getCurrBoundInflation());
var targetClass = isMovingOneBond? Kekule.ChemStructureConnector: Kekule.ChemStructureNode;
var targetObj = isMovingOneBond? originManipulatedObjs[0]: manipulatedObjs[0];
var checkFunc = isMovingOneBond?
function(obj)
{
return self._canMergeConnectors(targetObj, obj);
}:
function(obj)
{
return self._canMergeNodes(targetObj, obj);
};
var overlapBoundInfo = this._findSuitableMergeTargetBoundInfo(boundInfos, excludedObjs, targetClass, checkFunc);
if (overlapBoundInfo) // has bound info, do merge
{
var destObj = overlapBoundInfo.obj;
//console.log('can merge to', destObj.getClassName());
if (destObj)
{
this.setAllManipulateObjsMerged(true);
// can actual do merge, hot track on editor
editor.hotTrackOnObj(destObj);
var oldMergeOper = oldMergeOpers[0];
if ((isMovingOneBond && this.isSameConnectorMerge(oldMergeOper, targetObj, destObj))
|| (isMovingOneNode && this.isSameNodeMerge(oldMergeOper, targetObj, destObj))) // merged already in last phrase
{
//console.log('!!!!same merge!!!!!');
return;
// do nothing here
}
else
{
if (oldMergeOper)
{
//this.reverseMergeOpers();
this.reverseMergeOpers(this.getMergeOperationsInManipulating());
}
/*
var mergeOper = isMovingOneBond?
this.createConnectorMergeOperation(targetObj, destObj):
this.createNodeMergeOperation(targetObj, destObj);
this.getMergeOperations()[0] = mergeOper;
*/
if (useMergePreview)
{
var mergeOper = isMovingOneBond?
this.createConnectorMergeOperation(targetObj, destObj, useMergePreview):
this.createNodeMergeOperation(targetObj, destObj, useMergePreview);
this.getMergePreviewOperations()[0] = mergeOper;
}
else
{
var mergeOper = isMovingOneBond?
this.createConnectorMergeOperation(targetObj, destObj):
this.createNodeMergeOperation(targetObj, destObj);
this.getMergeOperations()[0] = mergeOper;
}
//mergeOper.execute();
//this.executeMergeOpers();
this.executeMergeOpers(this.getMergeOperationsInManipulating());
return;
}
}
}
}
}
// no merge, just reverse old one and do normal move
//this.reverseMergeOpers();
this.reverseMergeOpers(this.getMergeOperationsInManipulating());
$super(endScreenCoord);
},
/**
* @private
* @deprecated
*/
doMoveManipulatedObj_old: function($super, objIndex, obj, newScreenCoord, moverScreenCoord)
{
var editor = this.getEditor();
var info = this.getManipulateObjInfoMap().get(obj);
//var actualNewScreenCoord = this._calcActualMovedScreenCoord(obj, info, newScreenCoord);
var actualNewScreenCoord = newScreenCoord;
var manipulatedObjs = this.getManipulateOriginObjs(); //this.getManipulateObjs();
var manipulateType = this.getManipulationType();
var isMovingBond = (manipulateType === Kekule.Editor.BasicManipulationIaController.ManipulationType.MOVE)
&& (manipulatedObjs.length === 1) && (manipulatedObjs[0] instanceof Kekule.ChemStructureConnector);
// check if magnetic merge
var coord = actualNewScreenCoord;
var mergeOper = null;
//var merged = false;
var mergeOpers = this.getMergeOperations();
var oldMergeOper = mergeOpers[objIndex];
var currManipulatingObjs = this.getManipulateObjs();
if (!isMovingBond && this.getEnableMagneticMerge() && (obj instanceof Kekule.ChemStructureNode))
{
var boundInfos = editor.getBoundInfosAtCoord(coord);
if (boundInfos && boundInfos.length) // may magnetic merge
{
for (var i = boundInfos.length - 1; i >= 0; --i)
{
var info = boundInfos[i];
var boundObj = info.obj;
if (boundObj === obj)
continue;
if (currManipulatingObjs.indexOf(boundObj) >= 0)
continue;
if (boundObj instanceof Kekule.ChemStructureNode) // node on node, may merge
{
if (this._canMergeNodes(obj, boundObj)) // do merge
{
editor.addHotTrackedObj(boundObj);
if (this.isSameNodeMerge(oldMergeOper, obj, boundObj))
return;
mergeOper = this.createNodeMergeOperation(obj, boundObj);
//editor.hotTrackOnObj(boundObj);
//console.log('add hot track', boundObj.getId(), editor.getHotTrackedObjs());
//console.log('merge on', boundObj);
break;
}
}
}
if (!mergeOper)
{
editor.hotTrackOnObj(null);
//console.log('can not merge');
}
else // actually do merge
{
if (oldMergeOper)
{
this.reverseMergeOpers(objIndex);
}
this.getMergeOperations()[objIndex] = mergeOper;
//this.getPrevMergeOperations()[objIndex] = null; // avoid this oper be reversed in following old manipulation
//mergeOper.execute();
this.executeMergeOpers();
}
}
}
/*
if (mergeOper) // do magnetic merge
return;
*/
//console.log('move', objIndex, newContextCoord);
// try do mouse pointed merge
var coord = moverScreenCoord;
var merged = false;
if ((manipulateType === Kekule.Editor.BasicManipulationIaController.ManipulationType.MOVE))
{
var currObj = manipulatedObjs[0];
if (manipulatedObjs && (manipulatedObjs.length === 1)) // move object, only one obj to move, merge is possible
{
//console.log('may merge');
// check if newScreenCoord overlaps with a bound item
var boundItem = editor.getTopmostBoundInfoAtCoord(coord, [currObj]); // exclude obj, find the most top other object
if (boundItem) // overlaps on mouse position, may merge
{
var destObj = boundItem.obj;
if (destObj && (currManipulatingObjs.indexOf(destObj) < 0))
{
if (isMovingBond && (currObj instanceof Kekule.ChemStructureConnector) && (destObj instanceof Kekule.ChemStructureConnector)) // merge connector
{
if (this.getEnableConnectorMerge())
{
editor.hotTrackOnObj(destObj);
if (this.getEnableStructFragmentMerge())
merged = true;
else
merged = (currObj.getParent() === destObj.getParent());
if (merged)
{
mergeOper = this.createConnectorMergeOperation(currObj, destObj);
}
}
}
else if ((currObj instanceof Kekule.ChemStructureNode) && (destObj instanceof Kekule.ChemStructureNode)) // merge node
{
//if ((currObj.getLinkedObjs().indexOf(destObj) < 0)) // connected node can not merge
{
if (this._canMergeNodes(currObj, destObj))
{
editor.hotTrackOnObj(destObj);
if (this.isSameNodeMerge(oldMergeOper, currObj, destObj))
{
return;
}
else
{
//console.log('different merge', oldMergeOper);
}
mergeOper = this.createNodeMergeOperation(currObj, destObj);
}
}
}
if (mergeOper) // actually do merge
{
var oldMergeOper = mergeOpers[objIndex]; // important, must retrieve again, as mergeOpers[index] may be set at magnetic merge
if (oldMergeOper)
{
this.reverseMergeOpers(objIndex);
}
this.getMergeOperations()[objIndex] = mergeOper;
//this.getPrevMergeOperations()[objIndex] = null; // avoid this oper be reversed in following old manipulation
//mergeOper.execute();
this.executeMergeOpers();
return;
}
}
}
}
}
/*
else
$super(objIndex, obj, newScreenCoord, moverScreenCoord);
*/
if (!mergeOper) // no merge available
{
// hide hot track
editor.hideHotTrack();
var prevMerged = /*!this._mergeReversed &&*/ !!this.getMergeOperations().length;
//if (this.getMergeOperation() && (this.getMergeOperation() === this.getActiveOperation())) // a merge operation is created and executed before
if (prevMerged)
{
this.reverseMergeOpers(objIndex);
}
$super(objIndex, obj, newScreenCoord, moverScreenCoord);
}
},
/*
* Add a merge operation.
* @param {Kekule.Operation} mergeOper
* @private
*/
/*
addMergeOperation: function(mergeOper)
{
var oper = this.getMergeOperation();
if (!oper)
{
oper = new Kekule.MacroOperation();
this.setMergeOperation(oper);
}
oper.add(mergeOper);
},
*/
/*
* Remove an unneed merge operation.
* @param {Kekule.Operation} mergeOper
* @private
*/
/*
removeMergeOperation: function(mergeOper)
{
var oper = this.getMergeOperation();
if (oper)
{
oper.remove(mergeOper);
if (oper.getChildCount() <= 0) // empty
{
this.setMergeOperation(null);
oper.finalize();
}
}
},
*/
/*
* Returns a merge operation related with object.
* @param {Kekule.ChemObject} obj
* @returns {Kekule.Operation}
* @private
*/
/*
getMergeOperOnObject: function(obj)
{
var opers = this.getMergeOperation();
if (opers)
{
var count = opers.getChildCount();
for (var i = 0; i < count; ++i)
{
var oper = opers.getChildAt(i);
if (oper.getTarget && oper.getTarget() === obj)
return oper;
}
}
return null;
},
*/
/** @private */
createNodeMergeOperation: function(fromNode, toNode, useMergePreview)
{
var allowMolMerge = this.getEnableStructFragmentMerge();
/*
var parent = fromNode.getParent();
if ((parent !== toNode.getParent()) && !allowMolMerge) // not same parent, can not merge
return null;
*/
if (!Kekule.ChemStructOperation.MergeNodes.canMerge(fromNode, toNode, allowMolMerge, this.getEnableNeighborNodeMerge()))
return null;
else
{
//var op = new Kekule.EditorOperation.OpMergeNodes(this.getEditor(), parent, fromNode, toNode);
//var op = new Kekule.ChemStructOperation.MergeNodes(fromNode, toNode, allowMolMerge);
var op = useMergePreview?
(new Kekule.ChemStructOperation.MergeNodesPreview(fromNode, toNode, allowMolMerge, this.getEditor())):
(new Kekule.ChemStructOperation.MergeNodes(fromNode, toNode, allowMolMerge, this.getEditor()));
return op;
}
},
/** @private */
createConnectorMergeOperation: function(fromConnector, toConnector, useMergePreview)
{
var allowMolMerge = this.getEnableStructFragmentMerge();
if (!Kekule.ChemStructOperation.MergeConnectors.canMerge(fromConnector, toConnector, allowMolMerge))
return null;
else
{
//var op = new Kekule.EditorOperation.OpMergeNodes(this.getEditor(), parent, fromNode, toNode);
//var op = new Kekule.ChemStructOperation.MergeConnectors(fromConnector, toConnector, this.getEditor().getCoordMode(), allowMolMerge);
var mergeClass = useMergePreview? Kekule.ChemStructOperation.MergeConnectorsPreview: Kekule.ChemStructOperation.MergeConnectors;
var op = new mergeClass(fromConnector, toConnector, this.getEditor().getCoordMode(), allowMolMerge, this.getEditor());
return op;
}
},
/** @private */
isSameConnectorMerge: function(mergeOper, fromConnector, toConnector)
{
return mergeOper && (mergeOper instanceof Kekule.ChemStructOperation.MergeConnectorsBase)
&& (fromConnector === mergeOper.getTarget()) && (toConnector === mergeOper.getDest());
},
/** @private */
isSameNodeMerge: function(mergeOper, fromNode, toNode)
{
//console.log('check same', fromNode.getId(), toNode.getId(), mergeOper.getTarget().getId(), mergeOper.getDest().getId());
return mergeOper && (mergeOper instanceof Kekule.ChemStructOperation.MergeNodesBase)
&& (fromNode === mergeOper.getTarget()) && (toNode === mergeOper.getDest());
},
/** @private */
executeMergeOpers: function(mergeOpers)
{
//var opers = Kekule.ArrayUtils.toUnique(this.getMergeOperations());
var editor = this.getEditor();
var opers = Kekule.ArrayUtils.toUnique(mergeOpers || this.getMergeOperationsInManipulating());
var mergingDests = [];
editor.beginUpdateObject();
try
{
for (var i = 0, l = opers.length; i < l; ++i)
{
if (opers[i])
{
opers[i].execute();
var dest = opers[i].getDest ? opers[i].getDest() : null;
if (dest)
AU.pushUnique(mergingDests, dest);
}
}
this.setMergingDests(mergingDests.length ? mergingDests : null);
//console.log('[merge!!!!!]');
this.refreshManipulateObjs();
this._mergeJustReversed = false;
}
finally
{
editor.endUpdateObject();
}
},
/** @private */
reverseMergeOpers: function(mergeOperations)
{
var editor = this.getEditor();
this.setMergingDests(null);
var originOpers = mergeOperations || this.getMergeOperationsInManipulating();
var opers = Kekule.ArrayUtils.toUnique(originOpers);
//var opers = Kekule.ArrayUtils.toUnique(mergeOperations || this.getMergeOperations());
if (!opers || !opers.length)
; // do nothing
else
{
editor.beginUpdateObject();
try
{
for (var i = opers.length - 1; i >= 0; --i)
{
if (opers[i])
{
//console.log('reverse at', i, opers.length);
opers[i].reverse();
//delete opers[i];
}
}
//this._mergeReversed = true;
//this.setMergeOperations([]);
//console.log('reverse merge oper', opers);
//this.getMergeOperations().length = utilIndex;
originOpers.length = 0;
this._mergeJustReversed = true; // a special flag
//console.log('reverse', this.getManipulateObjs());
this.refreshManipulateObjs();
}
finally
{
editor.endUpdateObject();
}
}
},
/** @private */
react_pointermove: function($super, e)
{
// check if ALT key is pressed, if so, constrained move/rotate mode should be disabled
this._suppressConstrainedMoving = e.getAltKey();
this._suppressConstrainedRotating = e.getAltKey();
this._isInDirectedMoving = e.getShiftKey();
return $super(e);
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.BasicMolManipulationIaController, Kekule.Editor.ChemSpaceEditor);
/**
* IA Controller to select and manipulate objects in editor.
* @class
* @augments Kekule.Editor.BasicMolManipulationIaController
*/
Kekule.Editor.SelectIaController = Class.create(Kekule.Editor.BasicMolManipulationIaController,
/** @lends Kekule.Editor.SelectIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.SelectIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setEnableSelect(true);
this.setEnableGestureManipulation(true);
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.SelectIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Base IA Controller to insert structure objects (bonds, structure fragments...) into editor.
* @class
* @augments Kekule.Editor.BasicMolManipulationIaController
*/
Kekule.Editor.StructureInsertIaController = Class.create(Kekule.Editor.BasicMolManipulationIaController,
/** @lends Kekule.Editor.StructureInsertIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.StructureInsertIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this._manipulatedBasicObjs = null; // used internally
},
/**
* Returns newly inserted objects to editor.
* Descendants may override this method.
* @private
*/
getInsertedObjs: function()
{
return this._manipulatedBasicObjs;
},
/** @ignore */
doSetManipulateOriginObjs: function($super, objs)
{
this._manipulatedBasicObjs = this._getManipulatedBasicObjects(objs);
return $super(objs);
},
/** @private */
_getManipulatedBasicObjects: function(manipulatingObjs)
{
var result = [];
for (var i = 0, l = manipulatingObjs.length; i < l; ++i)
{
var obj = manipulatingObjs[i];
if (obj instanceof Kekule.ChemStructureObject)
{
if (obj instanceof Kekule.StructureFragment && obj.isExpanded())
{
var children = [].concat(obj.getNodes()).concat(obj.getConnectors());
AU.pushUnique(result, children);
}
else
AU.pushUnique(result, obj);
}
else
AU.pushUniqueEx(result, obj);
}
return result;
},
/** @ignore */
stopManipulate: function($super)
{
if (this.getEditorConfigs().getInteractionConfigs().getAutoSelectNewlyInsertedObjects())
{
var basicObjs = this.getInsertedObjs();
this.doneInsertOrModifyBasicObjects(basicObjs);
//console.log(basicObjs.length, filteredObjs.length);
}
return $super();
}
});
/**
* Controller to add bond or change bond property.
* @class
* @augments Kekule.Editor.StructureInsertIaController
*
* @property {Bool} allowBondingToBond Whether bond-bond connection (e.g., in Zeise salt) is allowed.
* @property {Bool} enableBondModification Whether modification existing bond is enabled.
* @property {Bool} autoSwitchBondOrder If true, click on bond will switch bond order between single, double and triple.
* @property {Kekule.ChemStructureObject} startingObj
* @property {Kekule.ChemStructureObject} endingObj
* @property {String} bondType
* @property {Int} bondOrder
* @property {Int} bondStereo
* //@property {Bool} autoCreateNewStructFragment Whether a new molecule is created when a bond appointed to a blank space in editor.
*/
Kekule.Editor.MolBondIaController = Class.create(Kekule.Editor.StructureInsertIaController,
/** @lends Kekule.Editor.MolBondIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolBondIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setState(BC.State.INITIAL);
this.setBondOrder(Kekule.BondOrder.SINGLE); // default is single bond
this.setAllowBondingToBond(false);
this.setEnableBondModification(true);
this.setEnableSelect(false);
this.setEnableMove(true);
//this.setEnableRemove(false);
this.setAutoSwitchBondOrder(false);
this.setEnableNeighborNodeMerge(false);
},
/** @private */
initProperties: function()
{
this.defineProp('allowBondingToBond', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('state', {'dataType': DataType.INT, 'serializable': false});
this.defineProp('enableBondModification', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('autoSwitchBondOrder', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('startingObj', {'dataType': 'Kekule.ChemStructureObject', 'serializable': false});
this.defineProp('endingObj', {'dataType': 'Kekule.ChemStructureObject', 'serializable': false});
this.defineProp('structFragment', {'dataType': 'Kekule.ChemStructureFragment', 'serializable': false});
this.defineProp('autoCreatedStructFragment', {'dataType': 'Kekule.StructureFragment', 'serializable': false});
this.defineProp('autoCreatedStartingObj', {'dataType': 'Kekule.ChemStructureObject', 'serializable': false});
this.defineProp('bond', {'dataType': 'Kekule.Bond', 'serializable': false});
this.defineProp('bondType', {'dataType': DataType.STRING, 'serializable': false});
this.defineProp('bondOrder', {'dataType': DataType.INT, 'serializable': false});
this.defineProp('bondStereo', {'dataType': DataType.INT, 'serializable': false});
//this.defineProp('autoCreateNewStructFragment', {'dataType': DataType.BOOL});
this.defineProp('initialBondDirection', {'dataType': DataType.FLOAT});
},
/** @private */
_isBondDifferent: function(bond)
{
var stereo = this.getBondStereo();
var BS = Kekule.BondStereo;
if (stereo === BS.UP || stereo === BS.UP_INVERTED
|| stereo === BS.DOWN || stereo === BS.DOWN_INVERTED) // can swith between normal and inverted
return true;
else
return (bond.getBondType() !== this.getBondType())
|| (bond.getBondOrder() !== this.getBondOrder())
|| (bond.getStereo() !== this.getBondStereo());
},
/** @private */
canInteractWithObj: function($super, obj)
{
var state = this.getState();
//console.log(state, BC.State.INITIAL);
if (state === BC.State.INITIAL)
{
if (obj instanceof Kekule.ChemStructureNode)
return true;
else if (obj instanceof Kekule.ChemStructureConnector)
{
if (this.getAllowBondingToBond())
return true;
else if (this.getEnableBondModification())
{
if (obj instanceof Kekule.Bond)
{
/*
//console.log(obj.getBondOrder(), this.getBondOrder());
return this.getAutoSwitchBondOrder()
|| (obj.getBondOrder() !== this.getBondOrder());
*/
return this._isBondDifferent(obj);
}
}
else return false;
}
}
else
return $super(obj);
},
/** @ignore */
doTestMouseCursor: function(coord, e)
{
return ''; // do not change mouse cursor
},
/** @ignore */
getInsertedObjs: function($super)
{
var bond = this.getBond();
return bond? [bond]: $super();
},
/**
* Check if a object can be a valid starting or ending point of bond.
* @private
*/
isObjValidBondTerminator: function(obj)
{
if (!obj)
return false;
if (!this.getAllowBondingToBond()) // allow only node to be a starting/ending point
return (obj instanceof Kekule.ChemStructureNode);
else // allow bond-bond connection, every object can be a starting/ending point
return ((obj instanceof Kekule.ChemStructureNode) || (obj instanceof Kekule.ChemStructureConnector));
},
/**
* Check if an object is a modifiable bond.
* @param {Object} obj
*/
isBond: function(obj)
{
return (obj instanceof Kekule.Bond);
},
/** @private */
getNewBondDefAngle: function(startObj, newBondOrder)
{
var structConfig = this.getEditorConfigs().getStructureConfigs();
/*
var result;
var surroundingObjs = Kekule.Editor.StructureUtils.getSurroundingObjs(startObj);
if (surroundingObjs.length === 1) // one existing bond, defAngle is decided by new bond order and existing bond order
{
var existingConnector = startObj.getLinkedConnectorAt(0);
if (existingConnector && (existingConnector.getConnectedObjs().indexOf(surroundingObjs[0]) >= 0))
{
var existingBondOrder = existingConnector.getBondOrder? existingConnector.getBondOrder(): null;
if (Kekule.ObjUtils.notUnset(existingBondOrder))
result = structConfig.getDefAngleOfBonds(newBondOrder, existingBondOrder);
//result = Math.max(result, structConfig.getDefAngleOfBond(existingBondOrder));
}
}
else if (surroundingObjs.length === 0) // no connected bond, use initialDirection
result = structConfig.getInitialBondDirection();
else
result = structConfig.getDefAngleOfBonds(newBondOrder, 0);
return result;
*/
return structConfig.getNewBondDefAngle(startObj, newBondOrder);
},
/** @private */
addDefBond: function(startCoord, startObj, notifyEditor)
{
var result;
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
if (!startObj) // add bond from blank, create new node or growing existing blank molecule
{
//if (!this.getEditor().getAutoCreateNewStructFragment())
if (!this.getEditor().canAddUnconnectedStructFragment())
return null;
var objCoord = this.getEditor().screenCoordToObj(startCoord);
var baseMol = this.getEditor().getOnlyOneBlankStructFragment();
if (!baseMol) // totally blank space
{
startObj = this.getEditor().createNewStructFragmentAndStartingAtom(null, objCoord);
this.setAutoCreatedStartingObj(null);
this.setAutoCreatedStructFragment(startObj? startObj.getParent(): null);
}
else // create on base Mol
{
startObj = this.getEditor().createStructStartingAtom(baseMol, null, objCoord);
this.setAutoCreatedStructFragment(null);
this.setAutoCreatedStartingObj(startObj);
}
}
else
{
this.setAutoCreatedStartingObj(null);
this.setAutoCreatedStructFragment(null);
}
if (this.isObjValidBondTerminator(startObj))
{
var editor = this.getEditor();
// set starting point
this.setStartingObj(startObj);
var parent = startObj.getParent();
this.setStructFragment(parent); // IMPORTANT, save current parent, as molecule may be merged during operation
var structConfigs = this.getEditorConfigs().getStructureConfigs();
// add a bond at preferred position
var bondType = Kekule.oneOf(this.getBondType(), structConfigs.getDefBondType());
var bondOrder = Kekule.oneOf(this.getBondOrder(), structConfigs.getDefBondOrder());
var bondStereo = Kekule.oneOf(this.getBondStereo(), Kekule.BondStereo.NONE);
//var bondLength = this.getEditor().getDefBondLength();
var bondLength = editor.getDefBondLength? editor.getDefBondLength(): structConfigs.getDefBondLength();
var defbondAngle = this.getNewBondDefAngle(startObj, bondOrder); //structConfigs.getDefAngleOfBond(bondOrder);
var endCoord = Kekule.Editor.StructureUtils.calcPreferred2DBondGrowingLocation(startObj, bondLength, defbondAngle, this.getEditor().getAllowCoordBorrow());
//console.log('add bond', bondLength, startCoord, objCoord, endCoord);
// create a node and a new bond
var node = this.getEditor().createDefaultNode(endCoord, Kekule.Editor.CoordSys.OBJ, parent);
//parent.appendNode(node);
this.setEndingObj(node);
var bond = new Kekule.Bond();
bond.setBondType(bondType);
bond.setBondOrder(bondOrder);
bond.setStereo(bondStereo);
parent.appendConnector(bond);
bond.appendConnectedObj(startObj);
bond.appendConnectedObj(node);
this.setBond(bond);
if (notifyEditor)
{
this.getEditor().objectsChanged([{'obj': bond}, {'obj': node}]);
}
result = endCoord;
}
else
result = null;
}
finally
{
editor.endUpdateObject();
}
return result;
},
/** @private */
addOperationToEditor: function($super)
{
/*
if (this.getAllManipulateObjsMerged())
return null;
else
*/
return $super();
},
/** @ignore */
getAllObjOperations: function($super, isTheFinalOperationToEditor)
{
var result = $super(isTheFinalOperationToEditor) || [];
var op = this.getAddBondOperation();
if (op)
result.unshift(op);
return result;
},
/** @private */
getAddBondOperation: function()
{
var group = new Kekule.MacroOperation();
// new add node / bond operation
var editor = this.getEditor();
var mol = this.getAutoCreatedStructFragment();
if (mol)
{
var addMolOperation = new Kekule.ChemObjOperation.Add(mol, this.getEditor().getChemObj(), null, this.getEditor());
group.add(addMolOperation);
}
var startObj = this.getStartingObj();
var endObj = this.getEndingObj();
var bond = this.getBond();
//var parent = startObj.getParent();
var parent = this.getStructFragment();
var editor = this.getEditor();
var node = this.getAutoCreatedStartingObj();
if (node)
{
var addNodeOperation = new Kekule.ChemStructOperation.AddNode(node, parent, null, editor);
group.add(addNodeOperation);
}
var addNodeOperation = new Kekule.ChemStructOperation.AddNode(endObj, parent, null, editor);
group.add(addNodeOperation);
var addConnectorOperation = new Kekule.ChemStructOperation.AddConnector(bond, parent, null, [startObj, endObj], editor);
group.add(addConnectorOperation);
return group;
},
/* @private */
/*
wrapAddBondOperation: function()
{
if (this.getAllManipulateObjsMerged()) // bond been merged into old molecule, do not need to add operation
{
return null;
}
var moveOperation = this.getActiveOperation(); // move operation
var group = new Kekule.MacroOperation();
// new add node / bond operation
var editor = this.getEditor();
var mol = this.getAutoCreatedStructFragment();
if (mol)
{
var addMolOperation = new Kekule.ChemObjOperation.Add(mol, this.getEditor().getChemObj(), null);
group.add(addMolOperation);
}
var startObj = this.getStartingObj();
var endObj = this.getEndingObj();
var bond = this.getBond();
//var parent = startObj.getParent();
var parent = this.getStructFragment();
var addNodeOperation = new Kekule.ChemStructOperation.AddNode(endObj, parent);
group.add(addNodeOperation);
var addConnectorOperation = new Kekule.ChemStructOperation.AddConnector(bond, parent, null, [startObj, endObj]);
group.add(addConnectorOperation);
if (moveOperation)
group.add(moveOperation);
//this.setActiveOperation(group);
return group;
},
*/
/**
* Change property of an existing bond
* @param {Kekule.Bond} bond
*/
modifyBond: function(bond)
{
var bondType = this.getBondType();
// change bond order
var bondOrder;
var BO = Kekule.BondOrder;
var loopBondOrders = [BO.SINGLE, BO.DOUBLE, BO.TRIPLE];
if (this.getAutoSwitchBondOrder() && (bondType === Kekule.BondType.COVALENT) && (bond.getBondType === Kekule.BondType.COVALENT))
{
var oldOrder = bond.getBondOrder();
var index = loopBondOrders.indexOf(oldOrder);
if (index >= 0)
{
index = (++index) % loopBondOrders.length;
}
else
index = 0;
bondOrder = loopBondOrders[index];
}
else
bondOrder = this.getBondOrder();
var oldBondStereo = bond.getStereo();
var bondStereo = this.getBondStereo();
if (oldBondStereo === bondStereo) // need switch between normal and inv
{
bondStereo = Kekule.BondStereo.getInvertedDirection(bondStereo);
//console.log('inv stereo', oldBondStereo, bondStereo);
}
var newPropValues = {'bondType': bondType, 'bondOrder': bondOrder, 'stereo': bondStereo};
var needModify = false;
for (var prop in newPropValues) // check if bond is really need to be changed
{
var oldValue = bond.getPropValue(prop);
if (oldValue !== newPropValues[prop]) // need change
{
needModify = true;
break;
}
}
if (needModify)
{
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
// create operation
//var oper = new Kekule.EditorOperation.OpModifyConnector(this.getEditor(), bond.getParent(), bond, newPropValues);
var oper = new Kekule.ChemObjOperation.Modify(bond, newPropValues, this.getEditor());
oper.execute();
editor.pushOperation(oper);
// notify editor, the connected objecs should be redrawn too
var changedObjs = [bond].concat(bond.getConnectedObjs());
var changedDetails = [];
for (var i = 0, l = changedObjs.length; i < l; ++i)
changedDetails.push({'obj':changedObjs[i]});
this.getEditor().objectsChanged(changedDetails);
}
finally
{
editor.endUpdateObject();
}
}
},
/** @private */
react_pointerdown: function(e)
{
if (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT)
{
var S = BC.State;
var coord = this._getEventMouseCoord(e);
var state = this.getState();
if (state === S.INITIAL)
{
var boundItem = this.getEditor().getTopmostBoundInfoAtCoord(coord, null, this.getCurrBoundInflation());
if (boundItem)
{
var obj = boundItem.obj;
if (this.isObjValidBondTerminator(obj))
{
var endCoord = this.addDefBond(coord, obj, true);
//endCoord = this.getEditor().objCoordToContext(endCoord);
//console.log('add bond end with', endCoord, coord);
if (endCoord) // add bond success
{
// then manipulate the bond
this.startDirectManipulate(null, this.getEndingObj(), coord);
this.moveManipulatedObjs(coord); // force a "move" action, to apply possible merge
}
return true; // important
}
else if (this.isBond(obj) && this.getEnableBondModification()) // change bond property
{
this.modifyBond(obj);
return true;
}
e.preventDefault();
}
else if (this.getEditor().canAddUnconnectedStructFragment()) // click on a blank area, add new molecule if needed
{
/*
var objCoord = this.getEditor().screenCoordToObj(coord);
var startNode = this.getEditor().createNewStructFragmentAndStartingAtom(null, objCoord);
if (startNode && this.isObjValidBondTerminator(startNode))
{
var endCoord = this.addDefBond(startNode, true);
//endCoord = this.getEditor().objCoordToContext(endCoord);
//console.log('add bond end with', endCoord, coord);
// then manipulate the bond
this.startDirectManipulate(this.getEndingObj(), coord);
return true; // important
}
*/
var endCoord = this.addDefBond(coord, null, true);
if (endCoord) // add successfully
{
//this.setManipulateOriginObjs(obj);
this.startDirectManipulate(null, this.getEndingObj(), coord);
this.moveManipulatedObjs(coord); // force a "move" action, to apply possible merge
}
e.preventDefault();
return true; // important
}
}
}
},
/** @private */
react_pointerup: function($super, e)
{
var state = this.getState();
var startCoord = this.getStartCoord();
var endCoord = this._getEventMouseCoord(e);
var S = Kekule.Editor.BasicManipulationIaController.State;
if ((state === S.MANIPULATING) && (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT))
{
e.preventDefault();
if (Kekule.CoordUtils.isEqual(startCoord, endCoord)) // click
{
// wrap up, add bond and append operation
this.addOperationToEditor();
this.stopManipulate();
this.setState(S.NORMAL);
return true;
}
}
return $super(e); // finish move operation first;
}
});
/**
* Enumeration of state of a {@link Kekule.Editor.MolBondIaController}.
* Currently not used.
* @class
*/
Kekule.Editor.MolBondIaController.State = {
/** Normal state. */
INITIAL: 0,
/** A starting node is set. */
START_NODE_SET: 21,
/** Starting node set and is now adjust bond direction and ending node */
ADJUSTING: 22
};
var BC = Kekule.Editor.MolBondIaController;
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.MolBondIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to set atom property.
* @class
* @augments Kekule.Editor.BaseEditorIaController
*/
Kekule.Editor.MolAtomIaController_OLD = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.MolAtomIaController_OLD# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolAtomIaController_OLD',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this._createNonAtomLabelInfos();
this._setterShown = false; // user internally
},
finalize: function($super)
{
if (this.getAtomSetter())
this.getAtomSetter().finalize();
$super();
},
/** @private */
initProperties: function()
{
this.defineProp('currAtom', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('atomSetter', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('periodicTable', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('periodicTableDialog', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('nonAtomLabelInfos', {'dataType': DataType.ARRAY, 'serializable': false}); // private
},
/** @private */
canInteractWithObj: function($super, obj)
{
if (this.isValidNode(obj))
return true;
else
return false;
},
/**
* Check if obj is a valid chem node and can be edited.
* @param {Kekule.ChemObject} obj
* @returns {Bool}
* @private
*/
isValidNode: function(obj)
{
return obj instanceof Kekule.ChemStructureNode;
},
/**
* Returns label that shows in node edit.
* @param {Kekule.ChemStructureNode} node
* @returns {String}
* @private
*/
getNodeLabel: function(node)
{
var editor = this.getEditor();
var labelConfigs = editor.getRenderConfigs().getDisplayLabelConfigs();
if (node.getIsotopeId) // atom
return node.getIsotopeId();
else if (node instanceof Kekule.SubGroup)
{
var groupLabel = node.getAbbr() || node.getFormulaText();
return groupLabel || labelConfigs.getRgroup();
}
else if (node instanceof Kekule.VariableAtom) // return 'L' rather than actual atom list to activate element table
{
var allowedIds = node.getAllowedIsotopeIds();
var disallowedIds = node.getDisallowedIsotopeIds();
return (allowedIds && allowedIds.length)? this._getVarAtomListLabel():
(disallowedIds && disallowedIds.length)? this._getVarAtomNotListLabel():
this._getVarAtomListLabel();
}
else
{
var ri = node.getCoreDisplayRichTextItem(null, null, labelConfigs);
return Kekule.Render.RichTextUtils.toText(ri);
}
},
/** @private */
_getVarAtomListLabel: function()
{
var labelConfigs = this.getEditor().getRenderConfigs().getDisplayLabelConfigs();
return labelConfigs.getVariableAtom();
},
_getVarAtomNotListLabel: function()
{
var labelConfigs = this.getEditor().getRenderConfigs().getDisplayLabelConfigs();
return '~' + labelConfigs.getVariableAtom();
},
/** @private */
_createNonAtomLabelInfos: function()
{
var result = [];
var labelConfigs = this.getEditor().getRenderConfigs().getDisplayLabelConfigs();
// R group
result.push({
'nodeLabel': labelConfigs.getRgroup(), 'nodeClass': Kekule.RGroup,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_RGROUP') //Kekule.ChemWidgetTexts.CAPTION_RGROUP
});
// Kekule.Pseudoatom
result.push({
'nodeLabel': labelConfigs.getDummyAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.DUMMY},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_DUMMY_ATOM') //Kekule.ChemWidgetTexts.CAPTION_DUMMY_ATOM
});
result.push({
'nodeLabel': labelConfigs.getHeteroAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.HETERO},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_HETERO_ATOM') //Kekule.ChemWidgetTexts.CAPTION_HETERO_ATOM
});
result.push({
'nodeLabel': labelConfigs.getAnyAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.ANY},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_ANY_ATOM') //Kekule.ChemWidgetTexts.CAPTION_ANY_ATOM
});
// Kekule.VariableAtom List and Not List
result.push({
'nodeLabel': this._getVarAtomListLabel(), 'nodeClass': Kekule.VariableAtom,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_VARIABLE_ATOM') //Kekule.ChemWidgetTexts.CAPTION_VARIABLE_ATOM
});
result.push({
'nodeLabel': this._getVarAtomNotListLabel(), 'nodeClass': Kekule.VariableAtom,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_VARIABLE_NOT_ATOM') //Kekule.ChemWidgetTexts.CAPTION_VARIABLE_NOT_ATOM
});
this.setNonAtomLabelInfos(result);
return result;
},
/** @private */
_indexOfNonAtomLabel: function(nodeLabel)
{
var infos = this.getNonAtomLabelInfos();
for (var i = 0, l = infos.length; i < l; ++i)
{
var info = infos[i];
if (info.nodeLabel === nodeLabel)
return i;
}
return -1;
},
/** @private */
_getNonAtomInfo: function(nodeLabel)
{
var index = this._indexOfNonAtomLabel(nodeLabel);
return (index < 0)? null: this.getNonAtomLabelInfos()[index];
},
/** @private */
getAtomSetterWidget: function(canCreate)
{
var result = this.getAtomSetter();
if (!result && canCreate) // create new one
{
var parentElem = this.getEditor().getCoreElement();
var doc = parentElem.ownerDocument;
result = this._createAtomSetterWidget(doc, parentElem);
this.setAtomSetter(result);
}
return result;
},
/** @private */
_createAtomSetterWidget: function(doc, parentElem)
{
var result = new Kekule.Widget.ComboBox(doc);
var listAtoms = this.getEditor().getEditorConfigs().getStructureConfigs().getPrimaryOrgChemAtoms();
var listItems = [];
// add ususal atoms
for (var i = 0, l = listAtoms.length; i < l; ++i)
{
listItems.push({'value': listAtoms[i], 'data': {'props': {'isotopeId': listAtoms[i]}}});
}
// add "open periodic table" option
listItems.push({'value': Kekule.$L('ChemWidgetTexts.CAPTION_ATOMLIST_PERIODIC_TABLE') /*CWT.CAPTION_ATOMLIST_PERIODIC_TABLE*/});
// non-atom nodes
var nonAtomLabelInfos = this.getNonAtomLabelInfos();
for (var i = 0, l = nonAtomLabelInfos.length; i < l; ++i)
{
var info = nonAtomLabelInfos[i];
var listItem = {'value': info.nodeLabel, 'data': {'props': info.props}};
if (info.description)
listItem.text = info.nodeLabel + ' - ' + info.description;
listItems.push(listItem);
}
result.setItems(listItems);
result.appendToElem(parentElem);
// event handler
var self = this;
result.addEventListener('keyup', function(e)
{
var ev = e.htmlEvent;
if (ev.getKeyCode() === Kekule.X.Event.KeyCode.ENTER)
{
self.applySetter(result);
result.dismiss(); // avoid call apply setter twice
}
}
);
result.addEventListener('valueSelect', function(e)
{
self.applySetter(result);
result.dismiss(); // avoid call apply setter twice
}
);
result.addEventListener('showStateChange', function(e)
{
if (!e.isShown && !e.isDismissed) // widget hidden, feedback the edited value
{
if (self.getAtomSetter() && self.getAtomSetter().isShown())
self.applySetter(result);
}
}
);
return result;
},
/** @private */
getPeriodicTableDialogWidget: function(canCreate)
{
var result = this.getPeriodicTableDialog();
if (!result && canCreate) // create new one
{
var parentElem = this.getEditor().getCoreElement();
var doc = parentElem.ownerDocument;
result = this._createPeriodicTableDialogWidget(doc, parentElem);
this.setPeriodicTableDialog(result);
}
//console.log(result);
return result;
},
/** @private */
_createPeriodicTableDialogWidget: function(doc, parentElem)
{
var dialog = new Kekule.Widget.Dialog(doc, Kekule.$L('ChemWidgetTexts.CAPTION_PERIODIC_TABLE_DIALOG') /*CWT.CAPTION_PERIODIC_TABLE_DIALOG*/,
[Kekule.Widget.DialogButtons.OK, Kekule.Widget.DialogButtons.CANCEL]
);
var table = new Kekule.ChemWidget.PeriodicTable(doc);
table.setUseMiniMode(true);
table.setEnableSelect(true);
table.appendToElem(dialog.getClientElem());
this.setPeriodicTable(table);
return dialog;
},
/**
* Open atom edit box for obj in coord.
* @param {Hash} coord
* @param {Object} obj
*/
openSetterUi: function(coord, obj)
{
var oldSetter = this.getAtomSetter();
/*
if (oldSetter && oldSetter.isShown()) // has a old setter
{
this.applySetter(oldSetter, this.getCurrAtom());
}
*/
if (!this.isValidNode(obj))
return;
this.setCurrAtom(obj);
var fontSize = this.getEditor().getEditorConfigs().getInteractionConfigs().getAtomSetterFontSize() || 0;
var posAdjust = fontSize / 1.5; // adjust position to align to atom center
var slabel = this.getNodeLabel(obj);
var setter = this.getAtomSetterWidget(true);
setter.setValue(slabel);
setter.setIsDirty(false);
//setter.setIsPopup(true);
var style = setter.getElement().style;
style.position = 'absolute';
style.fontSize = fontSize;
style.left = (coord.x - posAdjust) + 'px';
style.top = (coord.y - posAdjust) + 'px';
/*
style.marginTop = -posAdjust + 'px';
style.marginLeft = -posAdjust + 'px';
*/
//setter.show();
setter._applied = false;
setter.show(null, null, Kekule.Widget.ShowHideType.POPUP);
(function(){
setter.focus();
setter.selectAll();
}).defer();
//result.selectAll.bind(result).defer();
/*
setter.selectAll();
setter.focus();
*/
},
/** @private */
applySetter: function(setter, atom)
{
if (setter._applied) // avoid called twice
return;
if (!setter.getIsDirty()) // setter not modified
return;
if (!atom)
atom = this.getCurrAtom();
var nodeClass;
var modifiedProps = null;
//var isNonAtom = false;
var text = setter.getValue();
//console.log('value', text);
// check if setter list need to raise other widget
var periodicTableDialog;
if (text === Kekule.$L('ChemWidgetTexts.CAPTION_ATOMLIST_PERIODIC_TABLE')/*CWT.CAPTION_ATOMLIST_PERIODIC_TABLE*/) // open periodic table to select atom
{
var periodicTableDialog = this.getPeriodicTableDialogWidget(true);
periodicTableDialog.setCaption(/*CWT.CAPTION_PERIODIC_TABLE_DIALOG_SEL_ELEM*/Kekule.$L('ChemWidgetTexts.CAPTION_PERIODIC_TABLE_DIALOG_SEL_ELEM'));
var currSymbol = atom.getSymbol? atom.getSymbol(): null;
this.getPeriodicTable().setEnableSelect(true).setEnableMultiSelect(false).setSelectedSymbol(currSymbol);
}
else if (text === this._getVarAtomListLabel() || text === this._getVarAtomNotListLabel()) // select list of atoms
{
var notList = text === this._getVarAtomNotListLabel();
var periodicTableDialog = this.getPeriodicTableDialogWidget(true);
periodicTableDialog.setCaption(/*CWT.CAPTION_PERIODIC_TABLE_DIALOG_SEL_ELEMS*/Kekule.$L('ChemWidgetTexts.CAPTION_PERIODIC_TABLE_DIALOG_SEL_ELEM'));
var allowedSymbols = atom.getAllowedIsotopeIds? atom.getAllowedIsotopeIds(): null;
var disallowedSymbols = atom.getDisallowedIsotopeIds? atom.getDisallowedIsotopeIds(): null;
this.getPeriodicTable().setEnableSelect(true).setEnableMultiSelect(true)
.setSelectedSymbols(notList? disallowedSymbols: allowedSymbols);
}
if (periodicTableDialog)
{
var self = this;
periodicTableDialog.openModal(function(result)
{
if (result === Kekule.Widget.DialogButtons.OK)
{
var nodeClass;
var modifiedProps;
if (text === Kekule.$L('ChemWidgetTexts.CAPTION_ATOMLIST_PERIODIC_TABLE')/*CWT.CAPTION_ATOMLIST_PERIODIC_TABLE*/) // select single atom
{
var symbol = self.getPeriodicTable().getSelectedSymbol();
nodeClass = Kekule.Atom;
modifiedProps = {'isotopeId': symbol};
}
if (text === self._getVarAtomListLabel())
{
var symbols = self.getPeriodicTable().getSelectedSymbols();
nodeClass = Kekule.VariableAtom;
modifiedProps = {'allowedIsotopeIds': symbols, 'disallowedIsotopeIds': null};
}
if (text === self._getVarAtomNotListLabel())
{
var symbols = self.getPeriodicTable().getSelectedSymbols();
nodeClass = Kekule.VariableAtom;
modifiedProps = {'allowedIsotopeIds': null, 'disallowedIsotopeIds': symbols};
}
self.applyModification(atom, null, nodeClass, modifiedProps);
}
}, this
);
return;
}
var newNode = null;
var nonAtomInfo = this._getNonAtomInfo(text);
if (nonAtomInfo) // is not an atom
{
nodeClass = nonAtomInfo.nodeClass;
modifiedProps = nonAtomInfo.props;
//isNonAtom = true;
}
else
{
// check if it is predefined subgroups first
var subGroupRepositoryItem = Kekule.Editor.StoredSubgroupRepositoryItem2D.getRepItemOfInputText(text);
if (subGroupRepositoryItem) // add subgroup
{
var repResult = subGroupRepositoryItem.createObjects(atom) || {};
var repObjects = repResult.objects;
var transformParams = Kekule.Editor.RepositoryStructureUtils.calcRepObjInitialTransformParams(this.getEditor(), subGroupRepositoryItem, repResult, atom, null);
this.getEditor().transformCoordAndSizeOfObjects(repObjects, transformParams);
newNode = repObjects[0];
nodeClass = newNode.getClass();
}
else // add normal node
{
nodeClass = Kekule.ChemStructureNodeFactory.getClassByLabel(text, null); // explicit set defaultClass parameter to null
if (!nodeClass)
{
if (this.getEditorConfigs().getInteractionConfigs().getAllowUnknownAtomSymbol())
nodeClass = Kekule.Pseudoatom;
}
modifiedProps = (nodeClass === Kekule.Atom) ? {'isotopeId': text} :
(nodeClass === Kekule.Pseudoatom) ? {'symbol': text} :
{};
}
}
if (!nodeClass)
{
Kekule.error(Kekule.$L('ErrorMsg.INVALID_ATOM_SYMBOL'));
}
else
{
this.applyModification(atom, newNode, nodeClass, modifiedProps);
setter._applied = true;
}
},
/**
* Save changes to current edited node.
* @param node
* @param newNodeClass
* @param modifiedProps
* @private
*/
applyModification: function(node, newNode, newNodeClass, modifiedProps)
{
var newNode;
var operGroup, oper;
var oldNodeClass = node.getClass();
if (newNode && !newNodeClass)
newNodeClass = newNode.getClass();
if (newNode || newNodeClass !== oldNodeClass) // need to replace node
{
operGroup = new Kekule.MacroOperation();
if (!newNode)
newNode = new newNodeClass();
var tempNode = new Kekule.ChemStructureNode();
tempNode.assign(node);
newNode.assign(tempNode); // copy some basic info of old node
var operReplace = new Kekule.ChemStructOperation.ReplaceNode(node, newNode, null, this.getEditor());
operGroup.add(operReplace);
}
else // no need to replace
newNode = node;
if (modifiedProps)
{
oper = new Kekule.ChemObjOperation.Modify(newNode, modifiedProps, this.getEditor());
if (operGroup)
operGroup.add(oper);
}
var operation = operGroup || oper;
if (operation) // only execute when there is real modification
{
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
operation.execute();
}
catch (e)
{
//Kekule.error(/*Kekule.ErrorMsg.NOT_A_VALID_ATOM*/Kekule.$L('ErrorMsg.NOT_A_VALID_ATOM'));
throw(e);
}
finally
{
editor.endUpdateObject();
}
if (editor && editor.getEnableOperHistory() && operation)
{
editor.pushOperation(operation);
}
}
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT)
{
this.getEditor().setSelection(null);
var coord = this._getEventMouseCoord(e);
{
var boundItem = this.getEditor().getTopmostBoundInfoAtCoord(coord, null, this.getCurrBoundInflation());
if (boundItem)
{
var obj = boundItem.obj;
if (this.isValidNode(obj)) // can modify atom of this object
{
var baseCoord = this.getEditor().getObjectScreenCoord(obj);
e.preventDefault();
e.stopPropagation();
// important, prevent event bubble to document, otherwise reactDocumentClick will be evoked
// and the atom setter will be closed immediately.
this.openSetterUi(baseCoord, obj);
this.getEditor().setSelection([obj]);
}
return true; // important
}
}
}
}
});
/**
* Controller to set atom property.
* @class
* @augments Kekule.Editor.BaseEditorIaController
*/
Kekule.Editor.MolAtomIaController = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.MolAtomIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolAtomIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this._createNonAtomLabelInfos();
this._setterShown = false; // user internally
},
finalize: function($super)
{
if (this.getAtomSetter())
this.getAtomSetter().finalize();
$super();
},
/** @private */
initProperties: function()
{
this.defineProp('currAtom', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('atomSetter', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('nonAtomLabelInfos', {'dataType': DataType.ARRAY, 'serializable': false}); // private
},
/** @private */
canInteractWithObj: function($super, obj)
{
if (this.isValidNode(obj))
return true;
else
return false;
},
/**
* Check if obj is a valid chem node and can be edited.
* @param {Kekule.ChemObject} obj
* @returns {Bool}
* @private
*/
isValidNode: function(obj)
{
return (obj instanceof Kekule.ChemStructureNode) && !(obj instanceof Kekule.StructureFragment && obj.isStandalone());
},
/** @private */
/*
_getVarAtomListLabel: function()
{
var labelConfigs = this.getEditor().getRenderConfigs().getDisplayLabelConfigs();
return labelConfigs? labelConfigs.getVariableAtom(): Kekule.ChemStructureNodeLabels.VARIABLE_ATOM;
},
_getVarAtomNotListLabel: function()
{
var labelConfigs = this.getEditor().getRenderConfigs().getDisplayLabelConfigs();
return '~' + (labelConfigs? labelConfigs.getVariableAtom(): Kekule.ChemStructureNodeLabels.VARIABLE_ATOM);
},
*/
/** @private */
_createNonAtomLabelInfos: function()
{
/*
var result = [];
var labelConfigs = this.getEditor().getRenderConfigs().getDisplayLabelConfigs();
// R group
result.push({
'text': labelConfigs.getRgroup(), 'nodeClass': Kekule.RGroup,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_RGROUP') //Kekule.ChemWidgetTexts.CAPTION_RGROUP
});
// Kekule.Pseudoatom
result.push({
'text': labelConfigs.getDummyAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.DUMMY},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_DUMMY_ATOM') //Kekule.ChemWidgetTexts.CAPTION_DUMMY_ATOM
});
result.push({
'text': labelConfigs.getHeteroAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.HETERO},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_HETERO_ATOM') //Kekule.ChemWidgetTexts.CAPTION_HETERO_ATOM
});
result.push({
'text': labelConfigs.getAnyAtom(), 'nodeClass': Kekule.Pseudoatom,
'props': {'atomType': Kekule.PseudoatomType.ANY},
'description': Kekule.$L('ChemWidgetTexts.CAPTION_ANY_ATOM') //Kekule.ChemWidgetTexts.CAPTION_ANY_ATOM
});
// Kekule.VariableAtom List and Not List
result.push({
'text': this._getVarAtomListLabel(), 'nodeClass': Kekule.VariableAtom, 'isVarList': true,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_VARIABLE_ATOM') //Kekule.ChemWidgetTexts.CAPTION_VARIABLE_ATOM
});
result.push({
'text': this._getVarAtomNotListLabel(), 'nodeClass': Kekule.VariableAtom, 'isVarNotList': true,
'description': Kekule.$L('ChemWidgetTexts.CAPTION_VARIABLE_NOT_ATOM') //Kekule.ChemWidgetTexts.CAPTION_VARIABLE_NOT_ATOM
});
*/
var editor = this.getEditor();
var result = editor && editor.getEnabledNonAtomInputData && editor.getEnabledNonAtomInputData();
this.setNonAtomLabelInfos(result);
return result;
},
/** @private */
_indexOfNonAtomLabel: function(nodeLabel)
{
var infos = this.getNonAtomLabelInfos();
for (var i = 0, l = infos.length; i < l; ++i)
{
var info = infos[i];
if (info.nodeLabel === nodeLabel)
return i;
}
return -1;
},
/** @private */
_getNonAtomInfo: function(nodeLabel)
{
var index = this._indexOfNonAtomLabel(nodeLabel);
return (index < 0)? null: this.getNonAtomLabelInfos()[index];
},
/** @private */
getAtomSetterWidget: function(canCreate)
{
var result = this.getAtomSetter();
if (!result && canCreate) // create new one
{
var parentElem = this.getEditor().getCoreElement();
var doc = parentElem.ownerDocument;
result = this._createAtomSetterWidget(doc, parentElem);
this.setAtomSetter(result);
}
return result;
},
/** @private */
_createAtomSetterWidget: function(doc, parentElem)
{
var result = new Kekule.ChemWidget.StructureNodeSetter(this.getEditor());
result.setUseDropDownSelectPanel(true);
result.addClassName(CCNS.CHEMEDITOR_ATOM_SETTER);
var listAtoms = AU.clone(this.getEditor().getEditorConfigs().getStructureConfigs().getPrimaryOrgChemAtoms());
listAtoms.push('...'); // add periodic table item
//result.setSelectableElementSymbols(listAtoms);
// non-atom nodes
var nonAtomLabelInfos = this.getNonAtomLabelInfos();
//result.setSelectableNonElementInfos(nonAtomLabelInfos);
// subgroups
//result.setSelectableSubGroupRepItems(Kekule.Editor.StoredSubgroupRepositoryItem2D.getAllRepItems());
result.setSelectableInfos({
'elementSymbols': listAtoms,
'nonElementInfos': nonAtomLabelInfos,
'subGroupRepItems': Kekule.Editor.StoredSubgroupRepositoryItem2D.getAllRepItems()
});
// react to value change of setter
var self = this;
result.addEventListener('keyup', function(e)
{
var ev = e.htmlEvent;
if (ev.getKeyCode() === Kekule.X.Event.KeyCode.ENTER)
{
self.applySetter(result);
result.dismiss(); // avoid call apply setter twice
}
}
);
result.addEventListener('valueSelect', function(e){
//var data = e.value;
//console.log(e.target, e.currentTarget);
if (self.getAtomSetter() && self.getAtomSetter().isShown())
{
self.applySetter(result);
result.dismiss(); // avoid call apply setter twice
}
});
result.addEventListener('showStateChange', function(e)
{
if (e.target === result && !e.byDomChange)
{
//console.log('show state change', e);
if (!e.isShown && !e.isDismissed) // widget hidden, feedback the edited value
{
if (self.getAtomSetter() && self.getAtomSetter().isShown())
self.applySetter(result);
}
}
}
);
result.appendToElem(parentElem);
return result;
},
/** @private */
getPeriodicTableDialogWidget: function(canCreate)
{
var result = this.getPeriodicTableDialog();
if (!result && canCreate) // create new one
{
var parentElem = this.getEditor().getCoreElement();
var doc = parentElem.ownerDocument;
result = this._createPeriodicTableDialogWidget(doc, parentElem);
this.setPeriodicTableDialog(result);
}
//console.log(result);
return result;
},
/** @private */
_createPeriodicTableDialogWidget: function(doc, parentElem)
{
var dialog = new Kekule.Widget.Dialog(doc, Kekule.$L('ChemWidgetTexts.CAPTION_PERIODIC_TABLE_DIALOG') /*CWT.CAPTION_PERIODIC_TABLE_DIALOG*/,
[Kekule.Widget.DialogButtons.OK, Kekule.Widget.DialogButtons.CANCEL]
);
var table = new Kekule.ChemWidget.PeriodicTable(doc);
table.setUseMiniMode(true);
table.setEnableSelect(true);
table.appendToElem(dialog.getClientElem());
this.setPeriodicTable(table);
return dialog;
},
/**
* Open atom edit box for obj in coord.
* @param {Hash} coord
* @param {Object} obj
*/
openSetterUi: function(coord, obj)
{
var oldSetter = this.getAtomSetter(); // check if there is old already created setter
if (oldSetter && oldSetter.isShown()) // has a old setter
{
//this.applySetter(oldSetter, this.getCurrAtom());
oldSetter.hide();
// IMPORTANT: ensure the hide process done quickly
// and the unprepare process of popup atom setter do not imfluence the prepare process of it
oldSetter._haltPrevShowHideProcess();
}
if (!this.isValidNode(obj))
return;
this.setCurrAtom(obj);
var fontSize = this.getEditor().getEditorConfigs().getInteractionConfigs().getAtomSetterFontSize() || 0;
fontSize *= this.getEditor().getZoom() || 1;
var posAdjust = fontSize / 1.5; // adjust position to align to atom center
var setter = this.getAtomSetterWidget(true);
//setter.setEditor(this.getEditor());
setter.setLabelConfigs(this.getEditor().getRenderConfigs().getDisplayLabelConfigs());
setter.setNodes([obj]);
var parentElem = this.getEditor().getCoreElement();
setter.appendToElem(parentElem); // ensure setter widget is a child of parentElem, since popup show may change the parent each time
var inputBox = setter.getNodeInputBox();
//setter.setIsDirty(false);
//setter.setIsPopup(true);
var style = setter.getElement().style;
style.position = 'absolute';
style.left = (coord.x - posAdjust) + 'px';
style.top = (coord.y - posAdjust) + 'px';
inputBox.getElement().style.fontSize = fontSize + 'px';
/*
style.marginTop = -posAdjust + 'px';
style.marginLeft = -posAdjust + 'px';
*/
//setter.show();
setter._applied = false;
setter.show(null, null, Kekule.Widget.ShowHideType.POPUP);
(function(){
inputBox.focus();
inputBox.selectAll();
}).defer();
//result.selectAll.bind(result).defer();
/*
setter.selectAll();
setter.focus();
*/
},
/** @private */
applySetter: function(setter, atom)
{
if (setter._applied) // avoid called twice
return;
if (!atom)
atom = this.getCurrAtom();
var newData = setter.getValue();
if (!newData)
return;
//console.log('apply setter', newData);
var nodeClass = newData.nodeClass;
var modifiedProps = newData.props;
var repItem = newData.repositoryItem;
var newNode;
if (repItem) // need to apply structure repository item
{
var repResult = repItem.createObjects(atom) || {};
var repObjects = repResult.objects;
var transformParams = Kekule.Editor.RepositoryStructureUtils.calcRepObjInitialTransformParams(this.getEditor(), repItem, repResult, atom, null);
this.getEditor().transformCoordAndSizeOfObjects(repObjects, transformParams);
newNode = repObjects[0];
nodeClass = newNode.getClass();
}
if (newData.isUnknownPseudoatom && !this.getEditorConfigs().getInteractionConfigs().getAllowUnknownAtomSymbol())
nodeClass = null;
if (!nodeClass)
{
Kekule.error(Kekule.$L('ErrorMsg.INVALID_ATOM_SYMBOL'));
}
else
{
this.applyModification(atom, newNode, nodeClass, modifiedProps);
setter._applied = true;
}
},
/**
* Save changes to current edited node.
* @param node
* @param newNodeClass
* @param modifiedProps
* @private
*/
applyModification: function(node, newNode, newNodeClass, modifiedProps)
{
var newNode;
var operGroup, oper;
var oldNodeClass = node.getClass();
if (newNode && !newNodeClass)
newNodeClass = newNode.getClass();
if (newNode || newNodeClass !== oldNodeClass) // need to replace node
{
operGroup = new Kekule.MacroOperation();
if (!newNode)
newNode = new newNodeClass();
var tempNode = new Kekule.ChemStructureNode();
tempNode.assign(node);
newNode.assign(tempNode); // copy some basic info of old node
var operReplace = new Kekule.ChemStructOperation.ReplaceNode(node, newNode, null, this.getEditor());
operGroup.add(operReplace);
}
else // no need to replace
newNode = node;
if (modifiedProps)
{
oper = new Kekule.ChemObjOperation.Modify(newNode, modifiedProps, this.getEditor());
if (operGroup)
operGroup.add(oper);
}
var operation = operGroup || oper;
if (operation) // only execute when there is real modification
{
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
operation.execute();
}
catch (e)
{
//Kekule.error(/*Kekule.ErrorMsg.NOT_A_VALID_ATOM*/Kekule.$L('ErrorMsg.NOT_A_VALID_ATOM'));
throw(e);
}
finally
{
editor.endUpdateObject();
}
if (editor && editor.getEnableOperHistory() && operation)
{
editor.pushOperation(operation);
}
}
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT)
{
this.getEditor().setSelection(null);
var coord = this._getEventMouseCoord(e);
{
var boundItem = this.getEditor().getTopmostBoundInfoAtCoord(coord, null, this.getCurrBoundInflation());
if (boundItem)
{
var obj = boundItem.obj;
if (this.isValidNode(obj)) // can modify atom of this object
{
var baseCoord = this.getEditor().getObjectScreenCoord(obj);
e.preventDefault();
e.stopPropagation();
// important, prevent event bubble to document, otherwise reactDocumentClick will be evoked
// and the atom setter will be closed immediately.
this.openSetterUi(baseCoord, obj);
this.doneInsertOrModifyBasicObjects([obj]);
//this.getEditor().setSelection([obj]);
}
return true; // important
}
}
}
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.MolAtomIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to add repository structure fragments or other objects into chem space.
* @class
* @augments Kekule.Editor.StructureInsertIaController
*/
Kekule.Editor.RepositoryIaController = Class.create(Kekule.Editor.StructureInsertIaController,
/** @lends Kekule.Editor.RepositoryIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.RepositoryIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setEnableSelect(false);
this._repObjStartingScreenCoord = null;
},
/** @private */
initProperties: function()
{
this.defineProp('repositoryItem', {'dataType': 'Kekule.AbstractRepositoryItem', 'serializable': false});
this.defineProp('addRepObjsOper', {'dataType': 'Kekule.MacroOperation', 'serializable': false});
this.defineProp('initialTransformParams', {'dataType': DataType.HASH, 'serializable': false});
this.defineProp('currRepositoryObjects', {'dataType': 'Kekule.ChemObject', 'serializable': false});
},
/** @ignore */
doTestMouseCursor: function(coord, e)
{
// Overwrite parent BasicMolManipulationIaController,
// Always show pointer cursor
return '';
},
/** @private */
canInteractWithObj: function($super, obj)
{
return $super(obj);
},
/**
* Returns if obj is a valid starting point of creating repository item.
* Descendants may override this method.
* @param obj
* @returns {boolean}
*/
isValidStartingObj: function(obj)
{
return true;
},
/** @private */
calcInitialTransformParams: function(repItem, repResult, destObj, targetCoord)
{
return Kekule.Editor.RepositoryStructureUtils.calcRepObjInitialTransformParams(this.getEditor(), repItem, repResult, destObj, targetCoord);
},
/** @private */
getRepObjCreationOptions: function()
{
return null;
},
/** @private */
createObjFromRepositoryItem: function(targetObj, repItem)
{
var repResult = repItem.createObjects(targetObj, this.getRepObjCreationOptions()) || {};
return repResult;
},
/** @private */
addRepositoryObj: function(targetObj, screenCoord, ignoreUnconnectedStructCheck)
{
var editor = this.getEditor();
this.setAddRepObjsOper(null);
var repResult = null;
editor.beginUpdateObject();
try
{
var chemSpace = editor.getChemSpace();
var repItem = this.getRepositoryItem();
/*
repResult = repItem.createObjects(targetObj) || {};
*/
var repResult = this.createObjFromRepositoryItem(targetObj, repItem);
var repObjects = repResult.objects;
this.setCurrRepositoryObjects(repObjects);
var isOneStructItem = repItem.isOneStructureFragmentObj();
var addToBlankMol = false;
var blankMol = this.getEditor().getOnlyOneBlankStructFragment();
if (!editor.canCreateNewChild() && !repResult.mergeObj && !ignoreUnconnectedStructCheck) // can not create a standalone child
{
if (!isOneStructItem || !editor.canAddUnconnectedStructFragment())
return null;
else
addToBlankMol = true;
}
var macroOper = new Kekule.MacroOperation();
for (var i = 0, l = repObjects.length; i < l; ++i)
{
var obj = repObjects[i];
var oper;
if (addToBlankMol)
oper = new Kekule.ChemStructOperation.MergeStructFragment(obj, blankMol, editor);
else
oper = new Kekule.ChemObjOperation.Add(obj, chemSpace, null, editor);
macroOper.add(oper);
}
macroOper.execute();
this.setAddRepObjsOper(macroOper);
var addedObjs = addToBlankMol? [blankMol]: repObjects;
var transformParams = this.calcInitialTransformParams(repItem, repResult, targetObj, screenCoord);
this._transformObjectsCoordAndSize(addedObjs, transformParams);
this.setInitialTransformParams(transformParams);
repResult.objects = addedObjs;
//repResult.centerObj = repItem.getMolManipulationCenterObj();
var manCenterCoord = repItem.getMolManipulationCenterCoord && repItem.getMolManipulationCenterCoord();
if (manCenterCoord)
repResult.manipulationCenterCoord = Kekule.CoordUtils.transform2D(manCenterCoord, transformParams);
var defDirectionCoord = repItem.getMolManipulationDefDirectionCoord && repItem.getMolManipulationDefDirectionCoord();
if (defDirectionCoord)
repResult.manipulationDefDirectionCoord = Kekule.CoordUtils.transform2D(defDirectionCoord, transformParams);
}
finally
{
editor.endUpdateObject();
}
//this.addOperationToEditor();
return repResult;
},
/**
* Change the inserted repository structures in editor.
* Note, the atoms and bonds in the structure can be changed, but the structure itself should not be replaced.
* @private
*/
doRefreshRepositoryObj: function(manipulatingObjs, startScreenCoord, startBox, rotateCenter, rotateRefCoord)
{
this.doPrepareManipulatingObjects(manipulatingObjs, startScreenCoord);
this.doPrepareManipulatingStartingCoords(startScreenCoord, startBox, rotateCenter, rotateRefCoord);
//this.createManipulateOperation();
this.doCreateManipulateMoveAndResizeOperation(); // only recreate move operations, but retain merge ones
},
/** @private */
_transformObjectsCoordAndSize: function(objects, transformParams)
{
/*
var coordMode = this.getEditor().getCoordMode();
var allowCoordBorrow = this.getEditor().getAllowCoordBorrow();
var matrix = (coordMode === Kekule.CoordMode.COORD3D)?
Kekule.CoordUtils.calcTransform3DMatrix(transformParams):
Kekule.CoordUtils.calcTransform2DMatrix(transformParams);
var childTransformParams = Object.extend({}, transformParams);
childTransformParams = Object.extend(childTransformParams, {
'translateX': 0,
'translateY': 0,
'translateZ': 0,
'center': {'x': 0, 'y': 0, 'z': 0}
});
var childMatrix = (coordMode === Kekule.CoordMode.COORD3D)?
Kekule.CoordUtils.calcTransform3DMatrix(childTransformParams):
Kekule.CoordUtils.calcTransform2DMatrix(childTransformParams);
for (var i = 0, l = objects.length; i < l; ++i)
{
var obj = objects[i];
obj.transformAbsCoordByMatrix(matrix, childMatrix, coordMode, true, allowCoordBorrow);
obj.scaleSize(transformParams.scale, coordMode, true, allowCoordBorrow);
}
*/
return this.getEditor().transformCoordAndSizeOfObjects(objects, transformParams);
},
/** @ignore */
getAllObjOperations: function($super, isTheFinalOperationToEditor)
{
var result = $super(isTheFinalOperationToEditor) || [];
var repOper = this.getAddRepObjsOper();
if (repOper)
result.unshift(repOper);
return result;
},
/** @private */
addOperationToEditor: function($super)
{
/*
if (this.getAllManipulateObjsMerged())
return null;
else
*/
// even all nodes are merged, bond may be added as well
return $super();
},
/**
* Returns manipulate type that should be used after adding a repository object.
* Descendant may override this method.
* @returns {Int}
* @private
*/
getInitManipulationType: function()
{
return Kekule.Editor.BasicManipulationIaController.ManipulationType.ROTATE;
},
/** @private */
doInsertRepositoryObjToEditor: function(addedRepObjsResult, startingCoord, startingObj, isUpdate)
{
if (!isUpdate)
{
// save new insertion params
this._initialStartingObj = startingObj;
this._repObjStartingScreenCoord = startingCoord;
}
//var addedResult = this.addRepositoryObj(startingObj, startingCoord);
var addedResult = addedRepObjsResult;
if (addedResult)
{
var addedObjects = addedResult.objects;
var rotateCenter = null, rotateRefCoord = null;
var manType = this.getInitManipulationType();
if (addedResult.mergeObj)
{
rotateCenter = this.getEditor().getObjectScreenCoord(addedResult.mergeObj);
manType = Kekule.Editor.BasicManipulationIaController.ManipulationType.ROTATE; // always rotate when there is merge
}
else
{
/*
if (addedResult.centerObj)
rotateCenter = this.getEditor().getObjectScreenCoord(addedResult.centerObj);
*/
if (addedResult.manipulationCenterCoord)
rotateCenter = this.getEditor().objCoordToScreen(addedResult.manipulationCenterCoord);
}
if (addedResult.manipulationDefDirectionCoord)
rotateRefCoord = this.getEditor().objCoordToScreen(addedResult.manipulationDefDirectionCoord);
//console.log(addedResult, rotateCenter);
var box = this.getEditor().getObjectsContainerBox(addedObjects);
//console.log(manType, rotateCenter, rotateRefCoord);
//this.moveManipulatedObjs(startingObj); // force a "move" action, to apply possible merge
return {'objects': addedObjects, 'manipulateType': manType, 'coord': startingCoord,
'box': box, 'rotateCenter': rotateCenter, 'rotateRefCoord': rotateRefCoord,
'startingObj': startingObj};
}
else
{
// not added, do nothing
return null;
}
},
/** @private */
insertRepositoryObjToEditor: function(startingCoord, startingObj, isUpdate)
{
var addedResult = this.addRepositoryObj(startingObj, startingCoord);
return this.doInsertRepositoryObjToEditor(addedResult, startingCoord, startingObj, isUpdate);
},
/**
* Update (and change) the inserted structure in editor.
* You must ensure that the returned structures of method insertRepositoryObjToEditor
* returns all the same old inserted structures.
* @private
*/
updateRepositoryObjInEditor: function()
{
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
// when merge preview is not used in editor, the merge operation may be messed up when change curr manipulation objects,
// so empty them first
if (!this.useMergePreview())
{
this.reverseActiveOperation();
}
//console.log(this._initialStartingObj, this._repObjStartingScreenCoord);
//var result = this.insertRepositoryObjToEditor(this._repObjStartingScreenCoord, this._initialStartingObj, true);
var oldObjects = this.getCurrRepositoryObjects();
var addedRepObjResult = this.addRepositoryObj(this._initialStartingObj, this._repObjStartingScreenCoord, true);
var unneedObjs = AU.exclude(oldObjects, (addedRepObjResult || {}).objects); // objects should be removed
for (var i = 0, l = unneedObjs.length; i < l; ++i)
{
var obj = unneedObjs[i];
var parent = obj.getParent();
if (parent)
parent.removeChild(obj);
}
//console.log('unnedded', unneedObjs);
// when merge preview is not used in editor, the merge operation may be messed up when change curr manipulation objects,
// so empty them first
if (!this.useMergePreview() || unneedObjs.length)
{
this.setMergeOperations([]);
this.setMergePreviewOperations([]);
}
var result = this.doInsertRepositoryObjToEditor(addedRepObjResult, this._repObjStartingScreenCoord, this._initialStartingObj, true);
if (result)
{
this.doRefreshRepositoryObj(result.objects, result.coord, result.box,
result.rotateCenter, result.rotateRefCoord);
this.moveManipulatedObjs(this._repObjStartingScreenCoord); // force a "move" action, to apply possible merge
}
}
finally
{
editor.endUpdateObject();
}
return result;
},
/** @private */
react_pointerdown: function(e)
{
if (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT)
{
var S = BC.State;
var coord = this._getEventMouseCoord(e);
var state = this.getState();
if (state === S.INITIAL)
{
var boundItem = this.getEditor().getTopmostBoundInfoAtCoord(coord, null, this.getCurrBoundInflation());
var boundObj = boundItem? boundItem.obj: null;
var insertResult = this.insertRepositoryObjToEditor(coord, boundObj);
if (insertResult)
{
this.startDirectManipulate(insertResult.manipulateType, insertResult.objects,
insertResult.coord, insertResult.box, insertResult.rotateCenter);
this.moveManipulatedObjs(coord); // force a "move" action, to apply possible merge
}
e.preventDefault();
return true; // important
}
}
},
/** @private */
react_pointerup: function($super, e)
{
var state = this.getState();
var startCoord = this.getStartCoord();
var endCoord = this._getEventMouseCoord(e);
var S = Kekule.Editor.BasicManipulationIaController.State;
if ((state === S.MANIPULATING) && (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT))
{
if (Kekule.CoordUtils.isEqual(startCoord, endCoord)) // click
{
this.addOperationToEditor();
this.stopManipulate();
this.setState(S.NORMAL);
e.preventDefault();
return true;
}
}
return $super(e); // finish move operation
}
});
/**
* Controller to add ring structure into chem space.
* @class
* @augments Kekule.Editor.RepositoryIaController
*
* @property {Int} ringAtomCount Atom count on ring.
* @property {Bool} isAromatic Whether this ring is a aromatic one (single/double bond intersect),
*/
Kekule.Editor.MolRingIaController = Class.create(Kekule.Editor.RepositoryIaController,
/** @lends Kekule.Editor.MolRingIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolRingIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setRepositoryItem(new Kekule.Editor.MolRingRepositoryItem2D());
},
/** @ignore */
getActualManipulatingObjects: function(objs)
{
// since we are sure that the manipulated objects is carbon chain itself,
// we can return all its atoms as the actual manipulating objects / coord dependent objects
var mol = this.getCurrRepositoryObjects()[0];
//console.log(mol);
return mol? AU.clone(mol.getNodes()): [];
},
/** @private */
initProperties: function()
{
this.defineProp('ringAtomCount', {'dataType': DataType.INT,
'getter': function() { return this.getRepositoryItem().getRingAtomCount(); },
'setter': function(value) { this.getRepositoryItem().setRingAtomCount(value); }
});
this.defineProp('isAromatic', {'dataType': DataType.INT,
'getter': function() { return this.getRepositoryItem().getIsAromatic(); },
'setter': function(value) { this.getRepositoryItem().setIsAromatic(value); }
});
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.MolRingIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to add repository structure into chem space.
* @class
* @augments Kekule.Editor.RepositoryIaController
*
* @property {Int} ringAtomCount Atom count on ring.
* @property {Bool} isAromatic Whether this ring is a aromatic one (single/double bond intersect),
*/
Kekule.Editor.RepositoryStructureFragmentIaController = Class.create(Kekule.Editor.RepositoryIaController,
/** @lends Kekule.Editor.RepositoryStructureFragmentIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.RepositoryStructureFragmentIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
},
/** @private */
initProperties: function()
{
this.defineProp('repItemName', {'dataType': DataType.STRING,
'setter': function(value)
{
if (value !== this.getRepItemName())
{
var repItem = Kekule.Editor.RepositoryItemManager.getItem(value);
if (repItem)
{
this.setRepositoryItem(repItem);
this.setPropStoreFieldValue('repItemName', value);
}
}
}
});
}
});
Kekule.Editor.IaControllerManager.register(Kekule.Editor.RepositoryStructureFragmentIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Base controller to add a flex structure into chem space.
* @class
* @augments Kekule.Editor.RepositoryIaController
*/
Kekule.Editor.MolFlexStructureIaController = Class.create(Kekule.Editor.RepositoryIaController,
/** @lends Kekule.Editor.MolFlexStructureIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolFlexStructureIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setEnableSelect(false);
},
/** @private */
initProperties: function()
{
// marker to display the ring atom count
this.defineProp('assocMarker', {'dataType': DataType.OBJECT, 'serializable': false,
'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('assocMarker');
if (!result)
{
result = new Kekule.ChemWidget.TextUiMarker();
this.setPropStoreFieldValue('assocMarker', result);
}
return result;
}
});
},
/** @ignore */
manipulateEnd: function($super)
{
$super();
this.hideAssocMarker();
},
/** @private */
initAssocMarker: function()
{
var marker = this.getAssocMarker();
var styleConfigs = this.getEditorConfigs().getUiMarkerConfigs();
var drawStyles = {
'color': styleConfigs.getFlexStructureAssocMarkerColor(),
'opacity': styleConfigs.getFlexStructureAssocMarkerOpacity(),
'fontSize': styleConfigs.getFlexStructureAssocMarkerFontSize(),
'fontFamily': styleConfigs.getFlexStructureAssocMarkerFontFamily(),
'textBoxXAlignment': Kekule.Render.BoxXAlignment.CENTER,
'textBoxYAlignment': Kekule.Render.BoxYAlignment.CENTER
};
//console.log('draw styles', drawStyles);
marker.setDrawStyles(drawStyles);
marker.setVisible(true);
this.getEditor().getUiMarkers().addMarker(marker);
return marker;
},
/** @private */
hideAssocMarker: function()
{
var marker = this.getPropStoreFieldValue('assocMarker');
if (marker)
{
marker.setVisible(false);
this.getEditor().getUiMarkers().removeMarker(marker);
this.repaintMarker();
}
},
/** @private */
updateAssocMarker: function(coord, props, doNotRepaint)
{
var marker = this.getAssocMarker();
if (coord)
marker.setCoord(coord);
if (props)
marker.setPropValues(props);
// request a repaint
if (!doNotRepaint)
this.repaintMarker();
},
/** @private */
repaintMarker: function()
{
//console.log(this.getEditor().isUpdatingUiMarkers());
this.getEditor().repaintUiMarker();
}
});
/**
* Controller to add a flex carbon chain into chem space.
* @class
* @augments Kekule.Editor.MolFlexStructureIaController
*/
Kekule.Editor.MolFlexChainIaController = Class.create(Kekule.Editor.MolFlexStructureIaController,
/** @lends Kekule.Editor.MolFlexChainIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolFlexChainIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setEnableSelect(false);
// private
this._deltaDistance = null;
this._deltaCount = 0;
this._lastDeltaCount = 1;
this._repObjStartingScreenCoord = null;
this._repObjNeedUpdate = false;
this._isForceReversedChainDirection = false;
this._assocMarkerInitCoord = null;
this._manipulateObjInfoCache = [];
this._chain = null; // internally, stores the chain molecule object
var rep = new Kekule.Editor.MolChainRepositoryItem2D(2);
rep.setEnableCoordCache(true); // use cache to reduce dynamic coord calculation time
this.setRepositoryItem(rep);
},
/** @ignore */
updateAssocMarker: function($super, coord, props, doNotRepaint)
{
var editor = this.getEditor();
var style = this.getAssocMarker().getDrawStyles();
var mol = this._chain;
// update draw position
var currCoord = coord; // || this._assocMarkerInitCoord;
if (!currCoord && mol)
{
// draw near the last node of chain
var firstNode = mol.getNodeAt(0);
var lastNode = mol.getNodeAt(mol.getNodeCount() - 1);
var firstCoord = editor.getObjectContextCoord(firstNode);
currCoord = editor.getObjectContextCoord(lastNode);
var v = CU.substract(currCoord, firstCoord);
if (v)
{
var fontSize = style.fontSize || 10;
var gap = fontSize / 2; // TODO: currently fixed
var deltaCoord = {x: 0, y: 0};
if (currCoord)
{
/*
if (Math.abs(v.x) >= Math.abs(v.y))
{
style.textBoxYAlignment = Kekule.Render.BoxYAlignment.CENTER;
style.textBoxXAlignment = (v.x >= 0) ? Kekule.Render.BoxXAlignment.RIGHT : Kekule.Render.BoxXAlignment.LEFT;
deltaCoord.x = gap * (Math.sign(v.x) || 1); // * -1;
}
else
{
style.textBoxXAlignment = Kekule.Render.BoxXAlignment.CENTER;
style.textBoxYAlignment = (v.y >= 0) ? Kekule.Render.BoxYAlignment.BOTTOM : Kekule.Render.BoxYAlignment.TOP;
deltaCoord.y = gap * (Math.sign(v.y) || 1); // * -1;
}
*/
var d = CU.getDistance(v);
var ratioX = v.x / d;
var ratioY = v.y / d;
var deltaCoord = {x: gap * ratioX, y: gap * ratioY};
currCoord = Kekule.CoordUtils.add(currCoord, deltaCoord);
//console.log(currCoord, deltaCoord);
}
}
}
// then call $super, and repaint the marker
$super(currCoord, props, doNotRepaint);
},
/** @private */
getChainMaxAtomCount: function()
{
return Math.max(this.getEditorConfigs().getStructureConfigs().getMaxFlexChainAtomCount(), 0);
},
/** @ignore */
manipulateEnd: function($super)
{
this._clearManipulateObjInfoCache();
$super();
},
/** @ignore */
createManipulateObjInfo: function($super, obj, objIndex, startContextCoord)
{
// try use cached info first
var negative = this.getRepositoryItem().getNegativeDirection();
var cachedInfo = this._getCachedManipulateObjInfo(objIndex, negative);
if (cachedInfo)
{
//console.log('use cached info', objIndex, cachedInfo);
return cachedInfo;
}
else // calculate and save to cache
{
var info = $super(obj, objIndex, startContextCoord);
this._setCachedManipulateObjInfo(info, objIndex, negative);
return info;
}
},
/** @ignore */
canInteractWithObj: function($super, obj)
{
return $super(obj) && (obj instanceof Kekule.ChemStructureNode);
},
/** @ignore */
getActualManipulatingObjects: function(objs)
{
// since we are sure that the manipulated objects is carbon chain itself,
// we can return all its atoms as the actual manipulating objects / coord dependent objects
var mol = this.getCurrRepositoryObjects()[0];
//console.log(mol);
return mol? AU.clone(mol.getNodes()): [];
},
/** @private */
_setCachedManipulateObjInfo: function(info, atomIndex, isNegativeChain)
{
var cacheIndex = isNegativeChain? 1: 0;
var cache = this._manipulateObjInfoCache[cacheIndex];
if (!cache)
{
cache = [];
this._manipulateObjInfoCache[cacheIndex] = cache;
}
cache[atomIndex] = info;
},
/** @private */
_getCachedManipulateObjInfo: function(atomIndex, isNegativeChain)
{
var cacheIndex = isNegativeChain? 1: 0;
var cache = this._manipulateObjInfoCache[cacheIndex];
return cache && cache[atomIndex];
},
/** @private */
_clearManipulateObjInfoCache: function()
{
this._manipulateObjInfoCache = [];
},
/** @private */
_calcStepDeltaDistance: function()
{
var bondLength = this.getRepositoryItem().getBondLength();
var transformParams = this.getInitialTransformParams();
//console.log('repository item bond length', bondLength, transformParams);
var editor = this.getEditor();
var coord1 = editor.objCoordToScreen(Kekule.CoordUtils.transform2D({x: 0, y: 0}, transformParams));
var coord2 = editor.objCoordToScreen(Kekule.CoordUtils.transform2D({x: bondLength * Math.cos(Math.PI / 6), y: 0}, transformParams));
var result = Kekule.CoordUtils.getDistance(coord1, coord2);
/*
console.log('bond length', Kekule.CoordUtils.getDistance(
Kekule.CoordUtils.transform2D({x: 0, y: 0}, transformParams),
Kekule.CoordUtils.transform2D({x: bondLength, y: 0}, transformParams)
));
*/
return result;
},
/** @ignore */
getRepObjCreationOptions: function()
{
//return null;
if (this._isUpdateRepObj) // just update chain, can reuse the old structure to avoid unnecessary merge operation
{
var oldMol = this.getCurrRepositoryObjects()[0];
//console.log(oldMol);
return {'originalStructFragment': oldMol};
}
else
return null;
},
/** @ignore */
addRepositoryObj: function($super, targetObj, screenCoord, ignoreUnconnectedStructCheck)
{
var result = $super(targetObj, screenCoord, ignoreUnconnectedStructCheck);
/*
// debug
var mol = result.objects[0];
var coords = [];
mol.getNodes().forEach(function(n) {coords.push(n.getCoord2D());} );
console.log(coords);
*/
// save bond length
this._deltaDistance = this._calcStepDeltaDistance();
//this._repObjStartingScreenCoord = screenCoord;
this._repObjNeedUpdate = false;
return result;
},
/** @ignore */
insertRepositoryObjToEditor: function($super, startingCoord, startingObj, isUpdate)
{
if (!isUpdate)
{
//this._initialStartingObj = startingObj;
//this._repObjStartingScreenCoord = null;
this._deltaDistance = null;
this._deltaCount = 0;
this._lastDeltaCount = 1;
var initialAtomCount = 2;
this.getRepositoryItem().setAtomCount(initialAtomCount);
}
this._isUpdateRepObj = isUpdate; // a flag indicaing whether is update chain
var result = $super(startingCoord, startingObj, isUpdate);
this._chain = result && result.objects[0];
if (result && !isUpdate)
{
var marker = this.initAssocMarker();
//this._assocMarkerInitCoord = startingCoord;
this.updateAssocMarker(null, {'text': '' + initialAtomCount}); // the marker will restain in starting coord
}
return result;
},
/** @private */
_updateChain: function()
{
this._isUpdateRepObj = true;
var result = this.updateRepositoryObjInEditor();
this._chain = result.objects[0];
this._repObjNeedUpdate = false;
return result;
},
/** @ignore */
doTransformManipulatedObjs: function($super, manipulateType, endScreenCoord, explicitTransformParams)
{
var endCoord = endScreenCoord;
var state = this.getState();
if (state === Kekule.Editor.BasicManipulationIaController.State.MANIPULATING)
{
//var startCoord = this.getRotateCenter();
var startCoord = this.getBaseCoord();
if (startCoord)
{
// check chain length
var dis = Kekule.CoordUtils.getDistance(endCoord, startCoord);
//var dis = Math.abs(coord.x - startCoord.x);
var deltaCount = Math.max(Math.round(dis / this._deltaDistance), 1);
//console.log(dis, this._deltaDistance, deltaCount);
if (!this._lastDeltaCount || deltaCount !== this._lastDeltaCount)
{
this._deltaCount = deltaCount;
// need recreate repository
var maxAtomCount = this.getChainMaxAtomCount();
var atomCount = maxAtomCount? Math.min(maxAtomCount, deltaCount + 1): deltaCount + 1;
if (this.getRepositoryItem().getAtomCount() !== atomCount)
{
this.getRepositoryItem().setAtomCount(atomCount);
this._repObjNeedUpdate = true;
this.updateAssocMarker(null, {'text': '' + atomCount}, true); // update text but suspend repaint
}
this._lastDeltaCount = deltaCount;
}
// console.log('dynamic', startCoord, coord, this._deltaDistance, deltaCount);
// check chain direction
var angleInfo = this._calcRotateAngle(endCoord);
var angle = angleInfo && angleInfo.angle;
var isConstrained = this.isInActualConstrainedRotation();
var negativeDirection;
if (isConstrained)
{
// note: here we use doubled constrainedRotateStep (30 degree) to avoid too violent variation
// TODO: this step value should be customizable
var angleStep = isConstrained ?
this.getEditorConfigs().getInteractionConfigs().getConstrainedRotateStep() * 2 : null;
var times = Math.round(angle / angleStep);
var tailNum = times * angleStep - angle;
negativeDirection = tailNum < 0;
//console.log(angleStep, angle, times, tailNum);
}
else
{
negativeDirection = false;
}
if (this._isForceReversedChainDirection)
negativeDirection = !negativeDirection;
if (!!this.getRepositoryItem().getNegativeDirection() !== negativeDirection)
{
this.getRepositoryItem().setNegativeDirection(negativeDirection);
this._repObjNeedUpdate = true;
}
}
}
if (this._repObjNeedUpdate)
this._updateChain();
var mol = this.getCurrRepositoryObjects()[0];
mol.beginUpdate();
try
{
var result = $super(manipulateType, endScreenCoord, explicitTransformParams);
//var manipulationDirectionVector = Kekule.CoordUtils.substract(endScreenCoord, startCoord);
this.updateAssocMarker(null, null, false); // force repaint marker
}
finally
{
mol.endUpdate();
}
return result;
},
/** @ignore */
react_pointermove: function($super, e)
{
//var tStart = Date.now();
this._isForceReversedChainDirection = !!e.getShiftKey();
var result = $super(e);
//var tEnd = Date.now();
//console.log('duration', tEnd - tStart);
return result;
}
});
Kekule.Editor.IaControllerManager.register(Kekule.Editor.MolFlexChainIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to add a flex ring structure to chem space.
* @class
* @augments Kekule.Editor.MolFlexStructureIaController
*/
Kekule.Editor.MolFlexRingIaController = Class.create(Kekule.Editor.MolFlexStructureIaController,
/** @lends Kekule.Editor.MolFlexRingIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolFlexRingIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setEnableSelect(false);
// private
this._deltaDistance = null;
this._lastDeltaCount = 1;
this._repObjStartingScreenCoord = null;
this._repObjNeedUpdate = false;
this._manipulateObjInfoCache = [];
var rep = new Kekule.Editor.MolRingRepositoryItem2D(3);
rep.setEnableCoordCache(true); // use cache to reduce dynamic coord calculation time
this.setRepositoryItem(rep);
},
/** @private */
getRingMaxAtomCount: function()
{
return Math.max(this.getEditorConfigs().getStructureConfigs().getMaxFlexRingAtomCount(), 0);
},
/** @private */
getRingMinAtomCount: function()
{
return Math.max(this.getEditorConfigs().getStructureConfigs().getMinFlexRingAtomCount(), 3);
},
/** @ignore */
getActualManipulatingObjects: function(objs)
{
// since we are sure that the manipulated objects is carbon ring itself,
// we can return all its atoms as the actual manipulating objects / coord dependent objects
var mol = this.getCurrRepositoryObjects()[0];
//console.log(mol);
return mol? AU.clone(mol.getNodes()): [];
},
/** @ignore */
manipulateEnd: function($super)
{
this._clearManipulateObjInfoCache();
$super();
},
/** @ignore */
createManipulateObjInfo: function($super, obj, objIndex, startContextCoord)
{
// try use cached info first
var atomCount = this.getRepositoryItem().getRingAtomCount();
var cachedInfo = this._getCachedManipulateObjInfo(objIndex, atomCount);
if (cachedInfo)
{
//console.log('use cached info', objIndex, cachedInfo);
return cachedInfo;
}
else // calculate and save to cache
{
var info = $super(obj, objIndex, startContextCoord);
this._setCachedManipulateObjInfo(info, objIndex, atomCount);
return info;
}
},
/** @private */
_setCachedManipulateObjInfo: function(info, atomIndex, atomCount)
{
var cache = this._manipulateObjInfoCache[atomCount];
if (!cache)
{
cache = [];
this._manipulateObjInfoCache[atomCount] = cache;
}
cache[atomIndex] = info;
},
/** @private */
_getCachedManipulateObjInfo: function(atomIndex, atomCount)
{
var cache = this._manipulateObjInfoCache[atomCount];
return cache && cache[atomIndex];
},
/** @private */
_clearManipulateObjInfoCache: function()
{
this._manipulateObjInfoCache = [];
},
/** @private */
_calcStepDeltaDistance: function()
{
var bondLength = this.getRepositoryItem().getBondLength();
var transformParams = this.getInitialTransformParams();
//console.log('repository item bond length', bondLength, transformParams);
var editor = this.getEditor();
var coord1 = editor.objCoordToScreen(Kekule.CoordUtils.transform2D({x: 0, y: 0}, transformParams));
var coord2 = editor.objCoordToScreen(Kekule.CoordUtils.transform2D({x: bondLength, y: 0}, transformParams));
// TODO: 1/3 bond length, currently fixed
var result = Kekule.CoordUtils.getDistance(coord1, coord2) / 3;
return result;
},
/** @ignore */
getRepObjCreationOptions: function()
{
if (this._isUpdateRepObj) // just update ring, can reuse the old structure to avoid unnecessary merge operation
{
var oldMol = this.getCurrRepositoryObjects()[0];
return {'originalStructFragment': oldMol};
}
else
return null;
},
/** @ignore */
addRepositoryObj: function($super, targetObj, screenCoord, ignoreUnconnectedStructCheck)
{
var result = $super(targetObj, screenCoord, ignoreUnconnectedStructCheck);
// save bond length
this._deltaDistance = this._calcStepDeltaDistance();
//this._repObjStartingScreenCoord = screenCoord;
this._repObjNeedUpdate = false;
return result;
},
/** @ignore */
insertRepositoryObjToEditor: function($super, startingCoord, startingObj, isUpdate)
{
if (!isUpdate)
{
//this._initialStartingObj = startingObj;
//this._repObjStartingScreenCoord = null;
this._deltaDistance = null;
this._lastDeltaCount = 1;
var initialAtomCount = this.getEditorConfigs().getStructureConfigs().getMinFlexRingAtomCount();
this.getRepositoryItem().setRingAtomCount(initialAtomCount);
}
this._isUpdateRepObj = isUpdate; // a flag indicaing whether is update ring
var result = $super(startingCoord, startingObj);
if (result && !isUpdate) // really add new obj
{
var ring = result.objects[0];
//this._ring = ring;
this.initAssocMarker();
this.updateAssocMarker(this._getRingCenterAbsContextCoord(ring), {'text': '' + initialAtomCount});
//this._updateRingAssocMarker(ring);
}
return result;
},
/** @ignore */
updateAssocMarker: function($super, coord, props, doNotRepaint)
{
if (!coord)
{
var centerCoord = this._getManipulateObjsCenterCoord();
coord = this.getEditor().objCoordToContext(centerCoord);
}
//console.log('update marker', coord, props, doNotRepaint);
return $super(coord, props, doNotRepaint);
},
/** @private */
_getRingCenterAbsContextCoord: function(ring)
{
var editor = this.getEditor();
var objCoord = Kekule.Editor.StructureUtils.getStructureCenterAbsBaseCoord(ring, editor.getCoordMode(), editor.getAllowCoordBorrow());
return editor.objCoordToContext(objCoord);
},
/* @private */
/*
_updateRingAssocMarker: function(ring)
{
if (!ring)
ring = this._ring;
{
var atomCount = ring.getNodeCount();
var centerCoord = this._getRingCenterAbsContextCoord(ring);
var marker = this.initAssocMarker();
this.updateAssocMarker(centerCoord, {'text': '' + atomCount}); // the marker will be shown at the center of ring
}
},
*/
/** @private */
_updateRing: function()
{
/*
// reverse prev operation (and removes it from editor) first
var editor = this.getEditor();
editor.beginUpdateObject();
try
{
var prevOperation = this.getActiveOperation();
if (prevOperation)
{
prevOperation.reverse();
}
var insertResult = this.insertRepositoryObjToEditor(this._repObjStartingScreenCoord, this._initialStartingObj, true);
if (insertResult)
{
this.moveManipulatedObjs(this._repObjStartingScreenCoord); // force a "move" action, to apply possible merge
}
this._repObjNeedUpdate = false;
}
finally
{
editor.endUpdateObject();
}
*/
this._isUpdateRepObj = true;
var result = this.updateRepositoryObjInEditor();
//this._ring = result.objects[0];
this._repObjNeedUpdate = false;
return result;
},
/** @ignore */
doTransformManipulatedObjs: function($super, manipulateType, endScreenCoord, explicitTransformParams)
{
var endCoord = endScreenCoord;
var state = this.getState();
if (state === Kekule.Editor.BasicManipulationIaController.State.MANIPULATING)
{
//var startCoord = this.getRotateCenter();
var startCoord = this.getBaseCoord();
if (startCoord)
{
// check ring atom count
var minAtomCount = this.getRingMinAtomCount();
var dis = Kekule.CoordUtils.getDistance(endCoord, startCoord);
var deltaCount = Math.round(dis / this._deltaDistance) + minAtomCount; // min ring is 3 atoms
//console.log(dis, this._deltaDistance, deltaCount);
if (!this._lastDeltaCount || deltaCount !== this._lastDeltaCount)
{
// need recreate repository
var maxAtomCount = this.getRingMaxAtomCount();
var atomCount = maxAtomCount? Math.min(maxAtomCount, deltaCount): deltaCount;
atomCount = minAtomCount? Math.max(atomCount, minAtomCount): atomCount;
if (this.getRepositoryItem().getRingAtomCount() !== atomCount)
{
this.getRepositoryItem().setRingAtomCount(atomCount);
this._repObjNeedUpdate = true;
this.updateAssocMarker(null, {'text': '' + atomCount}, true); // suspend repaint
}
//this._updateChain();
this._lastDeltaCount = deltaCount;
}
// console.log('dynamic', startCoord, coord, this._deltaDistance, deltaCount);
}
}
if (this._repObjNeedUpdate)
this._updateRing();
var mol = this.getCurrRepositoryObjects()[0];
mol.beginUpdate();
try
{
var result = $super(manipulateType, endScreenCoord, explicitTransformParams);
//this._updateRingAssocMarker(mol);
// update assoc marker and coord
this.updateAssocMarker(null, null, false); // force repaint marker
}
finally
{
mol.endUpdate();
}
return result;
}
});
Kekule.Editor.IaControllerManager.register(Kekule.Editor.MolFlexRingIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to add arrow/line into chem space.
* @class
* @augments Kekule.Editor.RepositoryIaController
*
* @property {Class} glyphClass Class to create glyph.
* @property {Float} glyphRefLength Default length to generate glyph.
* @property {Hash} glyphInitialParams Initial params to create glyph.
*/
Kekule.Editor.ArrowLineIaController = Class.create(Kekule.Editor.RepositoryIaController,
/** @lends Kekule.Editor.ArrowLineIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.ArrowLineIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setRepositoryItem(new Kekule.Editor.PathGlyphRepositoryItem2D());
},
/** @private */
initProperties: function()
{
this.defineProp('glyphRefLength', {'dataType': DataType.FLOAT,
'getter': function() { return this.getRepositoryItem().getGlyphRefLength(); },
'setter': function(value) { this.getRepositoryItem().setGlyphRefLength(value); }
});
this.defineProp('glyphClass', {'dataType': DataType.CLASS, 'serializable': false,
'getter': function()
{
return this.getRepositoryItem().getGlyphClass();
},
'setter': function(value)
{
this.getRepositoryItem().setGlyphClass(value);
}
});
this.defineProp('glyphInitialParams', {'dataType': DataType.HASH,
'getter': function() { return this.getRepositoryItem().getGlyphInitialParams(); },
'setter': function(value) { this.getRepositoryItem().setGlyphInitialParams(value); }
});
},
/** @ignore */
getInitManipulationType: function()
{
return Kekule.Editor.BasicManipulationIaController.ManipulationType.MOVE;
},
/** @ignore */
addRepositoryObj: function($super, targetObj, screenCoord, ignoreUnconnectedStructCheck)
{
// set ref length before adding new object
this.getRepositoryItem().setGlyphRefLength(this.getEditor().getChemSpace().getDefAutoScaleRefLength());
return $super(targetObj, screenCoord, ignoreUnconnectedStructCheck);
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.ArrowLineIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to add or edit formula based molecule in document.
* @class
* @augments Kekule.Editor.BaseEditorIaController
*/
Kekule.Editor.FormulaIaController = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.FormulaIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.FormulaIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this._operAddMol = null; // private
},
/** @private */
initProperties: function()
{
this.defineProp('currMol', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('textSetter', {'dataType': DataType.OBJECT, 'serializable': false}); // private
},
/** @private */
canInteractWithObj: function($super, obj)
{
if (obj && this.isValidMol(obj))
return true;
else
return false;
},
/**
* Check if obj is a valid formula based molecule and can be edited.
* @param {Kekule.ChemObject} obj
* @returns {Bool}
* @private
*/
isValidMol: function(obj)
{
return (obj instanceof Kekule.StructureFragment) && obj.hasFormula() && !obj.hasCtab();
},
/**
* Returns plain text of formula that shows in text setter.
* @param {Kekule.StructureFragment} mol
* @returns {String}
* @private
*/
getFormulaText: function(mol)
{
return mol.hasFormula()? mol.getFormula().getText(): '';
},
/** @private */
createNewMol: function(chemSpace, coord)
{
var mol;
var editor = this.getEditor();
if (!editor.canCreateNewChild())
{
mol = editor.getOnlyOneBlankStructFragment();
if (!mol)
return null;
}
editor.beginUpdateObject();
try
{
if (!mol)
var mol = new Kekule.Molecule();
mol.getFormula(true); // create a forumla
chemSpace.appendChild(mol);
editor.setObjectScreenCoord(mol, coord);
var addOperation = new Kekule.ChemObjOperation.Add(mol, chemSpace, null, editor);
this._operAddBlock = addOperation;
}
finally
{
editor.endUpdateObject();
}
return mol;
},
/** @private */
getTextSetterWidget: function(canCreate)
{
var result = this.getTextSetter();
if (!result && canCreate) // create new one
{
var parentElem = this.getEditor().getCoreElement();
var doc = parentElem.ownerDocument;
result = this._createTextSetterWidget(doc, parentElem);
this.setTextSetter(result);
}
return result;
},
/** @private */
_createTextSetterWidget: function(doc, parentElem)
{
var result = new Kekule.Widget.TextBox(this.getEditor());
/*
result.setAutoSizeX(true);
result.setAutoSizeY(true);
*/
result.addClassName(CCNS.CHEMEDITOR_FORMULA_SETTER);
result.appendToElem(parentElem);
// event handler
var self = this;
result.addEventListener('keyup', function(e)
{
var ev = e.htmlEvent;
var keyCode = ev.getKeyCode();
if (keyCode === Kekule.X.Event.KeyCode.ENTER) // ctrl+enter
{
self.applySetter(result);
result.dismiss(); // avoid call apply setter twice
}
else if (keyCode === Kekule.X.Event.KeyCode.ESC) // ESC, cancel editor
{
result.dismiss();
self.cancelSetter();
}
}
);
result.addEventListener('showStateChange', function(e)
{
//console.log('show state change', e.isShown, e.isDismissed, e.byDomChange);
if (!e.byDomChange)
{
if (!e.isShown && !e.isDismissed) // widget hidden, feedback the edited value
{
self.applySetter(result);
}
if (e.isShown) // set applied to false on newly shown widget
result._applied = false;
}
}
);
return result;
},
/** @private */
cancelSetter: function()
{
if (this._operAddBlock) // already created new formula, remove it
{
this._operAddBlock.reverse();
this._operAddBlock = null;
}
},
/** @private */
applySetter: function(setter, mol)
{
if (setter._applied) // avoid call twice
return;
if (!mol)
mol = this.getCurrMol();
var oper;
var text = setter.getText();
if (!text) // no input, delete
{
if (this._operAddBlock) // new forumla just added to space
this.cancelSetter();
else // old one, delete it
{
oper = new Kekule.ChemObjOperation.Remove(mol, mol.getParent(), null, this.getEditor());
}
}
else
{
var oper = new Kekule.ChemObjOperation.Modify(mol.getFormula(), {'text': text}, this.getEditor());
}
if (oper)
{
oper.execute();
var editor = this.getEditor();
if (editor && editor.getEnableOperHistory())
{
if (this._operAddBlock)
{
var group = new Kekule.MacroOperation();
group.add(this._operAddBlock);
group.add(oper);
editor.pushOperation(group);
this._operAddBlock = null;
}
else
editor.pushOperation(oper);
this.doneInsertOrModifyBasicObjects([mol]);
}
}
setter._applied = true;
},
/**
* Open formula edit box in coord.
* @param {Hash} coord
* @param {Object} mol
*/
openSetterUi: function(coord, mol)
{
var oldSetter = this.getTextSetter();
if (oldSetter && oldSetter.isShown()) // has a old setter
{
this.applySetter(oldSetter, this.getCurrMol());
oldSetter.hide();
oldSetter._haltPrevShowHideProcess();
}
if (!mol) // need create new
mol = this.createNewMol(this.getEditor().getChemObj(), coord);
if (!this.isValidMol(mol))
return;
this.setCurrMol(mol);
this.getEditor().setSelection([mol]);
//console.log(block.getCascadedRenderOption('fontSize'));
var fontSize = mol.getCascadedRenderOption('fontSize') || this.getEditor().getEditorConfigs().getInteractionConfigs().getAtomSetterFontSize();
fontSize *= this.getEditor().getZoom() || 1;
var fontName = mol.getCascadedRenderOption('fontFamily') || '';
//var posAdjust = fontSize / 1.5; // adjust position to align to atom center
var text = this.getFormulaText(mol);
var setter = this.getTextSetterWidget(true);
var parentElem = this.getEditor().getCoreElement();
//setter._setEnableShowHideEvents(false);
setter.appendToElem(parentElem); // ensure setter widget is a child of parentElem, since popup show may change the parent each time
//setter._setEnableShowHideEvents(true);
var slabel = text || '';
//console.log(block, text, slabel);
setter.setValue(slabel);
//setter.setValue('hehr');
//setter.setIsPopup(true);
var style = setter.getElement().style;
style.position = 'absolute';
style.fontSize = fontSize + 'px';
style.left = coord.x + 'px';
style.top = coord.y + 'px';
style.fontFamily = fontName;
/*
style.marginTop = -posAdjust + 'px';
style.marginLeft = -posAdjust + 'px';
*/
setter._applied = false;
setter.show(null, null, Kekule.Widget.ShowHideType.POPUP);
setter.selectAll();
setter.focus();
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MOUSE_BTN_LEFT)
{
this.getEditor().setSelection(null);
var coord = this._getEventMouseCoord(e);
{
var mol;
var boundItem = this.getEditor().getTopmostBoundInfoAtCoord(coord, null, this.getCurrBoundInflation());
if (boundItem)
{
var obj = boundItem.obj;
if (this.isValidMol(obj)) // can modify atom of this object
{
mol = obj;
}
}
/*
if (!block) // create new
{
block = this.createNewBlock(this.getEditor().getChemObj(), coord);
//console.log(coord, this.getEditor().getObjectScreenCoord(block));
}
if (block)
*/
{
//var baseCoord = mol? this.getEditor().getObjectScreenCoord(mol): coord;
var baseCoord;
if (boundItem && boundItem.boundInfo)
{
baseCoord = boundItem.boundInfo.coords[0];
}
else
baseCoord = coord;
e.preventDefault();
e.stopPropagation();
// important, prevent event bubble to document, otherwise reactDocumentClick will be evoked
// and the setter will be closed immediately.
this.openSetterUi(baseCoord, mol);
//this.getEditor().setSelection([block]);
return true; // important
}
}
}
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.FormulaIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Base controller to add or edit content block in document.
* @class
* @augments Kekule.Editor.BaseEditorIaController
*/
Kekule.Editor.ContentBlockIaController = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.ContentBlockIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.ContentBlockIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
},
/** @private */
initProperties: function()
{
this.defineProp('currBlock', {'dataType': DataType.OBJECT, 'serializable': false}); // private
},
/** @private */
canInteractWithObj: function($super, obj)
{
return (obj && this.isValidBlock(obj));
},
/**
* Check if obj is a valid text block and can be edited.
* Descendants must override this method.
* @param {Kekule.ChemObject} obj
* @returns {Bool}
* @private
*/
isValidBlock: function(obj)
{
return obj instanceof Kekule.ContentBlock;
},
/**
* Called when IaController is selected and mouse clicked on document.
* Need to modify block or create new one when param block is null.
* Descendants need to override this method.
* @param {Kekule.ChemSpace} chemSpace
* @param {Hash} baseCoord
* @param {Kekule.ContentBlock} block
*/
execute: function(chemSpace, baseCoord, block)
{
// do nothing here
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
this.getEditor().setSelection(null);
var coord = this._getEventMouseCoord(e);
{
var block;
var boundItem = this.getEditor().getTopmostBoundInfoAtCoord(coord, null, this.getCurrBoundInflation());
if (boundItem)
{
var obj = boundItem.obj;
if (this.isValidBlock(obj)) // can modify atom of this object
{
block = obj;
}
}
var baseCoord = block? this.getEditor().getObjectScreenCoord(block): coord;
e.preventDefault();
e.stopPropagation();
// important, prevent event bubble to document, otherwise reactDocumentClick will be evoked
// and the setter may be closed immediately.
//console.log('block execute', baseCoord, e.getTarget(), e.getCurrentTarget());
this.execute(this.getEditor().getChemObj(), baseCoord, block);
//this.getEditor().setSelection([block]);
return true; // important
}
}
}
});
/**
* Controller to add or edit text block in document.
* @class
* @augments Kekule.Editor.ContentBlockIaController
*/
Kekule.Editor.TextBlockIaController = Class.create(Kekule.Editor.ContentBlockIaController,
/** @lends Kekule.Editor.TextBlockIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.TextBlockIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this._operAddBlock = null; // private
},
/** @private */
initProperties: function()
{
//this.defineProp('currBlock', {'dataType': DataType.OBJECT, 'serializable': false}); // private
this.defineProp('textSetter', {'dataType': DataType.OBJECT, 'serializable': false}); // private
},
/** @ignore */
isValidBlock: function(obj)
{
return obj instanceof Kekule.TextBlock;
},
/**
* Create new content block on document.
* @private
*/
createNewBlock: function(chemSpace, coord)
{
var editor = this.getEditor();
if (!editor.canCreateNewChild())
return null;
editor.beginUpdateObject();
try
{
var block = new Kekule.TextBlock();
if (block)
{
chemSpace.appendChild(block);
editor.setObjectScreenCoord(block, coord, Kekule.Render.CoordPos.CORNER_TL);
//console.log('set text block cord', coord, block.getSize2D());
var addOperation = new Kekule.ChemObjOperation.Add(block, chemSpace, null, editor);
this._operAddBlock = addOperation;
}
}
finally
{
editor.endUpdateObject();
}
return block;
},
/**
* Returns text that shows in text block.
* @param {Kekule.TextBlock} block
* @returns {String}
* @private
*/
getBlockText: function(block)
{
return block.getText();
},
/** @ignore */
execute: function(chemSpace, baseCoord, block)
{
this.openSetterUi(baseCoord, block);
},
/** @private */
getTextSetterWidget: function(canCreate)
{
var result = this.getTextSetter();
if (!result && canCreate) // create new one
{
var parentElem = this.getEditor().getCoreElement();
var doc = parentElem.ownerDocument;
result = this._createTextSetterWidget(doc, parentElem);
this.setTextSetter(result);
}
return result;
},
/** @private */
_createTextSetterWidget: function(doc, parentElem)
{
var result = new Kekule.Widget.TextArea(this.getEditor());
result.setAutoSizeX(true);
result.setAutoSizeY(true);
result.setDisplayed(false);
result.addClassName(CCNS.CHEMEDITOR_TEXT_SETTER);
result.appendToElem(parentElem);
// event handler
var self = this;
result.addEventListener('keyup', function(e)
{
var ev = e.htmlEvent;
var keyCode = ev.getKeyCode();
if ((keyCode === Kekule.X.Event.KeyCode.ENTER) && (ev.getCtrlKey())) // ctrl+enter
{
self.applySetter(result);
result.dismiss(); // avoid call apply setter twice
}
else if (keyCode === Kekule.X.Event.KeyCode.ESC) // ESC, cancel editor
{
result.dismiss();
self.cancelSetter();
}
}
);
result.addEventListener('showStateChange', function(e)
{
if (!e.byDomChange)
{
if (!e.isShown && !e.isDismissed) // widget hidden, feedback the edited value
{
self.applySetter(result);
}
if (e.isShown) // set applied to false on newly shown widget
result._applied = false;
}
}
);
return result;
},
/** @private */
cancelSetter: function()
{
if (this._operAddBlock) // already created new textblock, remove it
{
this._operAddBlock.reverse();
this._operAddBlock = null;
}
},
/** @private */
applySetter: function(setter, block)
{
if (setter._applied) // avoid call twice
return;
if (!block)
block = this.getCurrBlock();
var text = setter.getText();
var oper;
if (!text)
{
if (this._operAddBlock) // just added text block
this.cancelSetter();
else
oper = new Kekule.ChemObjOperation.Remove(block, block.getParent(), null, this.getEditor());
}
else
oper = new Kekule.ChemObjOperation.Modify(block, {'text': text}, this.getEditor());
if (oper)
{
oper.execute();
var editor = this.getEditor();
if (editor && editor.getEnableOperHistory())
{
if (this._operAddBlock)
{
var group = new Kekule.MacroOperation();
group.add(this._operAddBlock);
group.add(oper);
editor.pushOperation(group);
this._operAddBlock = null;
}
else
editor.pushOperation(oper);
}
this.doneInsertOrModifyBasicObjects([block]);
}
setter._applied = true;
},
/**
* Open edit box for text block in coord.
* @param {Hash} coord
* @param {Object} block
*/
openSetterUi: function(coord, block)
{
var oldSetter = this.getTextSetter();
if (oldSetter && oldSetter.isShown()) // has a old setter
{
this.applySetter(oldSetter, this.getCurrBlock());
//oldSetter.dismiss();
oldSetter._haltPrevShowHideProcess();
}
if (!block) // need create new
{
block = this.createNewBlock(this.getEditor().getChemObj(), coord);
}
if (!this.isValidBlock(block))
return;
this.setCurrBlock(block);
//this.doneInsertOrModifyBasicObjects([block]);
//this.getEditor().setSelection([block]);
// calculate the top-left position of block
var setterCoord = this.getEditor().getObjectScreenCoord(block, Kekule.Render.CoordPos.CORNER_TL);
//console.log(block.getCascadedRenderOption('fontSize'));
var fontSize = block.getCascadedRenderOption('fontSize') || this.getEditor().getEditorConfigs().getInteractionConfigs().getAtomSetterFontSize();
fontSize *= (this.getEditor().getZoom() || 1);
var fontName = block.getCascadedRenderOption('fontFamily') || '';
//var posAdjust = fontSize / 1.5; // adjust position to align to atom center
var text = this.getBlockText(block);
var setter = this.getTextSetterWidget(true);
setter._applied = false;
//console.log(block, text, slabel);
setter.setValue(text);
var slabel = text || Kekule.$L('ChemWidgetTexts.CAPTION_TEXTBLOCK_INIT'); //Kekule.ChemWidgetTexts.CAPTION_TEXTBLOCK_INIT;
setter.setPlaceholder(slabel);
var parentElem = this.getEditor().getCoreElement();
setter.appendToElem(parentElem); // ensure setter widget is a child of parentElem, since popup show may change the parent each time
//setter.setValue('hehr');
//setter.setIsPopup(true);
var style = setter.getElement().style;
style.position = 'absolute';
style.fontSize = fontSize + 'px';
style.left = setterCoord.x + 'px';
style.top = setterCoord.y + 'px';
style.fontFamily = fontName;
/*
style.marginTop = -posAdjust + 'px';
style.marginLeft = -posAdjust + 'px';
*/
setter.show(null, null, Kekule.Widget.ShowHideType.POPUP);
(function()
{
setter.selectAll();
setter.focus();
}).defer();
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.TextBlockIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to add or edit image block in document.
* @class
* @augments Kekule.Editor.ContentBlockIaController
*/
Kekule.Editor.ImageBlockIaController = Class.create(Kekule.Editor.ContentBlockIaController,
/** @lends Kekule.Editor.ImageBlockIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.ImageBlockIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this._operAddBlock = null; // private
this._imgProbeElem = null;
this._actionOpenFile = this.createOpenAction();
},
doFinalize: function()
{
if (this._actionOpenFile)
this.actionOpenFile.finalize();
},
/** @private */
initProperties: function()
{
//this.defineProp('currBlock', {'dataType': DataType.OBJECT, 'serializable': false}); // private
},
/**
* Check if obj is a valid text block and can be edited.
* @param {Kekule.ChemObject} obj
* @returns {Bool}
* @private
*/
isValidBlock: function(obj)
{
return obj instanceof Kekule.ImageBlock;
},
/** @private */
createNewBlock: function(chemSpace, coord, size, src)
{
var editor = this.getEditor();
if (!editor.canCreateNewChild())
return null;
editor.beginUpdateObject();
try
{
var block = new Kekule.ImageBlock();
if (src)
block.setSrc(src);
if (size)
block.setSize2D(size);
//chemSpace.appendChild(block);
editor.setObjectScreenCoord(block, coord, Kekule.Render.CoordPos.CORNER_TL);
var addOperation = new Kekule.ChemObjOperation.Add(block, chemSpace, null, editor);
this._operAddBlock = addOperation;
}
finally
{
editor.endUpdateObject();
}
return block;
},
/** @private */
_getImgFilters: function()
{
// add png, jpg, gif and svg
var result = [
{'title': Kekule.$L('WidgetTexts.TITLE_IMG_FORMAT_PNG'), 'filter': '.png'},
{'title': Kekule.$L('WidgetTexts.TITLE_IMG_FORMAT_JPG'), 'filter': '.jpg,.jpeg'},
{'title': Kekule.$L('WidgetTexts.TITLE_IMG_FORMAT_GIF'), 'filter': '.gif'},
{'title': Kekule.$L('WidgetTexts.TITLE_IMG_FORMAT_SVG'), 'filter': '.svg'},
Kekule.NativeServices.FILTER_ALL_SUPPORT,
Kekule.NativeServices.FILTER_ANY
];
return result;
},
/** @private */
createOpenAction: function()
{
var result = new Kekule.ActionFileOpen();
result.setFilters(this._getImgFilters());
result.on('open', this.reactImageFileOpen, this);
return result;
},
/** @private */
reactImageFileOpen: function(e)
{
var file = e.file;
if (file)
{
var self = this;
var reader = new FileReader();
reader.addEventListener('load', function(){
var imgElem = self._getImageProbeElem();
var doc = imgElem.ownerDocument;
// hide imgElem and append it to body to calculate size
//imgElem.style.display = 'none';
//doc.body.appendChild(imgElem);
try
{
// clear img prev width/height
delete imgElem.width;
delete imgElem.height;
}
catch(e)
{
}
imgElem.src = reader.result;
var editor = self.getEditor();
//(function(){ console.log(imgElem.width, imgElem.height); }).defer();
(function(){
var size = {'x': imgElem.width, 'y': imgElem.height};
if (size.x <= 0 || size.y <= 0) // empty image
{
Kekule.error(Kekule.$L('ErrorMsg.INVALID_OR_EMPTY_IMAGE'));
return;
}
//console.log('imgSize', size);
//imgElem.parentNode.removeChild(imgElem);
//size = editor.translateCoord(size, Kekule.Editor.CoordSys.SCREEN, Kekule.Editor.CoordSys.CHEM);
//console.log('transSize', size);
var coord1 = self._currCoord;
var contextCoord1 = editor.objCoordToContext(coord1);
var contextCoord2 = Kekule.CoordUtils.add(contextCoord1, size);
var coord2 = editor.contextCoordToObj(contextCoord2);
var size = Kekule.CoordUtils.substract(coord2, coord1);
size = Kekule.CoordUtils.absValue(size);
var currBlock = self.getCurrBlock();
var oper;
var chemSpace = editor.getChemObj();
if (currBlock) // modify existed
{
oper = new Kekule.ChemObjOperation.Modify(currBlock, {'src': reader.result, 'size2D': size});
}
else // create new
{
var block = self.createNewBlock(self.getEditor().getChemObj(), self._currCoord, size, reader.result);
self.setCurrBlock(block);
oper = new Kekule.ChemObjOperation.Add(block, chemSpace, null, editor);
}
if (oper)
{
//editor.beginUpdateObject();
try
{
oper.execute();
if (editor && editor.getEnableOperHistory())
editor.pushOperation(oper);
self.doneInsertOrModifyBasicObjects([self.getCurrBlock()]);
}
finally
{
//editor.endUpdateObject();
}
}
}).defer(); // execute later, get accurate image size
});
reader.readAsDataURL(file);
}
},
/** @private */
_getImageProbeElem: function()
{
if (!this._imgProbeElem)
{
var doc = this.getEditor().getElement().ownerDocument;
this._imgProbeElem = doc.createElement('img');
}
return this._imgProbeElem;
},
/** @private */
execute: function(chemSpace, baseCoord, block)
{
this.setCurrBlock(block);
this._currCoord = baseCoord;
this._actionOpenFile.execute(this.getEditor());
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.ImageBlockIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to explicitly create attached markers to a existing object.
* @class
* @augments Kekule.Editor.BaseEditorIaController
*
* @property {Class} markerClass Class of marker that should be created.
* @property {Class} targetClass Class of the legal parent object of newly create marker.
* @property {Hash} initialPropValues Property values set to newly created marker.
*/
Kekule.Editor.AttachedMarkerIaController = Class.create(Kekule.Editor.BaseEditorIaController,
/** @lends Kekule.Editor.AttachedMarkerIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.AttachedMarkerIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
},
/** @private */
initProperties: function()
{
this.defineProp('markerClass', {'dataType': DataType.CLASS, 'serializable': false});
this.defineProp('targetClass', {'dataType': DataType.CLASS, 'serializable': false});
this.defineProp('markerClassName', {'dataType': DataType.STRING,
'getter': function() { return ClassEx.getClassName(this.getMarkerClass()); },
'setter': function(value) { this.setMarkerClass(ClassEx.findClass(value)); }
});
this.defineProp('targetClassName', {'dataType': DataType.STRING,
'getter': function() { return ClassEx.getClassName(this.getTargetClass()); },
'setter': function(value) { this.setTargetClass(ClassEx.findClass(value)); }
});
this.defineProp('initialPropValues', {'dataType': DataType.HASH});
},
/** @ignore */
canInteractWithObj: function($super, obj)
{
return this.isValidTarget(obj);
},
/**
* Check if obj is a valid object to add marker.
* @param {Kekule.ChemObject} obj
* @returns {Bool}
* @private
*/
isValidTarget: function(obj)
{
var targetClass = this.getTargetClass();
return (obj instanceof targetClass) || !targetClass;
},
/** @private */
createMarker: function()
{
var result;
var markerClass = this.getMarkerClass();
if (markerClass)
{
result = new markerClass();
var initValues = this.getInitialPropValues();
if (initValues && result && result.setPropValues)
{
result.setPropValues(initValues);
}
}
return result;
},
/** @private */
createOperations: function(targetObj)
{
var result;
var marker = this.createMarker();
if (marker) // add to target object
{
result = new Kekule.ChemObjOperation.Add(marker, targetObj, null, this.getEditor());
}
return [result];
},
/**
* Execute on the target object, add a new marker.
* @param {Kekule.ChemObject} targetObj
* @private
*/
apply: function(targetObj)
{
var operations = this.createOperations(targetObj);
var oper = (operations.length <= 0)? null:
(operations.length === 1)? operations[0]:
new Kekule.MacroOperation(operations);
if (oper)
{
oper.execute();
var editor = this.getEditor();
if (editor && editor.getEnableOperHistory())
{
editor.pushOperation(oper);
}
}
},
/** @private */
react_pointerup: function(e)
{
if (e.getButton() === Kekule.X.Event.MouseButton.LEFT)
{
//this.getEditor().setSelection(null);
var coord = this._getEventMouseCoord(e);
{
var boundItem = this.getEditor().getTopmostBoundInfoAtCoord(coord, null, this.getCurrBoundInflation());
if (boundItem)
{
var obj = boundItem.obj;
if (this.isValidTarget(obj)) // can add marker to this object
{
this.apply(obj);
e.preventDefault();
e.stopPropagation();
}
return true; // important
}
}
}
}
});
Kekule.Editor.IaControllerManager.register(Kekule.Editor.AttachedMarkerIaController, Kekule.Editor.ChemSpaceEditor);
/**
* Controller to set atom/group charge and radical.
* @class
* @augments Kekule.Editor.AttachedMarkerIaController
*
* @property {Number} chargeInc The node's charge will become charge + chargeInc after execution of controller.
* @property {Number} charge The node's charge will become this value after execution of controller.
* @property {Int} radical Radical type set to node
*/
Kekule.Editor.MolNodeChargeIaController = Class.create(Kekule.Editor.AttachedMarkerIaController,
/** @lends Kekule.Editor.MolNodeChargeIaController# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.MolNodeChargeIaController',
/** @construct */
initialize: function($super, editor)
{
$super(editor);
this.setChargeInc(1); // default is +1
},
/** @private */
initProperties: function()
{
this.defineProp('chargeInc', {'dataType': DataType.NUMBER, 'serializable': false});
this.defineProp('charge', {'dataType': DataType.NUMBER, 'serializable': false});
this.defineProp('radical', {'dataType': DataType.INT, 'serializable': false});
this.defineProp('currNode', {'dataType': DataType.OBJECT, 'serializable': false}); // private
},
initPropValues: function($super)
{
$super();
this.setTargetClass(Kekule.ChemStructureNode);
},
/** @private */
createOperations: function(node)
{
var result;
// check charge
var chargeModified = false;
var radicalModified = false;
var modifiedData = {};
var charge = node.getCharge();
if (Kekule.ObjUtils.notUnset(this.getCharge()))
{
if (charge !== this.getCharge())
{
modifiedData.charge = this.getCharge();
chargeModified = true;
}
}
else if (this.getChargeInc())
{
charge += this.getChargeInc();
modifiedData.charge = charge;
chargeModified = true;
}
var radical = node.getRadical();
if (this.getRadical() !== radical)
{
//console.log(radical, this.getRadical());
modifiedData.radical = this.getRadical();
radicalModified = true;
}
var result = [];
// add or remove marker operation
var markerOperations = [];
var marker;
var editor = this.getEditor();
if (chargeModified)
{
var chargeMarker = node.fetchChargeMarker(false); // do not auto create
if (modifiedData.charge !== 0 && !chargeMarker)
{
// need add new charge marker
marker = new Kekule.ChemMarker.Charge();
marker.setValue(modifiedData.charge);
markerOperations.push(new Kekule.ChemObjOperation.Add(marker, node, null, editor));
}
if (modifiedData.charge === 0 && chargeMarker)
{
// need remove existing charge marker
markerOperations.push(new Kekule.ChemObjOperation.Remove(chargeMarker, null, null, editor));
}
}
if (radicalModified)
{
var radicalMarker = node.fetchRadicalMarker(false); // do not auto create
if (modifiedData.radical !== 0 && !radicalMarker)
{
// need add new radical marker
marker = new Kekule.ChemMarker.Radical();
marker.setValue(modifiedData.radical);
markerOperations.push(new Kekule.ChemObjOperation.Add(marker, node, null, editor));
}
if (modifiedData.radical === 0 && radicalMarker)
{
// need remove existing radical marker
markerOperations.push(new Kekule.ChemObjOperation.Remove(radicalMarker, null, null, editor));
}
}
result = result.concat(markerOperations);
// modifcation operation
var propModifyOper;
if (chargeModified || radicalModified)
{
propModifyOper = new Kekule.ChemObjOperation.Modify(node, modifiedData, this.getEditor());
result.push(propModifyOper);
}
return result;
}
});
// register
Kekule.Editor.IaControllerManager.register(Kekule.Editor.MolNodeChargeIaController, Kekule.Editor.ChemSpaceEditor);
})();