OSDN Git Service

パッケージ情報を追加、その他リファクタリング
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / MidiSequenceEditorDialog.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.event.ActionEvent;
10 import java.awt.event.ComponentAdapter;
11 import java.awt.event.ComponentEvent;
12 import java.awt.event.ComponentListener;
13 import java.awt.event.ItemEvent;
14 import java.awt.event.ItemListener;
15 import java.awt.event.MouseEvent;
16 import java.io.File;
17 import java.io.FileOutputStream;
18 import java.io.IOException;
19 import java.nio.charset.Charset;
20 import java.security.AccessControlException;
21 import java.util.Arrays;
22 import java.util.EventObject;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26
27 import javax.sound.midi.InvalidMidiDataException;
28 import javax.sound.midi.MidiChannel;
29 import javax.sound.midi.MidiEvent;
30 import javax.sound.midi.MidiMessage;
31 import javax.sound.midi.Sequence;
32 import javax.sound.midi.Sequencer;
33 import javax.sound.midi.ShortMessage;
34 import javax.swing.AbstractAction;
35 import javax.swing.AbstractCellEditor;
36 import javax.swing.Action;
37 import javax.swing.Box;
38 import javax.swing.BoxLayout;
39 import javax.swing.DefaultCellEditor;
40 import javax.swing.Icon;
41 import javax.swing.JButton;
42 import javax.swing.JCheckBox;
43 import javax.swing.JComboBox;
44 import javax.swing.JDialog;
45 import javax.swing.JFileChooser;
46 import javax.swing.JLabel;
47 import javax.swing.JOptionPane;
48 import javax.swing.JPanel;
49 import javax.swing.JScrollPane;
50 import javax.swing.JSplitPane;
51 import javax.swing.JTable;
52 import javax.swing.JToggleButton;
53 import javax.swing.ListSelectionModel;
54 import javax.swing.TransferHandler;
55 import javax.swing.border.EtchedBorder;
56 import javax.swing.event.ListSelectionEvent;
57 import javax.swing.event.ListSelectionListener;
58 import javax.swing.event.TableModelEvent;
59 import javax.swing.filechooser.FileNameExtensionFilter;
60 import javax.swing.table.JTableHeader;
61 import javax.swing.table.TableCellEditor;
62 import javax.swing.table.TableCellRenderer;
63 import javax.swing.table.TableColumn;
64 import javax.swing.table.TableColumnModel;
65 import javax.swing.table.TableModel;
66
67 import camidion.chordhelper.ButtonIcon;
68 import camidion.chordhelper.ChordHelperApplet;
69 import camidion.chordhelper.mididevice.MidiSequencerModel;
70 import camidion.chordhelper.mididevice.VirtualMidiDevice;
71 import camidion.chordhelper.music.MIDISpec;
72
73 /**
74  * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
75  *
76  * @author
77  *      Copyright (C) 2006-2016 Akiyoshi Kamide
78  *      http://www.yk.rim.or.jp/~kamide/music/chordhelper/
79  */
80 public class MidiSequenceEditorDialog extends JDialog {
81         /**
82          * このダイアログを表示するアクション
83          */
84         public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
85                 {
86                         String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
87                         putValue(Action.SHORT_DESCRIPTION, tooltip);
88                 }
89                 @Override
90                 public void actionPerformed(ActionEvent e) {
91                         if( isVisible() ) toFront(); else setVisible(true);
92                 }
93         };
94
95         /**
96          * エラーメッセージダイアログを表示します。
97          * @param message エラーメッセージ
98          */
99         public void showError(String message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
100         /**
101          * 警告メッセージダイアログを表示します。
102          * @param message 警告メッセージ
103          */
104         public void showWarning(String message) { showMessage(message, JOptionPane.WARNING_MESSAGE); }
105         private void showMessage(String message, int messageType) {
106                 JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, messageType);
107         }
108         /**
109          * 確認ダイアログを表示します。
110          * @param message 確認メッセージ
111          * @return 確認OKのときtrue
112          */
113         public boolean confirm(String message) {
114                 return JOptionPane.showConfirmDialog(this, message, ChordHelperApplet.VersionInfo.NAME,
115                                 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION ;
116         }
117
118         /**
119          * ドロップされた複数のMIDIファイルを読み込むハンドラー
120          */
121         public final TransferHandler transferHandler = new TransferHandler() {
122                 @Override
123                 public boolean canImport(TransferSupport support) {
124                         return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
125                 }
126                 @SuppressWarnings("unchecked")
127                 @Override
128                 public boolean importData(TransferSupport support) {
129                         try {
130                                 loadAndPlay((List<File>)support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
131                                 return true;
132                         } catch (Exception e) {
133                                 e.printStackTrace();
134                                 showError(e.getMessage());
135                                 return false;
136                         }
137                 }
138         };
139
140         /**
141          * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
142          * すでに再生されていた場合、このエディタダイアログを表示します。
143          *
144          * @param fileList 読み込むMIDIファイルのリスト
145          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
146          * @see #loadAndPlay(File)
147          */
148         public void loadAndPlay(List<File> fileList) {
149                 int indexOfAddedTop = -1;
150                 PlaylistTableModel playlist = sequenceListTable.getModel();
151                 try {
152                         indexOfAddedTop = playlist.addSequences(fileList);
153                 } catch(IOException|InvalidMidiDataException e) {
154                         showWarning(e.getMessage());
155                 } catch(AccessControlException e) {
156                         e.printStackTrace();
157                         showError(e.getMessage());
158                 }
159                 MidiSequencerModel sequencerModel = playlist.getSequencerModel();
160                 if( sequencerModel.getSequencer().isRunning() ) {
161                         String command = (String)openAction.getValue(Action.NAME);
162                         openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
163                         return;
164                 }
165                 if( indexOfAddedTop >= 0 ) {
166                         try {
167                                 playlist.loadToSequencer(indexOfAddedTop);
168                         } catch (InvalidMidiDataException ex) {
169                                 ex.printStackTrace();
170                                 showError(ex.getMessage());
171                                 return;
172                         }
173                         sequencerModel.start();
174                 }
175         }
176         /**
177          * 1件のMIDIファイルを読み込み、再生されていなかったら再生します。
178          * すでに再生されていた場合、このエディタダイアログを表示します。
179          *
180          * @param file 読み込むMIDIファイル
181          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
182          * @see #loadAndPlay(List) loadAndPlay(List&lt;File&gt;)
183          */
184         public void loadAndPlay(File file) throws InvalidMidiDataException {
185                 loadAndPlay(Arrays.asList(file));
186         }
187
188         private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
189         private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
190         /**
191          * 新しいMIDIシーケンスを生成するダイアログ
192          */
193         public NewSequenceDialog newSequenceDialog;
194         /**
195          * BASE64テキスト入力ダイアログ
196          */
197         public Base64Dialog base64Dialog = new Base64Dialog(this);
198         /**
199          * プレイリストビュー(シーケンスリスト)
200          */
201         public SequenceListTable sequenceListTable;
202         /**
203          * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
204          */
205         private TrackListTable trackListTable;
206         /**
207          * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
208          */
209         private EventListTable eventListTable;
210         /**
211          * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
212          */
213         public MidiEventDialog eventDialog = new MidiEventDialog();
214         private VirtualMidiDevice outputMidiDevice;
215         /**
216          * プレイリストビュー(シーケンスリスト)
217          */
218         public class SequenceListTable extends JTable {
219                 /**
220                  * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
221                  */
222                 private MidiFileChooser midiFileChooser;
223                 /**
224                  * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
225                  */
226                 private Action base64EncodeAction;
227                 /**
228                  * プレイリストビューを構築します。
229                  * @param model プレイリストデータモデル
230                  */
231                 public SequenceListTable(PlaylistTableModel model) {
232                         super(model, null, model.sequenceListSelectionModel);
233                         try {
234                                 midiFileChooser = new MidiFileChooser();
235                         }
236                         catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
237                                 // アプレットの場合、Webクライアントマシンのローカルファイルには
238                                 // アクセスできないので、ファイル選択ダイアログは使用不可。
239                                 midiFileChooser = null;
240                         }
241                         // 再生ボタンを埋め込む
242                         new PlayButtonCellEditor();
243                         new PositionCellEditor();
244                         //
245                         // 文字コード選択をプルダウンにする
246                         int column = PlaylistTableModel.Column.CHARSET.ordinal();
247                         TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
248                                 Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
249                                 for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
250                         }});
251                         getColumnModel().getColumn(column).setCellEditor(ce);
252                         setAutoCreateColumnsFromModel(false);
253                         //
254                         // Base64エンコードアクションの生成
255                         if( base64Dialog.isBase64Available() ) {
256                                 base64EncodeAction = new AbstractAction("Base64") {
257                                         {
258                                                 String tooltip = "Base64 text conversion - Base64テキスト変換";
259                                                 putValue(Action.SHORT_DESCRIPTION, tooltip);
260                                         }
261                                         @Override
262                                         public void actionPerformed(ActionEvent e) {
263                                                 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
264                                                 byte[] data = null;
265                                                 String filename = null;
266                                                 if( mstm != null ) {
267                                                         data = mstm.getMIDIdata();
268                                                         filename = mstm.getFilename();
269                                                 }
270                                                 base64Dialog.setMIDIData(data, filename);
271                                                 base64Dialog.setVisible(true);
272                                         }
273                                 };
274                         }
275                         TableColumnModel colModel = getColumnModel();
276                         for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
277                                 TableColumn tc = colModel.getColumn(c.ordinal());
278                                 tc.setPreferredWidth(c.preferredWidth);
279                                 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
280                         }
281                 }
282                 private TableColumn lengthColumn;
283                 @Override
284                 public void tableChanged(TableModelEvent event) {
285                         super.tableChanged(event);
286                         //
287                         // タイトルに合計シーケンス長を表示
288                         if( lengthColumn != null ) {
289                                 int sec = getModel().getTotalSeconds();
290                                 String title = PlaylistTableModel.Column.LENGTH.title;
291                                 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
292                                 lengthColumn.setHeaderValue(title);
293                         }
294                         //
295                         // シーケンス削除時など、合計シーケンス長が変わっても
296                         // 列モデルからではヘッダタイトルが再描画されないことがある。
297                         // そこで、ヘッダビューから repaint() で突っついて再描画させる。
298                         JTableHeader th = getTableHeader();
299                         if( th != null ) th.repaint();
300                 }
301                 /**
302                  * 時間位置表示セルエディタ(ダブルクリック専用)
303                  */
304                 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
305                         public PositionCellEditor() {
306                                 int column = PlaylistTableModel.Column.POSITION.ordinal();
307                                 TableColumn tc = getColumnModel().getColumn(column);
308                                 tc.setCellEditor(this);
309                         }
310                         /**
311                          * セルをダブルクリックしたときだけ編集モードに入るようにします。
312                          * @param e イベント(マウスイベント)
313                          * @return 編集可能になったらtrue
314                          */
315                         @Override
316                         public boolean isCellEditable(EventObject e) {
317                                 // マウスイベント以外のイベントでは編集不可
318                                 if( ! (e instanceof MouseEvent) ) return false;
319                                 return ((MouseEvent)e).getClickCount() == 2;
320                         }
321                         @Override
322                         public Object getCellEditorValue() { return null; }
323                         /**
324                          * 編集モード時のコンポーネントを返すタイミングで
325                          * そのシーケンスをシーケンサーにロードしたあと、
326                          * すぐに編集モードを解除します。
327                          * @return 常にnull
328                          */
329                         @Override
330                         public Component getTableCellEditorComponent(
331                                 JTable table, Object value, boolean isSelected, int row, int column
332                         ) {
333                                 try {
334                                         getModel().loadToSequencer(row);
335                                 } catch (InvalidMidiDataException ex) {
336                                         ex.printStackTrace();
337                                         showError(ex.getMessage());
338                                 }
339                                 fireEditingStopped();
340                                 return null;
341                         }
342                 }
343                 /**
344                  * プレイボタンを埋め込んだセルエディタ
345                  */
346                 private class PlayButtonCellEditor extends AbstractCellEditor
347                         implements TableCellEditor, TableCellRenderer
348                 {
349                         private JToggleButton playButton = new JToggleButton(
350                                 getModel().getSequencerModel().startStopAction
351                         ) {
352                                 { setMargin(ZERO_INSETS); }
353                         };
354                         public PlayButtonCellEditor() {
355                                 int column = PlaylistTableModel.Column.PLAY.ordinal();
356                                 TableColumn tc = getColumnModel().getColumn(column);
357                                 tc.setCellRenderer(this);
358                                 tc.setCellEditor(this);
359                         }
360                         /**
361                          * {@inheritDoc}
362                          *
363                          * <p>この実装では、クリックしたセルのシーケンスが
364                          * シーケンサーにロードされている場合に
365                          * trueを返してプレイボタンを押せるようにします。
366                          * そうでない場合はプレイボタンのないセルなので、
367                          * ダブルクリックされたときだけtrueを返します。
368                          * </p>
369                          */
370                         @Override
371                         public boolean isCellEditable(EventObject e) {
372                                 // マウスイベント以外はデフォルトメソッドにお任せ
373                                 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
374                                 fireEditingStopped();
375                                 MouseEvent me = (MouseEvent)e;
376                                 //
377                                 // クリックされたセルの行を特定
378                                 int row = rowAtPoint(me.getPoint());
379                                 if( row < 0 ) return false;
380                                 PlaylistTableModel model = getModel();
381                                 if( row >= model.getRowCount() ) return false;
382                                 //
383                                 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
384                                 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
385                                 return model.getSequenceList().get(row).isOnSequencer() || me.getClickCount() == 2;
386                         }
387                         @Override
388                         public Object getCellEditorValue() { return null; }
389                         /**
390                          * {@inheritDoc}
391                          *
392                          * <p>この実装では、行の表すシーケンスが
393                          * シーケンサーにロードされている場合にプレイボタンを返します。
394                          * そうでない場合は、
395                          * そのシーケンスをシーケンサーにロードしてnullを返します。
396                          * </p>
397                          */
398                         @Override
399                         public Component getTableCellEditorComponent(
400                                 JTable table, Object value, boolean isSelected, int row, int column
401                         ) {
402                                 fireEditingStopped();
403                                 PlaylistTableModel model = getModel();
404                                 if( model.getSequenceList().get(row).isOnSequencer() ) return playButton;
405                                 try {
406                                         model.loadToSequencer(row);
407                                 } catch (InvalidMidiDataException ex) {
408                                         ex.printStackTrace();
409                                         showError(ex.getMessage());
410                                 }
411                                 return null;
412                         }
413                         @Override
414                         public Component getTableCellRendererComponent(
415                                 JTable table, Object value, boolean isSelected,
416                                 boolean hasFocus, int row, int column
417                         ) {
418                                 PlaylistTableModel model = getModel();
419                                 if(model.getSequenceList().get(row).isOnSequencer()) return playButton;
420                                 Class<?> cc = model.getColumnClass(column);
421                                 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
422                                 return defaultRenderer.getTableCellRendererComponent(
423                                         table, value, isSelected, hasFocus, row, column
424                                 );
425                         }
426                 }
427                 /**
428                  * このプレイリスト(シーケンスリスト)が表示するデータを提供する
429                  * プレイリストモデルを返します。
430                  * @return プレイリストモデル
431                  */
432                 @Override
433                 public PlaylistTableModel getModel() { return (PlaylistTableModel)super.getModel(); }
434                 /**
435                  * シーケンスを削除するアクション
436                  */
437                 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
438                         "Delete", MidiSequenceEditorDialog.deleteIcon,
439                         "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
440                 ) {
441                         @Override
442                         public void actionPerformed(ActionEvent event) {
443                                 PlaylistTableModel model = getModel();
444                                 if( midiFileChooser != null ) {
445                                         if( model.getSelectedSequenceModel().isModified() ) {
446                                                 String message =
447                                                         "Selected MIDI sequence not saved - delete it ?\n" +
448                                                         "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
449                                                 if( ! confirm(message) ) return;
450                                         }
451                                 }
452                                 try {
453                                         model.removeSelectedSequence();
454                                 } catch (InvalidMidiDataException ex) {
455                                         ex.printStackTrace();
456                                         showError(ex.getMessage());
457                                 }
458                         }
459                 };
460                 /**
461                  * ファイル選択ダイアログ(アプレットでは使用不可)
462                  */
463                 private class MidiFileChooser extends JFileChooser {
464                         {
465                                 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
466                         }
467                         /**
468                          * ファイル保存アクション
469                          */
470                         public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
471                                 "Save",
472                                 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
473                         ) {
474                                 @Override
475                                 public void actionPerformed(ActionEvent event) {
476                                         PlaylistTableModel playlistModel = getModel();
477                                         SequenceTrackListTableModel sequenceModel = playlistModel.getSelectedSequenceModel();
478                                         String fn = sequenceModel.getFilename();
479                                         if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
480                                         if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
481                                         File f = getSelectedFile();
482                                         if( f.exists() ) {
483                                                 fn = f.getName();
484                                                 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
485                                         }
486                                         try ( FileOutputStream out = new FileOutputStream(f) ) {
487                                                 out.write(sequenceModel.getMIDIdata());
488                                                 sequenceModel.setModified(false);
489                                                 playlistModel.fireSequenceModified(sequenceModel, false);
490                                         }
491                                         catch( IOException ex ) {
492                                                 ex.printStackTrace();
493                                                 showError( ex.getMessage() );
494                                         }
495                                 }
496                         };
497                         /**
498                          * ファイルを開くアクション
499                          */
500                         public Action openMidiFileAction = new AbstractAction("Open") {
501                                 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
502                                 @Override
503                                 public void actionPerformed(ActionEvent event) {
504                                         if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
505                                         try {
506                                                 loadAndPlay(getSelectedFile());
507                                         } catch (InvalidMidiDataException ex) {
508                                                 ex.printStackTrace();
509                                                 showError(ex.getMessage());
510                                         }
511                                 }
512                         };
513                 };
514         }
515
516         /**
517          * シーケンス(トラックリスト)テーブルビュー
518          */
519         public class TrackListTable extends JTable {
520                 /**
521                  * トラックリストテーブルビューを構築します。
522                  * @param model シーケンス(トラックリスト)データモデル
523                  */
524                 public TrackListTable(SequenceTrackListTableModel model) {
525                         super(model, null, model.getSelectionModel());
526                         //
527                         // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
528                         int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
529                         TableColumn tc = getColumnModel().getColumn(colIndex);
530                         tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
531                                 addItem("OFF");
532                                 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
533                                 addItem("ALL");
534                         }}));
535                         setAutoCreateColumnsFromModel(false);
536                         //
537                         titleLabel = new TitleLabel();
538                         model.getParent().sequenceListSelectionModel.addListSelectionListener(titleLabel);
539                         TableColumnModel colModel = getColumnModel();
540                         for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
541                                 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
542                 }
543                 /**
544                  * このテーブルビューが表示するデータを提供する
545                  * シーケンス(トラックリスト)データモデルを返します。
546                  * @return シーケンス(トラックリスト)データモデル
547                  */
548                 @Override
549                 public SequenceTrackListTableModel getModel() {
550                         return (SequenceTrackListTableModel) super.getModel();
551                 }
552                 /**
553                  * タイトルラベル
554                  */
555                 TitleLabel titleLabel;
556                 /**
557                  * 親テーブルの選択シーケンスの変更に反応する
558                  * 曲番号表示付きタイトルラベル
559                  */
560                 private class TitleLabel extends JLabel implements ListSelectionListener {
561                         private static final String TITLE = "Tracks";
562                         public TitleLabel() { setText(TITLE); }
563                         @Override
564                         public void valueChanged(ListSelectionEvent event) {
565                                 if( event.getValueIsAdjusting() ) return;
566                                 SequenceTrackListTableModel oldModel = getModel();
567                                 SequenceTrackListTableModel newModel = oldModel.getParent().getSelectedSequenceModel();
568                                 if( oldModel == newModel ) return;
569                                 //
570                                 // MIDIチャンネル選択中のときはキャンセルする
571                                 cancelCellEditing();
572                                 //
573                                 int index = oldModel.getParent().sequenceListSelectionModel.getMinSelectionIndex();
574                                 String text = TITLE;
575                                 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
576                                 setText(text);
577                                 if( newModel == null ) {
578                                         newModel = oldModel.getParent().emptyTrackListTableModel;
579                                         addTrackAction.setEnabled(false);
580                                 }
581                                 else {
582                                         addTrackAction.setEnabled(true);
583                                 }
584                                 oldModel.getSelectionModel().removeListSelectionListener(trackSelectionListener);
585                                 setModel(newModel);
586                                 setSelectionModel(newModel.getSelectionModel());
587                                 newModel.getSelectionModel().addListSelectionListener(trackSelectionListener);
588                                 trackSelectionListener.valueChanged(null);
589                         }
590                 }
591                 /**
592                  * トラック選択リスナー
593                  */
594                 ListSelectionListener trackSelectionListener = new ListSelectionListener() {
595                         @Override
596                         public void valueChanged(ListSelectionEvent e) {
597                                 if( e != null && e.getValueIsAdjusting() ) return;
598                                 ListSelectionModel tlsm = getModel().getSelectionModel();
599                                 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
600                                 eventListTable.titleLabel.update(tlsm, getModel());
601                         }
602                 };
603                 /**
604                  * {@inheritDoc}
605                  *
606                  * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
607                  * 例えば、イベントが編集された場合や、
608                  * シーケンサーからこのモデルが外された場合がこれに該当します。
609                  * </p>
610                  */
611                 @Override
612                 public void tableChanged(TableModelEvent e) {
613                         super.tableChanged(e);
614                         cancelCellEditing();
615                 }
616                 /**
617                  * このトラックリストテーブルが編集モードになっていたら解除します。
618                  */
619                 private void cancelCellEditing() {
620                         TableCellEditor currentCellEditor = getCellEditor();
621                         if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
622                 }
623                 /**
624                  * トラック追加アクション
625                  */
626                 Action addTrackAction = new AbstractAction("New") {
627                         {
628                                 String tooltip = "Append new track - 新しいトラックの追加";
629                                 putValue(Action.SHORT_DESCRIPTION, tooltip);
630                                 setEnabled(false);
631                         }
632                         @Override
633                         public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
634                 };
635                 /**
636                  * トラック削除アクション
637                  */
638                 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
639                         {
640                                 String tooltip = "Delete selected track - 選択したトラックを削除";
641                                 putValue(Action.SHORT_DESCRIPTION, tooltip);
642                                 setEnabled(false);
643                         }
644                         @Override
645                         public void actionPerformed(ActionEvent e) {
646                                 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
647                                 if( confirm(message) ) getModel().deleteSelectedTracks();
648                         }
649                 };
650         }
651
652         /**
653          * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
654          */
655         public class EventListTable extends JTable {
656                 /**
657                  * 新しいイベントリストテーブルを構築します。
658                  * <p>データモデルとして一つのトラックのイベントリストを指定できます。
659                  * トラックを切り替えたいときは {@link #setModel(TableModel)}
660                  * でデータモデルを異なるトラックのものに切り替えます。
661                  * </p>
662                  *
663                  * @param model トラック(イベントリスト)データモデル
664                  */
665                 public EventListTable(TrackEventListTableModel model) {
666                         super(model, null, model.getSelectionModel());
667                         //
668                         // 列モデルにセルエディタを設定
669                         eventCellEditor = new MidiEventCellEditor();
670                         setAutoCreateColumnsFromModel(false);
671                         //
672                         eventSelectionListener = new EventSelectionListener();
673                         titleLabel = new TitleLabel();
674                         //
675                         TableColumnModel colModel = getColumnModel();
676                         for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
677                                 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
678                 }
679                 /**
680                  * このテーブルビューが表示するデータを提供する
681                  * トラック(イベントリスト)データモデルを返します。
682                  * @return トラック(イベントリスト)データモデル
683                  */
684                 @Override
685                 public TrackEventListTableModel getModel() {
686                         return (TrackEventListTableModel) super.getModel();
687                 }
688                 /**
689                  * タイトルラベル
690                  */
691                 TitleLabel titleLabel;
692                 /**
693                  * 親テーブルの選択トラックの変更に反応する
694                  * トラック番号つきタイトルラベル
695                  */
696                 private class TitleLabel extends JLabel {
697                         private static final String TITLE = "MIDI Events";
698                         public TitleLabel() { super(TITLE); }
699                         public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
700                                 String text = TITLE;
701                                 TrackEventListTableModel oldTrackModel = getModel();
702                                 int index = tlsm.getMinSelectionIndex();
703                                 if( index >= 0 ) {
704                                         text = String.format(TITLE+" - track No.%d", index);
705                                 }
706                                 setText(text);
707                                 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
708                                 if( oldTrackModel == newTrackModel )
709                                         return;
710                                 if( newTrackModel == null ) {
711                                         newTrackModel = getModel().getParent().getParent().emptyEventListTableModel;
712                                         queryJumpEventAction.setEnabled(false);
713                                         queryAddEventAction.setEnabled(false);
714
715                                         queryPasteEventAction.setEnabled(false);
716                                         copyEventAction.setEnabled(false);
717                                         deleteEventAction.setEnabled(false);
718                                         cutEventAction.setEnabled(false);
719                                 }
720                                 else {
721                                         queryJumpEventAction.setEnabled(true);
722                                         queryAddEventAction.setEnabled(true);
723                                 }
724                                 oldTrackModel.getSelectionModel().removeListSelectionListener(eventSelectionListener);
725                                 setModel(newTrackModel);
726                                 setSelectionModel(newTrackModel.getSelectionModel());
727                                 newTrackModel.getSelectionModel().addListSelectionListener(eventSelectionListener);
728                         }
729                 }
730
731                 /**
732                  * イベント選択リスナー
733                  */
734                 private EventSelectionListener eventSelectionListener;
735                 /**
736                  * 選択イベントの変更に反応するリスナー
737                  */
738                 private class EventSelectionListener implements ListSelectionListener {
739                         public EventSelectionListener() {
740                                 getModel().getSelectionModel().addListSelectionListener(this);
741                         }
742                         @Override
743                         public void valueChanged(ListSelectionEvent e) {
744                                 if( e.getValueIsAdjusting() )
745                                         return;
746                                 if( getSelectionModel().isSelectionEmpty() ) {
747                                         queryPasteEventAction.setEnabled(false);
748                                         copyEventAction.setEnabled(false);
749                                         deleteEventAction.setEnabled(false);
750                                         cutEventAction.setEnabled(false);
751                                 }
752                                 else {
753                                         copyEventAction.setEnabled(true);
754                                         deleteEventAction.setEnabled(true);
755                                         cutEventAction.setEnabled(true);
756                                         TrackEventListTableModel trackModel = getModel();
757                                         int minIndex = getSelectionModel().getMinSelectionIndex();
758                                         MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
759                                         if( midiEvent != null ) {
760                                                 MidiMessage msg = midiEvent.getMessage();
761                                                 if( msg instanceof ShortMessage ) {
762                                                         ShortMessage sm = (ShortMessage)msg;
763                                                         int cmd = sm.getCommand();
764                                                         if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
765                                                                 // ノート番号を持つ場合、音を鳴らす。
766                                                                 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
767                                                                 int ch = sm.getChannel();
768                                                                 int note = sm.getData1();
769                                                                 int vel = sm.getData2();
770                                                                 outMidiChannels[ch].noteOn(note, vel);
771                                                                 outMidiChannels[ch].noteOff(note, vel);
772                                                         }
773                                                 }
774                                         }
775                                         if( pairNoteOnOffModel.isSelected() ) {
776                                                 int maxIndex = getSelectionModel().getMaxSelectionIndex();
777                                                 int partnerIndex;
778                                                 for( int i=minIndex; i<=maxIndex; i++ ) {
779                                                         if( ! getSelectionModel().isSelectedIndex(i) ) continue;
780                                                         partnerIndex = trackModel.getIndexOfPartnerFor(i);
781                                                         if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
782                                                                 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
783                                                 }
784                                         }
785                                 }
786                         }
787                 }
788                 /**
789                  * Pair noteON/OFF トグルボタンモデル
790                  */
791                 private JToggleButton.ToggleButtonModel
792                         pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
793                                 {
794                                         addItemListener(new ItemListener() {
795                                                 public void itemStateChanged(ItemEvent e) {
796                                                         eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
797                                                 }
798                                         });
799                                         setSelected(true);
800                                 }
801                         };
802                 private class EventEditContext {
803                         /**
804                          * 編集対象トラック
805                          */
806                         private TrackEventListTableModel trackModel;
807                         /**
808                          * tick位置入力モデル
809                          */
810                         private TickPositionModel tickPositionModel = new TickPositionModel();
811                         /**
812                          * 選択されたイベント
813                          */
814                         private MidiEvent selectedMidiEvent = null;
815                         /**
816                          * 選択されたイベントの場所
817                          */
818                         private int selectedIndex = -1;
819                         /**
820                          * 選択されたイベントのtick位置
821                          */
822                         private long currentTick = 0;
823                         /**
824                          * 上書きして削除対象にする変更前イベント(null可)
825                          */
826                         private MidiEvent[] midiEventsToBeOverwritten;
827                         /**
828                          * 選択したイベントを入力ダイアログなどに反映します。
829                          * @param model 対象データモデル
830                          */
831                         private void setSelectedEvent(TrackEventListTableModel trackModel) {
832                                 this.trackModel = trackModel;
833                                 SequenceTrackListTableModel sequenceTableModel = trackModel.getParent();
834                                 int ppq = sequenceTableModel.getSequence().getResolution();
835                                 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
836                                 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
837
838                                 selectedIndex = trackModel.getSelectionModel().getMinSelectionIndex();
839                                 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
840                                 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
841                                 tickPositionModel.setTickPosition(currentTick);
842                         }
843                         public void setupForEdit(TrackEventListTableModel trackModel) {
844                                 MidiEvent partnerEvent = null;
845                                 eventDialog.midiMessageForm.setMessage(
846                                         selectedMidiEvent.getMessage(),
847                                         trackModel.getParent().charset
848                                 );
849                                 if( eventDialog.midiMessageForm.isNote() ) {
850                                         int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
851                                         if( partnerIndex < 0 ) {
852                                                 eventDialog.midiMessageForm.durationForm.setDuration(0);
853                                         }
854                                         else {
855                                                 partnerEvent = trackModel.getMidiEvent(partnerIndex);
856                                                 long partnerTick = partnerEvent.getTick();
857                                                 long duration = currentTick > partnerTick ?
858                                                         currentTick - partnerTick : partnerTick - currentTick ;
859                                                 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
860                                         }
861                                 }
862                                 if(partnerEvent == null)
863                                         midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
864                                 else
865                                         midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
866                         }
867                         private Action jumpEventAction = new AbstractAction() {
868                                 { putValue(NAME,"Jump"); }
869                                 public void actionPerformed(ActionEvent e) {
870                                         long tick = tickPositionModel.getTickPosition();
871                                         scrollToEventAt(tick);
872                                         eventDialog.setVisible(false);
873                                         trackModel = null;
874                                 }
875                         };
876                         private Action pasteEventAction = new AbstractAction() {
877                                 { putValue(NAME,"Paste"); }
878                                 public void actionPerformed(ActionEvent e) {
879                                         long tick = tickPositionModel.getTickPosition();
880                                         clipBoard.paste(trackModel, tick);
881                                         scrollToEventAt(tick);
882                                         // ペーストで曲の長さが変わったことをプレイリストに通知
883                                         SequenceTrackListTableModel seqModel = trackModel.getParent();
884                                         seqModel.getParent().fireSequenceModified(seqModel, true);
885                                         eventDialog.setVisible(false);
886                                         trackModel = null;
887                                 }
888                         };
889                         private boolean applyEvent() {
890                                 long tick = tickPositionModel.getTickPosition();
891                                 MidiMessageForm form = eventDialog.midiMessageForm;
892                                 SequenceTrackListTableModel seqModel = trackModel.getParent();
893                                 MidiEvent newMidiEvent = new MidiEvent(form.getMessage(seqModel.charset), tick);
894                                 if( midiEventsToBeOverwritten != null ) {
895                                         // 上書き消去するための選択済イベントがあった場合
896                                         trackModel.removeMidiEvents(midiEventsToBeOverwritten);
897                                 }
898                                 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
899                                         System.out.println("addMidiEvent failure");
900                                         return false;
901                                 }
902                                 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
903                                         ShortMessage sm = form.createPartnerMessage();
904                                         if(sm == null)
905                                                 scrollToEventAt( tick );
906                                         else {
907                                                 int duration = form.durationForm.getDuration();
908                                                 if( form.isNote(false) ) {
909                                                         duration = -duration;
910                                                 }
911                                                 long partnerTick = tick + (long)duration;
912                                                 if( partnerTick < 0L ) partnerTick = 0L;
913                                                 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
914                                                 if( ! trackModel.addMidiEvent(partner) ) {
915                                                         System.out.println("addMidiEvent failure (note on/off partner message)");
916                                                 }
917                                                 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
918                                         }
919                                 }
920                                 seqModel.getParent().fireSequenceModified(seqModel, true);
921                                 eventDialog.setVisible(false);
922                                 return true;
923                         }
924                 }
925                 private EventEditContext editContext = new EventEditContext();
926                 /**
927                  * 指定のTick位置へジャンプするアクション
928                  */
929                 Action queryJumpEventAction = new AbstractAction() {
930                         {
931                                 putValue(NAME,"Jump to ...");
932                                 setEnabled(false);
933                         }
934                         public void actionPerformed(ActionEvent e) {
935                                 editContext.setSelectedEvent(getModel());
936                                 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
937                         }
938                 };
939                 /**
940                  * 新しいイベントの追加を行うアクション
941                  */
942                 Action queryAddEventAction = new AbstractAction() {
943                         {
944                                 putValue(NAME,"New");
945                                 setEnabled(false);
946                         }
947                         public void actionPerformed(ActionEvent e) {
948                                 TrackEventListTableModel model = getModel();
949                                 editContext.setSelectedEvent(model);
950                                 editContext.midiEventsToBeOverwritten = null;
951                                 eventDialog.openEventForm(
952                                         "New MIDI event",
953                                         eventCellEditor.applyEventAction,
954                                         model.getChannel()
955                                 );
956                         }
957                 };
958                 /**
959                  * MIDIイベントのコピー&ペーストを行うためのクリップボード
960                  */
961                 private class LocalClipBoard {
962                         private MidiEvent copiedEventsToPaste[];
963                         private int copiedEventsPPQ = 0;
964                         public void copy(TrackEventListTableModel model, boolean withRemove) {
965                                 copiedEventsToPaste = model.getSelectedMidiEvents();
966                                 copiedEventsPPQ = model.getParent().getSequence().getResolution();
967                                 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
968                                 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
969                                 queryPasteEventAction.setEnabled(en);
970                         }
971                         public void cut(TrackEventListTableModel model) {copy(model,true);}
972                         public void copy(TrackEventListTableModel model){copy(model,false);}
973                         public void paste(TrackEventListTableModel model, long tick) {
974                                 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
975                         }
976                 }
977                 private LocalClipBoard clipBoard = new LocalClipBoard();
978                 /**
979                  * 指定のTick位置へ貼り付けるアクション
980                  */
981                 Action queryPasteEventAction = new AbstractAction() {
982                         {
983                                 putValue(NAME,"Paste to ...");
984                                 setEnabled(false);
985                         }
986                         public void actionPerformed(ActionEvent e) {
987                                 editContext.setSelectedEvent(getModel());
988                                 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
989                         }
990                 };
991                 /**
992                  * イベントカットアクション
993                  */
994                 public Action cutEventAction = new AbstractAction("Cut") {
995                         {
996                                 setEnabled(false);
997                         }
998                         @Override
999                         public void actionPerformed(ActionEvent e) {
1000                                 TrackEventListTableModel model = getModel();
1001                                 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
1002                                         return;
1003                                 clipBoard.cut(model);
1004                         }
1005                 };
1006                 /**
1007                  * イベントコピーアクション
1008                  */
1009                 public Action copyEventAction = new AbstractAction("Copy") {
1010                         {
1011                                 setEnabled(false);
1012                         }
1013                         @Override
1014                         public void actionPerformed(ActionEvent e) {
1015                                 clipBoard.copy(getModel());
1016                         }
1017                 };
1018                 /**
1019                  * イベント削除アクション
1020                  */
1021                 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
1022                         {
1023                                 setEnabled(false);
1024                         }
1025                         @Override
1026                         public void actionPerformed(ActionEvent e) {
1027                                 TrackEventListTableModel model = getModel();
1028                                 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1029                                         return;
1030                                 model.removeSelectedMidiEvents();
1031                         }
1032                 };
1033                 /**
1034                  * MIDIイベント表のセルエディタ
1035                  */
1036                 private MidiEventCellEditor eventCellEditor;
1037                 /**
1038                  * MIDIイベント表のセルエディタ
1039                  */
1040                 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1041                         /**
1042                          * MIDIイベントセルエディタを構築します。
1043                          */
1044                         public MidiEventCellEditor() {
1045                                 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
1046                                 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1047                                 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1048                                 getColumnModel().getColumn(index).setCellEditor(this);
1049                         }
1050                         /**
1051                          * セルをダブルクリックしないと編集できないようにします。
1052                          * @param e イベント(マウスイベント)
1053                          * @return 編集可能になったらtrue
1054                          */
1055                         @Override
1056                         public boolean isCellEditable(EventObject e) {
1057                                 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1058                                 return ((MouseEvent)e).getClickCount() == 2;
1059                         }
1060                         @Override
1061                         public Object getCellEditorValue() { return null; }
1062                         /**
1063                          * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1064                          */
1065                         private ComponentListener dialogComponentListener = new ComponentAdapter() {
1066                                 @Override
1067                                 public void componentHidden(ComponentEvent e) {
1068                                         fireEditingCanceled();
1069                                         // 用が済んだら当リスナーを除去
1070                                         eventDialog.removeComponentListener(this);
1071                                 }
1072                         };
1073                         /**
1074                          * 既存イベントを編集するアクション
1075                          */
1076                         private Action editEventAction = new AbstractAction() {
1077                                 public void actionPerformed(ActionEvent e) {
1078                                         TrackEventListTableModel model = getModel();
1079                                         editContext.setSelectedEvent(model);
1080                                         if( editContext.selectedMidiEvent == null )
1081                                                 return;
1082                                         editContext.setupForEdit(model);
1083                                         eventDialog.addComponentListener(dialogComponentListener);
1084                                         eventDialog.openEventForm("Change MIDI event", applyEventAction);
1085                                 }
1086                         };
1087                         /**
1088                          * イベント編集ボタン
1089                          */
1090                         private JButton editEventButton = new JButton(editEventAction){{
1091                                 setHorizontalAlignment(JButton.LEFT);
1092                         }};
1093                         @Override
1094                         public Component getTableCellEditorComponent(
1095                                 JTable table, Object value, boolean isSelected, int row, int column
1096                         ) {
1097                                 editEventButton.setText(value.toString());
1098                                 return editEventButton;
1099                         }
1100                         /**
1101                          * 入力したイベントを反映するアクション
1102                          */
1103                         private Action applyEventAction = new AbstractAction() {
1104                                 {
1105                                         putValue(NAME,"OK");
1106                                 }
1107                                 public void actionPerformed(ActionEvent e) {
1108                                         if( editContext.applyEvent() ) fireEditingStopped();
1109                                 }
1110                         };
1111                 }
1112                 /**
1113                  * スクロール可能なMIDIイベントテーブルビュー
1114                  */
1115                 private JScrollPane scrollPane = new JScrollPane(this);
1116                 /**
1117                  * 指定の MIDI tick のイベントへスクロールします。
1118                  * @param tick MIDI tick
1119                  */
1120                 public void scrollToEventAt(long tick) {
1121                         int index = getModel().tickToIndex(tick);
1122                         scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1123                         getSelectionModel().setSelectionInterval(index, index);
1124                 }
1125         }
1126
1127         /**
1128          * 新しい {@link MidiSequenceEditorDialog} を構築します。
1129          * @param playlistTableModel このエディタが参照するプレイリストモデル
1130          * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1131          */
1132         public MidiSequenceEditorDialog(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice) {
1133                 this.outputMidiDevice = outputMidiDevice;
1134                 sequenceListTable = new SequenceListTable(playlistTableModel);
1135                 trackListTable = new TrackListTable(
1136                         new SequenceTrackListTableModel(playlistTableModel, null, null)
1137                 );
1138                 eventListTable = new EventListTable(new TrackEventListTableModel(trackListTable.getModel(), null));
1139                 newSequenceDialog = new NewSequenceDialog(playlistTableModel, outputMidiDevice);
1140                 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1141                 setBounds( 150, 200, 900, 500 );
1142                 setLayout(new FlowLayout());
1143                 setTransferHandler(transferHandler);
1144                 //
1145                 // パネルレイアウト
1146                 JPanel playlistPanel = new JPanel() {{
1147                         JPanel playlistOperationPanel = new JPanel() {{
1148                                 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1149                                 add(Box.createRigidArea(new Dimension(10, 0)));
1150                                 add(new JButton(newSequenceDialog.openAction) {{ setMargin(ZERO_INSETS); }});
1151                                 if( sequenceListTable.midiFileChooser != null ) {
1152                                         add( Box.createRigidArea(new Dimension(5, 0)) );
1153                                         add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {
1154                                                 { setMargin(ZERO_INSETS); }
1155                                         });
1156                                 }
1157                                 if(sequenceListTable.base64EncodeAction != null) {
1158                                         add(Box.createRigidArea(new Dimension(5, 0)));
1159                                         add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1160                                 }
1161                                 add(Box.createRigidArea(new Dimension(5, 0)));
1162                                 PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
1163                                 add(new JButton(playlistTableModel.moveToTopAction) {{ setMargin(ZERO_INSETS); }});
1164                                 add(Box.createRigidArea(new Dimension(5, 0)));
1165                                 add(new JButton(playlistTableModel.moveToBottomAction) {{ setMargin(ZERO_INSETS); }});
1166                                 if( sequenceListTable.midiFileChooser != null ) {
1167                                         add(Box.createRigidArea(new Dimension(5, 0)));
1168                                         add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {
1169                                                 { setMargin(ZERO_INSETS); }
1170                                         });
1171                                 }
1172                                 add( Box.createRigidArea(new Dimension(5, 0)) );
1173                                 add(new JButton(sequenceListTable.deleteSequenceAction) {{ setMargin(ZERO_INSETS); }});
1174                                 add( Box.createRigidArea(new Dimension(5, 0)) );
1175                                 add(new SequencerSpeedSlider(playlistTableModel.getSequencerModel().speedSliderModel));
1176                                 add( Box.createRigidArea(new Dimension(5, 0)) );
1177                                 add(new JPanel() {{
1178                                         setBorder(new EtchedBorder());
1179                                         MidiSequencerModel sequencerModel = sequenceListTable.getModel().getSequencerModel();
1180                                         add(new JLabel("Sync Master"));
1181                                         add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1182                                         add(new JLabel("Slave"));
1183                                         add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1184                                 }});
1185                         }};
1186                         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1187                         add(new JScrollPane(sequenceListTable));
1188                         add(Box.createRigidArea(new Dimension(0, 10)));
1189                         add(playlistOperationPanel);
1190                         add(Box.createRigidArea(new Dimension(0, 10)));
1191                 }};
1192                 JPanel trackListPanel = new JPanel() {{
1193                         JPanel trackListOperationPanel = new JPanel() {{
1194                                 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ZERO_INSETS); }});
1195                                 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
1196                         }};
1197                         setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1198                         add(trackListTable.titleLabel);
1199                         add(Box.createRigidArea(new Dimension(0, 5)));
1200                         add(new JScrollPane(trackListTable));
1201                         add(Box.createRigidArea(new Dimension(0, 5)));
1202                         add(trackListOperationPanel);
1203                 }};
1204                 JPanel eventListPanel = new JPanel() {{
1205                         JPanel eventListOperationPanel = new JPanel() {{
1206                                 add(new JCheckBox("Pair NoteON/OFF") {{
1207                                         setModel(eventListTable.pairNoteOnOffModel);
1208                                         setToolTipText("NoteON/OFFをペアで同時選択する");
1209                                 }});
1210                                 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ZERO_INSETS); }});
1211                                 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ZERO_INSETS); }});
1212                                 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ZERO_INSETS); }});
1213                                 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ZERO_INSETS); }});
1214                                 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ZERO_INSETS); }});
1215                                 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ZERO_INSETS); }});
1216                         }};
1217                         setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1218                         add(eventListTable.titleLabel);
1219                         add(eventListTable.scrollPane);
1220                         add(eventListOperationPanel);
1221                 }};
1222                 Container cp = getContentPane();
1223                 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1224                 cp.add(Box.createVerticalStrut(2));
1225                 cp.add(
1226                         new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1227                                 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1228                                         setDividerLocation(300);
1229                                 }}
1230                         ) {{
1231                                 setDividerLocation(160);
1232                         }}
1233                 );
1234         }
1235
1236 }