OSDN Git Service

944a81b807e32ad7c2e97d8890e752d3ef1dd084
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / MidiSequenceEditor.java
1 package camidion.chordhelper.midieditor;
2
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;
23 import java.io.File;
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;
31 import java.util.Map;
32 import java.util.Set;
33
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;
71
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;
77
78 /**
79  * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
80  *
81  * @author
82  *      Copyright (C) 2006-2016 Akiyoshi Kamide
83  *      http://www.yk.rim.or.jp/~kamide/music/chordhelper/
84  */
85 public class MidiSequenceEditor extends JDialog {
86         /**
87          * このダイアログを表示するアクション
88          */
89         public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
90                 {
91                         String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
92                         putValue(Action.SHORT_DESCRIPTION, tooltip);
93                 }
94                 @Override
95                 public void actionPerformed(ActionEvent e) {
96                         if( isVisible() ) toFront(); else setVisible(true);
97                 }
98         };
99
100         /**
101          * エラーメッセージダイアログを表示します。
102          * @param message エラーメッセージ
103          */
104         public void showError(String message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
105         /**
106          * 警告メッセージダイアログを表示します。
107          * @param message 警告メッセージ
108          */
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);
112         }
113         /**
114          * 確認ダイアログを表示します。
115          * @param message 確認メッセージ
116          * @return 確認OKのときtrue
117          */
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 ;
121         }
122
123         /**
124          * ドロップされた複数のMIDIファイルを読み込むリスナー
125          */
126         public final DropTargetListener dropTargetListener = new DropTargetAdapter() {
127                 @Override
128                 public void dragEnter(DropTargetDragEvent event) {
129                         if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
130                                 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
131                         }
132                 }
133                 @Override
134                 @SuppressWarnings("unchecked")
135                 public void drop(DropTargetDropEvent event) {
136                         event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
137                         try {
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);
143                                                 return;
144                                         }
145                                 }
146                         }
147                         catch (Exception ex) {
148                                 ex.printStackTrace();
149                         }
150                         event.dropComplete(false);
151                 }
152         };
153
154         /**
155          * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
156          * すでに再生されていた場合、このエディタダイアログを表示します。
157          *
158          * @param fileList 読み込むMIDIファイルのリスト
159          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
160          * @see #loadAndPlay(File)
161          */
162         public void loadAndPlay(List<File> fileList) {
163                 int indexOfAddedTop = -1;
164                 PlaylistTableModel playlist = sequenceListTable.getModel();
165                 try {
166                         indexOfAddedTop = playlist.addSequences(fileList);
167                 } catch(IOException|InvalidMidiDataException e) {
168                         showWarning(e.getMessage());
169                 } catch(AccessControlException e) {
170                         showError(e.getMessage());
171                         e.printStackTrace();
172                 }
173                 if( playlist.sequencerModel.getSequencer().isRunning() ) {
174                         String command = (String)openAction.getValue(Action.NAME);
175                         openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
176                         return;
177                 }
178                 if( indexOfAddedTop >= 0 ) {
179                         try {
180                                 playlist.loadToSequencer(indexOfAddedTop);
181                         } catch (InvalidMidiDataException ex) {
182                                 ex.printStackTrace();
183                                 showError(ex.getMessage());
184                                 return;
185                         }
186                         playlist.sequencerModel.start();
187                 }
188         }
189         /**
190          * 1件のMIDIファイルを読み込み、再生されていなかったら再生します。
191          * すでに再生されていた場合、このエディタダイアログを表示します。
192          *
193          * @param file 読み込むMIDIファイル
194          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
195          * @see #loadAndPlay(List) loadAndPlay(List&lt;File&gt;)
196          */
197         public void loadAndPlay(File file) throws InvalidMidiDataException {
198                 loadAndPlay(Arrays.asList(file));
199         }
200
201         private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
202         private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
203         /**
204          * 新しいMIDIシーケンスを生成するダイアログ
205          */
206         public NewSequenceDialog newSequenceDialog;
207         /**
208          * BASE64テキスト入力ダイアログ
209          */
210         public Base64Dialog base64Dialog = new Base64Dialog(this);
211         /**
212          * プレイリストビュー(シーケンスリスト)
213          */
214         public SequenceListTable sequenceListTable;
215         /**
216          * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
217          */
218         private TrackListTable trackListTable;
219         /**
220          * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
221          */
222         private EventListTable eventListTable;
223         /**
224          * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
225          */
226         public MidiEventDialog eventDialog = new MidiEventDialog();
227         private VirtualMidiDevice outputMidiDevice;
228         /**
229          * プレイリストビュー(シーケンスリスト)
230          */
231         public class SequenceListTable extends JTable {
232                 /**
233                  * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
234                  */
235                 private MidiFileChooser midiFileChooser;
236                 /**
237                  * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
238                  */
239                 private Action base64EncodeAction;
240                 /**
241                  * プレイリストビューを構築します。
242                  * @param model プレイリストデータモデル
243                  */
244                 public SequenceListTable(PlaylistTableModel model) {
245                         super(model, null, model.sequenceListSelectionModel);
246                         try {
247                                 midiFileChooser = new MidiFileChooser();
248                         }
249                         catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
250                                 // アプレットの場合、Webクライアントマシンのローカルファイルには
251                                 // アクセスできないので、ファイル選択ダイアログは使用不可。
252                                 midiFileChooser = null;
253                         }
254                         // 再生ボタンを埋め込む
255                         new PlayButtonCellEditor();
256                         new PositionCellEditor();
257                         //
258                         // 文字コード選択をプルダウンにする
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());
263                         }});
264                         getColumnModel().getColumn(column).setCellEditor(ce);
265                         setAutoCreateColumnsFromModel(false);
266                         //
267                         // Base64エンコードアクションの生成
268                         if( base64Dialog.isBase64Available() ) {
269                                 base64EncodeAction = new AbstractAction("Base64") {
270                                         {
271                                                 String tooltip = "Base64 text conversion - Base64テキスト変換";
272                                                 putValue(Action.SHORT_DESCRIPTION, tooltip);
273                                         }
274                                         @Override
275                                         public void actionPerformed(ActionEvent e) {
276                                                 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
277                                                 byte[] data = null;
278                                                 String filename = null;
279                                                 if( mstm != null ) {
280                                                         data = mstm.getMIDIdata();
281                                                         filename = mstm.getFilename();
282                                                 }
283                                                 base64Dialog.setMIDIData(data, filename);
284                                                 base64Dialog.setVisible(true);
285                                         }
286                                 };
287                         }
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;
293                         }
294                 }
295                 private TableColumn lengthColumn;
296                 @Override
297                 public void tableChanged(TableModelEvent event) {
298                         super.tableChanged(event);
299                         //
300                         // タイトルに合計シーケンス長を表示
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);
306                         }
307                         //
308                         // シーケンス削除時など、合計シーケンス長が変わっても
309                         // 列モデルからではヘッダタイトルが再描画されないことがある。
310                         // そこで、ヘッダビューから repaint() で突っついて再描画させる。
311                         JTableHeader th = getTableHeader();
312                         if( th != null ) th.repaint();
313                 }
314                 /**
315                  * 時間位置表示セルエディタ(ダブルクリック専用)
316                  */
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);
322                         }
323                         /**
324                          * セルをダブルクリックしたときだけ編集モードに入るようにします。
325                          * @param e イベント(マウスイベント)
326                          * @return 編集可能になったらtrue
327                          */
328                         @Override
329                         public boolean isCellEditable(EventObject e) {
330                                 // マウスイベント以外のイベントでは編集不可
331                                 if( ! (e instanceof MouseEvent) ) return false;
332                                 return ((MouseEvent)e).getClickCount() == 2;
333                         }
334                         @Override
335                         public Object getCellEditorValue() { return null; }
336                         /**
337                          * 編集モード時のコンポーネントを返すタイミングで
338                          * そのシーケンスをシーケンサーにロードしたあと、
339                          * すぐに編集モードを解除します。
340                          * @return 常にnull
341                          */
342                         @Override
343                         public Component getTableCellEditorComponent(
344                                 JTable table, Object value, boolean isSelected, int row, int column
345                         ) {
346                                 try {
347                                         getModel().loadToSequencer(row);
348                                 } catch (InvalidMidiDataException ex) {
349                                         ex.printStackTrace();
350                                         showError(ex.getMessage());
351                                 }
352                                 fireEditingStopped();
353                                 return null;
354                         }
355                 }
356                 /**
357                  * プレイボタンを埋め込んだセルエディタ
358                  */
359                 private class PlayButtonCellEditor extends AbstractCellEditor
360                         implements TableCellEditor, TableCellRenderer
361                 {
362                         private JToggleButton playButton = new JToggleButton(
363                                 getModel().sequencerModel.startStopAction
364                         ) {
365                                 { setMargin(ZERO_INSETS); }
366                         };
367                         public PlayButtonCellEditor() {
368                                 int column = PlaylistTableModel.Column.PLAY.ordinal();
369                                 TableColumn tc = getColumnModel().getColumn(column);
370                                 tc.setCellRenderer(this);
371                                 tc.setCellEditor(this);
372                         }
373                         /**
374                          * {@inheritDoc}
375                          *
376                          * <p>この実装では、クリックしたセルのシーケンスが
377                          * シーケンサーにロードされている場合に
378                          * trueを返してプレイボタンを押せるようにします。
379                          * そうでない場合はプレイボタンのないセルなので、
380                          * ダブルクリックされたときだけtrueを返します。
381                          * </p>
382                          */
383                         @Override
384                         public boolean isCellEditable(EventObject e) {
385                                 // マウスイベント以外はデフォルトメソッドにお任せ
386                                 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
387                                 fireEditingStopped();
388                                 MouseEvent me = (MouseEvent)e;
389                                 //
390                                 // クリックされたセルの行を特定
391                                 int row = rowAtPoint(me.getPoint());
392                                 if( row < 0 ) return false;
393                                 PlaylistTableModel model = getModel();
394                                 if( row >= model.getRowCount() ) return false;
395                                 //
396                                 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
397                                 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
398                                 return model.getSequenceList().get(row).isOnSequencer() || me.getClickCount() == 2;
399                         }
400                         @Override
401                         public Object getCellEditorValue() { return null; }
402                         /**
403                          * {@inheritDoc}
404                          *
405                          * <p>この実装では、行の表すシーケンスが
406                          * シーケンサーにロードされている場合にプレイボタンを返します。
407                          * そうでない場合は、
408                          * そのシーケンスをシーケンサーにロードしてnullを返します。
409                          * </p>
410                          */
411                         @Override
412                         public Component getTableCellEditorComponent(
413                                 JTable table, Object value, boolean isSelected, int row, int column
414                         ) {
415                                 fireEditingStopped();
416                                 PlaylistTableModel model = getModel();
417                                 if( model.getSequenceList().get(row).isOnSequencer() ) return playButton;
418                                 try {
419                                         model.loadToSequencer(row);
420                                 } catch (InvalidMidiDataException ex) {
421                                         ex.printStackTrace();
422                                         showError(ex.getMessage());
423                                 }
424                                 return null;
425                         }
426                         @Override
427                         public Component getTableCellRendererComponent(
428                                 JTable table, Object value, boolean isSelected,
429                                 boolean hasFocus, int row, int column
430                         ) {
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
437                                 );
438                         }
439                 }
440                 /**
441                  * このプレイリスト(シーケンスリスト)が表示するデータを提供する
442                  * プレイリストモデルを返します。
443                  * @return プレイリストモデル
444                  */
445                 @Override
446                 public PlaylistTableModel getModel() { return (PlaylistTableModel)super.getModel(); }
447                 /**
448                  * シーケンスを削除するアクション
449                  */
450                 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
451                         "Delete", MidiSequenceEditor.deleteIcon,
452                         "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
453                 ) {
454                         @Override
455                         public void actionPerformed(ActionEvent event) {
456                                 PlaylistTableModel model = getModel();
457                                 if( midiFileChooser != null ) {
458                                         if( model.getSelectedSequenceModel().isModified() ) {
459                                                 String message =
460                                                         "Selected MIDI sequence not saved - delete it ?\n" +
461                                                         "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
462                                                 if( ! confirm(message) ) return;
463                                         }
464                                 }
465                                 try {
466                                         model.removeSelectedSequence();
467                                 } catch (InvalidMidiDataException ex) {
468                                         ex.printStackTrace();
469                                         showError(ex.getMessage());
470                                 }
471                         }
472                 };
473                 /**
474                  * ファイル選択ダイアログ(アプレットでは使用不可)
475                  */
476                 private class MidiFileChooser extends JFileChooser {
477                         {
478                                 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
479                         }
480                         /**
481                          * ファイル保存アクション
482                          */
483                         public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
484                                 "Save",
485                                 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
486                         ) {
487                                 @Override
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();
494                                         if( f.exists() ) {
495                                                 fn = f.getName();
496                                                 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
497                                         }
498                                         try ( FileOutputStream out = new FileOutputStream(f) ) {
499                                                 out.write(sequenceModel.getMIDIdata());
500                                                 sequenceModel.setModified(false);
501                                         }
502                                         catch( IOException ex ) {
503                                                 showError( ex.getMessage() );
504                                                 ex.printStackTrace();
505                                         }
506                                 }
507                         };
508                         /**
509                          * ファイルを開くアクション
510                          */
511                         public Action openMidiFileAction = new AbstractAction("Open") {
512                                 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
513                                 @Override
514                                 public void actionPerformed(ActionEvent event) {
515                                         if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
516                                         try {
517                                                 loadAndPlay(getSelectedFile());
518                                         } catch (InvalidMidiDataException ex) {
519                                                 ex.printStackTrace();
520                                                 showError(ex.getMessage());
521                                         }
522                                 }
523                         };
524                 };
525         }
526
527         /**
528          * シーケンス(トラックリスト)テーブルビュー
529          */
530         public class TrackListTable extends JTable {
531                 /**
532                  * トラックリストテーブルビューを構築します。
533                  * @param model シーケンス(トラックリスト)データモデル
534                  */
535                 public TrackListTable(SequenceTrackListTableModel model) {
536                         super(model, null, model.trackListSelectionModel);
537                         //
538                         // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
539                         int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
540                         TableColumn tc = getColumnModel().getColumn(colIndex);
541                         tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
542                                 addItem("OFF");
543                                 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
544                                 addItem("ALL");
545                         }}));
546                         setAutoCreateColumnsFromModel(false);
547                         //
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);
554                 }
555                 /**
556                  * このテーブルビューが表示するデータを提供する
557                  * シーケンス(トラックリスト)データモデルを返します。
558                  * @return シーケンス(トラックリスト)データモデル
559                  */
560                 @Override
561                 public SequenceTrackListTableModel getModel() {
562                         return (SequenceTrackListTableModel) super.getModel();
563                 }
564                 /**
565                  * タイトルラベル
566                  */
567                 TitleLabel titleLabel;
568                 /**
569                  * 親テーブルの選択シーケンスの変更に反応する
570                  * 曲番号表示付きタイトルラベル
571                  */
572                 private class TitleLabel extends JLabel implements ListSelectionListener {
573                         private static final String TITLE = "Tracks";
574                         public TitleLabel() { setText(TITLE); }
575                         @Override
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;
581                                 //
582                                 // MIDIチャンネル選択中のときはキャンセルする
583                                 cancelCellEditing();
584                                 //
585                                 int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();
586                                 String text = TITLE;
587                                 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
588                                 setText(text);
589                                 if( newModel == null ) {
590                                         newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;
591                                         addTrackAction.setEnabled(false);
592                                 }
593                                 else {
594                                         addTrackAction.setEnabled(true);
595                                 }
596                                 oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);
597                                 setModel(newModel);
598                                 setSelectionModel(newModel.trackListSelectionModel);
599                                 newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);
600                                 trackSelectionListener.valueChanged(null);
601                         }
602                 }
603                 /**
604                  * トラック選択リスナー
605                  */
606                 TrackSelectionListener trackSelectionListener;
607                 /**
608                  * 選択トラックの変更に反応するリスナー
609                  */
610                 private class TrackSelectionListener implements ListSelectionListener {
611                         @Override
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());
617                         }
618                 }
619                 /**
620                  * {@inheritDoc}
621                  *
622                  * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
623                  * 例えば、イベントが編集された場合や、
624                  * シーケンサーからこのモデルが外された場合がこれに該当します。
625                  * </p>
626                  */
627                 @Override
628                 public void tableChanged(TableModelEvent e) {
629                         super.tableChanged(e);
630                         cancelCellEditing();
631                 }
632                 /**
633                  * このトラックリストテーブルが編集モードになっていたら解除します。
634                  */
635                 private void cancelCellEditing() {
636                         TableCellEditor currentCellEditor = getCellEditor();
637                         if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
638                 }
639                 /**
640                  * トラック追加アクション
641                  */
642                 Action addTrackAction = new AbstractAction("New") {
643                         {
644                                 String tooltip = "Append new track - 新しいトラックの追加";
645                                 putValue(Action.SHORT_DESCRIPTION, tooltip);
646                                 setEnabled(false);
647                         }
648                         @Override
649                         public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
650                 };
651                 /**
652                  * トラック削除アクション
653                  */
654                 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
655                         {
656                                 String tooltip = "Delete selected track - 選択したトラックを削除";
657                                 putValue(Action.SHORT_DESCRIPTION, tooltip);
658                                 setEnabled(false);
659                         }
660                         @Override
661                         public void actionPerformed(ActionEvent e) {
662                                 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
663                                 if( confirm(message) ) getModel().deleteSelectedTracks();
664                         }
665                 };
666         }
667
668         /**
669          * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
670          */
671         public class EventListTable extends JTable {
672                 /**
673                  * 新しいイベントリストテーブルを構築します。
674                  * <p>データモデルとして一つのトラックのイベントリストを指定できます。
675                  * トラックを切り替えたいときは {@link #setModel(TableModel)}
676                  * でデータモデルを異なるトラックのものに切り替えます。
677                  * </p>
678                  *
679                  * @param model トラック(イベントリスト)データモデル
680                  */
681                 public EventListTable(TrackEventListTableModel model) {
682                         super(model, null, model.eventSelectionModel);
683                         //
684                         // 列モデルにセルエディタを設定
685                         eventCellEditor = new MidiEventCellEditor();
686                         setAutoCreateColumnsFromModel(false);
687                         //
688                         eventSelectionListener = new EventSelectionListener();
689                         titleLabel = new TitleLabel();
690                         //
691                         TableColumnModel colModel = getColumnModel();
692                         for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
693                                 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
694                 }
695                 /**
696                  * このテーブルビューが表示するデータを提供する
697                  * トラック(イベントリスト)データモデルを返します。
698                  * @return トラック(イベントリスト)データモデル
699                  */
700                 @Override
701                 public TrackEventListTableModel getModel() {
702                         return (TrackEventListTableModel) super.getModel();
703                 }
704                 /**
705                  * タイトルラベル
706                  */
707                 TitleLabel titleLabel;
708                 /**
709                  * 親テーブルの選択トラックの変更に反応する
710                  * トラック番号つきタイトルラベル
711                  */
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) {
716                                 String text = TITLE;
717                                 TrackEventListTableModel oldTrackModel = getModel();
718                                 int index = tlsm.getMinSelectionIndex();
719                                 if( index >= 0 ) {
720                                         text = String.format(TITLE+" - track No.%d", index);
721                                 }
722                                 setText(text);
723                                 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
724                                 if( oldTrackModel == newTrackModel )
725                                         return;
726                                 if( newTrackModel == null ) {
727                                         newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;
728                                         queryJumpEventAction.setEnabled(false);
729                                         queryAddEventAction.setEnabled(false);
730
731                                         queryPasteEventAction.setEnabled(false);
732                                         copyEventAction.setEnabled(false);
733                                         deleteEventAction.setEnabled(false);
734                                         cutEventAction.setEnabled(false);
735                                 }
736                                 else {
737                                         queryJumpEventAction.setEnabled(true);
738                                         queryAddEventAction.setEnabled(true);
739                                 }
740                                 oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);
741                                 setModel(newTrackModel);
742                                 setSelectionModel(newTrackModel.eventSelectionModel);
743                                 newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);
744                         }
745                 }
746
747                 /**
748                  * イベント選択リスナー
749                  */
750                 private EventSelectionListener eventSelectionListener;
751                 /**
752                  * 選択イベントの変更に反応するリスナー
753                  */
754                 private class EventSelectionListener implements ListSelectionListener {
755                         public EventSelectionListener() {
756                                 getModel().eventSelectionModel.addListSelectionListener(this);
757                         }
758                         @Override
759                         public void valueChanged(ListSelectionEvent e) {
760                                 if( e.getValueIsAdjusting() )
761                                         return;
762                                 if( getSelectionModel().isSelectionEmpty() ) {
763                                         queryPasteEventAction.setEnabled(false);
764                                         copyEventAction.setEnabled(false);
765                                         deleteEventAction.setEnabled(false);
766                                         cutEventAction.setEnabled(false);
767                                 }
768                                 else {
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 ) {
781                                                                 // ノート番号を持つ場合、音を鳴らす。
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);
788                                                         }
789                                                 }
790                                         }
791                                         if( pairNoteOnOffModel.isSelected() ) {
792                                                 int maxIndex = getSelectionModel().getMaxSelectionIndex();
793                                                 int partnerIndex;
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);
799                                                 }
800                                         }
801                                 }
802                         }
803                 }
804                 /**
805                  * Pair noteON/OFF トグルボタンモデル
806                  */
807                 private JToggleButton.ToggleButtonModel
808                         pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
809                                 {
810                                         addItemListener(new ItemListener() {
811                                                 public void itemStateChanged(ItemEvent e) {
812                                                         eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
813                                                 }
814                                         });
815                                         setSelected(true);
816                                 }
817                         };
818                 private class EventEditContext {
819                         /**
820                          * 編集対象トラック
821                          */
822                         private TrackEventListTableModel trackModel;
823                         /**
824                          * tick位置入力モデル
825                          */
826                         private TickPositionModel tickPositionModel = new TickPositionModel();
827                         /**
828                          * 選択されたイベント
829                          */
830                         private MidiEvent selectedMidiEvent = null;
831                         /**
832                          * 選択されたイベントの場所
833                          */
834                         private int selectedIndex = -1;
835                         /**
836                          * 選択されたイベントのtick位置
837                          */
838                         private long currentTick = 0;
839                         /**
840                          * 上書きして削除対象にする変更前イベント(null可)
841                          */
842                         private MidiEvent[] midiEventsToBeOverwritten;
843                         /**
844                          * 選択したイベントを入力ダイアログなどに反映します。
845                          * @param model 対象データモデル
846                          */
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());
853
854                                 selectedIndex = trackModel.eventSelectionModel.getMinSelectionIndex();
855                                 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
856                                 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
857                                 tickPositionModel.setTickPosition(currentTick);
858                         }
859                         public void setupForEdit(TrackEventListTableModel trackModel) {
860                                 MidiEvent partnerEvent = null;
861                                 eventDialog.midiMessageForm.setMessage(
862                                         selectedMidiEvent.getMessage(),
863                                         trackModel.sequenceTrackListTableModel.charset
864                                 );
865                                 if( eventDialog.midiMessageForm.isNote() ) {
866                                         int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
867                                         if( partnerIndex < 0 ) {
868                                                 eventDialog.midiMessageForm.durationForm.setDuration(0);
869                                         }
870                                         else {
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);
876                                         }
877                                 }
878                                 if(partnerEvent == null)
879                                         midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
880                                 else
881                                         midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
882                         }
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);
889                                         trackModel = null;
890                                 }
891                         };
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);
902                                         trackModel = null;
903                                 }
904                         };
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);
913                                 }
914                                 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
915                                         System.out.println("addMidiEvent failure");
916                                         return false;
917                                 }
918                                 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
919                                         ShortMessage sm = form.createPartnerMessage();
920                                         if(sm == null)
921                                                 scrollToEventAt( tick );
922                                         else {
923                                                 int duration = form.durationForm.getDuration();
924                                                 if( form.isNote(false) ) {
925                                                         duration = -duration;
926                                                 }
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)");
932                                                 }
933                                                 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
934                                         }
935                                 }
936                                 seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
937                                 eventDialog.setVisible(false);
938                                 return true;
939                         }
940                 }
941                 private EventEditContext editContext = new EventEditContext();
942                 /**
943                  * 指定のTick位置へジャンプするアクション
944                  */
945                 Action queryJumpEventAction = new AbstractAction() {
946                         {
947                                 putValue(NAME,"Jump to ...");
948                                 setEnabled(false);
949                         }
950                         public void actionPerformed(ActionEvent e) {
951                                 editContext.setSelectedEvent(getModel());
952                                 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
953                         }
954                 };
955                 /**
956                  * 新しいイベントの追加を行うアクション
957                  */
958                 Action queryAddEventAction = new AbstractAction() {
959                         {
960                                 putValue(NAME,"New");
961                                 setEnabled(false);
962                         }
963                         public void actionPerformed(ActionEvent e) {
964                                 TrackEventListTableModel model = getModel();
965                                 editContext.setSelectedEvent(model);
966                                 editContext.midiEventsToBeOverwritten = null;
967                                 eventDialog.openEventForm(
968                                         "New MIDI event",
969                                         eventCellEditor.applyEventAction,
970                                         model.getChannel()
971                                 );
972                         }
973                 };
974                 /**
975                  * MIDIイベントのコピー&ペーストを行うためのクリップボード
976                  */
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);
986                         }
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);
991                         }
992                 }
993                 private LocalClipBoard clipBoard = new LocalClipBoard();
994                 /**
995                  * 指定のTick位置へ貼り付けるアクション
996                  */
997                 Action queryPasteEventAction = new AbstractAction() {
998                         {
999                                 putValue(NAME,"Paste to ...");
1000                                 setEnabled(false);
1001                         }
1002                         public void actionPerformed(ActionEvent e) {
1003                                 editContext.setSelectedEvent(getModel());
1004                                 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
1005                         }
1006                 };
1007                 /**
1008                  * イベントカットアクション
1009                  */
1010                 public Action cutEventAction = new AbstractAction("Cut") {
1011                         {
1012                                 setEnabled(false);
1013                         }
1014                         @Override
1015                         public void actionPerformed(ActionEvent e) {
1016                                 TrackEventListTableModel model = getModel();
1017                                 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
1018                                         return;
1019                                 clipBoard.cut(model);
1020                         }
1021                 };
1022                 /**
1023                  * イベントコピーアクション
1024                  */
1025                 public Action copyEventAction = new AbstractAction("Copy") {
1026                         {
1027                                 setEnabled(false);
1028                         }
1029                         @Override
1030                         public void actionPerformed(ActionEvent e) {
1031                                 clipBoard.copy(getModel());
1032                         }
1033                 };
1034                 /**
1035                  * イベント削除アクション
1036                  */
1037                 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
1038                         {
1039                                 setEnabled(false);
1040                         }
1041                         @Override
1042                         public void actionPerformed(ActionEvent e) {
1043                                 TrackEventListTableModel model = getModel();
1044                                 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1045                                         return;
1046                                 model.removeSelectedMidiEvents();
1047                         }
1048                 };
1049                 /**
1050                  * MIDIイベント表のセルエディタ
1051                  */
1052                 private MidiEventCellEditor eventCellEditor;
1053                 /**
1054                  * MIDIイベント表のセルエディタ
1055                  */
1056                 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1057                         /**
1058                          * MIDIイベントセルエディタを構築します。
1059                          */
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);
1065                         }
1066                         /**
1067                          * セルをダブルクリックしないと編集できないようにします。
1068                          * @param e イベント(マウスイベント)
1069                          * @return 編集可能になったらtrue
1070                          */
1071                         @Override
1072                         public boolean isCellEditable(EventObject e) {
1073                                 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1074                                 return ((MouseEvent)e).getClickCount() == 2;
1075                         }
1076                         @Override
1077                         public Object getCellEditorValue() { return null; }
1078                         /**
1079                          * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1080                          */
1081                         private ComponentListener dialogComponentListener = new ComponentAdapter() {
1082                                 @Override
1083                                 public void componentHidden(ComponentEvent e) {
1084                                         fireEditingCanceled();
1085                                         // 用が済んだら当リスナーを除去
1086                                         eventDialog.removeComponentListener(this);
1087                                 }
1088                         };
1089                         /**
1090                          * 既存イベントを編集するアクション
1091                          */
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 )
1097                                                 return;
1098                                         editContext.setupForEdit(model);
1099                                         eventDialog.addComponentListener(dialogComponentListener);
1100                                         eventDialog.openEventForm("Change MIDI event", applyEventAction);
1101                                 }
1102                         };
1103                         /**
1104                          * イベント編集ボタン
1105                          */
1106                         private JButton editEventButton = new JButton(editEventAction){{
1107                                 setHorizontalAlignment(JButton.LEFT);
1108                         }};
1109                         @Override
1110                         public Component getTableCellEditorComponent(
1111                                 JTable table, Object value, boolean isSelected, int row, int column
1112                         ) {
1113                                 editEventButton.setText(value.toString());
1114                                 return editEventButton;
1115                         }
1116                         /**
1117                          * 入力したイベントを反映するアクション
1118                          */
1119                         private Action applyEventAction = new AbstractAction() {
1120                                 {
1121                                         putValue(NAME,"OK");
1122                                 }
1123                                 public void actionPerformed(ActionEvent e) {
1124                                         if( editContext.applyEvent() ) fireEditingStopped();
1125                                 }
1126                         };
1127                 }
1128                 /**
1129                  * スクロール可能なMIDIイベントテーブルビュー
1130                  */
1131                 private JScrollPane scrollPane = new JScrollPane(this);
1132                 /**
1133                  * 指定の MIDI tick のイベントへスクロールします。
1134                  * @param tick MIDI tick
1135                  */
1136                 public void scrollToEventAt(long tick) {
1137                         int index = getModel().tickToIndex(tick);
1138                         scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1139                         getSelectionModel().setSelectionInterval(index, index);
1140                 }
1141         }
1142
1143         /**
1144          * 新しい {@link MidiSequenceEditor} を構築します。
1145          * @param playlistTableModel このエディタが参照するプレイリストモデル
1146          * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1147          */
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)
1153                 );
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);
1160                 //
1161                 // パネルレイアウト
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); }});
1170                                 }
1171                                 if(sequenceListTable.base64EncodeAction != null) {
1172                                         add(Box.createRigidArea(new Dimension(5, 0)));
1173                                         add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1174                                 }
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); }});
1183                                 }
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)) );
1189                                 add(new JPanel() {{
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));
1196                                 }});
1197                         }};
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)));
1203                 }};
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); }});
1208                         }};
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);
1215                 }};
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をペアで同時選択する");
1221                                 }});
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); }});
1228                         }};
1229                         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1230                         add(eventListTable.titleLabel);
1231                         add(eventListTable.scrollPane);
1232                         add(eventListOperationPanel);
1233                 }};
1234                 Container cp = getContentPane();
1235                 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1236                 cp.add(Box.createVerticalStrut(2));
1237                 cp.add(
1238                         new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1239                                 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1240                                         setDividerLocation(300);
1241                                 }}
1242                         ) {{
1243                                 setDividerLocation(160);
1244                         }}
1245                 );
1246         }
1247
1248 }