All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.actelion.research.gui.CompoundCollectionPane Maven / Gradle / Ivy

There is a newer version: 2024.11.2
Show newest version
/*
 * 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);
			}
		}
	}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy