OSDN Git Service

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