kekule
Version:
Open source JavaScript toolkit for chemoinformatics
1,460 lines (1,373 loc) • 268 kB
JavaScript
/**
* @fileoverview
* Base types and classes used by chem editor.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /chemDoc/issueCheckers/kekule.issueCheckers.js
* requires /widgets/operation/kekule.operations.js
* requires /render/kekule.render.base.js
* requires /render/kekule.render.boundInfoRecorder.js
* requires /html/xbrowsers/kekule.x.js
* requires /widgets/kekule.widget.base.js
* requires /widgets/chem/kekule.chemWidget.chemObjDisplayers.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.modifications.js
*/
(function(){
"use strict";
var OU = Kekule.ObjUtils;
var AU = Kekule.ArrayUtils;
var EU = Kekule.HtmlElementUtils;
var CU = Kekule.CoordUtils;
var CNS = Kekule.Widget.HtmlClassNames;
var CCNS = Kekule.ChemWidget.HtmlClassNames;
/** @ignore */
Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, {
EDITOR: 'K-Chem-Editor',
EDITOR_CLIENT: 'K-Chem-Editor-Client',
EDITOR_UIEVENT_RECEIVER: 'K-Chem-Editor-UiEvent-Receiver',
EDITOR2D: 'K-Chem-Editor2D',
EDITOR3D: 'K-Chem-Editor3D'
});
/**
* Namespace for chem editor.
* @namespace
*/
Kekule.ChemWidget.Editor = {};
/**
* Alias to {@link Kekule.ChemWidget.Editor}.
* @namespace
*/
Kekule.Editor = Kekule.ChemWidget.Editor;
/**
* In editor, there exist three types of coord: one based on object system (inner coord),
* another one based on context of editor (outer coord, context coord),
* and the third based on screen.
* This enum is an alias of Kekule.Render.CoordSystem
* @class
*/
Kekule.Editor.CoordSys = Kekule.Render.CoordSystem;
/**
* Enumeration of regions in/out box.
* @enum
* @ignore
*/
Kekule.Editor.BoxRegion = {
OUTSIDE: 0,
CORNER_TL: 1,
CORNER_TR: 2,
CORNER_BL: 3,
CORNER_BR: 4,
EDGE_TOP: 11,
EDGE_LEFT: 12,
EDGE_BOTTOM: 13,
EDGE_RIGHT: 14,
INSIDE: 20
};
/**
* Enumeration of mode in selecting object in editor.
* @enum
* @ignore
*/
Kekule.Editor.SelectMode = {
/** Draw a box in editor when selecting, select all object inside a box. **/
RECT: 0,
/** Draw a curve in editor when selecting, select all object inside this curve polygon. **/
POLYGON: 1,
/** Draw a curve in editor when selecting, select all object intersecting this curve. **/
POLYLINE: 2,
/** Click on a child object to select the whole standalone ancestor. **/
ANCESTOR: 10
};
// add some global options
Kekule.globalOptions.add('chemWidget.editor', {
'enableIssueCheck': true,
'enableCreateNewDoc': true,
'enableOperHistory': true,
'enableOperContext': true,
'initOnNewDoc': true,
'enableSelect': true,
'enableMove': true,
'enableResize': true,
'enableAspectRatioLockedResize': true,
'enableRotate': true,
'enableGesture': true
});
Kekule.globalOptions.add('chemWidget.editor.issueChecker', {
'enableAutoIssueCheck': true,
'enableAutoScrollToActiveIssue': true,
'enableIssueMarkerHint': true,
'durationLimit': 50 // issue check must be finished in 50ms, avoid blocking the UI
});
/**
* A base chem editor.
* @class
* @augments Kekule.ChemWidget.ChemObjDisplayer
* @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.Editor.BaseEditorConfigs} editorConfigs Configuration of this editor.
* @property {Bool} enableCreateNewDoc Whether create new object in editor is allowed.
* @property {Bool} initOnNewDoc Whether create a new doc when editor instance is initialized.
* Note, the new doc will only be created when property enableCreateNewDoc is true.
* @property {Bool} enableOperHistory Whether undo/redo is enabled.
* @property {Kekule.OperationHistory} operHistory History of operations. Used to enable undo/redo function.
* @property {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}.
* @property {Kekule.ChemObject} chemObj The root object in editor.
* @property {Bool} enableIssueCheck Whether issue check is available in editor.
* @property {Array} issueCheckerIds Issue checker class IDs used in editor.
* @property {Bool} enableAutoIssueCheck Whether the issue checking is automatically executed when objects changing in editor.
* @property {Array} issueCheckResults Array of {@link Kekule.IssueCheck.CheckResult}, results of auto or manual check.
* @property {Kekule.IssueCheck.CheckResult} activeIssueCheckResult Current selected issue check result in issue inspector.
* @property {Bool} showAllIssueMarkers Whether all issue markers shouled be marked in editor.
* Note, the active issue will always be marked.
* @property {Bool} enableIssueMarkerHint Whether display hint text on issue markers.
* @property {Bool} enableAutoScrollToActiveIssue Whether the editor will automatically scroll to the issue object when selecting in issue inspector.
* @property {Bool} enableOperContext If this property is set to true, object being modified will be drawn in a
* separate context to accelerate the interface refreshing.
* @property {Object} objContext Context to draw basic chem objects. Can be 2D or 3D context. Alias of property drawContext
* @property {Object} operContext Context to draw objects being operated. Can be 2D or 3D context.
* @property {Object} uiContext Context to draw UI marks. Usually this is a 2D context.
* @property {Object} objDrawBridge Bridge to draw chem objects. Alias of property drawBridge.
* @property {Object} uiDrawBridge Bridge to draw UI markers.
* @property {Int} selectMode Value from Kekule.Editor.SelectMode, set the mode of selecting operation in editor.
* @property {Array} selection An array of selected basic object.
* @property {Hash} zoomCenter The center coord (based on client element) when zooming editor.
* //@property {Bool} standardizeObjectsBeforeSaving Whether standardize molecules (and other possible objects) before saving them.
*/
/**
* Invoked when the an chem object is loaded into editor.
* event param of it has one fields: {obj: Object}
* @name Kekule.Editor.BaseEditor#load
* @event
*/
/**
* Invoked when the chem object inside editor is changed.
* event param of it has one fields: {obj: Object, propNames: Array}
* @name Kekule.Editor.BaseEditor#editObjChanged
* @event
*/
/**
* Invoked when multiple chem objects inside editor is changed.
* event param of it has one fields: {details}.
* @name Kekule.Editor.BaseEditor#editObjsChanged
* @event
*/
/**
* Invoked when chem objects inside editor is changed and the changes has been updated by editor.
* event param of it has one fields: {details}.
* Note: this event is not the same as editObjsChanged. When beginUpdateObj is called, editObjsChanged
* event still will be invoked but editObjsUpdated event will be suppressed.
* @name Kekule.Editor.BaseEditor#editObjsUpdated
* @event
*/
/**
* Invoked when the selected objects in editor has been changed.
* When beginUpdateObj is called, selectedObjsUpdated event will be suppressed.
* event param of it has one fields: {objs}.
* @name Kekule.Editor.BaseEditor#selectedObjsUpdated
* @event
*/
/**
* Invoked when the pointer (usually the mouse) hovering on basic objects(s) in editor.
* Event param of it has field {objs}. When the pointer move out of the obj, the objs field will be a empty array.
* @name Kekule.Editor.BaseEditor#hoverOnObjs
* @event
*/
/**
* Invoked when the selection in editor has been changed.
* @name Kekule.Editor.BaseEditor#selectionChange
* @event
*/
/**
* Invoked when user begin to do manipulation in the editor.
* @name Kekule.Editor.BaseEditor#beginManipulateObject
* @event
*/
/**
* Invoked when the user manipulation (add new objects, move, rotate...) ends in the editor.
* This event is a safe and effective opportunity to retrieve the changes in editor.
* Note: operation undo/redo will change the object inside editor but will not evoke this event.
* @name Kekule.Editor.BaseEditor#endManipulateObject
* @event
*/
/**
* Invoked when the user modification to chem object ends in the editor.
* This event occurs when manipulation done or operation undone/redone.
* It is a safe and effective opportunity to retrieve the changes in editor.
* Note: operation undo/redo will change the object inside editor and will also evoke this event.
* @name Kekule.Editor.BaseEditor#userModificationDone
* @event
*/
/**
* Invoked when the operation history has modifications.
* @name Kekule.Editor.BaseEditor#operChange
* @event
*/
/**
* Invoked when the an operation is pushed into operation history.
* event param of it has one fields: {operation: Kekule.Operation}
* @name Kekule.Editor.BaseEditor#operPush
* @event
*/
/**
* Invoked when the an operation is popped from history.
* event param of it has one fields: {operation: Kekule.Operation}
* @name Kekule.Editor.BaseEditor#operPop
* @event
*/
/**
* Invoked when one operation is undone.
* event param of it has two fields: {operation: Kekule.Operation, currOperIndex: Int}
* @name Kekule.Editor.BaseEditor#operUndo
* @event
*/
/**
* Invoked when one operation is redone.
* event param of it has two fields: {operation: Kekule.Operation, currOperIndex: Int}
* @name Kekule.Editor.BaseEditor#operRedo
* @event
*/
/**
* Invoked when the operation history is cleared.
* event param of it has one field: {currOperIndex: Int}
* @name Kekule.Editor.BaseEditor#operHistoryClear
* @event
*/
Kekule.Editor.BaseEditor = Class.create(Kekule.ChemWidget.ChemObjDisplayer,
/** @lends Kekule.Editor.BaseEditor# */
{
/** @private */
CLASS_NAME: 'Kekule.Editor.BaseEditor',
/** @private */
BINDABLE_TAG_NAMES: ['div', 'span'],
/** @private */
OBSERVING_GESTURES: ['rotate', 'rotatestart', 'rotatemove', 'rotateend', 'rotatecancel',
'pinch', 'pinchstart', 'pinchmove', 'pinchend', 'pinchcancel', 'pinchin', 'pinchout'],
/** @constructs */
initialize: function(/*$super, */parentOrElementOrDocument, chemObj, renderType, editorConfigs)
{
this._objSelectFlag = 0; // used internally
this._objectUpdateFlag = 0; // used internally
this._objectManipulateFlag = 0; // used internally
this._uiMarkerUpdateFlag = 0; // used internally
this._updatedObjectDetails = []; // used internally
this._operatingObjs = []; // used internally
this._operatingRenderers = []; // used internally
this._initialRenderTransformParams = null; // used internally, must init before $super
// as in $super, chemObj may be loaded and _initialRenderTransformParams will be set at that time
this._objChanged = false; // used internally, mark whether some changes has been made to chem object
this._lengthCaches = {}; // used internally, stores some value related to distance and length
var getOptionValue = Kekule.globalOptions.get;
/*
this.setPropStoreFieldValue('enableIssueCheck', true);
this.setPropStoreFieldValue('enableCreateNewDoc', true);
this.setPropStoreFieldValue('enableOperHistory', true);
this.setPropStoreFieldValue('enableOperContext', true);
this.setPropStoreFieldValue('initOnNewDoc', true);
*/
this.setPropStoreFieldValue('enableIssueCheck', getOptionValue('chemWidget.editor.enableIssueCheck', true));
this.setPropStoreFieldValue('enableCreateNewDoc', getOptionValue('chemWidget.editor.enableCreateNewDoc', true));
this.setPropStoreFieldValue('enableOperHistory', getOptionValue('chemWidget.editor.enableOperHistory', true));
this.setPropStoreFieldValue('enableOperContext', getOptionValue('chemWidget.editor.enableOperContext', true));
this.setPropStoreFieldValue('initOnNewDoc', getOptionValue('chemWidget.editor.initOnNewDoc', true));
//this.setPropStoreFieldValue('initialZoom', 1.5);
//this.setPropStoreFieldValue('selectMode', Kekule.Editor.SelectMode.POLYGON); // debug
this.tryApplySuper('initialize', [parentOrElementOrDocument, chemObj, renderType]) /* $super(parentOrElementOrDocument, chemObj, renderType) */;
//this.initEventHandlers();
if (!this.getChemObj() && this.getInitOnNewDoc() && this.getEnableCreateNewDoc())
this.newDoc();
this.setPropStoreFieldValue('editorConfigs', editorConfigs || this.createDefaultConfigs());
//this.setPropStoreFieldValue('uiMarkers', []);
//this.setEnableGesture(true);
if (Kekule.ObjUtils.isUnset(this.getEnableGesture()))
this.setEnableGesture(getOptionValue('chemWidget.editor.enableGesture', true));
},
/** @private */
initProperties: function()
{
this.defineProp('editorConfigs', {'dataType': 'Kekule.Editor.BaseEditorConfigs', 'serializable': false,
'getter': function() { return this.getDisplayerConfigs(); },
'setter': function(value) { return this.setDisplayerConfigs(value); }
});
this.defineProp('defBondLength', {'dataType': DataType.FLOAT, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('defBondLength');
if (!result)
result = this.getEditorConfigs().getStructureConfigs().getDefBondLength();
return result;
}
});
this.defineProp('defBondScreenLength', {'dataType': DataType.FLOAT, 'serializable': false, 'setter': null,
'getter': function()
{
/*
var result = this.getPropStoreFieldValue('defBondScreenLength');
if (!result)
{
var bLength = this.getDefBondLength();
result = this.translateDistance(bLength, Kekule.Render.CoordSys.CHEM, Kekule.Render.CoordSys.SCREEN);
}
return result;
*/
var cached = this._lengthCaches.defBondScreenLength;
if (cached)
return cached;
else
{
var bLength = this.getDefBondLength() || 0;
var result = this.translateDistance(bLength, Kekule.Render.CoordSystem.CHEM, Kekule.Render.CoordSystem.SCREEN);
this._lengthCaches.defBondScreenLength = result;
return result;
}
}
});
// Different pointer event (mouse, touch) has different bound inflation settings, stores here
this.defineProp('currBoundInflation', {'dataType': DataType.NUMBER, 'serializable': false, 'setter': null,
'getter': function(){
var pType = this.getCurrPointerType();
return this.getInteractionBoundInflation(pType);
}
});
// The recent pointer device interacted with this editor
this.defineProp('currPointerType', {'dataType': DataType.STRING, 'serializable': false});
//this.defineProp('standardizeObjectsBeforeSaving', {'dataType': DataType.BOOL});
this.defineProp('enableCreateNewDoc', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('initOnNewDoc', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('enableOperHistory', {'dataType': DataType.BOOL, 'serializable': false});
this.defineProp('operHistory', {
'dataType': 'Kekule.OperationHistory', 'serializable': false,
'getter': function()
{
/*
if (!this.getEnableOperHistory())
return null;
*/
var result = this.getPropStoreFieldValue('operHistory');
if (!result)
{
result = new Kekule.OperationHistory();
this.setPropStoreFieldValue('operHistory', result);
// install event handlers
result.addEventListener('push', this.reactOperHistoryPush, this);
result.addEventListener('pop', this.reactOperHistoryPop, this);
result.addEventListener('undo', this.reactOperHistoryUndo, this);
result.addEventListener('redo', this.reactOperHistoryRedo, this);
result.addEventListener('clear', this.reactOperHistoryClear, this);
result.addEventListener('change', this.reactOperHistoryChange, this);
}
return result;
},
'setter': null
});
this.defineProp('operationsInCurrManipulation', {'dataType': DataType.ARRAY, 'scope': Class.PropertyScope.PRIVATE, 'serializable': false}); // private
this.defineProp('selection', {'dataType': DataType.ARRAY, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('selection');
if (!result)
{
result = [];
this.setPropStoreFieldValue('selection', result);
}
return result;
},
'setter': function(value)
{
this.setPropStoreFieldValue('selection', value);
this.selectionChanged();
}
});
this.defineProp('selectMode', {'dataType': DataType.INT,
'getter': function()
{
var result = this.getPropStoreFieldValue('selectMode');
if (Kekule.ObjUtils.isUnset(result))
result = Kekule.Editor.SelectMode.RECT; // default value
return result;
},
'setter': function(value)
{
if (this.getSelectMode() !== value)
{
//console.log('set select mode', value);
this.setPropStoreFieldValue('selectMode', value);
this.hideSelectingMarker();
}
}
});
// private, whether defaultly select in toggle mode
this.defineProp('isToggleSelectOn', {'dataType': DataType.BOOL});
this.defineProp('hotTrackedObjs', {'dataType': DataType.ARRAY, 'serializable': false,
'setter': function(value)
{
/*
if (this.getHotTrackedObjs() === value)
return;
*/
var objs = value? Kekule.ArrayUtils.toArray(value): [];
//console.log('setHotTrackedObjs', objs);
if (this.getEditorConfigs() && this.getEditorConfigs().getInteractionConfigs().getEnableHotTrack())
{
this.setPropStoreFieldValue('hotTrackedObjs', objs);
var bounds;
if (objs && objs.length)
{
bounds = [];
for (var i = 0, l = objs.length; i < l; ++i)
{
var bound = this.getBoundInfoRecorder().getBound(this.getObjContext(), objs[i]);
if (bounds)
{
//bounds.push(bound);
Kekule.ArrayUtils.pushUnique(bounds, bound); // bound may be an array of composite shape
}
}
}
if (bounds)
{
this.changeHotTrackMarkerBounds(bounds);
//console.log('show');
}
else
{
if (this.getUiHotTrackMarker().getVisible())
this.hideHotTrackMarker();
//console.log('hide');
}
}
}
});
this.defineProp('hotTrackedObj', {'dataType': DataType.OBJECT, 'serializable': false,
'getter': function() { return this.getHotTrackedObjs() && this.getHotTrackedObjs()[0]; },
'setter': function(value) { this.setHotTrackedObjs(value); }
});
this.defineProp('hoveredBasicObjs', {'dataType': DataType.ARRAY, 'serializable': false}); // a readonly array caching the basic objects at current pointer position
this.defineProp('enableOperContext', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('enableOperContext', !!value);
if (!value) // release operContext
{
var ctx = this.getPropStoreFieldValue('operContext');
var b = this.getPropStoreFieldValue('drawBridge');
if (b && ctx)
b.releaseContext(ctx);
}
}
});
this.defineProp('issueCheckExecutor', {'dataType': 'Kekule.IssueCheck.Executor', 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('issueCheckExecutor');
if (!result) // create default executor
{
result = this.createIssueCheckExecutor(); // new Kekule.IssueCheck.Executor();
var self = this;
result.addEventListener('execute', function(e){
self.setIssueCheckResults(e.checkResults);
});
this.setPropStoreFieldValue('issueCheckExecutor', result);
}
return result;
}
});
this.defineProp('issueCheckerIds', {'dataType': DataType.ARRAY,
'getter': function() { return this.getIssueCheckExecutor().getCheckerIds(); },
'setter': function(value) { this.getIssueCheckExecutor().setCheckerIds(value); }
});
this.defineProp('enableIssueCheck', {'dataType': DataType.BOOL,
'getter': function() { return this.getIssueCheckExecutor().getEnabled(); },
'setter': function(value) {
this.getIssueCheckExecutor().setEnabled(!!value);
if (!value) // when disable issue check, clear the check results
{
this.setIssueCheckResults(null);
}
}
});
this.defineProp('issueCheckDurationLimit', {'dataType': DataType.NUMBER,
'getter': function() { return this.getIssueCheckExecutor().getDurationLimit(); },
'setter': function(value) { this.getIssueCheckExecutor().setDurationLimit(value); }
});
this.defineProp('enableAutoIssueCheck', {'dataType': DataType.BOOL,
'setter': function(value)
{
if (!!value !== this.getEnableAutoIssueCheck())
{
this.setPropStoreFieldValue('enableAutoIssueCheck', !!value);
if (value) // when turn on from off, do a issue check
this.checkIssues();
// adjust property showAllIssueMarkers according to enableAutoIssueCheck
// If auto check is on, markers should be defaultly opened;
// if auto check is off, markers should be defaultly hidden.
this.setShowAllIssueMarkers(!!value);
}
}});
this.defineProp('issueCheckResults', {'dataType': DataType.ARRAY, 'serializable': false,
'setter': function(value)
{
var oldActive = this.getActiveIssueCheckResult();
this.setPropStoreFieldValue('issueCheckResults', value);
if (oldActive && (value || []).indexOf(oldActive) < 0)
this.setPropStoreFieldValue('activeIssueCheckResult', null);
this.issueCheckResultsChanged();
}
});
this.defineProp('activeIssueCheckResult', {'dataType': DataType.OBJECT, 'serializable': false,
'setter': function(value)
{
if (this.getActiveIssueCheckResult() !== value)
{
this.setPropStoreFieldValue('activeIssueCheckResult', value);
if (value) // when set active issue, deselect all selections to extrusive it
this.deselectAll();
this.issueCheckResultsChanged();
}
}
});
this.defineProp('enableAutoScrollToActiveIssue', {'dataType': DataType.BOOL});
this.defineProp('showAllIssueMarkers', {'dataType': DataType.BOOL,
'setter': function(value)
{
if (!!value !== this.getShowAllIssueMarkers())
{
this.setPropStoreFieldValue('showAllIssueMarkers', !!value);
this.recalcIssueCheckUiMarkers();
}
}
});
this.defineProp('enableIssueMarkerHint', {'dataType': DataType.BOOL});
this.defineProp('enableGesture', {'dataType': DataType.BOOL,
'setter': function(value)
{
var bValue = !!value;
if (this.getEnableGesture() !== bValue)
{
this.setPropStoreFieldValue('enableGesture', bValue);
if (bValue)
{
this.startObservingGestureEvents(this.OBSERVING_GESTURES);
}
else
{
this.stopObservingGestureEvents(this.OBSERVING_GESTURES);
}
}
}
});
// private
this.defineProp('uiEventReceiverElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
// context parent properties, private
this.defineProp('objContextParentElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
this.defineProp('operContextParentElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
this.defineProp('uiContextParentElem', {'dataType': DataType.OBJECT, 'serializable': false, setter: null});
this.defineProp('objContext', {'dataType': DataType.OBJECT, 'serializable': false, setter: null,
'getter': function() { return this.getDrawContext(); }
});
this.defineProp('operContext', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function()
{
if (!this.getEnableOperContext())
return null;
else
{
var result = this.getPropStoreFieldValue('operContext');
if (!result)
{
var bridge = this.getDrawBridge();
if (bridge)
{
var elem = this.getOperContextParentElem();
if (!elem)
return null;
else
{
var dim = Kekule.HtmlElementUtils.getElemScrollDimension(elem);
result = bridge.createContext(elem, dim.width, dim.height);
this.setPropStoreFieldValue('operContext', result);
}
}
}
return result;
}
}
});
this.defineProp('uiContext', {'dataType': DataType.OBJECT, 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiContext');
if (!result)
{
var bridge = this.getUiDrawBridge();
if (bridge)
{
var elem = this.getUiContextParentElem();
if (!elem)
return null;
else
{
var dim = Kekule.HtmlElementUtils.getElemScrollDimension(elem);
//var dim = Kekule.HtmlElementUtils.getElemClientDimension(elem);
result = bridge.createContext(elem, dim.width, dim.height);
this.setPropStoreFieldValue('uiContext', result);
}
}
}
return result;
}
});
this.defineProp('objDrawBridge', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function() { return this.getDrawBridge(); }
});
this.defineProp('uiDrawBridge', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiDrawBridge');
if (!result && !this.__$uiDrawBridgeInitialized$__)
{
this.__$uiDrawBridgeInitialized$__ = true;
result = this.createUiDrawBridge();
this.setPropStoreFieldValue('uiDrawBridge', result);
}
return result;
}
});
this.defineProp('uiPainter', {'dataType': 'Kekule.Render.ChemObjPainter', 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiPainter');
if (!result)
{
// ui painter will always in 2D mode
var markers = this.getUiMarkers();
result = new Kekule.Render.ChemObjPainter(Kekule.Render.RendererType.R2D, markers, this.getUiDrawBridge());
result.setCanModifyTargetObj(true);
this.setPropStoreFieldValue('uiPainter', result);
return result;
}
return result;
}
});
this.defineProp('uiRenderer', {'dataType': 'Kekule.Render.AbstractRenderer', 'serializable': false, 'setter': null,
'getter': function()
{
var p = this.getUiPainter();
if (p)
{
var r = p.getRenderer();
if (!r)
p.prepareRenderer();
return p.getRenderer() || null;
}
else
return null;
}
});
// private ui marks properties
//this.defineProp('uiMarkers', {'dataType': DataType.ARRAY, 'serializable': false, 'setter': null});
this.defineProp('uiMarkers', {'dataType': 'Kekule.ChemWidget.UiMarkerCollection', 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('uiMarkers');
if (!result)
{
result = new Kekule.ChemWidget.UiMarkerCollection();
this.setPropStoreFieldValue('uiMarkers', result);
}
return result;
}
});
/*
this.defineProp('uiHotTrackMarker', {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function() { return this.getUiMarkers().hotTrackMarker; },
'setter': function(value) { this.getUiMarkers().hotTrackMarker = value; }
});
this.defineProp('uiSelectionAreaMarker', {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function() { return this.getUiMarkers().selectionAreaMarker; },
'setter': function(value) { this.getUiMarkers().selectionAreaMarker = value; }
});
this.defineProp('uiSelectingMarker', {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function() { return this.getUiMarkers().selectingMarker; },
'setter': function(value) { this.getUiMarkers().selectingMarker = value; }
}); // marker of selecting rubber band
*/
this._defineUiMarkerProp('uiHotTrackMarker');
this._defineUiMarkerProp('uiSelectionAreaMarker'); // marker of selected range
this._defineUiMarkerProp('uiSelectingMarker'); // marker of selecting rubber band
this._defineIssueCheckUiMarkerGroupProps(); // error check marker group
this.defineProp('uiSelectionAreaContainerBox',
{'dataType': DataType.Object, 'serializable': false, 'scope': Class.PropertyScope.PRIVATE});
// a private chemObj-renderer map
this.defineProp('objRendererMap', {'dataType': 'Kekule.MapEx', 'serializable': false, 'setter': null,
'getter': function()
{
var result = this.getPropStoreFieldValue('objRendererMap');
if (!result)
{
result = new Kekule.MapEx(true);
this.setPropStoreFieldValue('objRendererMap', result);
}
return result;
}
});
// private object to record all bound infos
//this.defineProp('boundInfoRecorder', {'dataType': 'Kekule.Render.BoundInfoRecorder', 'serializable': false, 'setter': null});
this.defineProp('zoomCenter', {'dataType': DataType.HASH});
},
/** @ignore */
initPropValues: function(/*$super*/)
{
this.tryApplySuper('initPropValues') /* $super() */;
this.setOperationsInCurrManipulation([]);
/*
this.setEnableAutoIssueCheck(false);
this.setEnableAutoScrollToActiveIssue(true);
var ICIDs = Kekule.IssueCheck.CheckerIds;
this.setIssueCheckerIds([ICIDs.ATOM_VALENCE, ICIDs.BOND_ORDER, ICIDs.NODE_DISTANCE_2D]);
*/
var ICIDs = Kekule.IssueCheck.CheckerIds;
var getGlobalOptionValue = Kekule.globalOptions.get;
this.setEnableAutoIssueCheck(getGlobalOptionValue('chemWidget.editor.issueChecker.enableAutoIssueCheck', true));
this.setEnableAutoScrollToActiveIssue(getGlobalOptionValue('chemWidget.editor.issueChecker.enableAutoScrollToActiveIssue', true));
this.setIssueCheckerIds(getGlobalOptionValue('chemWidget.editor.issueChecker.issueCheckerIds', [ICIDs.ATOM_VALENCE, ICIDs.BOND_ORDER, ICIDs.NODE_DISTANCE_2D]));
this.setIssueCheckDurationLimit(getGlobalOptionValue('chemWidget.editor.issueChecker.durationLimit') || null);
this.setEnableIssueMarkerHint(getGlobalOptionValue('chemWidget.editor.issueChecker.enableIssueMarkerHint') || this.getEnableAutoIssueCheck());
},
/** @private */
_defineUiMarkerProp: function(propName, uiMarkerCollection)
{
return this.defineProp(propName, {'dataType': 'Kekule.ChemWidget.AbstractUIMarker', 'serializable': false,
'getter': function()
{
var result = this.getPropStoreFieldValue(propName);
if (!result)
{
result = this.createShapeBasedMarker(propName, null, null, false); // prop value already be set in createShapeBasedMarker method
}
return result;
},
'setter': function(value)
{
if (!uiMarkerCollection)
uiMarkerCollection = this.getUiMarkers();
var old = this.getPropValue(propName);
if (old)
{
uiMarkerCollection.removeMarker(old);
old.finalize();
}
uiMarkerCollection.addMarker(value);
this.setPropStoreFieldValue(propName, value);
}
});
},
/** @private */
_defineIssueCheckUiMarkerGroupProps: function(uiMarkerCollection)
{
var EL = Kekule.ErrorLevel;
var getSubPropName = function(baseName, errorLevel)
{
return baseName + '_' + EL.levelToString(errorLevel);
};
var baseName = 'issueCheckUiMarker';
var errorLevels = [EL.ERROR, EL.WARNING, EL.NOTE, EL.LOG];
for (var i = 0, l = errorLevels.length; i < l; ++i)
{
var pname = getSubPropName(baseName, errorLevels[i]);
this._defineUiMarkerProp(pname, uiMarkerCollection);
}
this._defineUiMarkerProp(baseName + '_active', uiMarkerCollection); // 'active' is special marker to mark the selected issue objects
// define getter/setter method for group
this['get' + baseName.upperFirst()] = function(level){
var p = getSubPropName(baseName, level);
return this.getPropValue(p);
};
this['set' + baseName.upperFirst()] = function(level, value){
var p = getSubPropName(baseName, level);
return this.setPropValue(p, value);
};
this['getActive' + baseName.upperFirst()] = function()
{
return this.getPropValue(baseName + '_active');
};
this['getAll' + baseName.upperFirst() + 's'] = function(){
var result = [];
for (var i = 0, l = errorLevels.length; i < l; ++i)
{
result.push(this.getIssueCheckUiMarker(errorLevels[i]));
}
var activeMarker = this.getActiveIssueCheckUiMarker();
if (activeMarker)
result.push(activeMarker);
return result;
}
},
/** @private */
doFinalize: function(/*$super*/)
{
var h = this.getPropStoreFieldValue('operHistory');
if (h)
{
h.finalize();
this.setPropStoreFieldValue('operHistory', null);
}
var b = this.getPropStoreFieldValue('objDrawBridge');
var ctx = this.getPropStoreFieldValue('operContext');
if (b && ctx)
{
b.releaseContext(ctx);
}
this.setPropStoreFieldValue('operContext', null);
var b = this.getPropStoreFieldValue('uiDrawBridge');
var ctx = this.getPropStoreFieldValue('uiContext');
if (b && ctx)
{
b.releaseContext(ctx);
}
this.setPropStoreFieldValue('uiDrawBridge', null);
this.setPropStoreFieldValue('uiContext', null);
var m = this.getPropStoreFieldValue('objRendererMap');
if (m)
m.finalize();
this.setPropStoreFieldValue('objRendererMap', null);
var e = this.getPropStoreFieldValue('issueCheckExecutor');
if (e)
e.finalize();
this.setPropStoreFieldValue('issueCheckExecutor', null);
this.tryApplySuper('doFinalize') /* $super() */;
},
/** @ignore */
elementBound: function(element)
{
this.setObserveElemResize(true);
},
/**
* Create a default editor config object.
* Descendants may override this method.
* @returns {Kekule.Editor.BaseEditorConfigs}
* @ignore
*/
createDefaultConfigs: function()
{
return new Kekule.Editor.BaseEditorConfigs();
},
/** @ignore */
doCreateRootElement: function(doc)
{
var result = doc.createElement('div');
return result;
},
/** @ignore */
doCreateSubElements: function(doc, rootElem)
{
var elem = doc.createElement('div');
elem.className = CCNS.EDITOR_CLIENT;
rootElem.appendChild(elem);
this._editClientElem = elem;
return [elem];
},
/** @ignore */
getCoreElement: function(/*$super*/)
{
return this._editClientElem || this.tryApplySuper('getCoreElement') /* $super() */;
},
/** @private */
getEditClientElem: function()
{
return this._editClientElem;
},
/** @ignore */
doGetWidgetClassName: function(/*$super*/)
{
var result = this.tryApplySuper('doGetWidgetClassName') /* $super() */ + ' ' + CCNS.EDITOR;
var additional = (this.getRenderType() === Kekule.Render.RendererType.R3D)?
CCNS.EDITOR3D: CCNS.EDITOR2D;
result += ' ' + additional;
return result;
},
/** @private */
doBindElement: function(element)
{
this.createContextParentElems();
this.createUiEventReceiverElem();
},
// override getter and setter of intialZoom property
/** @ignore */
doGetInitialZoom: function(/*$super*/)
{
var result;
var config = this.getEditorConfigs();
if (config)
result = config.getInteractionConfigs().getEditorInitialZoom();
if (!result)
result = this.tryApplySuper('doGetInitialZoom') /* $super() */;
return result;
},
/** @ignore */
doSetInitialZoom: function(/*$super, */value)
{
var config = this.getEditorConfigs();
if (config)
config.getInteractionConfigs().setEditorInitialZoom(value);
this.tryApplySuper('doSetInitialZoom', [value]) /* $super(value) */;
},
/** @ignore */
zoomTo: function(/*$super, */value, suspendRendering, zoomCenterCoord)
{
var CU = Kekule.CoordUtils;
var currZoomLevel = this.getCurrZoom();
var zoomLevel = value;
var result = this.tryApplySuper('zoomTo', [value, suspendRendering]) /* $super(value, suspendRendering) */;
// adjust zoom center
var selfElem = this.getElement();
var currScrollCoord = {'x': selfElem.scrollLeft, 'y': selfElem.scrollTop};
if (!zoomCenterCoord)
zoomCenterCoord = this.getZoomCenter();
if (!zoomCenterCoord ) // use the center of client as the zoom center
{
zoomCenterCoord = CU.add(currScrollCoord, {'x': selfElem.clientWidth / 2, 'y': selfElem.clientHeight / 2});
}
//console.log('zoom center info', this.getZoomCenter(), zoomCenterCoord);
//if (zoomCenterCoord)
{
var scrollDelta = CU.multiply(zoomCenterCoord, zoomLevel / currZoomLevel - 1);
selfElem.scrollLeft += scrollDelta.x;
selfElem.scrollTop += scrollDelta.y;
}
return result;
},
/**
* Zoom in.
*/
/*
zoomIn: function(step, zoomCenterCoord)
{
var curr = this.getCurrZoom();
var ratio = Kekule.ZoomUtils.getNextZoomInRatio(curr, step || 1);
return this.zoomTo(ratio, null, zoomCenterCoord);
},
*/
/**
* Zoom out.
*/
/*
zoomOut: function(step, zoomCenterCoord)
{
var curr = this.getCurrZoom();
var ratio = Kekule.ZoomUtils.getNextZoomOutRatio(curr, step || 1);
return this.zoomTo(ratio, null, zoomCenterCoord);
},
*/
/**
* Reset to normal size.
*/
/*
resetZoom: function(zoomCenterCoord)
{
return this.zoomTo(this.getInitialZoom() || 1, null, zoomCenterCoord);
},
*/
/**
* Change the size of client element.
* Width and height is based on px.
* @private
*/
changeClientSize: function(width, height, zoomLevel)
{
this._initialRenderTransformParams = null;
var elem = this.getCoreElement();
var style = elem.style;
if (!zoomLevel)
zoomLevel = 1;
var w = width * zoomLevel;
var h = height * zoomLevel;
if (w)
style.width = w + 'px';
if (h)
style.height = h + 'px';
var ctxes = [this.getObjContext(), this.getOperContext(), this.getUiContext()];
for (var i = 0, l = ctxes.length; i < l; ++i)
{
var ctx = ctxes[i];
if (ctx) // change ctx size also
{
this.getDrawBridge().setContextDimension(ctx, w, h);
}
}
this.repaint();
},
/**
* Returns the screen box (x1, y1, x2, y2) of current visible client area in editor.
* @returns {Hash}
*/
getVisibleClientScreenBox: function()
{
var elem = this.getEditClientElem().parentNode;
var result = Kekule.HtmlElementUtils.getElemClientDimension(elem);
var pos = this.getClientScrollPosition();
result.x1 = pos.x;
result.y1 = pos.y;
result.x2 = result.x1 + result.width;
result.y2 = result.y1 + result.height;
return result;
},
/**
* Returns the context box (x1, y1, x2, y2, in a specified coord system) of current visible client area in editor.
* @param {Int} coordSys
* @returns {Hash}
*/
getVisibleClientBoxOfSys: function(coordSys)
{
var screenBox = this.getVisibleClientScreenBox();
var coords = Kekule.BoxUtils.getMinMaxCoords(screenBox);
var c1 = this.translateCoord(coords.min, Kekule.Editor.CoordSys.SCREEN, coordSys);
var c2 = this.translateCoord(coords.max, Kekule.Editor.CoordSys.SCREEN, coordSys);
var result = Kekule.BoxUtils.createBox(c1, c2);
return result;
},
/**
* Returns the context box (x1, y1, x2, y2, in object coord system) of current visible client area in editor.
* @param {Int} coordSys
* @returns {Hash}
*/
getVisibleClientObjBox: function(coordSys)
{
return this.getVisibleClientBoxOfSys(Kekule.Editor.CoordSys.CHEM);
},
/**
* Returns whether the chem object inside editor has been modified since load.
* @returns {Bool}
*/
isDirty: function()
{
if (this.getEnableOperHistory())
return this.getOperHistory().getCurrIndex() >= 0;
else
return this._objChanged;
},
/**
* Returns srcInfo of chemObj. If editor is dirty (object been modified), srcInfo will be unavailable.
* @param {Kekule.ChemObject} chemObj
* @returns {Object}
*/
getChemObjSrcInfo: function(chemObj)
{
if (this.isDirty())
return null;
else
return chemObj.getSrcInfo? chemObj.getSrcInfo(): null;
},
/* @private */
/*
_calcPreferedTransformOptions: function()
{
var drawOptions = this.getDrawOptions();
return this.getPainter().calcPreferedTransformOptions(
this.getObjContext(), this.calcDrawBaseCoord(drawOptions), drawOptions);
},
*/
/** @private */
getActualDrawOptions: function(/*$super*/)
{
var old = this.tryApplySuper('getActualDrawOptions') /* $super() */;
if (this._initialRenderTransformParams)
{
var result = Object.extend({}, this._initialRenderTransformParams);
result = Object.extend(result, old);
//var result = Object.create(old);
//result.initialRenderTransformParams = this._initialRenderTransformParams;
//console.log('extended', this._initialRenderTransformParams, result);
return result;
}
else
return old;
},
/** @ignore */
/*
getDrawClientDimension: function()
{
},
*/
/** @ignore */
repaint: function(/*$super, */overrideOptions)
{
var ops = overrideOptions;
//console.log('repaint called', overrideOptions);
//console.log('repaint', this._initialRenderTransformParams);
/*
if (this._initialRenderTransformParams)
{
ops = Object.create(overrideOptions || {});
//console.log(this._initialRenderTransformParams);
ops = Object.extend(ops, this._initialRenderTransformParams);
}
else
{
ops = overrideOptions;
//this._initialRenderTransformParams = this._calcPreferedTransformOptions();
//console.log('init params: ', this._initialRenderTransformParams, drawOptions);
}
*/
var result = this.tryApplySuper('repaint', [ops]) /* $super(ops) */;
if (this.isRenderable())
{
// after paint the new obj the first time, save up the transform params (especially the translates)
if (!this._initialRenderTransformParams)
{
this._initialRenderTransformParams = this.getPainter().getActualInitialRenderTransformOptions(this.getObjContext());
/*
if (transParam)
{
var trans = {}
var unitLength = transParam.unitLength || 1;
if (Kekule.ObjUtils.notUnset(transParam.translateX))
trans.translateX = transParam.translateX / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateY))
trans.translateY = transParam.translateY / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateZ))
trans.translateZ = transParam.translateZ / unitLength;
if (transParam.center)
trans.center = transParam.center;
//var zoom = transParam.zoom || 1;
var zoom = 1;
trans.scaleX = transParam.scaleX / zoom;
trans.scaleY = transParam.scaleY / zoom;
trans.scaleZ = transParam.scaleZ / zoom;
this._initialRenderTransformParams = trans;
console.log(this._initialRenderTransformParams, this);
}
*/
}
// redraw ui markers
this.recalcUiMarkers();
}
return result;
},
/**
* Create a new object and load it in editor.
*/
newDoc: function()
{
//if (this.getEnableCreateNewDoc()) // enable property only affects UI, always could create new doc in code
this.load(this.doCreateNewDocObj());
},
/**
* Create a new object for new document.
* Descendants may override this method.
* @private
*/
doCreateNewDocObj: function()
{
return new Kekule.Molecule();
},
/**
* Returns array of classes that can be exported (saved) from editor.
* Descendants can override this method.
* @returns {Array}
*/
getExportableClasses: function()
{
var obj = this.getChemObj();
if (!obj)
return [];
else
return obj.getClass? [obj.getClass()]: [];
},
/**
* Returns exportable object for specified class.
* Descendants can override this method.
* @param {Class} objClass Set null to export default object.
* @returns {Object}
*/
exportObj: function(objClass)
{
return this.exportObjs(objClass)[0];
},
/**
* Returns all exportable objects for specified class.
* Descendants can override this method.
* @param {Class} objClass Set null to export default object.
* @returns {Array}
*/
exportObjs: function(objClass)
{
var obj = this.getChemObj();
if (!objClass)
return [obj];
else
{
return (obj && (obj instanceof objClass))? [obj]: [];
}
},
/** @private */
doLoad: function(/*$super, */chemObj)
{
// deselect all old objects first
this.deselectAll();
this._initialRenderTransformParams = null;
// clear rendererMap so that all old renderer info is removed
this.getObjRendererMap().clear();
if (this.getOperHistory())
this.getOperHistory().clear();
this.tryApplySuper('doLoad', [chemObj]) /* $super(chemObj) */;
this._objChanged = false;
// clear hovered object cache
this.setPropStoreFieldValue('hoveredBasicObjs', null);
// clear issue results
this.setIssueCheckResults(null);
this.requestAutoCheckIssuesIfNecessary();
},
/** @private */
doLoadEnd: function(/*$super, */chemObj)
{
this.tryApplySuper('doLoadEnd') /* $super() */;
//console.log('loadend: ', chemObj);
if (!chemObj)
this._initialRenderTransformParams = null;
/*
else
{
// after load the new obj the first time, save up the transform params (especially the translates)
var transParam = this.getPainter().getActualRenderTransformParams(this.getObjContext());
if (transParam)
{
var trans = {}
var unitLength = transParam.unitLength || 1;
if (Kekule.ObjUtils.notUnset(transParam.translateX))
trans.translateX = transParam.translateX / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateY))
trans.translateY = transParam.translateY / unitLength;
if (Kekule.ObjUtils.notUnset(transParam.translateZ))
trans.translateZ = transParam.translateZ / unitLength;
this._initialRenderTransformParams = trans;
console.log(this._initialRenderTransformParams, this);
}
}
*/
},
/** @private */
doResize: function(/*$super*/)
{
//console.log('doResize');
this._initialRenderTransformParams = null; // transform should be recalculated after resize
this.tryApplySuper('doResize') /* $super() */;
},
/** @ignore */
geometryOptionChanged: function(/*$super*/)
{
var zoom = this.getDrawOptions().zoom;
this.zoomChanged(zoom);
// clear some length related caches
this._clearLengthCaches();
this.tryApplySuper('geometryOptionChanged') /* $super() */;
},
/** @private */
zoomChanged: function(zoomLevel)
{
// do nothing here
},
/** @private */
_clearLengthCaches: function()
{
this._lengthCaches = {};
},
/**
* @private
*/
chemObjChanged: function(/*$super, */newObj, oldObj)
{
this.tryApplySuper('chemObjChanged', [newObj, oldObj]) /* $super(newObj, oldObj) */;
if (newObj !== oldObj)
{
if (oldObj)
this._uninstallChemObjEventListener(oldObj);
if (newObj)
this._installChemObjEventListener(newObj);
}
},
/** @private */
_installChemObjEventListener: function(chemObj)
{
chemObj.addEventListener('change', this.reactChemObjChange, this);
},
/** @private */
_uninstallChemObjEventListener: function(chemObj)
{
chemObj.removeEventListener('change', this.reactChemObjChange, this);
},
/**
* Create a transparent div element above all other elems of editor,
* this element is used to receive all UI events.
*/
createUiEventReceiverElem: function()
{
var parent = this.getCoreElement();
if (parent)
{
var result = parent.ownerDocument.createElement('div');
result.className = CCNS.EDITOR_UIEVENT_RECEIVER;
/*
result.id = 'overlayer';
*/
/*
var style = result.style;
style.background = 'transparent';
//style.background = 'yellow';
//style.opacity = 0;
style.position = 'absolute';
style.left = 0;
style.top = 0;
style.width = '100%';
style.height = '100%';
*/
//style.zIndex = 1000;
parent.appendChild(result);
EU.addClass(result, CNS.DYN_CREATED);
this.setPropStoreFieldValue('uiEventReceiverElem', result);
return result;
}
},
/** @private */
createContextParentElems: function()
{
var parent = this.getCoreElement();
if (parent)
{
var doc = parent.ownerDocument;
this._createContextParentElem(doc, parent, 'objContextParentElem');
this._createContextParentElem(doc, parent, 'operContextParentElem');
this._createContextParentElem(doc, parent, 'uiContextParentElem');
}
},
/** @private */
_createContextParentElem: function(doc, parentElem, contextElemPropName)
{
var result = doc.createElement('div');
result.style.position = 'absolute';
result.style.width = '100%';
result.style.height = '100%';
result.className = contextElemPropName + ' ' + CNS.DYN_CREATED; // debug
this.setPropStoreFieldValue(contextElemPropName, result);
parentElem.appendChild(result);
return result;
},
/** @private */
createNewPainter: function(/*$super, */chemObj)
{
var result = this.tryApplySuper('createNewPainter', [chemObj]) /* $super(chemObj) */;
if (result)
{
result.setCanModifyTargetObj(true);
this.installPainterEventHandlers(result);
/* Moved up to class ChemObjDisplayer
// create new bound info recorder
this.createNewBoundInfoRecorder(this.g