OSDN Git Service

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