diff --git a/example/src/ErrorModal/ErrorModal.tsx b/example/src/ErrorModal/ErrorModal.tsx index d197c8b1f9..6b566dbf94 100644 --- a/example/src/ErrorModal/ErrorModal.tsx +++ b/example/src/ErrorModal/ErrorModal.tsx @@ -31,7 +31,8 @@ const ErrorModal = ({ message, close }: ErrorModalProps) => { className={'ok'} onClick={() => { close() - }}> + }} + > OK diff --git a/packages/ketcher-core/src/application/editor/actions/rotate.ts b/packages/ketcher-core/src/application/editor/actions/rotate.ts index e48d9c7d46..deda7379eb 100644 --- a/packages/ketcher-core/src/application/editor/actions/rotate.ts +++ b/packages/ketcher-core/src/application/editor/actions/rotate.ts @@ -34,26 +34,37 @@ export function fromFlip(restruct, selection, dir, center) { const action = new Action() - if (!selection) selection = structSelection(struct) + if (!selection) { + selection = structSelection(struct) + } - if (!selection.atoms) return action.perform(restruct) + if (!selection.atoms) { + return action.perform(restruct) + } const fids = selection.atoms.reduce((acc, aid) => { const atom = struct.atoms.get(aid) - if (!acc[atom.fragment]) acc[atom.fragment] = [] + if (!acc[atom.fragment]) { + acc[atom.fragment] = [] + } acc[atom.fragment].push(aid) return acc }, {}) - const isFragFound = Object.keys(fids) - .map(frag => parseInt(frag, 10)) - .find(frag => { - return !struct.getFragmentIds(frag).equals(new Pile(fids[frag])) - }) + const fidsNumberKeys = Object.keys(fids).map(frag => parseInt(frag, 10)) + + const isFragFound = fidsNumberKeys.find(frag => { + const allFragmentsOfStructure = struct.getFragmentIds(frag) + const selectedFragmentsOfStructure = new Pile(fids[frag]) + const res = allFragmentsOfStructure.equals(selectedFragmentsOfStructure) + return !res + }) - if (isFragFound) return action // empty action + if (typeof isFragFound === 'number') { + return action // empty action + } Object.keys(fids).forEach(frag => { const fragment = new Pile(fids[frag]) @@ -80,7 +91,9 @@ export function fromFlip(restruct, selection, dir, center) { selection.bonds.forEach(bid => { const bond = struct.bonds.get(bid) - if (bond.type !== Bond.PATTERN.TYPE.SINGLE) return + if (bond.type !== Bond.PATTERN.TYPE.SINGLE) { + return + } if (bond.stereo === Bond.PATTERN.STEREO.UP) { action.addOp(new BondAttr(bid, 'stereo', Bond.PATTERN.STEREO.DOWN)) @@ -120,7 +133,9 @@ export function fromRotate(restruct, selection, center, angle) { const action = new Action() - if (!selection) selection = structSelection(struct) + if (!selection) { + selection = structSelection(struct) + } if (selection.atoms) { selection.atoms.forEach(aid => { diff --git a/packages/ketcher-core/src/application/formatters/formatProperties.ts b/packages/ketcher-core/src/application/formatters/formatProperties.ts index 5be72899c6..9e2fd133e1 100644 --- a/packages/ketcher-core/src/application/formatters/formatProperties.ts +++ b/packages/ketcher-core/src/application/formatters/formatProperties.ts @@ -89,8 +89,17 @@ const formatProperties: FormatPropertiesMap = { ) } +const imgFormatProperties = { + svg: { extension: '.svg', name: 'SVG Document' }, + png: { extension: '.png', name: 'PNG Image' } +} + +function getPropertiesByImgFormat(format) { + return imgFormatProperties[format] +} + function getPropertiesByFormat(format: SupportedFormat) { return formatProperties[format] } -export { formatProperties, getPropertiesByFormat } +export { formatProperties, getPropertiesByFormat, getPropertiesByImgFormat } diff --git a/packages/ketcher-core/src/application/render/restruct/reatom.ts b/packages/ketcher-core/src/application/render/restruct/reatom.ts index 95cf2a245d..70830104b1 100644 --- a/packages/ketcher-core/src/application/render/restruct/reatom.ts +++ b/packages/ketcher-core/src/application/render/restruct/reatom.ts @@ -305,7 +305,7 @@ class ReAtom extends ReObject { } if (this.a.attpnt) { - const lsb = bisectSmallestSector(this, restruct.molecule) + const lsb = bisectLargestSector(this, restruct.molecule) showAttpnt(this, render, lsb, restruct.addReObjectPath.bind(restruct)) } @@ -349,7 +349,7 @@ class ReAtom extends ReObject { draw.recenterText(aamPath, aamBox) const visel = this.visel let t = 3 - let dir = bisectSmallestSector(this, restruct.molecule) + let dir = bisectLargestSector(this, restruct.molecule) // estimate the shift to clear the atom label for (let i = 0; i < visel.exts.length; ++i) t = Math.max(t, util.shiftRayBox(ps, dir, visel.exts[i].translate(ps))) @@ -991,7 +991,7 @@ function pathAndRBoxTranslate(path, rbb, x, y) { rbb.y += y } -function bisectSmallestSector(atom: ReAtom, struct: Struct) { +function bisectLargestSector(atom: ReAtom, struct: Struct) { let angles: Array = [] atom.a.neighbors.forEach(hbid => { const hb = struct.halfBonds.get(hbid) @@ -1003,11 +1003,11 @@ function bisectSmallestSector(atom: ReAtom, struct: Struct) { da.push(angles[(i + 1) % angles.length] - angles[i]) } da.push(angles[0] - angles[angles.length - 1] + 2 * Math.PI) - let daMin = Number.MAX_VALUE + let daMax = 0 let ang = -Math.PI / 2 for (let i = 0; i < angles.length; ++i) { - if (da[i] < daMin) { - daMin = da[i] + if (da[i] > daMax) { + daMax = da[i] ang = angles[i] + da[i] / 2 } } diff --git a/packages/ketcher-react/src/Editor.module.less b/packages/ketcher-react/src/Editor.module.less index 8831c744b3..5997a2e960 100644 --- a/packages/ketcher-react/src/Editor.module.less +++ b/packages/ketcher-react/src/Editor.module.less @@ -110,8 +110,6 @@ &[readonly], fieldset[disabled] & { cursor: not-allowed; - background: #efefef; - opacity: 0.6; } } diff --git a/packages/ketcher-react/src/script/editor/Editor.ts b/packages/ketcher-react/src/script/editor/Editor.ts index 8d32a7f51a..81e99a6b2c 100644 --- a/packages/ketcher-react/src/script/editor/Editor.ts +++ b/packages/ketcher-react/src/script/editor/Editor.ts @@ -186,7 +186,14 @@ class Editor implements KetcherEditor { this._tool.cancel() } - const tool = toolMap[name](this, opts) + // TODO: when all tools are refactored to classes, remove this check + // and use new keyword for every tool + let tool + if (name === 'select') { + tool = new toolMap[name](this, opts) + } else { + tool = toolMap[name](this, opts) + } if (!tool) { return null } diff --git a/packages/ketcher-react/src/script/editor/tool/select.js b/packages/ketcher-react/src/script/editor/tool/select.js deleted file mode 100644 index 956e4b4840..0000000000 --- a/packages/ketcher-react/src/script/editor/tool/select.js +++ /dev/null @@ -1,560 +0,0 @@ -/**************************************************************************** - * Copyright 2021 EPAM Systems - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - ***************************************************************************/ - -import { - Action, - SGroup, - fromAtomsAttrs, - fromBondsAttrs, - fromItemsFuse, - fromMultipleMove, - fromTextDeletion, - fromTextUpdating, - getHoverToFuse, - getItemsToFuse, - FunctionalGroup, - fromSimpleObjectResizing -} from 'ketcher-core' - -import LassoHelper from './helper/lasso' -import { atomLongtapEvent } from './atom' -import { sgroupDialog } from './sgroup' -import utils from '../shared/utils' -import { xor } from 'lodash/fp' - -function SelectTool(editor, mode) { - if (!(this instanceof SelectTool)) return new SelectTool(editor, mode) - - this.editor = editor - this.sgroups = editor.render.ctab.sgroups - this.struct = editor.render.ctab - this.molecule = editor.render.ctab.molecule - this.functionalGroups = this.molecule.functionalGroups - this.lassoHelper = new LassoHelper( - mode === 'lasso' ? 0 : 1, - editor, - mode === 'fragment' - ) -} - -SelectTool.prototype.mousedown = function (event) { - // eslint-disable-line max-statements - const rnd = this.editor.render - const ctab = rnd.ctab - const struct = ctab.molecule - const selectedSgroups = [] - const newSelected = { atoms: [], bonds: [] } - let actualSgroupId - - this.editor.hover(null) // TODO review hovering for touch devicess - - const selectFragment = this.lassoHelper.fragment || event.ctrlKey - const ci = this.editor.findItem( - event, - selectFragment - ? [ - 'frags', - 'sgroups', - 'functionalGroups', - 'sgroupData', - 'rgroups', - 'rxnArrows', - 'rxnPluses', - 'enhancedFlags', - 'simpleObjects', - 'texts' - ] - : [ - 'atoms', - 'bonds', - 'sgroups', - 'functionalGroups', - 'sgroupData', - 'rgroups', - 'rxnArrows', - 'rxnPluses', - 'enhancedFlags', - 'simpleObjects', - 'texts' - ] - ) - - if (ci && ci.map === 'atoms' && this.functionalGroups.size) { - const atomId = FunctionalGroup.atomsInFunctionalGroup( - this.functionalGroups, - ci.id - ) - const atomFromStruct = atomId !== null && this.struct.atoms.get(ci.id).a - - if (atomFromStruct) { - for (let sgId of atomFromStruct.sgs.values()) { - actualSgroupId = sgId - } - } - if ( - atomFromStruct && - actualSgroupId !== undefined && - !selectedSgroups.includes(actualSgroupId) - ) - selectedSgroups.push(actualSgroupId) - } - if (ci && ci.map === 'bonds' && this.functionalGroups.size) { - const bondId = FunctionalGroup.bondsInFunctionalGroup( - this.molecule, - this.functionalGroups, - ci.id - ) - const sGroupId = FunctionalGroup.findFunctionalGroupByBond( - this.molecule, - this.functionalGroups, - bondId - ) - if (sGroupId !== null && !selectedSgroups.includes(sGroupId)) - selectedSgroups.push(sGroupId) - } - - if (selectedSgroups.length) { - for (let sgId of selectedSgroups) { - const sgroupAtoms = SGroup.getAtoms( - this.molecule, - this.struct.sgroups.get(sgId).item - ) - const sgroupBonds = SGroup.getBonds( - this.molecule, - this.struct.sgroups.get(sgId).item - ) - newSelected.atoms.push(...sgroupAtoms) && - newSelected.bonds.push(...sgroupBonds) - } - this.editor.selection(newSelected) - } - - this.dragCtx = { - item: ci, - xy0: rnd.page2obj(event) - } - - if (!ci || ci.map === 'atoms') atomLongtapEvent(this, rnd) - - if (!ci) { - // ci.type == 'Canvas' - this.editor.selection(null) - delete this.dragCtx.item - if (!this.lassoHelper.fragment) this.lassoHelper.begin(event) - return true - } - - let sel = closestToSel(ci) - const selection = this.editor.selection() - if (ci.map === 'frags') { - const frag = ctab.frags.get(ci.id) - sel = { - atoms: frag.fragGetAtoms(ctab, ci.id), - bonds: frag.fragGetBonds(ctab, ci.id) - } - } else if (ci.map === 'sgroups' || ci.map === 'functionalGroups') { - const sgroup = ctab.sgroups.get(ci.id).item - sel = { - atoms: SGroup.getAtoms(struct, sgroup), - bonds: SGroup.getBonds(struct, sgroup) - } - } else if (ci.map === 'rgroups') { - const rgroup = ctab.rgroups.get(ci.id) - sel = { - atoms: rgroup.getAtoms(rnd), - bonds: rgroup.getBonds(rnd) - } - } else if (ci.map === 'sgroupData') { - if (isSelected(selection, ci)) return true - } - - if (!event.shiftKey) { - this.editor.selection(isSelected(selection, ci) ? selection : sel) - } else { - this.editor.selection(selMerge(sel, selection, true)) - } - return true -} - -SelectTool.prototype.mousemove = function (event) { - const editor = this.editor - const rnd = editor.render - const restruct = editor.render.ctab - const dragCtx = this.dragCtx - if (dragCtx && dragCtx.stopTapping) dragCtx.stopTapping() - if (dragCtx && dragCtx.item) { - const atoms = restruct.molecule.atoms - const selection = editor.selection() - const shouldDisplayDegree = - dragCtx.item.map === 'atoms' && - atoms.get(dragCtx.item.id).neighbors.length === 1 && - selection.atoms.length === 1 && - !selection.bonds - if (shouldDisplayDegree) { - // moving selected objects - const pos = rnd.page2obj(event) - const angle = utils.calcAngle(dragCtx.xy0, pos) - const degrees = utils.degrees(angle) - this.editor.event.message.dispatch({ info: degrees + 'º' }) - } - if (dragCtx.item.map === 'simpleObjects' && dragCtx.item.ref) { - const current = rnd.page2obj(event) - const diff = current.sub(this.dragCtx.xy0) - dragCtx.action = fromSimpleObjectResizing( - rnd.ctab, - dragCtx.item.id, - diff, - current, - dragCtx.item.ref, - event.shiftKey - ) - editor.update(dragCtx.action, true) - return true - } - if (dragCtx.action) { - dragCtx.action.perform(restruct) - // redraw the elements in unshifted position, lest the have different offset - editor.update(dragCtx.action, true) - } - - const expSel = editor.explicitSelected() - dragCtx.action = fromMultipleMove( - restruct, - expSel, - editor.render.page2obj(event).sub(dragCtx.xy0) - ) - - dragCtx.mergeItems = getItemsToFuse(editor, expSel) - editor.hover(getHoverToFuse(dragCtx.mergeItems)) - - editor.update(dragCtx.action, true) - return true - } - - if (this.lassoHelper.running()) { - const sel = this.lassoHelper.addPoint(event) - editor.selection( - !event.shiftKey ? sel : selMerge(sel, editor.selection(), false) - ) - return true - } - - const maps = - this.lassoHelper.fragment || event.ctrlKey - ? [ - 'frags', - 'sgroups', - 'functionalGroups', - 'sgroupData', - 'rgroups', - 'rxnArrows', - 'rxnPluses', - 'enhancedFlags', - 'simpleObjects', - 'texts' - ] - : [ - 'atoms', - 'bonds', - 'sgroups', - 'functionalGroups', - 'sgroupData', - 'rgroups', - 'rxnArrows', - 'rxnPluses', - 'enhancedFlags', - 'simpleObjects', - 'texts' - ] - - editor.hover(editor.findItem(event, maps)) - - return true -} - -SelectTool.prototype.mouseup = function (event) { - const selected = this.editor.selection() - const selectedSgroups = [] - const newSelected = { atoms: [], bonds: [] } - let actualSgroupId - - if (selected && this.functionalGroups.size && selected.atoms) { - for (let atom of selected.atoms) { - const atomId = FunctionalGroup.atomsInFunctionalGroup( - this.functionalGroups, - atom - ) - const atomFromStruct = atomId !== null && this.struct.atoms.get(atomId).a - - if (atomFromStruct) { - for (let sgId of atomFromStruct.sgs.values()) { - actualSgroupId = sgId - } - } - if ( - atomFromStruct && - actualSgroupId !== undefined && - !selectedSgroups.includes(actualSgroupId) - ) - selectedSgroups.push(actualSgroupId) - } - } - - if (selected && this.functionalGroups.size && selected.bonds) { - for (let atom of selected.bonds) { - const bondId = FunctionalGroup.bondsInFunctionalGroup( - this.molecule, - this.functionalGroups, - atom - ) - const sGroupId = FunctionalGroup.findFunctionalGroupByBond( - this.molecule, - this.functionalGroups, - bondId - ) - if (sGroupId !== null && !selectedSgroups.includes(sGroupId)) - selectedSgroups.push(sGroupId) - } - } - - if (selectedSgroups.length) { - for (let sgId of selectedSgroups) { - const sgroupAtoms = SGroup.getAtoms( - this.molecule, - this.struct.sgroups.get(sgId).item - ) - const sgroupBonds = SGroup.getBonds( - this.molecule, - this.struct.sgroups.get(sgId).item - ) - newSelected.atoms.push(...sgroupAtoms) && - newSelected.bonds.push(...sgroupBonds) - } - } - - // eslint-disable-line max-statements - const editor = this.editor - const restruct = editor.render.ctab - const dragCtx = this.dragCtx - - if (dragCtx && dragCtx.stopTapping) dragCtx.stopTapping() - - if (dragCtx && dragCtx.item) { - dragCtx.action = dragCtx.action - ? fromItemsFuse(restruct, dragCtx.mergeItems).mergeWith(dragCtx.action) - : fromItemsFuse(restruct, dragCtx.mergeItems) - - editor.hover(null) - if (dragCtx.mergeItems) editor.selection(null) - if (dragCtx.action.operations.length !== 0) editor.update(dragCtx.action) - - delete this.dragCtx - } else if (this.lassoHelper.running()) { - // TODO it catches more events than needed, to be re-factored - const sel = - newSelected.atoms.length > 0 - ? selMerge(this.lassoHelper.end(), newSelected) - : this.lassoHelper.end() - editor.selection(!event.shiftKey ? sel : selMerge(sel, editor.selection())) - } else if (this.lassoHelper.fragment) { - if (!event.shiftKey) editor.selection(null) - } - this.editor.event.message.dispatch({ - info: false - }) - return true -} - -SelectTool.prototype.dblclick = function (event) { - // eslint-disable-line max-statements - var editor = this.editor - var rnd = this.editor.render - var ci = this.editor.findItem(event, [ - 'atoms', - 'bonds', - 'sgroups', - 'functionalGroups', - 'sgroupData', - 'texts' - ]) - - const atomResult = [] - const bondResult = [] - const result = [] - if (ci && this.functionalGroups && ci.map === 'atoms') { - const atomId = FunctionalGroup.atomsInFunctionalGroup( - this.functionalGroups, - ci.id - ) - const atomFromStruct = atomId !== null && this.struct.atoms.get(atomId).a - if ( - atomId && - !FunctionalGroup.isBondInContractedFunctionalGroup( - atomFromStruct, - this.sgroups, - this.functionalGroups, - true - ) - ) - atomResult.push(atomId) - } - if (ci && this.functionalGroups && ci.map === 'bonds') { - const bondId = FunctionalGroup.bondsInFunctionalGroup( - this.molecule, - this.functionalGroups, - ci.id - ) - const bondFromStruct = bondId !== null && this.struct.bonds.get(bondId).b - if ( - bondId && - !FunctionalGroup.isBondInContractedFunctionalGroup( - bondFromStruct, - this.sgroups, - this.functionalGroups, - true - ) - ) - bondResult.push(bondId) - } - if (atomResult.length > 0) { - for (let id of atomResult) { - const fgId = FunctionalGroup.findFunctionalGroupByAtom( - this.functionalGroups, - id - ) - if (fgId !== null && !result.includes(fgId)) { - result.push(fgId) - } - } - this.editor.event.removeFG.dispatch({ fgIds: result }) - return - } else if (bondResult.length > 0) { - for (let id of bondResult) { - const fgId = FunctionalGroup.findFunctionalGroupByBond( - this.molecule, - this.functionalGroups, - id - ) - if (fgId !== null && !result.includes(fgId)) { - result.push(fgId) - } - } - this.editor.event.removeFG.dispatch({ fgIds: result }) - return - } - if (!ci) return true - - var struct = rnd.ctab.molecule - if (ci.map === 'atoms') { - const action = new Action() - var atom = struct.atoms.get(ci.id) - var ra = editor.event.elementEdit.dispatch(atom) - const selection = this.editor.selection().atoms - Promise.resolve(ra) - .then(newatom => { - // TODO: deep compare to not produce dummy, e.g. - // atom.label != attrs.label || !atom.atomList.equals(attrs.atomList) - selection.forEach(aid => { - action.mergeWith(fromAtomsAttrs(rnd.ctab, aid, newatom)) - }) - editor.update(action) - }) - .catch(() => null) // w/o changes - } else if (ci.map === 'bonds') { - const action = new Action() - const selection = this.editor.selection().bonds - var bond = rnd.ctab.bonds.get(ci.id).b - var rb = editor.event.bondEdit.dispatch(bond) - Promise.resolve(rb) - .then(newbond => { - selection.forEach(bid => { - action.mergeWith(fromBondsAttrs(rnd.ctab, bid, newbond)) - }) - editor.update(action) - }) - .catch(() => null) // w/o changes - } else if ( - (ci.map === 'sgroups' && - !FunctionalGroup.isFunctionalGroup(struct.sgroups.get(ci.id))) || - ci.map === 'sgroupData' - ) { - this.editor.selection(closestToSel(ci)) - sgroupDialog(this.editor, ci.id) - } else if (ci.map === 'texts') { - this.editor.selection(closestToSel(ci)) - const text = struct.texts.get(ci.id) - const dialog = editor.event.elementEdit.dispatch({ ...text, type: 'text' }) - - dialog - .then(({ content }) => { - if (!content) { - editor.update(fromTextDeletion(editor.render.ctab, ci.id)) - } else if (content !== text.content) { - editor.update(fromTextUpdating(editor.render.ctab, ci.id, content)) - } - }) - .catch(() => null) - } - return true -} - -SelectTool.prototype.cancel = function () { - if (this.dragCtx && this.dragCtx.stopTapping) this.dragCtx.stopTapping() - - if (this.dragCtx && this.dragCtx.action) { - var action = this.dragCtx.action - this.editor.update(action) - } - if (this.lassoHelper.running()) this.editor.selection(this.lassoHelper.end()) - - delete this.dragCtx - - this.editor.hover(null) -} -SelectTool.prototype.mouseleave = SelectTool.prototype.cancel - -function closestToSel(ci) { - const res = {} - res[ci.map] = [ci.id] - return res -} - -// TODO: deep-merge? -export function selMerge(selection, add, reversible) { - if (add) { - Object.keys(add).forEach(item => { - if (!selection[item]) selection[item] = add[item].slice() - else selection[item] = uniqArray(selection[item], add[item], reversible) - }) - } - return selection -} - -function isSelected(selection, item) { - return ( - selection && selection[item.map] && selection[item.map].includes(item.id) - ) -} - -function uniqArray(dest, add, reversible) { - return add.reduce((res, item) => { - if (reversible) dest = xor(dest, [item]) - else if (!dest.includes(item)) dest.push(item) - return dest - }, []) -} - -export default SelectTool diff --git a/packages/ketcher-react/src/script/editor/tool/select.ts b/packages/ketcher-react/src/script/editor/tool/select.ts new file mode 100644 index 0000000000..2ab49a13e8 --- /dev/null +++ b/packages/ketcher-react/src/script/editor/tool/select.ts @@ -0,0 +1,572 @@ +/**************************************************************************** + * Copyright 2021 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +import { + Action, + SGroup, + fromAtomsAttrs, + fromBondsAttrs, + fromItemsFuse, + fromMultipleMove, + fromTextDeletion, + fromTextUpdating, + getHoverToFuse, + getItemsToFuse, + FunctionalGroup, + fromSimpleObjectResizing +} from 'ketcher-core' + +import LassoHelper from './helper/lasso' +import { atomLongtapEvent } from './atom' +import { sgroupDialog } from './sgroup' +import utils from '../shared/utils' +import { xor } from 'lodash/fp' +import { Editor } from '../Editor' + +class SelectTool { + #mode: string + #lassoHelper: any + editor: Editor + dragCtx: any + + constructor(editor, mode) { + this.editor = editor + this.#mode = mode + this.#lassoHelper = new LassoHelper( + this.#mode === 'lasso' ? 0 : 1, + editor, + this.#mode === 'fragment' + ) + } + + mousedown(event) { + const rnd = this.editor.render + const ctab = rnd.ctab + const molecule = ctab.molecule + const functionalGroups = molecule.functionalGroups + const selectedSgroups: any[] = [] + const newSelected = { atoms: [] as any[], bonds: [] as any[] } + let actualSgroupId + + this.editor.hover(null) // TODO review hovering for touch devicess + + const selectFragment = this.#lassoHelper.fragment || event.ctrlKey + const ci = this.editor.findItem( + event, + selectFragment + ? [ + 'frags', + 'sgroups', + 'functionalGroups', + 'sgroupData', + 'rgroups', + 'rxnArrows', + 'rxnPluses', + 'enhancedFlags', + 'simpleObjects', + 'texts' + ] + : [ + 'atoms', + 'bonds', + 'sgroups', + 'functionalGroups', + 'sgroupData', + 'rgroups', + 'rxnArrows', + 'rxnPluses', + 'enhancedFlags', + 'simpleObjects', + 'texts' + ], + null + ) + + if (ci && ci.map === 'atoms' && functionalGroups.size) { + const atomId = FunctionalGroup.atomsInFunctionalGroup( + functionalGroups, + ci.id + ) + const atomFromStruct = atomId !== null && ctab.atoms.get(ci.id)?.a + + if (atomFromStruct) { + for (let sgId of atomFromStruct.sgs.values()) { + actualSgroupId = sgId + } + } + if ( + atomFromStruct && + actualSgroupId !== undefined && + !selectedSgroups.includes(actualSgroupId) + ) + selectedSgroups.push(actualSgroupId) + } + if (ci && ci.map === 'bonds' && functionalGroups.size) { + const bondId = FunctionalGroup.bondsInFunctionalGroup( + molecule, + functionalGroups, + ci.id + ) + const sGroupId = FunctionalGroup.findFunctionalGroupByBond( + molecule, + functionalGroups, + bondId + ) + if (sGroupId !== null && !selectedSgroups.includes(sGroupId)) + selectedSgroups.push(sGroupId) + } + + if (selectedSgroups.length) { + for (let sgId of selectedSgroups) { + const sgroup = ctab.sgroups.get(sgId) + if (sgroup) { + const sgroupAtoms = SGroup.getAtoms(molecule, sgroup.item) + const sgroupBonds = SGroup.getBonds(molecule, sgroup.item) + newSelected.atoms.push(...sgroupAtoms) && + newSelected.bonds.push(...sgroupBonds) + } + } + this.editor.selection(newSelected) + } + + this.dragCtx = { + item: ci, + xy0: rnd.page2obj(event) + } + + if (!ci || ci.map === 'atoms') atomLongtapEvent(this, rnd) + + if (!ci) { + // ci.type == 'Canvas' + this.editor.selection(null) + delete this.dragCtx.item + if (!this.#lassoHelper.fragment) this.#lassoHelper.begin(event) + return true + } + + let sel = closestToSel(ci) + let sgroups = ctab.sgroups.get(ci.id) + const selection = this.editor.selection() + if (ci.map === 'frags') { + const frag = ctab.frags.get(ci.id) + sel = { + atoms: frag.fragGetAtoms(ctab, ci.id), + bonds: frag.fragGetBonds(ctab, ci.id) + } + } else if ( + (ci.map === 'sgroups' || ci.map === 'functionalGroups') && + sgroups + ) { + const sgroup = sgroups.item + sel = { + atoms: SGroup.getAtoms(molecule, sgroup), + bonds: SGroup.getBonds(molecule, sgroup) + } + } else if (ci.map === 'rgroups') { + const rgroup = ctab.rgroups.get(ci.id) + sel = { + atoms: rgroup.getAtoms(rnd), + bonds: rgroup.getBonds(rnd) + } + } else if (ci.map === 'sgroupData') { + if (isSelected(selection, ci)) return true + } + + if (!event.shiftKey) { + this.editor.selection(isSelected(selection, ci) ? selection : sel) + } else { + this.editor.selection(selMerge(sel, selection, true)) + } + return true + } + + mousemove(event) { + const editor = this.editor + const rnd = editor.render + const restruct = editor.render.ctab + const dragCtx = this.dragCtx + if (dragCtx && dragCtx.stopTapping) dragCtx.stopTapping() + if (dragCtx && dragCtx.item) { + const atoms = restruct.molecule.atoms + const selection = editor.selection() + const shouldDisplayDegree = + dragCtx.item.map === 'atoms' && + atoms?.get(dragCtx.item.id)?.neighbors.length === 1 && + selection?.atoms?.length === 1 && + !selection.bonds + if (shouldDisplayDegree) { + // moving selected objects + const pos = rnd.page2obj(event) + const angle = utils.calcAngle(dragCtx.xy0, pos) + const degrees = utils.degrees(angle) + this.editor.event.message.dispatch({ info: degrees + 'º' }) + } + if (dragCtx.item.map === 'simpleObjects' && dragCtx.item.ref) { + const current = rnd.page2obj(event) + const diff = current.sub(this.dragCtx.xy0) + dragCtx.action = fromSimpleObjectResizing( + rnd.ctab, + dragCtx.item.id, + diff, + current, + dragCtx.item.ref, + event.shiftKey + ) + editor.update(dragCtx.action, true) + return true + } + if (dragCtx.action) { + dragCtx.action.perform(restruct) + // redraw the elements in unshifted position, lest the have different offset + editor.update(dragCtx.action, true) + } + + const expSel = editor.explicitSelected() + dragCtx.action = fromMultipleMove( + restruct, + expSel, + editor.render.page2obj(event).sub(dragCtx.xy0) + ) + + dragCtx.mergeItems = getItemsToFuse(editor, expSel) + editor.hover(getHoverToFuse(dragCtx.mergeItems)) + + editor.update(dragCtx.action, true) + return true + } + + if (this.#lassoHelper.running()) { + const sel = this.#lassoHelper.addPoint(event) + editor.selection( + !event.shiftKey ? sel : selMerge(sel, editor.selection(), false) + ) + return true + } + + const maps = + this.#lassoHelper.fragment || event.ctrlKey + ? [ + 'frags', + 'sgroups', + 'functionalGroups', + 'sgroupData', + 'rgroups', + 'rxnArrows', + 'rxnPluses', + 'enhancedFlags', + 'simpleObjects', + 'texts' + ] + : [ + 'atoms', + 'bonds', + 'sgroups', + 'functionalGroups', + 'sgroupData', + 'rgroups', + 'rxnArrows', + 'rxnPluses', + 'enhancedFlags', + 'simpleObjects', + 'texts' + ] + + editor.hover(editor.findItem(event, maps, null)) + + return true + } + + mouseup(event) { + const editor = this.editor + const selected = editor.selection() + const struct = editor.render.ctab + const molecule = struct.molecule + const functionalGroups = molecule.functionalGroups + const selectedSgroups: any[] = [] + const newSelected = { atoms: [] as any[], bonds: [] as any[] } + let actualSgroupId + + if (selected && functionalGroups.size && selected.atoms) { + for (let atom of selected.atoms) { + const atomId = FunctionalGroup.atomsInFunctionalGroup( + functionalGroups, + atom + ) + const atomFromStruct = atomId !== null && struct.atoms.get(atomId)?.a + + if (atomFromStruct) { + for (let sgId of atomFromStruct.sgs.values()) { + actualSgroupId = sgId + } + } + if ( + atomFromStruct && + actualSgroupId !== undefined && + !selectedSgroups.includes(actualSgroupId) + ) + selectedSgroups.push(actualSgroupId) + } + } + + if (selected && functionalGroups.size && selected.bonds) { + for (let atom of selected.bonds) { + const bondId = FunctionalGroup.bondsInFunctionalGroup( + molecule, + functionalGroups, + atom + ) + const sGroupId = FunctionalGroup.findFunctionalGroupByBond( + molecule, + functionalGroups, + bondId + ) + if (sGroupId !== null && !selectedSgroups.includes(sGroupId)) + selectedSgroups.push(sGroupId) + } + } + + if (selectedSgroups.length) { + for (let sgId of selectedSgroups) { + const sgroup = struct.sgroups.get(sgId) + if (sgroup) { + const sgroupAtoms = SGroup.getAtoms(molecule, sgroup.item) + const sgroupBonds = SGroup.getBonds(molecule, sgroup.item) + newSelected.atoms.push(...sgroupAtoms) && + newSelected.bonds.push(...sgroupBonds) + } + } + } + + const dragCtx = this.dragCtx + + if (dragCtx && dragCtx.stopTapping) dragCtx.stopTapping() + + if (dragCtx && dragCtx.item) { + dragCtx.action = dragCtx.action + ? fromItemsFuse(struct, dragCtx.mergeItems).mergeWith(dragCtx.action) + : fromItemsFuse(struct, dragCtx.mergeItems) + + editor.hover(null) + if (dragCtx.mergeItems) editor.selection(null) + if (dragCtx.action.operations.length !== 0) editor.update(dragCtx.action) + + delete this.dragCtx + } else if (this.#lassoHelper.running()) { + // TODO it catches more events than needed, to be re-factored + const sel = + newSelected.atoms.length > 0 + ? selMerge(this.#lassoHelper.end(), newSelected, false) + : this.#lassoHelper.end() + editor.selection( + !event.shiftKey ? sel : selMerge(sel, editor.selection(), false) + ) + } else if (this.#lassoHelper.fragment) { + if (!event.shiftKey) editor.selection(null) + } + editor.event.message.dispatch({ + info: false + }) + return true + } + + dblclick(event) { + const editor = this.editor + const struct = editor.render.ctab + const { molecule, sgroups } = struct + const functionalGroups = molecule.functionalGroups + const rnd = editor.render + const ci = editor.findItem( + event, + ['atoms', 'bonds', 'sgroups', 'functionalGroups', 'sgroupData', 'texts'], + null + ) + + const atomResult: any[] = [] + const bondResult: any[] = [] + const result: any[] = [] + if (ci && functionalGroups && ci.map === 'atoms') { + const atomId = FunctionalGroup.atomsInFunctionalGroup( + functionalGroups, + ci.id + ) + const atomFromStruct = atomId !== null && struct.atoms.get(atomId)?.a + if ( + atomId && + !FunctionalGroup.isBondInContractedFunctionalGroup( + atomFromStruct, + sgroups, + functionalGroups, + true + ) + ) + atomResult.push(atomId) + } + if (ci && functionalGroups && ci.map === 'bonds') { + const bondId = FunctionalGroup.bondsInFunctionalGroup( + molecule, + functionalGroups, + ci.id + ) + const bondFromStruct = bondId !== null && struct.bonds.get(bondId)?.b + if ( + bondId && + !FunctionalGroup.isBondInContractedFunctionalGroup( + bondFromStruct, + sgroups, + functionalGroups, + true + ) + ) + bondResult.push(bondId) + } + if (atomResult.length > 0) { + for (let id of atomResult) { + const fgId = FunctionalGroup.findFunctionalGroupByAtom( + functionalGroups, + id + ) + if (fgId !== null && !result.includes(fgId)) { + result.push(fgId) + } + } + editor.event.removeFG.dispatch({ fgIds: result }) + return + } else if (bondResult.length > 0) { + for (let id of bondResult) { + const fgId = FunctionalGroup.findFunctionalGroupByBond( + molecule, + functionalGroups, + id + ) + if (fgId !== null && !result.includes(fgId)) { + result.push(fgId) + } + } + this.editor.event.removeFG.dispatch({ fgIds: result }) + return + } + if (!ci) return true + + const selection = this.editor.selection() + + if (ci.map === 'atoms') { + const action = new Action() + var atom = molecule.atoms.get(ci.id) + var ra = editor.event.elementEdit.dispatch(atom) + if (selection?.atoms) { + const selectionAtoms = selection.atoms + Promise.resolve(ra) + .then(newatom => { + // TODO: deep compare to not produce dummy, e.g. + // atom.label != attrs.label || !atom.atomList.equals(attrs.atomList) + selectionAtoms.forEach(aid => { + action.mergeWith(fromAtomsAttrs(struct, aid, newatom, false)) + }) + editor.update(action) + }) + .catch(() => null) + } + } else if (ci.map === 'bonds') { + const bond = rnd.ctab.bonds.get(ci.id)?.b + const rb = editor.event.bondEdit.dispatch(bond) + + if (selection?.bonds) { + const action = new Action() + const bondsSelection = selection.bonds + Promise.resolve(rb) + .then(newbond => { + bondsSelection.forEach(bid => { + action.mergeWith(fromBondsAttrs(struct, bid, newbond)) + }) + editor.update(action) + }) + .catch(() => null) // w/o changes + } + } else if ( + (ci.map === 'sgroups' && + !FunctionalGroup.isFunctionalGroup(molecule.sgroups.get(ci.id))) || + ci.map === 'sgroupData' + ) { + editor.selection(closestToSel(ci)) + sgroupDialog(editor, ci.id) + } else if (ci.map === 'texts') { + editor.selection(closestToSel(ci)) + const text = molecule.texts.get(ci.id) + const dialog = editor.event.elementEdit.dispatch({ + ...text, + type: 'text' + }) + + dialog + .then(({ content }) => { + if (!content) { + editor.update(fromTextDeletion(struct, ci.id)) + } else if (content !== text?.content) { + editor.update(fromTextUpdating(struct, ci.id, content)) + } + }) + .catch(() => null) + } + return true + } + + mouseleave(_) { + if (this.dragCtx && this.dragCtx.stopTapping) this.dragCtx.stopTapping() + + if (this.dragCtx && this.dragCtx.action) { + var action = this.dragCtx.action + this.editor.update(action) + } + if (this.#lassoHelper.running()) + this.editor.selection(this.#lassoHelper.end()) + + delete this.dragCtx + + this.editor.hover(null) + } +} +function closestToSel(ci) { + const res = {} + res[ci.map] = [ci.id] + return res +} + +// TODO: deep-merge? +export function selMerge(selection, add, reversible: boolean) { + if (add) { + Object.keys(add).forEach(item => { + if (!selection[item]) selection[item] = add[item].slice() + else selection[item] = uniqArray(selection[item], add[item], reversible) + }) + } + return selection +} + +function isSelected(selection, item) { + return ( + selection && selection[item.map] && selection[item.map].includes(item.id) + ) +} + +function uniqArray(dest, add, reversible: boolean) { + return add.reduce((_, item) => { + if (reversible) dest = xor(dest, [item]) + else if (!dest.includes(item)) dest.push(item) + return dest + }, []) +} + +export default SelectTool diff --git a/packages/ketcher-react/src/script/ui/state/hotkeys.js b/packages/ketcher-react/src/script/ui/state/hotkeys.js index 68ce245a01..cb7f83839a 100644 --- a/packages/ketcher-react/src/script/ui/state/hotkeys.js +++ b/packages/ketcher-react/src/script/ui/state/hotkeys.js @@ -16,7 +16,12 @@ import * as clipArea from '../component/cliparea/cliparea' -import { KetSerializer, MolSerializer, formatProperties } from 'ketcher-core' +import { + KetSerializer, + MolSerializer, + formatProperties, + ChemicalMimeType +} from 'ketcher-core' import { debounce, isEqual } from 'lodash/fp' import { load, onAction } from './shared' @@ -144,9 +149,9 @@ export function initClipboard(dispatch, getState) { }, onPaste(data) { const structStr = - data['application/json'] || - data['chemical/x-mdl-molfile'] || - data['chemical/x-mdl-rxnfile'] || + data[ChemicalMimeType.KET] || + data[ChemicalMimeType.Mol] || + data[ChemicalMimeType.Rxn] || data['text/plain'] if (structStr || !rxnTextPlain.test(data['text/plain'])) @@ -175,11 +180,9 @@ function clipData(editor) { try { const serializer = new KetSerializer() const ket = serializer.serialize(struct) - res['application/json'] = ket + res[ChemicalMimeType.KET] = ket - const type = struct.isReaction - ? 'chemical/x-mdl-molfile' - : 'chemical/x-mdl-rxnfile' + const type = struct.isReaction ? ChemicalMimeType.Mol : ChemicalMimeType.Rxn const data = molSerializer.serialize(struct) res['text/plain'] = data res[type] = data diff --git a/packages/ketcher-react/src/script/ui/views/components/Dialog/Dialog.module.less b/packages/ketcher-react/src/script/ui/views/components/Dialog/Dialog.module.less index 2dcf81eab0..70571e3e4a 100644 --- a/packages/ketcher-react/src/script/ui/views/components/Dialog/Dialog.module.less +++ b/packages/ketcher-react/src/script/ui/views/components/Dialog/Dialog.module.less @@ -138,11 +138,17 @@ } button { - background: #d1d5e3; + background: transparent; + border: 1px solid #343434; + color: #343434; float: left; - } - button:hover { - background: #aaadb9; + &:hover { + border: 1px solid #000; + color: #000; + } + &:disabled { + opacity: 0.4; + } } } .body { diff --git a/packages/ketcher-react/src/script/ui/views/modal/components/document/Save/Save.jsx b/packages/ketcher-react/src/script/ui/views/modal/components/document/Save/Save.jsx index 3881348b08..f53a8068e2 100644 --- a/packages/ketcher-react/src/script/ui/views/modal/components/document/Save/Save.jsx +++ b/packages/ketcher-react/src/script/ui/views/modal/components/document/Save/Save.jsx @@ -20,15 +20,15 @@ import Form, { Field } from '../../../../../component/form/form/form' import { FormatterFactory, formatProperties, - getPropertiesByFormat + getPropertiesByFormat, + getPropertiesByImgFormat, + KetSerializer } from 'ketcher-core' import { Component, createRef } from 'react' import { Dialog } from '../../../../components' import { ErrorsContext } from '../../../../../../../contexts' import SaveButton from '../../../../../component/view/savebutton' -import SaveImageTab from './SaveImageTab' -import Tabs from '../../../../../component/view/Tabs' import { check } from '../../../../../state/server' import classes from './Save.module.less' import { connect } from 'react-redux' @@ -40,7 +40,7 @@ const saveSchema = { type: 'object', properties: { filename: { - title: 'Filename', + title: 'File name:', type: 'string', maxLength: 128, pattern: /^[^.<>:?"*|/\\][^<>:?"*|/\\]*$/, @@ -51,7 +51,7 @@ const saveSchema = { } }, format: { - title: 'Format', + title: 'File format:', enum: Object.keys(formatProperties), enumNames: Object.keys(formatProperties).map( format => formatProperties[format].name @@ -79,7 +79,9 @@ class SaveDialog extends Component { 'smarts', 'inChI', 'inChIAuxInfo', - 'cml' + 'cml', + 'svg', + 'png' ) this.saveSchema = saveSchema @@ -87,7 +89,11 @@ class SaveDialog extends Component { this.saveSchema.properties.format, { enum: formats, - enumNames: formats.map(format => getPropertiesByFormat(format).name) + enumNames: formats.map(format => { + const formatProps = + getPropertiesByFormat(format) || getPropertiesByImgFormat(format) + return formatProps.name + }) } ) } @@ -100,41 +106,61 @@ class SaveDialog extends Component { ) } + isImageFormat = format => { + return !!getPropertiesByImgFormat(format) + } + showStructWarningMessage = format => { const { errors } = this.props.formState return format !== 'mol' && Object.keys(errors).length > 0 } changeType = type => { - const errorHandler = this.context.errorHandler - this.setState({ disableControls: true }) - const { struct, server, options, formState } = this.props + const errorHandler = this.context.errorHandler + if (this.isImageFormat(type)) { + const ketSerialize = new KetSerializer() + const structStr = ketSerialize.serialize(struct) + this.setState({ imageFormat: type, structStr }) + let options = {} + options.outputFormat = type - const factory = new FormatterFactory(server) - - const service = factory.create(type, options) - - return service - .getStructureFromStructAsync(struct) - .then( - structStr => { - this.setState({ structStr }) - setTimeout(() => { - if (this.textAreaRef.current) { - this.textAreaRef.current.select() - } - }, 10) // TODO: remove hack - }, - e => { - errorHandler(e.message) + return server + .generateImageAsBase64(structStr, options) + .then(base64 => { + this.setState({ imageSrc: base64 }) + }) + .catch(e => { + errorHandler(e) this.props.onResetForm(formState) return e - } - ) - .finally(() => { - this.setState({ disableControls: false }) - }) + }) + } else { + this.setState({ disableControls: true }) + const factory = new FormatterFactory(server) + const service = factory.create(type, options) + + return service + .getStructureFromStructAsync(struct) + .then( + structStr => { + this.setState({ structStr }) + setTimeout(() => { + if (this.textAreaRef.current) { + this.textAreaRef.current.select() + } + }, 10) // TODO: remove hack + }, + e => { + errorHandler(e.message) + this.props.onResetForm(formState) + return e + } + ) + .finally(() => { + this.setState({ disableControls: false }) + }) + } } getWarnings = format => { @@ -143,14 +169,15 @@ class SaveDialog extends Component { const structWarning = 'Structure contains errors, please check the data, otherwise you ' + 'can lose some properties or the whole structure after saving in this format.' - const saveWarning = structFormat.couldBeSaved(struct, format) - const isStructInvalid = this.showStructWarningMessage(format) - - if (isStructInvalid) { - warnings.push(structWarning) - } - if (saveWarning) { - warnings.push(saveWarning) + if (!this.isImageFormat(format)) { + const saveWarning = structFormat.couldBeSaved(struct, format) + const isStructInvalid = this.showStructWarningMessage(format) + if (isStructInvalid) { + warnings.push(structWarning) + } + if (saveWarning) { + warnings.push(saveWarning) + } } if (moleculeErrors) { @@ -159,20 +186,14 @@ class SaveDialog extends Component { return warnings } - changeTab = tabIndex => { - this.setState({ tabIndex }) - } - - changeImageFormat = imageFormat => { - this.setState({ imageFormat }) - } - renderSaveFile = () => { const formState = Object.assign({}, this.props.formState) delete formState.moleculeErrors const { filename, format } = formState.result const warnings = this.getWarnings(format) - const { structStr } = this.state + const { structStr, imageSrc } = this.state + const isCleanStruct = this.props.struct.isBlank() + return (
-