/* * BlendingAction.java * Eisenkraut * * Copyright (c) 2004-2016 Hanns Holger Rutz. All rights reserved. * * This software is published under the GNU General Public License v3+ * * * For further information, please contact Hanns Holger Rutz at * contact@sciss.de */ package de.sciss.eisenkraut.gui; import de.sciss.app.AbstractApplication; import de.sciss.app.AbstractWindow; import de.sciss.app.Application; import de.sciss.app.DynamicAncestorAdapter; import de.sciss.app.DynamicListening; import de.sciss.common.AppWindow; import de.sciss.eisenkraut.io.BlendContext; import de.sciss.eisenkraut.timeline.Timeline; import de.sciss.gui.CoverGrowBox; import de.sciss.gui.DefaultUnitViewFactory; import de.sciss.gui.GUIUtil; import de.sciss.gui.HelpButton; import de.sciss.gui.ParamField; import de.sciss.gui.PrefParamField; import de.sciss.gui.SpringPanel; import de.sciss.util.DefaultUnitTranslator; import de.sciss.util.Param; import de.sciss.util.ParamSpace; import javax.swing.*; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.geom.CubicCurve2D; import java.awt.geom.Point2D; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; //import java.util.ArrayList; //import java.util.List; //import javax.swing.event.ListDataEvent; //import javax.swing.event.ListDataListener; /** * A class implementing the <code>Action</code> interface * which deals with the blending setting. Each instance * generates a toggle button suitable for attaching to a tool bar; * this button reflects the blending preferences settings and * when alt+pressed will prompt the user to alter the blending settings. * * TODO: gui panels should be destroyed / disposed * because otherwise different BlendingAction instances * for different docs all create their own panels * * TODO: should not be a subclass of AbstractAction * * TODO: close action should be called whenever popup disappears, * and duplicates should be filtered out! */ @SuppressWarnings("serial") public class BlendingAction extends AbstractAction { public static final int MAX_RECENTNUM = 5; public static final String DEFAULT_NODE = "blending"; private static final String KEY_ACTIVE = "active"; public static final String KEY_DURATION = "duration"; private static final String NODE_RECENT = "recent"; private static final Stroke strkActive = new BasicStroke(1.5f); protected static final Param DEFAULT_DUR = new Param(100.0, ParamSpace.TIME | ParamSpace.MILLI | ParamSpace.SECS); private static PrefComboBoxModel pcbm = null; protected final AbstractButton b; protected final Preferences prefs; protected PrefParamField ggBlendTime; protected CurvePanel ggCurvePanel; private SpringPanel ggSettingsPane; private JComponent bottomPanel; private static final DefaultUnitTranslator ut = new DefaultUnitTranslator(); protected static final DefaultUnitViewFactory uvf = new DefaultUnitViewFactory(); private final CurvePanel.Icon curveIcon; private JPopupMenu popup = null; private AppWindow palette = null; private boolean active; private final Timeline timeline; protected final Settings current; private final Color colrFgSel, colrFgNorm; /** * Creates a new instance of an action * that tracks blending changes * * @param prefs node of preferences tree (usually DEFAULT_NODE) */ public BlendingAction(Timeline timeline, Preferences prefs) { super(); this.prefs = prefs != null ? prefs : AbstractApplication.getApplication().getUserPrefs().node( DEFAULT_NODE ); this.timeline = timeline; final boolean isDark = GraphicsUtil.isDarkSkin(); colrFgSel = isDark ? new Color(95, 142, 255) /* new Color(136, 136, 255) */ /* new Color(48, 77, 130) */ : Color.blue; // new Color(48, 77, 130); // b = new MultiStateButton(); b = new JButton("xxxxxxxx"); colrFgNorm = b.getForeground(); // b.setBorderPainted(false); b.putClientProperty("styleId", "icon-hover"); b.setBorder(null); b.setPreferredSize(b.getPreferredSize()); b.setText(null); // b.setNumColumns(8); // b.setAutoStep(false); b.addActionListener(this); // final Color colrFg = isDark ? new Color(200, 200, 200) : Color.black; // b.addItem("", colrFg, new Color(0, 0, 0, 0)); // b.addItem("", colrFg, isDark ? new Color(0x7F, 0x7F, 0xFF, 0x7F) /* new Color(48, 77, 130) */ : new Color(0xFF, 0xFA, 0x9D)); curveIcon = new CurvePanel.Icon(createBasicCurves()); b.setIcon(curveIcon); // b.setItemIcon(0, curveIcon); // b.setItemIcon(1, curveIcon); final PopupTriggerMonitor popMon = new PopupTriggerMonitor(b); popMon.addListener(new PopupTriggerMonitor.Listener() { public void componentClicked(PopupTriggerMonitor m) { /* empty */ } public void popupTriggered(PopupTriggerMonitor m) { b.getModel().setArmed(false); showPopup(m.getComponent(), 0, m.getComponent().getHeight()); } }); active = this.prefs.getBoolean(KEY_ACTIVE, false); current = Settings.fromPrefs(this.prefs); updateButton(); } public Preferences getPreferences() { return prefs; } private Preferences getRecentPreferences() { return prefs.node( NODE_RECENT ); } private void createBlendPan(boolean popped) { destroyBlendPan(); createGadgets( 0 ); // final Font fnt = AbstractApplication.getApplication().getGraphicsHandler() // .getFont(GraphicsHandler.FONT_SMALL); // GUIUtil.setDeepFont( ggSettingsPane, fnt ); // GUIUtil.setDeepFont( bottomPanel, fnt ); ggBlendTime.setCycling(popped); // cannot open popup menu in another popup menu! if (palette != null) { palette.getContentPane().add(ggSettingsPane, BorderLayout.CENTER); palette.getContentPane().add(bottomPanel, BorderLayout.SOUTH); palette.revalidate(); } else { popup.add(ggSettingsPane, BorderLayout.CENTER); popup.add(bottomPanel, BorderLayout.SOUTH); popup.revalidate(); } } private void destroyBlendPan() { if (ggSettingsPane != null) { ggSettingsPane.getParent().remove(ggSettingsPane); ggSettingsPane = null; } if (bottomPanel != null) { bottomPanel.getParent().remove(bottomPanel); bottomPanel = null; } } private void createGadgets(int flags) { bottomPanel = createBottomPanel(flags); ggSettingsPane = new SpringPanel( 4, 2, 4, 2 ); ut.setLengthAndRate( 0, timeline.getRate() ); ggBlendTime = new PrefParamField( ut ); ggBlendTime.addSpace( ParamSpace.spcTimeMillis ); ggBlendTime.addSpace( ParamSpace.spcTimeSmps ); ggBlendTime.setPreferences( prefs, KEY_DURATION ); ggBlendTime.setReadPrefs( false ); if (current.duration != null) { ggBlendTime.setValueAndSpace(current.duration); } ggBlendTime.addListener( new ParamField.Listener() { public void paramSpaceChanged( ParamField.Event e ) { paramValueChanged( e ); } public void paramValueChanged( ParamField.Event e ) { if( !e.isAdjusting() ) { current.duration = ggBlendTime.getValue(); updateButtonText(); } } }); ggCurvePanel = new CurvePanel( createBasicCurves(), this.prefs ); ggCurvePanel.setControlPoints( current.ctrlPt[ 0 ], current.ctrlPt[ 1 ]); ggCurvePanel.setPreferredSize( new Dimension( 162, 162 )); ggCurvePanel.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { final Point2D[] pt = ggCurvePanel.getControlPoints(); current.ctrlPt[ 0 ].setLocation( pt[ 0 ]); current.ctrlPt[ 1 ].setLocation( pt[ 1 ]); updateButtonIcon(); } }); ggSettingsPane.gridAdd( ggBlendTime, 0, 0 ); ggSettingsPane.gridAdd( ggCurvePanel, 0, 1 ); ggSettingsPane.makeCompactGrid(); } public void showPopup(Component invoker, int x, int y) { createPopup(); popup.show(invoker, x, y); // XXX this is necessary unfortunately // coz the DynamicAncestorAdapter doesn't seem // to work with the popup menu ... ggBlendTime.startListening(); } private void createPopup() { if (popup != null) return; if (palette != null) destroyPalette(); popup = new JPopupMenu(); createBlendPan(true); popup.addPopupMenuListener(new PopupMenuListener() { public void popupMenuCanceled(PopupMenuEvent e) { stopAndDispose(); } public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { /* empty */ } public void popupMenuWillBecomeVisible(PopupMenuEvent e) { /* empty */ } }); } protected void stopAndDispose() { // XXX this is necessary unfortunately // coz the DynamicAncestorAdapter doesn't seem // to work with the popup menu ... ggBlendTime.stopListening(); dispose(); } public void showPalette() { createPalette(); palette.setVisible(true); palette.toFront(); } private void destroyPalette() { if (palette == null) return; palette.dispose(); palette = null; } private void destroyPopup() { if (popup == null) return; popup.setVisible(false); popup = null; } protected static CubicCurve2D[] createBasicCurves() { return new CubicCurve2D[]{ new CubicCurve2D.Double(0.0, 1.0, 0.5, 0.0, 0.5, 0.0, 1.0, 0.0), new CubicCurve2D.Double(0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 1.0, 1.0) }; } /** * Returns the toggle button * which is connected to this action. * * @return a toggle button which is suitable for tool bar display */ public AbstractButton getButton() { return b; } public JComboBox mkComboBox() { final ComboBoxModel model = getComboBoxModel(); final ComboBoxModel emptyCBM = new DefaultComboBoxModel(); final AbstractButton button = b; final JComboBox ggBlend = new JComboBox(); // ( pcbm ); final ListCellRenderer blendRenderer = mkComboBoxRenderer(ggBlend.getRenderer()); ggBlend.setEditable(true); ggBlend.setEditor(new ComboBoxEditor() { public Component getEditorComponent() { return button; } public void setItem(Object o) { if (o != null) { current.setFrom((Settings) o); current.toPrefs(prefs); updateButton(); } } public Object getItem() { return current; } public void selectAll() { /* ignore */ } public void addActionListener(ActionListener l) { /* ignore */ } public void removeActionListener(ActionListener l) { /* ignore */ } }); ggBlend.setRenderer( blendRenderer ); button.setFocusable( false ); ggBlend.setFocusable( false ); GUIUtil.constrainSize( ggBlend, 120, 26 ); // 110 XXX (140 for MetalLAF) ggBlend.setOpaque( false ); // this is _crucial_ because since pcbm is global // and the combo-box registers with it, we have a // memory leak otherwise!! new DynamicAncestorAdapter(new DynamicListening() { public void startListening() { ggBlend.setModel(model); } public void stopListening() { ggBlend.setModel(emptyCBM); } }).addTo(ggBlend); return ggBlend; } private void updateButtonState() { // System.out.println("updateButtonState() " + active); // b.setSelected(active); // b.getModel().setArmed(active); // b.getModel().setPressed(active); b.setForeground(active ? colrFgSel : colrFgNorm); // b.setSelectedIndex(active ? 1 : 0); } protected void updateButton() { updateButtonState(); updateButtonText(); updateButtonIcon(); } protected void updateButtonText() { final Param p = current.duration; final Object view; final String text; if (p != null) { view = uvf.createView(p.unit); if (view instanceof Icon) { // XXX hmmm. should use composite icon } else { text = String.valueOf((int) p.val) + " " + view.toString(); b.setText(text); // b.setItemText(0, text); // b.setItemText(1, text); } if (ggBlendTime != null) ggBlendTime.setValueAndSpace(p); } } protected void updateButtonIcon() { curveIcon.update(current.ctrlPt[0], current.ctrlPt[1]); curveIcon.setStroke(active ? strkActive : null); if (ggCurvePanel != null) ggCurvePanel.setControlPoints(current.ctrlPt[0], current.ctrlPt[1]); b.repaint(); } private JComponent createBottomPanel(int flags) { final JPanel panel; final JButton ggClose; panel = new JPanel( new FlowLayout( FlowLayout.TRAILING, 4, 2 )); ggClose = new JButton( new CloseAction( getResourceString( "buttonClose" ))); GUIUtil.createKeyAction( ggClose, KeyStroke.getKeyStroke( KeyEvent.VK_ESCAPE, 0 )); ggClose.setFocusable( false ); panel.add(ggClose); panel.add(new HelpButton("Blending")); panel.add(CoverGrowBox.create()); return panel; } private String getResourceString(String key) { return AbstractApplication.getApplication().getResourceString( key ); } public void actionPerformed(ActionEvent e) { if ((e.getModifiers() & ActionEvent.ALT_MASK) != 0) { showPalette(); } else { active = !active; // b.isSelected(); // b.getSelectedIndex() == 0; prefs.putBoolean(KEY_ACTIVE, active); updateButtonState(); updateButtonIcon(); } } private void createPalette() { if (palette != null) return; if (popup != null) destroyPopup(); final Application app = AbstractApplication.getApplication(); palette = new AppWindow( AbstractWindow.PALETTE ); palette.setTitle( app.getResourceString( "inputDlgSetBlendSpan" )); createBlendPan( false ); palette.getContentPane().add( CoverGrowBox.create(), BorderLayout.SOUTH ); palette.setDefaultCloseOperation( WindowConstants.DO_NOTHING_ON_CLOSE ); palette.addListener( new AbstractWindow.Adapter() { public void windowClosing( AbstractWindow.Event e ) { dispose(); } }); palette.init(); } public void dispose() { destroyPalette(); destroyPopup(); destroyBlendPan(); } private ComboBoxModel getComboBoxModel() { if (pcbm == null) { pcbm = new PrefComboBoxModel() { public Object dataFromNode(Preferences node) { return Settings.fromPrefs(node); } public void dataToNode(Object data, Preferences node) { ((Settings) data).toPrefs(node); } }; pcbm.setPreferences( getRecentPreferences() ); } return pcbm; } private ListCellRenderer mkComboBoxRenderer(ListCellRenderer peer) { return new BlendCBRenderer(peer); } protected void storeRecent() { if (pcbm == null) return; pcbm.setSelectedItem(null); while (pcbm.getSize() >= MAX_RECENTNUM) { try { pcbm.remove(pcbm.getSize() - 1); } catch (BackingStoreException e1) { e1.printStackTrace(); return; } } pcbm.add(0, current.duplicate()); } /** * TODO: pre/post position not yet effective (using 0.5 right now) */ public BlendContext createBlendContext(long maxLeft, long maxRight) { if (!java.awt.EventQueue.isDispatchThread()) throw new IllegalMonitorStateException(); if (!active) return null; final long blendLen; final Param p; p = current.duration; // Param.fromPrefs( prefs, KEY_DURATION, null ); if( p != null ) { ut.setLengthAndRate( 0, timeline.getRate() ); blendLen = (long) (ut.translate( p, ParamSpace.spcTimeSmps ).val + 0.5); if( maxLeft + maxRight > blendLen ) { maxLeft = (long) (maxLeft * (double) blendLen / (maxLeft + maxRight) + 0.5 ); maxRight= blendLen - maxLeft; } } return new BlendContext( maxLeft, maxRight, CurvePanel.getControlPoints( prefs ) ); } private static class Settings { protected Param duration; protected final Point2D[] ctrlPt = new Point2D[] { new Point2D.Double(), new Point2D.Double() }; private Settings() { /* empty */ } private Settings( Settings orig ) { setFrom( orig ); } protected void setFrom( Settings orig ) { duration = orig.duration; ctrlPt[ 0 ].setLocation( orig.ctrlPt[ 0 ]); ctrlPt[ 1 ].setLocation( orig.ctrlPt[ 1 ]); } protected static Settings fromPrefs( Preferences node ) { final Settings s = new Settings(); s.duration = Param.fromPrefs( node, KEY_DURATION, DEFAULT_DUR ); final Point2D[] pt = CurvePanel.getControlPoints( node ); s.ctrlPt[ 0 ].setLocation( pt[ 0 ]); s.ctrlPt[ 1 ].setLocation( pt[ 1 ]); return s; } protected Settings duplicate() { return new Settings( this ); } protected void toPrefs( Preferences node ) { node.put( KEY_DURATION, duration.toString() ); CurvePanel.toPrefs( ctrlPt, node ); } public String toString() { return String.valueOf( duration.val ); } } @SuppressWarnings("serial") private static class BlendCBRenderer extends JLabel implements ListCellRenderer { // private final Color bgNorm, bgSel, fgNorm, fgSel; final CurvePanel.Icon curveIcon; private final ListCellRenderer peer; protected BlendCBRenderer(ListCellRenderer peer) { super(); this.peer = peer; // setOpaque(true); // bgNorm = UIManager.getColor("List.background"); // bgSel = UIManager.getColor("List.selectionBackground"); // fgNorm = UIManager.getColor("List.foreground"); // fgSel = UIManager.getColor("List.selectionForeground"); curveIcon = new CurvePanel.Icon(createBasicCurves()); setIcon(curveIcon); setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); } public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { final Settings s = (Settings) value; final Object view = uvf.createView(s.duration.unit); final String txt = String.valueOf((int) s.duration.val) + " " + view.toString(); final Component res = peer.getListCellRendererComponent(list, txt /* value */, index, isSelected, cellHasFocus); curveIcon.update(s.ctrlPt[0], s.ctrlPt[1]); if (res instanceof JLabel) { ((JLabel) res).setIcon(curveIcon); } // if (view instanceof Icon) { // // XXX hmmm. should use composite icon // } else { // setText(String.valueOf((int) s.duration.val) + " " + view.toString()); // } // setBackground(isSelected ? bgSel : bgNorm); // setForeground(isSelected ? fgSel : fgNorm); // return this; return res; } } private class CloseAction extends AbstractAction { protected CloseAction(String text) { super(text); } public void actionPerformed(ActionEvent e) { stopAndDispose(); storeRecent(); } } }