package interpolation; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Insets; import java.awt.Toolkit; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import gui.TransferableUtils; import kussmaulUtils.StringUtil; public class StringInterpolator extends GraphInterpolator { private static final long serialVersionUID = -1873283247289277395L; private JScrollPane sp; private JTextField tField; private JCheckBox interpolateBox; private ArrayList<Keyframe<String>> keyframes; private Keyframe<String> selectedFrame; private JPanel colorKeyframesPanel; private JLabel infoLabel; public StringInterpolator(String s0, String s1, GetSet gs, ChangeListener... listeners) { super(gs, listeners); keyframes = new ArrayList<Keyframe<String>>(); keyframes.add(new Keyframe<String>(0f, s0)); keyframes.add(new Keyframe<String>(1f, s1)); initializeComponents(); refresh(); gbc.insets = new Insets(5, 5, 5, 5); add(colorKeyframesPanel, gbc); gbc.gridy++; add(infoLabel, gbc); gbc.gridy++; add(interpolateBox, gbc); pack(); } private void initializeComponents() { tField = new JTextField(keyframes.get(0).getValue()) { private static final long serialVersionUID = -4597572910181970566L; @Override public Dimension getPreferredSize() { return new Dimension(50, super.getPreferredSize().height); } }; tField.setBorder(BorderFactory.createEmptyBorder()); tField.getDocument().addDocumentListener(new DocumentListener() { public void removeUpdate(DocumentEvent e) { refresh(); } public void insertUpdate(DocumentEvent e) { refresh(); } public void changedUpdate(DocumentEvent e) { refresh(); } }); sp = new JScrollPane(tField); sp.setPreferredSize(new Dimension(50, sp.getPreferredSize().height)); interpolateBox = new JCheckBox("Interpolate between keyframes"); colorKeyframesPanel = new JPanel(); infoLabel = new JLabel("Time: -- Value: --"); createMenuBar(); } @SuppressWarnings("unchecked") private void createMenuBar() { JMenuItem copy = new JMenuItem("Copy"); copy.addActionListener(ae -> TransferableUtils.copyObject(Keyframe.deepCopy(keyframes))); copy.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.META_MASK)); JMenuItem paste = new JMenuItem("Paste"); paste.addActionListener(ae -> { Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this); try { ArrayList<Keyframe<Serializable>> data = (ArrayList<Keyframe<Serializable>>) t.getTransferData(TransferableUtils.objectDataFlavor); if(data.isEmpty()) return; //Yes this is ugly. There is really no better way of doing it though. Forgive me if(data.get(0).getValue() instanceof String) keyframes = (ArrayList<Keyframe<String>>) (Object) data; //keyframes = (ArrayList<Keyframe<String>>) t.getTransferData(TransferableUtils.objectDataFlavor); refresh(); } catch (UnsupportedFlavorException | IOException e1) { System.err.println("Invalid data copied"); } }); paste.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.META_MASK)); JMenuBar bar = new JMenuBar(); JMenu edit = new JMenu("Edit"); edit.add(copy); edit.add(paste); bar.add(edit); setJMenuBar(bar); } @Override protected void graphMoved(float x, float y, boolean rightClick) { refreshInfo(x, y); } @Override protected void graphDragged(float x, float y, boolean rightClick) { selectedFrame.setTime(x); refresh(); refreshInfo(x, y); } @Override protected void graphPressed(float x, float y, boolean rightClick) { if(rightClick) { selectedFrame = new Keyframe<String>(x, (String) getAnimationValue(x)); addKeyframe(selectedFrame); } else selectedFrame = getNearestKeyframe(x); selectedFrame.setTime(x); refresh(); } public void refreshInfo(float x, float y) { infoLabel.setText(String.format(java.util.Locale.US, "Time: %.2f Value: %1s", Math.min(1, Math.max(0,x)), (String) getAnimationValue(x))); } @Override public boolean isKeyframable() { return true; } @Override public Object getAnimationValue(float time) { if(time <= keyframes.get(0).getTime()) return keyframes.get(0).getValue(); if(time >= keyframes.get(keyframes.size()-1).getTime() || keyframes.size() == 1) return keyframes.get(keyframes.size()-1).getValue(); for(int i = 0; i < keyframes.size()-1; i++) { if(keyframes.get(i).getTime() <= time && keyframes.get(i + 1).getTime() > time) { float distTo0 = time - keyframes.get(i).getTime(); float distTo1 = keyframes.get(i + 1).getTime() - time; float sum = distTo0 + distTo1; String s0 = keyframes.get(i).getValue(), s1 = keyframes.get(i+1).getValue(); if(interpolateBox.isSelected()) return StringUtil.merge(s0, s1, distTo0/sum); else return s0; } } return ""; } @Override public Object getStaticValue() { return tField.getText(); } @Override public String getInstructions() { return "Click and drag to move a keyframe, right click to create a new keyframe."; } @Override public JComponent getManualController() { return sp; } private void refresh() { Collections.sort(keyframes); repaint(); refreshStringKeyframePanelPanel(false, false); fireChangeEvent(); } private void softRefresh() { repaint(); fireChangeEvent(); } private void refreshStringKeyframePanelPanel(boolean add, boolean sub) { colorKeyframesPanel.removeAll(); colorKeyframesPanel.setLayout(new GridLayout(keyframes.size(), 1)); for(int i = 0; i < keyframes.size(); i++) { StringKeyframePanel p = new StringKeyframePanel(keyframes.get(i)); p.setCloseable(keyframes.size() != 1); colorKeyframesPanel.add(p); } if(!(add||sub)) colorKeyframesPanel.revalidate(); else SwingUtilities.invokeLater(() -> { setSize(getWidth(), getHeight() + (add ? 1 : -1) * colorKeyframesPanel.getComponent(0).getPreferredSize().height); }); } private void addKeyframe(Keyframe<String> key) { keyframes.add(key); refreshStringKeyframePanelPanel(true, false); refresh(); } private void removeKeyframe(Keyframe<String> key) { keyframes.remove(key); refreshStringKeyframePanelPanel(false, true); refresh(); } private Keyframe<String> getNearestKeyframe(float time) { if (time <= keyframes.get(0).getTime()) return keyframes.get(0); if (time >= keyframes.get(keyframes.size()-1).getTime()) return keyframes.get(keyframes.size()-1); int bestIdx = 0; for(int i = 1; i < keyframes.size(); i++) { if(Math.abs(keyframes.get(i).getTime() - time) > Math.abs(keyframes.get(bestIdx).getTime() - time)) break; else bestIdx = i; } return keyframes.get(bestIdx); } class StringKeyframePanel extends JPanel { private static final long serialVersionUID = -329577250085593950L; private JTextField changeField; private JButton deleteButton; public StringKeyframePanel(Keyframe<String> keyframe) { super(new BorderLayout()); changeField = new JTextField(keyframe.getValue()); deleteButton = new JButton("×"); add(changeField, BorderLayout.CENTER); add(deleteButton, BorderLayout.EAST); deleteButton.addActionListener(ae -> { if(keyframes.size() > 1) { removeKeyframe(keyframe); } }); changeField.getDocument().addDocumentListener(new DocumentListener() { public void removeUpdate(DocumentEvent e) { keyframe.setValue(changeField.getText()); softRefresh(); } public void insertUpdate(DocumentEvent e) { keyframe.setValue(changeField.getText()); softRefresh(); } public void changedUpdate(DocumentEvent e) { keyframe.setValue(changeField.getText()); softRefresh(); } }); } public void setCloseable(boolean closeable) { deleteButton.setEnabled(closeable); } } @Override public Dimension getGraphSize() { return new Dimension(500, 100); } @Override public void paintGraph(Graphics2D g, int width, int height) { g.setStroke(new BasicStroke(3)); //Draw red bar at each keyframe g.setColor(Color.BLACK);//new String(255, 0, 0, 32)); for(int i = 0; i < keyframes.size(); i++) g.drawLine((int) (keyframes.get(i).getTime() * width), 0, (int) (keyframes.get(i).getTime() * width), height); g.setStroke(new BasicStroke(1)); //Draw red bar at each keyframe g.setColor(Color.WHITE);//new String(255, 0, 0, 32)); for(int i = 0; i < keyframes.size(); i++) g.drawLine((int) (keyframes.get(i).getTime() * width), 0, (int) (keyframes.get(i).getTime() * width), height); } @Override public void paintButton(Graphics2D g, int width, int height) { // TODO Auto-generated method stub } }