OSDN Git Service

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