/*
* This file is part of the OSMembrane project.
* More informations under www.osmembrane.de
*
* The project is licensed under the GNU GENERAL PUBLIC LICENSE 3.0.
* for more details about the license see http://www.osmembrane.de/license/
*
* Source: $HeadURL$ ($Revision$)
* Last changed: $Date$
*/
package de.osmembrane.view.panels;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.EtchedBorder;
import de.osmembrane.Application;
import de.osmembrane.model.ModelProxy;
import de.osmembrane.model.pipeline.AbstractFunction;
import de.osmembrane.model.pipeline.FunctionGroup;
import de.osmembrane.tools.I18N;
import de.osmembrane.view.components.JTextFieldWithButton;
/**
* The function library panel that lists all the {@link FunctionGroup}s as
* {@link LibraryPanelGroup}s in a register style and gives you the ability to
* drag & drop them onto the {@link PipelinePanel}.
*
* Would have been fancy to be generic, but was not possible due to time
* constraints. Note smelly calls to getMainFrameByPass().getPipeline().
*
* @see "Spezifikation.pdf, chapter 2.1.4 (German)"
*
* @author tobias_kuhn
*
*/
public class LibraryPanel extends JPanel {
private static final long serialVersionUID = -865621422748326256L;
/**
* The index of the {@link LibraryPanelGroup} that is currently completely
* expanded, or -1 if none is completely expanded.
*/
private int expanded;
/**
* The index of the {@link LibraryPanelGroup} that is currently expanding,
* or -1 if none
*/
private int expanding;
/**
* The index of the {@link LibraryPanelGroup} that is currently contracting,
* or -1 if none
*/
private int contracting;
/**
* The start of the expandation in System relative time, in milliseconds
*/
private long expandingStart;
/**
* The desired duration of an expandation, in milliseconds
*/
private final static double expandingDuration = 333.0;
/**
* The {@link Thread} that performs the expanding/contracting animation
*/
private Thread expandingThread;
/**
* The {@link LibraryPanelGroup}s listed in this library panel
*/
private List<LibraryPanelGroup> groups;
/**
* Filter text field to filter the functions
*/
private JTextFieldWithButton editFilter;
/**
* {@link LibraryPanelGroup} for the filtered functions by editFilter.
*/
private LibraryPanelGroup filterGroup;
/**
* Initializes a new {@link LibraryPanel}
*
* @param pipeline
* pipeline to use for drag & drop
*/
public LibraryPanel(final PipelinePanel pipeline) {
// internal logic
this.expanded = -1;
this.expanding = -1;
this.contracting = -1;
this.expandingThread = null;
this.groups = new ArrayList<LibraryPanelGroup>();
final String noFiltering = "("
+ I18N.getInstance().getString("View.Library.NoFiltering")
+ ")";
this.editFilter = new JTextFieldWithButton();
editFilter.setLocation(3, 3);
editFilter.setValue(noFiltering);
editFilter.setCaption("x");
editFilter.addButtonActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
editFilter.setValue(noFiltering);
expanded = -1;
expanding = -1;
contracting = -1;
for (LibraryPanelGroup lpg : groups) {
lpg.setVisible(true);
}
filterGroup.setVisible(false);
rearrange(true);
}
});
editFilter.setValueHorizontalAlignment(SwingConstants.CENTER);
editFilter.addFieldFocusListener(new FocusListener() {
@Override
public void focusLost(FocusEvent e) {
if (editFilter.getValue().trim().isEmpty()) {
editFilter.setValue(noFiltering);
}
}
@Override
public void focusGained(FocusEvent e) {
if (editFilter.getValue().equals(noFiltering)) {
editFilter.setValue("");
}
}
});
editFilter.addFieldKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
if (!editFilter.getValue().trim().isEmpty()
&& !editFilter.getValue().equals(noFiltering)) {
// show filtered state
AbstractFunction[] result = ModelProxy.getInstance()
.getFunctions()
.getFilteredFunctions(editFilter.getValue());
filterGroup.populate(result, pipeline);
for (LibraryPanelGroup lpg : groups) {
lpg.setVisible(false);
}
filterGroup.setVisible(true);
filterGroup.setContentHeight(filterGroup
.getFullContentHeight());
expanded = filterGroup.getId();
expanding = -1;
contracting = -1;
} else {
// show normal state
for (LibraryPanelGroup lpg : groups) {
lpg.setVisible(true);
}
filterGroup.setVisible(false);
expanded = -1;
expanding = -1;
contracting = -1;
}
rearrange(true);
}
@Override
public void keyReleased(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
});
add(editFilter);
// display
setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED));
// best decision ever <- do not touch
setLayout(null);
filterGroup = new LibraryPanelGroup(this, pipeline, I18N.getInstance()
.getString("View.Library.Filter"), Color.WHITE);
filterGroup.setVisible(false);
addGroup(filterGroup);
}
/**
* Adds a new {@link LibraryPanelGroup} to select from
*
* @param lpg
*/
public void addGroup(LibraryPanelGroup lpg) {
groups.add(lpg);
// set the corresponding id, so the group knows it
lpg.setId(groups.size() - 1);
// make it contracted
lpg.setContentHeight(0);
add(lpg);
// check if we have to adjust our preferred width
if (lpg.getPreferredSize().width > getPreferredSize().width) {
setPreferredSize(new Dimension(lpg.getPreferredSize().width,
getHeight()));
}
rearrange(true);
}
/**
* Notify the panel that a {@link LibraryPanelGroup} has been clicked to
* expand/collapse
*
* @param group
* the {@link LibraryPanelGroup} to expand/collapse
*/
public void groupClicked(int group) {
// wait for the old thread to die before modifying variables
// possible error in year 292 473 179 (expandingStart is really 0L).
if ((expandingThread != null) && (expandingThread.isAlive())) {
expandingStart = 0L;
try {
expandingThread.join();
} catch (InterruptedException e) {
Application.handleException(e);
}
}
LibraryPanelGroup lpg;
// finish old animation, if one was still running
for (int i = 0; i < groups.size(); i++) {
lpg = groups.get(i);
if ((i != expanding) && (i != expanded)) {
lpg.setContentHeight(0);
}
}
if (expanding != -1) {
lpg = groups.get(expanding);
lpg.setContentHeight(lpg.getFullContentHeight());
expanded = expanding;
}
expanding = -1;
contracting = -1;
rearrange(true);
// check whether there is something to expand
if (expanded == group) {
expanding = -1;
contracting = expanded;
expanded = -1;
} else {
expanding = group;
contracting = expanded;
expanded = -1;
}
expandingStart = System.currentTimeMillis();
expandingThread = new Thread(new Runnable() {
@Override
public void run() {
LibraryPanelGroup lpg;
double timeFactor = (System.currentTimeMillis() - expandingStart)
/ expandingDuration;
while (timeFactor < 1) {
timeFactor = (System.currentTimeMillis() - expandingStart)
/ expandingDuration;
// actual animation
if (expanding > -1) {
lpg = groups.get(expanding);
lpg.setContentHeight(getExpandingHeight(timeFactor,
lpg.getFullContentHeight(), true));
}
if (contracting > -1) {
lpg = groups.get(contracting);
lpg.setContentHeight(getExpandingHeight(timeFactor,
lpg.getFullContentHeight(), false));
}
// prevents weird flickering... don't know why
rearrange(false);
// might be inaccurate by several factors, but will still
// guarantee a fluent animation
try {
Thread.sleep(20L);
} catch (InterruptedException e) {
// don't really care, we just redraw
}
}
// animation done
if (expanding > -1) {
lpg = groups.get(expanding);
lpg.setContentHeight(lpg.getFullContentHeight());
}
if (contracting > -1) {
lpg = groups.get(contracting);
lpg.setContentHeight(0);
}
expanded = expanding;
expanding = -1;
contracting = -1;
rearrange(true);
}
});
expandingThread.start();
}
/**
* Makes Swing thread-safe (or so we hope)
*/
@Override
public void repaint() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
LibraryPanel.super.repaint();
}
});
}
@Override
public void setSize(Dimension d) {
super.setSize(d);
rearrange(false);
}
/**
* @see {@link JPanel#setSize}
*/
private void setSizeNoArrange(Dimension d) {
super.setSize(d);
}
/**
* Rearranges the {@link LibraryPanelGroup}s to actually look like a
* {@link LibraryPanel}. Unlike the "LayoutManager" (incompetent, is a
* Manager)
*/
private void rearrange(boolean setSize) {
// bring the filter to its width
editFilter.setSize(this.getWidth() - 6,
editFilter.getPreferredSize().height);
int y = 3 + editFilter.getHeight();
for (LibraryPanelGroup lpg : groups) {
if (lpg.isVisible()) {
// determine top
lpg.setLocation(3, y);
// give it the width of the library & the height it needs
lpg.setSize(this.getWidth() - 6, lpg.getHeight());
// notify the arrangement
lpg.rearranged();
y += lpg.getHeight() + 6;
}
}
// update for the scroll bar
if (setSize) {
setPreferredSize(new Dimension(this.getPreferredSize().width, y));
setSizeNoArrange(getPreferredSize());
}
}
/**
* Calculates the correct height for a component during an
* expanding/contracting animation
*
* @param timeFactor
* currentTimeMillis() - expandingStart / expandingDuration
* @param originalHeight
* original height of the expanding/contracting component
* @param expanding
* true when component is expanding, false when it is contracting
* @return the current height of the component during animation
*/
private int getExpandingHeight(double timeFactor, int originalHeight,
boolean expanding) {
// function: timeFactor in [0, 1] ---> arg in [0, 1]
double arg = 0.5 - 0.5 * Math.cos(timeFactor * Math.PI);
if (expanding) {
return (int) (arg * originalHeight);
} else {
return (int) ((1.0 - arg) * originalHeight);
}
}
}