1 package camidion.chordhelper.midieditor;
3 import java.awt.Component;
4 import java.awt.Container;
5 import java.awt.Dimension;
6 import java.awt.FlowLayout;
7 import java.awt.datatransfer.DataFlavor;
8 import java.awt.event.ActionEvent;
9 import java.awt.event.ComponentAdapter;
10 import java.awt.event.ComponentEvent;
11 import java.awt.event.ComponentListener;
12 import java.awt.event.MouseEvent;
14 import java.io.FileInputStream;
15 import java.io.FileOutputStream;
16 import java.io.IOException;
17 import java.nio.charset.Charset;
18 import java.security.AccessControlException;
19 import java.util.Arrays;
20 import java.util.EventObject;
21 import java.util.Iterator;
22 import java.util.List;
24 import javax.sound.midi.InvalidMidiDataException;
25 import javax.sound.midi.MidiChannel;
26 import javax.sound.midi.MidiEvent;
27 import javax.sound.midi.MidiMessage;
28 import javax.sound.midi.MidiSystem;
29 import javax.sound.midi.Sequencer;
30 import javax.sound.midi.ShortMessage;
31 import javax.swing.AbstractAction;
32 import javax.swing.AbstractCellEditor;
33 import javax.swing.Action;
34 import javax.swing.Box;
35 import javax.swing.BoxLayout;
36 import javax.swing.DefaultCellEditor;
37 import javax.swing.Icon;
38 import javax.swing.JButton;
39 import javax.swing.JCheckBox;
40 import javax.swing.JComboBox;
41 import javax.swing.JDialog;
42 import javax.swing.JFileChooser;
43 import javax.swing.JLabel;
44 import javax.swing.JOptionPane;
45 import javax.swing.JPanel;
46 import javax.swing.JScrollPane;
47 import javax.swing.JSplitPane;
48 import javax.swing.JTable;
49 import javax.swing.JToggleButton;
50 import javax.swing.ListSelectionModel;
51 import javax.swing.TransferHandler;
52 import javax.swing.border.EtchedBorder;
53 import javax.swing.event.ListSelectionEvent;
54 import javax.swing.event.ListSelectionListener;
55 import javax.swing.event.TableModelEvent;
56 import javax.swing.filechooser.FileNameExtensionFilter;
57 import javax.swing.table.JTableHeader;
58 import javax.swing.table.TableCellEditor;
59 import javax.swing.table.TableCellRenderer;
60 import javax.swing.table.TableColumn;
61 import javax.swing.table.TableColumnModel;
62 import javax.swing.table.TableModel;
64 import camidion.chordhelper.ButtonIcon;
65 import camidion.chordhelper.ChordHelperApplet;
66 import camidion.chordhelper.mididevice.MidiSequencerModel;
67 import camidion.chordhelper.mididevice.VirtualMidiDevice;
68 import camidion.chordhelper.music.MIDISpec;
71 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
74 * Copyright (C) 2006-2016 Akiyoshi Kamide
75 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
77 public class MidiSequenceEditorDialog extends JDialog {
81 public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
83 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
84 putValue(Action.SHORT_DESCRIPTION, tooltip);
87 public void actionPerformed(ActionEvent e) {
88 if( isVisible() ) toFront(); else setVisible(true);
91 private Action midiDeviceDialogOpenAction;
94 * エラーメッセージダイアログを表示します。
95 * @param message エラーメッセージ
97 public void showError(Object message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
100 * @param message 警告メッセージ
102 public void showWarning(Object message) { showMessage(message, JOptionPane.WARNING_MESSAGE); }
103 private void showMessage(Object message, int messageType) {
104 JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, messageType);
108 * @param message 確認メッセージ
109 * @return 確認OKのときtrue
111 public boolean confirm(Object message) {
112 return JOptionPane.showConfirmDialog(this, message, ChordHelperApplet.VersionInfo.NAME,
113 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION ;
116 /** ドロップされた複数のMIDIファイルを読み込むハンドラー */
117 public final TransferHandler transferHandler = new TransferHandler() {
119 public boolean canImport(TransferSupport support) {
120 return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
122 @SuppressWarnings("unchecked")
124 public boolean importData(TransferSupport support) {
126 play((List<File>)support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
128 } catch (Exception e) { showError(e); return false; }
132 * このエディタダイアログが表示しているプレイリストモデルを返します。
135 public PlaylistTableModel getPlaylistModel() {
136 return sequenceListTable.getModel();
139 * 指定されたリストに格納されたMIDIファイルを読み込んで再生します。
140 * すでに再生されていた場合、このエディタダイアログを表示します。
141 * @param fileList 読み込むMIDIファイルのリスト
143 public void play(List<File> fileList) {
144 PlaylistTableModel playlist = getPlaylistModel();
146 Iterator<File> itr = fileList.iterator();
147 while(itr.hasNext()) {
148 File file = itr.next();
149 try (FileInputStream in = new FileInputStream(file)) {
150 int lastIndex = playlist.add(MidiSystem.getSequence(in), file.getName());
151 if( firstIndex < 0 ) firstIndex = lastIndex;
152 } catch(IOException|InvalidMidiDataException e) {
153 String message = "Could not open as MIDI file "+file+"\n"+e;
154 if( ! itr.hasNext() ) { showWarning(message); break; }
155 if( ! confirm(message + "\n\nContinue to open next file ?") ) break;
156 } catch(AccessControlException e) {
158 } catch(Exception e) {
163 MidiSequencerModel sequencerModel = playlist.getSequencerModel();
164 if( sequencerModel.getSequencer().isRunning() ) {
165 String command = (String)openAction.getValue(Action.NAME);
166 openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
169 if( firstIndex >= 0 ) playlist.play(firstIndex);
170 } catch (Exception e) { showError(e); }
173 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
175 * 新しいMIDIシーケンスを生成するダイアログ
177 public NewSequenceDialog newSequenceDialog;
181 public Base64Dialog base64Dialog;
183 * プレイリストビュー(シーケンスリスト)
185 private SequenceListTable sequenceListTable;
187 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
189 private TrackListTable trackListTable;
191 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
193 private EventListTable eventListTable;
195 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
197 public MidiEventDialog eventDialog = new MidiEventDialog();
201 private VirtualMidiDevice outputMidiDevice;
203 * プレイリストビュー(シーケンスリスト)
205 public class SequenceListTable extends JTable {
207 * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
209 private MidiFileChooser midiFileChooser;
211 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
213 private Action base64EncodeAction;
216 * @param model プレイリストデータモデル
218 public SequenceListTable(PlaylistTableModel model) {
219 super(model, null, model.sequenceListSelectionModel);
221 midiFileChooser = new MidiFileChooser();
223 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
224 // アプレットの場合、Webクライアントマシンのローカルファイルには
225 // アクセスできないので、ファイル選択ダイアログは使用不可。
226 midiFileChooser = null;
229 new PlayButtonCellEditor();
230 new PositionCellEditor();
233 int column = PlaylistTableModel.Column.CHARSET.ordinal();
234 TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
235 Charset.availableCharsets().values().stream().forEach(v->addItem(v));
237 getColumnModel().getColumn(column).setCellEditor(ce);
238 setAutoCreateColumnsFromModel(false);
240 // Base64画面を開くアクションの生成
241 base64Dialog = new Base64Dialog(model);
242 base64EncodeAction = new AbstractAction("Base64") {
244 String tooltip = "Base64 text conversion - Base64テキスト変換";
245 putValue(Action.SHORT_DESCRIPTION, tooltip);
248 public void actionPerformed(ActionEvent e) {
249 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
251 String filename = null;
253 filename = mstm.getFilename();
255 data = mstm.getMIDIdata();
256 } catch (IOException ioe) {
257 base64Dialog.setText("File["+filename+"]:"+ioe.toString());
258 base64Dialog.setVisible(true);
262 base64Dialog.setMIDIData(data, filename);
263 base64Dialog.setVisible(true);
266 TableColumnModel colModel = getColumnModel();
267 Arrays.stream(PlaylistTableModel.Column.values()).forEach(c->{
268 TableColumn tc = colModel.getColumn(c.ordinal());
269 tc.setPreferredWidth(c.preferredWidth);
270 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
273 private TableColumn lengthColumn;
275 public void tableChanged(TableModelEvent event) {
276 super.tableChanged(event);
279 if( lengthColumn != null ) {
280 int sec = getModel().getSecondLength();
281 String title = PlaylistTableModel.Column.LENGTH.title;
282 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
283 lengthColumn.setHeaderValue(title);
285 // シーケンス削除時など、合計シーケンス長が変わっても
286 // 列モデルからではヘッダタイトルが再描画されないことがある。
287 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
288 JTableHeader th = getTableHeader();
289 if( th != null ) th.repaint();
291 /** 時間位置を表示し、ダブルクリックによるシーケンサへのロードのみを受け付けるセルエディタ */
292 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
293 public PositionCellEditor() {
294 getColumnModel().getColumn(PlaylistTableModel.Column.POSITION.ordinal()).setCellEditor(this);
297 * セルをダブルクリックしたときだけ編集モードに入るようにします。
298 * @param e イベント(マウスイベント)
299 * @return 編集可能な場合true
302 public boolean isCellEditable(EventObject e) {
303 return (e instanceof MouseEvent) && ((MouseEvent)e).getClickCount() == 2;
306 public Object getCellEditorValue() { return null; }
308 * 編集モード時のコンポーネントを返すタイミングで
309 * そのシーケンスをシーケンサーにロードしたあと、すぐに編集モードを解除します。
313 public Component getTableCellEditorComponent(
314 JTable table, Object value, boolean isSelected, int row, int column
317 getModel().loadToSequencer(row);
318 } catch (InvalidMidiDataException|IllegalStateException ex) {
321 fireEditingStopped();
325 /** 再生ボタンを埋め込んだセルの編集、描画を行うクラスです。 */
326 private class PlayButtonCellEditor extends AbstractCellEditor implements TableCellEditor, TableCellRenderer {
328 private JToggleButton playButton = new JToggleButton(getModel().getSequencerModel().getStartStopAction()) {
329 { setMargin(ChordHelperApplet.ZERO_INSETS); }
332 * 埋め込み用のMIDIデバイス接続ボタン(そのシーケンスをロードしているシーケンサが開いていなかったときに表示)
334 private JButton midiDeviceConnectionButton = new JButton(midiDeviceDialogOpenAction) {
335 { setMargin(ChordHelperApplet.ZERO_INSETS); }
338 * 再生ボタンを埋め込むセルエディタを構築し、列に対するレンダラ、エディタとして登録します。
340 public PlayButtonCellEditor() {
341 TableColumn tc = getColumnModel().getColumn(PlaylistTableModel.Column.PLAY.ordinal());
342 tc.setCellRenderer(this);
343 tc.setCellEditor(this);
348 * <p>この実装では、クリックしたセルのシーケンスがシーケンサーで再生可能な場合に
349 * trueを返して再生ボタンを押せるようにします。
350 * それ以外のセルについては、新たにシーケンサーへのロードを可能にするため、
351 * ダブルクリックされたときだけtrueを返します。
355 public boolean isCellEditable(EventObject e) {
356 // マウスイベントのみを受け付け、それ以外はデフォルトエディタに振る
357 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
359 // エディタが編集を終了したことをリスナーに通知
360 fireEditingStopped();
362 // クリックされたセルの行位置を把握(欄外だったら編集不可)
363 MouseEvent me = (MouseEvent)e;
364 int row = rowAtPoint(me.getPoint());
365 if( row < 0 ) return false;
367 // シーケンサーにロード済みの場合は、シングルクリックを受け付ける。
368 // それ以外は、ダブルクリックのみ受け付ける。
369 return getModel().getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
372 public Object getCellEditorValue() { return null; }
376 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
377 * それ以外の場合は、新たにそのシーケンスをシーケンサーにロードしますが、
378 * 以降の編集は不可としてnullを返します。
382 public Component getTableCellEditorComponent(
383 JTable table, Object value, boolean isSelected, int row, int column
385 fireEditingStopped();
386 PlaylistTableModel model = getModel();
387 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
388 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
391 model.loadToSequencer(row);
392 } catch (InvalidMidiDataException ex) { showError(ex); }
398 * <p>この実装では、行の表すシーケンスの状態に応じたボタンを表示します。
399 * それ以外の場合はデフォルトレンダラーに描画させます。
403 public Component getTableCellRendererComponent(
404 JTable table, Object value, boolean isSelected,
405 boolean hasFocus, int row, int column
407 PlaylistTableModel model = getModel();
408 if( model.getSequenceModelList().get(row).isOnSequencer() ) {
409 return model.getSequencerModel().getSequencer().isOpen() ? playButton : midiDeviceConnectionButton;
411 return table.getDefaultRenderer(model.getColumnClass(column))
412 .getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
416 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
421 public PlaylistTableModel getModel() {
422 return (PlaylistTableModel)super.getModel();
427 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
428 "Delete", MidiSequenceEditorDialog.deleteIcon,
429 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
431 private static final String CONFIRM_MESSAGE =
432 "Selected MIDI sequence not saved - delete it from the playlist ?\n" +
433 "選択したMIDIシーケンスはまだ保存されていません。プレイリストから削除しますか?";
435 public void actionPerformed(ActionEvent event) {
436 PlaylistTableModel model = getModel();
437 if( midiFileChooser != null ) {
438 SequenceTrackListTableModel seqModel = model.getSelectedSequenceModel();
439 if( seqModel != null && seqModel.isModified() && ! confirm(CONFIRM_MESSAGE) ) {
444 model.removeSelectedSequence();
445 } catch (InvalidMidiDataException|IllegalStateException ex) {
451 * ファイル選択ダイアログ(アプレットでは使用不可)
453 private class MidiFileChooser extends JFileChooser {
454 { setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid")); }
458 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
460 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
463 public void actionPerformed(ActionEvent event) {
464 SequenceTrackListTableModel sequenceModel = getModel().getSelectedSequenceModel();
465 if( sequenceModel == null ) return;
466 String fn = sequenceModel.getFilename();
467 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
468 if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
469 File f = getSelectedFile();
472 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
474 try ( FileOutputStream o = new FileOutputStream(f) ) {
475 o.write(sequenceModel.getMIDIdata());
476 sequenceModel.setModified(false);
478 catch( Exception ex ) { showError(ex); }
484 public Action openMidiFileAction = new AbstractAction("Open") {
485 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
487 public void actionPerformed(ActionEvent event) {
489 if( showOpenDialog((Component)event.getSource()) == JFileChooser.APPROVE_OPTION ) {
490 play(Arrays.asList(getSelectedFile()));
492 } catch( Exception ex ) { showError(ex); }
499 * シーケンス(トラックリスト)テーブルビュー
501 public class TrackListTable extends JTable {
503 * トラックリストテーブルビューを構築します。
504 * @param model シーケンス(トラックリスト)データモデル
506 public TrackListTable(SequenceTrackListTableModel model) {
507 super(model, null, model.getSelectionModel());
509 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
511 .getColumn(SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal())
512 .setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
514 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
517 setAutoCreateColumnsFromModel(false);
518 model.getParent().sequenceListSelectionModel.addListSelectionListener(titleLabel);
519 TableColumnModel colModel = getColumnModel();
520 Arrays.stream(SequenceTrackListTableModel.Column.values()).forEach(c->
521 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth)
525 * このテーブルビューが表示するデータを提供する
526 * シーケンス(トラックリスト)データモデルを返します。
527 * @return シーケンス(トラックリスト)データモデル
530 public SequenceTrackListTableModel getModel() {
531 return (SequenceTrackListTableModel) super.getModel();
536 TitleLabel titleLabel = new TitleLabel();
538 * 親テーブルの選択シーケンスの変更に反応する
541 private class TitleLabel extends JLabel implements ListSelectionListener {
542 private static final String TITLE = "Tracks";
543 public TitleLabel() { setText(TITLE); }
545 public void valueChanged(ListSelectionEvent event) {
546 if( event.getValueIsAdjusting() ) return;
547 SequenceTrackListTableModel oldModel = getModel();
548 SequenceTrackListTableModel newModel = oldModel.getParent().getSelectedSequenceModel();
549 if( oldModel == newModel ) return;
551 // MIDIチャンネル選択中のときはキャンセルする
555 ListSelectionModel sm = oldModel.getParent().sequenceListSelectionModel;
556 if( ! sm.isSelectionEmpty() ) {
557 int index = sm.getMinSelectionIndex();
558 if( index >= 0 ) text = String.format(text+" - MIDI file #%d", index);
561 if( newModel == null ) {
562 newModel = oldModel.getParent().emptyTrackListTableModel;
563 addTrackAction.setEnabled(false);
566 addTrackAction.setEnabled(true);
568 oldModel.getSelectionModel().removeListSelectionListener(trackSelectionListener);
570 setSelectionModel(newModel.getSelectionModel());
571 newModel.getSelectionModel().addListSelectionListener(trackSelectionListener);
572 trackSelectionListener.valueChanged(null);
578 ListSelectionListener trackSelectionListener = new ListSelectionListener() {
580 public void valueChanged(ListSelectionEvent e) {
581 if( e != null && e.getValueIsAdjusting() ) return;
582 ListSelectionModel tlsm = getModel().getSelectionModel();
583 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
584 eventListTable.titleLabel.update(tlsm, getModel());
590 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
592 * シーケンサーからこのモデルが外された場合がこれに該当します。
596 public void tableChanged(TableModelEvent e) {
597 super.tableChanged(e);
601 * このトラックリストテーブルが編集モードになっていたら解除します。
603 private void cancelCellEditing() {
604 TableCellEditor currentCellEditor = getCellEditor();
605 if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
610 Action addTrackAction = new AbstractAction("New") {
612 String tooltip = "Append new track - 新しいトラックの追加";
613 putValue(Action.SHORT_DESCRIPTION, tooltip);
617 public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
622 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
623 public static final String CONFIRM_MESSAGE =
624 "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
626 putValue(Action.SHORT_DESCRIPTION, "Delete selected track - 選択したトラックを削除");
630 public void actionPerformed(ActionEvent e) {
631 if( confirm(CONFIRM_MESSAGE) ) getModel().deleteSelectedTracks();
637 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
639 public class EventListTable extends JTable {
641 * 新しいイベントリストテーブルを構築します。
642 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
643 * トラックを切り替えたいときは {@link #setModel(TableModel)}
644 * でデータモデルを異なるトラックのものに切り替えます。
647 * @param model トラック(イベントリスト)データモデル
649 public EventListTable(TrackEventListTableModel model) {
650 super(model, null, model.getSelectionModel());
653 eventCellEditor = new MidiEventCellEditor();
654 setAutoCreateColumnsFromModel(false);
656 eventSelectionListener = new EventSelectionListener();
657 titleLabel = new TitleLabel();
659 TableColumnModel cm = getColumnModel();
660 Arrays.stream(TrackEventListTableModel.Column.values()).forEach(c->
661 cm.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth)
665 * このテーブルビューが表示するデータを提供する
666 * トラック(イベントリスト)データモデルを返します。
667 * @return トラック(イベントリスト)データモデル
670 public TrackEventListTableModel getModel() {
671 return (TrackEventListTableModel) super.getModel();
676 TitleLabel titleLabel;
678 * 親テーブルの選択トラックの変更に反応する
681 private class TitleLabel extends JLabel {
682 private static final String TITLE = "MIDI Events";
683 public TitleLabel() { super(TITLE); }
684 public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
686 TrackEventListTableModel oldTrackModel = getModel();
687 int index = tlsm.getMinSelectionIndex();
689 text = String.format(TITLE+" - track #%d", index);
692 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
693 if( oldTrackModel == newTrackModel )
695 if( newTrackModel == null ) {
696 newTrackModel = getModel().getParent().getParent().emptyEventListTableModel;
697 queryJumpEventAction.setEnabled(false);
698 queryAddEventAction.setEnabled(false);
700 queryPasteEventAction.setEnabled(false);
701 copyEventAction.setEnabled(false);
702 deleteEventAction.setEnabled(false);
703 cutEventAction.setEnabled(false);
706 queryJumpEventAction.setEnabled(true);
707 queryAddEventAction.setEnabled(true);
709 oldTrackModel.getSelectionModel().removeListSelectionListener(eventSelectionListener);
710 setModel(newTrackModel);
711 setSelectionModel(newTrackModel.getSelectionModel());
712 newTrackModel.getSelectionModel().addListSelectionListener(eventSelectionListener);
719 private EventSelectionListener eventSelectionListener;
723 private class EventSelectionListener implements ListSelectionListener {
724 public EventSelectionListener() {
725 getModel().getSelectionModel().addListSelectionListener(this);
728 public void valueChanged(ListSelectionEvent e) {
729 if( e.getValueIsAdjusting() )
731 if( getSelectionModel().isSelectionEmpty() ) {
732 queryPasteEventAction.setEnabled(false);
733 copyEventAction.setEnabled(false);
734 deleteEventAction.setEnabled(false);
735 cutEventAction.setEnabled(false);
738 copyEventAction.setEnabled(true);
739 deleteEventAction.setEnabled(true);
740 cutEventAction.setEnabled(true);
741 TrackEventListTableModel trackModel = getModel();
742 int minIndex = getSelectionModel().getMinSelectionIndex();
743 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
744 if( midiEvent != null ) {
745 MidiMessage msg = midiEvent.getMessage();
746 if( msg instanceof ShortMessage ) {
747 ShortMessage sm = (ShortMessage)msg;
748 int cmd = sm.getCommand();
749 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
751 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
752 int ch = sm.getChannel();
753 int note = sm.getData1();
754 int vel = sm.getData2();
755 outMidiChannels[ch].noteOn(note, vel);
756 outMidiChannels[ch].noteOff(note, vel);
760 if( pairNoteOnOffModel.isSelected() ) {
761 int maxIndex = getSelectionModel().getMaxSelectionIndex();
763 for( int i=minIndex; i<=maxIndex; i++ ) {
764 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
765 partnerIndex = trackModel.getIndexOfPartnerFor(i);
766 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
767 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
774 * Pair noteON/OFF トグルボタンモデル
776 private JToggleButton.ToggleButtonModel
777 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
779 addItemListener(e->eventDialog.midiMessageForm.durationForm.setEnabled(isSelected()));
783 private class EventEditContext {
787 private TrackEventListTableModel trackModel;
791 private TickPositionModel tickPositionModel = new TickPositionModel();
795 private MidiEvent selectedMidiEvent = null;
799 private int selectedIndex = -1;
803 private long currentTick = 0;
805 * 上書きして削除対象にする変更前イベント(null可)
807 private MidiEvent[] midiEventsToBeOverwritten;
809 * 選択したイベントを入力ダイアログなどに反映します。
810 * @param model 対象データモデル
812 private void setSelectedEvent(TrackEventListTableModel trackModel) {
813 this.trackModel = trackModel;
814 SequenceTrackListTableModel sequenceTableModel = trackModel.getParent();
815 int ppq = sequenceTableModel.getSequence().getResolution();
816 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
817 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
819 selectedIndex = trackModel.getSelectionModel().getMinSelectionIndex();
820 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
821 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
822 tickPositionModel.setTickPosition(currentTick);
824 public void setupForEdit(TrackEventListTableModel trackModel) {
825 MidiEvent partnerEvent = null;
826 eventDialog.midiMessageForm.setMessage(
827 selectedMidiEvent.getMessage(),
828 trackModel.getParent().charset
830 if( eventDialog.midiMessageForm.isNote() ) {
831 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
832 if( partnerIndex < 0 ) {
833 eventDialog.midiMessageForm.durationForm.setDuration(0);
836 partnerEvent = trackModel.getMidiEvent(partnerIndex);
837 long partnerTick = partnerEvent.getTick();
838 long duration = currentTick > partnerTick ?
839 currentTick - partnerTick : partnerTick - currentTick ;
840 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
843 if(partnerEvent == null)
844 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
846 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
848 private Action jumpEventAction = new AbstractAction() {
849 { putValue(NAME,"Jump"); }
850 public void actionPerformed(ActionEvent e) {
851 long tick = tickPositionModel.getTickPosition();
852 scrollToEventAt(tick);
853 eventDialog.setVisible(false);
857 private Action pasteEventAction = new AbstractAction() {
858 { putValue(NAME,"Paste"); }
859 public void actionPerformed(ActionEvent e) {
860 long tick = tickPositionModel.getTickPosition();
861 clipBoard.paste(trackModel, tick);
862 scrollToEventAt(tick);
863 // ペーストされたので変更フラグを立てる(曲の長さが変わるが、それも自動的にプレイリストに通知される)
864 SequenceTrackListTableModel seqModel = trackModel.getParent();
865 seqModel.setModified(true);
866 eventDialog.setVisible(false);
870 private boolean applyEvent() {
871 long tick = tickPositionModel.getTickPosition();
872 MidiMessageForm form = eventDialog.midiMessageForm;
873 SequenceTrackListTableModel seqModel = trackModel.getParent();
874 MidiMessage msg = form.getMessage(seqModel.charset);
878 MidiEvent newMidiEvent = new MidiEvent(msg, tick);
879 if( midiEventsToBeOverwritten != null ) {
880 // 上書き消去するための選択済イベントがあった場合
881 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
883 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
884 System.out.println("addMidiEvent failure");
887 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
888 ShortMessage sm = form.createPartnerMessage();
890 scrollToEventAt( tick );
892 int duration = form.durationForm.getDuration();
893 if( form.isNote(false) ) {
894 duration = -duration;
896 long partnerTick = tick + (long)duration;
897 if( partnerTick < 0L ) partnerTick = 0L;
898 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
899 if( ! trackModel.addMidiEvent(partner) ) {
900 System.out.println("addMidiEvent failure (note on/off partner message)");
902 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
905 seqModel.setModified(true);
906 eventDialog.setVisible(false);
910 private EventEditContext editContext = new EventEditContext();
912 * 指定のTick位置へジャンプするアクション
914 Action queryJumpEventAction = new AbstractAction() {
916 putValue(NAME,"Jump to ...");
919 public void actionPerformed(ActionEvent e) {
920 editContext.setSelectedEvent(getModel());
921 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
927 Action queryAddEventAction = new AbstractAction() {
929 putValue(NAME,"New");
932 public void actionPerformed(ActionEvent e) {
933 TrackEventListTableModel model = getModel();
934 editContext.setSelectedEvent(model);
935 editContext.midiEventsToBeOverwritten = null;
936 eventDialog.openEventForm("New MIDI event", eventCellEditor.applyEventAction, model.getChannel());
940 * MIDIイベントのコピー&ペーストを行うためのクリップボード
942 private class LocalClipBoard {
943 private MidiEvent copiedEventsToPaste[];
944 private int copiedEventsPPQ = 0;
945 public void copy(TrackEventListTableModel model, boolean withRemove) {
946 copiedEventsToPaste = model.getSelectedMidiEvents();
947 copiedEventsPPQ = model.getParent().getSequence().getResolution();
948 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
949 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
950 queryPasteEventAction.setEnabled(en);
952 public void cut(TrackEventListTableModel model) {copy(model,true);}
953 public void copy(TrackEventListTableModel model){copy(model,false);}
954 public void paste(TrackEventListTableModel model, long tick) {
955 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
958 private LocalClipBoard clipBoard = new LocalClipBoard();
960 * 指定のTick位置へ貼り付けるアクション
962 Action queryPasteEventAction = new AbstractAction() {
964 putValue(NAME,"Paste to ...");
967 public void actionPerformed(ActionEvent e) {
968 editContext.setSelectedEvent(getModel());
969 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
975 public Action cutEventAction = new AbstractAction("Cut") {
976 private static final String CONFIRM_MESSAGE =
977 "Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?";
978 { setEnabled(false); }
980 public void actionPerformed(ActionEvent e) {
981 if( confirm(CONFIRM_MESSAGE) ) clipBoard.cut(getModel());
987 public Action copyEventAction = new AbstractAction("Copy") {
988 { setEnabled(false); }
990 public void actionPerformed(ActionEvent e) { clipBoard.copy(getModel()); }
995 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
996 private static final String CONFIRM_MESSAGE =
997 "Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?";
998 { setEnabled(false); }
1000 public void actionPerformed(ActionEvent e) {
1001 if( confirm(CONFIRM_MESSAGE)) getModel().removeSelectedMidiEvents();
1007 private MidiEventCellEditor eventCellEditor;
1011 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1013 * MIDIイベントセルエディタを構築します。
1015 public MidiEventCellEditor() {
1016 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
1017 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1018 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1019 getColumnModel().getColumn(index).setCellEditor(this);
1022 * セルをダブルクリックしないと編集できないようにします。
1023 * @param e イベント(マウスイベント)
1024 * @return 編集可能になったらtrue
1027 public boolean isCellEditable(EventObject e) {
1028 return (e instanceof MouseEvent) ?
1029 ((MouseEvent)e).getClickCount() == 2 : super.isCellEditable(e);
1032 public Object getCellEditorValue() { return null; }
1034 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1036 private ComponentListener dialogComponentListener = new ComponentAdapter() {
1038 public void componentHidden(ComponentEvent e) {
1039 fireEditingCanceled();
1041 eventDialog.removeComponentListener(this);
1047 private Action editEventAction = new AbstractAction() {
1048 public void actionPerformed(ActionEvent e) {
1049 TrackEventListTableModel model = getModel();
1050 editContext.setSelectedEvent(model);
1051 if( editContext.selectedMidiEvent == null )
1053 editContext.setupForEdit(model);
1054 eventDialog.addComponentListener(dialogComponentListener);
1055 eventDialog.openEventForm("Change MIDI event", applyEventAction);
1061 private JButton editEventButton = new JButton(editEventAction){{
1062 setHorizontalAlignment(JButton.LEFT);
1065 public Component getTableCellEditorComponent(
1066 JTable table, Object value, boolean isSelected, int row, int column
1068 editEventButton.setText(value.toString());
1069 return editEventButton;
1072 * 入力したイベントを反映するアクション
1074 private Action applyEventAction = new AbstractAction() {
1075 { putValue(NAME,"OK"); }
1076 public void actionPerformed(ActionEvent e) {
1077 if( editContext.applyEvent() ) fireEditingStopped();
1082 * スクロール可能なMIDIイベントテーブルビュー
1084 private JScrollPane scrollPane = new JScrollPane(this);
1086 * 指定の MIDI tick のイベントへスクロールします。
1087 * @param tick MIDI tick
1089 public void scrollToEventAt(long tick) {
1090 int index = getModel().tickToIndex(tick);
1091 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1092 getSelectionModel().setSelectionInterval(index, index);
1097 * 新しい {@link MidiSequenceEditorDialog} を構築します。
1098 * @param playlistTableModel このエディタが参照するプレイリストモデル
1099 * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1100 * @param midiDeviceDialogOpenAction MIDIデバイスダイアログを開くアクション
1102 public MidiSequenceEditorDialog(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice, Action midiDeviceDialogOpenAction) {
1103 this.outputMidiDevice = outputMidiDevice;
1104 this.midiDeviceDialogOpenAction = midiDeviceDialogOpenAction;
1105 sequenceListTable = new SequenceListTable(playlistTableModel);
1106 trackListTable = new TrackListTable(playlistTableModel.emptyTrackListTableModel);
1107 eventListTable = new EventListTable(playlistTableModel.emptyEventListTableModel);
1108 newSequenceDialog = new NewSequenceDialog(playlistTableModel, outputMidiDevice);
1109 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1110 setBounds( 150, 200, 900, 500 );
1111 setLayout(new FlowLayout());
1112 setTransferHandler(transferHandler);
1115 JPanel playlistPanel = new JPanel() {{
1116 JPanel playlistOperationPanel = new JPanel() {{
1117 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1118 add(Box.createRigidArea(new Dimension(10, 0)));
1119 add(new JButton(newSequenceDialog.openAction) {
1120 { setMargin(ChordHelperApplet.ZERO_INSETS); }
1122 if( sequenceListTable.midiFileChooser != null ) {
1123 add( Box.createRigidArea(new Dimension(5, 0)) );
1124 add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {
1125 { setMargin(ChordHelperApplet.ZERO_INSETS); }
1128 if(sequenceListTable.base64EncodeAction != null) {
1129 add(Box.createRigidArea(new Dimension(5, 0)));
1130 add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1132 add(Box.createRigidArea(new Dimension(5, 0)));
1133 add(new JButton(playlistTableModel.getMoveToTopAction()) {
1134 { setMargin(ChordHelperApplet.ZERO_INSETS); }
1136 add(Box.createRigidArea(new Dimension(5, 0)));
1137 add(new JButton(playlistTableModel.getMoveToBottomAction()) {
1138 { setMargin(ChordHelperApplet.ZERO_INSETS); }
1140 if( sequenceListTable.midiFileChooser != null ) {
1141 add(Box.createRigidArea(new Dimension(5, 0)));
1142 add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {
1143 { setMargin(ChordHelperApplet.ZERO_INSETS); }
1146 add( Box.createRigidArea(new Dimension(5, 0)) );
1147 add(new JButton(sequenceListTable.deleteSequenceAction) {
1148 { setMargin(ChordHelperApplet.ZERO_INSETS); }
1150 add( Box.createRigidArea(new Dimension(5, 0)) );
1151 add(new SequencerSpeedSlider(playlistTableModel.getSequencerModel().speedSliderModel));
1152 add( Box.createRigidArea(new Dimension(5, 0)) );
1154 setBorder(new EtchedBorder());
1155 MidiSequencerModel sequencerModel = getPlaylistModel().getSequencerModel();
1156 add(new JLabel("Sync Master"));
1157 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1158 add(new JLabel("Slave"));
1159 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1162 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1163 add(new JScrollPane(sequenceListTable));
1164 add(Box.createRigidArea(new Dimension(0, 10)));
1165 add(playlistOperationPanel);
1166 add(Box.createRigidArea(new Dimension(0, 10)));
1168 JPanel trackListPanel = new JPanel() {{
1169 JPanel trackListOperationPanel = new JPanel() {{
1170 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1171 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1173 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1174 add(trackListTable.titleLabel);
1175 add(Box.createRigidArea(new Dimension(0, 5)));
1176 add(new JScrollPane(trackListTable));
1177 add(Box.createRigidArea(new Dimension(0, 5)));
1178 add(trackListOperationPanel);
1180 JPanel eventListPanel = new JPanel() {{
1181 JPanel eventListOperationPanel = new JPanel() {{
1182 add(new JCheckBox("Pair NoteON/OFF") {{
1183 setModel(eventListTable.pairNoteOnOffModel);
1184 setToolTipText("NoteON/OFFをペアで同時選択する");
1186 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1187 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1188 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1189 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1190 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1191 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ChordHelperApplet.ZERO_INSETS); }});
1193 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1194 add(eventListTable.titleLabel);
1195 add(eventListTable.scrollPane);
1196 add(eventListOperationPanel);
1198 Container cp = getContentPane();
1199 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1200 cp.add(Box.createVerticalStrut(2));
1202 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1203 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1204 setDividerLocation(300);
1207 setDividerLocation(160);