/* * Created on Apr 14, 2006 * * Copyright (c) 2005 Peter Johan Salomonsen (http://www.petersalomonsen.com) * * 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.global; import java.awt.BorderLayout; import java.awt.Font; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import java.util.Vector; import javax.sound.midi.MidiDevice; import javax.sound.midi.MidiSystem; import javax.sound.midi.MidiUnavailableException; import javax.sound.midi.MidiDevice.Info; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JTextField; import javax.swing.event.ChangeEvent; import com.frinika.project.FrinikaAudioSystem; import com.frinika.gui.DefaultOptionsBinder; import com.frinika.gui.util.FontChooser; import com.frinika.gui.util.PresentationPanel; import com.frinika.project.gui.ProjectFrame; /** * Global settings to be stored and restored when starting the program. * * In order to provide compile-time safe access to options, * a field of type "Meta" with the prefix "_" is declared for each * config option. Example: * * public static Font TEXT_LANE_FONT = new Font("Arial",Font.PLAIN, 8); * public static Meta _TEXT_LANE_FONT; // field of type "Meta", value will automatically be set when class FrinikaConfig is loaded * * Attach a listener, e.g.: * * FrinikaConfig.addConfigListener(new ConfigListener() { * public void configurationChanged(ChangeEvent event) { * if (event.getSource() == FrinikaConfig._TEXT_LANE_FONT) { // test if it's the option we're interested in * // ...etc... * } * } * }); * * @author Peter Johan Salomonsen * @author Jens Gulden */ public class FrinikaConfig { // --- global Frinika options here ---------------------------------------------------------- public static boolean SETUP_DONE = false; public static Meta _SETUP_DONE; public static int TICKS_PER_QUARTER = 128; public static Meta _TICKS_PER_QUARTER; public static int SEQUENCER_PRIORITY = 0; public static Meta _SEQUENCER_PRIORITY; // used by JavaSoundVoiceServer public static int AUDIO_BUFFER_LENGTH = 512; // TODO: IS THIS IN MILLISECONDS? OTHERWISE CONVERSION MILLISECONDS (as in gui) <-> AUDIO_BUFFER_LENGTH is needed public static Meta _AUDIO_BUFFER_LENGTH; public static String MIDIIN_DEVICES_LIST = ""; public static Meta _MIDIIN_DEVICES_LIST; public static boolean DIRECT_MONITORING = false; public static Meta _DIRECT_MONITORING; public static boolean MULTIPLEXED_AUDIO = false; public static Meta _MULTIPLEXED_AUDIO; public static boolean BIG_ENDIAN = false; public static Meta _BIG_ENDIAN; public static int sampleRate = 44100; // lowercase spelling for 'historic' reasons public static Meta _sampleRate; public static boolean JACK_AUTO_CONNECT = false; public static Meta _JACK_AUTO_CONNECT; public static boolean AUTOMATIC_CHECK_FOR_NEW_VERSION = true; public static Meta _AUTOMATIC_CHECK_FOR_NEW_VERSION; // deprecated I believe PJL // public static int OS_LATENCY_MILLIS = 0; // public static Meta _OS_LATENCY_MILLIS; public static boolean MAXIMIZE_WINDOW = false; public static Meta _MAXIMIZE_WINDOW; public static float MOUSE_NUMBER_DRAG_INTENSITY = 2.0f; public static Meta _MOUSE_NUMBER_DRAG_INTENSITY; public static Font TEXT_LANE_FONT = new Font("Arial",Font.PLAIN, 8); public static Meta _TEXT_LANE_FONT; public static File GROOVE_PATTERN_DIRECTORY = new File(System.getProperty("user.home"), "frinika/groovepatterns/"); public static Meta _GROOVE_PATTERN_DIRECTORY; public static File SCRIPTS_DIRECTORY = new File(System.getProperty("user.home"), "frinika/scripts/"); public static Meta _SCRIPTS_DIRECTORY; public static File PATCHNAME_DIRECTORY = new File(System.getProperty("user.home"), "frinika/patchname/"); public static Meta _PATCHNAME_DIRECTORY; public static File AUDIO_DIRECTORY = new File(System.getProperty("user.home"), "frinika/audio/"); public static Meta _AUDIO_DIRECTORY; public static File SOUNDFONT_DIRECTORY = new File(System.getProperty("user.home"), "frinika/soundfonts/"); public static Meta _SOUNDFONT_DIRECTORY; public static File DEFAULT_SOUNDFONT = new File(System.getProperty("user.home"), "frinika/soundfonts/8MBGMSFX.SF2"); public static Meta _DEFAULT_SOUNDFONT; public static String LAST_PROJECT_FILENAME = null; public static Meta _LAST_PROJECT_FILENAME; // --- gui binding --- /** * Binds all option fields to GUI elements in the ConfigDialogPanel (or to null, if not used in GUI). * * The validity of this binding is verified during application startup * - ALL public static fields must be named here, none more or less * - (all must be spelled correctly, of course) * Otherwise the application refuses to start and gives an error. * * The fields are associated with concrete instances of GUI elements (e.g. JTextField, JCheckBox, ButtonGroup etc.). * The DefaultOptionsBinder should know how to convert between actual GUI elements and typed field values, otherwise * its refresh()/update() method should be overridden and customized here. * * @param d * @return */ private static Map<Meta, Object> bindMap(ConfigDialogPanel d) { Map<Meta, Object> m = new HashMap<Meta, Object>(); m.put( _AUDIO_BUFFER_LENGTH, d.spinnerBufferSize ); m.put( _DIRECT_MONITORING, d.checkboxUseDirectMonitoring ); m.put( _MULTIPLEXED_AUDIO, d.checkboxUseMultiplexedJavasoundServer ); // m.put( _OS_LATENCY_MILLIS, d.spinnerOutputLatency ); m.put( _JACK_AUTO_CONNECT, d.checkboxAutoconnectJack ); m.put( _sampleRate, d.comboboxSampleRate ); m.put(_TICKS_PER_QUARTER, d.spinnerTicksPerQuarter ); m.put(_SEQUENCER_PRIORITY, d.spinnerSequencerPriority ); m.put( _BIG_ENDIAN, d.checkboxBigEndian ); m.put( _MAXIMIZE_WINDOW, d.checkboxOpenMaximizedWindow ); m.put( _MOUSE_NUMBER_DRAG_INTENSITY, d.spinnerMouseDragSpeedSpinners ); m.put( _TEXT_LANE_FONT, d.textfieldFontTextLane ); m.put( _GROOVE_PATTERN_DIRECTORY, d.textfieldGroovePatternsDirectory ); m.put( _SCRIPTS_DIRECTORY, d.textfieldScriptsDirectory ); m.put( _AUDIO_DIRECTORY, d.textfieldAudioDirectory ); m.put( _SOUNDFONT_DIRECTORY, d.textfieldSoundFontDirectory ); m.put( _PATCHNAME_DIRECTORY, d.textfieldPatchNameDirectory ); m.put( _DEFAULT_SOUNDFONT, d.textfieldDefaultSoundFont ); m.put( _MIDIIN_DEVICES_LIST, null ); // handled 'manually' by dialog m.put( _LAST_PROJECT_FILENAME, null ); m.put( _SETUP_DONE, null ); m.put( _AUTOMATIC_CHECK_FOR_NEW_VERSION, null); return m; } /** * Bind map for options that are stored in dynamic properties, not as public static fields. * @param d * @return */ /*private static Object[][] dynamicBindMap(ConfigDialogPanel d) { return new Object[][] { //{ "javasound.output", d.comboboxOutputDevice}, }; }*/ // --- end of configurable part ---------------------------------------------- private static final String CONFIG_FILE_NAME = "FrinikaConfig.xml"; private static final String META_PREFIX = "_"; private static File userFrinikaDir = new File(System.getProperty("user.home"), "frinika"); private static File configFile = new File(userFrinikaDir, CONFIG_FILE_NAME); /** * If a field is of tpe java.io.File, this suffix denotes that a directory, * not a real file is required. */ private static final String DIRECTORY_SUFFIX = "_DIRECTORY"; private static Map<String, Field> fieldsByName; private static Map<String, Field> metafieldsByName; private static Map<Field, Meta> metasByField; private static Collection<ConfigListener> listeners; private static JDialog showingDialog = null; private static JFrame showingDialogFrame = null; private static Properties properties; // dynamic storage static { // class initializer properties = new Properties(); fieldsByName = new HashMap<String, Field>(); metafieldsByName = new HashMap<String, Field>(); metasByField = new HashMap<Field, Meta>(); listeners = new ArrayList<ConfigListener>(); Field[] fields = FrinikaConfig.class.getFields(); for (Field f : fields) { if ((f.getModifiers() & Modifier.STATIC) != 0) { // only look at static fields String name = f.getName(); if (f.getType() == Meta.class) { if (!name.startsWith(META_PREFIX)) { throw new ConfigError("meta-field '"+name+"' does not start with prefix "+ META_PREFIX); } else { metafieldsByName.put(name, f); } } else { fieldsByName.put(name, f); } } } // resolve meta-fields for (Map.Entry<String, Field> metafieldEntry : metafieldsByName.entrySet()) { String metafieldName = metafieldEntry.getKey(); Field metafield = metafieldEntry.getValue(); String name = metafieldName.substring(META_PREFIX.length()); Field field = fieldsByName.get(name); if (field == null) { throw new ConfigError("no corresponding option field '"+name+"' found for meta-field '"+metafieldName+"'"); } Meta meta = new Meta(field); try { metafield.set(null, meta); } catch (IllegalAccessException iae) { throw new ConfigError(iae); } metasByField.put(field, meta); } if (!userFrinikaDir.exists()) { System.out.println(" Creating frinka user settings directory " + userFrinikaDir); if (!userFrinikaDir.mkdir()) { System.err.println(" Failed to create " + userFrinikaDir); } } try { load(); } catch (FileNotFoundException fnfe) { System.out.println("Can't find file " + configFile.getAbsolutePath() + ". It will be created when you quit the program or change configuration options."); } catch (Exception e) { System.err.println("error loading configuration. defaults will be used where possible."); e.printStackTrace(); } // verify static bindMap // make sure all available options are at least named in the map Map<Meta, Object> m = bindMap(new ConfigDialogPanel(null)); // dummy for verifying Collection<Field> boundFields = new ArrayList<Field>(); /*for (int i = 0; i < m.length; i++) { Object[] pair = m[i]; boundFields.add(findField((String)pair[0])); }*/ for (Map.Entry<Meta, Object> e : m.entrySet()) { Meta meta = e.getKey(); Object component = e.getValue(); if ((component != null) && (component instanceof JComponent) && (((JComponent)component).getParent() == null)) { throw new ConfigError("gui element bound to config option '"+meta.getName()+"' has no parent, type "+component.getClass().getName()); } boundFields.add(meta.getField()); } Collection<Field> allfields = new ArrayList<Field>(fieldsByName.values()); allfields.removeAll(boundFields); int a = boundFields.size(); int b = fieldsByName.size(); if ((a != b) || (!allfields.isEmpty())) { for (Field f : allfields) { System.err.println("unbound field: "+f.getName()); } throw new ConfigError("there are fields which are not bound to gui elements (or to null), see above"); } } public static void showDialog(ProjectFrame frame) { if (showingDialog != null) { // already showing (or initialized and hidden)? if (showingDialogFrame == frame) { // for same frame? showingDialog.show(); showingDialog.toFront(); // then just put to front return; } else { // showing for different frame: close old one first showingDialog.dispose(); } } showingDialogFrame = frame; showingDialog = createDialog(frame); showingDialog.show(); } protected static JDialog createDialog(ProjectFrame frame) { ConfigDialogPanel configDialogPanel = new ConfigDialogPanel(frame); Map<Meta, Object> m = bindMap(configDialogPanel); Map<Field, Object> map = convertMap(m); /*Object[][] m2 = dynamicBindMap(configDialogPanel); Map<String, Object> map2 = new HashMap<String, Object>(); for (int i = 0; i < m2.length; i++) { map2.put((String)m2[i][0], m2[i][1]); }*/ //DefaultOptionsBinder optionsBinder = new DefaultOptionsBinder(map, map2, properties); DefaultOptionsBinder optionsBinder = new DefaultOptionsBinder(map, properties); ConfigDialog d = new ConfigDialog(frame, optionsBinder); PresentationPanel presentationPanel = new PresentationPanel(configDialogPanel.tabbedPane); d.getContentPane().add(presentationPanel, BorderLayout.CENTER); return d; } public static Properties getProperties() { return properties; } public static String getProperty(String key) { return properties.getProperty(key); } public static void setProperty(String key, String value) { properties.setProperty(key, value); } /*public static Map<String, Object> backup() { Map<String, Object> map = new HashMap<String, Object>(); for (Field f : fieldsByName.values()) { map.put(f.getName(), getFieldValue(f)); } return map; } public static void restore(Map<String, Object> map) { for (Map.Entry<String, Object> e : map.entrySet()) { String name = e.getKey(); Object value = e.getValue(); Field f = findField(name); } }*/ public static void load() throws IOException { InputStream r = new FileInputStream(configFile); load(r); } public static boolean store() { try { OutputStream w = new FileOutputStream(configFile); save(w); return true; } catch (IOException ioe) { ioe.printStackTrace(); JOptionPane.showConfirmDialog(null, "Error while saving configuration file.", "Error while saving.", JOptionPane.OK_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE, null); return false; } } public static void storeAndQuit() { if (!store()) { int answer = JOptionPane.showConfirmDialog(null, "Exit anyway?", "Error while saving.", JOptionPane.OK_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE, null); if (answer != JOptionPane.OK_OPTION) { return; // don't quit } } System.exit(0); } public static void load(InputStream r) throws IOException { properties = new Properties(); properties.loadFromXML(r); loadFields(properties); // remove fields from dynamic properties for (String fieldname : fieldsByName.keySet()) { properties.remove(fieldname); } } public static void save(OutputStream w) throws IOException { Properties p = new Properties(); // copy all dynamic properties into for (Object key : properties.keySet()) { p.setProperty((String)key, properties.getProperty((String)key)); } SETUP_DONE = true; saveFields(p); p.storeToXML(w, "Frinika configuration"); } public static void loadFields(Properties p) { for (Field field : fieldsByName.values()) { String name = field.getName(); String prop = p.getProperty(name); if (prop == null) { System.out.println("no saved property for configuration option "+name+", using default"); } else { Object o = stringToValue(prop, name, field.getType()); setFieldValue(field, o); } } } public static void saveFields(Properties p) { for (Field field : fieldsByName.values()) { String name = field.getName(); Object o = getFieldValue(field); String s = valueToString(o, name, field.getType()); if (s != null) { p.setProperty(name, s); } else { // don't save entry at all if null value } } } public static void addConfigListener(ConfigListener l) { listeners.add(l); } public static void removeConfigListener(ConfigListener l) { listeners.remove(l); } /** * Fire event if a public static field option has been altered. * No even is fired on changes of dynamic properties-options. * @param field */ public static void fireConfigurationChangedEvent(Meta meta) { ChangeEvent event = new ChangeEvent(meta); for (ConfigListener l : listeners) { l.configurationChanged(event); } } private static Field findField(String name) { Field field = fieldsByName.get(name); if (field == null) { // severe error, should fail here to ensure hard binding throw new ConfigError("dynamic bind error: configuration field "+name+" does not exist."); } return field; } private static Map<Field, Object> convertMap(Object[][] bindMap) { Map<Field, Object> m = new HashMap<Field, Object>(); for (int i = 0; i < bindMap.length; i++) { Object[] pair = bindMap[i]; Field field = findField((String)pair[0]); Object component = pair[1]; m.put(field, component); } return m; } private static Map<Field, Object> convertMap(Map<Meta, Object> map) { Map<Field, Object> m = new HashMap<Field, Object>(); for (Map.Entry<Meta, Object> e : map.entrySet()) { Field field = e.getKey().getField(); Object component = e.getValue(); m.put(field, component); } return m; } public static Object getFieldValue(Field field) { try { return field.get(null); } catch (IllegalAccessException iae) { throw new ConfigError("dynamic bind error: IllegalAccessException on getField " + iae.getMessage()); } } public static void setFieldValue(Field field, Object o) { Object oldValue; try { oldValue = field.get(null); } catch (IllegalAccessException iae) { throw new ConfigError("dynamic bind error: IllegalAccessException on iitial get of setField " + iae.getMessage()); } // System.out.println(" set field value "+ field + " " + o ); System.out.println("config: " + field.getName() + "=" + o ); try { field.set(null, o); // special: make sure directories exist if ((o instanceof File) && (field.getName().endsWith(DIRECTORY_SUFFIX))) { ((File)o).mkdirs(); } if (((o != null) && (!o.equals(oldValue))) || ((o == null) && (oldValue != null))) { Meta meta = metasByField.get(field); if (meta != null) { fireConfigurationChangedEvent(meta); } } } catch (IllegalAccessException iae) { throw new ConfigError("dynamic bind error: IllegalAccessException on setField " + iae.getMessage()); } } public static Object stringToValue(String prop, String name, Class type) { if (prop == null) return null; if (int.class.isAssignableFrom(type)) { return Integer.parseInt(prop); } else if (long.class.isAssignableFrom(type)) { return Long.parseLong(prop); } else if (double.class.isAssignableFrom(type)) { return Double.parseDouble(prop); } else if (float.class.isAssignableFrom(type)) { return Float.parseFloat(prop); } else if (boolean.class.isAssignableFrom(type)) { return Boolean.parseBoolean(prop); } else if (File.class.isAssignableFrom(type)) { return new File(prop); } else if (Font.class.isAssignableFrom(type)) { return stringToFont(prop); } else { return prop; } } public static String valueToString(Object o, String name, Class type) { if (o == null) return null; if (File.class.isAssignableFrom(type)) { return ((File)o).getAbsolutePath() ; } else if (Font.class.isAssignableFrom(type)) { return fontToString((Font)o); } else { return o.toString(); } } public static boolean isTrue(Object o) { if (o instanceof Boolean) { return ((Boolean)o).booleanValue(); } else if (o instanceof Number) { return !(Math.abs(((Number)o).doubleValue()) < 0.000000001d); } else { String s = o.toString(); s = s.trim().toLowerCase(); return s.equals("true")||s.equals("yes"); } } public static Font stringToFont(String s) { StringTokenizer st = new StringTokenizer(s, ",", false); String fontName = "Helvetica"; String fontSizeStr = "12"; String fontStyleStr = "plain"; if (st.hasMoreTokens()) { fontName = st.nextToken(); if (st.hasMoreTokens()) { fontSizeStr = st.nextToken(); if (st.hasMoreTokens()) { fontStyleStr = st.nextToken(); } } } int fontSize; try { fontSize = Integer.parseInt(fontSizeStr); } catch (NumberFormatException nfe) { fontSize = 12; } int fontStyle = 0; fontStyleStr = fontStyleStr.toLowerCase(); if (fontStyleStr.indexOf("bold") != -1) { fontStyle |= Font.BOLD; } if (fontStyleStr.indexOf("italic") != -1) { fontStyle |= Font.ITALIC; } return new Font(fontName, fontStyle, fontSize); } public static String fontToString(Font font) { int fontStyle = font.getStyle(); String fontStyleStr = ""; if ((fontStyle&Font.BOLD) != 0) { fontStyleStr += "bold"; } if ((fontStyle&Font.ITALIC) != 0) { fontStyleStr += "italic"; } if (fontStyleStr.length() == 0) { fontStyleStr = "plain"; } return font.getName()+","+font.getSize()+","+fontStyleStr; } public static String fileToString(File file) { String s; try { s = file.getCanonicalPath(); } catch (IOException ioe) { s = file.getAbsolutePath(); } return s; } public static void pickDirectory(ProjectFrame frame, JTextField boundTextField) { pickFile(frame, boundTextField, true); } public static void pickFile(ProjectFrame frame, JTextField boundTextField) { pickFile(frame, boundTextField, false); } public static void pickFile(ProjectFrame frame, JTextField boundTextField, boolean directory) { String s = boundTextField.getText(); File file = new File(s); JFileChooser fc = new JFileChooser(file); if (directory) { fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); } fc.showDialog(frame, "Choose"); file = fc.getSelectedFile(); if (file != null) { s = fileToString(file); boundTextField.setText(s); } } public static void pickFont(ProjectFrame frame, JTextField boundTextField) { String s = boundTextField.getText(); Font font = stringToFont(s); font = FontChooser.showDialog(frame, "Pick Font...", font); if (font != null) { s = fontToString(font); boundTextField.setText(s); } } // --- accessor methods -------------------------------------------------- public static int getAudioBufferLength() { // TODO: direct read-access to field instead return AUDIO_BUFFER_LENGTH; } public static void setAudioBufferLength(int len) { setFieldValue(findField("AUDIO_BUFFER_LENGTH"), len); } public static boolean getDirectMonitoring() { // TODO: direct read-access to field instead return DIRECT_MONITORING; } public static void setDirectMonitoring(boolean dm) { _DIRECT_MONITORING.set(dm); } public static void setMultiplexedAudio(boolean multiplex) { _MULTIPLEXED_AUDIO.set(multiplex); } public static void setJackAutoconnect(boolean auto) { _JACK_AUTO_CONNECT.set(auto); } public static String lastProjectFile() { // TODO: direct read-access to field instead return LAST_PROJECT_FILENAME; } public static void setLastProjectFilename(String filename) { setFieldValue(findField("LAST_PROJECT_FILENAME"), filename); } public static void setMidiInDeviceList(Vector<String> list) { StringBuffer buf = new StringBuffer(); boolean first = true; for (String o : list) { if (!first) buf.append(";"); buf.append(o.toString()); first = false; } System.out.println(buf); String s = buf.toString(); setFieldValue(findField("MIDIIN_DEVICES_LIST"), s); } public static Vector<String> getMidiInDeviceList() { String buf = MIDIIN_DEVICES_LIST; if (buf == null) buf = ""; String[] list = buf.split(";"); Vector<String> vec = new Vector<String>(); for (String str : list) { if (!str.equals("")) vec.add(str); } return vec; } public static Collection<String> getAvailableMidiInDevices() { ArrayList<String> a = new ArrayList<String>(); Info infos[] = MidiSystem.getMidiDeviceInfo(); for (Info info : infos) { try { MidiDevice dev=MidiSystem.getMidiDevice(info); if (dev.getMaxTransmitters() == 0 ) continue; String str = info.toString(); a.add(str); } catch (MidiUnavailableException e) { // TODO Auto-generated catch block e.printStackTrace(); } } return a; } public static Collection<String> getAvailableAudioDevices() { List<String> list = FrinikaAudioSystem.getAudioServer().getAvailableOutputNames(); return list; } public static boolean getAutomaticCheckForNewVersion() { return AUTOMATIC_CHECK_FOR_NEW_VERSION; } public static void setAutomatickCheckForNewVersion(boolean automaticCheckForNewVersion) { AUTOMATIC_CHECK_FOR_NEW_VERSION = automaticCheckForNewVersion; } /** * Meta-Info on option fields. */ public static class Meta { private Field field; Meta(Field field) { this.field = field; } public Field getField() { return field; } public String getName() { return getField().getName(); } public Class getType() { return getField().getType(); } public Object get() { try { return getField().get(null); } catch (IllegalAccessException iae) { throw new ConfigError(iae); } } public void set(Object o) { FrinikaConfig.setFieldValue(getField(), o); } } }