com.actelion.research.gui.CompoundCollectionPane Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openchemlib Show documentation
Show all versions of openchemlib Show documentation
Open Source Chemistry Library
/*
* Copyright (c) 1997 - 2016
* Actelion Pharmaceuticals Ltd.
* Gewerbestrasse 16
* CH-4123 Allschwil, Switzerland
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of the the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Thomas Sander
*/
package com.actelion.research.gui;
import com.actelion.research.chem.*;
import com.actelion.research.gui.clipboard.IClipboardHandler;
import com.actelion.research.gui.dnd.MoleculeDragAdapter;
import com.actelion.research.gui.dnd.MoleculeDropAdapter;
import com.actelion.research.gui.dnd.MoleculeTransferable;
import com.actelion.research.gui.editor.SwingEditorDialog;
import com.actelion.research.gui.generic.GenericRectangle;
import com.actelion.research.gui.hidpi.HiDPIHelper;
import com.actelion.research.util.ColorHelper;
import com.actelion.research.gui.swing.SwingCursorHelper;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.event.*;
import java.awt.geom.Rectangle2D;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
public class CompoundCollectionPane extends JScrollPane
implements ActionListener,CompoundCollectionListener,MouseListener,MouseMotionListener,StructureListener {
private static final long serialVersionUID = 0x20060904;
private static final String[] MESSAGE = { "" };
private static final String ADD = "Add...";
private static final String EDIT = "Edit...";
private static final String REMOVE = "Remove";
private static final String REMOVE_ALL = "Remove All";
private static final String COPY = "Copy";
private static final String PASTE = "Paste";
private static final String OPEN = "Add From File...";
private static final String SAVE_DWAR = "Save DataWarrior-File...";
private static final String SAVE_SDF2 = "Save SD-File V2...";
private static final String SAVE_SDF3 = "Save SD-File V3...";
public static final int FILE_SUPPORT_NONE = 0;
public static final int FILE_SUPPORT_OPEN_FILES = 1;
public static final int FILE_SUPPORT_SAVE_FILES = 2;
public static final int FILE_SUPPORT_OPEN_AND_SAVE_FILES = 3;
private static final int ALLOWED_DRAG_ACTIONS = DnDConstants.ACTION_COPY_OR_MOVE;
private static final int ALLOWED_DROP_ACTIONS = DnDConstants.ACTION_COPY_OR_MOVE;
private final static int cWhiteSpace = 4;
private CompoundCollectionModel mModel;
private IClipboardHandler mClipboardHandler;
private MoleculeFilter mCompoundFilter;
private int mDisplayMode,mSelectedIndex,mHighlightedIndex,
mEditedIndex,mFileSupport,mStructureSize,mDragIndex,mDropIndex;
private Dimension mContentSize,mCellSize;
private JPanel mContentPanel;
private boolean mIsVertical,mIsEditable,mIsSelectable,mCreateFragments,
mIsEnabled,mShowValidationError,mInternalDragAndDropIsMove;
private String[] mMessage;
private ArrayList mCustomPopupItemList;
private ScrollPaneAutoScrollerWhenDragging mScroller;
/**
* This is a visual component to display and edit a compound collection maintained
* by a CompoundCollectionModel. Three variations of DefaultCompoundCollectionModel
* (.Native, .Molecule, and .IDCode) are available, which internally keep molecule
* instances as Object, StereoMolecule or String, respectively. If one of these
* default model is used, than the CompoundCollectionPane's T must match this class.
* @param model
* @param isVertical
*/
public CompoundCollectionPane(CompoundCollectionModel model, boolean isVertical) {
this(model, isVertical, 0, ALLOWED_DRAG_ACTIONS, ALLOWED_DROP_ACTIONS);
}
public CompoundCollectionPane(CompoundCollectionModel model, boolean isVertical, int displayMode,
int dragAction, int dropAction) {
mModel = model;
mModel.addCompoundCollectionListener(this);
mIsEnabled = true;
mIsVertical = isVertical;
mDisplayMode = displayMode;
mFileSupport = FILE_SUPPORT_OPEN_AND_SAVE_FILES;
mStructureSize = 0;
mSelectedIndex = -1;
mHighlightedIndex = -1;
mDragIndex = -1;
mDropIndex = -1;
init();
initializeDragAndDrop(dragAction, dropAction);
mScroller = new ScrollPaneAutoScrollerWhenDragging(this, isVertical);
}
public CompoundCollectionModel getModel() {
return mModel;
}
public void setEnabled(boolean b) {
super.setEnabled(b);
if (mIsVertical)
getVerticalScrollBar().setEnabled(b);
else
getHorizontalScrollBar().setEnabled(b);
mIsEnabled = b;
repaint();
}
/**
* Defines the behaviour for internal drag&drop. The default is false,
* which means that a dragged structure is copied to the new internal position.
* @param b
*/
public void setInternalDragAndDropIsMove(boolean b) {
mInternalDragAndDropIsMove = b;
}
/**
* Defines the width or height of individual structure cells,
* depending on whether the the CompoundCollectionPane is horizontal
* or vertical, respectively. Setting size to 0 (default) causes
* an automatic behavior with a square cell areas and width and height
* being implicitly defined by the CompoundCollectionPane component size.
*/
public void setStructureSize(int size) {
mStructureSize = size;
validateSize();
repaint();
}
/**
* Defines, whether the list and individual structures can be edited.
* @param editable
*/
public void setEditable(boolean editable) {
mIsEditable = editable;
updateMouseListening();
}
/**
* Defines, whether the popup menu contains 'Open' and/or 'Save' items.
* As default both, OPEN and SAVE options are active.
* @param fileSupport one of the FILE_SUPPORT_... options
*/
public void setFileSupport(int fileSupport) {
mFileSupport = fileSupport;
}
public void setCompoundFilter(MoleculeFilter filter) {
if (mCompoundFilter != filter) {
mCompoundFilter = filter;
if (mCompoundFilter != null)
for (int i=mModel.getSize()-1; i>=0; i--)
if (!mCompoundFilter.moleculeQualifies(mModel.getMolecule(i)))
mModel.remove(i);
}
}
public void setSelectable(boolean selectable) {
mIsSelectable = selectable;
updateMouseListening();
}
/**
* Defines whether new created structures are fragments of molecules.
* @param createFragments
*/
public void setCreateFragments(boolean createFragments) {
mCreateFragments = createFragments;
}
/**
* Defines whether a large red question mark is shown
* in case of a structure validation error.
* @param showError
*/
public void setShowValidationError(boolean showError) {
mShowValidationError = showError;
}
/**
* call this in order to get clipboard support
*/
public void setClipboardHandler(IClipboardHandler h) {
mClipboardHandler = h;
}
public IClipboardHandler getClipboardHandler() {
return mClipboardHandler;
}
public void addCustomPopupItem(JMenuItem customItem) {
if (mCustomPopupItemList == null)
mCustomPopupItemList = new ArrayList<>();
mCustomPopupItemList.add(customItem);
}
/**
* @param msg null for default message or custom message to show if this pane is empty
*/
public void setMessage(String msg) {
if (msg == null) {
mMessage = null;
}
else {
mMessage = new String[1];
mMessage[0] = msg;
}
if (mModel.getSize() == 0)
repaint();
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getActionCommand().equals(COPY) && mHighlightedIndex != -1) {
mClipboardHandler.copyMolecule(mModel.getMolecule(mHighlightedIndex));
}
else if (e.getActionCommand().equals(PASTE)) {
int index = (mHighlightedIndex == -1) ? mModel.getSize() : mHighlightedIndex;
ArrayList molList = mClipboardHandler.pasteMolecules();
if (molList != null) {
int errorCount = 0;
for (StereoMolecule mol:molList) {
mol.setFragment(mCreateFragments);
if (mCompoundFilter == null || mCompoundFilter.moleculeQualifies(mol))
mModel.addMolecule(index, mol);
else
errorCount++;
}
if (errorCount != 0)
JOptionPane.showMessageDialog(getParentFrame(), errorCount+" compound(s) could not be added, because they doesn't qualify.");
}
}
else if (e.getActionCommand().equals(ADD)) {
editStructure(-1);
}
else if (e.getActionCommand().equals(EDIT) && mHighlightedIndex != -1) {
editStructure(mHighlightedIndex);
}
else if (e.getActionCommand().equals(REMOVE) && mHighlightedIndex != -1) {
mModel.remove(mHighlightedIndex);
mHighlightedIndex = -1;
}
else if (e.getActionCommand().equals(REMOVE_ALL)) {
mModel.clear();
mHighlightedIndex = -1;
}
else if (e.getActionCommand().equals(OPEN)) {
ArrayList compounds = new FileHelper(getParentFrame()).readStructuresFromFile(true);
if (compounds != null) {
for (StereoMolecule compound:compounds)
compound.setFragment(mCreateFragments);
if (mCompoundFilter != null) {
int count = 0;
for (int i = compounds.size() - 1; i >= 0; i--) {
if (!mCompoundFilter.moleculeQualifies(compounds.get(i))) {
compounds.remove(i);
count++;
}
}
if (count != 0) {
JOptionPane.showMessageDialog(getParentFrame(),Integer.toString(count).concat(" compounds were removed, because they don't qualify."));
}
}
mModel.addMoleculeList(compounds);
}
}
else if (e.getActionCommand().equals(SAVE_DWAR)) {
String filename = new FileHelper(getParentFrame()).selectFileToSave(
"Save DataWarrior File", FileHelper.cFileTypeDataWarrior, "Untitled");
if (filename != null) {
try {
String title = mCreateFragments ? "Fragment" : "Structure";
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filename),"UTF-8"));
writer.write("");
writer.newLine();
writer.write("");
writer.newLine();
writer.write("");
writer.newLine();
writer.write(" ");
writer.newLine();
writer.write("");
writer.newLine();
writer.write("");
writer.newLine();
writer.write("");
writer.newLine();
writer.write("");
writer.newLine();
writer.write("");
writer.newLine();
writer.write("");
writer.newLine();
writer.write(" ");
writer.newLine();
writer.write(title+"\tcoords");
writer.newLine();
for (int i=0; i=i1 && mDropIndex<=i2) {
Rectangle bounds = getMoleculeBounds(mDropIndex);
g.setColor(ColorHelper.getContrastColor(Color.GRAY, background));
if (mIsVertical)
g.fillRect(bounds.x-2, bounds.y-4, bounds.width+4, 5);
else
g.fillRect(bounds.x-4, bounds.y-2, 5, bounds.height+4);
}
}
};
setHorizontalScrollBarPolicy(mIsVertical ? JScrollPane.HORIZONTAL_SCROLLBAR_NEVER : JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
setVerticalScrollBarPolicy(mIsVertical ? JScrollPane.VERTICAL_SCROLLBAR_ALWAYS : JScrollPane.VERTICAL_SCROLLBAR_NEVER);
setViewportView(mContentPanel);
}
public void collectionUpdated(int fromIndex, int toIndex) {
if (mSelectedIndex >= fromIndex && mSelectedIndex <= toIndex)
mSelectedIndex = -1;
if (mHighlightedIndex >= fromIndex && mHighlightedIndex <= toIndex)
mHighlightedIndex = -1;
repaint();
}
private Rectangle getMoleculeBounds(int molIndex) {
int x = cWhiteSpace/2;
int y = cWhiteSpace/2;
if (mIsVertical)
y += molIndex * mCellSize.height;
else
x += molIndex * mCellSize.width;
return new Rectangle(x, y, mCellSize.width-cWhiteSpace, mCellSize.height-cWhiteSpace);
}
private int getMoleculeIndex(int x, int y) {
if (mModel.getSize() == 0 || mCellSize.width == 0 || mCellSize.height == 0)
return -1;
Point p = getViewport().getViewPosition();
int index = (mIsVertical) ? (y+p.y) / mCellSize.height
: (x+p.x) / mCellSize.width;
return (index < mModel.getSize()) ? index : -1;
}
public void mouseClicked(MouseEvent e) {
if (mIsEnabled && mIsEditable && e.getClickCount() == 2 && mHighlightedIndex != -1) {
editStructure(mHighlightedIndex);
}
}
public void mouseEntered(MouseEvent e) {}
public void mouseExited(MouseEvent e) {}
public void mousePressed(MouseEvent e) {
if (mIsEnabled) {
if (e.isPopupTrigger()) {
handlePopupTrigger(e);
}
else if (mIsSelectable) {
int index = getMoleculeIndex(e.getX(), e.getY());
if (mSelectedIndex != index) {
mSelectedIndex = index;
setSelection(index);
repaint();
}
}
}
}
public void mouseReleased(MouseEvent e) {
if (mIsEnabled && e.isPopupTrigger())
handlePopupTrigger(e);
}
public void mouseDragged(MouseEvent e) {}
public void mouseMoved(MouseEvent e) {
if (mIsEnabled) {
int index = getMoleculeIndex(e.getX(), e.getY());
if (mHighlightedIndex != index) {
mHighlightedIndex = index;
setCursor(SwingCursorHelper.getCursor(index == -1 ? SwingCursorHelper.cPointerCursor : SwingCursorHelper.cHandCursor));
repaint();
}
}
mDragIndex = -1;
}
private void handlePopupTrigger(MouseEvent e) {
JPopupMenu popup = new JPopupMenu();
JMenuItem item = new JMenuItem(ADD);
item.addActionListener(this);
popup.add(item);
if (mHighlightedIndex != -1) {
item = new JMenuItem(EDIT);
item.addActionListener(this);
popup.add(item);
item = new JMenuItem(REMOVE);
item.addActionListener(this);
popup.add(item);
}
if (mModel.getSize() != 0) {
item = new JMenuItem(REMOVE_ALL);
item.addActionListener(this);
popup.add(item);
}
if (mClipboardHandler != null) {
popup.addSeparator();
if (mHighlightedIndex != -1) {
item = new JMenuItem(COPY);
item.addActionListener(this);
popup.add(item);
}
item = new JMenuItem(PASTE);
item.addActionListener(this);
popup.add(item);
}
if (mFileSupport != 0) {
popup.addSeparator();
if ((mFileSupport & FILE_SUPPORT_OPEN_FILES) != 0) {
item = new JMenuItem(OPEN);
item.addActionListener(this);
popup.add(item);
}
if ((mFileSupport & FILE_SUPPORT_SAVE_FILES) != 0 && mModel.getSize() != 0) {
item = new JMenuItem(SAVE_DWAR);
item.addActionListener(this);
popup.add(item);
item = new JMenuItem(SAVE_SDF2);
item.addActionListener(this);
popup.add(item);
item = new JMenuItem(SAVE_SDF3);
item.addActionListener(this);
popup.add(item);
}
}
if (mCustomPopupItemList != null) {
popup.addSeparator();
for (JMenuItem customItem:mCustomPopupItemList)
popup.add(customItem);
}
popup.show(this, e.getX(), e.getY());
}
public void structureChanged(StereoMolecule mol) {
String reason = (mCompoundFilter == null) ? null
: (mCompoundFilter instanceof SubstructureFilter) ? "match the substructure" : "qualify";
if (mEditedIndex == -1) { // new structure
if (mol.getAllAtoms() != 0) {
if (mCompoundFilter == null || mCompoundFilter.moleculeQualifies(mol))
mModel.addMolecule(mModel.getSize(), mol);
else
JOptionPane.showMessageDialog(getParentFrame(),"The compound could not be added, because it doesn't "+reason+".");
}
}
else {
if (mol.getAllAtoms() == 0)
mModel.remove(mEditedIndex);
else {
if (mCompoundFilter == null || mCompoundFilter.moleculeQualifies(mol))
mModel.setMolecule(mEditedIndex, mol);
else
JOptionPane.showMessageDialog(getParentFrame(),"The compound could not be changed, because the changed structure doesn't "+reason+".");
}
}
}
/**
* May be overridden to act on selection changes
* @param molIndex
*/
public void setSelection(int molIndex) {}
private void validateSize() {
Rectangle viewportBounds = getViewportBorderBounds();
int width = mIsVertical ? viewportBounds.width : mStructureSize == 0 ? viewportBounds.height : mStructureSize;
int height = !mIsVertical ? viewportBounds.height : mStructureSize == 0 ? viewportBounds.width : mStructureSize;
mCellSize = new Dimension(width, height);
if (mIsVertical) {
height *= mModel.getSize();
if (height < viewportBounds.height)
height = viewportBounds.height;
}
else {
width *= mModel.getSize();
if (width < viewportBounds.width)
width = viewportBounds.width;
}
if (mContentSize.width != width
|| mContentSize.height != height) {
mContentSize.width = width;
mContentSize.height = height;
mContentPanel.setPreferredSize(mContentSize);
mContentPanel.revalidate();
}
}
private void initializeDragAndDrop(int dragAction, int dropAction) {
if (dragAction != DnDConstants.ACTION_NONE) {
new MoleculeDragAdapter(this) {
public Transferable getTransferable(Point p) {
if (mHighlightedIndex == -1)
return null;
setCursor(SwingCursorHelper.getCursor(SwingCursorHelper.cFistCursor));
mDragIndex = mHighlightedIndex;
return new MoleculeTransferable(mModel.getMolecule(mHighlightedIndex));
}
};
}
if (dropAction != DnDConstants.ACTION_NONE) {
MoleculeDropAdapter d = new MoleculeDropAdapter() {
@Override
public void onDropMolecule(StereoMolecule mol, Point pt) {
if (mIsEnabled && mIsEditable && mol != null && mol.getAllAtoms() != 0 && mDropIndex != -1) {
for (int atom=0; atom mDragIndex)
mDropIndex--;
}
mModel.addMolecule(mDropIndex, mol);
}
else {
String reason = (mCompoundFilter instanceof SubstructureFilter) ? "match the substructure" : "qualify";
JOptionPane.showMessageDialog(getParentFrame(),"The compound could not be added, because it doesn't "+reason+".");
}
}
updateDropPosition(-1);
}
@Override
public void dragEnter(DropTargetDragEvent e) {
boolean drop = mIsEnabled && mIsEditable && isDropOK(e) ;
if (!drop) {
e.rejectDrag();
}
else {
updateDropPosition(getDropIndex(e));
}
}
@Override
public void dragOver(DropTargetDragEvent e) {
mScroller.autoScroll();
updateDropPosition(getDropIndex(e));
}
@Override
public void dragExit(DropTargetEvent e) {
updateDropPosition(-1);
}
private int getDropIndex(DropTargetDragEvent e) {
int x = e.getLocation().x + (mIsVertical ? 0 : mCellSize.width / 2);
int y = e.getLocation().y + (mIsVertical ? mCellSize.height / 2 : 0);
int dropIndex = getMoleculeIndex(x, y);
if (dropIndex == -1)
dropIndex = mModel.getSize();
// if we move internally onto the same position, don't indicate it
if (mInternalDragAndDropIsMove
&& (dropIndex == mDragIndex || dropIndex == mDragIndex+1))
dropIndex = -1;
return dropIndex;
}
};
new DropTarget(this, dropAction, d, true, new OurFlavorMap());
}
}
private void updateDropPosition(int dropIndex) {
if (mIsEnabled && mIsEditable && mDropIndex != dropIndex) {
mDropIndex = dropIndex;
repaint();
}
}
private Component getParentFrame() {
Component c = this;
while (c != null && !(c instanceof Frame) && !(c instanceof Dialog))
c = c.getParent();
return c;
}
// This class is needed for inter-jvm drag&drop. Although not neccessary for standard environments, it prevents
// nasty "no native data was transfered" errors. It still might create ClassNotFoundException in the first place by
// the SystemFlavorMap, but as I found it does not hurt, since the context classloader will be installed after
// the first call. I know, that this depends heavely on a specific behaviour of the systemflavormap, but for now
// there's nothing I can do about it.
static class OurFlavorMap implements java.awt.datatransfer.FlavorMap {
public java.util.Map getNativesForFlavors(DataFlavor[] dfs) {
java.awt.datatransfer.FlavorMap m = java.awt.datatransfer.SystemFlavorMap.getDefaultFlavorMap();
return m.getNativesForFlavors(dfs);
}
public java.util.Map getFlavorsForNatives(String[] natives) {
java.awt.datatransfer.FlavorMap m = java.awt.datatransfer.SystemFlavorMap.getDefaultFlavorMap();
return m.getFlavorsForNatives(natives);
}
}
}