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 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);
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 int index = selectionModel.getMinSelectionIndex();
87 setEnabled(index >= 0);
89 private void init(String tooltip) {
90 putValue(Action.SHORT_DESCRIPTION, tooltip);
91 selectionModel.addListSelectionListener(this);
92 setEnebledBySelection();
97 * @param model プレイリストデータモデル
98 * @param midiDeviceDialogOpenAction MIDIデバイスダイアログを開くアクション
99 * @param trackListTable トラックリストテーブル(子テーブル)
101 public PlaylistTable(PlaylistTableModel model, Action midiDeviceDialogOpenAction, SequenceTrackListTable trackListTable) {
103 setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
104 this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
106 midiFileChooser = new MidiFileChooser();
108 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
109 // アプレットの場合、Webクライアントマシンのローカルファイルには
110 // アクセスできないので、ファイル選択ダイアログは使用不可。
111 midiFileChooser = null;
114 new PlayButtonCellEditor();
115 new PositionCellEditor();
118 getColumnModel().getColumn(PlaylistTableModel.Column.CHARSET.ordinal())
119 .setCellEditor(new DefaultCellEditor(new CharsetComboBox()));
120 setAutoCreateColumnsFromModel(false);
122 // Base64画面を開くアクションの生成
123 base64Dialog = new Base64Dialog(this);
124 base64EncodeAction = new AbstractAction("Base64") {
126 String tooltip = "Base64 text conversion - Base64テキスト変換";
127 putValue(Action.SHORT_DESCRIPTION, tooltip);
130 public void actionPerformed(ActionEvent e) {
131 base64Dialog.setSequenceModel(getSelectedSequenceModel());
132 base64Dialog.setVisible(true);
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;
141 selectionModel.addListSelectionListener(event->{
142 if( event.getValueIsAdjusting() ) return;
143 trackListTable.setModel(getSelectedSequenceModel());
144 trackListTable.titleLabel.showMidiFileNumber(selectionModel);
147 private TableColumn lengthColumn;
149 public void tableChanged(TableModelEvent event) {
150 super.tableChanged(event);
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);
159 // シーケンス削除時など、合計シーケンス長が変わっても
160 // 列モデルからではヘッダタイトルが再描画されないことがある。
161 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
162 JTableHeader th = getTableHeader();
163 if( th != null ) th.repaint();
165 /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
166 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
167 public PositionCellEditor() {
168 getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal()).setCellEditor(this);
171 * セルをダブルクリックしたときだけ編集モードに入るようにします。
172 * @param e イベント(マウスイベント)
173 * @return 編集可能な場合true
176 public boolean isCellEditable(EventObject e) {
177 return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
180 public Object getCellEditorValue() { return null; }
182 * 編集モード時のコンポーネントを返すタイミングで
183 * そのシーケンスをシーケンサーにロードしたあと、すぐに編集モードを解除します。
187 public Component getTableCellEditorComponent(
188 JTable table, Object value, boolean isSelected, int row, int column
191 getModel().loadToSequencer(row);
192 } catch (InvalidMidiDataException|IllegalStateException ex) {
193 JOptionPane.showMessageDialog(
194 table.getRootPane(), ex,
195 ChordHelperApplet.VersionInfo.NAME,
196 JOptionPane.ERROR_MESSAGE);
198 fireEditingStopped();
202 /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
203 private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
205 private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
206 { setMargin(ChordHelperApplet.ZERO_INSETS); }
209 * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
211 private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
212 { setMargin(ChordHelperApplet.ZERO_INSETS); }
215 * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
217 public PlayButtonCellEditor() {
218 TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
219 tc.setCellRenderer(this);
220 tc.setCellEditor(this);
225 * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
226 * trueを返して再生ボタンを押せるようにします。
227 * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
228 * ダブルクリックされたときだけtrueを返します。
232 public boolean isCellEditable(EventObject e) {
233 // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
234 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
236 // エディタが編集を終了したことをリスナーに通知
237 fireEditingStopped();
239 // クリックされたセルの行位置を把握(欄外だったら編集不可)
240 MouseEvent me = (MouseEvent)e;
241 int row = rowAtPoint(me.getPoint());
242 if( row < 0 ) return false;
244 // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
245 // それ以外は、ダブルクリックのみ受け付ける。
246 return getModel().getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
249 public Object getCellEditorValue() { return null; }
253 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
254 * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
255 * 以降の編集は不可としてnullを返します。
259 public Component getTableCellEditorComponent(
260 JTable table, Object value, boolean isSelected, int row, int column
262 fireEditingStopped();
263 PlaylistTableModel model = getModel();
264 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
265 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
268 model.loadToSequencer(row);
269 } catch (InvalidMidiDataException ex) {
270 JOptionPane.showMessageDialog(
271 table.getRootPane(), ex,
272 ChordHelperApplet.VersionInfo.NAME,
273 JOptionPane.ERROR_MESSAGE);
280 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
281 * それ以外の場合はデフォルトレンダラーに描画させます。
285 public Component getTableCellRendererComponent(
286 JTable table, Object value, boolean isSelected,
287 boolean hasFocus, int row, int column
289 PlaylistTableModel model = getModel();
290 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
291 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
293 return table.getDefaultRenderer(model.getColumnClass(column))
294 .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
298 * このプレイリスト(シーケンスリスト)が表示するデータを提供するプレイリストモデルを返します。
302 public PlaylistTableModel getModel() { return (PlaylistTableModel)dataModel; }
304 * {@link #add(List)} を呼び出し、このプレイリストにMIDIファイルを追加します。
305 * @param files MIDIファイル
306 * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
308 public int add(File... files) {
309 return add(Arrays.asList(files));
312 * このプレイリストにMIDIファイルを追加します。追加に失敗した場合はダイアログを表示し、
313 * 後続のMIDIファイルが残っていればそれを追加するかどうかをユーザに尋ねます。
314 * @param files MIDIファイルのリスト
315 * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
317 public int add(List<File> files) {
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);
335 if( JOptionPane.showConfirmDialog(
337 message + "\n\nContinue to open next file ?",
338 ChordHelperApplet.VersionInfo.NAME,
339 JOptionPane.YES_NO_OPTION,
340 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
342 } catch(Exception ex) {
343 JOptionPane.showMessageDialog(
344 getRootPane(), ex, ChordHelperApplet.VersionInfo.NAME,
345 JOptionPane.ERROR_MESSAGE);
352 * 指定されたシーケンスを追加して再生します。
353 * @param sequence 再生するシーケンス
354 * @param charset 文字コード
355 * @return 追加されたシーケンスのインデックス(先頭が 0)
356 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
357 * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
359 public int play(Sequence sequence, Charset charset) throws InvalidMidiDataException {
360 int index = getModel().play(sequence, charset);
361 selectionModel.setSelectionInterval(index, index);
367 Action deleteSequenceAction = new SelectedSequenceAction(
368 "Delete", MidiSequenceEditorDialog.deleteIcon,
369 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
371 private static final String CONFIRM_MESSAGE =
372 "Selected MIDI sequence not saved - delete it from the playlist ?\n" +
373 "選択したMIDIシーケンスはまだ保存されていません。プレイリストから削除しますか?";
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(),
382 ChordHelperApplet.VersionInfo.NAME,
383 JOptionPane.YES_NO_OPTION,
384 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
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);
398 * ファイル選択ダイアログ(アプレットでは使用不可)
400 class MidiFileChooser extends JFileChooser {
401 { setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")); }
405 public Action saveMidiFileAction = new SelectedSequenceAction(
407 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
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();
420 if( JOptionPane.showConfirmDialog(
422 "Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?",
423 ChordHelperApplet.VersionInfo.NAME,
424 JOptionPane.YES_NO_OPTION,
425 JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION
428 try ( FileOutputStream o = new FileOutputStream(f) ) {
429 o.write(sequenceModel.getMIDIdata());
430 sequenceModel.setModified(false);
432 catch( Exception ex ) {
433 JOptionPane.showMessageDialog(
434 rootPane, ex, ChordHelperApplet.VersionInfo.NAME,
435 JOptionPane.ERROR_MESSAGE);
442 public Action openMidiFileAction = new AbstractAction("Open") {
443 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
445 public void actionPerformed(ActionEvent event) {
446 JRootPane rootPane = ((JComponent)event.getSource()).getRootPane();
448 if( showOpenDialog(rootPane) != JFileChooser.APPROVE_OPTION ) return;
449 } catch( HeadlessException ex ) {
450 ex.printStackTrace();
453 int firstIndex = PlaylistTable.this.add(getSelectedFile());
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);
462 } catch (Exception ex) {
463 JOptionPane.showMessageDialog(
464 rootPane, ex, ChordHelperApplet.VersionInfo.NAME,
465 JOptionPane.ERROR_MESSAGE);