/* * Created on Jan 31, 2007 * * Copyright (c) 2007 Jens Gulden * * http://www.frinika.com * * This file is part of Frinika. * * Frinika is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * Frinika is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with Frinika; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.frinika.sequencer.gui; import com.frinika.sequencer.model.util.TimeUtils; import com.frinika.project.ProjectContainer; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.BorderLayout; import java.awt.GridBagLayout; import java.awt.GridBagConstraints; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; /** * GUI-element for selecting a duration/amount of time. Depending on the format parameter * passed to the constructor, different input mechanisms are used: * * <p> * BAR_BEAT: a text-field displaying and parsing a "<bar>.<beat>" string * </p> * <p> * BEAT_TICK: a text-field displaying and parsing a "<beat>:<tick>" string * </p> * <p> * BAR_BEAT_TICK: a text-field displaying and parsing a "<bar>.<beat>:<tick>" string * </p> * <p> * BEAT: a text-field displaying and parsing a numeric double-value representing a number of beats * </p> * <p> * NOTE_LENGTH: either a drop-down-list or a scrollable multi-line select-box for selecting * lengths as beat-fractios, uas used with the specification of note lengths, e.g. * "1/4", "1/8", "1/16", "1/8 .", "1/4 trio" etc. Use te constructor parameter * multiLine to choose between drop-down-box and multi-line list-box. * * @see com.frinika.sequencer.model.util.TimeUtils * @author Jens Gulden */ public class TimeSelector extends JPanel { // TODO only FORMAT_BAR_BEAT_TICK and FORMAT_NOTE_LENGTH are tested public final static String[] NOTE_LENGTH_NAMES = new String[] // \u00b7 is middot { "2/1", "1/1 \u00b7", "1/1", "1/2 \u00b7", "1/2", "1/4 \u00b7", "1/4", "1/8 \u00b7", "1/8", "1/16", "1/32", "1/64", "1/2 trio", "1/4 trio", "1/8 trio", "1/16 trio" }; public final static double[] NOTE_LENGTH_FACTORS = new double[] { 2d/1d, 1.5*1d/1d, 1d/1d, 1.5*1d/2d, 1d/2d, 1.5*1d/4d, 1d/4d, 1.5*1d/8d, 1d/8d, 1d/16d, 1d/32d, 1d/64d, (1d/2d)*2d/3d, (1d/4d)*2d/3d, (1d/8d)*2d/3d, (1d/16d)*2d/3d }; private TimeFormat format; private JLabel label; private TickSpinner spinner; private JComboBox comboBox; private JList listBox; private boolean multiLine; private TimeUtils timeUtil; private ProjectContainer project; public TimeSelector(String label, long defaultTicks, boolean allowNegative, ProjectContainer project, TimeFormat format, boolean multiLine) { this.project = project; this.format = format; this.multiLine = multiLine; // timeUtil = new TimeUtils(project); timeUtil = project.getTimeUtils(); // PJL GridBagConstraints gc = null; if (label != null) { this.setLayout(new GridBagLayout()); gc = new GridBagConstraints(); gc.insets.left = 2; gc.insets.right = 5; this.label = new JLabel(label); add(this.label, gc); gc.insets.left = 0; gc.fill = GridBagConstraints.HORIZONTAL; gc.weightx = 1f; } else { this.setLayout(new BorderLayout(0,0)); } if (format == TimeFormat.NOTE_LENGTH) { if (multiLine) { listBox = new JList(NOTE_LENGTH_NAMES); listBox.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); add(new JScrollPane(listBox)); } else { comboBox = new JComboBox(NOTE_LENGTH_NAMES); add(comboBox); } if (defaultTicks == 0) { defaultTicks = 128 / 2; // better default for note-lengths } setTicks(defaultTicks); } else { // normal time selector /* String s = formatString(defaultTicks); //textField = new JTextField(s, FORMAT_SIZES[format]); textField = new JTextField(s, FORMAT_SIZES[ findIndex(TimeFormat.values(), format) ]); if (label != null) { add(textField, gc); } else { add(textField); } // change notification via FocusListener, to remain compatible with original impl textField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { // delegate to action-listeners ActionListener l[] = textField.getActionListeners(); ActionEvent ee = new ActionEvent(e.getSource(), e.getID(), null); for (int i = 0; i < l.length; i++) { l[i].actionPerformed(ee); } } }); */ spinner = new TickSpinner(format, defaultTicks, allowNegative, timeUtil); if (label != null) { add(spinner, gc); } else { add(spinner); } /*spinner.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { // delegate to action-listeners ActionListener l[] = textField.getActionListeners(); ActionEvent ee = new ActionEvent(e.getSource(), e.getID(), null); for (int i = 0; i < l.length; i++) { l[i].actionPerformed(ee); } } });*/ } } public TimeSelector(String label, long defaultTicks, ProjectContainer project, TimeFormat format, boolean multiLine) { this(label, defaultTicks, false, project, format, multiLine); } public TimeSelector(String label, long defaultTicks, ProjectContainer project, TimeFormat format) { this(label, defaultTicks, project, format, false); } public TimeSelector(String label, long defaultTicks, boolean allowNegative, ProjectContainer project, TimeFormat format) { this(label, defaultTicks, allowNegative, project, format, false); } public TimeSelector(String label, String defaultStr, ProjectContainer project, TimeFormat format, boolean multiLine) { this(null, 0l, project, format, multiLine); setString(defaultStr); } public TimeSelector(String label, String defaultStr, ProjectContainer project, TimeFormat format) { this(null, 0l, project, format, false); } public TimeSelector(String defaultStr, ProjectContainer project, TimeFormat format, boolean multiLine) { this(null, defaultStr, project, format, multiLine); } public TimeSelector(String defaultStr, ProjectContainer project, TimeFormat format) { this(null, defaultStr, project, format, false); } public TimeSelector(long defaultTicks, ProjectContainer project, TimeFormat format, boolean multiLine) { this(null, defaultTicks, project, format,multiLine); } public TimeSelector(ProjectContainer project, TimeFormat format, boolean multiLine) { this(0l, project, format, multiLine); } public TimeSelector(long defaultTicks, ProjectContainer project, TimeFormat format) { this(null, defaultTicks, project, format, false); } public TimeSelector(long defaultTicks, boolean allowNegative, ProjectContainer project, TimeFormat format) { this(null, defaultTicks, allowNegative, project, format, false); } public TimeSelector(ProjectContainer project, TimeFormat format) { this(0l, project, format, false); } public TimeSelector(ProjectContainer project) { this(project, TimeFormat.BAR_BEAT_TICK); } /*public void addActionListener(final ActionListener a) { if (format == TimeFormat.NOTE_LENGTH) { if (multiLine) { listBox.addListSelectionListener(new ListSelectionListener() { // wrap ListSelectionEvent to ActionEvent public void valueChanged(ListSelectionEvent e) { ActionEvent ae = new ActionEvent(listBox, 0, null); a.actionPerformed(ae); } }); } else { comboBox.addActionListener(a); } } else { textField.addActionListener(a); } }*/ public void addChangeListener(final ChangeListener l) { if (format == TimeFormat.NOTE_LENGTH) { if (multiLine) { listBox.addListSelectionListener(new ListSelectionListener() { // wrap ListSelectionEvent to ActionEvent public void valueChanged(ListSelectionEvent e) { ChangeEvent ce = new ChangeEvent(listBox); l.stateChanged(ce); } }); } else { //comboBox.addActionListener(l); comboBox.addActionListener(new ActionListener() { // wrap ListSelectionEvent to ActionEvent public void actionPerformed(ActionEvent e) { ChangeEvent ce = new ChangeEvent(comboBox); l.stateChanged(ce); } }); } } else { //textField.addActionListener(a); spinner.addChangeListener(l); } } public synchronized void setTicks(long ticks) { if (format == TimeFormat.NOTE_LENGTH) { int i = findClosest(ticks, NOTE_LENGTH_FACTORS, project.getSequence().getResolution()); if (multiLine) { listBox.setSelectedIndex(i); listBox.ensureIndexIsVisible(i); } else { comboBox.setSelectedIndex(i); } } else { //String s = formatString(ticks); //textField.setText(s); spinner.setValue(ticks); } } public long getTicks() { if (format == TimeFormat.NOTE_LENGTH) { int i; if (multiLine) { i = listBox.getSelectedIndex(); } else { i = comboBox.getSelectedIndex(); } double f = NOTE_LENGTH_FACTORS[i]; long ticks = Math.round(f * project.getSequence().getResolution() * 4); return ticks; } else { String s = getString(); long ticks = parseString(s); return ticks; } } public synchronized void setString(String s) { //if (format == TimeFormat.NOTE_LENGTH) { long ticks = parseString(s); setTicks(ticks); //} else { // textField.setText(s); //} } public String getString() { if (format == TimeFormat.NOTE_LENGTH) { return comboBox.getSelectedItem().toString(); } else { //return textField.getText(); //return ((JSpinner.DefaultEditor)spinner.getEditor()).getTextField().getText(); return ((TickSpinnerModel)spinner.getModel()).ticksToString( (Long)spinner.getValue() ); } } public long parseString(String s) { return parseStringImpl(s, timeUtil, format, project); } public String formatString(long ticks) { return formatStringImpl(ticks, timeUtil, format); } private static long parseStringImpl(String s, TimeUtils timeUtil, TimeFormat format, ProjectContainer project) { int sgn = 1; s = s.trim(); if (s.length() == 0) return 0; char sgnch = s.charAt(0); if (sgnch=='-') { sgn = -1; s = s.substring(1); } else if (sgnch=='+') { s = s.substring(1); } switch (format) { case BAR_BEAT_TICK: return sgn * timeUtil.barBeatTickToTick(s); case BAR_BEAT: return sgn * timeUtil.barBeatTickToTick(s+":000"); //case BEAT_TICK: return sgn * timeUtil.barBeatTickToTick("0."+s); case BEAT_TICK: return sgn * timeUtil.beatTickToTick(s); case BEAT: try { return sgn * Math.round(Double.valueOf(s) * project.getSequence().getResolution()); } catch (NumberFormatException nfe) { return 0; } default: return 0; } } private static String formatStringImpl(long tick, TimeUtils timeUtil, TimeFormat format) { String sgn; if (tick < 0) { sgn = "-"; tick = -tick; } else { sgn = ""; } switch (format) { case BAR_BEAT_TICK: return sgn + timeUtil.tickToBarBeatTick(tick); case BAR_BEAT: return sgn + timeUtil.tickToBarBeat(tick); case BEAT_TICK: return sgn + timeUtil.tickToBeatTick(tick); case BEAT: return sgn + String.valueOf(timeUtil.tickToFloatBeat(tick)); default: return "---"; } } private static int findClosest(long ticks, double[] factors, int resolution) { long diff = Long.MAX_VALUE; int result = -1; for (int i = 0; i < factors.length; i++) { long t = Math.round(factors[i] * resolution * 4); if (t == ticks) return i; // optimiziation, will usually be the place ot exit here long d = ticks - t; if (d < 0) d = -d; if (d < diff) { diff = d; result = i; } } return result; } private static int findIndex(Object[] a, Object o) { for (int i = 0; i < a.length; i++) { if (a[i] == o) { return i; } } return -1; } }