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.datatransfer.Transferable;
10 import java.awt.dnd.DnDConstants;
11 import java.awt.dnd.DropTarget;
12 import java.awt.dnd.DropTargetAdapter;
13 import java.awt.dnd.DropTargetDragEvent;
14 import java.awt.dnd.DropTargetDropEvent;
15 import java.awt.dnd.DropTargetListener;
16 import java.awt.event.ActionEvent;
17 import java.awt.event.ComponentAdapter;
18 import java.awt.event.ComponentEvent;
19 import java.awt.event.ComponentListener;
20 import java.awt.event.ItemEvent;
21 import java.awt.event.ItemListener;
22 import java.awt.event.MouseEvent;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.nio.charset.Charset;
27 import java.security.AccessControlException;
28 import java.util.Arrays;
29 import java.util.EventObject;
30 import java.util.List;
34 import javax.sound.midi.InvalidMidiDataException;
35 import javax.sound.midi.MidiChannel;
36 import javax.sound.midi.MidiEvent;
37 import javax.sound.midi.MidiMessage;
38 import javax.sound.midi.Sequencer;
39 import javax.sound.midi.ShortMessage;
40 import javax.swing.AbstractAction;
41 import javax.swing.AbstractCellEditor;
42 import javax.swing.Action;
43 import javax.swing.Box;
44 import javax.swing.BoxLayout;
45 import javax.swing.DefaultCellEditor;
46 import javax.swing.Icon;
47 import javax.swing.JButton;
48 import javax.swing.JCheckBox;
49 import javax.swing.JComboBox;
50 import javax.swing.JDialog;
51 import javax.swing.JFileChooser;
52 import javax.swing.JLabel;
53 import javax.swing.JOptionPane;
54 import javax.swing.JPanel;
55 import javax.swing.JScrollPane;
56 import javax.swing.JSplitPane;
57 import javax.swing.JTable;
58 import javax.swing.JToggleButton;
59 import javax.swing.ListSelectionModel;
60 import javax.swing.event.ListSelectionEvent;
61 import javax.swing.event.ListSelectionListener;
62 import javax.swing.event.TableModelEvent;
63 import javax.swing.filechooser.FileNameExtensionFilter;
64 import javax.swing.table.JTableHeader;
65 import javax.swing.table.TableCellEditor;
66 import javax.swing.table.TableCellRenderer;
67 import javax.swing.table.TableColumn;
68 import javax.swing.table.TableColumnModel;
69 import javax.swing.table.TableModel;
71 import camidion.chordhelper.ButtonIcon;
72 import camidion.chordhelper.ChordHelperApplet;
73 import camidion.chordhelper.mididevice.MidiSequencerModel;
74 import camidion.chordhelper.mididevice.VirtualMidiDevice;
75 import camidion.chordhelper.music.MIDISpec;
78 * MIDIエディタ(MIDI Editor/Playlist for MIDI Chord Helper)
81 * Copyright (C) 2006-2016 Akiyoshi Kamide
82 * http://www.yk.rim.or.jp/~kamide/music/chordhelper/
84 public class MidiSequenceEditor extends JDialog {
88 public Action openAction = new AbstractAction("Edit/Playlist/Speed", new ButtonIcon(ButtonIcon.EDIT_ICON)) {
90 String tooltip = "MIDIシーケンスの編集/プレイリスト/再生速度調整";
91 putValue(Action.SHORT_DESCRIPTION, tooltip);
94 public void actionPerformed(ActionEvent e) {
95 if( isVisible() ) toFront(); else setVisible(true);
100 * エラーメッセージダイアログを表示します。
101 * @param message エラーメッセージ
103 public void showError(String message) { showMessage(message, JOptionPane.ERROR_MESSAGE); }
105 * 警告メッセージダイアログを表示します。
106 * @param message 警告メッセージ
108 public void showWarning(String message) { showMessage(message, JOptionPane.WARNING_MESSAGE); }
109 private void showMessage(String message, int messageType) {
110 JOptionPane.showMessageDialog(this, message, ChordHelperApplet.VersionInfo.NAME, messageType);
114 * @param message 確認メッセージ
115 * @return 確認OKのときtrue
117 public boolean confirm(String message) {
118 return JOptionPane.showConfirmDialog(this, message, ChordHelperApplet.VersionInfo.NAME,
119 JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION ;
123 * ドロップされた複数のMIDIファイルを読み込むリスナー
125 public final DropTargetListener dropTargetListener = new DropTargetAdapter() {
127 public void dragEnter(DropTargetDragEvent event) {
128 if( event.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
129 event.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE);
133 @SuppressWarnings("unchecked")
134 public void drop(DropTargetDropEvent event) {
135 event.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
137 int action = event.getDropAction();
138 if ( (action & DnDConstants.ACTION_COPY_OR_MOVE) != 0 ) {
139 Transferable t = event.getTransferable();
140 if( t.isDataFlavorSupported(DataFlavor.javaFileListFlavor) ) {
141 loadAndPlay((List<File>)t.getTransferData(DataFlavor.javaFileListFlavor));
142 event.dropComplete(true);
147 catch (Exception ex) {
148 ex.printStackTrace();
150 event.dropComplete(false);
155 * 複数のMIDIファイルを読み込み、再生されていなかったら再生します。
156 * すでに再生されていた場合、このエディタダイアログを表示します。
158 * @param fileList 読み込むMIDIファイルのリスト
159 * @see #loadAndPlay(File)
161 public void loadAndPlay(List<File> fileList) {
162 int indexOfAddedTop = -1;
163 PlaylistTableModel playlist = sequenceListTable.getModel();
165 indexOfAddedTop = playlist.addSequences(fileList);
166 } catch(IOException|InvalidMidiDataException e) {
167 showWarning(e.getMessage());
168 } catch(AccessControlException e) {
169 showError(e.getMessage());
172 MidiSequencerModel sequencerModel = playlist.sequencerModel;
173 if( sequencerModel.getSequencer().isRunning() ) {
174 String command = (String)openAction.getValue(Action.NAME);
175 openAction.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, command));
178 if( indexOfAddedTop >= 0 ) {
179 playlist.loadToSequencer(indexOfAddedTop);
180 sequencerModel.start();
184 * 1件のMIDIファイルを読み込み、再生されていなかったら再生します。
185 * すでに再生されていた場合、このエディタダイアログを表示します。
187 * @param file 読み込むMIDIファイル
188 * @see #loadAndPlay(List) loadAndPlay(List<File>)
190 public void loadAndPlay(File file) { loadAndPlay(Arrays.asList(file)); }
192 private static final Insets ZERO_INSETS = new Insets(0,0,0,0);
193 private static final Icon deleteIcon = new ButtonIcon(ButtonIcon.X_ICON);
195 * 新しいMIDIシーケンスを生成するダイアログ
197 public NewSequenceDialog newSequenceDialog;
201 public Base64Dialog base64Dialog = new Base64Dialog(this);
203 * プレイリストビュー(シーケンスリスト)
205 public SequenceListTable sequenceListTable;
207 * MIDIトラックリストテーブルビュー(選択中のシーケンスの中身)
209 private TrackListTable trackListTable;
211 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
213 private EventListTable eventListTable;
215 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
217 public MidiEventDialog eventDialog = new MidiEventDialog();
218 private VirtualMidiDevice outputMidiDevice;
220 * プレイリストビュー(シーケンスリスト)
222 public class SequenceListTable extends JTable {
224 * ファイル選択ダイアログ(アプレットの場合は使用不可なのでnull)
226 private MidiFileChooser midiFileChooser;
228 * BASE64エンコードアクション(ライブラリが見えている場合のみ有効)
230 private Action base64EncodeAction;
233 * @param model プレイリストデータモデル
235 public SequenceListTable(PlaylistTableModel model) {
236 super(model, null, model.sequenceListSelectionModel);
238 midiFileChooser = new MidiFileChooser();
240 catch( ExceptionInInitializerError|NoClassDefFoundError|AccessControlException e ) {
241 // アプレットの場合、Webクライアントマシンのローカルファイルには
242 // アクセスできないので、ファイル選択ダイアログは使用不可。
243 midiFileChooser = null;
246 new PlayButtonCellEditor();
247 new PositionCellEditor();
250 int column = PlaylistTableModel.Column.CHARSET.ordinal();
251 TableCellEditor ce = new DefaultCellEditor(new JComboBox<Charset>() {{
252 Set<Map.Entry<String,Charset>> entrySet = Charset.availableCharsets().entrySet();
253 for( Map.Entry<String,Charset> entry : entrySet ) addItem(entry.getValue());
255 getColumnModel().getColumn(column).setCellEditor(ce);
256 setAutoCreateColumnsFromModel(false);
258 // Base64エンコードアクションの生成
259 if( base64Dialog.isBase64Available() ) {
260 base64EncodeAction = new AbstractAction("Base64") {
262 String tooltip = "Base64 text conversion - Base64テキスト変換";
263 putValue(Action.SHORT_DESCRIPTION, tooltip);
266 public void actionPerformed(ActionEvent e) {
267 SequenceTrackListTableModel mstm = getModel().getSelectedSequenceModel();
269 String filename = null;
271 data = mstm.getMIDIdata();
272 filename = mstm.getFilename();
274 base64Dialog.setMIDIData(data, filename);
275 base64Dialog.setVisible(true);
279 TableColumnModel colModel = getColumnModel();
280 for( PlaylistTableModel.Column c : PlaylistTableModel.Column.values() ) {
281 TableColumn tc = colModel.getColumn(c.ordinal());
282 tc.setPreferredWidth(c.preferredWidth);
283 if( c == PlaylistTableModel.Column.LENGTH ) lengthColumn = tc;
286 private TableColumn lengthColumn;
288 public void tableChanged(TableModelEvent event) {
289 super.tableChanged(event);
292 if( lengthColumn != null ) {
293 int sec = getModel().getTotalSeconds();
294 String title = PlaylistTableModel.Column.LENGTH.title;
295 title = String.format(title+" [%02d:%02d]", sec/60, sec%60);
296 lengthColumn.setHeaderValue(title);
299 // シーケンス削除時など、合計シーケンス長が変わっても
300 // 列モデルからではヘッダタイトルが再描画されないことがある。
301 // そこで、ヘッダビューから repaint() で突っついて再描画させる。
302 JTableHeader th = getTableHeader();
303 if( th != null ) th.repaint();
306 * 時間位置表示セルエディタ(ダブルクリック専用)
308 private class PositionCellEditor extends AbstractCellEditor
309 implements TableCellEditor
311 public PositionCellEditor() {
312 int column = PlaylistTableModel.Column.POSITION.ordinal();
313 TableColumn tc = getColumnModel().getColumn(column);
314 tc.setCellEditor(this);
317 * セルをダブルクリックしたときだけ編集モードに入るようにします。
318 * @param e イベント(マウスイベント)
319 * @return 編集可能になったらtrue
322 public boolean isCellEditable(EventObject e) {
323 // マウスイベント以外のイベントでは編集不可
324 if( ! (e instanceof MouseEvent) ) return false;
325 return ((MouseEvent)e).getClickCount() == 2;
328 public Object getCellEditorValue() { return null; }
330 * 編集モード時のコンポーネントを返すタイミングで
331 * そのシーケンスをシーケンサーにロードしたあと、
336 public Component getTableCellEditorComponent(
337 JTable table, Object value, boolean isSelected,
340 getModel().loadToSequencer(row);
341 fireEditingStopped();
348 private class PlayButtonCellEditor extends AbstractCellEditor
349 implements TableCellEditor, TableCellRenderer
351 private JToggleButton playButton = new JToggleButton(
352 getModel().sequencerModel.startStopAction
354 { setMargin(ZERO_INSETS); }
356 public PlayButtonCellEditor() {
357 int column = PlaylistTableModel.Column.PLAY.ordinal();
358 TableColumn tc = getColumnModel().getColumn(column);
359 tc.setCellRenderer(this);
360 tc.setCellEditor(this);
365 * <p>この実装では、クリックしたセルのシーケンスが
367 * trueを返してプレイボタンを押せるようにします。
368 * そうでない場合はプレイボタンのないセルなので、
369 * ダブルクリックされたときだけtrueを返します。
373 public boolean isCellEditable(EventObject e) {
374 // マウスイベント以外はデフォルトメソッドにお任せ
375 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
376 fireEditingStopped();
377 MouseEvent me = (MouseEvent)e;
380 int row = rowAtPoint(me.getPoint());
381 if( row < 0 ) return false;
382 PlaylistTableModel model = getModel();
383 if( row >= model.getRowCount() ) return false;
385 // セル内にプレイボタンがあれば、シングルクリックを受け付ける。
386 // プレイボタンのないセルは、ダブルクリックのみ受け付ける。
387 return model.getSequenceList().get(row).isOnSequencer() || me.getClickCount() == 2;
390 public Object getCellEditorValue() { return null; }
394 * <p>この実装では、行の表すシーケンスが
395 * シーケンサーにロードされている場合にプレイボタンを返します。
397 * そのシーケンスをシーケンサーにロードしてnullを返します。
401 public Component getTableCellEditorComponent(
402 JTable table, Object value, boolean isSelected, int row, int column
404 fireEditingStopped();
405 PlaylistTableModel model = getModel();
406 if( model.getSequenceList().get(row).isOnSequencer() ) return playButton;
407 model.loadToSequencer(row);
411 public Component getTableCellRendererComponent(
412 JTable table, Object value, boolean isSelected,
413 boolean hasFocus, int row, int column
415 PlaylistTableModel model = getModel();
416 if(model.getSequenceList().get(row).isOnSequencer()) return playButton;
417 Class<?> cc = model.getColumnClass(column);
418 TableCellRenderer defaultRenderer = table.getDefaultRenderer(cc);
419 return defaultRenderer.getTableCellRendererComponent(
420 table, value, isSelected, hasFocus, row, column
425 * このプレイリスト(シーケンスリスト)が表示するデータを提供する
430 public PlaylistTableModel getModel() { return (PlaylistTableModel)super.getModel(); }
434 Action deleteSequenceAction = getModel().new SelectedSequenceAction(
435 "Delete", MidiSequenceEditor.deleteIcon,
436 "Delete selected MIDI sequence - 選択した曲をプレイリストから削除"
439 public void actionPerformed(ActionEvent e) {
440 PlaylistTableModel model = getModel();
441 if( midiFileChooser != null ) {
442 if( model.getSelectedSequenceModel().isModified() ) {
444 "Selected MIDI sequence not saved - delete it ?\n" +
445 "選択したMIDIシーケンスはまだ保存されていません。削除しますか?";
446 if( ! confirm(message) ) return;
449 model.removeSelectedSequence();
453 * ファイル選択ダイアログ(アプレットでは使用不可)
455 private class MidiFileChooser extends JFileChooser {
457 setFileFilter(new FileNameExtensionFilter("MIDI sequence (*.mid)", "mid"));
462 public Action saveMidiFileAction = getModel().new SelectedSequenceAction(
464 "Save selected MIDI sequence to file - 選択したMIDIシーケンスをファイルに保存"
467 public void actionPerformed(ActionEvent event) {
468 SequenceTrackListTableModel sequenceModel = getModel().getSelectedSequenceModel();
469 String fn = sequenceModel.getFilename();
470 if( fn != null && ! fn.isEmpty() ) setSelectedFile(new File(fn));
471 if( showSaveDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
472 File f = getSelectedFile();
475 if( ! confirm("Overwrite " + fn + " ?\n" + fn + " を上書きしてよろしいですか?") ) return;
477 try ( FileOutputStream out = new FileOutputStream(f) ) {
478 out.write(sequenceModel.getMIDIdata());
479 sequenceModel.setModified(false);
481 catch( IOException ex ) {
482 showError( ex.getMessage() );
483 ex.printStackTrace();
490 public Action openMidiFileAction = new AbstractAction("Open") {
491 { putValue(Action.SHORT_DESCRIPTION, "Open MIDI file - MIDIファイルを開く"); }
493 public void actionPerformed(ActionEvent event) {
494 if( showOpenDialog((Component)event.getSource()) != JFileChooser.APPROVE_OPTION ) return;
495 loadAndPlay(getSelectedFile());
502 * シーケンス(トラックリスト)テーブルビュー
504 public class TrackListTable extends JTable {
506 * トラックリストテーブルビューを構築します。
507 * @param model シーケンス(トラックリスト)データモデル
509 public TrackListTable(SequenceTrackListTableModel model) {
510 super(model, null, model.trackListSelectionModel);
512 // 録音対象のMIDIチャンネルをコンボボックスで選択できるようにする
513 int colIndex = SequenceTrackListTableModel.Column.RECORD_CHANNEL.ordinal();
514 TableColumn tc = getColumnModel().getColumn(colIndex);
515 tc.setCellEditor(new DefaultCellEditor(new JComboBox<String>(){{
517 for(int i=1; i <= MIDISpec.MAX_CHANNELS; i++) addItem(String.format("%d", i));
520 setAutoCreateColumnsFromModel(false);
522 trackSelectionListener = new TrackSelectionListener();
523 titleLabel = new TitleLabel();
524 model.sequenceListTableModel.sequenceListSelectionModel.addListSelectionListener(titleLabel);
525 TableColumnModel colModel = getColumnModel();
526 for( SequenceTrackListTableModel.Column c : SequenceTrackListTableModel.Column.values() )
527 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
530 * このテーブルビューが表示するデータを提供する
531 * シーケンス(トラックリスト)データモデルを返します。
532 * @return シーケンス(トラックリスト)データモデル
535 public SequenceTrackListTableModel getModel() {
536 return (SequenceTrackListTableModel) super.getModel();
541 TitleLabel titleLabel;
543 * 親テーブルの選択シーケンスの変更に反応する
546 private class TitleLabel extends JLabel implements ListSelectionListener {
547 private static final String TITLE = "Tracks";
548 public TitleLabel() { setText(TITLE); }
550 public void valueChanged(ListSelectionEvent event) {
551 if( event.getValueIsAdjusting() ) return;
552 SequenceTrackListTableModel oldModel = getModel();
553 SequenceTrackListTableModel newModel = oldModel.sequenceListTableModel.getSelectedSequenceModel();
554 if( oldModel == newModel ) return;
556 // MIDIチャンネル選択中のときはキャンセルする
559 int index = oldModel.sequenceListTableModel.sequenceListSelectionModel.getMinSelectionIndex();
561 if( index >= 0 ) text = String.format(text+" - MIDI file No.%d", index);
563 if( newModel == null ) {
564 newModel = oldModel.sequenceListTableModel.emptyTrackListTableModel;
565 addTrackAction.setEnabled(false);
568 addTrackAction.setEnabled(true);
570 oldModel.trackListSelectionModel.removeListSelectionListener(trackSelectionListener);
572 setSelectionModel(newModel.trackListSelectionModel);
573 newModel.trackListSelectionModel.addListSelectionListener(trackSelectionListener);
574 trackSelectionListener.valueChanged(null);
580 TrackSelectionListener trackSelectionListener;
584 private class TrackSelectionListener implements ListSelectionListener {
586 public void valueChanged(ListSelectionEvent e) {
587 if( e != null && e.getValueIsAdjusting() ) return;
588 ListSelectionModel tlsm = getModel().trackListSelectionModel;
589 deleteTrackAction.setEnabled(! tlsm.isSelectionEmpty());
590 eventListTable.titleLabel.update(tlsm, getModel());
596 * <p>このトラックリストテーブルのデータが変わったときに編集を解除します。
598 * シーケンサーからこのモデルが外された場合がこれに該当します。
602 public void tableChanged(TableModelEvent e) {
603 super.tableChanged(e);
607 * このトラックリストテーブルが編集モードになっていたら解除します。
609 private void cancelCellEditing() {
610 TableCellEditor currentCellEditor = getCellEditor();
611 if( currentCellEditor != null ) currentCellEditor.cancelCellEditing();
616 Action addTrackAction = new AbstractAction("New") {
618 String tooltip = "Append new track - 新しいトラックの追加";
619 putValue(Action.SHORT_DESCRIPTION, tooltip);
623 public void actionPerformed(ActionEvent e) { getModel().createTrack(); }
628 Action deleteTrackAction = new AbstractAction("Delete", deleteIcon) {
630 String tooltip = "Delete selected track - 選択したトラックを削除";
631 putValue(Action.SHORT_DESCRIPTION, tooltip);
635 public void actionPerformed(ActionEvent e) {
636 String message = "Do you want to delete selected track ?\n選択したトラックを削除しますか?";
637 if( confirm(message) ) getModel().deleteSelectedTracks();
643 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
645 public class EventListTable extends JTable {
647 * 新しいイベントリストテーブルを構築します。
648 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
649 * トラックを切り替えたいときは {@link #setModel(TableModel)}
650 * でデータモデルを異なるトラックのものに切り替えます。
653 * @param model トラック(イベントリスト)データモデル
655 public EventListTable(TrackEventListTableModel model) {
656 super(model, null, model.eventSelectionModel);
659 eventCellEditor = new MidiEventCellEditor();
660 setAutoCreateColumnsFromModel(false);
662 eventSelectionListener = new EventSelectionListener();
663 titleLabel = new TitleLabel();
665 TableColumnModel colModel = getColumnModel();
666 for( TrackEventListTableModel.Column c : TrackEventListTableModel.Column.values() )
667 colModel.getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth);
670 * このテーブルビューが表示するデータを提供する
671 * トラック(イベントリスト)データモデルを返します。
672 * @return トラック(イベントリスト)データモデル
675 public TrackEventListTableModel getModel() {
676 return (TrackEventListTableModel) super.getModel();
681 TitleLabel titleLabel;
683 * 親テーブルの選択トラックの変更に反応する
686 private class TitleLabel extends JLabel {
687 private static final String TITLE = "MIDI Events";
688 public TitleLabel() { super(TITLE); }
689 public void update(ListSelectionModel tlsm, SequenceTrackListTableModel sequenceModel) {
691 TrackEventListTableModel oldTrackModel = getModel();
692 int index = tlsm.getMinSelectionIndex();
694 text = String.format(TITLE+" - track No.%d", index);
697 TrackEventListTableModel newTrackModel = sequenceModel.getSelectedTrackModel();
698 if( oldTrackModel == newTrackModel )
700 if( newTrackModel == null ) {
701 newTrackModel = getModel().sequenceTrackListTableModel.sequenceListTableModel.emptyEventListTableModel;
702 queryJumpEventAction.setEnabled(false);
703 queryAddEventAction.setEnabled(false);
705 queryPasteEventAction.setEnabled(false);
706 copyEventAction.setEnabled(false);
707 deleteEventAction.setEnabled(false);
708 cutEventAction.setEnabled(false);
711 queryJumpEventAction.setEnabled(true);
712 queryAddEventAction.setEnabled(true);
714 oldTrackModel.eventSelectionModel.removeListSelectionListener(eventSelectionListener);
715 setModel(newTrackModel);
716 setSelectionModel(newTrackModel.eventSelectionModel);
717 newTrackModel.eventSelectionModel.addListSelectionListener(eventSelectionListener);
724 private EventSelectionListener eventSelectionListener;
728 private class EventSelectionListener implements ListSelectionListener {
729 public EventSelectionListener() {
730 getModel().eventSelectionModel.addListSelectionListener(this);
733 public void valueChanged(ListSelectionEvent e) {
734 if( e.getValueIsAdjusting() )
736 if( getSelectionModel().isSelectionEmpty() ) {
737 queryPasteEventAction.setEnabled(false);
738 copyEventAction.setEnabled(false);
739 deleteEventAction.setEnabled(false);
740 cutEventAction.setEnabled(false);
743 copyEventAction.setEnabled(true);
744 deleteEventAction.setEnabled(true);
745 cutEventAction.setEnabled(true);
746 TrackEventListTableModel trackModel = getModel();
747 int minIndex = getSelectionModel().getMinSelectionIndex();
748 MidiEvent midiEvent = trackModel.getMidiEvent(minIndex);
749 if( midiEvent != null ) {
750 MidiMessage msg = midiEvent.getMessage();
751 if( msg instanceof ShortMessage ) {
752 ShortMessage sm = (ShortMessage)msg;
753 int cmd = sm.getCommand();
754 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
756 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
757 int ch = sm.getChannel();
758 int note = sm.getData1();
759 int vel = sm.getData2();
760 outMidiChannels[ch].noteOn(note, vel);
761 outMidiChannels[ch].noteOff(note, vel);
765 if( pairNoteOnOffModel.isSelected() ) {
766 int maxIndex = getSelectionModel().getMaxSelectionIndex();
768 for( int i=minIndex; i<=maxIndex; i++ ) {
769 if( ! getSelectionModel().isSelectedIndex(i) ) continue;
770 partnerIndex = trackModel.getIndexOfPartnerFor(i);
771 if( partnerIndex >= 0 && ! getSelectionModel().isSelectedIndex(partnerIndex) )
772 getSelectionModel().addSelectionInterval(partnerIndex, partnerIndex);
779 * Pair noteON/OFF トグルボタンモデル
781 private JToggleButton.ToggleButtonModel
782 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
784 addItemListener(new ItemListener() {
785 public void itemStateChanged(ItemEvent e) {
786 eventDialog.midiMessageForm.durationForm.setEnabled(isSelected());
792 private class EventEditContext {
796 private TrackEventListTableModel trackModel;
800 private TickPositionModel tickPositionModel = new TickPositionModel();
804 private MidiEvent selectedMidiEvent = null;
808 private int selectedIndex = -1;
812 private long currentTick = 0;
814 * 上書きして削除対象にする変更前イベント(null可)
816 private MidiEvent[] midiEventsToBeOverwritten;
818 * 選択したイベントを入力ダイアログなどに反映します。
819 * @param model 対象データモデル
821 private void setSelectedEvent(TrackEventListTableModel trackModel) {
822 this.trackModel = trackModel;
823 SequenceTrackListTableModel sequenceTableModel = trackModel.sequenceTrackListTableModel;
824 int ppq = sequenceTableModel.getSequence().getResolution();
825 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
826 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
828 selectedIndex = trackModel.eventSelectionModel.getMinSelectionIndex();
829 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
830 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
831 tickPositionModel.setTickPosition(currentTick);
833 public void setupForEdit(TrackEventListTableModel trackModel) {
834 MidiEvent partnerEvent = null;
835 eventDialog.midiMessageForm.setMessage(
836 selectedMidiEvent.getMessage(),
837 trackModel.sequenceTrackListTableModel.charset
839 if( eventDialog.midiMessageForm.isNote() ) {
840 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
841 if( partnerIndex < 0 ) {
842 eventDialog.midiMessageForm.durationForm.setDuration(0);
845 partnerEvent = trackModel.getMidiEvent(partnerIndex);
846 long partnerTick = partnerEvent.getTick();
847 long duration = currentTick > partnerTick ?
848 currentTick - partnerTick : partnerTick - currentTick ;
849 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
852 if(partnerEvent == null)
853 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
855 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
857 private Action jumpEventAction = new AbstractAction() {
858 { putValue(NAME,"Jump"); }
859 public void actionPerformed(ActionEvent e) {
860 long tick = tickPositionModel.getTickPosition();
861 scrollToEventAt(tick);
862 eventDialog.setVisible(false);
866 private Action pasteEventAction = new AbstractAction() {
867 { putValue(NAME,"Paste"); }
868 public void actionPerformed(ActionEvent e) {
869 long tick = tickPositionModel.getTickPosition();
870 clipBoard.paste(trackModel, tick);
871 scrollToEventAt(tick);
872 // ペーストで曲の長さが変わったことをプレイリストに通知
873 SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
874 seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
875 eventDialog.setVisible(false);
879 private boolean applyEvent() {
880 long tick = tickPositionModel.getTickPosition();
881 MidiMessageForm form = eventDialog.midiMessageForm;
882 SequenceTrackListTableModel seqModel = trackModel.sequenceTrackListTableModel;
883 MidiEvent newMidiEvent = new MidiEvent(form.getMessage(seqModel.charset), tick);
884 if( midiEventsToBeOverwritten != null ) {
885 // 上書き消去するための選択済イベントがあった場合
886 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
888 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
889 System.out.println("addMidiEvent failure");
892 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
893 ShortMessage sm = form.createPartnerMessage();
895 scrollToEventAt( tick );
897 int duration = form.durationForm.getDuration();
898 if( form.isNote(false) ) {
899 duration = -duration;
901 long partnerTick = tick + (long)duration;
902 if( partnerTick < 0L ) partnerTick = 0L;
903 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
904 if( ! trackModel.addMidiEvent(partner) ) {
905 System.out.println("addMidiEvent failure (note on/off partner message)");
907 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
910 seqModel.sequenceListTableModel.fireSequenceModified(seqModel);
911 eventDialog.setVisible(false);
915 private EventEditContext editContext = new EventEditContext();
917 * 指定のTick位置へジャンプするアクション
919 Action queryJumpEventAction = new AbstractAction() {
921 putValue(NAME,"Jump to ...");
924 public void actionPerformed(ActionEvent e) {
925 editContext.setSelectedEvent(getModel());
926 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
932 Action queryAddEventAction = new AbstractAction() {
934 putValue(NAME,"New");
937 public void actionPerformed(ActionEvent e) {
938 TrackEventListTableModel model = getModel();
939 editContext.setSelectedEvent(model);
940 editContext.midiEventsToBeOverwritten = null;
941 eventDialog.openEventForm(
943 eventCellEditor.applyEventAction,
949 * MIDIイベントのコピー&ペーストを行うためのクリップボード
951 private class LocalClipBoard {
952 private MidiEvent copiedEventsToPaste[];
953 private int copiedEventsPPQ = 0;
954 public void copy(TrackEventListTableModel model, boolean withRemove) {
955 copiedEventsToPaste = model.getSelectedMidiEvents();
956 copiedEventsPPQ = model.sequenceTrackListTableModel.getSequence().getResolution();
957 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
958 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
959 queryPasteEventAction.setEnabled(en);
961 public void cut(TrackEventListTableModel model) {copy(model,true);}
962 public void copy(TrackEventListTableModel model){copy(model,false);}
963 public void paste(TrackEventListTableModel model, long tick) {
964 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
967 private LocalClipBoard clipBoard = new LocalClipBoard();
969 * 指定のTick位置へ貼り付けるアクション
971 Action queryPasteEventAction = new AbstractAction() {
973 putValue(NAME,"Paste to ...");
976 public void actionPerformed(ActionEvent e) {
977 editContext.setSelectedEvent(getModel());
978 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
984 public Action cutEventAction = new AbstractAction("Cut") {
989 public void actionPerformed(ActionEvent e) {
990 TrackEventListTableModel model = getModel();
991 if( ! confirm("Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?"))
993 clipBoard.cut(model);
999 public Action copyEventAction = new AbstractAction("Copy") {
1004 public void actionPerformed(ActionEvent e) {
1005 clipBoard.copy(getModel());
1011 public Action deleteEventAction = new AbstractAction("Delete", deleteIcon) {
1016 public void actionPerformed(ActionEvent e) {
1017 TrackEventListTableModel model = getModel();
1018 if( ! confirm("Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?"))
1020 model.removeSelectedMidiEvents();
1026 private MidiEventCellEditor eventCellEditor;
1030 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
1032 * MIDIイベントセルエディタを構築します。
1034 public MidiEventCellEditor() {
1035 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
1036 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
1037 int index = TrackEventListTableModel.Column.MESSAGE.ordinal();
1038 getColumnModel().getColumn(index).setCellEditor(this);
1041 * セルをダブルクリックしないと編集できないようにします。
1042 * @param e イベント(マウスイベント)
1043 * @return 編集可能になったらtrue
1046 public boolean isCellEditable(EventObject e) {
1047 if( ! (e instanceof MouseEvent) ) return super.isCellEditable(e);
1048 return ((MouseEvent)e).getClickCount() == 2;
1051 public Object getCellEditorValue() { return null; }
1053 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
1055 private ComponentListener dialogComponentListener = new ComponentAdapter() {
1057 public void componentHidden(ComponentEvent e) {
1058 fireEditingCanceled();
1060 eventDialog.removeComponentListener(this);
1066 private Action editEventAction = new AbstractAction() {
1067 public void actionPerformed(ActionEvent e) {
1068 TrackEventListTableModel model = getModel();
1069 editContext.setSelectedEvent(model);
1070 if( editContext.selectedMidiEvent == null )
1072 editContext.setupForEdit(model);
1073 eventDialog.addComponentListener(dialogComponentListener);
1074 eventDialog.openEventForm("Change MIDI event", applyEventAction);
1080 private JButton editEventButton = new JButton(editEventAction){{
1081 setHorizontalAlignment(JButton.LEFT);
1084 public Component getTableCellEditorComponent(
1085 JTable table, Object value, boolean isSelected, int row, int column
1087 editEventButton.setText(value.toString());
1088 return editEventButton;
1091 * 入力したイベントを反映するアクション
1093 private Action applyEventAction = new AbstractAction() {
1095 putValue(NAME,"OK");
1097 public void actionPerformed(ActionEvent e) {
1098 if( editContext.applyEvent() ) fireEditingStopped();
1103 * スクロール可能なMIDIイベントテーブルビュー
1105 private JScrollPane scrollPane = new JScrollPane(this);
1107 * 指定の MIDI tick のイベントへスクロールします。
1108 * @param tick MIDI tick
1110 public void scrollToEventAt(long tick) {
1111 int index = getModel().tickToIndex(tick);
1112 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
1113 getSelectionModel().setSelectionInterval(index, index);
1118 * 新しい {@link MidiSequenceEditor} を構築します。
1119 * @param playlistTableModel このエディタが参照するプレイリストモデル
1120 * @param outputMidiDevice イベントテーブルの操作音出力先MIDIデバイス
1122 public MidiSequenceEditor(PlaylistTableModel playlistTableModel, VirtualMidiDevice outputMidiDevice) {
1123 this.outputMidiDevice = outputMidiDevice;
1124 sequenceListTable = new SequenceListTable(playlistTableModel);
1125 trackListTable = new TrackListTable(
1126 new SequenceTrackListTableModel(sequenceListTable.getModel(), null, null)
1128 eventListTable = new EventListTable(new TrackEventListTableModel(trackListTable.getModel(), null));
1129 newSequenceDialog = new NewSequenceDialog(playlistTableModel, outputMidiDevice);
1130 setTitle("MIDI Editor/Playlist - MIDI Chord Helper");
1131 setBounds( 150, 200, 900, 500 );
1132 setLayout(new FlowLayout());
1133 new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, dropTargetListener, true);
1136 JPanel playlistPanel = new JPanel() {{
1137 JPanel playlistOperationPanel = new JPanel() {{
1138 setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
1139 add(Box.createRigidArea(new Dimension(10, 0)));
1140 add(new JButton(newSequenceDialog.openAction) {{ setMargin(ZERO_INSETS); }});
1141 if( sequenceListTable.midiFileChooser != null ) {
1142 add( Box.createRigidArea(new Dimension(5, 0)) );
1143 add(new JButton(sequenceListTable.midiFileChooser.openMidiFileAction) {{
1144 setMargin(ZERO_INSETS);
1147 if(sequenceListTable.base64EncodeAction != null) {
1148 add(Box.createRigidArea(new Dimension(5, 0)));
1149 add(new JButton(sequenceListTable.base64EncodeAction) {{ setMargin(ZERO_INSETS); }});
1151 add(Box.createRigidArea(new Dimension(5, 0)));
1152 PlaylistTableModel playlistTableModel = sequenceListTable.getModel();
1153 add(new JButton(playlistTableModel.moveToTopAction) {{ setMargin(ZERO_INSETS); }});
1154 add(Box.createRigidArea(new Dimension(5, 0)));
1155 add(new JButton(playlistTableModel.moveToBottomAction) {{ setMargin(ZERO_INSETS); }});
1156 if( sequenceListTable.midiFileChooser != null ) {
1157 add(Box.createRigidArea(new Dimension(5, 0)));
1158 add(new JButton(sequenceListTable.midiFileChooser.saveMidiFileAction) {{
1159 setMargin(ZERO_INSETS);
1162 add( Box.createRigidArea(new Dimension(5, 0)) );
1163 add(new JButton(sequenceListTable.deleteSequenceAction) {{ setMargin(ZERO_INSETS); }});
1164 add( Box.createRigidArea(new Dimension(5, 0)) );
1165 add(new SequencerSpeedSlider(playlistTableModel.sequencerModel.speedSliderModel));
1166 add( Box.createRigidArea(new Dimension(5, 0)) );
1168 MidiSequencerModel sequencerModel = sequenceListTable.getModel().sequencerModel;
1169 add(new JLabel("SyncMode:"));
1170 add(new JLabel("Master"));
1171 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.masterSyncModeModel));
1172 add(new JLabel("Slave"));
1173 add(new JComboBox<Sequencer.SyncMode>(sequencerModel.slaveSyncModeModel));
1176 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1177 add(new JScrollPane(sequenceListTable));
1178 add(Box.createRigidArea(new Dimension(0, 10)));
1179 add(playlistOperationPanel);
1180 add(Box.createRigidArea(new Dimension(0, 10)));
1182 JPanel trackListPanel = new JPanel() {{
1183 JPanel trackListOperationPanel = new JPanel() {{
1184 add(new JButton(trackListTable.addTrackAction) {{ setMargin(ZERO_INSETS); }});
1185 add(new JButton(trackListTable.deleteTrackAction) {{ setMargin(ZERO_INSETS); }});
1187 setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
1188 add(trackListTable.titleLabel);
1189 add(Box.createRigidArea(new Dimension(0, 5)));
1190 add(new JScrollPane(trackListTable));
1191 add(Box.createRigidArea(new Dimension(0, 5)));
1192 add(trackListOperationPanel);
1194 JPanel eventListPanel = new JPanel() {{
1195 JPanel eventListOperationPanel = new JPanel() {{
1196 add(new JCheckBox("Pair NoteON/OFF") {{
1197 setModel(eventListTable.pairNoteOnOffModel);
1198 setToolTipText("NoteON/OFFをペアで同時選択する");
1200 add(new JButton(eventListTable.queryJumpEventAction) {{ setMargin(ZERO_INSETS); }});
1201 add(new JButton(eventListTable.queryAddEventAction) {{ setMargin(ZERO_INSETS); }});
1202 add(new JButton(eventListTable.copyEventAction) {{ setMargin(ZERO_INSETS); }});
1203 add(new JButton(eventListTable.cutEventAction) {{ setMargin(ZERO_INSETS); }});
1204 add(new JButton(eventListTable.queryPasteEventAction) {{ setMargin(ZERO_INSETS); }});
1205 add(new JButton(eventListTable.deleteEventAction) {{ setMargin(ZERO_INSETS); }});
1207 setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
1208 add(eventListTable.titleLabel);
1209 add(eventListTable.scrollPane);
1210 add(eventListOperationPanel);
1212 Container cp = getContentPane();
1213 cp.setLayout(new BoxLayout(cp, BoxLayout.Y_AXIS));
1214 cp.add(Box.createVerticalStrut(2));
1216 new JSplitPane(JSplitPane.VERTICAL_SPLIT, playlistPanel,
1217 new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, trackListPanel, eventListPanel) {{
1218 setDividerLocation(300);
1221 setDividerLocation(160);