OSDN Git Service

・シーケンス削除後プレイリストが空になるときIndexOB例外が発生していた問題の修正
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / PlaylistTable.java
1 package camidion.chordhelper.midieditor;
2
3 import java.awt.Component;
4 import java.awt.HeadlessException;
5 import java.awt.event.ActionEvent;
6 import java.awt.event.MouseEvent;
7 import java.io.File;
8 import java.io.FileInputStream;
9 import java.io.FileOutputStream;
10 import java.io.IOException;
11 import java.nio.charset.Charset;
12 import java.security.AccessControlException;
13 import java.util.Arrays;
14 import java.util.EventObject;
15 import java.util.Iterator;
16 import java.util.List;
17
18 import javax.sound.midi.InvalidMidiDataException;
19 import javax.sound.midi.MidiSystem;
20 import javax.sound.midi.Sequence;
21 import javax.sound.midi.Sequencer;
22 import javax.swing.AbstractAction;
23 import javax.swing.AbstractCellEditor;
24 import javax.swing.Action;
25 import javax.swing.DefaultCellEditor;
26 import javax.swing.Icon;
27 import javax.swing.JButton;
28 import javax.swing.JComponent;
29 import javax.swing.JFileChooser;
30 import javax.swing.JOptionPane;
31 import javax.swing.JRootPane;
32 import javax.swing.JTable;
33 import javax.swing.JToggleButton;
34 import javax.swing.ListSelectionModel;
35 import javax.swing.event.ListSelectionEvent;
36 import javax.swing.event.ListSelectionListener;
37 import javax.swing.event.TableModelEvent;
38 import javax.swing.filechooser.FileNameExtensionFilter;
39 import javax.swing.table.JTableHeader;
40 import javax.swing.table.TableCellEditor;
41 import javax.swing.table.TableCellRenderer;
42 import javax.swing.table.TableColumn;
43 import javax.swing.table.TableColumnModel;
44
45 import camidion.chordhelper.ChordHelperApplet;
46 import camidion.chordhelper.mididevice.MidiSequencerModel;
47
48 /**
49  * プレイリストビュー(シーケンスリスト)
50  */
51 public class PlaylistTable extends JTable {
52         /** ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull) */
53         MidiFileChooser midiFileChooser;
54         /** BASE64エンコードアクション */
55         Action base64EncodeAction;
56         /** BASE64ダイアログ */
57         public Base64Dialog base64Dialog;
58         /** MIDIデバイスダイアログを開くアクション */
59         private Action midiDeviceDialogOpenAction;
60         /**
61          * 選択されたMIDIシーケンスのテーブルモデルを返します。
62          * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)
63          */
64         private SequenceTrackListTableModel getSelectedSequenceModel() {
65                 int i = getSelectedRow();
66                 if( i < 0 ) return null;
67                 List<SequenceTrackListTableModel> list = getModel().getSequenceModelList();
68                 return i >= list.size() ? null : list.get(i);
69         }
70         /**
71          * 行が選択されているときだけイネーブルになるアクション
72          */
73         private abstract class SelectedSequenceAction extends AbstractAction implements ListSelectionListener {
74                 public SelectedSequenceAction(String name, Icon icon, String tooltip) {
75                         super(name,icon); init(tooltip);
76                 }
77                 public SelectedSequenceAction(String name, String tooltip) {
78                         super(name); init(tooltip);
79                 }
80                 @Override
81                 public void valueChanged(ListSelectionEvent e) {
82                         if( e.getValueIsAdjusting() ) return;
83                         setEnebledBySelection();
84                 }
85                 protected void setEnebledBySelection() {
86                         setEnabled(getSelectedRow() >= 0);
87                 }
88                 private void init(String tooltip) {
89                         putValue(Action.SHORT_DESCRIPTION, tooltip);
90                         selectionModel.addListSelectionListener(this);
91                         setEnebledBySelection();
92                 }
93         }
94         /**
95          * プレイリストビューを構築します。
96          * @param model プレイリストデータモデル
97          * @param midiDeviceDialogOpenAction MIDIデバイスダイアログを開くアクション
98          * @param trackListTable トラックリストテーブル(子テーブル)
99          */
100         public PlaylistTable(PlaylistTableModel model, Action midiDeviceDialogOpenAction, SequenceTrackListTable trackListTable) {
101                 super(model);
102                 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
103                 this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
104                 try {
105                         midiFileChooser = new MidiFileChooser();
106                 }
107                 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
108                         // アプレットの場合、Webクライアントマシンのローカルファイルには
109                         // アクセスできないので、ファイル選択ダイアログは使用不可。
110                         midiFileChooser = null;
111                 }
112                 // 再生ボタンを埋め込む
113                 new PlayButtonCellEditor();
114                 new PositionCellEditor();
115                 //
116                 // 文字コード選択をプルダウンにする
117                 getColumnModel().getColumn(PlaylistTableModel.Column.CHARSET.ordinal())
118                         .setCellEditor(new DefaultCellEditor(new CharsetComboBox()));
119                 setAutoCreateColumnsFromModel(false);
120                 //
121                 // Base64画面を開くアクションの生成
122                 base64Dialog = new Base64Dialog(this);
123                 base64EncodeAction = new AbstractAction("Base64") {
124                         {
125                                 String tooltip = "Base64 text conversion - Base64テキスト変換";
126                                 putValue(Action.SHORT_DESCRIPTION, tooltip);
127                         }
128                         @Override
129                         public void actionPerformed(ActionEvent e) {
130                                 base64Dialog.setSequenceModel(getSelectedSequenceModel());
131                                 base64Dialog.setVisible(true);
132                         }
133                 };
134                 TableColumnModel colModel = getColumnModel();
135                 Arrays.stream(PlaylistTableModel.Column.values()).forEach(c->{
136                         TableColumn tc = colModel.getColumn(c.ordinal());
137                         tc.setPreferredWidth(c.preferredWidth);
138                         if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
139                 });
140                 selectionModel.addListSelectionListener(event->{
141                         if( event.getValueIsAdjusting() ) return;
142                         trackListTable.setModel(getSelectedSequenceModel());
143                         trackListTable.titleLabel.showMidiFileNumber(selectionModel);
144                 });
145         }
146         private TableColumn lengthColumn;
147         @Override
148         public void tableChanged(TableModelEvent event) {
149                 super.tableChanged(event);
150                 //
151                 // タイトルに合計シーケンス長を表示
152                 if( lengthColumn != null ) {
153                         int sec = getModel().getSecondLength();
154                         String title = PlaylistTableModel.Column.LENGTH.title;
155                         title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
156                         lengthColumn.setHeaderValue(title);
157                 }
158                 // シーケンス削除時など、合計シーケンス長が変わっても
159                 // 列モデルからではヘッダタイトルが再描画されないことがある。
160                 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
161                 JTableHeader th = getTableHeader();
162                 if( th != null ) th.repaint();
163         }
164         /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
165         private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
166                 public PositionCellEditor() {
167                         getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal()).setCellEditor(this);
168                 }
169                 /**
170                  * セルをダブルクリックしたときだけ編集モードに入るようにします。
171                  * @param e イベント(マウスイベント)
172                  * @return 編集可能な場合true
173                  */
174                 @Override
175                 public boolean isCellEditable(EventObject e) {
176                         return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
177                 }
178                 @Override
179                 public Object getCellEditorValue() { return null; }
180                 /**
181                  * 編集モード時のコンポーネントを返すタイミングで
182                  * そのシーケンスをシーケンサーにロードしたあと、すぐに編集モードを解除します。
183                  * @return 常にnull
184                  */
185                 @Override
186                 public Component getTableCellEditorComponent(
187                         JTable table, Object value, boolean isSelected, int row, int column
188                 ) {
189                         try {
190                                 getModel().loadToSequencer(row);
191                         } catch (InvalidMidiDataException|IllegalStateException ex) {
192                                 JOptionPane.showMessageDialog(
193                                                 table.getRootPane(), ex,
194                                                 ChordHelperApplet.VersionInfo.NAME,
195                                                 JOptionPane.ERROR_MESSAGE);
196                         }
197                         fireEditingStopped();
198                         return null;
199                 }
200         }
201         /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
202         private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
203                 /** 埋め込み用の再生ボタン */
204                 private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
205                         { setMargin(ChordHelperApplet.ZERO_INSETS); }
206                 };
207                 /**
208                  * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
209                  */
210                 private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
211                         { setMargin(ChordHelperApplet.ZERO_INSETS); }
212                 };
213                 /**
214                  * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
215                  */
216                 public PlayButtonCellEditor() {
217                         TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
218                         tc.setCellRenderer(this);
219                         tc.setCellEditor(this);
220                 }
221                 /**
222                  * {@inheritDoc}
223                  *
224                  * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
225                  * trueを返して再生ボタンを押せるようにします。
226                  * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
227                  * ダブルクリックされたときだけtrueを返します。
228                  * </p>
229                  */
230                 @Override
231                 public boolean isCellEditable(EventObject e) {
232                         // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
233                         if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
234                         //
235                         // エディタが編集を終了したことをリスナーに通知
236                         fireEditingStopped();
237                         //
238                         // クリックされたセルの行位置を把握(欄外だったら編集不可)
239                         MouseEvent me = (MouseEvent)e;
240                         int row = rowAtPoint(me.getPoint());
241                         if( row < 0 ) return false;
242                         //
243                         // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
244                         // それ以外は、ダブルクリックのみ受け付ける。
245                         return getModel().getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
246                 }
247                 @Override
248                 public Object getCellEditorValue() { return null; }
249                 /**
250                  * {@inheritDoc}
251                  *
252                  * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
253                  * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
254                  * 以降の編集は不可としてnullを返します。
255                  * </p>
256                  */
257                 @Override
258                 public Component getTableCellEditorComponent(
259                         JTable table, Object value, boolean isSelected, int row, int column
260                 ) {
261                         fireEditingStopped();
262                         PlaylistTableModel model = getModel();
263                         if( model.getSequenceModelList().get(row).isOnSequencer() ) {
264                                 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
265                         }
266                         try {
267                                 model.loadToSequencer(row);
268                         } catch (InvalidMidiDataException ex) {
269                                 JOptionPane.showMessageDialog(
270                                                 table.getRootPane(), ex,
271                                                 ChordHelperApplet.VersionInfo.NAME,
272                                                 JOptionPane.ERROR_MESSAGE);
273                         }
274                         return null;
275                 }
276                 /**
277                  * {@inheritDoc}
278                  *
279                  * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
280                  * それ以外の場合はデフォルトレンダラーに描画させます。
281                  * </p>
282                  */
283                 @Override
284                 public Component getTableCellRendererComponent(
285                         JTable table, Object value, boolean isSelected,
286                         boolean hasFocus, int row, int column
287                 ) {
288                         PlaylistTableModel model = getModel();
289                         if( model.getSequenceModelList().get(row).isOnSequencer() ) {
290                                 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
291                         }
292                         return table.getDefaultRenderer(model.getColumnClass(column))
293                                 .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
294                 }
295         }
296         /**
297          * このプレイリスト(シーケンスリスト)が表示するデータを提供するプレイリストモデルを返します。
298          * @return プレイリストモデル
299          */
300         @Override
301         public PlaylistTableModel getModel() { return (PlaylistTableModel)dataModel; }
302     /**
303      * {@link #add(List)} を呼び出し、このプレイリストにMIDIファイルを追加します。
304      * @param files MIDIファイル
305      * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
306      */
307         public int add(File... files) {
308                 return add(Arrays.asList(files));
309         }
310         /**
311          * このプレイリストにMIDIファイルを追加します。追加に失敗した場合はダイアログを表示し、
312          * 後続のMIDIファイルが残っていればそれを追加するかどうかをユーザに尋ねます。
313          * @param files MIDIファイルのリスト
314          * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
315          */
316         public int add(List<File> files) {
317                 int firstIndex = -1;
318                 Iterator<File> itr = files.iterator();
319                 while(itr.hasNext()) {
320                         File file = itr.next();
321                         try (FileInputStream in = new FileInputStream(file)) {
322                                 Sequence sequence = MidiSystem.getSequence(in);
323                                 int lastIndex = ((PlaylistTableModel)dataModel).add(sequence, file.getName());
324                                 if( firstIndex < 0 ) firstIndex = lastIndex;
325                         } catch(IOException|InvalidMidiDataException e) {
326                                 String message = "Could not open as MIDI file "+file+"\n"+e;
327                                 if( ! itr.hasNext() ) {
328                                         JOptionPane.showMessageDialog(
329                                                         getRootPane(), message,
330                                                         ChordHelperApplet.VersionInfo.NAME,
331                                                         JOptionPane.WARNING_MESSAGE);
332                                         break;
333                                 }
334                                 if( JOptionPane.showConfirmDialog(
335                                                 getRootPane(),
336                                                 message + "\n\nContinue to open next file ?",
337                                                 ChordHelperApplet.VersionInfo.NAME,
338                                                 JOptionPane.YES_NO_OPTION,
339                                                 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
340                                 ) break;
341                         } catch(Exception ex) {
342                                 JOptionPane.showMessageDialog(
343                                                 getRootPane(), ex, ChordHelperApplet.VersionInfo.NAME,
344                                                 JOptionPane.ERROR_MESSAGE);
345                                 break;
346                         }
347                 }
348                 return firstIndex;
349         }
350         /**
351          * 指定されたシーケンスを追加して再生します。
352          * @param sequence 再生するシーケンス
353          * @param charset 文字コード
354          * @return 追加されたシーケンスのインデックス(先頭が 0)
355          * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
356          * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
357          */
358         public int play(Sequence sequence, Charset charset) throws InvalidMidiDataException {
359                 int index = getModel().play(sequence, charset);
360                 selectionModel.setSelectionInterval(index, index);
361                 return index;
362         }
363         /**
364          * シーケンスを削除するアクション
365          */
366         Action deleteSequenceAction = new SelectedSequenceAction(
367                 "Delete", MidiSequenceEditorDialog.deleteIcon,
368                 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
369         ) {
370                 private static final String CONFIRM_MESSAGE =
371                         "Selected MIDI sequence not saved - delete it from the playlist ?\n" +
372                         "選択したMIDIシーケンスはまだ保存されていません。プレイリストから削除しますか?";
373                 @Override
374                 public void actionPerformed(ActionEvent event) {
375                         int index = getSelectedRow();
376                         if( index < 0 ) return;
377                         PlaylistTableModel model = getModel();
378                         List<SequenceTrackListTableModel> list = model.getSequenceModelList();
379                         if( index >= list.size() ) return;
380                         SequenceTrackListTableModel sequenceModel = list.get(index);
381                         if( sequenceModel == null ) return;
382                         if( midiFileChooser != null ) {
383                                 if( sequenceModel.isModified() && JOptionPane.showConfirmDialog(
384                                                 ((JComponent)event.getSource()).getRootPane(),
385                                                 CONFIRM_MESSAGE,
386                                                 ChordHelperApplet.VersionInfo.NAME,
387                                                 JOptionPane.YES_NO_OPTION,
388                                                 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
389                                 ) return;
390                         }
391                         try {
392                                 model.remove(index);
393                         } catch (Exception ex) {
394                                 JOptionPane.showMessageDialog(
395                                                 ((JComponent)event.getSource()).getRootPane(), ex,
396                                                 ChordHelperApplet.VersionInfo.NAME,
397                                                 JOptionPane.ERROR_MESSAGE);
398                         }
399                 }
400         };
401         /**
402          * ファイル選択ダイアログ(アプレットでは使用不可)
403          */
404         class MidiFileChooser extends JFileChooser {
405                 { setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")); }
406                 /**
407                  * ファイル保存アクション
408                  */
409                 public Action saveMidiFileAction = new SelectedSequenceAction(
410                         "Save",
411                         "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
412                 ) {
413                         @Override
414                         public void actionPerformed(ActionEvent event) {
415                                 SequenceTrackListTableModel sequenceModel = getSelectedSequenceModel();
416                                 if( sequenceModel == null ) return;
417                                 String fn = sequenceModel.getFilename();
418                                 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
419                                 JRootPane rootPane = ((JComponent)event.getSource()).getRootPane();
420                                 if( showSaveDialog(rootPane) != JFileChooser.APPROVE_OPTION ) return;
421                                 File f = getSelectedFile();
422                                 if( f.exists() ) {
423                                         fn = f.getName();
424                                         if( JOptionPane.showConfirmDialog(
425                                                         rootPane,
426                                                         "Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?",
427                                                         ChordHelperApplet.VersionInfo.NAME,
428                                                         JOptionPane.YES_NO_OPTION,
429                                                         JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
430                                         ) return;
431                                 }
432                                 try ( FileOutputStream o = new FileOutputStream(f) ) {
433                                         o.write(sequenceModel.getMIDIdata());
434                                         sequenceModel.setModified(false);
435                                 }
436                                 catch( Exception ex ) {
437                                         JOptionPane.showMessageDialog(
438                                                         rootPane, ex, ChordHelperApplet.VersionInfo.NAME,
439                                                         JOptionPane.ERROR_MESSAGE);
440                                 }
441                         }
442                 };
443                 /**
444                  * ファイルを開くアクション
445                  */
446                 public Action openMidiFileAction = new AbstractAction("Open") {
447                         { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
448                         @Override
449                         public void actionPerformed(ActionEvent event) {
450                                 JRootPane rootPane = ((JComponent)event.getSource()).getRootPane();
451                                 try {
452                                         if( showOpenDialog(rootPane) != JFileChooser.APPROVE_OPTION ) return;
453                                 } catch( HeadlessException ex ) {
454                                         ex.printStackTrace();
455                                         return;
456                                 }
457                                 int firstIndex = PlaylistTable.this.add(getSelectedFile());
458                                 try {
459                                         PlaylistTableModel model = getModel();
460                                         MidiSequencerModel sequencerModel = model.getSequencerModel();
461                                         if( sequencerModel.getSequencer().isRunning() ) return;
462                                         if( firstIndex >= 0 ) {
463                                                 model.play(firstIndex);
464                                                 selectionModel.setSelectionInterval(firstIndex, firstIndex);
465                                         }
466                                 } catch (Exception ex) {
467                                         JOptionPane.showMessageDialog(
468                                                         rootPane, ex, ChordHelperApplet.VersionInfo.NAME,
469                                                         JOptionPane.ERROR_MESSAGE);
470                                 }
471                         }
472                 };
473         };
474 }