OSDN Git Service

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