OSDN Git Service

・プレイリストで曲選択クリア時にイベント一覧がクリアされない問題の改善
[midichordhelper/MIDIChordHelper.git] / src / camidion / chordhelper / midieditor / SequenceTrackListTableModel.java
1 package camidion.chordhelper.midieditor;
2
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.nio.charset.Charset;
6 import java.util.ArrayList;
7 import java.util.List;
8
9 import javax.sound.midi.MidiSystem;
10 import javax.sound.midi.Sequence;
11 import javax.sound.midi.Track;
12 import javax.swing.DefaultListSelectionModel;
13 import javax.swing.ListSelectionModel;
14 import javax.swing.table.AbstractTableModel;
15
16 import camidion.chordhelper.mididevice.MidiSequencerModel;
17 import camidion.chordhelper.music.MIDISpec;
18
19 /**
20  * MIDIシーケンス(トラックリスト)のテーブルデータモデル
21  */
22 public class SequenceTrackListTableModel extends AbstractTableModel {
23         /**
24          * 列の列挙型
25          */
26         public enum Column {
27                 TRACK_NUMBER("#", Integer.class, 20),
28                 EVENTS("Events", Integer.class, 40),
29                 MUTE("Mute", Boolean.class, 30),
30                 SOLO("Solo", Boolean.class, 30),
31                 RECORD_CHANNEL("RecCh", String.class, 40),
32                 CHANNEL("Ch", String.class, 30),
33                 TRACK_NAME("Track name", String.class, 100);
34                 String title;
35                 Class<?> columnClass;
36                 int preferredWidth;
37                 /**
38                  * 列の識別子を構築します。
39                  * @param title 列のタイトル
40                  * @param widthRatio 幅の割合
41                  * @param columnClass 列のクラス
42                  * @param perferredWidth 列の適切な幅
43                  */
44                 private Column(String title, Class<?> columnClass, int preferredWidth) {
45                         this.title = title;
46                         this.columnClass = columnClass;
47                         this.preferredWidth = preferredWidth;
48                 }
49         }
50         /**
51          * このモデルを収容している親のプレイリストを返します。
52          */
53         public PlaylistTableModel getParent() { return sequenceListTableModel; }
54         private PlaylistTableModel sequenceListTableModel;
55         /**
56          * ラップされたMIDIシーケンスのtickインデックス
57          */
58         private SequenceTickIndex sequenceTickIndex;
59         /**
60          * ファイル名を返します。
61          * @return ファイル名
62          */
63         public String getFilename() { return filename; }
64         private String filename;
65         /**
66          * ファイル名を設定します。
67          * @param filename ファイル名
68          */
69         public void setFilename(String filename) { this.filename = filename; }
70         /**
71          * タイトルや歌詞などで使うテキストの文字コードを返します。
72          * @return テキストの文字コード
73          */
74         public Charset getCharset() { return charset; }
75         private Charset charset = Charset.defaultCharset();
76         /**
77          * タイトルや歌詞などで使うテキストの文字コードを設定します。
78          * @param charset テキストの文字コード
79          */
80         public void setCharset(Charset charset) { this.charset = charset; }
81         /**
82          * トラックリスト
83          */
84         private List<TrackEventListTableModel> trackModelList = new ArrayList<>();
85         private ListSelectionModel selectionModel = new DefaultListSelectionModel(){
86                 {
87                         setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
88                 }
89         };
90         /**
91          * このトラックリストの選択モデルを返します。
92          */
93         public ListSelectionModel getSelectionModel() { return selectionModel; }
94         /**
95          * MIDIシーケンスとファイル名から {@link SequenceTrackListTableModel} を構築します。
96          * @param sequenceListTableModel 親のプレイリスト
97          * @param sequence MIDIシーケンス
98          * @param filename ファイル名
99          */
100         public SequenceTrackListTableModel(
101                 PlaylistTableModel sequenceListTableModel,
102                 Sequence sequence,
103                 String filename
104         ) {
105                 this.sequenceListTableModel = sequenceListTableModel;
106                 setSequence(sequence);
107                 setFilename(filename);
108         }
109         @Override
110         public int getRowCount() {
111                 return sequence == null ? 0 : sequence.getTracks().length;
112         }
113         @Override
114         public int getColumnCount() { return Column.values().length; }
115         /**
116          * 列名を返します。
117          * @return 列名
118          */
119         @Override
120         public String getColumnName(int column) {
121                 return Column.values()[column].title;
122         }
123         /**
124          * 指定された列の型を返します。
125          * @return 指定された列の型
126          */
127         @Override
128         public Class<?> getColumnClass(int column) {
129                 SequenceTrackListTableModel.Column c = Column.values()[column];
130                 switch(c) {
131                 case MUTE:
132                 case SOLO: if( ! isOnSequencer() ) return String.class;
133                         // FALLTHROUGH
134                 default: return c.columnClass;
135                 }
136         }
137         @Override
138         public Object getValueAt(int row, int column) {
139                 SequenceTrackListTableModel.Column c = Column.values()[column];
140                 switch(c) {
141                 case TRACK_NUMBER: return row;
142                 case EVENTS: return sequence.getTracks()[row].size();
143                 case MUTE:
144                         return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackMute(row) : "";
145                 case SOLO:
146                         return isOnSequencer() ? sequenceListTableModel.getSequencerModel().getSequencer().getTrackSolo(row) : "";
147                 case RECORD_CHANNEL:
148                         return isOnSequencer() ? trackModelList.get(row).getRecordingChannel() : "";
149                 case CHANNEL: {
150                         int ch = trackModelList.get(row).getChannel();
151                         return ch < 0 ? "" : ch + 1 ;
152                 }
153                 case TRACK_NAME: return trackModelList.get(row).toString();
154                 default: return "";
155                 }
156         }
157         /**
158          * セルが編集可能かどうかを返します。
159          */
160         @Override
161         public boolean isCellEditable(int row, int column) {
162                 SequenceTrackListTableModel.Column c = Column.values()[column];
163                 switch(c) {
164                 case MUTE:
165                 case SOLO:
166                 case RECORD_CHANNEL: return isOnSequencer();
167                 case CHANNEL:
168                 case TRACK_NAME: return true;
169                 default: return false;
170                 }
171         }
172         /**
173          * 列の値を設定します。
174          */
175         @Override
176         public void setValueAt(Object val, int row, int column) {
177                 SequenceTrackListTableModel.Column c = Column.values()[column];
178                 switch(c) {
179                 case MUTE:
180                         sequenceListTableModel.getSequencerModel().getSequencer().setTrackMute(row, ((Boolean)val).booleanValue());
181                         break;
182                 case SOLO:
183                         sequenceListTableModel.getSequencerModel().getSequencer().setTrackSolo(row, ((Boolean)val).booleanValue());
184                         break;
185                 case RECORD_CHANNEL:
186                         trackModelList.get(row).setRecordingChannel((String)val);
187                         break;
188                 case CHANNEL: {
189                         Integer ch;
190                         try {
191                                 ch = new Integer((String)val);
192                         }
193                         catch( NumberFormatException e ) {
194                                 ch = -1;
195                                 break;
196                         }
197                         if( --ch <= 0 || ch > MIDISpec.MAX_CHANNELS )
198                                 break;
199                         TrackEventListTableModel trackTableModel = trackModelList.get(row);
200                         if( ch == trackTableModel.getChannel() ) break;
201                         trackTableModel.setChannel(ch);
202                         setModified(true);
203                         fireTableCellUpdated(row, Column.EVENTS.ordinal());
204                         break;
205                 }
206                 case TRACK_NAME: trackModelList.get(row).setString((String)val); break;
207                 default: break;
208                 }
209                 fireTableCellUpdated(row,column);
210         }
211         /**
212          * MIDIシーケンスを返します。
213          * @return MIDIシーケンス
214          */
215         public Sequence getSequence() { return sequence; }
216         private Sequence sequence;
217         /**
218          * MIDIシーケンスのマイクロ秒単位の長さを返します。
219          * 曲が長すぎて {@link Sequence#getMicrosecondLength()} が負数を返してしまった場合の補正も行います。
220          * @return MIDIシーケンスの長さ[マイクロ秒]
221          */
222         public long getMicrosecondLength() {
223                 long usec = sequence.getMicrosecondLength();
224                 return usec < 0 ? usec += 0x100000000L : usec;
225         }
226         /**
227          * シーケンスtickインデックスを返します。
228          * @return シーケンスtickインデックス
229          */
230         public SequenceTickIndex getSequenceTickIndex() { return sequenceTickIndex; }
231         /**
232          * MIDIシーケンスを設定します。
233          * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
234          */
235         private void setSequence(Sequence sequence) {
236                 // 旧シーケンスの録音モードを解除
237                 MidiSequencerModel sequencerModel = sequenceListTableModel.getSequencerModel();
238                 if( sequencerModel != null ) sequencerModel.getSequencer().recordDisable(null);
239                 //
240                 // トラックリストをクリア
241                 int oldSize = trackModelList.size();
242                 if( oldSize > 0 ) {
243                         trackModelList.clear();
244                         fireTableRowsDeleted(0, oldSize-1);
245                 }
246                 // 新シーケンスに置き換える
247                 if( (this.sequence = sequence) == null ) {
248                         // 新シーケンスがない場合
249                         sequenceTickIndex = null;
250                         return;
251                 }
252                 // tickインデックスを再構築
253                 fireTimeSignatureChanged();
254                 //
255                 // トラックリストを再構築
256                 Track tracks[] = sequence.getTracks();
257                 for(Track track : tracks) {
258                         trackModelList.add(new TrackEventListTableModel(this, track));
259                 }
260                 // 文字コードの判定
261                 Charset cs = MIDISpec.getCharsetOf(sequence);
262                 charset = cs==null ? Charset.defaultCharset() : cs;
263                 //
264                 // トラックが挿入されたことを通知
265                 fireTableRowsInserted(0, tracks.length-1);
266         }
267         /**
268          * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
269          */
270         public void fireTimeSignatureChanged() {
271                 sequenceTickIndex = new SequenceTickIndex(sequence);
272         }
273         /**
274          * 変更されたかどうかを返します。
275          * @return 変更済みのときtrue
276          */
277         public boolean isModified() { return isModified; }
278         private boolean isModified = false;
279         /**
280          * 変更されたかどうかを設定します。
281          * @param isModified 変更されたときtrue
282          */
283         public void setModified(boolean isModified) {
284                 this.isModified = isModified;
285                 int index = sequenceListTableModel.getSequenceModelList().indexOf(this);
286                 if( index >= 0 ) sequenceListTableModel.fireTableRowsUpdated(index, index);
287         }
288         /**
289          * このシーケンスを表す文字列としてシーケンス名を返します。シーケンス名がない場合は空文字列を返します。
290          */
291         @Override
292         public String toString() {
293                 byte b[] = MIDISpec.getNameBytesOf(sequence);
294                 return b == null ? "" : new String(b, charset);
295         }
296
297         /**
298          * シーケンス名を設定します。
299          * @param name シーケンス名
300          * @return 成功したらtrue
301          */
302         public boolean setName(String name) {
303                 if( name.equals(toString()) ) return false;
304                 if( ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) ) return false;
305                 setModified(true);
306                 fireTableDataChanged();
307                 return true;
308         }
309         /**
310          * このシーケンスのMIDIデータのバイト列を返します。
311          * @return MIDIデータのバイト列(ない場合はnull)
312          * @throws IOException バイト列の出力に失敗した場合
313          */
314         public byte[] getMIDIdata() throws IOException {
315                 if( sequence == null || sequence.getTracks().length == 0 ) {
316                         return null;
317                 }
318                 try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
319                         MidiSystem.write(sequence, 1, out);
320                         return out.toByteArray();
321                 } catch ( IOException e ) {
322                         throw e;
323                 }
324         }
325         /**
326          * 指定のトラックが変更されたことを通知します。
327          * @param track トラック
328          */
329         public void fireTrackChanged(Track track) {
330                 int row = indexOf(track);
331                 if( row < 0 ) return;
332                 fireTableRowsUpdated(row, row);
333                 setModified(true);
334         }
335         /**
336          * 選択されているトラックモデルを返します。
337          * @param index トラックのインデックス
338          * @return トラックモデル(見つからない場合null)
339          */
340         public TrackEventListTableModel getSelectedTrackModel() {
341                 if( selectionModel.isSelectionEmpty() ) return null;
342                 Track tracks[] = sequence.getTracks();
343                 if( tracks.length == 0 ) return null;
344                 Track t = tracks[selectionModel.getMinSelectionIndex()];
345                 return trackModelList.stream().filter(tm -> tm.getTrack() == t).findFirst().orElse(null);
346         }
347         /**
348          * 指定のトラックがある位置のインデックスを返します。
349          * @param track トラック
350          * @return トラックのインデックス(先頭 0、トラックが見つからない場合 -1)
351          */
352         public int indexOf(Track track) {
353                 Track tracks[] = sequence.getTracks();
354                 for( int i=0; i<tracks.length; i++ ) if( tracks[i] == track ) return i;
355                 return -1;
356         }
357         /**
358          * 新しいトラックを生成し、末尾に追加します。
359          * @return 追加したトラックのインデックス(先頭 0)
360          */
361         public int createTrack() {
362                 trackModelList.add(new TrackEventListTableModel(this, sequence.createTrack()));
363                 setModified(true);
364                 int lastRow = getRowCount() - 1;
365                 fireTableRowsInserted(lastRow, lastRow);
366                 selectionModel.setSelectionInterval(lastRow, lastRow);
367                 return lastRow;
368         }
369         /**
370          * 選択されているトラックを削除します。
371          */
372         public void deleteSelectedTracks() {
373                 if( selectionModel.isSelectionEmpty() )
374                         return;
375                 int minIndex = selectionModel.getMinSelectionIndex();
376                 int maxIndex = selectionModel.getMaxSelectionIndex();
377                 Track tracks[] = sequence.getTracks();
378                 for( int i = maxIndex; i >= minIndex; i-- ) {
379                         if( ! selectionModel.isSelectedIndex(i) ) continue;
380                         sequence.deleteTrack(tracks[i]);
381                         trackModelList.remove(i);
382                 }
383                 fireTableRowsDeleted(minIndex, maxIndex);
384                 setModified(true);
385         }
386         /**
387          * このシーケンスモデルのシーケンスをシーケンサーが操作しているか調べます。
388          * @return シーケンサーが操作していたらtrue
389          */
390         public boolean isOnSequencer() {
391                 return sequence == sequenceListTableModel.getSequencerModel().getSequencer().getSequence();
392         }
393         /**
394          * 録音しようとしているチャンネルの設定されたトラックがあるか調べます。
395          * @return 該当トラックがあればtrue
396          */
397         public boolean hasRecordChannel() {
398                 int rowCount = getRowCount();
399                 for( int row=0; row < rowCount; row++ ) {
400                         Object value = getValueAt(row, Column.RECORD_CHANNEL.ordinal());
401                         if( ! "OFF".equals(value) ) return true;
402                 }
403                 return false;
404         }
405 }