1 package camidion.chordhelper.midieditor;
3 import java.awt.Component;
4 import java.awt.Container;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.Insets;
8 import java.awt.datatransfer.DataFlavor;
9 import java.awt.event.ActionEvent;
10 import java.awt.event.ComponentAdapter;
11 import java.awt.event.ComponentEvent;
12 import java.awt.event.ComponentListener;
13 import java.awt.event.ItemEvent;
14 import java.awt.event.ItemListener;
15 import java.awt.event.MouseEvent;
17 import java.io.FileOutputStream;
18 import java.io.IOException;
19 import java.nio.charset.Charset;
20 import java.security.AccessControlException;
21 import java.util.Arrays;
22 import java.util.EventObject;
23 import java.util.List;
27 import javax.sound.midi.InvalidMidiDataException;
28 import javax.sound.midi.MidiChannel;
29 import javax.sound.midi.MidiEvent;
30 import javax.sound.midi.MidiMessage;
31 import javax.sound.midi.Sequence;
32 import javax.sound.midi.Sequencer;
33 import javax.sound.midi.ShortMessage;
34 import javax.swing.AbstractAction;
35 import javax.swing.AbstractCellEditor;
36 import javax.swing.Action;
37 import javax.swing.Box;
38 import javax.swing.BoxLayout;
39 import javax.swing.DefaultCellEditor;
40 import javax.swing.Icon;
41 import javax.swing.JButton;
42 import javax.swing.JCheckBox;
43 import javax.swing.JComboBox;
44 import javax.swing.JDialog;
45 import javax.swing.JFileChooser;
46 import javax.swing.JLabel;
47 import javax.swing.JOptionPane;
48 import javax.swing.JPanel;
49 import javax.swing.JScrollPane;
50 import javax.swing.JSplitPane;
51 import javax.swing.JTable;
52 import javax.swing.JToggleButton;
53 import javax.swing.ListSelectionModel;
54 import javax.swing.TransferHandler;
55 import javax.swing.border.EtchedBorder;
56 import javax.swing.event.ListSelectionEvent;
57 import javax.swing.event.ListSelectionListener;
58 import javax.swing.event.TableModelEvent;
59 import javax.swing.filechooser.FileNameExtensionFilter;
60 import javax.swing.table.JTableHeader;
61 import javax.swing.table.TableCellEditor;
62 import javax.swing.table.TableCellRenderer;
63 import javax.swing.table.TableColumn;
64 import javax.swing.table.TableColumnModel;
65 import javax.swing.table.TableModel;
67 import camidion.chordhelper.ButtonIcon;
68 import camidion.chordhelper.ChordHelperApplet;
69 import camidion.chordhelper.mididevice.MidiSequencerModel;
70 import camidion.chordhelper.mididevice.VirtualMidiDevice;
71 import camidion.chordhelper.music.MIDISpec;
74 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
77 * Copyright (C) 2006-2016 Akiyoshi Kamide
78 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
80 public class MidiSequenceEditorDialog extends JDialog {
84 public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
86 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
87 putValue(Action.SHORT_DESCRIPTION, tooltip);
90 public void actionPerformed(ActionEvent e) {
91 if( isVisible() ) toFront(); else setVisible(true);
96 * エラーメッセージダイアログを表示します。
97 * @param message エラーメッセージ
99 public void showError(String message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
101 * 警告メッセージダイアログを表示します。
102 * @param message 警告メッセージ
104 public void showWarning(String message) { showMessage(message, JOptionPane.WARNING_MESSAGE); }
105 private void showMessage(String message, int messageType) {
106 JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, messageType);
110 * @param message 確認メッセージ
111 * @return 確認OKのときtrue
113 public boolean confirm(String message) {
114 return JOptionPane.showConfirmDialog(this, message, ChordHelperApplet.VersionInfo.NAME,
115 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION ;
119 * ドロップされた複数のMIDIファイルを読み込むハンドラー
121 public final TransferHandler transferHandler = new TransferHandler() {
123 public boolean canImport(TransferSupport support) {
124 return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
126 @SuppressWarnings("unchecked")
128 public boolean importData(TransferSupport support) {
130 loadAndPlay((List<File>)support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
132 } catch (Exception e) {
134 showError(e.getMessage());
141 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
142 * すでに再生されていた場合、このエディタダイアログを表示します。
144 * @param fileList 読み込むMIDIファイルのリスト
145 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
146 * @see #loadAndPlay(File)
148 public void loadAndPlay(List<File> fileList) {
149 int indexOfAddedTop = -1;
150 PlaylistTableModel playlist = sequenceListTable.getModel();
152 indexOfAddedTop = playlist.addSequences(fileList);
153 } catch(IOException|InvalidMidiDataException e) {
154 showWarning(e.getMessage());
155 } catch(AccessControlException e) {
157 showError(e.getMessage());
159 MidiSequencerModel sequencerModel = playlist.getSequencerModel();
160 if( sequencerModel.getSequencer().isRunning() ) {
161 String command = (String)openAction.getValue(Action.NAME);
162 openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
165 if( indexOfAddedTop >= 0 ) {
167 playlist.loadToSequencer(indexOfAddedTop);
168 } catch (InvalidMidiDataException ex) {
169 ex.printStackTrace();
170 showError(ex.getMessage());
173 sequencerModel.start();
177 * 1件のMIDIファイルを読み込み、再生されていなかったら再生します。
178 * すでに再生されていた場合、このエディタダイアログを表示します。
180 * @param file 読み込むMIDIファイル
181 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
182 * @see #loadAndPlay(List) loadAndPlay(List<File>)
184 public void loadAndPlay(File file) throws InvalidMidiDataException {
185 loadAndPlay(Arrays.asList(file));
188 private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
189 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
191 * 新しいMIDIシーケンスを生成するダイアログ
193 public NewSequenceDialog newSequenceDialog;
197 public Base64Dialog base64Dialog = new Base64Dialog(this);
199 * プレイリストビュー(シーケンスリスト)
201 public SequenceListTable sequenceListTable;
203 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
205 private TrackListTable trackListTable;
207 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
209 private EventListTable eventListTable;
211 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
213 public MidiEventDialog eventDialog = new MidiEventDialog();
214 private VirtualMidiDevice outputMidiDevice;
216 * プレイリストビュー(シーケンスリスト)
218 public class SequenceListTable extends JTable {
220 * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
222 private MidiFileChooser midiFileChooser;
224 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
226 private Action base64EncodeAction;
229 * @param model プレイリストデータモデル
231 public SequenceListTable(PlaylistTableModel model) {
232 super(model, null, model.sequenceListSelectionModel);
234 midiFileChooser = new MidiFileChooser();
236 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
237 // アプレットの場合、Webクライアントマシンのローカルファイルには
238 // アクセスできないので、ファイル選択ダイアログは使用不可。
239 midiFileChooser = null;
242 new PlayButtonCellEditor();
243 new PositionCellEditor();
246 int column = PlaylistTableModel.Column.CHARSET.ordinal();
247 TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
248 Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
249 for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
251 getColumnModel().getColumn(column).setCellEditor(ce);
252 setAutoCreateColumnsFromModel(false);
254 // Base64エンコードアクションの生成
255 if( base64Dialog.isBase64Available() ) {
256 base64EncodeAction = new AbstractAction("Base64") {
258 String tooltip = "Base64 text conversion - Base64テキスト変換";
259 putValue(Action.SHORT_DESCRIPTION, tooltip);
262 public void actionPerformed(ActionEvent e) {
263 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
265 String filename = null;
267 data = mstm.getMIDIdata();
268 filename = mstm.getFilename();
270 base64Dialog.setMIDIData(data, filename);
271 base64Dialog.setVisible(true);
275 TableColumnModel colModel = getColumnModel();
276 for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
277 TableColumn tc = colModel.getColumn(c.ordinal());
278 tc.setPreferredWidth(c.preferredWidth);
279 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
282 private TableColumn lengthColumn;
284 public void tableChanged(TableModelEvent event) {
285 super.tableChanged(event);
288 if( lengthColumn != null ) {
289 int sec = getModel().getTotalSeconds();
290 String title = PlaylistTableModel.Column.LENGTH.title;
291 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
292 lengthColumn.setHeaderValue(title);
295 // シーケンス削除時など、合計シーケンス長が変わっても
296 // 列モデルからではヘッダタイトルが再描画されないことがある。
297 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
298 JTableHeader th = getTableHeader();
299 if( th != null ) th.repaint();
302 * 時間位置表示セルエディタ(ダブルクリック専用)
304 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
305 public PositionCellEditor() {
306 int column = PlaylistTableModel.Column.POSITION.ordinal();
307 TableColumn tc = getColumnModel().getColumn(column);
308 tc.setCellEditor(this);
311 * セルをダブルクリックしたときだけ編集モードに入るようにします。
312 * @param e イベント(マウスイベント)
313 * @return 編集可能になったらtrue
316 public boolean isCellEditable(EventObject e) {
317 // マウスイベント以外のイベントでは編集不可
318 if( ! (e instanceof MouseEvent) ) return false;
319 return ((MouseEvent)e).getClickCount() == 2;
322 public Object getCellEditorValue() { return null; }
324 * 編集モード時のコンポーネントを返すタイミングで
325 * そのシーケンスをシーケンサーにロードしたあと、
330 public Component getTableCellEditorComponent(
331 JTable table, Object value, boolean isSelected, int row, int column
334 getModel().loadToSequencer(row);
335 } catch (InvalidMidiDataException ex) {
336 ex.printStackTrace();
337 showError(ex.getMessage());
339 fireEditingStopped();
346 private class PlayButtonCellEditor extends AbstractCellEditor
347 implements TableCellEditor, TableCellRenderer
349 private JToggleButton playButton = new JToggleButton(
350 getModel().getSequencerModel().startStopAction
352 { setMargin(ZERO_INSETS); }
354 public PlayButtonCellEditor() {
355 int column = PlaylistTableModel.Column.PLAY.ordinal();
356 TableColumn tc = getColumnModel().getColumn(column);
357 tc.setCellRenderer(this);
358 tc.setCellEditor(this);
363 * <p>この実装では、クリックしたセルのシーケンスが
365 * trueを返してプレイボタンを押せるようにします。
366 * そうでない場合はプレイボタンのないセルなので、
367 * ダブルクリックされたときだけtrueを返します。
371 public boolean isCellEditable(EventObject e) {
372 // マウスイベント以外はデフォルトメソッドにお任せ
373 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
374 fireEditingStopped();
375 MouseEvent me = (MouseEvent)e;
378 int row = rowAtPoint(me.getPoint());
379 if( row < 0 ) return false;
380 PlaylistTableModel model = getModel();
381 if( row >= model.getRowCount() ) return false;
383 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
384 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
385 return model.getSequenceList().get(row).isOnSequencer() || me.getClickCount() == 2;
388 public Object getCellEditorValue() { return null; }
392 * <p>この実装では、行の表すシーケンスが
393 * シーケンサーにロードされている場合にプレイボタンを返します。
395 * そのシーケンスをシーケンサーにロードしてnullを返します。
399 public Component getTableCellEditorComponent(
400 JTable table, Object value, boolean isSelected, int row, int column
402 fireEditingStopped();
403 PlaylistTableModel model = getModel();
404 if( model.getSequenceList().get(row).isOnSequencer() ) return playButton;
406 model.loadToSequencer(row);
407 } catch (InvalidMidiDataException ex) {
408 ex.printStackTrace();
409 showError(ex.getMessage());
414 public Component getTableCellRendererComponent(
415 JTable table, Object value, boolean isSelected,
416 boolean hasFocus, int row, int column
418 PlaylistTableModel model = getModel();
419 if(model.getSequenceList().get(row).isOnSequencer()) return playButton;
420 Class<?> cc = model.getColumnClass(column);
421 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
422 return defaultRenderer.getTableCellRendererComponent(
423 table, value, isSelected, hasFocus, row, column
428 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
433 public PlaylistTableModel getModel() { return (PlaylistTableModel)super.getModel(); }
437 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
438 "Delete", MidiSequenceEditorDialog.deleteIcon,
439 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
442 public void actionPerformed(ActionEvent event) {
443 PlaylistTableModel model = getModel();
444 if( midiFileChooser != null ) {
445 if( model.getSelectedSequenceModel().isModified() ) {
447 "Selected MIDI sequence not saved - delete it ?\n" +
448 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
449 if( ! confirm(message) ) return;
453 model.removeSelectedSequence();
454 } catch (InvalidMidiDataException ex) {
455 ex.printStackTrace();
456 showError(ex.getMessage());
461 * ファイル選択ダイアログ(アプレットでは使用不可)
463 private class MidiFileChooser extends JFileChooser {
465 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
470 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
472 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
475 public void actionPerformed(ActionEvent event) {
476 PlaylistTableModel playlistModel = getModel();
477 SequenceTrackListTableModel sequenceModel = playlistModel.getSelectedSequenceModel();
478 String fn = sequenceModel.getFilename();
479 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
480 if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
481 File f = getSelectedFile();
484 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
486 try ( FileOutputStream out = new FileOutputStream(f) ) {
487 out.write(sequenceModel.getMIDIdata());
488 sequenceModel.setModified(false);
489 playlistModel.fireSequenceModified(sequenceModel, false);
491 catch( IOException ex ) {
492 ex.printStackTrace();
493 showError( ex.getMessage() );
500 public Action openMidiFileAction = new AbstractAction("Open") {
501 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
503 public void actionPerformed(ActionEvent event) {
504 if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
506 loadAndPlay(getSelectedFile());
507 } catch (InvalidMidiDataException ex) {
508 ex.printStackTrace();
509 showError(ex.getMessage());
517 * シーケンス(トラックリスト)テーブルビュー
519 public class TrackListTable extends JTable {
521 * トラックリストテーブルビューを構築します。
522 * @param model シーケンス(トラックリスト)データモデル
524 public TrackListTable(SequenceTrackListTableModel model) {
525 super(model, null, model.getSelectionModel());
527 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
528 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
529 TableColumn tc = getColumnModel().getColumn(colIndex);
530 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
532 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
535 setAutoCreateColumnsFromModel(false);
537 titleLabel = new TitleLabel();
538 model.getParent().sequenceListSelectionModel.addListSelectionListener(titleLabel);
539 TableColumnModel colModel = getColumnModel();
540 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
541 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
544 * このテーブルビューが表示するデータを提供する
545 * シーケンス(トラックリスト)データモデルを返します。
546 * @return シーケンス(トラックリスト)データモデル
549 public SequenceTrackListTableModel getModel() {
550 return (SequenceTrackListTableModel) super.getModel();
555 TitleLabel titleLabel;
557 * 親テーブルの選択シーケンスの変更に反応する
560 private class TitleLabel extends JLabel implements ListSelectionListener {
561 private static final String TITLE = "Tracks";
562 public TitleLabel() { setText(TITLE); }
564 public void valueChanged(ListSelectionEvent event) {
565 if( event.getValueIsAdjusting() ) return;
566 SequenceTrackListTableModel oldModel = getModel();
567 SequenceTrackListTableModel newModel = oldModel.getParent().getSelectedSequenceModel();
568 if( oldModel == newModel ) return;
570 // MIDIチャンネル選択中のときはキャンセルする
573 int index = oldModel.getParent().sequenceListSelectionModel.getMinSelectionIndex();
575 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
577 if( newModel == null ) {
578 newModel = oldModel.getParent().emptyTrackListTableModel;
579 addTrackAction.setEnabled(false);
582 addTrackAction.setEnabled(true);
584 oldModel.getSelectionModel().removeListSelectionListener(trackSelectionListener);
586 setSelectionModel(newModel.getSelectionModel());
587 newModel.getSelectionModel().addListSelectionListener(trackSelectionListener);
588 trackSelectionListener.valueChanged(null);
594 ListSelectionListener trackSelectionListener = new ListSelectionListener() {
596 public void valueChanged(ListSelectionEvent e) {
597 if( e != null && e.getValueIsAdjusting() ) return;
598 ListSelectionModel tlsm = getModel().getSelectionModel();
599 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
600 eventListTable.titleLabel.update(tlsm, getModel());
606 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
608 * シーケンサーからこのモデルが外された場合がこれに該当します。
612 public void tableChanged(TableModelEvent e) {
613 super.tableChanged(e);
617 * このトラックリストテーブルが編集モードになっていたら解除します。
619 private void cancelCellEditing() {
620 TableCellEditor currentCellEditor = getCellEditor();
621 if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
626 Action addTrackAction = new AbstractAction("New") {
628 String tooltip = "Append new track - 新しいトラックの追加";
629 putValue(Action.SHORT_DESCRIPTION, tooltip);
633 public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
638 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
640 String tooltip = "Delete selected track - 選択したトラックを削除";
641 putValue(Action.SHORT_DESCRIPTION, tooltip);
645 public void actionPerformed(ActionEvent e) {
646 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
647 if( confirm(message) ) getModel().deleteSelectedTracks();
653 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
655 public class EventListTable extends JTable {
657 * 新しいイベントリストテーブルを構築します。
658 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
659 * トラックを切り替えたいときは {@link #setModel(TableModel)}
660 * でデータモデルを異なるトラックのものに切り替えます。
663 * @param model トラック(イベントリスト)データモデル
665 public EventListTable(TrackEventListTableModel model) {
666 super(model, null, model.getSelectionModel());
669 eventCellEditor = new MidiEventCellEditor();
670 setAutoCreateColumnsFromModel(false);
672 eventSelectionListener = new EventSelectionListener();
673 titleLabel = new TitleLabel();
675 TableColumnModel colModel = getColumnModel();
676 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
677 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
680 * このテーブルビューが表示するデータを提供する
681 * トラック(イベントリスト)データモデルを返します。
682 * @return トラック(イベントリスト)データモデル
685 public TrackEventListTableModel getModel() {
686 return (TrackEventListTableModel) super.getModel();
691 TitleLabel titleLabel;
693 * 親テーブルの選択トラックの変更に反応する
696 private class TitleLabel extends JLabel {
697 private static final String TITLE = "MIDI Events";
698 public TitleLabel() { super(TITLE); }
699 public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
701 TrackEventListTableModel oldTrackModel = getModel();
702 int index = tlsm.getMinSelectionIndex();
704 text = String.format(TITLE+" - track No.%d", index);
707 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
708 if( oldTrackModel == newTrackModel )
710 if( newTrackModel == null ) {
711 newTrackModel = getModel().getParent().getParent().emptyEventListTableModel;
712 queryJumpEventAction.setEnabled(false);
713 queryAddEventAction.setEnabled(false);
715 queryPasteEventAction.setEnabled(false);
716 copyEventAction.setEnabled(false);
717 deleteEventAction.setEnabled(false);
718 cutEventAction.setEnabled(false);
721 queryJumpEventAction.setEnabled(true);
722 queryAddEventAction.setEnabled(true);
724 oldTrackModel.getSelectionModel().removeListSelectionListener(eventSelectionListener);
725 setModel(newTrackModel);
726 setSelectionModel(newTrackModel.getSelectionModel());
727 newTrackModel.getSelectionModel().addListSelectionListener(eventSelectionListener);
734 private EventSelectionListener eventSelectionListener;
738 private class EventSelectionListener implements ListSelectionListener {
739 public EventSelectionListener() {
740 getModel().getSelectionModel().addListSelectionListener(this);
743 public void valueChanged(ListSelectionEvent e) {
744 if( e.getValueIsAdjusting() )
746 if( getSelectionModel().isSelectionEmpty() ) {
747 queryPasteEventAction.setEnabled(false);
748 copyEventAction.setEnabled(false);
749 deleteEventAction.setEnabled(false);
750 cutEventAction.setEnabled(false);
753 copyEventAction.setEnabled(true);
754 deleteEventAction.setEnabled(true);
755 cutEventAction.setEnabled(true);
756 TrackEventListTableModel trackModel = getModel();
757 int minIndex = getSelectionModel().getMinSelectionIndex();
758 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
759 if( midiEvent != null ) {
760 MidiMessage msg = midiEvent.getMessage();
761 if( msg instanceof ShortMessage ) {
762 ShortMessage sm = (ShortMessage)msg;
763 int cmd = sm.getCommand();
764 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
766 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
767 int ch = sm.getChannel();
768 int note = sm.getData1();
769 int vel = sm.getData2();
770 outMidiChannels[ch].noteOn(note, vel);
771 outMidiChannels[ch].noteOff(note, vel);
775 if( pairNoteOnOffModel.isSelected() ) {
776 int maxIndex = getSelectionModel().getMaxSelectionIndex();
778 for( int i=minIndex; i<=maxIndex; i++ ) {
779 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
780 partnerIndex = trackModel.getIndexOfPartnerFor(i);
781 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
782 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
789 * Pair noteON/OFF トグルボタンモデル
791 private JToggleButton.ToggleButtonModel
792 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
794 addItemListener(new ItemListener() {
795 public void itemStateChanged(ItemEvent e) {
796 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
802 private class EventEditContext {
806 private TrackEventListTableModel trackModel;
810 private TickPositionModel tickPositionModel = new TickPositionModel();
814 private MidiEvent selectedMidiEvent = null;
818 private int selectedIndex = -1;
822 private long currentTick = 0;
824 * 上書きして削除対象にする変更前イベント(null可)
826 private MidiEvent[] midiEventsToBeOverwritten;
828 * 選択したイベントを入力ダイアログなどに反映します。
829 * @param model 対象データモデル
831 private void setSelectedEvent(TrackEventListTableModel trackModel) {
832 this.trackModel = trackModel;
833 SequenceTrackListTableModel sequenceTableModel = trackModel.getParent();
834 int ppq = sequenceTableModel.getSequence().getResolution();
835 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
836 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
838 selectedIndex = trackModel.getSelectionModel().getMinSelectionIndex();
839 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
840 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
841 tickPositionModel.setTickPosition(currentTick);
843 public void setupForEdit(TrackEventListTableModel trackModel) {
844 MidiEvent partnerEvent = null;
845 eventDialog.midiMessageForm.setMessage(
846 selectedMidiEvent.getMessage(),
847 trackModel.getParent().charset
849 if( eventDialog.midiMessageForm.isNote() ) {
850 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
851 if( partnerIndex < 0 ) {
852 eventDialog.midiMessageForm.durationForm.setDuration(0);
855 partnerEvent = trackModel.getMidiEvent(partnerIndex);
856 long partnerTick = partnerEvent.getTick();
857 long duration = currentTick > partnerTick ?
858 currentTick - partnerTick : partnerTick - currentTick ;
859 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
862 if(partnerEvent == null)
863 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
865 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
867 private Action jumpEventAction = new AbstractAction() {
868 { putValue(NAME,"Jump"); }
869 public void actionPerformed(ActionEvent e) {
870 long tick = tickPositionModel.getTickPosition();
871 scrollToEventAt(tick);
872 eventDialog.setVisible(false);
876 private Action pasteEventAction = new AbstractAction() {
877 { putValue(NAME,"Paste"); }
878 public void actionPerformed(ActionEvent e) {
879 long tick = tickPositionModel.getTickPosition();
880 clipBoard.paste(trackModel, tick);
881 scrollToEventAt(tick);
882 // ペーストで曲の長さが変わったことをプレイリストに通知
883 SequenceTrackListTableModel seqModel = trackModel.getParent();
884 seqModel.getParent().fireSequenceModified(seqModel, true);
885 eventDialog.setVisible(false);
889 private boolean applyEvent() {
890 long tick = tickPositionModel.getTickPosition();
891 MidiMessageForm form = eventDialog.midiMessageForm;
892 SequenceTrackListTableModel seqModel = trackModel.getParent();
893 MidiEvent newMidiEvent = new MidiEvent(form.getMessage(seqModel.charset), tick);
894 if( midiEventsToBeOverwritten != null ) {
895 // 上書き消去するための選択済イベントがあった場合
896 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
898 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
899 System.out.println("addMidiEvent failure");
902 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
903 ShortMessage sm = form.createPartnerMessage();
905 scrollToEventAt( tick );
907 int duration = form.durationForm.getDuration();
908 if( form.isNote(false) ) {
909 duration = -duration;
911 long partnerTick = tick + (long)duration;
912 if( partnerTick < 0L ) partnerTick = 0L;
913 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
914 if( ! trackModel.addMidiEvent(partner) ) {
915 System.out.println("addMidiEvent failure (note on/off partner message)");
917 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
920 seqModel.getParent().fireSequenceModified(seqModel, true);
921 eventDialog.setVisible(false);
925 private EventEditContext editContext = new EventEditContext();
927 * 指定のTick位置へジャンプするアクション
929 Action queryJumpEventAction = new AbstractAction() {
931 putValue(NAME,"Jump to ...");
934 public void actionPerformed(ActionEvent e) {
935 editContext.setSelectedEvent(getModel());
936 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
942 Action queryAddEventAction = new AbstractAction() {
944 putValue(NAME,"New");
947 public void actionPerformed(ActionEvent e) {
948 TrackEventListTableModel model = getModel();
949 editContext.setSelectedEvent(model);
950 editContext.midiEventsToBeOverwritten = null;
951 eventDialog.openEventForm(
953 eventCellEditor.applyEventAction,
959 * MIDIイベントのコピー&ペーストを行うためのクリップボード
961 private class LocalClipBoard {
962 private MidiEvent copiedEventsToPaste[];
963 private int copiedEventsPPQ = 0;
964 public void copy(TrackEventListTableModel model, boolean withRemove) {
965 copiedEventsToPaste = model.getSelectedMidiEvents();
966 copiedEventsPPQ = model.getParent().getSequence().getResolution();
967 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
968 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
969 queryPasteEventAction.setEnabled(en);
971 public void cut(TrackEventListTableModel model) {copy(model,true);}
972 public void copy(TrackEventListTableModel model){copy(model,false);}
973 public void paste(TrackEventListTableModel model, long tick) {
974 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
977 private LocalClipBoard clipBoard = new LocalClipBoard();
979 * 指定のTick位置へ貼り付けるアクション
981 Action queryPasteEventAction = new AbstractAction() {
983 putValue(NAME,"Paste to ...");
986 public void actionPerformed(ActionEvent e) {
987 editContext.setSelectedEvent(getModel());
988 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
994 public Action cutEventAction = new AbstractAction("Cut") {
999 public void actionPerformed(ActionEvent e) {
1000 TrackEventListTableModel model = getModel();
1001 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
1003 clipBoard.cut(model);
1009 public Action copyEventAction = new AbstractAction("Copy") {
1014 public void actionPerformed(ActionEvent e) {
1015 clipBoard.copy(getModel());
1021 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
1026 public void actionPerformed(ActionEvent e) {
1027 TrackEventListTableModel model = getModel();
1028 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1030 model.removeSelectedMidiEvents();
1036 private MidiEventCellEditor eventCellEditor;
1040 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1042 * MIDIイベントセルエディタを構築します。
1044 public MidiEventCellEditor() {
1045 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
1046 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1047 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1048 getColumnModel().getColumn(index).setCellEditor(this);
1051 * セルをダブルクリックしないと編集できないようにします。
1052 * @param e イベント(マウスイベント)
1053 * @return 編集可能になったらtrue
1056 public boolean isCellEditable(EventObject e) {
1057 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1058 return ((MouseEvent)e).getClickCount() == 2;
1061 public Object getCellEditorValue() { return null; }
1063 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1065 private ComponentListener dialogComponentListener = new ComponentAdapter() {
1067 public void componentHidden(ComponentEvent e) {
1068 fireEditingCanceled();
1070 eventDialog.removeComponentListener(this);
1076 private Action editEventAction = new AbstractAction() {
1077 public void actionPerformed(ActionEvent e) {
1078 TrackEventListTableModel model = getModel();
1079 editContext.setSelectedEvent(model);
1080 if( editContext.selectedMidiEvent == null )
1082 editContext.setupForEdit(model);
1083 eventDialog.addComponentListener(dialogComponentListener);
1084 eventDialog.openEventForm("Change MIDI event", applyEventAction);
1090 private JButton editEventButton = new JButton(editEventAction){{
1091 setHorizontalAlignment(JButton.LEFT);
1094 public Component getTableCellEditorComponent(
1095 JTable table, Object value, boolean isSelected, int row, int column
1097 editEventButton.setText(value.toString());
1098 return editEventButton;
1101 * 入力したイベントを反映するアクション
1103 private Action applyEventAction = new AbstractAction() {
1105 putValue(NAME,"OK");
1107 public void actionPerformed(ActionEvent e) {
1108 if( editContext.applyEvent() ) fireEditingStopped();
1113 * スクロール可能なMIDIイベントテーブルビュー
1115 private JScrollPane scrollPane = new JScrollPane(this);
1117 * 指定の MIDI tick のイベントへスクロールします。
1118 * @param tick MIDI tick
1120 public void scrollToEventAt(long tick) {
1121 int index = getModel().tickToIndex(tick);
1122 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1123 getSelectionModel().setSelectionInterval(index, index);
1128 * 新しい {@link MidiSequenceEditorDialog} を構築します。
1129 * @param playlistTableModel このエディタが参照するプレイリストモデル
1130 * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1132 public MidiSequenceEditorDialog(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice) {
1133 this.outputMidiDevice = outputMidiDevice;
1134 sequenceListTable = new SequenceListTable(playlistTableModel);
1135 trackListTable = new TrackListTable(
1136 new SequenceTrackListTableModel(playlistTableModel, null, null)
1138 eventListTable = new EventListTable(new TrackEventListTableModel(trackListTable.getModel(), null));
1139 newSequenceDialog = new NewSequenceDialog(playlistTableModel, outputMidiDevice);
1140 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1141 setBounds( 150, 200, 900, 500 );
1142 setLayout(new FlowLayout());
1143 setTransferHandler(transferHandler);
1146 JPanel playlistPanel = new JPanel() {{
1147 JPanel playlistOperationPanel = new JPanel() {{
1148 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1149 add(Box.createRigidArea(new Dimension(10, 0)));
1150 add(new JButton(newSequenceDialog.openAction) {{ setMargin(ZERO_INSETS); }});
1151 if( sequenceListTable.midiFileChooser != null ) {
1152 add( Box.createRigidArea(new Dimension(5, 0)) );
1153 add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {
1154 { setMargin(ZERO_INSETS); }
1157 if(sequenceListTable.base64EncodeAction != null) {
1158 add(Box.createRigidArea(new Dimension(5, 0)));
1159 add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1161 add(Box.createRigidArea(new Dimension(5, 0)));
1162 PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
1163 add(new JButton(playlistTableModel.moveToTopAction) {{ setMargin(ZERO_INSETS); }});
1164 add(Box.createRigidArea(new Dimension(5, 0)));
1165 add(new JButton(playlistTableModel.moveToBottomAction) {{ setMargin(ZERO_INSETS); }});
1166 if( sequenceListTable.midiFileChooser != null ) {
1167 add(Box.createRigidArea(new Dimension(5, 0)));
1168 add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {
1169 { setMargin(ZERO_INSETS); }
1172 add( Box.createRigidArea(new Dimension(5, 0)) );
1173 add(new JButton(sequenceListTable.deleteSequenceAction) {{ setMargin(ZERO_INSETS); }});
1174 add( Box.createRigidArea(new Dimension(5, 0)) );
1175 add(new SequencerSpeedSlider(playlistTableModel.getSequencerModel().speedSliderModel));
1176 add( Box.createRigidArea(new Dimension(5, 0)) );
1178 setBorder(new EtchedBorder());
1179 MidiSequencerModel sequencerModel = sequenceListTable.getModel().getSequencerModel();
1180 add(new JLabel("Sync Master"));
1181 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1182 add(new JLabel("Slave"));
1183 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1186 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1187 add(new JScrollPane(sequenceListTable));
1188 add(Box.createRigidArea(new Dimension(0, 10)));
1189 add(playlistOperationPanel);
1190 add(Box.createRigidArea(new Dimension(0, 10)));
1192 JPanel trackListPanel = new JPanel() {{
1193 JPanel trackListOperationPanel = new JPanel() {{
1194 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ZERO_INSETS); }});
1195 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
1197 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1198 add(trackListTable.titleLabel);
1199 add(Box.createRigidArea(new Dimension(0, 5)));
1200 add(new JScrollPane(trackListTable));
1201 add(Box.createRigidArea(new Dimension(0, 5)));
1202 add(trackListOperationPanel);
1204 JPanel eventListPanel = new JPanel() {{
1205 JPanel eventListOperationPanel = new JPanel() {{
1206 add(new JCheckBox("Pair NoteON/OFF") {{
1207 setModel(eventListTable.pairNoteOnOffModel);
1208 setToolTipText("NoteON/OFFをペアで同時選択する");
1210 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ZERO_INSETS); }});
1211 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ZERO_INSETS); }});
1212 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ZERO_INSETS); }});
1213 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ZERO_INSETS); }});
1214 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ZERO_INSETS); }});
1215 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ZERO_INSETS); }});
1217 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1218 add(eventListTable.titleLabel);
1219 add(eventListTable.scrollPane);
1220 add(eventListOperationPanel);
1222 Container cp = getContentPane();
1223 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1224 cp.add(Box.createVerticalStrut(2));
1226 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1227 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1228 setDividerLocation(300);
1231 setDividerLocation(160);