/* -*- c-basic-offset: 2; indent-tabs-mode: nil; -*- */
/*
* FreeDots -- MusicXML to braille music transcription
*
* Copyright 2008-2010 Mario Lang All Rights Reserved.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3, as
* published by the Free Software Foundation.
*
* This code 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 (a copy is included in the LICENSE.txt file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License
* along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This file is maintained by Mario Lang <mlang@delysid.org>.
*/
package freedots.gui.swing;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.InputStream;
import javax.sound.midi.MidiUnavailableException;
import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.event.CaretEvent;
import freedots.Options;
import freedots.braille.Sign;
import freedots.musicxml.Library;
import freedots.musicxml.MIDISequence;
import freedots.musicxml.Note;
import freedots.musicxml.Score;
import freedots.playback.MIDIPlayer;
import freedots.playback.MetaEventRelay;
import freedots.playback.MetaEventListeningUnavailableException;
import freedots.transcription.Transcriber;
/**
* Main class for Swing based graphical user interface.
*/
public final class Main extends JFrame
implements javax.swing.event.CaretListener,
freedots.gui.GraphicalUserInterface,
freedots.playback.PlaybackObserver {
private static final String WELCOME_MESSAGE =
"Use \"File -> Open\" (Ctrl+O) to open a new score";
private Score score;
public Score getScore() { return score; }
private final Transcriber transcriber;
Transcriber getTranscriber() { return transcriber; }
private Options options = Options.getInstance();
private StatusBar statusBar = new StatusBar();
void setStatusMessage(String text) {
statusBar.setMessage(text);
update(getGraphics());
}
private SingleNoteRenderer noteRenderer = null;
void setScore(final Score score) {
this.score = score;
transcriber.setScore(score);
textPane.setText(transcriber.getSigns());
final boolean scoreAvailable = score != null;
fileSaveAsAction.setEnabled(scoreAvailable);
playScoreAction.setEnabled(scoreAvailable);
}
private BraillePane textPane;
private Object lastObject = null;
private boolean autoPlay = false;
private boolean caretFollowsPlayback = true;
public void caretUpdate(CaretEvent caretEvent) {
int index = caretEvent.getDot();
Object scoreObject = null;
if (transcriber != null) {
scoreObject = transcriber.getScoreObjectAtIndex(index);
final Sign brailleSign = transcriber.getSignAtIndex(index);
if (statusBar != null && brailleSign != null) {
setStatusMessage(brailleSign.toString() + ": "
+ brailleSign.getDescription());
}
}
if (scoreObject != lastObject) {
final boolean isPitchedNote =
scoreObject != null
&& scoreObject instanceof Note && !((Note)scoreObject).isRest();
editFingeringAction.setEnabled(isPitchedNote);
if (scoreObject != null) {
if (scoreObject instanceof Note) {
Note note = (Note)scoreObject;
noteRenderer.setNote(note);
if (autoPlay) {
midiPlayer.stop();
try {
midiPlayer.setSequence(new MIDISequence(note));
midiPlayer.start();
} catch (javax.sound.midi.InvalidMidiDataException e) {
e.printStackTrace();
}
}
}
}
lastObject = scoreObject;
}
}
private MIDIPlayer midiPlayer;
private MetaEventRelay metaEventRelay = new MetaEventRelay(this);
public void objectPlaying(Object object) {
if (caretFollowsPlayback) {
final int pos = transcriber.getIndexOfScoreObject(object);
if (pos >= 0) {
javax.swing.SwingUtilities.invokeLater(
new Runnable() {
public void run() {
boolean old = autoPlay;
autoPlay = false;
textPane.setCaretPosition(pos);
autoPlay = old;
}
}
);
}
}
}
private boolean paused = false, begin = true;
boolean startPlayback() {
if (score != null) {
// Play from the beginning
if (begin)
try {
midiPlayer.setSequence(new MIDISequence(score, true, metaEventRelay));
midiPlayer.start();
paused = false;
begin = false;
return true;
} catch (javax.sound.midi.InvalidMidiDataException exception) {
exception.printStackTrace();
}
else {
// Resume
if (paused) {
midiPlayer.start();
paused = false;
begin = false;
return true;
} else {
// Pause
if (midiPlayer != null) {
midiPlayer.stop();
paused = true;
begin = false;
return true;
}
}
}
}
return false;
}
void stopPlayback() {
if (midiPlayer != null) {
midiPlayer.stop();
paused = true;
begin = true;
}
}
private Action playScoreAction = new PlayScoreAction(this);
private Action fileSaveAsAction = new FileSaveAsAction(this);
private Action editFingeringAction = new EditFingeringAction(this);
public Main(final Transcriber transcriber) {
super("FreeDots " + freedots.Main.VERSION);
this.transcriber = transcriber;
try {
MIDIPlayer player = new MIDIPlayer(metaEventRelay);
midiPlayer = player;
} catch (MidiUnavailableException e) {
String message = "MIDI playback unavailable";
JOptionPane.showMessageDialog(this, message, "Alert",
JOptionPane.ERROR_MESSAGE);
} catch (javax.sound.midi.InvalidMidiDataException e) {
e.printStackTrace();
} catch (MetaEventListeningUnavailableException e) {
e.printStackTrace();
}
/* Load a font capable of displaying unicode braille */
try {
InputStream dejaVu = getClass().getResourceAsStream("DejaVuSerif.ttf");
GraphicsEnvironment
graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
graphicsEnvironment.registerFont(Font.createFont(Font.TRUETYPE_FONT,
dejaVu));
} catch (java.io.IOException ioe) {
ioe.printStackTrace();
} catch (FontFormatException ffe) {
ffe.printStackTrace();
}
setJMenuBar(createMenuBar());
setContentPane(createContentPane());
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
quit();
}
});
pack();
}
public void run() {
setVisible(true);
if (options.getSoundfont() != null) {
if (!midiPlayer.loadSoundbank(options.getSoundfont())) {
String message = "Requested Soundfont '"+options.getSoundfont()
+" could not be loaded";
JOptionPane.showMessageDialog(this, message, "Alert",
JOptionPane.ERROR_MESSAGE);
}
}
}
public void quit() {
if (midiPlayer != null) midiPlayer.close();
System.exit(0);
}
private JPanel createContentPane() {
textPane = new BraillePane();
textPane.setSize(options.getPageWidth(), options.getPageHeight());
final boolean scoreAvailable = transcriber.getScore() != null;
if (scoreAvailable) {
this.score = transcriber.getScore();
textPane.setText(transcriber.getSigns());
} else textPane.setText(WELCOME_MESSAGE);
fileSaveAsAction.setEnabled(scoreAvailable);
playScoreAction.setEnabled(scoreAvailable);
textPane.addCaretListener(this);
// Lay out the content pane.
JPanel contentPane = new JPanel();
contentPane.setLayout(new BorderLayout());
contentPane.add(new JScrollPane(textPane), BorderLayout.CENTER);
contentPane.add(statusBar, BorderLayout.SOUTH);
noteRenderer = new SingleNoteRenderer();
contentPane.add(noteRenderer, BorderLayout.AFTER_LAST_LINE);
return contentPane;
}
private JMenuBar createMenuBar() {
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
fileMenu.setMnemonic(KeyEvent.VK_F);
fileMenu.add(new FileOpenAction(this));
fileMenu.add(fileSaveAsAction);
fileMenu.addSeparator();
JMenuItem quitItem = new JMenuItem(new QuitAction(this));
quitItem.setMnemonic(KeyEvent.VK_Q);
quitItem.getAccessibleContext().setAccessibleDescription(
"Exit this application.");
fileMenu.add(quitItem);
menuBar.add(fileMenu);
JMenu editMenu = new JMenu("Edit");
editMenu.setMnemonic(KeyEvent.VK_E);
editMenu.add(editFingeringAction);
menuBar.add(editMenu);
menuBar.add(createTranscriptionMenu());
menuBar.add(createPlaybackMenu());
menuBar.add(createLibraryMenu());
JMenu helpMenu = new JMenu("Help");
helpMenu.setMnemonic(KeyEvent.VK_H);
helpMenu.add(new DescribeSignAction(this));
helpMenu.add(new DescribeColorAction(this));
menuBar.add(helpMenu);
return menuBar;
}
private JMenu createTranscriptionMenu() {
JMenu menu = new JMenu("Transcription");
menu.setMnemonic(KeyEvent.VK_T);
JRadioButtonMenuItem sectionBySectionItem =
new JRadioButtonMenuItem("Section by Section");
JRadioButtonMenuItem barOverBarItem =
new JRadioButtonMenuItem("Bar over Bar");
ButtonGroup group = new ButtonGroup();
group.add(sectionBySectionItem);
group.add(barOverBarItem);
switch (options.getMethod()) {
case SectionBySection: sectionBySectionItem.setSelected(true); break;
case BarOverBar: barOverBarItem.setSelected(true); break;
default: throw new AssertionError(options.getMethod());
}
sectionBySectionItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
options.setMethod(Options.Method.SectionBySection);
triggerTranscription();
}
});
barOverBarItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
options.setMethod(Options.Method.BarOverBar);
triggerTranscription();
}
});
menu.add(sectionBySectionItem);
menu.add(barOverBarItem);
JCheckBoxMenuItem showFingeringItem =
new JCheckBoxMenuItem("Show fingering");
showFingeringItem.setSelected(options.getShowFingering());
showFingeringItem.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
options.setShowFingering(e.getStateChange() == ItemEvent.SELECTED);
triggerTranscription();
}
});
menu.add(showFingeringItem);
return menu;
}
private JMenu createPlaybackMenu() {
JMenu menu = new JMenu("Playback");
menu.setMnemonic(KeyEvent.VK_P);
menu.add(playScoreAction);
menu.add(new StopPlaybackAction(this));
JCheckBoxMenuItem autoPlayItem =
new JCheckBoxMenuItem("Play on caret move");
autoPlayItem.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
autoPlay = e.getStateChange() == ItemEvent.SELECTED;
}
});
autoPlayItem.setSelected(autoPlay);
menu.add(autoPlayItem);
JCheckBoxMenuItem caretFollowsPlaybackItem =
new JCheckBoxMenuItem("Caret follows playback");
caretFollowsPlaybackItem.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
caretFollowsPlayback = e.getStateChange() == ItemEvent.SELECTED;
}
});
caretFollowsPlaybackItem.setSelected(caretFollowsPlayback);
menu.add(caretFollowsPlaybackItem);
return menu;
}
private JMenu createLibraryMenu() {
JMenu libraryMenu = new JMenu("Library");
libraryMenu.setMnemonic(KeyEvent.VK_L);
JMenu baroqueMenu = new JMenu("Baroque");
baroqueMenu.setMnemonic(KeyEvent.VK_B);
JMenu jsBachMenu = new JMenu("Johann Sebastian Bach");
jsBachMenu.setMnemonic(KeyEvent.VK_B);
JMenu bwv988Menu = new JMenu("BWV 988: Aria con Variazioni");
JMenuItem bwv988_ariaItem = new JMenuItem("Aria", KeyEvent.VK_A);
bwv988_ariaItem.getAccessibleContext().setAccessibleDescription(
"Aria from goldberg variations");
bwv988_ariaItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("bwv988-aria.xml"));
}
});
bwv988Menu.add(bwv988_ariaItem);
JMenuItem bwv988_1Item = new JMenuItem("Variation 1", KeyEvent.VK_1);
bwv988_1Item.getAccessibleContext().setAccessibleDescription(
"Variation 1 from goldberg variations");
bwv988_1Item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("bwv988-1.xml"));
}
});
bwv988Menu.add(bwv988_1Item);
jsBachMenu.add(bwv988Menu);
JMenu bwv1013Menu =
new JMenu("BWV 1013: Partita in A minor for solo flute");
JMenuItem bwv1013_1Item = new JMenuItem("1. Allemande", KeyEvent.VK_A);
bwv1013_1Item.getAccessibleContext().setAccessibleDescription(
"Partita in A minor for solo flute: first movement (Allemande)");
bwv1013_1Item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("bwv1013-1.xml"));
}
});
bwv1013Menu.add(bwv1013_1Item);
JMenuItem bwv1013_2Item = new JMenuItem("2. Corrente", KeyEvent.VK_C);
bwv1013_2Item.getAccessibleContext().setAccessibleDescription(
"Partita in A minor for solo flute: second movement (Corrente)");
bwv1013_2Item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("bwv1013-2.xml"));
}
});
bwv1013Menu.add(bwv1013_2Item);
JMenuItem bwv1013_3Item = new JMenuItem("3. Sarabande", KeyEvent.VK_S);
bwv1013_3Item.getAccessibleContext().setAccessibleDescription(
"Partita in A minor for solo flute: second movement (Sarabande)");
bwv1013_3Item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("bwv1013-3.xml"));
}
});
bwv1013Menu.add(bwv1013_3Item);
JMenuItem bwv1013_4Item = new JMenuItem("4. Bouree", KeyEvent.VK_B);
bwv1013_4Item.getAccessibleContext().setAccessibleDescription(
"Partita in A minor for solo flute: second movement (Bouree)");
bwv1013_4Item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("bwv1013-4.xml"));
}
});
bwv1013Menu.add(bwv1013_4Item);
jsBachMenu.add(bwv1013Menu);
JMenu wtcMenu = new JMenu("The Well Tempered Clavier");
JMenuItem wtc_3Item = new JMenuItem("BWV 847: Praeludium II", KeyEvent.VK_P);
wtc_3Item.getAccessibleContext().setAccessibleDescription(
"BWV 847 prelude from the well tempered clavier");
wtc_3Item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("bwv847-p.xml"));
}
});
wtcMenu.add(wtc_3Item);
jsBachMenu.add(wtcMenu);
baroqueMenu.add(jsBachMenu);
libraryMenu.add(baroqueMenu);
JMenu classicalMenu = new JMenu("Classical");
classicalMenu.setMnemonic(KeyEvent.VK_C);
JMenu lvBeethovenMenu = new JMenu("Ludwig Van Beethoven");
lvBeethovenMenu.setMnemonic(KeyEvent.VK_B);
JMenu moonshineMenu = new JMenu("Sonata XIV Op.27 No.2");
lvBeethovenMenu.add(moonshineMenu);
JMenuItem moonshine_1Item =
new JMenuItem("1. First movement", KeyEvent.VK_1);
moonshine_1Item.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("lvb-moonlight-1.xml"));
}
});
moonshineMenu.add(moonshine_1Item);
classicalMenu.add(lvBeethovenMenu);
libraryMenu.add(classicalMenu);
JMenu fakebookMenu = new JMenu("Fakebook");
fakebookMenu.setMnemonic(KeyEvent.VK_F);
JMenuItem autumnLeavesItem = new JMenuItem("Autumn Leaves", KeyEvent.VK_A);
autumnLeavesItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("autumn_leaves.xml"));
}
});
fakebookMenu.add(autumnLeavesItem);
JMenuItem blueAndSentimentalItem =
new JMenuItem("Blue and sentimental", KeyEvent.VK_B);
blueAndSentimentalItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Library library = new Library();
setScore(library.loadScore("blue_and_sentimental.xml"));
}
});
fakebookMenu.add(blueAndSentimentalItem);
libraryMenu.add(fakebookMenu);
return libraryMenu;
}
Object getScoreObjectAtCaretPosition() {
return transcriber.getScoreObjectAtIndex(textPane.getCaretPosition());
}
Sign getSignAtCaretPosition() {
if (score != null)
return transcriber.getSignAtIndex(textPane.getCaretPosition());
return null;
}
/** Retranscribes the score to braille and preserves logical caret position.
* <p>
* In the event that transcription options are changed (for example switching
* to a different format or changing line width) or score objects are changed
* or removed (fingering editing) this method needs to be called to update
* the braille representation. It will try to preserve the logical score
* position so that upon switching of formats the caret will end up on
* the same note it used to be in the previous format used.
*/
void triggerTranscription() {
int position = textPane.getCaretPosition();
final Object object = getScoreObjectAtCaretPosition();
setScore(score);
if (object != null) {
final int objectPosition = transcriber.getIndexOfScoreObject(object);
if (objectPosition != -1) position = objectPosition;
}
textPane.setCaretPosition(position);
}
}