Skip to content

Commit

Permalink
Add right click menu for atoms
Browse files Browse the repository at this point in the history
Depends on: #1896
Resolves: #1899

* Add atom single/batch operations
* Update logic in bond operations (more details in PR description)
* Make `changeAtomsStereoAction` static for invoking outside the class
  • Loading branch information
yuleicul committed Dec 22, 2022
1 parent 4bd0069 commit e79cf5b
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 48 deletions.
21 changes: 11 additions & 10 deletions packages/ketcher-react/src/script/editor/tool/enhanced-stereo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,23 @@ class EnhancedStereoTool {
return
}

this.changeAtomsStereoAction().then(
(action) => action && editor.update(action)
)
EnhancedStereoTool.changeAtomsStereoAction(
this.editor,
this.stereoAtoms
).then((action) => action && editor.update(action))
}

changeAtomsStereoAction() {
const struct = this.editor.struct()
const restruct = this.editor.render.ctab
const stereoLabels = this.stereoAtoms.map((stereoAtom) => {
static changeAtomsStereoAction(editor: Editor, stereoAtoms: Array<number>) {
const struct = editor.struct()
const restruct = editor.render.ctab
const stereoLabels = stereoAtoms.map((stereoAtom) => {
const atom = struct.atoms.get(stereoAtom)
return atom && atom.stereoLabel
})
const hasAnotherLabel = stereoLabels.some(
(stereoLabel) => stereoLabel !== stereoLabels[0]
)
const res = this.editor.event.enhancedStereoEdit.dispatch({
const res = editor.event.enhancedStereoEdit.dispatch({
stereoLabel: hasAnotherLabel ? null : stereoLabels[0]
})

Expand All @@ -67,7 +68,7 @@ class EnhancedStereoTool {
return null
}

const action = this.stereoAtoms.reduce(
const action = stereoAtoms.reduce(
(acc, stereoAtom) => {
return acc.mergeWith(
fromStereoFlagUpdate(
Expand All @@ -78,7 +79,7 @@ class EnhancedStereoTool {
},
fromAtomsAttrs(
restruct,
this.stereoAtoms,
stereoAtoms,
{
stereoLabel
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@
import { Menu } from 'react-contexify'
import 'react-contexify/ReactContexify.css'
import styles from './ContextMenu.module.less'
import AtomBatchOperations from './items/AtomBatchOperations'
import AtomSingleOperations from './items/AtomSingleOperations'
import BondBatchOperations from './items/BondBatchOperations'
import BondSingleOperations from './items/BondSingleOperations'

export const CONTEXT_MENU_ID = 'ketcherBondContextMenu'
export const CONTEXT_MENU_ID = 'ketcherBondAndAtomContextMenu'

const ContextMenu: React.FC = () => {
return (
<Menu id={CONTEXT_MENU_ID} animation={false} className={styles.contextMenu}>
<BondSingleOperations />
<AtomSingleOperations />
<BondBatchOperations />
<AtomBatchOperations />
</Menu>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const ContextMenuTrigger: React.FC = ({ children }) => {
const handleDisplay = useCallback<React.MouseEventHandler<HTMLDivElement>>(
(event) => {
const editor = getKetcherInstance().editor as Editor
const closestItem = editor.findItem(event, ['bonds'])
const closestItem = editor.findItem(event, null)

if (!closestItem) {
hideAll()
Expand Down Expand Up @@ -118,7 +118,7 @@ const ContextMenuTrigger: React.FC = ({ children }) => {
closestItem
}
})
} else if (closestItem.map === 'bonds') {
} else if (closestItem.map === 'bonds' || closestItem.map === 'atoms') {
// Show menu items for single update
if (selection) {
editor.render.ctab.setSelection(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/****************************************************************************
* Copyright 2022 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,
findStereoAtoms,
fromAtomsAttrs,
fromOneAtomDeletion
} from 'ketcher-core'
import { useCallback, useRef } from 'react'
import type { PredicateParams } from 'react-contexify'
import { Item } from 'react-contexify'
import 'react-contexify/ReactContexify.css'
import { useAppContext } from 'src/hooks'
import Editor from 'src/script/editor'
import EnhancedStereoTool from 'src/script/editor/tool/enhanced-stereo'
import { toElement } from 'src/script/ui/data/convert/structconv'
import type {
ContextMenuItemData,
ContextMenuItemProps
} from '../contextMenu.types'
import { noOperation } from './utils'

const AtomBatchOperations: React.FC = (props) => {
const { getKetcherInstance } = useAppContext()
const stereoAtomIdsRef = useRef<number[] | undefined>()

const handleBatchEdit = useCallback(async () => {
const editor = getKetcherInstance().editor as Editor
const defaultAtom = toElement({
label: 'C',
charge: 0,
isotope: 0,
explicitValence: -1,
radical: 0,
invRet: 0,
ringBondCount: 0,
substitutionCount: 0,
hCount: 0,
stereoParity: 0
})

try {
const newAtom = await editor.event.elementEdit.dispatch(defaultAtom)
const selectedAtomIds = editor.selection()?.atoms

editor.update(
fromAtomsAttrs(editor.render.ctab, selectedAtomIds, newAtom, false)
)
} catch (error) {
noOperation()
}
}, [getKetcherInstance])

const handleStereoBatchEdit = useCallback(async () => {
if (!stereoAtomIdsRef.current) {
return
}

const editor = getKetcherInstance().editor as Editor

try {
const action = await EnhancedStereoTool.changeAtomsStereoAction(
editor,
stereoAtomIdsRef.current
)

action && editor.update(action)
} catch (error) {
noOperation()
}
}, [getKetcherInstance])

const handleBatchDelete = useCallback(() => {
const editor = getKetcherInstance().editor as Editor
const action = new Action()
const selectedAtomIds = editor.selection()?.atoms

selectedAtomIds?.forEach((atomId) => {
action.mergeWith(fromOneAtomDeletion(editor.render.ctab, atomId))
})

editor.update(action)
}, [getKetcherInstance])

const isHidden = useCallback(
({ props }: PredicateParams<ContextMenuItemProps, ContextMenuItemData>) =>
!props?.selected,
[]
)

const isDisabled = useCallback(
({
props,
triggerEvent
}: PredicateParams<ContextMenuItemProps, ContextMenuItemData>) => {
if (isHidden({ props, triggerEvent })) {
return true
}

const editor = getKetcherInstance().editor as Editor
const selectedAtomIds = editor.selection()?.atoms

if (Array.isArray(selectedAtomIds) && selectedAtomIds.length !== 0) {
return false
}

return true
},
[getKetcherInstance, isHidden]
)

const isStereoDisabled = useCallback(
({
props,
triggerEvent
}: PredicateParams<ContextMenuItemProps, ContextMenuItemData>) => {
if (isDisabled({ props, triggerEvent })) {
return true
}
const editor = getKetcherInstance().editor as Editor
const selectedAtomIds = editor.selection()?.atoms

const stereoAtomIds: undefined | number[] = findStereoAtoms(
editor.struct(),
selectedAtomIds
)
stereoAtomIdsRef.current = stereoAtomIds

if (Array.isArray(stereoAtomIds) && stereoAtomIds.length !== 0) {
return false
}

return true
},
[getKetcherInstance, isDisabled]
)

return (
<>
<Item
hidden={isHidden}
disabled={isDisabled}
onClick={handleBatchEdit}
{...props}
>
Edit selected atom(s)
</Item>

<Item
hidden={isHidden}
disabled={isStereoDisabled}
onClick={handleStereoBatchEdit}
{...props}
>
Enhanced stereochemistry
</Item>

<Item
hidden={isHidden}
disabled={isDisabled}
onClick={handleBatchDelete}
{...props}
>
Delete selected atom(s)
</Item>
</>
)
}

export default AtomBatchOperations
Loading

0 comments on commit e79cf5b

Please sign in to comment.