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.Sequencer;
39 import javax.sound.midi.ShortMessage;
40 import javax.swing.AbstractAction;
41 import javax.swing.AbstractCellEditor;
42 import javax.swing.Action;
43 import javax.swing.Box;
44 import javax.swing.BoxLayout;
45 import javax.swing.DefaultCellEditor;
46 import javax.swing.Icon;
47 import javax.swing.JButton;
48 import javax.swing.JCheckBox;
49 import javax.swing.JComboBox;
50 import javax.swing.JDialog;
51 import javax.swing.JFileChooser;
52 import javax.swing.JLabel;
53 import javax.swing.JOptionPane;
54 import javax.swing.JPanel;
55 import javax.swing.JScrollPane;
56 import javax.swing.JSplitPane;
57 import javax.swing.JTable;
58 import javax.swing.JToggleButton;
59 import javax.swing.ListSelectionModel;
60 import javax.swing.event.ListSelectionEvent;
61 import javax.swing.event.ListSelectionListener;
62 import javax.swing.event.TableModelEvent;
63 import javax.swing.filechooser.FileNameExtensionFilter;
64 import javax.swing.table.JTableHeader;
65 import javax.swing.table.TableCellEditor;
66 import javax.swing.table.TableCellRenderer;
67 import javax.swing.table.TableColumn;
68 import javax.swing.table.TableColumnModel;
69 import javax.swing.table.TableModel;
71 import camidion.chordhelper.ButtonIcon;
72 import camidion.chordhelper.ChordHelperApplet;
73 import camidion.chordhelper.mididevice.AbstractVirtualMidiDevice;
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 * このMIDIエディタにおける操作音の出力先となる仮想MIDIデバイスを返します。
103 public VirtualMidiDevice getVirtualMidiDevice() { return virtualMidiDevice; }
104 private static VirtualMidiDevice virtualMidiDevice = new AbstractVirtualMidiDevice() {
107 setMaxReceivers(0); // 送信専用とする(MIDI IN はサポートしない)
112 protected MyInfo info;
114 public Info getDeviceInfo() { return info; }
115 class MyInfo extends Info {
116 protected MyInfo() { super("MIDI Editor","Unknown vendor","MIDI sequence editor",""); }
121 * エラーメッセージダイアログを表示します。
122 * @param message エラーメッセージ
124 public void showError(String message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
126 * 警告メッセージダイアログを表示します。
127 * @param message 警告メッセージ
129 public void showWarning(String message) { showMessage(message, JOptionPane.WARNING_MESSAGE); }
130 private void showMessage(String message, int messageType) {
131 JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, messageType);
135 * @param message 確認メッセージ
136 * @return 確認OKのときtrue
138 boolean confirm(String message) {
139 return JOptionPane.showConfirmDialog(this, message, ChordHelperApplet.VersionInfo.NAME,
140 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION ;
144 * ドロップされた複数のMIDIファイルを読み込むリスナー
146 public final DropTargetListener dropTargetListener = new DropTargetAdapter() {
148 public void dragEnter(DropTargetDragEvent event) {
149 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
150 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
154 @SuppressWarnings("unchecked")
155 public void drop(DropTargetDropEvent event) {
156 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
158 int action = event.getDropAction();
159 if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
160 Transferable t = event.getTransferable();
161 if( t.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
162 loadAndPlay((List<File>)t.getTransferData(DataFlavor.javaFileListFlavor));
163 event.dropComplete(true);
168 catch (Exception ex) {
169 ex.printStackTrace();
171 event.dropComplete(false);
176 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
177 * すでに再生されていた場合、このエディタダイアログを表示します。
179 * @param fileList 読み込むMIDIファイルのリスト
180 * @see #loadAndPlay(File)
182 public void loadAndPlay(List<File> fileList) {
183 int indexOfAddedTop = -1;
184 PlaylistTableModel playlist = sequenceListTable.getModel();
186 indexOfAddedTop = playlist.addSequences(fileList);
187 } catch(IOException|InvalidMidiDataException e) {
188 showWarning(e.getMessage());
189 } catch(AccessControlException e) {
190 showError(e.getMessage());
193 MidiSequencerModel sequencerModel = playlist.sequencerModel;
194 if( sequencerModel.getSequencer().isRunning() ) {
195 String command = (String)openAction.getValue(Action.NAME);
196 openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
199 if( indexOfAddedTop >= 0 ) {
200 playlist.loadToSequencer(indexOfAddedTop);
201 sequencerModel.start();
205 * 1件のMIDIファイルを読み込み、再生されていなかったら再生します。
206 * すでに再生されていた場合、このエディタダイアログを表示します。
208 * @param file 読み込むMIDIファイル
209 * @see #loadAndPlay(List) loadAndPlay(List<File>)
211 public void loadAndPlay(File file) { loadAndPlay(Arrays.asList(file)); }
213 private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
214 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
216 * 新しいMIDIシーケンスを生成するダイアログ
218 public NewSequenceDialog newSequenceDialog = new NewSequenceDialog(this);
222 public Base64Dialog base64Dialog = new Base64Dialog(this);
224 * プレイリストビュー(シーケンスリスト)
226 public SequenceListTable sequenceListTable;
228 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
230 private TrackListTable trackListTable;
232 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
234 private EventListTable eventListTable;
236 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
238 public MidiEventDialog eventDialog = new MidiEventDialog();
240 * プレイリストビュー(シーケンスリスト)
242 public class SequenceListTable extends JTable {
244 * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
246 private MidiFileChooser midiFileChooser;
248 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
250 private Action base64EncodeAction;
253 * @param model プレイリストデータモデル
255 public SequenceListTable(PlaylistTableModel model) {
256 super(model, null, model.sequenceListSelectionModel);
258 midiFileChooser = new MidiFileChooser();
260 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
261 // アプレットの場合、Webクライアントマシンのローカルファイルには
262 // アクセスできないので、ファイル選択ダイアログは使用不可。
263 midiFileChooser = null;
266 new PlayButtonCellEditor();
267 new PositionCellEditor();
270 int column = PlaylistTableModel.Column.CHARSET.ordinal();
271 TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
272 Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
273 for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
275 getColumnModel().getColumn(column).setCellEditor(ce);
276 setAutoCreateColumnsFromModel(false);
278 // Base64エンコードアクションの生成
279 if( base64Dialog.isBase64Available() ) {
280 base64EncodeAction = new AbstractAction("Base64") {
282 String tooltip = "Base64 text conversion - Base64テキスト変換";
283 putValue(Action.SHORT_DESCRIPTION, tooltip);
286 public void actionPerformed(ActionEvent e) {
287 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
289 String filename = null;
291 data = mstm.getMIDIdata();
292 filename = mstm.getFilename();
294 base64Dialog.setMIDIData(data, filename);
295 base64Dialog.setVisible(true);
299 TableColumnModel colModel = getColumnModel();
300 for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
301 TableColumn tc = colModel.getColumn(c.ordinal());
302 tc.setPreferredWidth(c.preferredWidth);
303 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
306 private TableColumn lengthColumn;
308 public void tableChanged(TableModelEvent event) {
309 super.tableChanged(event);
312 if( lengthColumn != null ) {
313 int sec = getModel().getTotalSeconds();
314 String title = PlaylistTableModel.Column.LENGTH.title;
315 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
316 lengthColumn.setHeaderValue(title);
319 // シーケンス削除時など、合計シーケンス長が変わっても
320 // 列モデルからではヘッダタイトルが再描画されないことがある。
321 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
322 JTableHeader th = getTableHeader();
323 if( th != null ) th.repaint();
326 * 時間位置表示セルエディタ(ダブルクリック専用)
328 private class PositionCellEditor extends AbstractCellEditor
329 implements TableCellEditor
331 public PositionCellEditor() {
332 int column = PlaylistTableModel.Column.POSITION.ordinal();
333 TableColumn tc = getColumnModel().getColumn(column);
334 tc.setCellEditor(this);
337 * セルをダブルクリックしたときだけ編集モードに入るようにします。
338 * @param e イベント(マウスイベント)
339 * @return 編集可能になったらtrue
342 public boolean isCellEditable(EventObject e) {
343 // マウスイベント以外のイベントでは編集不可
344 if( ! (e instanceof MouseEvent) ) return false;
345 return ((MouseEvent)e).getClickCount() == 2;
348 public Object getCellEditorValue() { return null; }
350 * 編集モード時のコンポーネントを返すタイミングで
351 * そのシーケンスをシーケンサーにロードしたあと、
356 public Component getTableCellEditorComponent(
357 JTable table, Object value, boolean isSelected,
360 getModel().loadToSequencer(row);
361 fireEditingStopped();
368 private class PlayButtonCellEditor extends AbstractCellEditor
369 implements TableCellEditor, TableCellRenderer
371 private JToggleButton playButton = new JToggleButton(
372 getModel().sequencerModel.startStopAction
374 { setMargin(ZERO_INSETS); }
376 public PlayButtonCellEditor() {
377 int column = PlaylistTableModel.Column.PLAY.ordinal();
378 TableColumn tc = getColumnModel().getColumn(column);
379 tc.setCellRenderer(this);
380 tc.setCellEditor(this);
385 * <p>この実装では、クリックしたセルのシーケンスが
387 * trueを返してプレイボタンを押せるようにします。
388 * そうでない場合はプレイボタンのないセルなので、
389 * ダブルクリックされたときだけtrueを返します。
393 public boolean isCellEditable(EventObject e) {
394 // マウスイベント以外はデフォルトメソッドにお任せ
395 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
396 fireEditingStopped();
397 MouseEvent me = (MouseEvent)e;
400 int row = rowAtPoint(me.getPoint());
401 if( row < 0 ) return false;
402 PlaylistTableModel model = getModel();
403 if( row >= model.getRowCount() ) return false;
405 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
406 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
407 return model.sequenceList.get(row).isOnSequencer() || me.getClickCount() == 2;
410 public Object getCellEditorValue() { return null; }
414 * <p>この実装では、行の表すシーケンスが
415 * シーケンサーにロードされている場合にプレイボタンを返します。
417 * そのシーケンスをシーケンサーにロードしてnullを返します。
421 public Component getTableCellEditorComponent(
422 JTable table, Object value, boolean isSelected, int row, int column
424 fireEditingStopped();
425 PlaylistTableModel model = getModel();
426 if( model.sequenceList.get(row).isOnSequencer() ) return playButton;
427 model.loadToSequencer(row);
431 public Component getTableCellRendererComponent(
432 JTable table, Object value, boolean isSelected,
433 boolean hasFocus, int row, int column
435 PlaylistTableModel model = getModel();
436 if(model.sequenceList.get(row).isOnSequencer()) return playButton;
437 Class<?> cc = model.getColumnClass(column);
438 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
439 return defaultRenderer.getTableCellRendererComponent(
440 table, value, isSelected, hasFocus, row, column
445 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
450 public PlaylistTableModel getModel() { return (PlaylistTableModel)super.getModel(); }
454 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
455 "Delete", MidiSequenceEditor.deleteIcon,
456 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
459 public void actionPerformed(ActionEvent e) {
460 PlaylistTableModel model = getModel();
461 if( midiFileChooser != null ) {
462 if( model.getSelectedSequenceModel().isModified() ) {
464 "Selected MIDI sequence not saved - delete it ?\n" +
465 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
466 if( ! confirm(message) ) return;
469 model.removeSelectedSequence();
473 * ファイル選択ダイアログ(アプレットでは使用不可)
475 private class MidiFileChooser extends JFileChooser {
477 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
482 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
484 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
487 public void actionPerformed(ActionEvent event) {
488 SequenceTrackListTableModel sequenceModel = getModel().getSelectedSequenceModel();
489 String fn = sequenceModel.getFilename();
490 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
491 if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
492 File f = getSelectedFile();
495 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
497 try ( FileOutputStream out = new FileOutputStream(f) ) {
498 out.write(sequenceModel.getMIDIdata());
499 sequenceModel.setModified(false);
501 catch( IOException ex ) {
502 showError( ex.getMessage() );
503 ex.printStackTrace();
510 public Action openMidiFileAction = new AbstractAction("Open") {
511 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
513 public void actionPerformed(ActionEvent event) {
514 if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
515 loadAndPlay(getSelectedFile());
522 * シーケンス(トラックリスト)テーブルビュー
524 public class TrackListTable extends JTable {
526 * トラックリストテーブルビューを構築します。
527 * @param model シーケンス(トラックリスト)データモデル
529 public TrackListTable(SequenceTrackListTableModel model) {
530 super(model, null, model.trackListSelectionModel);
532 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
533 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
534 TableColumn tc = getColumnModel().getColumn(colIndex);
535 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
537 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
540 setAutoCreateColumnsFromModel(false);
542 trackSelectionListener = new TrackSelectionListener();
543 titleLabel = new TitleLabel();
544 model.sequenceListTableModel.sequenceListSelectionModel.addListSelectionListener(titleLabel);
545 TableColumnModel colModel = getColumnModel();
546 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
547 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
550 * このテーブルビューが表示するデータを提供する
551 * シーケンス(トラックリスト)データモデルを返します。
552 * @return シーケンス(トラックリスト)データモデル
555 public SequenceTrackListTableModel getModel() {
556 return (SequenceTrackListTableModel) super.getModel();
561 TitleLabel titleLabel;
563 * 親テーブルの選択シーケンスの変更に反応する
566 private class TitleLabel extends JLabel implements ListSelectionListener {
567 private static final String TITLE = "Tracks";
568 public TitleLabel() { setText(TITLE); }
570 public void valueChanged(ListSelectionEvent event) {
571 if( event.getValueIsAdjusting() ) return;
572 SequenceTrackListTableModel oldModel = getModel();
573 SequenceTrackListTableModel newModel = oldModel.sequenceListTableModel.getSelectedSequenceModel();
574 if( oldModel == newModel ) return;
576 // MIDIチャンネル選択中のときはキャンセルする
579 int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();
581 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
583 if( newModel == null ) {
584 newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;
585 addTrackAction.setEnabled(false);
588 addTrackAction.setEnabled(true);
590 oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);
592 setSelectionModel(newModel.trackListSelectionModel);
593 newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);
594 trackSelectionListener.valueChanged(null);
600 TrackSelectionListener trackSelectionListener;
604 private class TrackSelectionListener implements ListSelectionListener {
606 public void valueChanged(ListSelectionEvent e) {
607 if( e != null && e.getValueIsAdjusting() ) return;
608 ListSelectionModel tlsm = getModel().trackListSelectionModel;
609 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
610 eventListTable.titleLabel.update(tlsm, getModel());
616 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
618 * シーケンサーからこのモデルが外された場合がこれに該当します。
622 public void tableChanged(TableModelEvent e) {
623 super.tableChanged(e);
627 * このトラックリストテーブルが編集モードになっていたら解除します。
629 private void cancelCellEditing() {
630 TableCellEditor currentCellEditor = getCellEditor();
631 if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
636 Action addTrackAction = new AbstractAction("New") {
638 String tooltip = "Append new track - 新しいトラックの追加";
639 putValue(Action.SHORT_DESCRIPTION, tooltip);
643 public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
648 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
650 String tooltip = "Delete selected track - 選択したトラックを削除";
651 putValue(Action.SHORT_DESCRIPTION, tooltip);
655 public void actionPerformed(ActionEvent e) {
656 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
657 if( confirm(message) ) getModel().deleteSelectedTracks();
663 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
665 public class EventListTable extends JTable {
667 * 新しいイベントリストテーブルを構築します。
668 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
669 * トラックを切り替えたいときは {@link #setModel(TableModel)}
670 * でデータモデルを異なるトラックのものに切り替えます。
673 * @param model トラック(イベントリスト)データモデル
675 public EventListTable(TrackEventListTableModel model) {
676 super(model, null, model.eventSelectionModel);
679 eventCellEditor = new MidiEventCellEditor();
680 setAutoCreateColumnsFromModel(false);
682 eventSelectionListener = new EventSelectionListener();
683 titleLabel = new TitleLabel();
685 TableColumnModel colModel = getColumnModel();
686 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
687 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
690 * このテーブルビューが表示するデータを提供する
691 * トラック(イベントリスト)データモデルを返します。
692 * @return トラック(イベントリスト)データモデル
695 public TrackEventListTableModel getModel() {
696 return (TrackEventListTableModel) super.getModel();
701 TitleLabel titleLabel;
703 * 親テーブルの選択トラックの変更に反応する
706 private class TitleLabel extends JLabel {
707 private static final String TITLE = "MIDI Events";
708 public TitleLabel() { super(TITLE); }
709 public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
711 TrackEventListTableModel oldTrackModel = getModel();
712 int index = tlsm.getMinSelectionIndex();
714 text = String.format(TITLE+" - track No.%d", index);
717 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
718 if( oldTrackModel == newTrackModel )
720 if( newTrackModel == null ) {
721 newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;
722 queryJumpEventAction.setEnabled(false);
723 queryAddEventAction.setEnabled(false);
725 queryPasteEventAction.setEnabled(false);
726 copyEventAction.setEnabled(false);
727 deleteEventAction.setEnabled(false);
728 cutEventAction.setEnabled(false);
731 queryJumpEventAction.setEnabled(true);
732 queryAddEventAction.setEnabled(true);
734 oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);
735 setModel(newTrackModel);
736 setSelectionModel(newTrackModel.eventSelectionModel);
737 newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);
743 private EventSelectionListener eventSelectionListener;
747 private class EventSelectionListener implements ListSelectionListener {
748 public EventSelectionListener() {
749 getModel().eventSelectionModel.addListSelectionListener(this);
752 public void valueChanged(ListSelectionEvent e) {
753 if( e.getValueIsAdjusting() )
755 if( getSelectionModel().isSelectionEmpty() ) {
756 queryPasteEventAction.setEnabled(false);
757 copyEventAction.setEnabled(false);
758 deleteEventAction.setEnabled(false);
759 cutEventAction.setEnabled(false);
762 copyEventAction.setEnabled(true);
763 deleteEventAction.setEnabled(true);
764 cutEventAction.setEnabled(true);
765 TrackEventListTableModel trackModel = getModel();
766 int minIndex = getSelectionModel().getMinSelectionIndex();
767 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
768 if( midiEvent != null ) {
769 MidiMessage msg = midiEvent.getMessage();
770 if( msg instanceof ShortMessage ) {
771 ShortMessage sm = (ShortMessage)msg;
772 int cmd = sm.getCommand();
773 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
775 MidiChannel outMidiChannels[] = virtualMidiDevice.getChannels();
776 int ch = sm.getChannel();
777 int note = sm.getData1();
778 int vel = sm.getData2();
779 outMidiChannels[ch].noteOn(note, vel);
780 outMidiChannels[ch].noteOff(note, vel);
784 if( pairNoteOnOffModel.isSelected() ) {
785 int maxIndex = getSelectionModel().getMaxSelectionIndex();
787 for( int i=minIndex; i<=maxIndex; i++ ) {
788 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
789 partnerIndex = trackModel.getIndexOfPartnerFor(i);
790 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
791 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
798 * Pair noteON/OFF トグルボタンモデル
800 private JToggleButton.ToggleButtonModel
801 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
803 addItemListener(new ItemListener() {
804 public void itemStateChanged(ItemEvent e) {
805 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
811 private class EventEditContext {
815 private TrackEventListTableModel trackModel;
819 private TickPositionModel tickPositionModel = new TickPositionModel();
823 private MidiEvent selectedMidiEvent = null;
827 private int selectedIndex = -1;
831 private long currentTick = 0;
833 * 上書きして削除対象にする変更前イベント(null可)
835 private MidiEvent[] midiEventsToBeOverwritten;
837 * 選択したイベントを入力ダイアログなどに反映します。
838 * @param model 対象データモデル
840 private void setSelectedEvent(TrackEventListTableModel trackModel) {
841 this.trackModel = trackModel;
842 SequenceTrackListTableModel sequenceTableModel = trackModel.sequenceTrackListTableModel;
843 int ppq = sequenceTableModel.getSequence().getResolution();
844 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
845 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
847 selectedIndex = trackModel.eventSelectionModel.getMinSelectionIndex();
848 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
849 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
850 tickPositionModel.setTickPosition(currentTick);
852 public void setupForEdit(TrackEventListTableModel trackModel) {
853 MidiEvent partnerEvent = null;
854 eventDialog.midiMessageForm.setMessage(
855 selectedMidiEvent.getMessage(),
856 trackModel.sequenceTrackListTableModel.charset
858 if( eventDialog.midiMessageForm.isNote() ) {
859 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
860 if( partnerIndex < 0 ) {
861 eventDialog.midiMessageForm.durationForm.setDuration(0);
864 partnerEvent = trackModel.getMidiEvent(partnerIndex);
865 long partnerTick = partnerEvent.getTick();
866 long duration = currentTick > partnerTick ?
867 currentTick - partnerTick : partnerTick - currentTick ;
868 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
871 if(partnerEvent == null)
872 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
874 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
876 private Action jumpEventAction = new AbstractAction() {
877 { putValue(NAME,"Jump"); }
878 public void actionPerformed(ActionEvent e) {
879 long tick = tickPositionModel.getTickPosition();
880 scrollToEventAt(tick);
881 eventDialog.setVisible(false);
885 private Action pasteEventAction = new AbstractAction() {
886 { putValue(NAME,"Paste"); }
887 public void actionPerformed(ActionEvent e) {
888 long tick = tickPositionModel.getTickPosition();
889 clipBoard.paste(trackModel, tick);
890 scrollToEventAt(tick);
891 // ペーストで曲の長さが変わったことをプレイリストに通知
892 SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
893 seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
894 eventDialog.setVisible(false);
898 private boolean applyEvent() {
899 long tick = tickPositionModel.getTickPosition();
900 MidiMessageForm form = eventDialog.midiMessageForm;
901 SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
902 MidiEvent newMidiEvent = new MidiEvent(form.getMessage(seqModel.charset), tick);
903 if( midiEventsToBeOverwritten != null ) {
904 // 上書き消去するための選択済イベントがあった場合
905 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
907 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
908 System.out.println("addMidiEvent failure");
911 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
912 ShortMessage sm = form.createPartnerMessage();
914 scrollToEventAt( tick );
916 int duration = form.durationForm.getDuration();
917 if( form.isNote(false) ) {
918 duration = -duration;
920 long partnerTick = tick + (long)duration;
921 if( partnerTick < 0L ) partnerTick = 0L;
922 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
923 if( ! trackModel.addMidiEvent(partner) ) {
924 System.out.println("addMidiEvent failure (note on/off partner message)");
926 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
929 seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
930 eventDialog.setVisible(false);
934 private EventEditContext editContext = new EventEditContext();
936 * 指定のTick位置へジャンプするアクション
938 Action queryJumpEventAction = new AbstractAction() {
940 putValue(NAME,"Jump to ...");
943 public void actionPerformed(ActionEvent e) {
944 editContext.setSelectedEvent(getModel());
945 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
951 Action queryAddEventAction = new AbstractAction() {
953 putValue(NAME,"New");
956 public void actionPerformed(ActionEvent e) {
957 TrackEventListTableModel model = getModel();
958 editContext.setSelectedEvent(model);
959 editContext.midiEventsToBeOverwritten = null;
960 eventDialog.openEventForm(
962 eventCellEditor.applyEventAction,
968 * MIDIイベントのコピー&ペーストを行うためのクリップボード
970 private class LocalClipBoard {
971 private MidiEvent copiedEventsToPaste[];
972 private int copiedEventsPPQ = 0;
973 public void copy(TrackEventListTableModel model, boolean withRemove) {
974 copiedEventsToPaste = model.getSelectedMidiEvents();
975 copiedEventsPPQ = model.sequenceTrackListTableModel.getSequence().getResolution();
976 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
977 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
978 queryPasteEventAction.setEnabled(en);
980 public void cut(TrackEventListTableModel model) {copy(model,true);}
981 public void copy(TrackEventListTableModel model){copy(model,false);}
982 public void paste(TrackEventListTableModel model, long tick) {
983 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
986 private LocalClipBoard clipBoard = new LocalClipBoard();
988 * 指定のTick位置へ貼り付けるアクション
990 Action queryPasteEventAction = new AbstractAction() {
992 putValue(NAME,"Paste to ...");
995 public void actionPerformed(ActionEvent e) {
996 editContext.setSelectedEvent(getModel());
997 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
1003 public Action cutEventAction = new AbstractAction("Cut") {
1008 public void actionPerformed(ActionEvent e) {
1009 TrackEventListTableModel model = getModel();
1010 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
1012 clipBoard.cut(model);
1018 public Action copyEventAction = new AbstractAction("Copy") {
1023 public void actionPerformed(ActionEvent e) {
1024 clipBoard.copy(getModel());
1030 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
1035 public void actionPerformed(ActionEvent e) {
1036 TrackEventListTableModel model = getModel();
1037 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1039 model.removeSelectedMidiEvents();
1045 private MidiEventCellEditor eventCellEditor;
1049 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1051 * MIDIイベントセルエディタを構築します。
1053 public MidiEventCellEditor() {
1054 eventDialog.midiMessageForm.setOutputMidiChannels(virtualMidiDevice.getChannels());
1055 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1056 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1057 getColumnModel().getColumn(index).setCellEditor(this);
1060 * セルをダブルクリックしないと編集できないようにします。
1061 * @param e イベント(マウスイベント)
1062 * @return 編集可能になったらtrue
1065 public boolean isCellEditable(EventObject e) {
1066 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1067 return ((MouseEvent)e).getClickCount() == 2;
1070 public Object getCellEditorValue() { return null; }
1072 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1074 private ComponentListener dialogComponentListener = new ComponentAdapter() {
1076 public void componentHidden(ComponentEvent e) {
1077 fireEditingCanceled();
1079 eventDialog.removeComponentListener(this);
1085 private Action editEventAction = new AbstractAction() {
1086 public void actionPerformed(ActionEvent e) {
1087 TrackEventListTableModel model = getModel();
1088 editContext.setSelectedEvent(model);
1089 if( editContext.selectedMidiEvent == null )
1091 editContext.setupForEdit(model);
1092 eventDialog.addComponentListener(dialogComponentListener);
1093 eventDialog.openEventForm("Change MIDI event", applyEventAction);
1099 private JButton editEventButton = new JButton(editEventAction){{
1100 setHorizontalAlignment(JButton.LEFT);
1103 public Component getTableCellEditorComponent(
1104 JTable table, Object value, boolean isSelected, int row, int column
1106 editEventButton.setText(value.toString());
1107 return editEventButton;
1110 * 入力したイベントを反映するアクション
1112 private Action applyEventAction = new AbstractAction() {
1114 putValue(NAME,"OK");
1116 public void actionPerformed(ActionEvent e) {
1117 if( editContext.applyEvent() ) fireEditingStopped();
1122 * スクロール可能なMIDIイベントテーブルビュー
1124 private JScrollPane scrollPane = new JScrollPane(this);
1126 * 指定の MIDI tick のイベントへスクロールします。
1127 * @param tick MIDI tick
1129 public void scrollToEventAt(long tick) {
1130 int index = getModel().tickToIndex(tick);
1131 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1132 getSelectionModel().setSelectionInterval(index, index);
1137 * 新しい {@link MidiSequenceEditor} を構築します。
1138 * @param deviceModelList MIDIデバイスモデルリスト
1140 public MidiSequenceEditor(MidiSequencerModel sequencerModel) {
1141 sequenceListTable = new SequenceListTable(new PlaylistTableModel(sequencerModel));
1142 trackListTable = new TrackListTable(
1143 new SequenceTrackListTableModel(sequenceListTable.getModel(), null, null)
1145 eventListTable = new EventListTable(new TrackEventListTableModel(trackListTable.getModel(), null));
1146 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1147 setBounds( 150, 200, 900, 500 );
1148 setLayout(new FlowLayout());
1149 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, dropTargetListener, true);
1152 JPanel playlistPanel = new JPanel() {{
1153 JPanel playlistOperationPanel = new JPanel() {{
1154 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1155 add(Box.createRigidArea(new Dimension(10, 0)));
1156 add(new JButton(newSequenceDialog.openAction) {{ setMargin(ZERO_INSETS); }});
1157 if( sequenceListTable.midiFileChooser != null ) {
1158 add( Box.createRigidArea(new Dimension(5, 0)) );
1159 add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {{
1160 setMargin(ZERO_INSETS);
1163 if(sequenceListTable.base64EncodeAction != null) {
1164 add(Box.createRigidArea(new Dimension(5, 0)));
1165 add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1167 add(Box.createRigidArea(new Dimension(5, 0)));
1168 PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
1169 add(new JButton(playlistTableModel.moveToTopAction) {{ setMargin(ZERO_INSETS); }});
1170 add(Box.createRigidArea(new Dimension(5, 0)));
1171 add(new JButton(playlistTableModel.moveToBottomAction) {{ setMargin(ZERO_INSETS); }});
1172 if( sequenceListTable.midiFileChooser != null ) {
1173 add(Box.createRigidArea(new Dimension(5, 0)));
1174 add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {{
1175 setMargin(ZERO_INSETS);
1178 add( Box.createRigidArea(new Dimension(5, 0)) );
1179 add(new JButton(sequenceListTable.deleteSequenceAction) {{ setMargin(ZERO_INSETS); }});
1180 add( Box.createRigidArea(new Dimension(5, 0)) );
1181 add(new SequencerSpeedSlider(playlistTableModel.sequencerModel.speedSliderModel));
1182 add( Box.createRigidArea(new Dimension(5, 0)) );
1184 MidiSequencerModel sequencerModel = sequenceListTable.getModel().sequencerModel;
1185 add(new JLabel("SyncMode:"));
1186 add(new JLabel("Master"));
1187 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1188 add(new JLabel("Slave"));
1189 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1192 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1193 add(new JScrollPane(sequenceListTable));
1194 add(Box.createRigidArea(new Dimension(0, 10)));
1195 add(playlistOperationPanel);
1196 add(Box.createRigidArea(new Dimension(0, 10)));
1198 JPanel trackListPanel = new JPanel() {{
1199 JPanel trackListOperationPanel = new JPanel() {{
1200 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ZERO_INSETS); }});
1201 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
1203 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1204 add(trackListTable.titleLabel);
1205 add(Box.createRigidArea(new Dimension(0, 5)));
1206 add(new JScrollPane(trackListTable));
1207 add(Box.createRigidArea(new Dimension(0, 5)));
1208 add(trackListOperationPanel);
1210 JPanel eventListPanel = new JPanel() {{
1211 JPanel eventListOperationPanel = new JPanel() {{
1212 add(new JCheckBox("Pair NoteON/OFF") {{
1213 setModel(eventListTable.pairNoteOnOffModel);
1214 setToolTipText("NoteON/OFFをペアで同時選択する");
1216 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ZERO_INSETS); }});
1217 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ZERO_INSETS); }});
1218 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ZERO_INSETS); }});
1219 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ZERO_INSETS); }});
1220 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ZERO_INSETS); }});
1221 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ZERO_INSETS); }});
1223 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1224 add(eventListTable.titleLabel);
1225 add(eventListTable.scrollPane);
1226 add(eventListOperationPanel);
1228 Container cp = getContentPane();
1229 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1230 cp.add(Box.createVerticalStrut(2));
1232 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1233 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1234 setDividerLocation(300);
1237 setDividerLocation(160);