UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,460 lines (1,373 loc) 268 kB
/** * @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