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.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;
45 import camidion.chordhelper.ChordHelperApplet;
46 import camidion.chordhelper.mididevice.MidiSequencerModel;
51 public class PlaylistTable extends JTable {
52 /** ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull) */
53 MidiFileChooser midiFileChooser;
54 /** BASE64エンコードアクション */
55 Action base64EncodeAction;
57 public Base64Dialog base64Dialog;
58 /** MIDIデバイスダイアログを開くアクション */
59 private Action midiDeviceDialogOpenAction;
61 * 選択されたMIDIシーケンスのテーブルモデルを返します。
62 * @return 選択されたMIDIシーケンスのテーブルモデル(非選択時はnull)
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);
71 * 行が選択されているときだけイネーブルになるアクション
73 private abstract class SelectedSequenceAction extends AbstractAction implements ListSelectionListener {
74 public SelectedSequenceAction(String name, Icon icon, String tooltip) {
75 super(name,icon); init(tooltip);
77 public SelectedSequenceAction(String name, String tooltip) {
78 super(name); init(tooltip);
81 public void valueChanged(ListSelectionEvent e) {
82 if( e.getValueIsAdjusting() ) return;
83 setEnebledBySelection();
85 protected void setEnebledBySelection() {
86 setEnabled(getSelectedRow() >= 0);
88 private void init(String tooltip) {
89 putValue(Action.SHORT_DESCRIPTION, tooltip);
90 selectionModel.addListSelectionListener(this);
91 setEnebledBySelection();
96 * @param model プレイリストデータモデル
97 * @param midiDeviceDialogOpenAction MIDIデバイスダイアログを開くアクション
98 * @param trackListTable トラックリストテーブル(子テーブル)
100 public PlaylistTable(PlaylistTableModel model, Action midiDeviceDialogOpenAction, SequenceTrackListTable trackListTable) {
102 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
103 this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
105 midiFileChooser = new MidiFileChooser();
107 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
108 // アプレットの場合、Webクライアントマシンのローカルファイルには
109 // アクセスできないので、ファイル選択ダイアログは使用不可。
110 midiFileChooser = null;
113 new PlayButtonCellEditor();
114 new PositionCellEditor();
117 getColumnModel().getColumn(PlaylistTableModel.Column.CHARSET.ordinal())
118 .setCellEditor(new DefaultCellEditor(new CharsetComboBox()));
119 setAutoCreateColumnsFromModel(false);
121 // Base64画面を開くアクションの生成
122 base64Dialog = new Base64Dialog(this);
123 base64EncodeAction = new AbstractAction("Base64") {
125 String tooltip = "Base64 text conversion - Base64テキスト変換";
126 putValue(Action.SHORT_DESCRIPTION, tooltip);
129 public void actionPerformed(ActionEvent e) {
130 base64Dialog.setSequenceModel(getSelectedSequenceModel());
131 base64Dialog.setVisible(true);
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;
140 selectionModel.addListSelectionListener(event->{
141 if( event.getValueIsAdjusting() ) return;
142 trackListTable.setModel(getSelectedSequenceModel());
143 trackListTable.titleLabel.showMidiFileNumber(selectionModel);
146 private TableColumn lengthColumn;
148 public void tableChanged(TableModelEvent event) {
149 super.tableChanged(event);
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);
158 // シーケンス削除時など、合計シーケンス長が変わっても
159 // 列モデルからではヘッダタイトルが再描画されないことがある。
160 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
161 JTableHeader th = getTableHeader();
162 if( th != null ) th.repaint();
164 /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
165 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
166 public PositionCellEditor() {
167 getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal()).setCellEditor(this);
170 * セルをダブルクリックしたときだけ編集モードに入るようにします。
171 * @param e イベント(マウスイベント)
172 * @return 編集可能な場合true
175 public boolean isCellEditable(EventObject e) {
176 return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
179 public Object getCellEditorValue() { return null; }
181 * 編集モード時のコンポーネントを返すタイミングで
182 * そのシーケンスをシーケンサーにロードしたあと、すぐに編集モードを解除します。
186 public Component getTableCellEditorComponent(
187 JTable table, Object value, boolean isSelected, int row, int column
190 getModel().loadToSequencer(row);
191 } catch (InvalidMidiDataException|IllegalStateException ex) {
192 JOptionPane.showMessageDialog(
193 table.getRootPane(), ex,
194 ChordHelperApplet.VersionInfo.NAME,
195 JOptionPane.ERROR_MESSAGE);
197 fireEditingStopped();
201 /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
202 private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
204 private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
205 { setMargin(ChordHelperApplet.ZERO_INSETS); }
208 * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
210 private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
211 { setMargin(ChordHelperApplet.ZERO_INSETS); }
214 * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
216 public PlayButtonCellEditor() {
217 TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
218 tc.setCellRenderer(this);
219 tc.setCellEditor(this);
224 * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
225 * trueを返して再生ボタンを押せるようにします。
226 * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
227 * ダブルクリックされたときだけtrueを返します。
231 public boolean isCellEditable(EventObject e) {
232 // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
233 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
235 // エディタが編集を終了したことをリスナーに通知
236 fireEditingStopped();
238 // クリックされたセルの行位置を把握(欄外だったら編集不可)
239 MouseEvent me = (MouseEvent)e;
240 int row = rowAtPoint(me.getPoint());
241 if( row < 0 ) return false;
243 // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
244 // それ以外は、ダブルクリックのみ受け付ける。
245 return getModel().getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
248 public Object getCellEditorValue() { return null; }
252 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
253 * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
254 * 以降の編集は不可としてnullを返します。
258 public Component getTableCellEditorComponent(
259 JTable table, Object value, boolean isSelected, int row, int column
261 fireEditingStopped();
262 PlaylistTableModel model = getModel();
263 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
264 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
267 model.loadToSequencer(row);
268 } catch (InvalidMidiDataException ex) {
269 JOptionPane.showMessageDialog(
270 table.getRootPane(), ex,
271 ChordHelperApplet.VersionInfo.NAME,
272 JOptionPane.ERROR_MESSAGE);
279 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
280 * それ以外の場合はデフォルトレンダラーに描画させます。
284 public Component getTableCellRendererComponent(
285 JTable table, Object value, boolean isSelected,
286 boolean hasFocus, int row, int column
288 PlaylistTableModel model = getModel();
289 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
290 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
292 return table.getDefaultRenderer(model.getColumnClass(column))
293 .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
297 * このプレイリスト(シーケンスリスト)が表示するデータを提供するプレイリストモデルを返します。
301 public PlaylistTableModel getModel() { return (PlaylistTableModel)dataModel; }
303 * {@link #add(List)} を呼び出し、このプレイリストにMIDIファイルを追加します。
304 * @param files MIDIファイル
305 * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
307 public int add(File... files) {
308 return add(Arrays.asList(files));
311 * このプレイリストにMIDIファイルを追加します。追加に失敗した場合はダイアログを表示し、
312 * 後続のMIDIファイルが残っていればそれを追加するかどうかをユーザに尋ねます。
313 * @param files MIDIファイルのリスト
314 * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
316 public int add(List<File> files) {
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);
334 if( JOptionPane.showConfirmDialog(
336 message + "\n\nContinue to open next file ?",
337 ChordHelperApplet.VersionInfo.NAME,
338 JOptionPane.YES_NO_OPTION,
339 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
341 } catch(Exception ex) {
342 JOptionPane.showMessageDialog(
343 getRootPane(), ex, ChordHelperApplet.VersionInfo.NAME,
344 JOptionPane.ERROR_MESSAGE);
351 * 指定されたシーケンスを追加して再生します。
352 * @param sequence 再生するシーケンス
353 * @param charset 文字コード
354 * @return 追加されたシーケンスのインデックス(先頭が 0)
355 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
356 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
358 public int play(Sequence sequence, Charset charset) throws InvalidMidiDataException {
359 int index = getModel().play(sequence, charset);
360 selectionModel.setSelectionInterval(index, index);
366 Action deleteSequenceAction = new SelectedSequenceAction(
367 "Delete", MidiSequenceEditorDialog.deleteIcon,
368 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
370 private static final String CONFIRM_MESSAGE =
371 "Selected MIDI sequence not saved - delete it from the playlist ?\n" +
372 "選択したMIDIシーケンスはまだ保存されていません。プレイリストから削除しますか?";
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(),
386 ChordHelperApplet.VersionInfo.NAME,
387 JOptionPane.YES_NO_OPTION,
388 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
393 } catch (Exception ex) {
394 JOptionPane.showMessageDialog(
395 ((JComponent)event.getSource()).getRootPane(), ex,
396 ChordHelperApplet.VersionInfo.NAME,
397 JOptionPane.ERROR_MESSAGE);
402 * ファイル選択ダイアログ(アプレットでは使用不可)
404 class MidiFileChooser extends JFileChooser {
405 { setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")); }
409 public Action saveMidiFileAction = new SelectedSequenceAction(
411 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
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();
424 if( JOptionPane.showConfirmDialog(
426 "Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?",
427 ChordHelperApplet.VersionInfo.NAME,
428 JOptionPane.YES_NO_OPTION,
429 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
432 try ( FileOutputStream o = new FileOutputStream(f) ) {
433 o.write(sequenceModel.getMIDIdata());
434 sequenceModel.setModified(false);
436 catch( Exception ex ) {
437 JOptionPane.showMessageDialog(
438 rootPane, ex, ChordHelperApplet.VersionInfo.NAME,
439 JOptionPane.ERROR_MESSAGE);
446 public Action openMidiFileAction = new AbstractAction("Open") {
447 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
449 public void actionPerformed(ActionEvent event) {
450 JRootPane rootPane = ((JComponent)event.getSource()).getRootPane();
452 if( showOpenDialog(rootPane) != JFileChooser.APPROVE_OPTION ) return;
453 } catch( HeadlessException ex ) {
454 ex.printStackTrace();
457 int firstIndex = PlaylistTable.this.add(getSelectedFile());
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);
466 } catch (Exception ex) {
467 JOptionPane.showMessageDialog(
468 rootPane, ex, ChordHelperApplet.VersionInfo.NAME,
469 JOptionPane.ERROR_MESSAGE);