OSDN Git Service

・シーケンス削除後プレイリストが空になるときIndexOB例外が発生していた問題の修正
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / MidiEventTable.java
1 package camidion.chordhelper.midieditor;
2
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;
11
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;
28
29 import camidion.chordhelper.ChordHelperApplet;
30 import camidion.chordhelper.mididevice.VirtualMidiDevice;
31
32 /**
33  * MIDIイベントリストテーブルビュー(選択中のトラックの中身)
34  */
35 public class MidiEventTable extends JTable {
36         /**
37          * 新しいイベントリストテーブルを構築します。
38          * <p>データモデルとして一つのトラックのイベントリストを指定できます。
39          * トラックを切り替えたいときは {@link #setModel(TableModel)}
40          * でデータモデルを異なるトラックのものに切り替えます。
41          * </p>
42          *
43          * @param model トラック(イベントリスト)データモデル
44          * @param eventDialog MIDIイベントダイアログ
45          * @param outputMidiDevice 操作音出力先MIDIデバイス
46          */
47         public MidiEventTable(MidiEventTableModel model, MidiEventDialog eventDialog, VirtualMidiDevice outputMidiDevice) {
48                 super(model);
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)
54                 );
55                 pairNoteOnOffModel = new JToggleButton.ToggleButtonModel() {
56                         {
57                                 addItemListener(e->eventDialog.midiMessageForm.durationForm.setEnabled(isSelected()));
58                                 setSelected(true);
59                         }
60                 };
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);
70                         }
71                         else {
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 ) {
83                                                         // ノート番号を持つ場合、音を鳴らす。
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);
90                                                 }
91                                         }
92                                 }
93                                 if( pairNoteOnOffModel.isSelected() ) {
94                                         int maxIndex = selectionModel.getMaxSelectionIndex();
95                                         int partnerIndex;
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);
101                                         }
102                                 }
103                         }
104                 });
105         }
106         /**
107          * MIDIイベント入力ダイアログ(イベント入力とイベント送出で共用)
108          */
109         private MidiEventDialog eventDialog;
110         /**
111          * 操作音を鳴らすMIDI出力デバイス
112          */
113         private VirtualMidiDevice outputMidiDevice;
114         /**
115          * このテーブルビューが表示するデータを提供するトラック(イベントリスト)データモデルを返します。
116          * @return トラック(イベントリスト)データモデル
117          */
118         @Override
119         public MidiEventTableModel getModel() {
120                 return (MidiEventTableModel) dataModel;
121         }
122         /**
123          * このテーブルビューが表示するデータを提供するトラック(イベントリスト)データモデルを設定します。
124          * @param model トラック(イベントリスト)データモデル
125          */
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);
132
133                         queryPasteEventAction.setEnabled(false);
134                         copyEventAction.setEnabled(false);
135                         deleteEventAction.setEnabled(false);
136                         cutEventAction.setEnabled(false);
137                 }
138                 else {
139                         queryJumpEventAction.setEnabled(true);
140                         queryAddEventAction.setEnabled(true);
141                 }
142                 super.setModel(model);
143         }
144         /**
145          * タイトルラベル
146          */
147         TitleLabel titleLabel;
148         /**
149          * 親テーブルの選択トラックの変更に反応する
150          * トラック番号つきタイトルラベル
151          */
152         class TitleLabel extends JLabel {
153                 private static final String TITLE = "MIDI Events";
154                 private TitleLabel() { super(TITLE); }
155                 void showTrackNumber(int index) {
156                         String text = TITLE;
157                         if( index >= 0 ) text = String.format(TITLE+" - track #%d", index);
158                         setText(text);
159                 }
160         }
161         /**
162          * Pair noteON/OFF トグルボタンモデル
163          */
164         JToggleButton.ToggleButtonModel pairNoteOnOffModel;
165         private class EventEditContext {
166                 /**
167                  * 編集対象トラック
168                  */
169                 private MidiEventTableModel trackModel;
170                 /**
171                  * tick位置入力モデル
172                  */
173                 private TickPositionModel tickPositionModel = new TickPositionModel();
174                 /**
175                  * 選択されたイベント
176                  */
177                 private MidiEvent selectedMidiEvent = null;
178                 /**
179                  * 選択されたイベントの場所
180                  */
181                 private int selectedIndex = -1;
182                 /**
183                  * 選択されたイベントのtick位置
184                  */
185                 private long currentTick = 0;
186                 /**
187                  * 上書きして削除対象にする変更前イベント(null可)
188                  */
189                 private MidiEvent[] midiEventsToBeOverwritten;
190                 /**
191                  * 選択したイベントを入力ダイアログなどに反映します。
192                  * @param model 対象データモデル
193                  */
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());
200
201                         selectedIndex = getSelectedRow();
202                         selectedMidiEvent = selectedIndex < 0 ? null : trackModel.getMidiEvent(selectedIndex);
203                         currentTick = selectedMidiEvent == null ? 0 : selectedMidiEvent.getTick();
204                         tickPositionModel.setTickPosition(currentTick);
205                 }
206                 public void setupForEdit(MidiEventTableModel trackModel) {
207                         MidiEvent partnerEvent = null;
208                         eventDialog.midiMessageForm.setMessage(
209                                 selectedMidiEvent.getMessage(),
210                                 trackModel.getParent().getCharset()
211                         );
212                         if( eventDialog.midiMessageForm.isNote() ) {
213                                 int partnerIndex = trackModel.getIndexOfPartnerFor(selectedIndex);
214                                 if( partnerIndex < 0 ) {
215                                         eventDialog.midiMessageForm.durationForm.setDuration(0);
216                                 }
217                                 else {
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);
223                                 }
224                         }
225                         if(partnerEvent == null)
226                                 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent};
227                         else
228                                 midiEventsToBeOverwritten = new MidiEvent[] {selectedMidiEvent, partnerEvent};
229                 }
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);
236                                 trackModel = null;
237                         }
238                 };
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);
249                                 trackModel = null;
250                         }
251                 };
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());
257                         if( msg == null ) {
258                                 return false;
259                         }
260                         MidiEvent newMidiEvent = new MidiEvent(msg, tick);
261                         if( midiEventsToBeOverwritten != null ) {
262                                 // 上書き消去するための選択済イベントがあった場合
263                                 trackModel.removeMidiEvents(midiEventsToBeOverwritten);
264                         }
265                         if( ! trackModel.addMidiEvent(newMidiEvent) ) {
266                                 System.out.println("addMidiEvent failure");
267                                 return false;
268                         }
269                         if(pairNoteOnOffModel.isSelected() && form.isNote()) {
270                                 ShortMessage sm = form.createPartnerMessage();
271                                 if(sm == null)
272                                         scrollToEventAt( tick );
273                                 else {
274                                         int duration = form.durationForm.getDuration();
275                                         if( form.isNote(false) ) {
276                                                 duration = -duration;
277                                         }
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)");
283                                         }
284                                         scrollToEventAt(partnerTick > tick ? partnerTick : tick);
285                                 }
286                         }
287                         seqModel.setModified(true);
288                         eventDialog.setVisible(false);
289                         return true;
290                 }
291         }
292         private EventEditContext editContext = new EventEditContext();
293         /**
294          * 指定のTick位置へジャンプするアクション
295          */
296         Action queryJumpEventAction = new AbstractAction() {
297                 {
298                         putValue(NAME,"Jump to ...");
299                         setEnabled(false);
300                 }
301                 public void actionPerformed(ActionEvent e) {
302                         editContext.setSelectedEvent(getModel());
303                         eventDialog.openTickForm("Jump selection to", editContext.jumpEventAction);
304                 }
305         };
306         /**
307          * 新しいイベントの追加を行うアクション
308          */
309         Action queryAddEventAction = new AbstractAction() {
310                 {
311                         putValue(NAME,"New");
312                         setEnabled(false);
313                 }
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());
319                 }
320         };
321         /**
322          * MIDIイベントのコピー&ペーストを行うためのクリップボード
323          */
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);
333                 }
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);
338                 }
339         }
340         private LocalClipBoard clipBoard = new LocalClipBoard();
341         /**
342          * 指定のTick位置へ貼り付けるアクション
343          */
344         Action queryPasteEventAction = new AbstractAction() {
345                 {
346                         putValue(NAME,"Paste to ...");
347                         setEnabled(false);
348                 }
349                 public void actionPerformed(ActionEvent e) {
350                         editContext.setSelectedEvent(getModel());
351                         eventDialog.openTickForm("Paste to", editContext.pasteEventAction);
352                 }
353         };
354         /**
355          * イベントカットアクション
356          */
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); }
361                 @Override
362                 public void actionPerformed(ActionEvent event) {
363                         if( JOptionPane.showConfirmDialog(
364                                         ((JComponent)event.getSource()).getRootPane(),
365                                         CONFIRM_MESSAGE,
366                                         ChordHelperApplet.VersionInfo.NAME,
367                                         JOptionPane.YES_NO_OPTION,
368                                         JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION
369                         ) clipBoard.cut(getModel());
370                 }
371         };
372         /**
373          * イベントコピーアクション
374          */
375         public Action copyEventAction = new AbstractAction("Copy") {
376                 { setEnabled(false); }
377                 @Override
378                 public void actionPerformed(ActionEvent e) { clipBoard.copy(getModel()); }
379         };
380         /**
381          * イベント削除アクション
382          */
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); }
387                 @Override
388                 public void actionPerformed(ActionEvent event) {
389                         if( JOptionPane.showConfirmDialog(
390                                         ((JComponent)event.getSource()).getRootPane(),
391                                         CONFIRM_MESSAGE,
392                                         ChordHelperApplet.VersionInfo.NAME,
393                                         JOptionPane.YES_NO_OPTION,
394                                         JOptionPane.WARNING_MESSAGE) == JOptionPane.YES_OPTION
395                         ) {
396                                 getModel().removeMidiEvents(getModel().getSelectedMidiEvents(selectionModel));
397                         }
398                 }
399         };
400         /**
401          * MIDIイベント表のセルエディタ
402          */
403         private MidiEventCellEditor eventCellEditor;
404         /**
405          * MIDIイベント表のセルエディタ
406          */
407         class MidiEventCellEditor extends AbstractCellEditor implements TableCellEditor {
408                 /**
409                  * MIDIイベントセルエディタを構築します。
410                  */
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);
416                 }
417                 /**
418                  * セルをダブルクリックしないと編集できないようにします。
419                  * @param e イベント(マウスイベント)
420                  * @return 編集可能になったらtrue
421                  */
422                 @Override
423                 public boolean isCellEditable(EventObject e) {
424                         return (e instanceof MouseEvent) ?
425                                 ((MouseEvent)e).getClickCount() == 2 : super.isCellEditable(e);
426                 }
427                 @Override
428                 public Object getCellEditorValue() { return null; }
429                 /**
430                  * MIDIメッセージダイアログが閉じたときにセル編集を中止するリスナー
431                  */
432                 private ComponentListener dialogComponentListener = new ComponentAdapter() {
433                         @Override
434                         public void componentHidden(ComponentEvent e) {
435                                 fireEditingCanceled();
436                                 // 用が済んだら当リスナーを除去
437                                 eventDialog.removeComponentListener(this);
438                         }
439                 };
440                 /**
441                  * 既存イベントを編集するアクション
442                  */
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 )
448                                         return;
449                                 editContext.setupForEdit(model);
450                                 eventDialog.addComponentListener(dialogComponentListener);
451                                 eventDialog.openEventForm("Change MIDI event", applyEventAction);
452                         }
453                 };
454                 /**
455                  * イベント編集ボタン
456                  */
457                 private JButton editEventButton = new JButton(editEventAction){{
458                         setHorizontalAlignment(JButton.LEFT);
459                 }};
460                 @Override
461                 public Component getTableCellEditorComponent(
462                         JTable table, Object value, boolean isSelected, int row, int column
463                 ) {
464                         editEventButton.setText(value.toString());
465                         return editEventButton;
466                 }
467                 /**
468                  * 入力したイベントを反映するアクション
469                  */
470                 private Action applyEventAction = new AbstractAction() {
471                         { putValue(NAME,"OK"); }
472                         public void actionPerformed(ActionEvent e) {
473                                 if( editContext.applyEvent() ) fireEditingStopped();
474                         }
475                 };
476         }
477         /**
478          * スクロール可能なMIDIイベントテーブルビュー
479          */
480         JScrollPane scrollPane = new JScrollPane(this);
481         /**
482          * 指定の MIDI tick のイベントへスクロールします。
483          * @param tick MIDI tick
484          */
485         public void scrollToEventAt(long tick) {
486                 int index = getModel().tickToIndex(tick);
487                 scrollPane.getVerticalScrollBar().setValue(index * getRowHeight());
488                 getSelectionModel().setSelectionInterval(index, index);
489         }
490 }