UNPKG

kekule

Version:

Open source JavaScript toolkit for chemoinformatics

1,473 lines (1,403 loc) 146 kB
/** * @fileoverview * Related types and classes of chem viewer. * Viewer is a widget to show chem objects on HTML page. * @author Partridge Jiang */ /* * requires /lan/classes.js * requires /utils/kekule.utils.js * requires /xbrowsers/kekule.x.js * requires /core/kekule.common.js * requires /widgets/kekule.widget.base.js * requires /widgets/kekule.widget.menus.js * requires /widgets/kekule.widget.dialogs.js * requires /widgets/kekule.widget.helpers.js * requires /widgets/chem/kekule.chemWidget.base.js * requires /widgets/chem/kekule.chemWidget.chemObjDisplayers.js * requires /widgets/chem/uiMarker/kekule.chemWidget.uiMarkers.js * requires /widgets/operation/kekule.actions.js * requires /widgets/commonCtrls/kekule.widget.buttons.js * requires /widgets/commonCtrls/kekule.widget.containers.js * * requires /localization/kekule.localize.widget.js */ (function(){ "use strict"; var PS = Class.PropertyScope; var AU = Kekule.ArrayUtils; var ZU = Kekule.ZoomUtils; var BNS = Kekule.ChemWidget.ComponentWidgetNames; var CW = Kekule.ChemWidget; var EM = Kekule.Widget.EvokeMode; /** @ignore */ Kekule.globalOptions.add('chemWidget.viewer', { toolButtons: [ //BNS.loadFile, BNS.loadData, BNS.saveData, //BNS.clearObjs, BNS.molDisplayType, BNS.molHideHydrogens, //BNS.molAutoGenerateCoords, BNS.zoomIn, BNS.zoomOut, BNS.rotateX, BNS.rotateY, BNS.rotateZ, BNS.rotateLeft, BNS.rotateRight, BNS.reset, BNS.copy, BNS.openEditor ], menuItems: [ BNS.loadData, BNS.saveData, Kekule.Widget.MenuItem.SEPARATOR_TEXT, BNS.molDisplayType, BNS.molHideHydrogens, BNS.zoomIn, BNS.zoomOut, { 'text': Kekule.$L('ChemWidgetTexts.CAPTION_ROTATE'), 'hint': Kekule.$L('ChemWidgetTexts.HINT_ROTATE'), 'children': [ BNS.rotateLeft, BNS.rotateRight, BNS.rotateX, BNS.rotateY, BNS.rotateZ ] }, BNS.reset, Kekule.Widget.MenuItem.SEPARATOR_TEXT, BNS.copy, BNS.openEditor, BNS.config ], 'toolbar': { 'evokeModes': [EM.EVOKEE_CLICK, EM.EVOKEE_MOUSE_ENTER, EM.EVOKEE_TOUCH], 'revokeModes': [EM.EVOKEE_MOUSE_LEAVE, EM.EVOKER_TIMEOUT], 'pos': Kekule.Widget.Position.AUTO, 'marginHorizontal': 10, 'marginVertical': 10 }, 'editor': { 'modal': true, 'restrainEditorWithCurrObj': true, 'shareEditorInstance': true }, 'enableToolbar': false, 'enableDirectInteraction': true, 'enableTouchInteraction': false, 'enableGestureInteraction': false, 'enabledDirectInteractionModes': { 'move': true, 'rotate': true, 'zoom': true }, 'showCaption': false, 'useNormalBackground': false, 'enableCustomCssProperties': true, 'restraintRotation3DEdgeRatio': 0.18, 'enableRestraintRotation3D': true }); /** @ignore */ Kekule.ChemWidget.HtmlClassNames = Object.extend(Kekule.ChemWidget.HtmlClassNames, { VIEWER: 'K-Chem-Viewer', VIEWER2D: 'K-Chem-Viewer2D', VIEWER3D: 'K-Chem-Viewer3D', VIEWER_CLIENT: 'K-Chem-Viewer-Client', VIEWER_CAPTION: 'K-Chem-Viewer-Caption', VIEWER_EMBEDDED_TOOLBAR: 'K-Chem-Viewer-Embedded-Toolbar', VIEWER_UICONTEXT_PARENT: 'K-Chem-Viewer-UiContext-Parent', VIEWER_MENU_BUTTON: 'K-Chem-Viewer-Menu-Button', VIEWER_EDITOR_FULLCLIENT: 'K-Chem-Viewer-Editor-FullClient', // predefined actions ACTION_ROTATE_LEFT: 'K-Chem-RotateLeft', ACTION_ROTATE_RIGHT: 'K-Chem-RotateRight', ACTION_ROTATE_X: 'K-Chem-RotateX', ACTION_ROTATE_Y: 'K-Chem-RotateY', ACTION_ROTATE_Z: 'K-Chem-RotateZ', ACTION_VIEWER_EDIT: 'K-Chem-Viewer-Edit' }); var CNS = Kekule.Widget.HtmlClassNames; var CCNS = Kekule.ChemWidget.HtmlClassNames; /** * Enumeration of some common used UI markers groups. * @enum */ Kekule.ChemWidget.ViewerUiMarkerGroup = { /** Ui markers for hot tracking objects. */ HOTTRACK: 'hotTrack', /** Ui markers for selecting objects. */ SELECT: 'select' }; /** * An universal viewer widget for chem objects (especially molecules). * @class * @augments Kekule.ChemWidget.ChemObjDisplayer * * @param {Variant} parentOrElementOrDocument * @param {Kekule.ChemObject} chemObj * @param {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}. * @param {Kekule.ChemWidget.ChemObjDisplayerConfigs} viewerConfigs * * @property {Int} renderType Display in 2D or 3D. Value from {@link Kekule.Render.RendererType}. Read only. * @property {Kekule.ChemObject} chemObj Object to be drawn. Set this property will repaint the client. * @property {Bool} chemObjLoaded Whether the chemObj is successful loaded and drawn in viewer. * //@property {Object} renderConfigs Configuration for rendering. * // This property should be an instance of {@link Kekule.Render.Render2DConfigs} or {@link Kekule.Render.Render3DConfigs} * //@property {Hash} drawOptions Options to draw object. * //@property {Float} zoom Zoom ratio to draw chem object. Note this setting will overwrite drawOptions.zoom. * //@property {Bool} autoSize Whether the widget change its size to fit the dimension of chem object. * //@property {Int} padding Padding between chem object and edge of widget, in px. Only works when autoSize is true. * * @property {String} caption Caption of viewer. * @property {Bool} showCaption Whether show caption below or above viewer. * @property {Int} captionPos Value from {@link Kekule.Widget.Position}, now only TOP and BOTTOM are usable. * @property {Bool} autoCaption Set caption automatically by chemObj info. * * //@property {Bool} liveUpdate Whether viewer repaint itself automatically when containing chem object changed. * * @property {Bool} enableHotKey Whether hot key is allowed. * @property {Bool} enableDirectInteraction Whether interact without tool button is allowed (e.g., zoom/rotate by mouse). * @property {Bool} enableTouchInteraction Whether touch interaction is allowed. Note if enableDirectInteraction is false, touch interaction will also be disabled. * @property {Bool} enableRestraintRotation3D Set to true to rotate only on one axis of X/Y/Z when the starting point is near edge of viewer. * @property {Float} restraintRotation3DEdgeRatio * @property {Bool} enableEdit Whether a edit button is shown in toolbar to edit object in viewer. Works only in 2D mode. * @property {Bool} modalEdit Whether opens a modal dialog when editting object in viewer. * @property {Bool} enableEditFromVoid Whether editor can be launched even if viewer is empty. * @property {Hash} editorProperties Hash object to set properties of popup editor. * @property {Bool} restrainEditorWithCurrObj If true, the editor popuped can only edit current object in viewer (and add new * objects is disabled). If false, the editor can do everything like a normal composer, viewer will load objects in composer * after editting (and will not keep the original object in viewer). * @property {Bool} shareEditorInstance If true, all viewers in one document will shares one editor. * This setting may reduce the cost of creating many composer widgets. * * @property {Array} toolButtons buttons in interaction tool bar. This is a array of predefined strings, e.g.: ['zoomIn', 'zoomOut', 'resetZoom', 'molDisplayType', ...]. <br /> * In the array, complex hash can also be used to add custom buttons, e.g.: <br /> * [ <br /> * 'zoomIn', 'zoomOut',<br /> * {'name': 'myCustomButton1', 'widgetClass': 'Kekule.Widget.Button', 'action': actionClass},<br /> * {'name': 'myCustomButton2', 'htmlClass': 'MyClass' 'caption': 'My Button', 'hint': 'My Hint', '#execute': function(){ ... }},<br /> * ]<br /> * most hash fields are similar to the param of {@link Kekule.Widget.Utils.createFromHash}.<br /> * If this property is not set, default buttons will be used. * @property {Bool} enableToolbar Whether show tool bar in viewer. * @property {Int} toolbarPos Value from {@link Kekule.Widget.Position}, position of toolbar in viewer. * For example, set this property to TOP will make toolbar shows in the center below the top edge of viewer, * TOP_RIGHT will make the toolbar shows at the top right corner. Default value is BOTTOM_RIGHT. * Set this property to AUTO, viewer will set toolbar position (including margin) automatically. * @property {Int} toolbarMarginHorizontal Horizontal margin of toolbar to viewer edge, in px. * Negative value means toolbar outside viewer. * @property {Int} toolbarMarginVertical Vertical margin of toolbar to viewer edge, in px. * Negative value means toolbar outside viewer. * //@property {Array} toolbarShowEvents Events to cause the display of toolbar. If set to null, the toolbar will always be visible. * @property {Array} toolbarEvokeModes Interaction modes to show the toolbar. Array item values should from {@link Kekule.Widget.EvokeMode}. * Set enableToolbar to true and include {@link Kekule.Widget.EvokeMode.ALWAYS} will always show the toolbar. * @property {Array} toolbarRevokeModes Interaction modes to hide the toolbar. Array item values should from {@link Kekule.Widget.EvokeMode}. * @property {Int} toolbarRevokeTimeout Toolbar should be hidden after how many milliseconds after shown. * Only available when {@link Kekule.Widget.EvokeMode.EVOKEE_TIMEOUT} or {@link Kekule.Widget.EvokeMode.EVOKER_TIMEOUT} in toolbarRevokeModes. * * @property {Array} allowedMolDisplayTypes Molecule types can be changed in tool bar. */ /** * Invoked when the chem object (or null) in viewer has been edited by the popup editor. * event param of it has one fields: {obj: Object} * @name Kekule.ChemWidget.Viewer#editingDone * @event */ /** * Invoked when the pointer is hot tracking on objects in viewer. * event param of it has fields: {objects: Array}. * @name Kekule.ChemWidget.Viewer#hotTrackOnObjects * @event */ /** * Invoked when objects are selected in view. * event param of it has fields: {objects: Array}. * @name Kekule.ChemWidget.Viewer#selectionChange * @event */ Kekule.ChemWidget.Viewer = Class.create(Kekule.ChemWidget.ChemObjDisplayer, /** @lends Kekule.ChemWidget.Viewer# */ { /** @private */ CLASS_NAME: 'Kekule.ChemWidget.Viewer', /** @private */ BINDABLE_TAG_NAMES: ['div', 'span', 'img'], /** @private */ DEF_BGCOLOR_2D: null, /** @private */ DEF_BGCOLOR_3D: '#000000', /** @private */ DEF_TOOLBAR_EVOKE_MODES: [/*EM.ALWAYS,*/ EM.EVOKEE_CLICK, EM.EVOKEE_MOUSE_ENTER, EM.EVOKEE_TOUCH], /** @private */ DEF_TOOLBAR_REVOKE_MODES: [/*EM.ALWAYS,*/ /*EM.EVOKEE_CLICK,*/ EM.EVOKEE_MOUSE_LEAVE, EM.EVOKER_TIMEOUT], /** @private */ OBSERVING_GESTURES: ['pinch', 'pinchstart', 'pinchmove', 'pinchend', 'pinchcancel', 'pinchin', 'pinchout'], /** @construct */ initialize: function(/*$super, */parentOrElementOrDocument, chemObj, renderType, viewerConfigs) { //this._errorReportElem = null; // use internally this.setPropStoreFieldValue('renderType', renderType || Kekule.Render.RendererType.R2D); // must set this value first this.setPropStoreFieldValue('useCornerDecoration', true); /* this.setPropStoreFieldValue('enableToolbar', false); this.setPropStoreFieldValue('toolbarEvokeModes', this.DEF_TOOLBAR_EVOKE_MODES); this.setPropStoreFieldValue('toolbarRevokeModes', this.DEF_TOOLBAR_REVOKE_MODES); this.setPropStoreFieldValue('enableDirectInteraction', true); this.setPropStoreFieldValue('toolbarPos', Kekule.Widget.Position.AUTO); this.setPropStoreFieldValue('toolbarMarginHorizontal', 10); this.setPropStoreFieldValue('toolbarMarginVertical', 10); this.setPropStoreFieldValue('showCaption', false); */ var oneOf = Kekule.oneOf; var options = Kekule.globalOptions.get('chemWidget.viewer') || {}; this.setPropStoreFieldValue('enableToolbar', oneOf(options.enableToolbar)); this.setPropStoreFieldValue('enableDirectInteraction', oneOf(options.enableDirectInteraction, true)); this.setPropStoreFieldValue('showCaption', oneOf(options.showCaption, false)); options = options.toolbar || {}; this.setPropStoreFieldValue('toolbarEvokeModes', oneOf(options.evokeModes, this.DEF_TOOLBAR_EVOKE_MODES)); this.setPropStoreFieldValue('toolbarRevokeModes', oneOf(options.revokeModes, this.DEF_TOOLBAR_REVOKE_MODES)); this.setPropStoreFieldValue('toolbarPos', oneOf(options.pos, Kekule.Widget.Position.AUTO)); this.setPropStoreFieldValue('toolbarMarginHorizontal', oneOf(options.marginHorizontal,10)); this.setPropStoreFieldValue('toolbarMarginVertical', oneOf(options.marginVertical, 10)); this.tryApplySuper('initialize', [parentOrElementOrDocument, chemObj, renderType /*, viewerConfigs*/]) /* $super(parentOrElementOrDocument, chemObj, renderType, viewerConfigs) */; this.setPropStoreFieldValue('viewerConfigs', viewerConfigs || this.createDefaultConfigs()); this.beginUpdate(); try { this.setPadding(this.getRenderConfigs().getLengthConfigs().getActualLength('autofitContextPadding')); /* if (chemObj) { this.setChemObj(chemObj); } */ this._isContinuousRepainting = false; // flag, use internally //this._lastRotate3DMatrix = null; // store the last 3D rotation information var RT = Kekule.Render.RendererType; var color2D = (this.getRenderType() === RT.R2D) ? (this.getBackgroundColor() || this.DEF_BGCOLOR_2D) : this.DEF_BGCOLOR_2D; var color3D = (this.getRenderType() === RT.R3D) ? (this.getBackgroundColor() || this.DEF_BGCOLOR_3D) : this.DEF_BGCOLOR_3D; this.setBackgroundColorOfType(color2D, RT.R2D); this.setBackgroundColorOfType(color3D, RT.R3D); this.useCornerDecorationChanged(); this.doResize(); // adjust caption and drawParent size } finally { this.endUpdate(); } var gOptions = Kekule.globalOptions.get('chemWidget.viewer') || {}; if (Kekule.ObjUtils.isUnset(this.getEnableGesture())) this.setEnableGesture(oneOf(gOptions.enableGestureInteraction, false)); // the hammer event reactor need to be installed after element is bind this.addIaController('default', new Kekule.ChemWidget.ViewerBasicInteractionController(this), true); }, /** @private */ doFinalize: function(/*$super*/) { var markers = this.getUiMarkers(); if (markers) markers.finalize(); //this.getPainter().finalize(); var toolBar = this.getToolbar(); this.tryApplySuper('doFinalize') /* $super() */; if (toolBar) toolBar.finalize(); if (this._composerDialog) this._composerDialog.finalize(); if (this._composerPanel) this._composerPanel.finalize(); }, /** @private */ initProperties: function() { /* this.defineProp('chemObj', {'dataType': 'Kekule.ChemObject', 'serializable': false, 'setter': function(value) { this.setPropStoreFieldValue('chemObj', value); this.chemObjChanged(value); } }); this.defineProp('chemObjLoaded', {'dataType': DataType.BOOL, 'serializable': false, 'setter': null, 'getter': function() { return this.getChemObj() && this.getPropStoreFieldValue('chemObjLoaded'); } }); this.defineProp('renderType', {'dataType': DataType.INT, 'serializable': false, 'setter': null}); this.defineProp('renderConfigs', {'dataType': DataType.OBJECT, 'serializable': false}); this.defineProp('drawOptions', {'dataType': DataType.HASH, 'serializable': false, 'getter': function() { var result = this.getPropStoreFieldValue('drawOptions'); if (!result) { result = {}; this.setPropStoreFieldValue('drawOptions', result); } return result; } }); */ //this.defineProp('zoom', {'dataType': DataType.FLOAT, 'serializable': false}); this.defineProp('viewerConfigs', {'dataType': 'Kekule.ChemWidget.ChemObjDisplayerConfigs', 'serializable': false, 'getter': function() { return this.getDisplayerConfigs(); }, 'setter': function(value) { return this.setDisplayerConfigs(value); } }); //this.defineProp('enableUiContext', {'dataType': DataType.BOOL}); this.defineProp('uiDrawBridge', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': null, 'getter': function() { if (!this.getEnableUiContext()) return null; var result = this.getPropStoreFieldValue('uiDrawBridge'); if (!result && !this.__$uiDrawBridgeInitialized$__) { this.__$uiDrawBridgeInitialized$__ = true; result = this.createUiDrawBridge(); this.setPropStoreFieldValue('uiDrawBridge', result); } return result; } }); this.defineProp('uiContext', {'dataType': DataType.OBJECT, 'serializable': false, 'getter': function() { if (!this.getEnableUiContext()) return null; 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('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': '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('allowedMolDisplayTypes', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC, 'setter': function(value) { this.setPropStoreFieldValue('allowedMolDisplayTypes', value); //this.updateToolbar(); this.updateUiComps(); } }); this.defineProp('retainAspect', {'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { var op = this.getDrawOptions() || {}; return op.retainAspect; }, 'setter': function(value) { var op = this.getDrawOptions() || {}; op.retainAspect = !!value; return this.setDrawOptions(op); } }); this.defineProp('enableRestraintRotation3D', {'dataType': DataType.BOOL}); this.defineProp('restraintRotation3DEdgeRatio', {'dataType': DataType.FLOAT}); //this.defineProp('liveUpdate', {'dataType': DataType.BOOL}); this.defineProp('enableHotKey', {'dataType': DataType.FLOAT}); this.defineProp('enableEdit', {'dataType': DataType.BOOL, 'getter': function() { // TODO: now only allows 2D editing return this.getPropStoreFieldValue('enableEdit') && (this.getCoordMode() !== Kekule.CoordMode.COORD3D); } }); this.defineProp('shareEditorInstance', {'dataType': DataType.BOOL}); this.defineProp('enableEditFromVoid', {'dataType': DataType.BOOL}); this.defineProp('restrainEditorWithCurrObj', {'dataType': DataType.BOOL}); this.defineProp('modalEdit', {'dataType': DataType.BOOL}); this.defineProp('editorProperties', {'dataType': DataType.HASH}); this.defineProp('toolButtons', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC, 'getter': function() { var result = this.getPropStoreFieldValue('toolButtons'); /* if (!result) // create default one { result = this.getDefaultToolBarButtons(); this.setPropStoreFieldValue('toolButtons', result); } */ return result; }, 'setter': function(value) { this.setPropStoreFieldValue('toolButtons', value); this.updateToolbar(); } }); this.defineProp('menuItems', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC, 'setter': function(value) { this.setPropStoreFieldValue('menuItems', value); this.updateMenu(); } }); /* // private this.defineProp('toolButtonNameMapping', {'dataType': DataType.HASH, 'serializable': false, 'scope': PS.PRIVATE, 'setter': null, 'getter': function() { var result = this.getPropStoreFieldValue('toolButtonNameMapping'); if (!result) // create default one { result = this.createDefaultToolButtonNameMapping(); this.setPropStoreFieldValue('toolButtonNameMapping', result); } return result; } }); */ // private this.defineProp('menu', {'dataType': 'Kekule.Widget.Menu', 'serializable': false, 'scope': PS.PRIVATE, 'setter': function(value) { var old = this.getMenu(); if (value !== old) { if (old) { old.finalize(); old = null; } this.setPropStoreFieldValue('menu', value); } } }); this.defineProp('toolbar', {'dataType': 'Kekule.Widget.ButtonGroup', 'serializable': false, 'scope': PS.PRIVATE, 'setter': function(value) { var old = this.getToolbar(); var evokeHelper = this.getToolbarEvokeHelper(); if (value !== old) { if (old) { old.finalize(); var helper = this.getToolbarEvokeHelper(); if (helper) helper.finalize(); old = null; } if (evokeHelper) evokeHelper.finalize(); this.setPropStoreFieldValue('toolbar', value); // hide the new toolbar and wait for the evoke helper to display it //value.setDisplayed(false); if (value) { this.setPropStoreFieldValue('toolbarEvokeHelper', new Kekule.Widget.DynamicEvokeHelper(this, value, this.getToolbarEvokeModes(), this.getToolbarRevokeModes())); } } } }); this.defineProp('enableToolbar', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('enableToolbar', value); this.updateToolbar(); } }); this.defineProp('toolbarPos', {'dataType': DataType.INT, 'enumSource': Kekule.Widget.Position, 'setter': function(value) { this.setPropStoreFieldValue('toolbarPos', value); this.adjustToolbarPos(); } }); this.defineProp('toolbarMarginVertical', {'dataType': DataType.INT, 'setter': function(value) { this.setPropStoreFieldValue('toolbarMarginVertical', value); this.adjustToolbarPos(); } }); this.defineProp('toolbarMarginHorizontal', {'dataType': DataType.INT, 'setter': function(value) { this.setPropStoreFieldValue('toolbarMarginHorizontal', value); this.adjustToolbarPos(); } }); /* this.defineProp('toolbarShowEvents', {'dataType': DataType.ARRAY}); this.defineProp('toolbarAlwaysShow', {'dataType': DataType.BOOL, 'serializable': false, 'getter': function() { return !!this.getToolbarShowEvents(); }, 'setter': null }); */ this.defineProp('toolbarEvokeHelper', {'dataType': 'Kekule.Widget.DynamicEvokeHelper', 'serializable': false, 'setter': null, 'scope': PS.PRIVATE}); // private this.defineProp('toolbarEvokeModes', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC, 'setter': function(value) { this.setPropStoreFieldValue('toolbarEvokeModes', value || []); if (this.getToolbarEvokeHelper()) this.getToolbarEvokeHelper().setEvokeModes(value || []); } }); this.defineProp('toolbarRevokeModes', {'dataType': DataType.ARRAY, 'scope': PS.PUBLIC, 'setter': function(value) { this.setPropStoreFieldValue('toolbarRevokeModes', value || []); if (this.getToolbarEvokeHelper()) this.getToolbarEvokeHelper().setRevokeModes(value || []); } }); this.defineProp('toolbarRevokeTimeout', {'dataType': DataType.INT, 'setter': function(value) { this.setPropStoreFieldValue('toolbarRevokeTimeout', value); if (this.getToolbarEvokeHelper()) this.getToolbarEvokeHelper().setTimeout(value); } }); this.defineProp('toolbarParentElem', {'dataType': DataType.OBJECT, 'serializable': false, 'setter': function(value) { if (this.getToolbarParentElem() !== value) { this.setPropStoreFieldValue('toolbarParentElem', value); this.updateToolbar(); } } }); this.defineProp('caption', {'dataType': DataType.STRING, 'setter': function(value) { this.setPropStoreFieldValue('caption', value); Kekule.DomUtils.setElementText(this.getCaptionElem(), value || ''); this.captionChanged(); } }); this.defineProp('showCaption', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('showCaption', value); this.captionChanged(); } }); this.defineProp('captionPos', {'dataType': DataType.INT, 'enumSource': Kekule.Widget.Position, 'setter': function(value) { this.setPropStoreFieldValue('captionPos', value); this.captionChanged(); } }); this.defineProp('autoCaption', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('autoCaption', value); if (value) this.autoDetectCaption(); } }); this.defineProp('captionElem', {'dataType': DataType.OBJECT, 'scope': PS.PRIVATE, 'setter': null, 'getter': function(doNotAutoCreate) { var result = this.getPropStoreFieldValue('captionElem'); if (!result && !doNotAutoCreate) // create new { result = this.doCreateCaptionElem(); this.setPropStoreFieldValue('captionElem', result); } return result; } }); this.defineProp('actions', {'dataType': 'Kekule.ActionList', 'serializable': false, 'scope': PS.PUBLIC, 'setter': null, 'getter': function() { var result = this.getPropStoreFieldValue('actions'); if (!result) { result = new Kekule.ActionList(); this.setPropStoreFieldValue('actions', result); } return result; } }); this.defineProp('actionMap', {'dataType': 'Kekule.MapEx', 'serializable': false, 'scope': PS.PRIVATE, 'setter': null, 'getter': function() { var result = this.getPropStoreFieldValue('actionMap'); if (!result) { result = new Kekule.MapEx(true); this.setPropStoreFieldValue('actionMap', result); } return result; } }); this.defineProp('enableDirectInteraction', {'dataType': DataType.BOOL}); this.defineProp('enableTouchInteraction', {'dataType': DataType.BOOL, 'setter': function(value) { this.setPropStoreFieldValue('enableTouchInteraction', !!value); this.setTouchAction(value? 'none': null); } }); 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); } } } }); this.defineProp('enabledDirectInteractionModes', {'dataType': DataType.HASH}); this.defineProp('hotTrackedObjects', { 'dataType': DataType.ARRAY, 'serializable': false, 'getter': function() { return this.getPropStoreFieldValue('hotTrackedObjects') || []; }, 'setter': function(value) { //this.setPropStoreFieldValue('hotTrackedObjects', AU.toArray(value)); this.changeHotTrackedObjects(value && AU.toArray(value), true); } }); this.defineProp('selectedObjects', { 'dataType': DataType.ARRAY, 'serializable': false, 'getter': function() { return this.getPropStoreFieldValue('selectedObjects') || []; }, 'setter': function(value) { this.changeSelectedObjects(value && AU.toArray(value), true); } }); }, /** @ignore */ initPropValues: function(/*$super*/) { // debug /* this.setEnableEdit(true); */ this.setStyleMode(Kekule.Widget.StyleMode.INHERITED); // embedded in document /* this.setUseNormalBackground(false); //this.setInheritedRenderColor(true); this.setEnableCustomCssProperties(true); this.setModalEdit(true); this.setRestrainEditorWithCurrObj(true); this.setRestraintRotation3DEdgeRatio(0.18); this.setEnableRestraintRotation3D(true); this.setShareEditorInstance(true); this.setEnableTouchInteraction(!true); */ var oneOf = Kekule.oneOf; var options = Kekule.globalOptions.get('chemWidget.viewer') || {}; this.setUseNormalBackground(oneOf(options.useNormalBackground, false)); this.setEnableCustomCssProperties(oneOf(options.enableCustomCssProperties, true)); this.setRestraintRotation3DEdgeRatio(oneOf(options.restraintRotation3DEdgeRatio, 0.18)); this.setEnableRestraintRotation3D(oneOf(options.enableRestraintRotation3D, true)); this.setEnableTouchInteraction(oneOf(options.enableTouchInteraction, false)); this.setEnabledDirectInteractionModes(oneOf(options.enabledDirectInteractionModes, { 'move': true, 'rotate': true, 'zoom': true })); options = options.editor || {}; this.setModalEdit(oneOf(options.modal, true)); this.setRestrainEditorWithCurrObj(oneOf(options.restrainEditorWithCurrObj, true)); this.setShareEditorInstance(oneOf(options.shareEditorInstance, true)); }, /** @ignore */ createDefaultConfigs: function() { return new Kekule.ChemWidget.ViewerConfigs(); }, /** @ignore */ canUsePlaceHolderOnElem: function(elem) { // When using a img element with src image, it may contains the figure of chem object var imgSrc = elem.getAttribute('src'); return (elem.tagName.toLowerCase() === 'img') && (!!imgSrc); }, /** @ignore */ doObjectChange: function(/*$super, */modifiedPropNames) { this.tryApplySuper('doObjectChange', [modifiedPropNames]) /* $super(modifiedPropNames) */; this.updateActions(); }, /** @ignore */ doSetElement: function(/*$super, */element) { var elem = element; if (elem) { var tagName = elem.tagName.toLowerCase(); if (tagName === 'img') // is an image element, need to use span to replace it { elem = Kekule.DomUtils.replaceTagName(elem, 'span'); //this.setElement(elem); //console.log('replace img to span'); } } return this.tryApplySuper('doSetElement', [elem]) /* $super(elem) */; }, /** @ignore */ doUnbindElement: function(/*$super, */element) { // unbind old element, the context parent element should be set to null if (this._drawContextParentElem && this._drawContextParentElem.parentNode) { this._drawContextParentElem.parentNode.removeChild(this._drawContextParentElem); this._drawContextParentElem = null; } return this.tryApplySuper('doUnbindElement', [element]) /* $super(element) */; }, /** @ignore */ elementBound: function(element) { this.setObserveElemResize(true); }, /** @ignore */ doCreateRootElement: function(doc) { var result = doc.createElement('div'); return result; }, /** @ignore */ doCreateSubElements: function(doc, docFragment) { // client element var clientElem = doc.createElement('div'); clientElem.className = CCNS.VIEWER_CLIENT; this._clientElement = clientElem; docFragment.appendChild(clientElem); return [clientElem]; }, /** @ignore */ doGetWidgetClassName: function(/*$super*/) { var result = this.tryApplySuper('doGetWidgetClassName') /* $super() */ + ' ' + CCNS.VIEWER; try // may raise exception when called with class prototype (required by placeholder related methods) { var renderType = this.getRenderType(); var additional = this._getRenderTypeSpecifiedHtmlClassName(renderType); result += ' ' + additional; } catch(e) { } return result; }, /** @private */ _getRenderTypeSpecifiedHtmlClassName: function(renderType) { return (renderType === Kekule.Render.RendererType.R3D)? CCNS.VIEWER3D: CCNS.VIEWER2D; }, /** @ignore */ getClientElement: function() { return this._clientElement; }, /** @ignore */ getResizerElement: function() { //return this.getDrawContextParentElem(); return this.getElement(); }, /** @ignore */ doResize: function(/*$super*/) { //$super(); this.adjustDrawParentDim(); this.adjustToolbarPos(); this.tryApplySuper('doResize') /* $super() */; }, /** @ignore */ doWidgetShowStateChanged: function(isShown) { if (isShown) { //console.log('update toolbar'); //this.updateToolbar(); this.updateActions(); } }, /** @ignore */ refitDrawContext: function(/*$super, */doNotRepaint) { // resize context, means client size changed, so toolbar should also be adjusted. this.tryApplySuper('refitDrawContext', [doNotRepaint]) /* $super(doNotRepaint) */; this.adjustToolbarPos(); }, /** @ignore */ changeContextDimension: function(newDimension) { if (this.getEnableUiContext()) { if (this.getUiDrawBridge() && this.getUiContext()) { this.doChangeContextDimension(this.getUiContext(), this.getUiDrawBridge(), newDimension, true); } } return this.tryApplySuper('changeContextDimension', [newDimension]); }, /** @ignore */ getAllowRenderTypeChange: function() { return true; }, /** @ignore */ resetRenderType: function(/*$super, */oldType, newType) { this.tryApplySuper('resetRenderType', [oldType, newType]) /* $super(oldType, newType) */; // classname var oldHtmlClassName = this._getRenderTypeSpecifiedHtmlClassName(oldType); var newHtmlClassName = this._getRenderTypeSpecifiedHtmlClassName(newType); this.removeClassName(oldHtmlClassName); this.addClassName(newHtmlClassName); // toolbar //this.updateToolbar(); this.updateUiComps(); }, /** @ignore */ doLoad: function(chemObj) { // clear UI markers when loading a new object /* this.setVisibleOfUiMarkerGroup(Kekule.ChemWidget.ViewerUiMarkerGroup.SELECT, false, false); this.setVisibleOfUiMarkerGroup(Kekule.ChemWidget.ViewerUiMarkerGroup.HOTTRACK, false, false); */ this.beginUpdateUiMarkers(); try { this.clearHotTrackedItems(); this.clearSelectedItems(); this.clearUiMarkers(); } finally { this.endUpdateUiMarkers(); } this.tryApplySuper('doLoad', [chemObj]); }, /** @private */ doLoadEnd: function(chemObj) { this.updateActions(); this.autoDetectCaption(); }, /** @ignore */ _repaintCore: function(overrideOptions) { //console.log('do repaint'); this.tryApplySuper('_repaintCore', [overrideOptions]); }, /** @ignore */ chemObjRendered: function(chemObj, renderOptions) { var result = this.tryApplySuper('chemObjRendered', [chemObj, renderOptions]); this.updateUiMarkers(true); //this.repaintUiMarker(); return result; }, /** @private */ doSetUseCornerDecoration: function(/*$super, */value) { this.tryApplySuper('doSetUseCornerDecoration', [value]) /* $super(value) */; this.useCornerDecorationChanged(); }, /** @private */ useCornerDecorationChanged: function() { var elem = this.getDrawContextParentElem(); // do not auto create element if (elem) { var v = this.getUseCornerDecoration(); if (v) Kekule.HtmlElementUtils.addClass(elem, CNS.CORNER_ALL); else Kekule.HtmlElementUtils.removeClass(elem, CNS.CORNER_ALL); } }, /** @ignore */ getBoundInfosAtCoord: function(screenCoord, filterFunc, boundInflation) { var boundInfos = this.tryApplySuper('getBoundInfosAtCoord', [screenCoord, filterFunc, boundInflation]); var enableTrackNearest = this.getViewerConfigs().getInteractionConfigs().getEnableTrackOnNearest(); if (boundInfos && boundInfos.length && enableTrackNearest) // sort result by distance to screenCoord { var distanceMap = new Kekule.MapEx(); try { var SU = Kekule.Render.MetaShapeUtils; for (var i = boundInfos.length - 1; i >= 0; --i) { var info = boundInfos[i]; var shapeInfo = info.boundInfo; var currDistance = SU.getDistance(screenCoord, shapeInfo); distanceMap.set(info, currDistance); } // sort by z-index, the smaller index on bottom boundInfos.sort(function (b1, b2) { var result = - (b1.boundInfo.shapeType - b2.boundInfo.shapeType); if (result === 0) result = - (distanceMap.get(b1) - distanceMap.get(b2)); return result; }); } finally { distanceMap.finalize(); } } //console.log('boundInfos', screenCoord, boundInfos); return boundInfos; }, /** @ignore */ /* getTopmostBoundInfoAtCoord: function(screenCoord, excludeObjs, boundInflation) { var enableTrackNearest = this.getViewerConfigs().getInteractionConfigs().getEnableTrackOnNearest(); if (!enableTrackNearest) return this.tryApplySuper('getTopmostBoundInfoAtCoord', [screenCoord, excludeObjs, boundInflation]); // else, track on nearest var SU = Kekule.Render.MetaShapeUtils; var boundInfos = this.getBoundInfosAtCoord(screenCoord, null, boundInflation); //var filteredBoundInfos = []; var result, lastShapeInfo, lastDistance; var setResult = function(boundInfo, shapeInfo, distance) { result = boundInfo; lastShapeInfo = shapeInfo || boundInfo.boundInfo; if (Kekule.ObjUtils.notUnset(distance)) lastDistance = distance; else lastDistance = SU.getDistance(screenCoord, lastShapeInfo); }; for (var i = boundInfos.length - 1; i >= 0; --i) { var info = boundInfos[i]; if (excludeObjs && (excludeObjs.indexOf(info.obj) >= 0)) continue; if (!result) setResult(info); else { var shapeInfo = info.boundInfo; if (shapeInfo.shapeType < lastShapeInfo.shapeType) setResult(info, shapeInfo); else if (shapeInfo.shapeType === lastShapeInfo.shapeType) { var currDistance = SU.getDistance(screenCoord, shapeInfo); if (currDistance < lastDistance) { //console.log('distanceCompare', currDistance, lastDistance); setResult(info, shapeInfo, currDistance); } } } } return result; }, */ /** * Returns whether the UI marker context is enabled in viewer. * Descendants or extensions may override this method. * @returns {Bool} */ getEnableUiContext: function() { return true; }, /** * Returns parent element to create UI context inside viewer. * @private */ getUiContextParentElem: function(disableAutoCreate) { if (!this.getEnableUiContext()) return null; var result = this._uiContextParentElem; if (!result && !disableAutoCreate) // create new { result = this.getDocument().createElement('div'); // IMPORTANT: span may cause dimension calc problem of context this._uiContextParentElem = result; Kekule.HtmlElementUtils.addClass(result, CNS.DYN_CREATED); Kekule.HtmlElementUtils.addClass(result, CCNS.VIEWER_UICONTEXT_PARENT); // IMPORTANT: force to fullfill the parent, otherwise draw context dimension calculation may have problem result.style.width = '100%'; result.style.height = '100%'; // insert after draw context parent elment var drawContextParentElem = this.getDrawContextParentElem(); var root = drawContextParentElem? drawContextParentElem.parentNode: this.getElement(); var refSibling = drawContextParentElem && drawContextParentElem.nextSibling; if (refSibling) root.insertBefore(result, refSibling); else root.appendChild(result); } return result; }, /** @private */ createUiDrawBridge: function() { // UI marker will always be in 2D var result = Kekule.Render.DrawBridge2DMananger.getPreferredBridgeInstance(); if (!result) // can not find suitable draw bridge { //Kekule.error(Kekule.$L('ErrorMsg.DRAW_BRIDGE_NOT_SUPPORTED')); var errorMsg = Kekule.Render.DrawBridge2DMananger.getUnavailableMessage() || Kekule.error(Kekule.$L('ErrorMsg.DRAW_BRIDGE_NOT_SUPPORTED')); if (errorMsg) this.reportException(errorMsg, Kekule.ExceptionLevel.NOT_FATAL_ERROR); } return result; }, /** @private */ adjustDrawParentDim: function() { var drawParentElem = this.getDrawContextParentElem(); var parentElem = drawParentElem.parentNode; var captionElem = this.getCaptionElem(true); // do not auto create var dimParent = Kekule.HtmlElementUtils.getElemClientDimension(parentElem); //var t, h; // drawParentElem is now position: absolute if (captionElem && this.captionIsShown() && captionElem.parentNode === parentElem) { var dimCaption = Kekule.HtmlElementUtils.getElemClientDimension(captionElem); var h = dimCaption.height || 0; //console.log('here'); if (this.getCaptionPos() & Kekule.Widget.Position.TOP) { drawParentElem.style.top = h + 'px'; drawParentElem.style.bottom = '0px'; } else { drawParentElem.style.top = '0px'; drawParentElem.style.bottom = h + 'px'; } Kekule.StyleUtils.removeStyleProperty(drawParentElem.style, 'height'); /* h = Math.max(dimParent.height - dimCaption.height, 0); // avoid value < 0 t = (this.getCaptionPos() & Kekule.Widget.Position.TOP)? dimCaption.height: 0; drawParentElem.style.top = t + 'px'; drawParentElem.style.height = h + 'px'; */ } else { /* t = 0; h = dimParent.height; */ /* // restore 100% height setting Kekule.StyleUtils.removeStyleProperty(drawParentElem.style, 'top'); //Kekule.StyleUtils.removeStyleProperty(drawParentElem.style, 'height'); //drawParentElem.style.height = dimParent.height + 'px'; // explicit set height, or the height may not be updated in some mobile browsers drawParentElem.style.height = '100%'; // some mobile browser has wrong height of parentElem, so here we set it to 100% */ Kekule.StyleUtils.removeStyleProperty(drawParentElem.style, 'top'); Kekule.StyleUtils.removeStyleProperty(drawParentElem.style, 'bottom'); drawParentElem.style.height = '100%'; // some mobile browser has wrong height of parentElem, so here we set it to 100% } //this.refitDrawContext(); }, /** @private */ getInteractionReceiverElem: function() { //return this.getDrawContextParentElem(); return this.getClientElement(); }, /** @ignore */ setDrawDimension: function(/*$super, */width, height) { var newHeight = height; if (this.captionIsShown()) // height need add the height of caption { var dimCaption = Kekule.HtmlElementUtils.getElemClientDimension(this.getCaptionElem()); newHeight += dimCaption.height || 0; } this.tryApplySuper('setDrawDimension', [width, newHeight]) /* $super(width, newHeight) */; }, /// Methods about caption: currently not used /////////// /* @private */ doCreateCaptionElem: function() { var result = this.getDocument().createElement('span'); result.className = CNS.DYN_CREATED + ' ' + ' ' + CNS.SELECTABLE + ' ' + CCNS.VIEWER_CAPTION; this.getElement().appendChild(result); return result; }, /** * Called when caption or showCaption or captionPos property changes. * @private */ captionChanged: function() { if (this.captionIsShown()) { var elem = this.getCaptionElem(); var style = elem.style; var pos = this.getCaptionPos(); if (pos & Kekule.Widget.Position.TOP) { style.top = 0; style.bottom = 'auto'; } else { style.bottom = 0; style.top = 'auto'; } style.display = 'block'; } else // caption need to be hidden { var elem = this.getCaptionElem(true); // do not auto create if (elem) elem.style.display = 'none'; } //this.adjustDrawParentDim(); this.doResize(); }, /** * Returns whether the caption is actually displayed. */ captionIsShown: function() { return this.getCaption() && this.getShowCaption(); }, /* * Called when caption or showCaption property has been changed. * @private */ /* captionChanged: function() { var displayCaption = this.getShowCaption() && this.getCaption(); var elem = this.getCaptionElem(); Kekule.DomUtils.setElementText(elem, this.getCaption()); elem.style.display = displayCaption? 'inherit': 'none'; }, */ //////////////////// methods about UI markers /////////////////////////////// /** * Notify that currently is modifing UI markers and the editor need not to repaint them. * @private */ beginUpdateUiMarkers: function() { if (Kekule.ObjUtils.isUnset(this._uiMarkerUpdateFlag)) this._uiMarkerUpdateFlag = 0; --this._uiMarkerUpdateFlag; this.beginUpdate(); // some times the context should also be repainted to reflect the select/hot track markers }, /** * Call this method to indicate the UI marker update process is over and should be immediately updated. * @private */ endUpdateUiMarkers: function() { this.endUpdate(); ++this._uiMarkerUpdateFlag; if (this._uiMarkerUpdateFlag > 0) this._uiMarkerUpdateFlag = 0; if (!this.isUpdatingUiMarkers()) this.repaintUiMarker(); }, /** * Check if the editor is under continuous UI marker update. * @returns {Bool} * @private */ isUpdatingUiMarkers: function() { return (this._uiMarkerUpdateFlag < 0); }, /** @private */ _getUiMarkerOfName: function(markerName, groups, creationMethod) { var result = this.getUiMarkers().getMarkerOfName(markerName); if (!result && creationMethod) // auto create one { result = creationMethod.apply(this); if (result) { result.setName(markerName); result.setGroups(groups); } } return result; }, /** @private */ _getDefaultHotTrackUiMarker: function(autoCreate) { var creationMethod; if (autoCreate) creationMethod = this.createShapeBasedMarker; var result = this._getUiMarkerOfName('hotTrackMarker', [Kekule.ChemWidget.ViewerUiMarkerGroup.HOTTRACK], creationMethod); return result; }, /** @private */ _getDefaultSelectionUiMarker: function(autoCreate) { var creationMethod; if (autoCreate) creationMethod = this.createShapeBasedMarker; var result = this._getUiMarkerOfName('selectionMarker', [Kekule.ChemWidget.ViewerUiMarkerGroup.SELECT], creationMethod); return result; }, /** * Called when transform has been made to objects and UI markers need to be modified according to it. * The UI markers will also be repainted. * @private */ recalcUiMarkers: function() { if (this.getUiDrawBridge()) { this.beginUpdateUiMarkers(); try { var marker; // hot track var hotTrackedObjs = this.getHotTrackedObjects(); //console.log('recalcUiMarkers ui', hotTrackedObjs); if (hotTrackedObjs && hotTrackedObjs.length) { var bounds = this._calcBoundsOfObjects(hotTrackedObjs); var drawStyles = this.getViewerConfigs().getUiMarkerConfigs().getHotTrackMarkerStyles() || {}; marker = this._getDefaultHotTrackUiMarker(true); // auto create this.modifyShapeBasedMarker(marker, bounds, drawStyles, false); this.showUiMarker(marker); } else // hide hot track marker { marker = this._getDefaultHotTrackUiMarker(false); // not need to auto create if (marker) this.hideUiMarker(marker, false); } // selected var selectedObjs = this.getSelectedObjects(); if (selectedObjs && selectedObjs.length) { var bounds = this._calcBoundsOfObjects(selectedObjs); var drawStyles = this.getViewerConfigs().getUiMarkerConfigs().getSelectionMarkerStyles() || {}; marker = this._getDefaultSelectionUiMarker(true); // auto create this.modifyShapeBasedMarker(marker, bounds, drawStyles, false); this.showUiMarker(marker); } else // hide hot track marker { marker = this._getDefaultSelectionUiMarker(false); // not need to auto create if (marker) this.hideUiMarker(marker, false); } } finally { this.endUpdateUiMarkers(); } } }, /** @private */ repaintUiMarker: function() { //console.log('call repaintUiMarker', this._uiMarkerUpdateFlag, this.getHotTrackedObjects()); if (this.isUpdatingUiMarkers()) return; if (this.getUiDrawBridge() && this.getUiContext()) { this.clearUiContext(); var drawParams = this.calcDrawParams(); this.getUiPainter().draw(this.getUiContext(), drawParams.baseCoord, drawParams.drawOptions); } }, /** * Update the properties of existed UI markers according the current chem object state. * @private */ updateUiMarkers: function(doRepaint) { this.doUpdateUiMarkers(); if (doRepaint) this.repaintUiMarker(); }, /** @private */ doUpdateUiMarkers: function() { this.recalcUiMarkers(); }, /** * Create a new marker based on shapeIn