OSDN Git Service

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