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.FileInputStream;
16 import java.io.FileOutputStream;
17 import java.io.IOException;
18 import java.nio.charset.Charset;
19 import java.security.AccessControlException;
20 import java.util.Arrays;
21 import java.util.EventObject;
22 import java.util.Iterator;
23 import java.util.List;
27 import javax.sound.midi.InvalidMidiDataException;
28 import javax.sound.midi.MidiChannel;
29 import javax.sound.midi.MidiEvent;
30 import javax.sound.midi.MidiMessage;
31 import javax.sound.midi.MidiSystem;
32 import javax.sound.midi.Sequencer;
33 import javax.sound.midi.ShortMessage;
34 import javax.swing.AbstractAction;
35 import javax.swing.AbstractCellEditor;
36 import javax.swing.Action;
37 import javax.swing.Box;
38 import javax.swing.BoxLayout;
39 import javax.swing.DefaultCellEditor;
40 import javax.swing.Icon;
41 import javax.swing.JButton;
42 import javax.swing.JCheckBox;
43 import javax.swing.JComboBox;
44 import javax.swing.JDialog;
45 import javax.swing.JFileChooser;
46 import javax.swing.JLabel;
47 import javax.swing.JOptionPane;
48 import javax.swing.JPanel;
49 import javax.swing.JScrollPane;
50 import javax.swing.JSplitPane;
51 import javax.swing.JTable;
52 import javax.swing.JToggleButton;
53 import javax.swing.ListSelectionModel;
54 import javax.swing.TransferHandler;
55 import javax.swing.border.EtchedBorder;
56 import javax.swing.event.ListSelectionEvent;
57 import javax.swing.event.ListSelectionListener;
58 import javax.swing.event.TableModelEvent;
59 import javax.swing.filechooser.FileNameExtensionFilter;
60 import javax.swing.table.JTableHeader;
61 import javax.swing.table.TableCellEditor;
62 import javax.swing.table.TableCellRenderer;
63 import javax.swing.table.TableColumn;
64 import javax.swing.table.TableColumnModel;
65 import javax.swing.table.TableModel;
67 import camidion.chordhelper.ButtonIcon;
68 import camidion.chordhelper.ChordHelperApplet;
69 import camidion.chordhelper.mididevice.MidiSequencerModel;
70 import camidion.chordhelper.mididevice.VirtualMidiDevice;
71 import camidion.chordhelper.music.MIDISpec;
74 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
77 * Copyright (C) 2006-2016 Akiyoshi Kamide
78 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
80 public class MidiSequenceEditorDialog extends JDialog {
84 public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
86 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
87 putValue(Action.SHORT_DESCRIPTION, tooltip);
90 public void actionPerformed(ActionEvent e) {
91 if( isVisible() ) toFront(); else setVisible(true);
96 * エラーメッセージダイアログを表示します。
97 * @param message エラーメッセージ
99 public void showError(Object message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
101 * 警告メッセージダイアログを表示します。
102 * @param message 警告メッセージ
104 public void showWarning(Object message) { showMessage(message, JOptionPane.WARNING_MESSAGE); }
105 private void showMessage(Object message, int messageType) {
106 JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, messageType);
110 * @param message 確認メッセージ
111 * @return 確認OKのときtrue
113 public boolean confirm(Object message) {
114 return JOptionPane.showConfirmDialog(this, message, ChordHelperApplet.VersionInfo.NAME,
115 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION ;
119 * ドロップされた複数のMIDIファイルを読み込むハンドラー
121 public final TransferHandler transferHandler = new TransferHandler() {
123 public boolean canImport(TransferSupport support) {
124 return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);
126 @SuppressWarnings("unchecked")
128 public boolean importData(TransferSupport support) {
130 play((List<File>)support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor));
132 } catch (Exception e) { showError(e); return false; }
137 * 指定されたリストに格納されたMIDIファイルを読み込んで再生します。
138 * すでに再生されていた場合、このエディタダイアログを表示します。
139 * @param fileList 読み込むMIDIファイルのリスト
141 public void play(List<File> fileList) {
142 PlaylistTableModel playlist = sequenceListTable.getModel();
144 Iterator<File> itr = fileList.iterator();
145 while(itr.hasNext()) {
146 File file = itr.next();
147 try (FileInputStream in = new FileInputStream(file)) {
148 int lastIndex = playlist.add(MidiSystem.getSequence(in), file.getName());
149 if( firstIndex < 0 ) firstIndex = lastIndex;
150 } catch(IOException|InvalidMidiDataException e) {
151 String message = "Could not open as MIDI file "+file+"\n"+e;
152 if( ! itr.hasNext() ) { showWarning(message); break; }
153 if( ! confirm(message + "\n\nContinue to open next file ?") ) break;
154 } catch(AccessControlException e) {
158 MidiSequencerModel sequencerModel = playlist.getSequencerModel();
159 if( sequencerModel.getSequencer().isRunning() ) {
160 String command = (String)openAction.getValue(Action.NAME);
161 openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
164 if( firstIndex >= 0 ) {
166 playlist.loadToSequencer(firstIndex);
167 } catch (InvalidMidiDataException e) { showError(e); return; }
168 sequencerModel.start();
172 private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
173 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
175 * 新しいMIDIシーケンスを生成するダイアログ
177 public NewSequenceDialog newSequenceDialog;
181 public Base64Dialog base64Dialog = new Base64Dialog(this);
183 * プレイリストビュー(シーケンスリスト)
185 public SequenceListTable sequenceListTable;
187 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
189 private TrackListTable trackListTable;
191 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
193 private EventListTable eventListTable;
195 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
197 public MidiEventDialog eventDialog = new MidiEventDialog();
198 private VirtualMidiDevice outputMidiDevice;
200 * プレイリストビュー(シーケンスリスト)
202 public class SequenceListTable extends JTable {
204 * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
206 private MidiFileChooser midiFileChooser;
208 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
210 private Action base64EncodeAction;
213 * @param model プレイリストデータモデル
215 public SequenceListTable(PlaylistTableModel model) {
216 super(model, null, model.sequenceListSelectionModel);
218 midiFileChooser = new MidiFileChooser();
220 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
221 // アプレットの場合、Webクライアントマシンのローカルファイルには
222 // アクセスできないので、ファイル選択ダイアログは使用不可。
223 midiFileChooser = null;
226 new PlayButtonCellEditor();
227 new PositionCellEditor();
230 int column = PlaylistTableModel.Column.CHARSET.ordinal();
231 TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
232 Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
233 for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
235 getColumnModel().getColumn(column).setCellEditor(ce);
236 setAutoCreateColumnsFromModel(false);
238 // Base64エンコードアクションの生成
239 base64EncodeAction = new AbstractAction("Base64") {
241 String tooltip = "Base64 text conversion - Base64テキスト変換";
242 putValue(Action.SHORT_DESCRIPTION, tooltip);
245 public void actionPerformed(ActionEvent e) {
246 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
248 String filename = null;
250 filename = mstm.getFilename();
252 data = mstm.getMIDIdata();
253 } catch (IOException ioe) {
254 base64Dialog.setText("File["+filename+"]:"+ioe.toString());
255 base64Dialog.setVisible(true);
259 base64Dialog.setMIDIData(data, filename);
260 base64Dialog.setVisible(true);
263 TableColumnModel colModel = getColumnModel();
264 for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
265 TableColumn tc = colModel.getColumn(c.ordinal());
266 tc.setPreferredWidth(c.preferredWidth);
267 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
270 private TableColumn lengthColumn;
272 public void tableChanged(TableModelEvent event) {
273 super.tableChanged(event);
276 if( lengthColumn != null ) {
277 int sec = getModel().getTotalTimeInSeconds();
278 String title = PlaylistTableModel.Column.LENGTH.title;
279 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
280 lengthColumn.setHeaderValue(title);
283 // シーケンス削除時など、合計シーケンス長が変わっても
284 // 列モデルからではヘッダタイトルが再描画されないことがある。
285 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
286 JTableHeader th = getTableHeader();
287 if( th != null ) th.repaint();
290 * 時間位置表示セルエディタ(ダブルクリック専用)
292 private class PositionCellEditor extends AbstractCellEditor implements TableCellEditor {
293 public PositionCellEditor() {
294 int column = PlaylistTableModel.Column.POSITION.ordinal();
295 TableColumn tc = getColumnModel().getColumn(column);
296 tc.setCellEditor(this);
299 * セルをダブルクリックしたときだけ編集モードに入るようにします。
300 * @param e イベント(マウスイベント)
301 * @return 編集可能になったらtrue
304 public boolean isCellEditable(EventObject e) {
305 // マウスイベント以外のイベントでは編集不可
306 if( ! (e instanceof MouseEvent) ) return false;
307 return ((MouseEvent)e).getClickCount() == 2;
310 public Object getCellEditorValue() { return null; }
312 * 編集モード時のコンポーネントを返すタイミングで
313 * そのシーケンスをシーケンサーにロードしたあと、
318 public Component getTableCellEditorComponent(
319 JTable table, Object value, boolean isSelected, int row, int column
322 getModel().loadToSequencer(row);
323 } catch (InvalidMidiDataException ex) { showError(ex); }
324 fireEditingStopped();
331 private class PlayButtonCellEditor extends AbstractCellEditor
332 implements TableCellEditor, TableCellRenderer
334 private JToggleButton playButton = new JToggleButton(
335 getModel().getSequencerModel().getStartStopAction()
337 { setMargin(ZERO_INSETS); }
339 public PlayButtonCellEditor() {
340 int column = PlaylistTableModel.Column.PLAY.ordinal();
341 TableColumn tc = getColumnModel().getColumn(column);
342 tc.setCellRenderer(this);
343 tc.setCellEditor(this);
348 * <p>この実装では、クリックしたセルのシーケンスが
350 * trueを返してプレイボタンを押せるようにします。
351 * そうでない場合はプレイボタンのないセルなので、
352 * ダブルクリックされたときだけtrueを返します。
356 public boolean isCellEditable(EventObject e) {
357 // マウスイベント以外はデフォルトメソッドにお任せ
358 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
359 fireEditingStopped();
360 MouseEvent me = (MouseEvent)e;
363 int row = rowAtPoint(me.getPoint());
364 if( row < 0 ) return false;
365 PlaylistTableModel model = getModel();
366 if( row >= model.getRowCount() ) return false;
368 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
369 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
370 return model.getSequenceModelList().get(row).isOnSequencer() || me.getClickCount() == 2;
373 public Object getCellEditorValue() { return null; }
377 * <p>この実装では、行の表すシーケンスがシーケンサーにロードされている場合にプレイボタンを返します。
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() ) return playButton;
389 model.loadToSequencer(row);
390 } catch (InvalidMidiDataException ex) { showError(ex); }
394 public Component getTableCellRendererComponent(
395 JTable table, Object value, boolean isSelected,
396 boolean hasFocus, int row, int column
398 PlaylistTableModel model = getModel();
399 if(model.getSequenceModelList().get(row).isOnSequencer()) return playButton;
400 Class<?> cc = model.getColumnClass(column);
401 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
402 return defaultRenderer.getTableCellRendererComponent(
403 table, value, isSelected, hasFocus, row, column
408 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
413 public PlaylistTableModel getModel() {
414 return (PlaylistTableModel)super.getModel();
419 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
420 "Delete", MidiSequenceEditorDialog.deleteIcon,
421 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
424 public void actionPerformed(ActionEvent event) {
425 PlaylistTableModel model = getModel();
426 if( midiFileChooser != null ) {
427 if( model.getSelectedSequenceModel().isModified() ) {
429 "Selected MIDI sequence not saved - delete it ?\n" +
430 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
431 if( ! confirm(message) ) return;
435 model.removeSelectedSequence();
436 } catch (InvalidMidiDataException ex) {
442 * ファイル選択ダイアログ(アプレットでは使用不可)
444 private class MidiFileChooser extends JFileChooser {
446 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
451 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
453 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
456 public void actionPerformed(ActionEvent event) {
457 PlaylistTableModel playlistModel = getModel();
458 SequenceTrackListTableModel sequenceModel = playlistModel.getSelectedSequenceModel();
459 String fn = sequenceModel.getFilename();
460 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
461 if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
462 File f = getSelectedFile();
465 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
467 try ( FileOutputStream out = new FileOutputStream(f) ) {
468 out.write(sequenceModel.getMIDIdata());
469 sequenceModel.setModified(false);
470 playlistModel.fireSequenceModified(sequenceModel, false);
472 catch( IOException ex ) { showError(ex); }
478 public Action openMidiFileAction = new AbstractAction("Open") {
479 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
481 public void actionPerformed(ActionEvent event) {
482 if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
483 play(Arrays.asList(getSelectedFile()));
490 * シーケンス(トラックリスト)テーブルビュー
492 public class TrackListTable extends JTable {
494 * トラックリストテーブルビューを構築します。
495 * @param model シーケンス(トラックリスト)データモデル
497 public TrackListTable(SequenceTrackListTableModel model) {
498 super(model, null, model.getSelectionModel());
500 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
501 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
502 TableColumn tc = getColumnModel().getColumn(colIndex);
503 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
505 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
508 setAutoCreateColumnsFromModel(false);
510 titleLabel = new TitleLabel();
511 model.getParent().sequenceListSelectionModel.addListSelectionListener(titleLabel);
512 TableColumnModel colModel = getColumnModel();
513 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
514 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
517 * このテーブルビューが表示するデータを提供する
518 * シーケンス(トラックリスト)データモデルを返します。
519 * @return シーケンス(トラックリスト)データモデル
522 public SequenceTrackListTableModel getModel() {
523 return (SequenceTrackListTableModel) super.getModel();
528 TitleLabel titleLabel;
530 * 親テーブルの選択シーケンスの変更に反応する
533 private class TitleLabel extends JLabel implements ListSelectionListener {
534 private static final String TITLE = "Tracks";
535 public TitleLabel() { setText(TITLE); }
537 public void valueChanged(ListSelectionEvent event) {
538 if( event.getValueIsAdjusting() ) return;
539 SequenceTrackListTableModel oldModel = getModel();
540 SequenceTrackListTableModel newModel = oldModel.getParent().getSelectedSequenceModel();
541 if( oldModel == newModel ) return;
543 // MIDIチャンネル選択中のときはキャンセルする
546 int index = oldModel.getParent().sequenceListSelectionModel.getMinSelectionIndex();
548 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
550 if( newModel == null ) {
551 newModel = oldModel.getParent().emptyTrackListTableModel;
552 addTrackAction.setEnabled(false);
555 addTrackAction.setEnabled(true);
557 oldModel.getSelectionModel().removeListSelectionListener(trackSelectionListener);
559 setSelectionModel(newModel.getSelectionModel());
560 newModel.getSelectionModel().addListSelectionListener(trackSelectionListener);
561 trackSelectionListener.valueChanged(null);
567 ListSelectionListener trackSelectionListener = new ListSelectionListener() {
569 public void valueChanged(ListSelectionEvent e) {
570 if( e != null && e.getValueIsAdjusting() ) return;
571 ListSelectionModel tlsm = getModel().getSelectionModel();
572 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
573 eventListTable.titleLabel.update(tlsm, getModel());
579 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
581 * シーケンサーからこのモデルが外された場合がこれに該当します。
585 public void tableChanged(TableModelEvent e) {
586 super.tableChanged(e);
590 * このトラックリストテーブルが編集モードになっていたら解除します。
592 private void cancelCellEditing() {
593 TableCellEditor currentCellEditor = getCellEditor();
594 if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
599 Action addTrackAction = new AbstractAction("New") {
601 String tooltip = "Append new track - 新しいトラックの追加";
602 putValue(Action.SHORT_DESCRIPTION, tooltip);
606 public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
611 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
613 String tooltip = "Delete selected track - 選択したトラックを削除";
614 putValue(Action.SHORT_DESCRIPTION, tooltip);
618 public void actionPerformed(ActionEvent e) {
619 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
620 if( confirm(message) ) getModel().deleteSelectedTracks();
626 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
628 public class EventListTable extends JTable {
630 * 新しいイベントリストテーブルを構築します。
631 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
632 * トラックを切り替えたいときは {@link #setModel(TableModel)}
633 * でデータモデルを異なるトラックのものに切り替えます。
636 * @param model トラック(イベントリスト)データモデル
638 public EventListTable(TrackEventListTableModel model) {
639 super(model, null, model.getSelectionModel());
642 eventCellEditor = new MidiEventCellEditor();
643 setAutoCreateColumnsFromModel(false);
645 eventSelectionListener = new EventSelectionListener();
646 titleLabel = new TitleLabel();
648 TableColumnModel colModel = getColumnModel();
649 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
650 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
653 * このテーブルビューが表示するデータを提供する
654 * トラック(イベントリスト)データモデルを返します。
655 * @return トラック(イベントリスト)データモデル
658 public TrackEventListTableModel getModel() {
659 return (TrackEventListTableModel) super.getModel();
664 TitleLabel titleLabel;
666 * 親テーブルの選択トラックの変更に反応する
669 private class TitleLabel extends JLabel {
670 private static final String TITLE = "MIDI Events";
671 public TitleLabel() { super(TITLE); }
672 public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
674 TrackEventListTableModel oldTrackModel = getModel();
675 int index = tlsm.getMinSelectionIndex();
677 text = String.format(TITLE+" - track No.%d", index);
680 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
681 if( oldTrackModel == newTrackModel )
683 if( newTrackModel == null ) {
684 newTrackModel = getModel().getParent().getParent().emptyEventListTableModel;
685 queryJumpEventAction.setEnabled(false);
686 queryAddEventAction.setEnabled(false);
688 queryPasteEventAction.setEnabled(false);
689 copyEventAction.setEnabled(false);
690 deleteEventAction.setEnabled(false);
691 cutEventAction.setEnabled(false);
694 queryJumpEventAction.setEnabled(true);
695 queryAddEventAction.setEnabled(true);
697 oldTrackModel.getSelectionModel().removeListSelectionListener(eventSelectionListener);
698 setModel(newTrackModel);
699 setSelectionModel(newTrackModel.getSelectionModel());
700 newTrackModel.getSelectionModel().addListSelectionListener(eventSelectionListener);
707 private EventSelectionListener eventSelectionListener;
711 private class EventSelectionListener implements ListSelectionListener {
712 public EventSelectionListener() {
713 getModel().getSelectionModel().addListSelectionListener(this);
716 public void valueChanged(ListSelectionEvent e) {
717 if( e.getValueIsAdjusting() )
719 if( getSelectionModel().isSelectionEmpty() ) {
720 queryPasteEventAction.setEnabled(false);
721 copyEventAction.setEnabled(false);
722 deleteEventAction.setEnabled(false);
723 cutEventAction.setEnabled(false);
726 copyEventAction.setEnabled(true);
727 deleteEventAction.setEnabled(true);
728 cutEventAction.setEnabled(true);
729 TrackEventListTableModel trackModel = getModel();
730 int minIndex = getSelectionModel().getMinSelectionIndex();
731 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
732 if( midiEvent != null ) {
733 MidiMessage msg = midiEvent.getMessage();
734 if( msg instanceof ShortMessage ) {
735 ShortMessage sm = (ShortMessage)msg;
736 int cmd = sm.getCommand();
737 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
739 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
740 int ch = sm.getChannel();
741 int note = sm.getData1();
742 int vel = sm.getData2();
743 outMidiChannels[ch].noteOn(note, vel);
744 outMidiChannels[ch].noteOff(note, vel);
748 if( pairNoteOnOffModel.isSelected() ) {
749 int maxIndex = getSelectionModel().getMaxSelectionIndex();
751 for( int i=minIndex; i<=maxIndex; i++ ) {
752 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
753 partnerIndex = trackModel.getIndexOfPartnerFor(i);
754 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
755 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
762 * Pair noteON/OFF トグルボタンモデル
764 private JToggleButton.ToggleButtonModel
765 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
767 addItemListener(e->eventDialog.midiMessageForm.durationForm.setEnabled(isSelected()));
771 private class EventEditContext {
775 private TrackEventListTableModel trackModel;
779 private TickPositionModel tickPositionModel = new TickPositionModel();
783 private MidiEvent selectedMidiEvent = null;
787 private int selectedIndex = -1;
791 private long currentTick = 0;
793 * 上書きして削除対象にする変更前イベント(null可)
795 private MidiEvent[] midiEventsToBeOverwritten;
797 * 選択したイベントを入力ダイアログなどに反映します。
798 * @param model 対象データモデル
800 private void setSelectedEvent(TrackEventListTableModel trackModel) {
801 this.trackModel = trackModel;
802 SequenceTrackListTableModel sequenceTableModel = trackModel.getParent();
803 int ppq = sequenceTableModel.getSequence().getResolution();
804 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
805 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
807 selectedIndex = trackModel.getSelectionModel().getMinSelectionIndex();
808 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
809 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
810 tickPositionModel.setTickPosition(currentTick);
812 public void setupForEdit(TrackEventListTableModel trackModel) {
813 MidiEvent partnerEvent = null;
814 eventDialog.midiMessageForm.setMessage(
815 selectedMidiEvent.getMessage(),
816 trackModel.getParent().charset
818 if( eventDialog.midiMessageForm.isNote() ) {
819 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
820 if( partnerIndex < 0 ) {
821 eventDialog.midiMessageForm.durationForm.setDuration(0);
824 partnerEvent = trackModel.getMidiEvent(partnerIndex);
825 long partnerTick = partnerEvent.getTick();
826 long duration = currentTick > partnerTick ?
827 currentTick - partnerTick : partnerTick - currentTick ;
828 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
831 if(partnerEvent == null)
832 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
834 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
836 private Action jumpEventAction = new AbstractAction() {
837 { putValue(NAME,"Jump"); }
838 public void actionPerformed(ActionEvent e) {
839 long tick = tickPositionModel.getTickPosition();
840 scrollToEventAt(tick);
841 eventDialog.setVisible(false);
845 private Action pasteEventAction = new AbstractAction() {
846 { putValue(NAME,"Paste"); }
847 public void actionPerformed(ActionEvent e) {
848 long tick = tickPositionModel.getTickPosition();
849 clipBoard.paste(trackModel, tick);
850 scrollToEventAt(tick);
851 // ペーストで曲の長さが変わったことをプレイリストに通知
852 SequenceTrackListTableModel seqModel = trackModel.getParent();
853 seqModel.getParent().fireSequenceModified(seqModel, true);
854 eventDialog.setVisible(false);
858 private boolean applyEvent() {
859 long tick = tickPositionModel.getTickPosition();
860 MidiMessageForm form = eventDialog.midiMessageForm;
861 SequenceTrackListTableModel seqModel = trackModel.getParent();
862 MidiMessage msg = form.getMessage(seqModel.charset);
866 MidiEvent newMidiEvent = new MidiEvent(msg, tick);
867 if( midiEventsToBeOverwritten != null ) {
868 // 上書き消去するための選択済イベントがあった場合
869 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
871 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
872 System.out.println("addMidiEvent failure");
875 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
876 ShortMessage sm = form.createPartnerMessage();
878 scrollToEventAt( tick );
880 int duration = form.durationForm.getDuration();
881 if( form.isNote(false) ) {
882 duration = -duration;
884 long partnerTick = tick + (long)duration;
885 if( partnerTick < 0L ) partnerTick = 0L;
886 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
887 if( ! trackModel.addMidiEvent(partner) ) {
888 System.out.println("addMidiEvent failure (note on/off partner message)");
890 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
893 seqModel.getParent().fireSequenceModified(seqModel, true);
894 eventDialog.setVisible(false);
898 private EventEditContext editContext = new EventEditContext();
900 * 指定のTick位置へジャンプするアクション
902 Action queryJumpEventAction = new AbstractAction() {
904 putValue(NAME,"Jump to ...");
907 public void actionPerformed(ActionEvent e) {
908 editContext.setSelectedEvent(getModel());
909 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
915 Action queryAddEventAction = new AbstractAction() {
917 putValue(NAME,"New");
920 public void actionPerformed(ActionEvent e) {
921 TrackEventListTableModel model = getModel();
922 editContext.setSelectedEvent(model);
923 editContext.midiEventsToBeOverwritten = null;
924 eventDialog.openEventForm(
926 eventCellEditor.applyEventAction,
932 * MIDIイベントのコピー&ペーストを行うためのクリップボード
934 private class LocalClipBoard {
935 private MidiEvent copiedEventsToPaste[];
936 private int copiedEventsPPQ = 0;
937 public void copy(TrackEventListTableModel model, boolean withRemove) {
938 copiedEventsToPaste = model.getSelectedMidiEvents();
939 copiedEventsPPQ = model.getParent().getSequence().getResolution();
940 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
941 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
942 queryPasteEventAction.setEnabled(en);
944 public void cut(TrackEventListTableModel model) {copy(model,true);}
945 public void copy(TrackEventListTableModel model){copy(model,false);}
946 public void paste(TrackEventListTableModel model, long tick) {
947 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
950 private LocalClipBoard clipBoard = new LocalClipBoard();
952 * 指定のTick位置へ貼り付けるアクション
954 Action queryPasteEventAction = new AbstractAction() {
956 putValue(NAME,"Paste to ...");
959 public void actionPerformed(ActionEvent e) {
960 editContext.setSelectedEvent(getModel());
961 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
967 public Action cutEventAction = new AbstractAction("Cut") {
972 public void actionPerformed(ActionEvent e) {
973 TrackEventListTableModel model = getModel();
974 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
976 clipBoard.cut(model);
982 public Action copyEventAction = new AbstractAction("Copy") {
987 public void actionPerformed(ActionEvent e) {
988 clipBoard.copy(getModel());
994 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
999 public void actionPerformed(ActionEvent e) {
1000 TrackEventListTableModel model = getModel();
1001 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1003 model.removeSelectedMidiEvents();
1009 private MidiEventCellEditor eventCellEditor;
1013 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1015 * MIDIイベントセルエディタを構築します。
1017 public MidiEventCellEditor() {
1018 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
1019 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1020 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1021 getColumnModel().getColumn(index).setCellEditor(this);
1024 * セルをダブルクリックしないと編集できないようにします。
1025 * @param e イベント(マウスイベント)
1026 * @return 編集可能になったらtrue
1029 public boolean isCellEditable(EventObject e) {
1030 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1031 return ((MouseEvent)e).getClickCount() == 2;
1034 public Object getCellEditorValue() { return null; }
1036 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1038 private ComponentListener dialogComponentListener = new ComponentAdapter() {
1040 public void componentHidden(ComponentEvent e) {
1041 fireEditingCanceled();
1043 eventDialog.removeComponentListener(this);
1049 private Action editEventAction = new AbstractAction() {
1050 public void actionPerformed(ActionEvent e) {
1051 TrackEventListTableModel model = getModel();
1052 editContext.setSelectedEvent(model);
1053 if( editContext.selectedMidiEvent == null )
1055 editContext.setupForEdit(model);
1056 eventDialog.addComponentListener(dialogComponentListener);
1057 eventDialog.openEventForm("Change MIDI event", applyEventAction);
1063 private JButton editEventButton = new JButton(editEventAction){{
1064 setHorizontalAlignment(JButton.LEFT);
1067 public Component getTableCellEditorComponent(
1068 JTable table, Object value, boolean isSelected, int row, int column
1070 editEventButton.setText(value.toString());
1071 return editEventButton;
1074 * 入力したイベントを反映するアクション
1076 private Action applyEventAction = new AbstractAction() {
1078 putValue(NAME,"OK");
1080 public void actionPerformed(ActionEvent e) {
1081 if( editContext.applyEvent() ) fireEditingStopped();
1086 * スクロール可能なMIDIイベントテーブルビュー
1088 private JScrollPane scrollPane = new JScrollPane(this);
1090 * 指定の MIDI tick のイベントへスクロールします。
1091 * @param tick MIDI tick
1093 public void scrollToEventAt(long tick) {
1094 int index = getModel().tickToIndex(tick);
1095 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1096 getSelectionModel().setSelectionInterval(index, index);
1101 * 新しい {@link MidiSequenceEditorDialog} を構築します。
1102 * @param playlistTableModel このエディタが参照するプレイリストモデル
1103 * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1105 public MidiSequenceEditorDialog(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice) {
1106 this.outputMidiDevice = outputMidiDevice;
1107 sequenceListTable = new SequenceListTable(playlistTableModel);
1108 trackListTable = new TrackListTable(
1109 new SequenceTrackListTableModel(playlistTableModel, null, null)
1111 eventListTable = new EventListTable(new TrackEventListTableModel(trackListTable.getModel(), null));
1112 newSequenceDialog = new NewSequenceDialog(playlistTableModel, outputMidiDevice);
1113 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1114 setBounds( 150, 200, 900, 500 );
1115 setLayout(new FlowLayout());
1116 setTransferHandler(transferHandler);
1119 JPanel playlistPanel = new JPanel() {{
1120 JPanel playlistOperationPanel = new JPanel() {{
1121 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1122 add(Box.createRigidArea(new Dimension(10, 0)));
1123 add(new JButton(newSequenceDialog.openAction) {{ setMargin(ZERO_INSETS); }});
1124 if( sequenceListTable.midiFileChooser != null ) {
1125 add( Box.createRigidArea(new Dimension(5, 0)) );
1126 add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {
1127 { setMargin(ZERO_INSETS); }
1130 if(sequenceListTable.base64EncodeAction != null) {
1131 add(Box.createRigidArea(new Dimension(5, 0)));
1132 add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1134 add(Box.createRigidArea(new Dimension(5, 0)));
1135 PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
1136 add(new JButton(playlistTableModel.getMoveToTopAction()) {{ setMargin(ZERO_INSETS); }});
1137 add(Box.createRigidArea(new Dimension(5, 0)));
1138 add(new JButton(playlistTableModel.getMoveToBottomAction()) {{ setMargin(ZERO_INSETS); }});
1139 if( sequenceListTable.midiFileChooser != null ) {
1140 add(Box.createRigidArea(new Dimension(5, 0)));
1141 add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {
1142 { setMargin(ZERO_INSETS); }
1145 add( Box.createRigidArea(new Dimension(5, 0)) );
1146 add(new JButton(sequenceListTable.deleteSequenceAction) {{ setMargin(ZERO_INSETS); }});
1147 add( Box.createRigidArea(new Dimension(5, 0)) );
1148 add(new SequencerSpeedSlider(playlistTableModel.getSequencerModel().speedSliderModel));
1149 add( Box.createRigidArea(new Dimension(5, 0)) );
1151 setBorder(new EtchedBorder());
1152 MidiSequencerModel sequencerModel = sequenceListTable.getModel().getSequencerModel();
1153 add(new JLabel("Sync Master"));
1154 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1155 add(new JLabel("Slave"));
1156 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1159 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1160 add(new JScrollPane(sequenceListTable));
1161 add(Box.createRigidArea(new Dimension(0, 10)));
1162 add(playlistOperationPanel);
1163 add(Box.createRigidArea(new Dimension(0, 10)));
1165 JPanel trackListPanel = new JPanel() {{
1166 JPanel trackListOperationPanel = new JPanel() {{
1167 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ZERO_INSETS); }});
1168 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
1170 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1171 add(trackListTable.titleLabel);
1172 add(Box.createRigidArea(new Dimension(0, 5)));
1173 add(new JScrollPane(trackListTable));
1174 add(Box.createRigidArea(new Dimension(0, 5)));
1175 add(trackListOperationPanel);
1177 JPanel eventListPanel = new JPanel() {{
1178 JPanel eventListOperationPanel = new JPanel() {{
1179 add(new JCheckBox("Pair NoteON/OFF") {{
1180 setModel(eventListTable.pairNoteOnOffModel);
1181 setToolTipText("NoteON/OFFをペアで同時選択する");
1183 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ZERO_INSETS); }});
1184 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ZERO_INSETS); }});
1185 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ZERO_INSETS); }});
1186 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ZERO_INSETS); }});
1187 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ZERO_INSETS); }});
1188 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ZERO_INSETS); }});
1190 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1191 add(eventListTable.titleLabel);
1192 add(eventListTable.scrollPane);
1193 add(eventListOperationPanel);
1195 Container cp = getContentPane();
1196 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1197 cp.add(Box.createVerticalStrut(2));
1199 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1200 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1201 setDividerLocation(300);
1204 setDividerLocation(160);