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