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