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.Insets;
8 import java.awt.datatransfer.DataFlavor;
9 import java.awt.event.ActionEvent;
10 import java.awt.event.ComponentAdapter;
11 import java.awt.event.ComponentEvent;
12 import java.awt.event.ComponentListener;
13 import java.awt.event.MouseEvent;
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.List;
25 import javax.sound.midi.InvalidMidiDataException;
26 import javax.sound.midi.MidiChannel;
27 import javax.sound.midi.MidiEvent;
28 import javax.sound.midi.MidiMessage;
29 import javax.sound.midi.Sequence;
30 import javax.sound.midi.Sequencer;
31 import javax.sound.midi.ShortMessage;
32 import javax.swing.AbstractAction;
33 import javax.swing.AbstractCellEditor;
34 import javax.swing.Action;
35 import javax.swing.Box;
36 import javax.swing.BoxLayout;
37 import javax.swing.DefaultCellEditor;
38 import javax.swing.Icon;
39 import javax.swing.JButton;
40 import javax.swing.JCheckBox;
41 import javax.swing.JComboBox;
42 import javax.swing.JDialog;
43 import javax.swing.JFileChooser;
44 import javax.swing.JLabel;
45 import javax.swing.JOptionPane;
46 import javax.swing.JPanel;
47 import javax.swing.JScrollPane;
48 import javax.swing.JSplitPane;
49 import javax.swing.JTable;
50 import javax.swing.JToggleButton;
51 import javax.swing.ListSelectionModel;
52 import javax.swing.TransferHandler;
53 import javax.swing.border.EtchedBorder;
54 import javax.swing.event.ListSelectionEvent;
55 import javax.swing.event.ListSelectionListener;
56 import javax.swing.event.TableModelEvent;
57 import javax.swing.filechooser.FileNameExtensionFilter;
58 import javax.swing.table.JTableHeader;
59 import javax.swing.table.TableCellEditor;
60 import javax.swing.table.TableCellRenderer;
61 import javax.swing.table.TableColumn;
62 import javax.swing.table.TableColumnModel;
63 import javax.swing.table.TableModel;
65 import camidion.chordhelper.ButtonIcon;
66 import camidion.chordhelper.ChordHelperApplet;
67 import camidion.chordhelper.mididevice.MidiSequencerModel;
68 import camidion.chordhelper.mididevice.VirtualMidiDevice;
69 import camidion.chordhelper.music.MIDISpec;
72 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
75 * Copyright (C) 2006-2016 Akiyoshi Kamide
76 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
78 public class MidiSequenceEditorDialog extends JDialog {
82 public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
84 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
85 putValue(Action.SHORT_DESCRIPTION, tooltip);
88 public void actionPerformed(ActionEvent e) {
89 if( isVisible() ) toFront(); else setVisible(true);
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 ;
117 * ドロップされた複数のMIDIファイルを読み込むハンドラー
119 public final TransferHandler transferHandler = new TransferHandler() {
121 public boolean canImport(TransferSupport support) {
122 return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
124 @SuppressWarnings("unchecked")
126 public boolean importData(TransferSupport support) {
128 loadAndPlay((List<File>)support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
130 } catch (Exception e) { showError(e); return false; }
135 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
136 * すでに再生されていた場合、このエディタダイアログを表示します。
138 * @param fileList 読み込むMIDIファイルのリスト
139 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
140 * @see #loadAndPlay(File)
142 public void loadAndPlay(List<File> fileList) {
143 int indexOfAddedTop = -1;
144 PlaylistTableModel playlist = sequenceListTable.getModel();
146 indexOfAddedTop = playlist.addSequences(fileList);
147 } catch(IOException|InvalidMidiDataException e) {
149 } catch(AccessControlException e) {
152 MidiSequencerModel sequencerModel = playlist.getSequencerModel();
153 if( sequencerModel.getSequencer().isRunning() ) {
154 String command = (String)openAction.getValue(Action.NAME);
155 openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
158 if( indexOfAddedTop >= 0 ) {
160 playlist.loadToSequencer(indexOfAddedTop);
161 } catch (InvalidMidiDataException e) { showError(e); return; }
162 sequencerModel.start();
166 * 1件のMIDIファイルを読み込み、再生されていなかったら再生します。
167 * すでに再生されていた場合、このエディタダイアログを表示します。
169 * @param file 読み込むMIDIファイル
170 * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
171 * @see #loadAndPlay(List) loadAndPlay(List<File>)
173 public void loadAndPlay(File file) throws InvalidMidiDataException {
174 loadAndPlay(Arrays.asList(file));
177 private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
178 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
180 * 新しいMIDIシーケンスを生成するダイアログ
182 public NewSequenceDialog newSequenceDialog;
186 public Base64Dialog base64Dialog = new Base64Dialog(this);
188 * プレイリストビュー(シーケンスリスト)
190 public SequenceListTable sequenceListTable;
192 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
194 private TrackListTable trackListTable;
196 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
198 private EventListTable eventListTable;
200 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
202 public MidiEventDialog eventDialog = new MidiEventDialog();
203 private VirtualMidiDevice outputMidiDevice;
205 * プレイリストビュー(シーケンスリスト)
207 public class SequenceListTable extends JTable {
209 * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
211 private MidiFileChooser midiFileChooser;
213 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
215 private Action base64EncodeAction;
218 * @param model プレイリストデータモデル
220 public SequenceListTable(PlaylistTableModel model) {
221 super(model, null, model.sequenceListSelectionModel);
223 midiFileChooser = new MidiFileChooser();
225 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
226 // アプレットの場合、Webクライアントマシンのローカルファイルには
227 // アクセスできないので、ファイル選択ダイアログは使用不可。
228 midiFileChooser = null;
231 new PlayButtonCellEditor();
232 new PositionCellEditor();
235 int column = PlaylistTableModel.Column.CHARSET.ordinal();
236 TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
237 Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
238 for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
240 getColumnModel().getColumn(column).setCellEditor(ce);
241 setAutoCreateColumnsFromModel(false);
243 // Base64エンコードアクションの生成
244 base64EncodeAction = new AbstractAction("Base64") {
246 String tooltip = "Base64 text conversion - Base64テキスト変換";
247 putValue(Action.SHORT_DESCRIPTION, tooltip);
250 public void actionPerformed(ActionEvent e) {
251 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
253 String filename = null;
255 data = mstm.getMIDIdata();
256 filename = mstm.getFilename();
258 base64Dialog.setMIDIData(data, filename);
259 base64Dialog.setVisible(true);
262 TableColumnModel colModel = getColumnModel();
263 for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
264 TableColumn tc = colModel.getColumn(c.ordinal());
265 tc.setPreferredWidth(c.preferredWidth);
266 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
269 private TableColumn lengthColumn;
271 public void tableChanged(TableModelEvent event) {
272 super.tableChanged(event);
275 if( lengthColumn != null ) {
276 int sec = getModel().getTotalTimeInSeconds();
277 String title = PlaylistTableModel.Column.LENGTH.title;
278 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
279 lengthColumn.setHeaderValue(title);
282 // シーケンス削除時など、合計シーケンス長が変わっても
283 // 列モデルからではヘッダタイトルが再描画されないことがある。
284 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
285 JTableHeader th = getTableHeader();
286 if( th != null ) th.repaint();
289 * 時間位置表示セルエディタ(ダブルクリック専用)
291 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
292 public PositionCellEditor() {
293 int column = PlaylistTableModel.Column.POSITION.ordinal();
294 TableColumn tc = getColumnModel().getColumn(column);
295 tc.setCellEditor(this);
298 * セルをダブルクリックしたときだけ編集モードに入るようにします。
299 * @param e イベント(マウスイベント)
300 * @return 編集可能になったらtrue
303 public boolean isCellEditable(EventObject e) {
304 // マウスイベント以外のイベントでは編集不可
305 if( ! (e instanceof MouseEvent) ) return false;
306 return ((MouseEvent)e).getClickCount() == 2;
309 public Object getCellEditorValue() { return null; }
311 * 編集モード時のコンポーネントを返すタイミングで
312 * そのシーケンスをシーケンサーにロードしたあと、
317 public Component getTableCellEditorComponent(
318 JTable table, Object value, boolean isSelected, int row, int column
321 getModel().loadToSequencer(row);
322 } catch (InvalidMidiDataException ex) { showError(ex); }
323 fireEditingStopped();
330 private class PlayButtonCellEditor extends AbstractCellEditor
331 implements TableCellEditor, TableCellRenderer
333 private JToggleButton playButton = new JToggleButton(
334 getModel().getSequencerModel().getStartStopAction()
336 { setMargin(ZERO_INSETS); }
338 public PlayButtonCellEditor() {
339 int column = PlaylistTableModel.Column.PLAY.ordinal();
340 TableColumn tc = getColumnModel().getColumn(column);
341 tc.setCellRenderer(this);
342 tc.setCellEditor(this);
347 * <p>この実装では、クリックしたセルのシーケンスが
349 * trueを返してプレイボタンを押せるようにします。
350 * そうでない場合はプレイボタンのないセルなので、
351 * ダブルクリックされたときだけtrueを返します。
355 public boolean isCellEditable(EventObject e) {
356 // マウスイベント以外はデフォルトメソッドにお任せ
357 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
358 fireEditingStopped();
359 MouseEvent me = (MouseEvent)e;
362 int row = rowAtPoint(me.getPoint());
363 if( row < 0 ) return false;
364 PlaylistTableModel model = getModel();
365 if( row >= model.getRowCount() ) return false;
367 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
368 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
369 return model.getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
372 public Object getCellEditorValue() { return null; }
376 * <p>この実装では、行の表すシーケンスがシーケンサーにロードされている場合にプレイボタンを返します。
377 * そうでない場合は、そのシーケンスをシーケンサーにロードしてnullを返します。
381 public Component getTableCellEditorComponent(
382 JTable table, Object value, boolean isSelected, int row, int column
384 fireEditingStopped();
385 PlaylistTableModel model = getModel();
386 if( model.getSequenceModelList().get(row).isOnSequencer() ) return playButton;
388 model.loadToSequencer(row);
389 } catch (InvalidMidiDataException ex) { showError(ex); }
393 public Component getTableCellRendererComponent(
394 JTable table, Object value, boolean isSelected,
395 boolean hasFocus, int row, int column
397 PlaylistTableModel model = getModel();
398 if(model.getSequenceModelList().get(row).isOnSequencer()) return playButton;
399 Class<?> cc = model.getColumnClass(column);
400 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
401 return defaultRenderer.getTableCellRendererComponent(
402 table, value, isSelected, hasFocus, row, column
407 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
412 public PlaylistTableModel getModel() {
413 return (PlaylistTableModel)super.getModel();
418 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
419 "Delete", MidiSequenceEditorDialog.deleteIcon,
420 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
423 public void actionPerformed(ActionEvent event) {
424 PlaylistTableModel model = getModel();
425 if( midiFileChooser != null ) {
426 if( model.getSelectedSequenceModel().isModified() ) {
428 "Selected MIDI sequence not saved - delete it ?\n" +
429 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
430 if( ! confirm(message) ) return;
434 model.removeSelectedSequence();
435 } catch (InvalidMidiDataException ex) {
441 * ファイル選択ダイアログ(アプレットでは使用不可)
443 private class MidiFileChooser extends JFileChooser {
445 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
450 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
452 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
455 public void actionPerformed(ActionEvent event) {
456 PlaylistTableModel playlistModel = getModel();
457 SequenceTrackListTableModel sequenceModel = playlistModel.getSelectedSequenceModel();
458 String fn = sequenceModel.getFilename();
459 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
460 if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
461 File f = getSelectedFile();
464 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
466 try ( FileOutputStream out = new FileOutputStream(f) ) {
467 out.write(sequenceModel.getMIDIdata());
468 sequenceModel.setModified(false);
469 playlistModel.fireSequenceModified(sequenceModel, false);
471 catch( IOException ex ) { showError(ex); }
477 public Action openMidiFileAction = new AbstractAction("Open") {
478 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
480 public void actionPerformed(ActionEvent event) {
481 if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
483 loadAndPlay(getSelectedFile());
484 } catch (InvalidMidiDataException ex) { showError(ex); }
491 * シーケンス(トラックリスト)テーブルビュー
493 public class TrackListTable extends JTable {
495 * トラックリストテーブルビューを構築します。
496 * @param model シーケンス(トラックリスト)データモデル
498 public TrackListTable(SequenceTrackListTableModel model) {
499 super(model, null, model.getSelectionModel());
501 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
502 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
503 TableColumn tc = getColumnModel().getColumn(colIndex);
504 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
506 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
509 setAutoCreateColumnsFromModel(false);
511 titleLabel = new TitleLabel();
512 model.getParent().sequenceListSelectionModel.addListSelectionListener(titleLabel);
513 TableColumnModel colModel = getColumnModel();
514 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
515 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
518 * このテーブルビューが表示するデータを提供する
519 * シーケンス(トラックリスト)データモデルを返します。
520 * @return シーケンス(トラックリスト)データモデル
523 public SequenceTrackListTableModel getModel() {
524 return (SequenceTrackListTableModel) super.getModel();
529 TitleLabel titleLabel;
531 * 親テーブルの選択シーケンスの変更に反応する
534 private class TitleLabel extends JLabel implements ListSelectionListener {
535 private static final String TITLE = "Tracks";
536 public TitleLabel() { setText(TITLE); }
538 public void valueChanged(ListSelectionEvent event) {
539 if( event.getValueIsAdjusting() ) return;
540 SequenceTrackListTableModel oldModel = getModel();
541 SequenceTrackListTableModel newModel = oldModel.getParent().getSelectedSequenceModel();
542 if( oldModel == newModel ) return;
544 // MIDIチャンネル選択中のときはキャンセルする
547 int index = oldModel.getParent().sequenceListSelectionModel.getMinSelectionIndex();
549 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
551 if( newModel == null ) {
552 newModel = oldModel.getParent().emptyTrackListTableModel;
553 addTrackAction.setEnabled(false);
556 addTrackAction.setEnabled(true);
558 oldModel.getSelectionModel().removeListSelectionListener(trackSelectionListener);
560 setSelectionModel(newModel.getSelectionModel());
561 newModel.getSelectionModel().addListSelectionListener(trackSelectionListener);
562 trackSelectionListener.valueChanged(null);
568 ListSelectionListener trackSelectionListener = new ListSelectionListener() {
570 public void valueChanged(ListSelectionEvent e) {
571 if( e != null && e.getValueIsAdjusting() ) return;
572 ListSelectionModel tlsm = getModel().getSelectionModel();
573 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
574 eventListTable.titleLabel.update(tlsm, getModel());
580 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
582 * シーケンサーからこのモデルが外された場合がこれに該当します。
586 public void tableChanged(TableModelEvent e) {
587 super.tableChanged(e);
591 * このトラックリストテーブルが編集モードになっていたら解除します。
593 private void cancelCellEditing() {
594 TableCellEditor currentCellEditor = getCellEditor();
595 if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
600 Action addTrackAction = new AbstractAction("New") {
602 String tooltip = "Append new track - 新しいトラックの追加";
603 putValue(Action.SHORT_DESCRIPTION, tooltip);
607 public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
612 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
614 String tooltip = "Delete selected track - 選択したトラックを削除";
615 putValue(Action.SHORT_DESCRIPTION, tooltip);
619 public void actionPerformed(ActionEvent e) {
620 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
621 if( confirm(message) ) getModel().deleteSelectedTracks();
627 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
629 public class EventListTable extends JTable {
631 * 新しいイベントリストテーブルを構築します。
632 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
633 * トラックを切り替えたいときは {@link #setModel(TableModel)}
634 * でデータモデルを異なるトラックのものに切り替えます。
637 * @param model トラック(イベントリスト)データモデル
639 public EventListTable(TrackEventListTableModel model) {
640 super(model, null, model.getSelectionModel());
643 eventCellEditor = new MidiEventCellEditor();
644 setAutoCreateColumnsFromModel(false);
646 eventSelectionListener = new EventSelectionListener();
647 titleLabel = new TitleLabel();
649 TableColumnModel colModel = getColumnModel();
650 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
651 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
654 * このテーブルビューが表示するデータを提供する
655 * トラック(イベントリスト)データモデルを返します。
656 * @return トラック(イベントリスト)データモデル
659 public TrackEventListTableModel getModel() {
660 return (TrackEventListTableModel) super.getModel();
665 TitleLabel titleLabel;
667 * 親テーブルの選択トラックの変更に反応する
670 private class TitleLabel extends JLabel {
671 private static final String TITLE = "MIDI Events";
672 public TitleLabel() { super(TITLE); }
673 public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
675 TrackEventListTableModel oldTrackModel = getModel();
676 int index = tlsm.getMinSelectionIndex();
678 text = String.format(TITLE+" - track No.%d", index);
681 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
682 if( oldTrackModel == newTrackModel )
684 if( newTrackModel == null ) {
685 newTrackModel = getModel().getParent().getParent().emptyEventListTableModel;
686 queryJumpEventAction.setEnabled(false);
687 queryAddEventAction.setEnabled(false);
689 queryPasteEventAction.setEnabled(false);
690 copyEventAction.setEnabled(false);
691 deleteEventAction.setEnabled(false);
692 cutEventAction.setEnabled(false);
695 queryJumpEventAction.setEnabled(true);
696 queryAddEventAction.setEnabled(true);
698 oldTrackModel.getSelectionModel().removeListSelectionListener(eventSelectionListener);
699 setModel(newTrackModel);
700 setSelectionModel(newTrackModel.getSelectionModel());
701 newTrackModel.getSelectionModel().addListSelectionListener(eventSelectionListener);
708 private EventSelectionListener eventSelectionListener;
712 private class EventSelectionListener implements ListSelectionListener {
713 public EventSelectionListener() {
714 getModel().getSelectionModel().addListSelectionListener(this);
717 public void valueChanged(ListSelectionEvent e) {
718 if( e.getValueIsAdjusting() )
720 if( getSelectionModel().isSelectionEmpty() ) {
721 queryPasteEventAction.setEnabled(false);
722 copyEventAction.setEnabled(false);
723 deleteEventAction.setEnabled(false);
724 cutEventAction.setEnabled(false);
727 copyEventAction.setEnabled(true);
728 deleteEventAction.setEnabled(true);
729 cutEventAction.setEnabled(true);
730 TrackEventListTableModel trackModel = getModel();
731 int minIndex = getSelectionModel().getMinSelectionIndex();
732 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
733 if( midiEvent != null ) {
734 MidiMessage msg = midiEvent.getMessage();
735 if( msg instanceof ShortMessage ) {
736 ShortMessage sm = (ShortMessage)msg;
737 int cmd = sm.getCommand();
738 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
740 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
741 int ch = sm.getChannel();
742 int note = sm.getData1();
743 int vel = sm.getData2();
744 outMidiChannels[ch].noteOn(note, vel);
745 outMidiChannels[ch].noteOff(note, vel);
749 if( pairNoteOnOffModel.isSelected() ) {
750 int maxIndex = getSelectionModel().getMaxSelectionIndex();
752 for( int i=minIndex; i<=maxIndex; i++ ) {
753 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
754 partnerIndex = trackModel.getIndexOfPartnerFor(i);
755 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
756 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
763 * Pair noteON/OFF トグルボタンモデル
765 private JToggleButton.ToggleButtonModel
766 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
768 addItemListener(e->eventDialog.midiMessageForm.durationForm.setEnabled(isSelected()));
772 private class EventEditContext {
776 private TrackEventListTableModel trackModel;
780 private TickPositionModel tickPositionModel = new TickPositionModel();
784 private MidiEvent selectedMidiEvent = null;
788 private int selectedIndex = -1;
792 private long currentTick = 0;
794 * 上書きして削除対象にする変更前イベント(null可)
796 private MidiEvent[] midiEventsToBeOverwritten;
798 * 選択したイベントを入力ダイアログなどに反映します。
799 * @param model 対象データモデル
801 private void setSelectedEvent(TrackEventListTableModel trackModel) {
802 this.trackModel = trackModel;
803 SequenceTrackListTableModel sequenceTableModel = trackModel.getParent();
804 int ppq = sequenceTableModel.getSequence().getResolution();
805 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
806 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
808 selectedIndex = trackModel.getSelectionModel().getMinSelectionIndex();
809 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
810 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
811 tickPositionModel.setTickPosition(currentTick);
813 public void setupForEdit(TrackEventListTableModel trackModel) {
814 MidiEvent partnerEvent = null;
815 eventDialog.midiMessageForm.setMessage(
816 selectedMidiEvent.getMessage(),
817 trackModel.getParent().charset
819 if( eventDialog.midiMessageForm.isNote() ) {
820 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
821 if( partnerIndex < 0 ) {
822 eventDialog.midiMessageForm.durationForm.setDuration(0);
825 partnerEvent = trackModel.getMidiEvent(partnerIndex);
826 long partnerTick = partnerEvent.getTick();
827 long duration = currentTick > partnerTick ?
828 currentTick - partnerTick : partnerTick - currentTick ;
829 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
832 if(partnerEvent == null)
833 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
835 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
837 private Action jumpEventAction = new AbstractAction() {
838 { putValue(NAME,"Jump"); }
839 public void actionPerformed(ActionEvent e) {
840 long tick = tickPositionModel.getTickPosition();
841 scrollToEventAt(tick);
842 eventDialog.setVisible(false);
846 private Action pasteEventAction = new AbstractAction() {
847 { putValue(NAME,"Paste"); }
848 public void actionPerformed(ActionEvent e) {
849 long tick = tickPositionModel.getTickPosition();
850 clipBoard.paste(trackModel, tick);
851 scrollToEventAt(tick);
852 // ペーストで曲の長さが変わったことをプレイリストに通知
853 SequenceTrackListTableModel seqModel = trackModel.getParent();
854 seqModel.getParent().fireSequenceModified(seqModel, true);
855 eventDialog.setVisible(false);
859 private boolean applyEvent() {
860 long tick = tickPositionModel.getTickPosition();
861 MidiMessageForm form = eventDialog.midiMessageForm;
862 SequenceTrackListTableModel seqModel = trackModel.getParent();
863 MidiEvent newMidiEvent = new MidiEvent(form.getMessage(seqModel.charset), tick);
864 if( midiEventsToBeOverwritten != null ) {
865 // 上書き消去するための選択済イベントがあった場合
866 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
868 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
869 System.out.println("addMidiEvent failure");
872 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
873 ShortMessage sm = form.createPartnerMessage();
875 scrollToEventAt( tick );
877 int duration = form.durationForm.getDuration();
878 if( form.isNote(false) ) {
879 duration = -duration;
881 long partnerTick = tick + (long)duration;
882 if( partnerTick < 0L ) partnerTick = 0L;
883 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
884 if( ! trackModel.addMidiEvent(partner) ) {
885 System.out.println("addMidiEvent failure (note on/off partner message)");
887 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
890 seqModel.getParent().fireSequenceModified(seqModel, true);
891 eventDialog.setVisible(false);
895 private EventEditContext editContext = new EventEditContext();
897 * 指定のTick位置へジャンプするアクション
899 Action queryJumpEventAction = new AbstractAction() {
901 putValue(NAME,"Jump to ...");
904 public void actionPerformed(ActionEvent e) {
905 editContext.setSelectedEvent(getModel());
906 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
912 Action queryAddEventAction = new AbstractAction() {
914 putValue(NAME,"New");
917 public void actionPerformed(ActionEvent e) {
918 TrackEventListTableModel model = getModel();
919 editContext.setSelectedEvent(model);
920 editContext.midiEventsToBeOverwritten = null;
921 eventDialog.openEventForm(
923 eventCellEditor.applyEventAction,
929 * MIDIイベントのコピー&ペーストを行うためのクリップボード
931 private class LocalClipBoard {
932 private MidiEvent copiedEventsToPaste[];
933 private int copiedEventsPPQ = 0;
934 public void copy(TrackEventListTableModel model, boolean withRemove) {
935 copiedEventsToPaste = model.getSelectedMidiEvents();
936 copiedEventsPPQ = model.getParent().getSequence().getResolution();
937 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
938 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
939 queryPasteEventAction.setEnabled(en);
941 public void cut(TrackEventListTableModel model) {copy(model,true);}
942 public void copy(TrackEventListTableModel model){copy(model,false);}
943 public void paste(TrackEventListTableModel model, long tick) {
944 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
947 private LocalClipBoard clipBoard = new LocalClipBoard();
949 * 指定のTick位置へ貼り付けるアクション
951 Action queryPasteEventAction = new AbstractAction() {
953 putValue(NAME,"Paste to ...");
956 public void actionPerformed(ActionEvent e) {
957 editContext.setSelectedEvent(getModel());
958 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
964 public Action cutEventAction = new AbstractAction("Cut") {
969 public void actionPerformed(ActionEvent e) {
970 TrackEventListTableModel model = getModel();
971 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
973 clipBoard.cut(model);
979 public Action copyEventAction = new AbstractAction("Copy") {
984 public void actionPerformed(ActionEvent e) {
985 clipBoard.copy(getModel());
991 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
996 public void actionPerformed(ActionEvent e) {
997 TrackEventListTableModel model = getModel();
998 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1000 model.removeSelectedMidiEvents();
1006 private MidiEventCellEditor eventCellEditor;
1010 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1012 * MIDIイベントセルエディタを構築します。
1014 public MidiEventCellEditor() {
1015 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
1016 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1017 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1018 getColumnModel().getColumn(index).setCellEditor(this);
1021 * セルをダブルクリックしないと編集できないようにします。
1022 * @param e イベント(マウスイベント)
1023 * @return 編集可能になったらtrue
1026 public boolean isCellEditable(EventObject e) {
1027 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1028 return ((MouseEvent)e).getClickCount() == 2;
1031 public Object getCellEditorValue() { return null; }
1033 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1035 private ComponentListener dialogComponentListener = new ComponentAdapter() {
1037 public void componentHidden(ComponentEvent e) {
1038 fireEditingCanceled();
1040 eventDialog.removeComponentListener(this);
1046 private Action editEventAction = new AbstractAction() {
1047 public void actionPerformed(ActionEvent e) {
1048 TrackEventListTableModel model = getModel();
1049 editContext.setSelectedEvent(model);
1050 if( editContext.selectedMidiEvent == null )
1052 editContext.setupForEdit(model);
1053 eventDialog.addComponentListener(dialogComponentListener);
1054 eventDialog.openEventForm("Change MIDI event", applyEventAction);
1060 private JButton editEventButton = new JButton(editEventAction){{
1061 setHorizontalAlignment(JButton.LEFT);
1064 public Component getTableCellEditorComponent(
1065 JTable table, Object value, boolean isSelected, int row, int column
1067 editEventButton.setText(value.toString());
1068 return editEventButton;
1071 * 入力したイベントを反映するアクション
1073 private Action applyEventAction = new AbstractAction() {
1075 putValue(NAME,"OK");
1077 public void actionPerformed(ActionEvent e) {
1078 if( editContext.applyEvent() ) fireEditingStopped();
1083 * スクロール可能なMIDIイベントテーブルビュー
1085 private JScrollPane scrollPane = new JScrollPane(this);
1087 * 指定の MIDI tick のイベントへスクロールします。
1088 * @param tick MIDI tick
1090 public void scrollToEventAt(long tick) {
1091 int index = getModel().tickToIndex(tick);
1092 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1093 getSelectionModel().setSelectionInterval(index, index);
1098 * 新しい {@link MidiSequenceEditorDialog} を構築します。
1099 * @param playlistTableModel このエディタが参照するプレイリストモデル
1100 * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1102 public MidiSequenceEditorDialog(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice) {
1103 this.outputMidiDevice = outputMidiDevice;
1104 sequenceListTable = new SequenceListTable(playlistTableModel);
1105 trackListTable = new TrackListTable(
1106 new SequenceTrackListTableModel(playlistTableModel, null, null)
1108 eventListTable = new EventListTable(new TrackEventListTableModel(trackListTable.getModel(), null));
1109 newSequenceDialog = new NewSequenceDialog(playlistTableModel, outputMidiDevice);
1110 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1111 setBounds( 150, 200, 900, 500 );
1112 setLayout(new FlowLayout());
1113 setTransferHandler(transferHandler);
1116 JPanel playlistPanel = new JPanel() {{
1117 JPanel playlistOperationPanel = new JPanel() {{
1118 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1119 add(Box.createRigidArea(new Dimension(10, 0)));
1120 add(new JButton(newSequenceDialog.openAction) {{ setMargin(ZERO_INSETS); }});
1121 if( sequenceListTable.midiFileChooser != null ) {
1122 add( Box.createRigidArea(new Dimension(5, 0)) );
1123 add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {
1124 { setMargin(ZERO_INSETS); }
1127 if(sequenceListTable.base64EncodeAction != null) {
1128 add(Box.createRigidArea(new Dimension(5, 0)));
1129 add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1131 add(Box.createRigidArea(new Dimension(5, 0)));
1132 PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
1133 add(new JButton(playlistTableModel.getMoveToTopAction()) {{ setMargin(ZERO_INSETS); }});
1134 add(Box.createRigidArea(new Dimension(5, 0)));
1135 add(new JButton(playlistTableModel.getMoveToBottomAction()) {{ setMargin(ZERO_INSETS); }});
1136 if( sequenceListTable.midiFileChooser != null ) {
1137 add(Box.createRigidArea(new Dimension(5, 0)));
1138 add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {
1139 { setMargin(ZERO_INSETS); }
1142 add( Box.createRigidArea(new Dimension(5, 0)) );
1143 add(new JButton(sequenceListTable.deleteSequenceAction) {{ setMargin(ZERO_INSETS); }});
1144 add( Box.createRigidArea(new Dimension(5, 0)) );
1145 add(new SequencerSpeedSlider(playlistTableModel.getSequencerModel().speedSliderModel));
1146 add( Box.createRigidArea(new Dimension(5, 0)) );
1148 setBorder(new EtchedBorder());
1149 MidiSequencerModel sequencerModel = sequenceListTable.getModel().getSequencerModel();
1150 add(new JLabel("Sync Master"));
1151 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1152 add(new JLabel("Slave"));
1153 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1156 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1157 add(new JScrollPane(sequenceListTable));
1158 add(Box.createRigidArea(new Dimension(0, 10)));
1159 add(playlistOperationPanel);
1160 add(Box.createRigidArea(new Dimension(0, 10)));
1162 JPanel trackListPanel = new JPanel() {{
1163 JPanel trackListOperationPanel = new JPanel() {{
1164 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ZERO_INSETS); }});
1165 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
1167 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1168 add(trackListTable.titleLabel);
1169 add(Box.createRigidArea(new Dimension(0, 5)));
1170 add(new JScrollPane(trackListTable));
1171 add(Box.createRigidArea(new Dimension(0, 5)));
1172 add(trackListOperationPanel);
1174 JPanel eventListPanel = new JPanel() {{
1175 JPanel eventListOperationPanel = new JPanel() {{
1176 add(new JCheckBox("Pair NoteON/OFF") {{
1177 setModel(eventListTable.pairNoteOnOffModel);
1178 setToolTipText("NoteON/OFFをペアで同時選択する");
1180 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ZERO_INSETS); }});
1181 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ZERO_INSETS); }});
1182 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ZERO_INSETS); }});
1183 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ZERO_INSETS); }});
1184 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ZERO_INSETS); }});
1185 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ZERO_INSETS); }});
1187 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1188 add(eventListTable.titleLabel);
1189 add(eventListTable.scrollPane);
1190 add(eventListOperationPanel);
1192 Container cp = getContentPane();
1193 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1194 cp.add(Box.createVerticalStrut(2));
1196 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1197 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1198 setDividerLocation(300);
1201 setDividerLocation(160);