1 package camidion.chordhelper.midieditor;
3 import java.awt.Component;
4 import java.awt.event.ActionEvent;
5 import java.awt.event.ComponentAdapter;
6 import java.awt.event.ComponentEvent;
7 import java.awt.event.ComponentListener;
8 import java.awt.event.MouseEvent;
9 import java.util.Arrays;
10 import java.util.EventObject;
12 import javax.sound.midi.MidiChannel;
13 import javax.sound.midi.MidiEvent;
14 import javax.sound.midi.MidiMessage;
15 import javax.sound.midi.ShortMessage;
16 import javax.swing.AbstractAction;
17 import javax.swing.AbstractCellEditor;
18 import javax.swing.Action;
19 import javax.swing.JButton;
20 import javax.swing.JComponent;
21 import javax.swing.JLabel;
22 import javax.swing.JOptionPane;
23 import javax.swing.JScrollPane;
24 import javax.swing.JTable;
25 import javax.swing.JToggleButton;
26 import javax.swing.table.TableCellEditor;
27 import javax.swing.table.TableModel;
29 import camidion.chordhelper.ChordHelperApplet;
30 import camidion.chordhelper.mididevice.VirtualMidiDevice;
33 * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
35 public class MidiEventTable extends JTable {
37 * 新しいイベントリストテーブルを構築します。
38 * <p>データモデルとして一つのトラックのイベントリストを指定できます。
39 * トラックを切り替えたいときは {@link #setModel(TableModel)}
40 * でデータモデルを異なるトラックのものに切り替えます。
43 * @param model トラック(イベントリスト)データモデル
44 * @param eventDialog MIDIイベントダイアログ
45 * @param outputMidiDevice 操作音出力先MIDIデバイス
47 public MidiEventTable(MidiEventTableModel model, MidiEventDialog eventDialog, VirtualMidiDevice outputMidiDevice) {
49 this.outputMidiDevice = outputMidiDevice;
50 this.eventDialog = eventDialog;
51 titleLabel = new TitleLabel();
52 Arrays.stream(MidiEventTableModel.Column.values()).forEach(c->
53 getColumnModel().getColumn(c.ordinal()).setPreferredWidth(c.preferredWidth)
55 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
57 addItemListener(e->eventDialog.midiMessageForm.durationForm.setEnabled(isSelected()));
61 eventCellEditor = new MidiEventCellEditor();
62 setAutoCreateColumnsFromModel(false);
63 selectionModel.addListSelectionListener(event->{
64 if( event.getValueIsAdjusting() ) return;
65 if( selectionModel.isSelectionEmpty() ) {
66 queryPasteEventAction.setEnabled(false);
67 copyEventAction.setEnabled(false);
68 deleteEventAction.setEnabled(false);
69 cutEventAction.setEnabled(false);
72 copyEventAction.setEnabled(true);
73 deleteEventAction.setEnabled(true);
74 cutEventAction.setEnabled(true);
75 int minIndex = selectionModel.getMinSelectionIndex();
76 MidiEvent midiEvent = model.getMidiEvent(minIndex);
77 if( midiEvent != null ) {
78 MidiMessage msg = midiEvent.getMessage();
79 if( msg instanceof ShortMessage ) {
80 ShortMessage sm = (ShortMessage)msg;
81 int cmd = sm.getCommand();
82 if( cmd == 0x80 || cmd == 0x90 || cmd == 0xA0 ) {
84 MidiChannel outMidiChannels[] = outputMidiDevice.getChannels();
85 int ch = sm.getChannel();
86 int note = sm.getData1();
87 int vel = sm.getData2();
88 outMidiChannels[ch].noteOn(note, vel);
89 outMidiChannels[ch].noteOff(note, vel);
93 if( pairNoteOnOffModel.isSelected() ) {
94 int maxIndex = selectionModel.getMaxSelectionIndex();
96 for( int i=minIndex; i<=maxIndex; i++ ) {
97 if( ! selectionModel.isSelectedIndex(i) ) continue;
98 partnerIndex = model.getIndexOfPartnerFor(i);
99 if( partnerIndex >= 0 && ! selectionModel.isSelectedIndex(partnerIndex) )
100 selectionModel.addSelectionInterval(partnerIndex, partnerIndex);
107 * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
109 private MidiEventDialog eventDialog;
113 private VirtualMidiDevice outputMidiDevice;
115 * このテーブルビューが表示するデータを提供するトラック(イベントリスト)データモデルを返します。
116 * @return トラック(イベントリスト)データモデル
119 public MidiEventTableModel getModel() {
120 return (MidiEventTableModel) dataModel;
123 * このテーブルビューが表示するデータを提供するトラック(イベントリスト)データモデルを設定します。
124 * @param model トラック(イベントリスト)データモデル
126 public void setModel(MidiEventTableModel model) {
127 if( dataModel == model ) return;
128 if( model == null ) {
129 model = getModel().getParent().getParent().emptyEventListTableModel;
130 queryJumpEventAction.setEnabled(false);
131 queryAddEventAction.setEnabled(false);
133 queryPasteEventAction.setEnabled(false);
134 copyEventAction.setEnabled(false);
135 deleteEventAction.setEnabled(false);
136 cutEventAction.setEnabled(false);
139 queryJumpEventAction.setEnabled(true);
140 queryAddEventAction.setEnabled(true);
142 super.setModel(model);
147 TitleLabel titleLabel;
149 * 親テーブルの選択トラックの変更に反応する
152 class TitleLabel extends JLabel {
153 private static final String TITLE = "MIDI Events";
154 private TitleLabel() { super(TITLE); }
155 void showTrackNumber(int index) {
157 if( index >= 0 ) text = String.format(TITLE+" - track #%d", index);
162 * Pair noteON/OFF トグルボタンモデル
164 JToggleButton.ToggleButtonModel pairNoteOnOffModel;
165 private class EventEditContext {
169 private MidiEventTableModel trackModel;
173 private TickPositionModel tickPositionModel = new TickPositionModel();
177 private MidiEvent selectedMidiEvent = null;
181 private int selectedIndex = -1;
185 private long currentTick = 0;
187 * 上書きして削除対象にする変更前イベント(null可)
189 private MidiEvent[] midiEventsToBeOverwritten;
191 * 選択したイベントを入力ダイアログなどに反映します。
192 * @param model 対象データモデル
194 private void setSelectedEvent(MidiEventTableModel trackModel) {
195 this.trackModel = trackModel;
196 SequenceTrackListTableModel sequenceTableModel = trackModel.getParent();
197 int ppq = sequenceTableModel.getSequence().getResolution();
198 eventDialog.midiMessageForm.durationForm.setPPQ(ppq);
199 tickPositionModel.setSequenceIndex(sequenceTableModel.getSequenceTickIndex());
201 selectedIndex = selectionModel.getMinSelectionIndex();
202 selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
203 currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
204 tickPositionModel.setTickPosition(currentTick);
206 public void setupForEdit(MidiEventTableModel trackModel) {
207 MidiEvent partnerEvent = null;
208 eventDialog.midiMessageForm.setMessage(
209 selectedMidiEvent.getMessage(),
210 trackModel.getParent().getCharset()
212 if( eventDialog.midiMessageForm.isNote() ) {
213 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
214 if( partnerIndex < 0 ) {
215 eventDialog.midiMessageForm.durationForm.setDuration(0);
218 partnerEvent = trackModel.getMidiEvent(partnerIndex);
219 long partnerTick = partnerEvent.getTick();
220 long duration = currentTick > partnerTick ?
221 currentTick - partnerTick : partnerTick - currentTick ;
222 eventDialog.midiMessageForm.durationForm.setDuration((int)duration);
225 if(partnerEvent == null)
226 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
228 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
230 private Action jumpEventAction = new AbstractAction() {
231 { putValue(NAME,"Jump"); }
232 public void actionPerformed(ActionEvent e) {
233 long tick = tickPositionModel.getTickPosition();
234 scrollToEventAt(tick);
235 eventDialog.setVisible(false);
239 private Action pasteEventAction = new AbstractAction() {
240 { putValue(NAME,"Paste"); }
241 public void actionPerformed(ActionEvent e) {
242 long tick = tickPositionModel.getTickPosition();
243 clipBoard.paste(trackModel, tick);
244 scrollToEventAt(tick);
245 // ペーストされたので変更フラグを立てる(曲の長さが変わるが、それも自動的にプレイリストに通知される)
246 SequenceTrackListTableModel seqModel = trackModel.getParent();
247 seqModel.setModified(true);
248 eventDialog.setVisible(false);
252 private boolean applyEvent() {
253 long tick = tickPositionModel.getTickPosition();
254 MidiMessageForm form = eventDialog.midiMessageForm;
255 SequenceTrackListTableModel seqModel = trackModel.getParent();
256 MidiMessage msg = form.getMessage(seqModel.getCharset());
260 MidiEvent newMidiEvent = new MidiEvent(msg, tick);
261 if( midiEventsToBeOverwritten != null ) {
262 // 上書き消去するための選択済イベントがあった場合
263 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
265 if( ! trackModel.addMidiEvent(newMidiEvent) ) {
266 System.out.println("addMidiEvent failure");
269 if(pairNoteOnOffModel.isSelected() && form.isNote()) {
270 ShortMessage sm = form.createPartnerMessage();
272 scrollToEventAt( tick );
274 int duration = form.durationForm.getDuration();
275 if( form.isNote(false) ) {
276 duration = -duration;
278 long partnerTick = tick + (long)duration;
279 if( partnerTick < 0L ) partnerTick = 0L;
280 MidiEvent partner = new MidiEvent((MidiMessage)sm, partnerTick);
281 if( ! trackModel.addMidiEvent(partner) ) {
282 System.out.println("addMidiEvent failure (note on/off partner message)");
284 scrollToEventAt(partnerTick > tick ? partnerTick : tick);
287 seqModel.setModified(true);
288 eventDialog.setVisible(false);
292 private EventEditContext editContext = new EventEditContext();
294 * 指定のTick位置へジャンプするアクション
296 Action queryJumpEventAction = new AbstractAction() {
298 putValue(NAME,"Jump to ...");
301 public void actionPerformed(ActionEvent e) {
302 editContext.setSelectedEvent(getModel());
303 eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
309 Action queryAddEventAction = new AbstractAction() {
311 putValue(NAME,"New");
314 public void actionPerformed(ActionEvent e) {
315 MidiEventTableModel model = getModel();
316 editContext.setSelectedEvent(model);
317 editContext.midiEventsToBeOverwritten = null;
318 eventDialog.openEventForm("New MIDI event", eventCellEditor.applyEventAction, model.getChannel());
322 * MIDIイベントのコピー&ペーストを行うためのクリップボード
324 private class LocalClipBoard {
325 private MidiEvent copiedEventsToPaste[];
326 private int copiedEventsPPQ = 0;
327 public void copy(MidiEventTableModel model, boolean withRemove) {
328 copiedEventsToPaste = model.getSelectedMidiEvents(selectionModel);
329 copiedEventsPPQ = model.getParent().getSequence().getResolution();
330 if( withRemove ) model.removeMidiEvents(copiedEventsToPaste);
331 boolean en = (copiedEventsToPaste != null && copiedEventsToPaste.length > 0);
332 queryPasteEventAction.setEnabled(en);
334 public void cut(MidiEventTableModel model) {copy(model,true);}
335 public void copy(MidiEventTableModel model){copy(model,false);}
336 public void paste(MidiEventTableModel model, long tick) {
337 model.addMidiEvents(copiedEventsToPaste, tick, copiedEventsPPQ);
340 private LocalClipBoard clipBoard = new LocalClipBoard();
342 * 指定のTick位置へ貼り付けるアクション
344 Action queryPasteEventAction = new AbstractAction() {
346 putValue(NAME,"Paste to ...");
349 public void actionPerformed(ActionEvent e) {
350 editContext.setSelectedEvent(getModel());
351 eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
357 public Action cutEventAction = new AbstractAction("Cut") {
358 private static final String CONFIRM_MESSAGE =
359 "Do you want to cut selected event ?\n選択したMIDIイベントを切り取りますか?";
360 { setEnabled(false); }
362 public void actionPerformed(ActionEvent event) {
363 if( JOptionPane.showConfirmDialog(
364 ((JComponent)event.getSource()).getRootPane(),
366 ChordHelperApplet.VersionInfo.NAME,
367 JOptionPane.YES_NO_OPTION,
368 JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION
369 ) clipBoard.cut(getModel());
375 public Action copyEventAction = new AbstractAction("Copy") {
376 { setEnabled(false); }
378 public void actionPerformed(ActionEvent e) { clipBoard.copy(getModel()); }
383 public Action deleteEventAction = new AbstractAction("Delete", MidiSequenceEditorDialog.deleteIcon) {
384 private static final String CONFIRM_MESSAGE =
385 "Do you want to delete selected event ?\n選択したMIDIイベントを削除しますか?";
386 { setEnabled(false); }
388 public void actionPerformed(ActionEvent event) {
389 if( JOptionPane.showConfirmDialog(
390 ((JComponent)event.getSource()).getRootPane(),
392 ChordHelperApplet.VersionInfo.NAME,
393 JOptionPane.YES_NO_OPTION,
394 JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION
396 getModel().removeMidiEvents(getModel().getSelectedMidiEvents(selectionModel));
403 private MidiEventCellEditor eventCellEditor;
407 class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
409 * MIDIイベントセルエディタを構築します。
411 public MidiEventCellEditor() {
412 eventDialog.midiMessageForm.setOutputMidiChannels(outputMidiDevice.getChannels());
413 eventDialog.tickPositionInputForm.setModel(editContext.tickPositionModel);
414 int index = MidiEventTableModel.Column.MESSAGE.ordinal();
415 getColumnModel().getColumn(index).setCellEditor(this);
418 * セルをダブルクリックしないと編集できないようにします。
419 * @param e イベント(マウスイベント)
420 * @return 編集可能になったらtrue
423 public boolean isCellEditable(EventObject e) {
424 return (e instanceof MouseEvent) ?
425 ((MouseEvent)e).getClickCount() == 2 : super.isCellEditable(e);
428 public Object getCellEditorValue() { return null; }
430 * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
432 private ComponentListener dialogComponentListener = new ComponentAdapter() {
434 public void componentHidden(ComponentEvent e) {
435 fireEditingCanceled();
437 eventDialog.removeComponentListener(this);
443 private Action editEventAction = new AbstractAction() {
444 public void actionPerformed(ActionEvent e) {
445 MidiEventTableModel model = getModel();
446 editContext.setSelectedEvent(model);
447 if( editContext.selectedMidiEvent == null )
449 editContext.setupForEdit(model);
450 eventDialog.addComponentListener(dialogComponentListener);
451 eventDialog.openEventForm("Change MIDI event", applyEventAction);
457 private JButton editEventButton = new JButton(editEventAction){{
458 setHorizontalAlignment(JButton.LEFT);
461 public Component getTableCellEditorComponent(
462 JTable table, Object value, boolean isSelected, int row, int column
464 editEventButton.setText(value.toString());
465 return editEventButton;
470 private Action applyEventAction = new AbstractAction() {
471 { putValue(NAME,"OK"); }
472 public void actionPerformed(ActionEvent e) {
473 if( editContext.applyEvent() ) fireEditingStopped();
478 * スクロール可能なMIDIイベントテーブルビュー
480 JScrollPane scrollPane = new JScrollPane(this);
482 * 指定の MIDI tick のイベントへスクロールします。
483 * @param tick MIDI tick
485 public void scrollToEventAt(long tick) {
486 int index = getModel().tickToIndex(tick);
487 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
488 getSelectionModel().setSelectionInterval(index, index);