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.datatransfer.Transferable;
10 import java.awt.dnd.DnDConstants;
11 import java.awt.dnd.DropTarget;
12 import java.awt.dnd.DropTargetAdapter;
13 import java.awt.dnd.DropTargetDragEvent;
14 import java.awt.dnd.DropTargetDropEvent;
15 import java.awt.dnd.DropTargetListener;
16 import java.awt.event.ActionEvent;
17 import java.awt.event.ComponentAdapter;
18 import java.awt.event.ComponentEvent;
19 import java.awt.event.ComponentListener;
20 import java.awt.event.ItemEvent;
21 import java.awt.event.ItemListener;
22 import java.awt.event.MouseEvent;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.nio.charset.Charset;
27 import java.security.AccessControlException;
28 import java.util.Arrays;
29 import java.util.EventObject;
30 import java.util.List;
34 import javax.sound.midi.InvalidMidiDataException;
35 import javax.sound.midi.MidiChannel;
36 import javax.sound.midi.MidiEvent;
37 import javax.sound.midi.MidiMessage;
38 import javax.sound.midi.Sequence;
39 import javax.sound.midi.Sequencer;
40 import javax.sound.midi.ShortMessage;
41 import javax.swing.AbstractAction;
42 import javax.swing.AbstractCellEditor;
43 import javax.swing.Action;
44 import javax.swing.Box;
45 import javax.swing.BoxLayout;
46 import javax.swing.DefaultCellEditor;
47 import javax.swing.Icon;
48 import javax.swing.JButton;
49 import javax.swing.JCheckBox;
50 import javax.swing.JComboBox;
51 import javax.swing.JDialog;
52 import javax.swing.JFileChooser;
53 import javax.swing.JLabel;
54 import javax.swing.JOptionPane;
55 import javax.swing.JPanel;
56 import javax.swing.JScrollPane;
57 import javax.swing.JSplitPane;
58 import javax.swing.JTable;
59 import javax.swing.JToggleButton;
60 import javax.swing.ListSelectionModel;
61 import javax.swing.event.ListSelectionEvent;
62 import javax.swing.event.ListSelectionListener;
63 import javax.swing.event.TableModelEvent;
64 import javax.swing.filechooser.FileNameExtensionFilter;
65 import javax.swing.table.JTableHeader;
66 import javax.swing.table.TableCellEditor;
67 import javax.swing.table.TableCellRenderer;
68 import javax.swing.table.TableColumn;
69 import javax.swing.table.TableColumnModel;
70 import javax.swing.table.TableModel;
72 import camidion.chordhelper.ButtonIcon;
73 import camidion.chordhelper.ChordHelperApplet;
74 import camidion.chordhelper.mididevice.MidiSequencerModel;
75 import camidion.chordhelper.mididevice.VirtualMidiDevice;
76 import camidion.chordhelper.music.MIDISpec;
79 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
82 * Copyright (C) 2006-2016 Akiyoshi Kamide
83 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
85 public class MidiSequenceEditor extends JDialog {
89 public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
91 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
92 putValue(Action.SHORT_DESCRIPTION, tooltip);
95 public void actionPerformed(ActionEvent e) {
96 if( isVisible() ) toFront(); else setVisible(true);
101 * エラーメッセージダイアログを表示します。
102 * @param message エラーメッセージ
104 public void showError(String message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
106 * 警告メッセージダイアログを表示します。
107 * @param message 警告メッセージ
109 public void showWarning(String message) { showMessage(message, JOptionPane.WARNING_MESSAGE); }
110 private void showMessage(String message, int messageType) {
111 JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, messageType);
115 * @param message 確認メッセージ
116 * @return 確認OKのときtrue
118 public boolean confirm(String message) {
119 return JOptionPane.showConfirmDialog(this, message, ChordHelperApplet.VersionInfo.NAME,
120 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION ;
124 * ドロップされた複数のMIDIファイルを読み込むリスナー
126 public final DropTargetListener dropTargetListener = new DropTargetAdapter() {
128 public void dragEnter(DropTargetDragEvent event) {
129 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
130 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
134 @SuppressWarnings("unchecked")
135 public void drop(DropTargetDropEvent event) {
136 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
138 if ( (event.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
139 Transferable t = event.getTransferable();
140 if( t.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
141 loadAndPlay((List<File>)t.getTransferData(DataFlavor.javaFileListFlavor));
142 event.dropComplete(true);
147 catch (Exception ex) {
148 ex.printStackTrace();
150 event.dropComplete(false);
155 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
156 * すでに再生されていた場合、このエディタダイアログを表示します。
158 * @param fileList 読み込むMIDIファイルのリスト
159 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
160 * @see #loadAndPlay(File)
162 public void loadAndPlay(List<File> fileList) {
163 int indexOfAddedTop = -1;
164 PlaylistTableModel playlist = sequenceListTable.getModel();
166 indexOfAddedTop = playlist.addSequences(fileList);
167 } catch(IOException|InvalidMidiDataException e) {
168 showWarning(e.getMessage());
169 } catch(AccessControlException e) {
170 showError(e.getMessage());
173 if( playlist.sequencerModel.getSequencer().isRunning() ) {
174 String command = (String)openAction.getValue(Action.NAME);
175 openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
178 if( indexOfAddedTop >= 0 ) {
180 playlist.loadToSequencer(indexOfAddedTop);
181 } catch (InvalidMidiDataException ex) {
182 ex.printStackTrace();
183 showError(ex.getMessage());
186 playlist.sequencerModel.start();
190 * 1件のMIDIファイルを読み込み、再生されていなかったら再生します。
191 * すでに再生されていた場合、このエディタダイアログを表示します。
193 * @param file 読み込むMIDIファイル
194 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
195 * @see #loadAndPlay(List) loadAndPlay(List<File>)
197 public void loadAndPlay(File file) throws InvalidMidiDataException {
198 loadAndPlay(Arrays.asList(file));
201 private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
202 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
204 * 新しいMIDIシーケンスを生成するダイアログ
206 public NewSequenceDialog newSequenceDialog;
210 public Base64Dialog base64Dialog = new Base64Dialog(this);
212 * プレイリストビュー(シーケンスリスト)
214 public SequenceListTable sequenceListTable;
216 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
218 private TrackListTable trackListTable;
220 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
222 private EventListTable eventListTable;
224 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
226 public MidiEventDialog eventDialog = new MidiEventDialog();
227 private VirtualMidiDevice outputMidiDevice;
229 * プレイリストビュー(シーケンスリスト)
231 public class SequenceListTable extends JTable {
233 * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
235 private MidiFileChooser midiFileChooser;
237 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
239 private Action base64EncodeAction;
242 * @param model プレイリストデータモデル
244 public SequenceListTable(PlaylistTableModel model) {
245 super(model, null, model.sequenceListSelectionModel);
247 midiFileChooser = new MidiFileChooser();
249 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
250 // アプレットの場合、Webクライアントマシンのローカルファイルには
251 // アクセスできないので、ファイル選択ダイアログは使用不可。
252 midiFileChooser = null;
255 new PlayButtonCellEditor();
256 new PositionCellEditor();
259 int column = PlaylistTableModel.Column.CHARSET.ordinal();
260 TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
261 Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
262 for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
264 getColumnModel().getColumn(column).setCellEditor(ce);
265 setAutoCreateColumnsFromModel(false);
267 // Base64エンコードアクションの生成
268 if( base64Dialog.isBase64Available() ) {
269 base64EncodeAction = new AbstractAction("Base64") {
271 String tooltip = "Base64 text conversion - Base64テキスト変換";
272 putValue(Action.SHORT_DESCRIPTION, tooltip);
275 public void actionPerformed(ActionEvent e) {
276 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
278 String filename = null;
280 data = mstm.getMIDIdata();
281 filename = mstm.getFilename();
283 base64Dialog.setMIDIData(data, filename);
284 base64Dialog.setVisible(true);
288 TableColumnModel colModel = getColumnModel();
289 for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
290 TableColumn tc = colModel.getColumn(c.ordinal());
291 tc.setPreferredWidth(c.preferredWidth);
292 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
295 private TableColumn lengthColumn;
297 public void tableChanged(TableModelEvent event) {
298 super.tableChanged(event);
301 if( lengthColumn != null ) {
302 int sec = getModel().getTotalSeconds();
303 String title = PlaylistTableModel.Column.LENGTH.title;
304 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
305 lengthColumn.setHeaderValue(title);
308 // シーケンス削除時など、合計シーケンス長が変わっても
309 // 列モデルからではヘッダタイトルが再描画されないことがある。
310 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
311 JTableHeader th = getTableHeader();
312 if( th != null ) th.repaint();
315 * 時間位置表示セルエディタ(ダブルクリック専用)
317 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
318 public PositionCellEditor() {
319 int column = PlaylistTableModel.Column.POSITION.ordinal();
320 TableColumn tc = getColumnModel().getColumn(column);
321 tc.setCellEditor(this);
324 * セルをダブルクリックしたときだけ編集モードに入るようにします。
325 * @param e イベント(マウスイベント)
326 * @return 編集可能になったらtrue
329 public boolean isCellEditable(EventObject e) {
330 // マウスイベント以外のイベントでは編集不可
331 if( ! (e instanceof MouseEvent) ) return false;
332 return ((MouseEvent)e).getClickCount() == 2;
335 public Object getCellEditorValue() { return null; }
337 * 編集モード時のコンポーネントを返すタイミングで
338 * そのシーケンスをシーケンサーにロードしたあと、
343 public Component getTableCellEditorComponent(
344 JTable table, Object value, boolean isSelected, int row, int column
347 getModel().loadToSequencer(row);
348 } catch (InvalidMidiDataException ex) {
349 ex.printStackTrace();
350 showError(ex.getMessage());
352 fireEditingStopped();
359 private class PlayButtonCellEditor extends AbstractCellEditor
360 implements TableCellEditor, TableCellRenderer
362 private JToggleButton playButton = new JToggleButton(
363 getModel().sequencerModel.startStopAction
365 { setMargin(ZERO_INSETS); }
367 public PlayButtonCellEditor() {
368 int column = PlaylistTableModel.Column.PLAY.ordinal();
369 TableColumn tc = getColumnModel().getColumn(column);
370 tc.setCellRenderer(this);
371 tc.setCellEditor(this);
376 * <p>この実装では、クリックしたセルのシーケンスが
378 * trueを返してプレイボタンを押せるようにします。
379 * そうでない場合はプレイボタンのないセルなので、
380 * ダブルクリックされたときだけtrueを返します。
384 public boolean isCellEditable(EventObject e) {
385 // マウスイベント以外はデフォルトメソッドにお任せ
386 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
387 fireEditingStopped();
388 MouseEvent me = (MouseEvent)e;
391 int row = rowAtPoint(me.getPoint());
392 if( row < 0 ) return false;
393 PlaylistTableModel model = getModel();
394 if( row >= model.getRowCount() ) return false;
396 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
397 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
398 return model.getSequenceList().get(row).isOnSequencer() || me.getClickCount() == 2;
401 public Object getCellEditorValue() { return null; }
405 * <p>この実装では、行の表すシーケンスが
406 * シーケンサーにロードされている場合にプレイボタンを返します。
408 * そのシーケンスをシーケンサーにロードしてnullを返します。
412 public Component getTableCellEditorComponent(
413 JTable table, Object value, boolean isSelected, int row, int column
415 fireEditingStopped();
416 PlaylistTableModel model = getModel();
417 if( model.getSequenceList().get(row).isOnSequencer() ) return playButton;
419 model.loadToSequencer(row);
420 } catch (InvalidMidiDataException ex) {
421 ex.printStackTrace();
422 showError(ex.getMessage());
427 public Component getTableCellRendererComponent(
428 JTable table, Object value, boolean isSelected,
429 boolean hasFocus, int row, int column
431 PlaylistTableModel model = getModel();
432 if(model.getSequenceList().get(row).isOnSequencer()) return playButton;
433 Class<?> cc = model.getColumnClass(column);
434 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
435 return defaultRenderer.getTableCellRendererComponent(
436 table, value, isSelected, hasFocus, row, column
441 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
446 public PlaylistTableModel getModel() { return (PlaylistTableModel)super.getModel(); }
450 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
451 "Delete", MidiSequenceEditor.deleteIcon,
452 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
455 public void actionPerformed(ActionEvent event) {
456 PlaylistTableModel model = getModel();
457 if( midiFileChooser != null ) {
458 if( model.getSelectedSequenceModel().isModified() ) {
460 "Selected MIDI sequence not saved - delete it ?\n" +
461 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
462 if( ! confirm(message) ) return;
466 model.removeSelectedSequence();
467 } catch (InvalidMidiDataException ex) {
468 ex.printStackTrace();
469 showError(ex.getMessage());
474 * ファイル選択ダイアログ(アプレットでは使用不可)
476 private class MidiFileChooser extends JFileChooser {
478 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
483 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
485 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
488 public void actionPerformed(ActionEvent event) {
489 SequenceTrackListTableModel sequenceModel = getModel().getSelectedSequenceModel();
490 String fn = sequenceModel.getFilename();
491 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
492 if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
493 File f = getSelectedFile();
496 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
498 try ( FileOutputStream out = new FileOutputStream(f) ) {
499 out.write(sequenceModel.getMIDIdata());
500 sequenceModel.setModified(false);
502 catch( IOException ex ) {
503 showError( ex.getMessage() );
504 ex.printStackTrace();
511 public Action openMidiFileAction = new AbstractAction("Open") {
512 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
514 public void actionPerformed(ActionEvent event) {
515 if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
517 loadAndPlay(getSelectedFile());
518 } catch (InvalidMidiDataException ex) {
519 ex.printStackTrace();
520 showError(ex.getMessage());
528 * シーケンス(トラックリスト)テーブルビュー
530 public class TrackListTable extends JTable {
532 * トラックリストテーブルビューを構築します。
533 * @param model シーケンス(トラックリスト)データモデル
535 public TrackListTable(SequenceTrackListTableModel model) {
536 super(model, null, model.trackListSelectionModel);
538 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
539 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
540 TableColumn tc = getColumnModel().getColumn(colIndex);
541 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
543 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
546 setAutoCreateColumnsFromModel(false);
548 trackSelectionListener = new TrackSelectionListener();
549 titleLabel = new TitleLabel();
550 model.sequenceListTableModel.sequenceListSelectionModel.addListSelectionListener(titleLabel);
551 TableColumnModel colModel = getColumnModel();
552 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
553 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
556 * このテーブルビューが表示するデータを提供する
557 * シーケンス(トラックリスト)データモデルを返します。
558 * @return シーケンス(トラックリスト)データモデル
561 public SequenceTrackListTableModel getModel() {
562 return (SequenceTrackListTableModel) super.getModel();
567 TitleLabel titleLabel;
569 * 親テーブルの選択シーケンスの変更に反応する
572 private class TitleLabel extends JLabel implements ListSelectionListener {
573 private static final String TITLE = "Tracks";
574 public TitleLabel() { setText(TITLE); }
576 public void valueChanged(ListSelectionEvent event) {
577 if( event.getValueIsAdjusting() ) return;
578 SequenceTrackListTableModel oldModel = getModel();
579 SequenceTrackListTableModel newModel = oldModel.sequenceListTableModel.getSelectedSequenceModel();
580 if( oldModel == newModel ) return;
582 // MIDIチャンネル選択中のときはキャンセルする
585 int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();
587 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
589 if( newModel == null ) {
590 newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;
591 addTrackAction.setEnabled(false);
594 addTrackAction.setEnabled(true);
596 oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);
598 setSelectionModel(newModel.trackListSelectionModel);
599 newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);
600 trackSelectionListener.valueChanged(null);
606 TrackSelectionListener trackSelectionListener;
610 private class TrackSelectionListener implements ListSelectionListener {
612 public void valueChanged(ListSelectionEvent e) {
613 if( e != null && e.getValueIsAdjusting() ) return;
614 ListSelectionModel tlsm = getModel().trackListSelectionModel;
615 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
616 eventListTable.titleLabel.update(tlsm, getModel());
622 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
624 * シーケンサーからこのモデルが外された場合がこれに該当します。
628 public void tableChanged(TableModelEvent e) {
629 super.tableChanged(e);
633 * このトラックリストテーブルが編集モードになっていたら解除します。
635 private void cancelCellEditing() {
636 TableCellEditor currentCellEditor = getCellEditor();
637 if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
642 Action addTrackAction = new AbstractAction("New") {
644 String tooltip = "Append new track - 新しいトラックの追加";
645 putValue(Action.SHORT_DESCRIPTION, tooltip);
649 public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
654 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
656 String tooltip = "Delete selected track - 選択したトラックを削除";
657 putValue(Action.SHORT_DESCRIPTION, tooltip);
661 public void actionPerformed(ActionEvent e) {
662 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
663 if( confirm(message) ) getModel().deleteSelectedTracks();
669 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
671 public class EventListTable extends JTable {
673 * 新しいイベントリストテーブルを構築します。
674 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
675 * トラックを切り替えたいときは {@link #setModel(TableModel)}
676 * でデータモデルを異なるトラックのものに切り替えます。
679 * @param model トラック(イベントリスト)データモデル
681 public EventListTable(TrackEventListTableModel model) {
682 super(model, null, model.eventSelectionModel);
685 eventCellEditor = new MidiEventCellEditor();
686 setAutoCreateColumnsFromModel(false);
688 eventSelectionListener = new EventSelectionListener();
689 titleLabel = new TitleLabel();
691 TableColumnModel colModel = getColumnModel();
692 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
693 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
696 * このテーブルビューが表示するデータを提供する
697 * トラック(イベントリスト)データモデルを返します。
698 * @return トラック(イベントリスト)データモデル
701 public TrackEventListTableModel getModel() {
702 return (TrackEventListTableModel) super.getModel();
707 TitleLabel titleLabel;
709 * 親テーブルの選択トラックの変更に反応する
712 private class TitleLabel extends JLabel {
713 private static final String TITLE = "MIDI Events";
714 public TitleLabel() { super(TITLE); }
715 public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
717 TrackEventListTableModel oldTrackModel = getModel();
718 int index = tlsm.getMinSelectionIndex();
720 text = String.format(TITLE+" - track No.%d", index);
723 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
724 if( oldTrackModel == newTrackModel )
726 if( newTrackModel == null ) {
727 newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;
728 queryJumpEventAction.setEnabled(false);
729 queryAddEventAction.setEnabled(false);
731 queryPasteEventAction.setEnabled(false);
732 copyEventAction.setEnabled(false);
733 deleteEventAction.setEnabled(false);
734 cutEventAction.setEnabled(false);
737 queryJumpEventAction.setEnabled(true);
738 queryAddEventAction.setEnabled(true);
740 oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);
741 setModel(newTrackModel);
742 setSelectionModel(newTrackModel.eventSelectionModel);
743 newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);
750 private EventSelectionListener eventSelectionListener;
754 private class EventSelectionListener implements ListSelectionListener {
755 public EventSelectionListener() {
756 getModel().eventSelectionModel.addListSelectionListener(this);
759 public void valueChanged(ListSelectionEvent e) {
760 if( e.getValueIsAdjusting() )
762 if( getSelectionModel().isSelectionEmpty() ) {
763 queryPasteEventAction.setEnabled(false);
764 copyEventAction.setEnabled(false);
765 deleteEventAction.setEnabled(false);
766 cutEventAction.setEnabled(false);
769 copyEventAction.setEnabled(true);
770 deleteEventAction.setEnabled(true);
771 cutEventAction.setEnabled(true);
772 TrackEventListTableModel trackModel = getModel();
773 int minIndex = getSelectionModel().getMinSelectionIndex();
774 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
775 if( midiEvent != null ) {
776 MidiMessage msg = midiEvent.getMessage();
777 if( msg instanceof ShortMessage ) {
778 ShortMessage sm = (ShortMessage)msg;
779 int cmd = sm.getCommand();
780 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
782 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
783 int ch = sm.getChannel();
784 int note = sm.getData1();
785 int vel = sm.getData2();
786 outMidiChannels[ch].noteOn(note, vel);
787 outMidiChannels[ch].noteOff(note, vel);
791 if( pairNoteOnOffModel.isSelected() ) {
792 int maxIndex = getSelectionModel().getMaxSelectionIndex();
794 for( int i=minIndex; i<=maxIndex; i++ ) {
795 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
796 partnerIndex = trackModel.getIndexOfPartnerFor(i);
797 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
798 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
805 * Pair noteON/OFF トグルボタンモデル
807 private JToggleButton.ToggleButtonModel
808 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
810 addItemListener(new ItemListener() {
811 public void itemStateChanged(ItemEvent e) {
812 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
818 private class EventEditContext {
822 private TrackEventListTableModel trackModel;
826 private TickPositionModel tickPositionModel = new TickPositionModel();
830 private MidiEvent selectedMidiEvent = null;
834 private int selectedIndex = -1;
838 private long currentTick = 0;
840 * 上書きして削除対象にする変更前イベント(null可)
842 private MidiEvent[] midiEventsToBeOverwritten;
844 * 選択したイベントを入力ダイアログなどに反映します。
845 * @param model 対象データモデル
847 private void setSelectedEvent(TrackEventListTableModel trackModel) {
848 this.trackModel = trackModel;
849 SequenceTrackListTableModel sequenceTableModel = trackModel.sequenceTrackListTableModel;
850 int ppq = sequenceTableModel.getSequence().getResolution();
851 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
852 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
854 selectedIndex = trackModel.eventSelectionModel.getMinSelectionIndex();
855 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
856 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
857 tickPositionModel.setTickPosition(currentTick);
859 public void setupForEdit(TrackEventListTableModel trackModel) {
860 MidiEvent partnerEvent = null;
861 eventDialog.midiMessageForm.setMessage(
862 selectedMidiEvent.getMessage(),
863 trackModel.sequenceTrackListTableModel.charset
865 if( eventDialog.midiMessageForm.isNote() ) {
866 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
867 if( partnerIndex < 0 ) {
868 eventDialog.midiMessageForm.durationForm.setDuration(0);
871 partnerEvent = trackModel.getMidiEvent(partnerIndex);
872 long partnerTick = partnerEvent.getTick();
873 long duration = currentTick > partnerTick ?
874 currentTick - partnerTick : partnerTick - currentTick ;
875 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
878 if(partnerEvent == null)
879 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
881 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
883 private Action jumpEventAction = new AbstractAction() {
884 { putValue(NAME,"Jump"); }
885 public void actionPerformed(ActionEvent e) {
886 long tick = tickPositionModel.getTickPosition();
887 scrollToEventAt(tick);
888 eventDialog.setVisible(false);
892 private Action pasteEventAction = new AbstractAction() {
893 { putValue(NAME,"Paste"); }
894 public void actionPerformed(ActionEvent e) {
895 long tick = tickPositionModel.getTickPosition();
896 clipBoard.paste(trackModel, tick);
897 scrollToEventAt(tick);
898 // ペーストで曲の長さが変わったことをプレイリストに通知
899 SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
900 seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
901 eventDialog.setVisible(false);
905 private boolean applyEvent() {
906 long tick = tickPositionModel.getTickPosition();
907 MidiMessageForm form = eventDialog.midiMessageForm;
908 SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
909 MidiEvent newMidiEvent = new MidiEvent(form.getMessage(seqModel.charset), tick);
910 if( midiEventsToBeOverwritten != null ) {
911 // 上書き消去するための選択済イベントがあった場合
912 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
914 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
915 System.out.println("addMidiEvent failure");
918 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
919 ShortMessage sm = form.createPartnerMessage();
921 scrollToEventAt( tick );
923 int duration = form.durationForm.getDuration();
924 if( form.isNote(false) ) {
925 duration = -duration;
927 long partnerTick = tick + (long)duration;
928 if( partnerTick < 0L ) partnerTick = 0L;
929 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
930 if( ! trackModel.addMidiEvent(partner) ) {
931 System.out.println("addMidiEvent failure (note on/off partner message)");
933 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
936 seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
937 eventDialog.setVisible(false);
941 private EventEditContext editContext = new EventEditContext();
943 * 指定のTick位置へジャンプするアクション
945 Action queryJumpEventAction = new AbstractAction() {
947 putValue(NAME,"Jump to ...");
950 public void actionPerformed(ActionEvent e) {
951 editContext.setSelectedEvent(getModel());
952 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
958 Action queryAddEventAction = new AbstractAction() {
960 putValue(NAME,"New");
963 public void actionPerformed(ActionEvent e) {
964 TrackEventListTableModel model = getModel();
965 editContext.setSelectedEvent(model);
966 editContext.midiEventsToBeOverwritten = null;
967 eventDialog.openEventForm(
969 eventCellEditor.applyEventAction,
975 * MIDIイベントのコピー&ペーストを行うためのクリップボード
977 private class LocalClipBoard {
978 private MidiEvent copiedEventsToPaste[];
979 private int copiedEventsPPQ = 0;
980 public void copy(TrackEventListTableModel model, boolean withRemove) {
981 copiedEventsToPaste = model.getSelectedMidiEvents();
982 copiedEventsPPQ = model.sequenceTrackListTableModel.getSequence().getResolution();
983 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
984 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
985 queryPasteEventAction.setEnabled(en);
987 public void cut(TrackEventListTableModel model) {copy(model,true);}
988 public void copy(TrackEventListTableModel model){copy(model,false);}
989 public void paste(TrackEventListTableModel model, long tick) {
990 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
993 private LocalClipBoard clipBoard = new LocalClipBoard();
995 * 指定のTick位置へ貼り付けるアクション
997 Action queryPasteEventAction = new AbstractAction() {
999 putValue(NAME,"Paste to ...");
1002 public void actionPerformed(ActionEvent e) {
1003 editContext.setSelectedEvent(getModel());
1004 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
1010 public Action cutEventAction = new AbstractAction("Cut") {
1015 public void actionPerformed(ActionEvent e) {
1016 TrackEventListTableModel model = getModel();
1017 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
1019 clipBoard.cut(model);
1025 public Action copyEventAction = new AbstractAction("Copy") {
1030 public void actionPerformed(ActionEvent e) {
1031 clipBoard.copy(getModel());
1037 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
1042 public void actionPerformed(ActionEvent e) {
1043 TrackEventListTableModel model = getModel();
1044 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1046 model.removeSelectedMidiEvents();
1052 private MidiEventCellEditor eventCellEditor;
1056 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1058 * MIDIイベントセルエディタを構築します。
1060 public MidiEventCellEditor() {
1061 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
1062 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1063 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1064 getColumnModel().getColumn(index).setCellEditor(this);
1067 * セルをダブルクリックしないと編集できないようにします。
1068 * @param e イベント(マウスイベント)
1069 * @return 編集可能になったらtrue
1072 public boolean isCellEditable(EventObject e) {
1073 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1074 return ((MouseEvent)e).getClickCount() == 2;
1077 public Object getCellEditorValue() { return null; }
1079 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1081 private ComponentListener dialogComponentListener = new ComponentAdapter() {
1083 public void componentHidden(ComponentEvent e) {
1084 fireEditingCanceled();
1086 eventDialog.removeComponentListener(this);
1092 private Action editEventAction = new AbstractAction() {
1093 public void actionPerformed(ActionEvent e) {
1094 TrackEventListTableModel model = getModel();
1095 editContext.setSelectedEvent(model);
1096 if( editContext.selectedMidiEvent == null )
1098 editContext.setupForEdit(model);
1099 eventDialog.addComponentListener(dialogComponentListener);
1100 eventDialog.openEventForm("Change MIDI event", applyEventAction);
1106 private JButton editEventButton = new JButton(editEventAction){{
1107 setHorizontalAlignment(JButton.LEFT);
1110 public Component getTableCellEditorComponent(
1111 JTable table, Object value, boolean isSelected, int row, int column
1113 editEventButton.setText(value.toString());
1114 return editEventButton;
1117 * 入力したイベントを反映するアクション
1119 private Action applyEventAction = new AbstractAction() {
1121 putValue(NAME,"OK");
1123 public void actionPerformed(ActionEvent e) {
1124 if( editContext.applyEvent() ) fireEditingStopped();
1129 * スクロール可能なMIDIイベントテーブルビュー
1131 private JScrollPane scrollPane = new JScrollPane(this);
1133 * 指定の MIDI tick のイベントへスクロールします。
1134 * @param tick MIDI tick
1136 public void scrollToEventAt(long tick) {
1137 int index = getModel().tickToIndex(tick);
1138 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1139 getSelectionModel().setSelectionInterval(index, index);
1144 * 新しい {@link MidiSequenceEditor} を構築します。
1145 * @param playlistTableModel このエディタが参照するプレイリストモデル
1146 * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1148 public MidiSequenceEditor(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice) {
1149 this.outputMidiDevice = outputMidiDevice;
1150 sequenceListTable = new SequenceListTable(playlistTableModel);
1151 trackListTable = new TrackListTable(
1152 new SequenceTrackListTableModel(playlistTableModel, null, null)
1154 eventListTable = new EventListTable(new TrackEventListTableModel(trackListTable.getModel(), null));
1155 newSequenceDialog = new NewSequenceDialog(playlistTableModel, outputMidiDevice);
1156 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1157 setBounds( 150, 200, 900, 500 );
1158 setLayout(new FlowLayout());
1159 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, dropTargetListener, true);
1162 JPanel playlistPanel = new JPanel() {{
1163 JPanel playlistOperationPanel = new JPanel() {{
1164 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1165 add(Box.createRigidArea(new Dimension(10, 0)));
1166 add(new JButton(newSequenceDialog.openAction) {{ setMargin(ZERO_INSETS); }});
1167 if( sequenceListTable.midiFileChooser != null ) {
1168 add( Box.createRigidArea(new Dimension(5, 0)) );
1169 add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {{ setMargin(ZERO_INSETS); }});
1171 if(sequenceListTable.base64EncodeAction != null) {
1172 add(Box.createRigidArea(new Dimension(5, 0)));
1173 add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1175 add(Box.createRigidArea(new Dimension(5, 0)));
1176 PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
1177 add(new JButton(playlistTableModel.moveToTopAction) {{ setMargin(ZERO_INSETS); }});
1178 add(Box.createRigidArea(new Dimension(5, 0)));
1179 add(new JButton(playlistTableModel.moveToBottomAction) {{ setMargin(ZERO_INSETS); }});
1180 if( sequenceListTable.midiFileChooser != null ) {
1181 add(Box.createRigidArea(new Dimension(5, 0)));
1182 add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {{ setMargin(ZERO_INSETS); }});
1184 add( Box.createRigidArea(new Dimension(5, 0)) );
1185 add(new JButton(sequenceListTable.deleteSequenceAction) {{ setMargin(ZERO_INSETS); }});
1186 add( Box.createRigidArea(new Dimension(5, 0)) );
1187 add(new SequencerSpeedSlider(playlistTableModel.sequencerModel.speedSliderModel));
1188 add( Box.createRigidArea(new Dimension(5, 0)) );
1190 MidiSequencerModel sequencerModel = sequenceListTable.getModel().sequencerModel;
1191 add(new JLabel("SyncMode:"));
1192 add(new JLabel("Master"));
1193 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1194 add(new JLabel("Slave"));
1195 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1198 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1199 add(new JScrollPane(sequenceListTable));
1200 add(Box.createRigidArea(new Dimension(0, 10)));
1201 add(playlistOperationPanel);
1202 add(Box.createRigidArea(new Dimension(0, 10)));
1204 JPanel trackListPanel = new JPanel() {{
1205 JPanel trackListOperationPanel = new JPanel() {{
1206 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ZERO_INSETS); }});
1207 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
1209 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1210 add(trackListTable.titleLabel);
1211 add(Box.createRigidArea(new Dimension(0, 5)));
1212 add(new JScrollPane(trackListTable));
1213 add(Box.createRigidArea(new Dimension(0, 5)));
1214 add(trackListOperationPanel);
1216 JPanel eventListPanel = new JPanel() {{
1217 JPanel eventListOperationPanel = new JPanel() {{
1218 add(new JCheckBox("Pair NoteON/OFF") {{
1219 setModel(eventListTable.pairNoteOnOffModel);
1220 setToolTipText("NoteON/OFFをペアで同時選択する");
1222 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ZERO_INSETS); }});
1223 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ZERO_INSETS); }});
1224 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ZERO_INSETS); }});
1225 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ZERO_INSETS); }});
1226 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ZERO_INSETS); }});
1227 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ZERO_INSETS); }});
1229 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1230 add(eventListTable.titleLabel);
1231 add(eventListTable.scrollPane);
1232 add(eventListOperationPanel);
1234 Container cp = getContentPane();
1235 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1236 cp.add(Box.createVerticalStrut(2));
1238 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1239 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1240 setDividerLocation(300);
1243 setDividerLocation(160);