OSDN Git Service

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