1 package camidion.chordhelper.midieditor;
3 import java.awt.Component;
4 import java.awt.HeadlessException;
5 import java.awt.event.ActionEvent;
6 import java.awt.event.MouseEvent;
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;
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;
40 import camidion.chordhelper.ChordHelperApplet;
41 import camidion.chordhelper.mididevice.MidiSequencerModel;
46 public class PlaylistTable extends JTable {
47 /** ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull) */
48 MidiFileChooser midiFileChooser;
49 /** BASE64エンコードアクション */
50 Action base64EncodeAction;
52 public Base64Dialog base64Dialog;
53 /** MIDIデバイスダイアログを開くアクション */
54 private Action midiDeviceDialogOpenAction;
57 * @param model プレイリストデータモデル
58 * @param midiDeviceDialogOpenAction MIDIデバイスダイアログを開くアクション
60 public PlaylistTable(PlaylistTableModel model, Action midiDeviceDialogOpenAction) {
61 super(model, null, model.sequenceListSelectionModel);
62 this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
64 midiFileChooser = new MidiFileChooser();
66 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
67 // アプレットの場合、Webクライアントマシンのローカルファイルには
68 // アクセスできないので、ファイル選択ダイアログは使用不可。
69 midiFileChooser = null;
72 new PlayButtonCellEditor();
73 new PositionCellEditor();
76 getColumnModel().getColumn(PlaylistTableModel.Column.CHARSET.ordinal())
77 .setCellEditor(new DefaultCellEditor(new JComboBox<Charset>() {{
78 Charset.availableCharsets().values().stream().forEach(v->addItem(v));
80 setAutoCreateColumnsFromModel(false);
82 // Base64画面を開くアクションの生成
83 base64Dialog = new Base64Dialog(model);
84 base64EncodeAction = new AbstractAction("Base64") {
86 String tooltip = "Base64 text conversion - Base64テキスト変換";
87 putValue(Action.SHORT_DESCRIPTION, tooltip);
90 public void actionPerformed(ActionEvent e) {
91 SequenceTrackListTableModel sequenceModel = getModel().getSelectedSequenceModel();
93 String filename = null;
94 if( sequenceModel != null ) {
95 filename = sequenceModel.getFilename();
97 data = sequenceModel.getMIDIdata();
98 } catch (IOException ioe) {
99 base64Dialog.setText("File["+filename+"]:"+ioe.toString());
100 base64Dialog.setVisible(true);
104 base64Dialog.setMIDIData(data, filename);
105 base64Dialog.setVisible(true);
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;
115 private TableColumn lengthColumn;
117 public void tableChanged(TableModelEvent event) {
118 super.tableChanged(event);
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);
127 // シーケンス削除時など、合計シーケンス長が変わっても
128 // 列モデルからではヘッダタイトルが再描画されないことがある。
129 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
130 JTableHeader th = getTableHeader();
131 if( th != null ) th.repaint();
133 /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
134 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
135 public PositionCellEditor() {
136 getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal()).setCellEditor(this);
139 * セルをダブルクリックしたときだけ編集モードに入るようにします。
140 * @param e イベント(マウスイベント)
141 * @return 編集可能な場合true
144 public boolean isCellEditable(EventObject e) {
145 return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
148 public Object getCellEditorValue() { return null; }
150 * 編集モード時のコンポーネントを返すタイミングで
151 * そのシーケンスをシーケンサーにロードしたあと、すぐに編集モードを解除します。
155 public Component getTableCellEditorComponent(
156 JTable table, Object value, boolean isSelected, int row, int column
159 getModel().loadToSequencer(row);
160 } catch (InvalidMidiDataException|IllegalStateException ex) {
161 JOptionPane.showMessageDialog(
162 table.getRootPane(), ex,
163 ChordHelperApplet.VersionInfo.NAME,
164 JOptionPane.ERROR_MESSAGE);
166 fireEditingStopped();
170 /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
171 private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
173 private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
174 { setMargin(ChordHelperApplet.ZERO_INSETS); }
177 * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
179 private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
180 { setMargin(ChordHelperApplet.ZERO_INSETS); }
183 * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
185 public PlayButtonCellEditor() {
186 TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
187 tc.setCellRenderer(this);
188 tc.setCellEditor(this);
193 * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
194 * trueを返して再生ボタンを押せるようにします。
195 * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
196 * ダブルクリックされたときだけtrueを返します。
200 public boolean isCellEditable(EventObject e) {
201 // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
202 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
204 // エディタが編集を終了したことをリスナーに通知
205 fireEditingStopped();
207 // クリックされたセルの行位置を把握(欄外だったら編集不可)
208 MouseEvent me = (MouseEvent)e;
209 int row = rowAtPoint(me.getPoint());
210 if( row < 0 ) return false;
212 // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
213 // それ以外は、ダブルクリックのみ受け付ける。
214 return getModel().getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
217 public Object getCellEditorValue() { return null; }
221 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
222 * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
223 * 以降の編集は不可としてnullを返します。
227 public Component getTableCellEditorComponent(
228 JTable table, Object value, boolean isSelected, int row, int column
230 fireEditingStopped();
231 PlaylistTableModel model = getModel();
232 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
233 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
236 model.loadToSequencer(row);
237 } catch (InvalidMidiDataException ex) {
238 JOptionPane.showMessageDialog(
239 table.getRootPane(), ex,
240 ChordHelperApplet.VersionInfo.NAME,
241 JOptionPane.ERROR_MESSAGE);
248 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
249 * それ以外の場合はデフォルトレンダラーに描画させます。
253 public Component getTableCellRendererComponent(
254 JTable table, Object value, boolean isSelected,
255 boolean hasFocus, int row, int column
257 PlaylistTableModel model = getModel();
258 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
259 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
261 return table.getDefaultRenderer(model.getColumnClass(column))
262 .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
266 * このプレイリスト(シーケンスリスト)が表示するデータを提供するプレイリストモデルを返します。
270 public PlaylistTableModel getModel() { return (PlaylistTableModel)dataModel; }
272 * このプレイリストにMIDIファイルを追加します。追加に失敗した場合はダイアログを表示し、
273 * 後続のMIDIファイルが残っていればそれを追加するかどうかをユーザに尋ねます。
274 * @param fileList MIDIファイルのリスト
275 * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
277 public int add(List<File> fileList) {
278 PlaylistTableModel model = getModel();
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);
295 if( JOptionPane.showConfirmDialog(
297 message + "\n\nContinue to open next file ?",
298 ChordHelperApplet.VersionInfo.NAME,
299 JOptionPane.YES_NO_OPTION,
300 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
302 } catch(Exception ex) {
303 JOptionPane.showMessageDialog(
305 ChordHelperApplet.VersionInfo.NAME,
306 JOptionPane.ERROR_MESSAGE);
315 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
316 "Delete", MidiSequenceEditorDialog.deleteIcon,
317 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
319 private static final String CONFIRM_MESSAGE =
320 "Selected MIDI sequence not saved - delete it from the playlist ?\n" +
321 "選択したMIDIシーケンスはまだ保存されていません。プレイリストから削除しますか?";
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(),
330 ChordHelperApplet.VersionInfo.NAME,
331 JOptionPane.YES_NO_OPTION,
332 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
335 if( ! model.sequenceListSelectionModel.isSelectionEmpty() ) try {
336 model.remove(model.sequenceListSelectionModel.getMinSelectionIndex());
337 } catch (Exception ex) {
338 JOptionPane.showMessageDialog(
339 ((JComponent)event.getSource()).getRootPane(),
341 ChordHelperApplet.VersionInfo.NAME,
342 JOptionPane.ERROR_MESSAGE);
347 * ファイル選択ダイアログ(アプレットでは使用不可)
349 class MidiFileChooser extends JFileChooser {
350 { setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")); }
354 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
356 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
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();
369 if( JOptionPane.showConfirmDialog(
371 "Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?",
372 ChordHelperApplet.VersionInfo.NAME,
373 JOptionPane.YES_NO_OPTION,
374 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
377 try ( FileOutputStream o = new FileOutputStream(f) ) {
378 o.write(sequenceModel.getMIDIdata());
379 sequenceModel.setModified(false);
381 catch( Exception ex ) {
382 JOptionPane.showMessageDialog(
384 ChordHelperApplet.VersionInfo.NAME,
385 JOptionPane.ERROR_MESSAGE);
392 public Action openMidiFileAction = new AbstractAction("Open") {
393 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
395 public void actionPerformed(ActionEvent event) {
396 JRootPane rootPane = ((JComponent)event.getSource()).getRootPane();
398 if( showOpenDialog(rootPane) != JFileChooser.APPROVE_OPTION ) return;
399 } catch( HeadlessException ex ) {
400 ex.printStackTrace();
403 int firstIndex = PlaylistTable.this.add(Arrays.asList(getSelectedFile()));
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(
412 ChordHelperApplet.VersionInfo.NAME,
413 JOptionPane.ERROR_MESSAGE);