OSDN Git Service

リファクタリング、エラー処理改善など
authorAkiyoshi Kamide <kamide@yk.rim.or.jp>
Sat, 30 Sep 2017 17:00:51 +0000 (02:00 +0900)
committerAkiyoshi Kamide <kamide@yk.rim.or.jp>
Sat, 30 Sep 2017 17:00:51 +0000 (02:00 +0900)
src/camidion/chordhelper/ChordHelperApplet.java
src/camidion/chordhelper/ChordTextField.java
src/camidion/chordhelper/MidiChordHelper.java
src/camidion/chordhelper/midieditor/Base64Dialog.java
src/camidion/chordhelper/midieditor/PlaylistTable.java
src/camidion/chordhelper/midieditor/PlaylistTableModel.java
src/camidion/chordhelper/midieditor/SequenceTrackListTableModel.java
src/camidion/chordhelper/music/MIDISpec.java

index 7fe130d..3701b2e 100644 (file)
@@ -13,7 +13,6 @@ import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.nio.charset.Charset;
 import java.util.Arrays;
 
 import javax.sound.midi.InvalidMidiDataException;
@@ -60,7 +59,6 @@ import camidion.chordhelper.midieditor.TempoSelecter;
 import camidion.chordhelper.midieditor.TimeSignatureSelecter;
 import camidion.chordhelper.music.Chord;
 import camidion.chordhelper.music.Key;
-import camidion.chordhelper.music.MIDISpec;
 import camidion.chordhelper.music.Range;
 import camidion.chordhelper.pianokeyboard.MidiKeyboardPanel;
 import camidion.chordhelper.pianokeyboard.PianoKeyboardAdapter;
@@ -114,9 +112,7 @@ public class ChordHelperApplet extends JApplet {
                        URL url = (new URI(midiFileUrl)).toURL();
                        String filename = url.getFile().replaceFirst("^.*/","");
                        Sequence sequence = MidiSystem.getSequence(url);
-                       Charset charset = MIDISpec.getCharsetOf(sequence);
-                       if( charset == null ) charset = Charset.defaultCharset();
-                       int index = playlistModel.add(sequence, charset, filename);
+                       int index = playlistModel.add(sequence, filename);
                        midiEditor.playlistTable.getSelectionModel().setSelectionInterval(index, index);
                        return index;
                } catch( URISyntaxException|IOException|InvalidMidiDataException e ) {
@@ -144,10 +140,8 @@ public class ChordHelperApplet extends JApplet {
         */
        public int addToPlaylistBase64(String base64EncodedText, String filename) {
                Base64Dialog d = midiEditor.playlistTable.base64Dialog;
-               d.setBase64Data(base64EncodedText, filename);
-               int index = d.addToPlaylist();
-               midiEditor.playlistTable.getSelectionModel().setSelectionInterval(index, index);
-               return index;
+               d.setBase64TextData(base64EncodedText, filename);
+               return d.addToPlaylist();
        }
        /**
         * プレイリスト上で現在選択されているMIDIシーケンスをシーケンサへロードして再生します。
@@ -172,15 +166,13 @@ public class ChordHelperApplet extends JApplet {
        public boolean isPlaying() { return isRunning(); }
        /**
         * 現在シーケンサにロードされているMIDIデータをBase64テキストに変換した結果を返します。
-        * @return MIDIデータをBase64テキストに変換した結果(シーケンサにロードされていない場合null)
+        * @return MIDIデータをBase64テキストに変換した結果
         * @throws IOException MIDIデータの読み込みに失敗した場合
         */
        public String getMidiDataBase64() throws IOException {
-               SequenceTrackListTableModel s = sequencerModel.getSequenceTrackListTableModel();
-               if( s == null ) return null;
                Base64Dialog d = midiEditor.playlistTable.base64Dialog;
-               d.setMIDIData(s.getMIDIdata());
-               return d.getBase64Data();
+               d.setSequenceModel(sequencerModel.getSequenceTrackListTableModel());
+               return d.getBase64TextData();
        }
        /**
         * 現在シーケンサにロードされているMIDIファイルのファイル名を返します。
@@ -274,7 +266,7 @@ public class ChordHelperApplet extends JApplet {
         */
        public static class VersionInfo {
                public static final String NAME = "MIDI Chord Helper";
-               public static final String VERSION = "Ver.20170722.1";
+               public static final String VERSION = "Ver.20170930.1";
                public static final String COPYRIGHT = "Copyright (C) 2004-2017";
                public static final String AUTHER = "@きよし - Akiyoshi Kamide";
                public static final String URL = "http://www.yk.rim.or.jp/~kamide/music/chordhelper/";
@@ -288,9 +280,6 @@ public class ChordHelperApplet extends JApplet {
        /** ボタンの余白を詰めたいときに setMargin() の引数に指定するインセット */
        public static final Insets ZERO_INSETS = new Insets(0,0,0,0);
 
-       // MIDIエディタダイアログ(Javaアプリメインからもアクセスできるようprivateにしていない)
-       MidiSequenceEditorDialog midiEditor;
-
        // GUIコンポーネント(内部保存用)
        private PlaylistTableModel playlistModel;
        private MidiSequencerModel sequencerModel;
@@ -310,6 +299,15 @@ public class ChordHelperApplet extends JApplet {
        private JToggleButton anoGakkiToggleButton;
        private MidiDeviceTreeModel deviceTreeModel;
 
+       private MidiSequenceEditorDialog midiEditor;
+       /**
+        * MIDIエディタダイアログを返します。
+        * @return MIDIエディタダイアログ
+        */
+       public MidiSequenceEditorDialog getMidiEditor() {
+               return midiEditor;
+       }
+
        // アイコン画像
        private Image iconImage;
        public Image getIconImage() { return iconImage; }
@@ -412,7 +410,8 @@ public class ChordHelperApplet extends JApplet {
                });
                // 再生時間位置の移動、シーケンス名の変更、またはシーケンスの入れ替えが発生したときに呼び出されるリスナーを登録
                SongTitleLabel songTitleLabel = new SongTitleLabel();
-               sequencerModel.addChangeListener(e->{
+               sequencerModel.addChangeListener(event->{
+                       MidiSequencerModel sequencerModel = (MidiSequencerModel) event.getSource();
                        Sequencer sequencer = sequencerModel.getSequencer();
                        chordMatrix.setPlaying(sequencer.isRunning());
                        SequenceTrackListTableModel sequenceModel = sequencerModel.getSequenceTrackListTableModel();
@@ -457,8 +456,8 @@ public class ChordHelperApplet extends JApplet {
                        add( Box.createHorizontalStrut(5) );
                        add( darkModeToggleButton = new JToggleButton(new ButtonIcon(ButtonIcon.DARK_MODE_ICON)) {{
                                setMargin(ZERO_INSETS);
-                               addItemListener(e->{
-                                       boolean isDark = darkModeToggleButton.isSelected();
+                               addItemListener(event->{
+                                       boolean isDark = ((JToggleButton)event.getSource()).isSelected();
                                        Color col = isDark ? Color.black : null;
                                        getContentPane().setBackground(isDark ? Color.black : rootPaneDefaultBgcolor);
                                        mainSplitPane.setBackground(col);
@@ -488,9 +487,9 @@ public class ChordHelperApplet extends JApplet {
                                setMargin(ZERO_INSETS);
                                setBorder(null);
                                setToolTipText("あの楽器");
-                               addItemListener(e->
+                               addItemListener(event->
                                        keyboardPanel.keyboardCenterPanel.keyboard.anoGakkiPane
-                                       = anoGakkiToggleButton.isSelected() ? anoGakkiPane : null
+                                       = ((JToggleButton)event.getSource()).isSelected() ? anoGakkiPane : null
                                );
                        }} );
                        add( Box.createHorizontalStrut(5) );
index 2157eb5..c94b1d2 100644 (file)
@@ -104,7 +104,7 @@ public class ChordTextField extends JTextField {
        private Chord currentChord = null;
        /**
         * コードを追加します。
-        * @param chord コード
+        * @param chord 追加するコード
         */
        public void appendChord(Chord chord) {
                if( currentChord == null && chord == null )
index 114740b..e738517 100644 (file)
@@ -25,9 +25,9 @@ import javax.swing.JLabel;
 import javax.swing.JOptionPane;
 import javax.swing.SwingUtilities;
 import javax.swing.WindowConstants;
-import javax.swing.event.TableModelEvent;
 
 import camidion.chordhelper.mididevice.MidiSequencerModel;
+import camidion.chordhelper.midieditor.MidiSequenceEditorDialog;
 import camidion.chordhelper.midieditor.PlaylistTableModel;
 import camidion.chordhelper.midieditor.SequenceTrackListTableModel;
 
@@ -41,7 +41,7 @@ public class MidiChordHelper extends JFrame implements AppletStub, AppletContext
         * @throws Exception 何らかの異常が発生した場合にスローされる
         */
        public static void main(String[] args) throws Exception {
-               List<File> fileList = Arrays.asList(args).stream()
+               List<File> fileList = Arrays.stream(args)
                                .map(arg -> new File(arg))
                                .collect(Collectors.toList());
                SwingUtilities.invokeLater(()->new MidiChordHelper(fileList));
@@ -57,21 +57,17 @@ public class MidiChordHelper extends JFrame implements AppletStub, AppletContext
        private JLabel statusBar = new JLabel("Welcome to "+ChordHelperApplet.VersionInfo.NAME) {
                { setFont(getFont().deriveFont(Font.PLAIN)); }
        };
-       private void updateFilename(SequenceTrackListTableModel sequence) {
+       private static String titleOf(SequenceTrackListTableModel sequence) {
                String title = ChordHelperApplet.VersionInfo.NAME;
                if( sequence != null ) {
                        String filename = sequence.getFilename();
                        if( filename != null && ! filename.isEmpty() )
                                title = filename+" - "+title;
                }
-               setTitle(title);
+               return title;
        }
-       private void updateFilename(MidiSequencerModel sequencer) {
-               updateFilename(sequencer.getSequenceTrackListTableModel());
-       }
-       private void updateFilename(TableModelEvent event) {
-               if( ! PlaylistTableModel.filenameChanged(event) ) return;
-               updateFilename(((PlaylistTableModel)event.getSource()).getSequencerModel());
+       private void setTitleOf(MidiSequencerModel sequencer) {
+               setTitle(titleOf(sequencer.getSequenceTrackListTableModel()));
        }
        private MidiChordHelper(List<File> fileList) {
                setTitle(ChordHelperApplet.VersionInfo.NAME);
@@ -103,13 +99,17 @@ public class MidiChordHelper extends JFrame implements AppletStub, AppletContext
                                        System.exit(0);
                                }
                        });
-                       PlaylistTableModel playlist = applet.midiEditor.getPlaylistModel();
+                       MidiSequenceEditorDialog editor = applet.getMidiEditor();
+                       PlaylistTableModel playlist = editor.getPlaylistModel();
                        MidiSequencerModel sequencer = playlist.getSequencerModel();
-                       sequencer.addChangeListener(e->updateFilename((MidiSequencerModel)e.getSource()));
-                       playlist.addTableModelListener(e->updateFilename(e));
-                       updateFilename(sequencer);
+                       sequencer.addChangeListener(ce->setTitleOf(sequencer));
+                       playlist.addTableModelListener(tme->{
+                               if( playlist.isLoadedSequenceChanged(tme, PlaylistTableModel.Column.FILENAME) )
+                                       setTitleOf(sequencer);
+                       });
+                       setTitleOf(sequencer);
                        applet.start();
-                       applet.midiEditor.play(fileList);
+                       editor.play(fileList);
                });
        }
        @Override
index 63c9181..3fdca39 100644 (file)
@@ -4,8 +4,8 @@ import java.awt.event.ActionEvent;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.nio.charset.Charset;
 import java.util.Base64;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import javax.sound.midi.InvalidMidiDataException;
@@ -21,52 +21,80 @@ import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.JScrollPane;
 import javax.swing.JTextArea;
+import javax.swing.ListSelectionModel;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
 
 import camidion.chordhelper.ButtonIcon;
 import camidion.chordhelper.ChordHelperApplet;
-import camidion.chordhelper.music.MIDISpec;
 
 /**
  * Base64テキスト入力ダイアログ
  */
-public class Base64Dialog extends JDialog implements DocumentListener {
-       public static final Pattern HEADER_PATTERN = Pattern.compile("^.*:.*$", Pattern.MULTILINE);
+public class Base64Dialog extends JDialog {
        private JTextArea base64TextArea = new JTextArea(8,56);
-       private void error(String message) {
+       private PlaylistTable playlistTable;
+       private void decodeError(String message) {
                JOptionPane.showMessageDialog(base64TextArea, (Object)message,
                                ChordHelperApplet.VersionInfo.NAME, JOptionPane.WARNING_MESSAGE);
                base64TextArea.requestFocusInWindow();
        }
-       private String createHeader(String filename) {
-               return "Content-Type: audio/midi; name=\"" + filename + "\"\n"
-                               + "Content-Transfer-Encoding: base64\n\n";
+       private static String createHeaderOfFilename(String filename) {
+               String header = "Content-Type: audio/midi; name=\"";
+               if( filename != null ) header += filename;
+               header += "\"\nContent-Transfer-Encoding: base64\n\n";
+               return header;
+       }
+       private static String createBase64TextWithHeader(SequenceTrackListTableModel sequenceModel) throws IOException {
+               if( sequenceModel == null ) return null;
+               String text = createHeaderOfFilename(sequenceModel.getFilename());
+               byte[] midiData = sequenceModel.getMIDIdata();
+               if( midiData != null && midiData.length > 0 )
+                       text += Base64.getMimeEncoder().encodeToString(midiData);
+               text += "\n";
+               return text;
+       }
+       private static String bodyOf(String base64TextWithHeader) {
+               // bodyには":"が含まれないのでヘッダと混同する心配なし
+               return Pattern.compile("^.*:.*$", Pattern.MULTILINE).matcher(base64TextWithHeader).replaceAll("");
+       }
+       private static String filenameOf(String base64TextWithHeader) {
+               Matcher m = Pattern.compile("(?i)^Content-Type:.*name=\"(.*)\"$", Pattern.MULTILINE).matcher(base64TextWithHeader);
+               return m.find() ? m.group(1) : "";
        }
-       public PlaylistTable playlistTable;
        /**
         * 入力されたBase64テキストをデコードし、MIDIシーケンスとしてプレイリストに追加します。
         * @return プレイリストに追加されたMIDIシーケンスのインデックス(先頭が0)、追加に失敗した場合は -1
         */
        public int addToPlaylist() {
-               byte[] midiData = null;
+               String base64Text = base64TextArea.getText();
+               byte[] decodedData;
                try {
-                       midiData = getMIDIData();
-               } catch(Exception e) {
-                       error("Base64デコードに失敗しました。\n"+e);
+                       decodedData = Base64.getMimeDecoder().decode(bodyOf(base64Text).getBytes());
+               } catch(Exception ex) {
+                       // 不正なBase64テキストが入力された場合
+                       decodeError("Base64デコードに失敗しました。\n"+ex);
                        return -1;
                }
-               try (InputStream in = new ByteArrayInputStream(midiData)) {
-                       Sequence sequence = MidiSystem.getSequence(in);
-                       Charset charset = MIDISpec.getCharsetOf(sequence);
-                       if( charset == null ) charset = Charset.defaultCharset();
-                       int index = playlistTable.getModel().add(sequence, charset, null);
-                       playlistTable.getSelectionModel().setSelectionInterval(index, index);
-                       return index;
-               } catch( IOException|InvalidMidiDataException e ) {
-                       error("Base64デコードした結果をMIDIシーケンスとして読み込めませんでした。\n"+e);
+               Sequence sequence;
+               try (InputStream in = new ByteArrayInputStream(decodedData)) {
+                       sequence = MidiSystem.getSequence(in);
+               } catch( IOException|InvalidMidiDataException ex ) {
+                       // MIDI以外のデータをエンコードしたBase64テキストが入力された場合
+                       decodeError("Base64デコードした結果をMIDIシーケンスとして読み込めませんでした。\n"+ex);
+                       return -1;
+               }
+               int newIndex;
+               try {
+                       newIndex = playlistTable.getModel().add(sequence, filenameOf(base64Text));
+               } catch(Exception ex) {
+                       // 何らかの理由でプレイリストへの追加ができなかった場合
+                       decodeError("Base64デコードしたMIDIシーケンスをプレイリストに追加できませんでした。\n"+ex);
                        return -1;
                }
+               ListSelectionModel sm = playlistTable.getSelectionModel();
+               if( sm != null ) sm.setSelectionInterval(newIndex, newIndex);
+               return newIndex;
        }
        /**
         * Base64デコードアクション
@@ -84,8 +112,12 @@ public class Base64Dialog extends JDialog implements DocumentListener {
        public Action clearAction = new AbstractAction("Clear", new ButtonIcon(ButtonIcon.X_ICON)) {
                { putValue(Action.SHORT_DESCRIPTION, "Base64テキスト欄を消去"); }
                @Override
-               public void actionPerformed(ActionEvent e) { setText(null); }
+               public void actionPerformed(ActionEvent e) { base64TextArea.setText(null); }
        };
+       private void setActionEnabled(boolean b) {
+               addBase64Action.setEnabled(b);
+               clearAction.setEnabled(b);
+       }
        /**
         * Base64テキスト入力ダイアログを構築します。
         * @param playlistTable Base64デコードされたMIDIシーケンスの追加先プレイリストビュー
@@ -105,73 +137,47 @@ public class Base64Dialog extends JDialog implements DocumentListener {
                }});
                setBounds( 300, 250, 660, 300 );
                base64TextArea.setToolTipText("Paste Base64-encoded MIDI sequence here");
-               base64TextArea.getDocument().addDocumentListener(this);
-               addBase64Action.setEnabled(false);
-               clearAction.setEnabled(false);
-       }
-       @Override
-       public void insertUpdate(DocumentEvent e) {
-               addBase64Action.setEnabled(true);
-               clearAction.setEnabled(true);
-       }
-       @Override
-       public void removeUpdate(DocumentEvent e) {
-               if( e.getDocument().getLength() > 0 ) return;
-               addBase64Action.setEnabled(false);
-               clearAction.setEnabled(false);
+               base64TextArea.getDocument().addDocumentListener(new DocumentListener() {
+                       @Override
+                       public void insertUpdate(DocumentEvent e) {
+                               setActionEnabled(true);
+                       }
+                       @Override
+                       public void removeUpdate(DocumentEvent e) {
+                               if( e.getDocument().getLength() > 0 ) return;
+                               setActionEnabled(false);
+                       }
+                       @Override
+                       public void changedUpdate(DocumentEvent e) { }
+               });
+               setActionEnabled(false);
        }
-       @Override
-       public void changedUpdate(DocumentEvent e) { }
        /**
-        * バイナリー形式でMIDIデータを返します。
-        * @return バイナリー形式のMIDIデータ
-        * @throws IllegalArgumentException 入力されているテキストが有効なBase64スキームになっていない場合
+        * MIDIシーケンスモデルを設定します。
+        * @param sequenceModel MIDIシーケンスモデル
         */
-       public byte[] getMIDIData() {
-               String body = HEADER_PATTERN.matcher(base64TextArea.getText()).replaceAll("");
-               return Base64.getMimeDecoder().decode(body.getBytes());
-       }
-       /**
-        * バイナリー形式のMIDIデータを設定します。
-        * @param midiData バイナリー形式のMIDIデータ
-        */
-       public void setMIDIData(byte[] midiData) { setMIDIData(midiData, null); }
-       /**
-        * バイナリー形式のMIDIデータを、ファイル名をつけて設定します。
-        * @param midiData バイナリー形式のMIDIデータ
-        * @param filename ファイル名
-        */
-       public void setMIDIData(byte[] midiData, String filename) {
-               if( midiData == null || midiData.length == 0 ) return;
-               if( filename == null ) filename = "";
-               setText(createHeader(filename) + Base64.getMimeEncoder().encodeToString(midiData) + "\n");
+       public void setSequenceModel(SequenceTrackListTableModel sequenceModel) {
+               String text;
+               try {
+                       text = createBase64TextWithHeader(sequenceModel);
+               } catch (IOException ioex) {
+                       text = "File[" + sequenceModel.getFilename() + "]:" + ioex;
+               }
+               base64TextArea.setText(text);
                base64TextArea.selectAll();
        }
        /**
-        * Base64形式でMIDIデータを返します。
+        * Base64形式でテキスト化されたMIDIデータを返します。
         * @return  Base64形式のMIDIデータ
         */
-       public String getBase64Data() { return base64TextArea.getText(); }
+       public String getBase64TextData() { return base64TextArea.getText(); }
        /**
-        * Base64形式のMIDIデータを設定します。
-        * @param base64Data Base64形式のMIDIデータ
+        * Base64形式でテキスト化されたMIDIデータを、ヘッダつきで設定します。
+        * @param base64TextData Base64形式のMIDIデータ
+        * @param filename ヘッダに含めるファイル名(nullを指定すると""として設定される)
         */
-       public void setBase64Data(String base64Data) {
-               setText(null);
-               base64TextArea.append(base64Data);
+       public void setBase64TextData(String base64TextData, String filename) {
+               base64TextArea.setText(createHeaderOfFilename(filename));
+               base64TextArea.append(base64TextData);
        }
-       /**
-        * Base64形式のMIDIデータを、ファイル名をつけて設定します。
-        * @param base64Data Base64形式のMIDIデータ
-        * @param filename ファイル名
-        */
-       public void setBase64Data(String base64Data, String filename) {
-               setText(createHeader(filename));
-               base64TextArea.append(base64Data);
-       }
-       /**
-        * テキスト文字列を設定します。
-        * @param text テキスト文字列
-        */
-       public void setText(String text) { base64TextArea.setText(text); }
 }
index b8e5004..c91496c 100644 (file)
@@ -44,7 +44,6 @@ import javax.swing.table.TableColumnModel;
 
 import camidion.chordhelper.ChordHelperApplet;
 import camidion.chordhelper.mididevice.MidiSequencerModel;
-import camidion.chordhelper.music.MIDISpec;
 
 /**
  * プレイリストビュー(シーケンスリスト)
@@ -129,20 +128,7 @@ public class PlaylistTable extends JTable {
                        }
                        @Override
                        public void actionPerformed(ActionEvent e) {
-                               SequenceTrackListTableModel sequenceModel = getSelectedSequenceModel();
-                               byte[] data = null;
-                               String filename = null;
-                               if( sequenceModel != null ) {
-                                       filename = sequenceModel.getFilename();
-                                       try {
-                                               data = sequenceModel.getMIDIdata();
-                                       } catch (IOException ioe) {
-                                               base64Dialog.setText("File["+filename+"]:"+ioe.toString());
-                                               base64Dialog.setVisible(true);
-                                               return;
-                                       }
-                               }
-                               base64Dialog.setMIDIData(data, filename);
+                               base64Dialog.setSequenceModel(getSelectedSequenceModel());
                                base64Dialog.setVisible(true);
                        }
                };
@@ -314,22 +300,28 @@ public class PlaylistTable extends JTable {
         */
        @Override
        public PlaylistTableModel getModel() { return (PlaylistTableModel)dataModel; }
+    /**
+     * {@link #add(List)} を呼び出し、このプレイリストにMIDIファイルを追加します。
+     * @param files MIDIファイル
+     * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
+     */
+       public int add(File... files) {
+               return add(Arrays.asList(files));
+       }
        /**
         * このプレイリストにMIDIファイルを追加します。追加に失敗した場合はダイアログを表示し、
         * 後続のMIDIファイルが残っていればそれを追加するかどうかをユーザに尋ねます。
-        * @param fileList MIDIファイルのリスト
+        * @param files MIDIファイルのリスト
         * @return 追加されたMIDIファイルのインデックス値(先頭が0、追加されなかった場合は-1)
         */
-       public int add(List<File> fileList) {
+       public int add(List<File> files) {
                int firstIndex = -1;
-               Iterator<File> itr = fileList.iterator();
+               Iterator<File> itr = files.iterator();
                while(itr.hasNext()) {
                        File file = itr.next();
                        try (FileInputStream in = new FileInputStream(file)) {
                                Sequence sequence = MidiSystem.getSequence(in);
-                               Charset charset = MIDISpec.getCharsetOf(sequence);
-                               if( charset == null ) charset = Charset.defaultCharset();
-                               int lastIndex = ((PlaylistTableModel)dataModel).add(sequence, charset, file.getName());
+                               int lastIndex = ((PlaylistTableModel)dataModel).add(sequence, file.getName());
                                if( firstIndex < 0 ) firstIndex = lastIndex;
                        } catch(IOException|InvalidMidiDataException e) {
                                String message = "Could not open as MIDI file "+file+"\n"+e;
@@ -458,7 +450,7 @@ public class PlaylistTable extends JTable {
                                        ex.printStackTrace();
                                        return;
                                }
-                               int firstIndex = PlaylistTable.this.add(Arrays.asList(getSelectedFile()));
+                               int firstIndex = PlaylistTable.this.add(getSelectedFile());
                                try {
                                        PlaylistTableModel model = getModel();
                                        MidiSequencerModel sequencerModel = model.getSequencerModel();
index d3bc6fe..7d04651 100644 (file)
@@ -7,6 +7,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Vector;
+import java.util.stream.IntStream;
 
 import javax.sound.midi.InvalidMidiDataException;
 import javax.sound.midi.Sequence;
@@ -21,7 +22,7 @@ import javax.swing.table.AbstractTableModel;
 
 import camidion.chordhelper.ButtonIcon;
 import camidion.chordhelper.mididevice.MidiSequencerModel;
-import camidion.chordhelper.music.ChordProgression;
+import camidion.chordhelper.music.MIDISpec;
 
 /**
  * プレイリスト(MIDIシーケンスリスト)のテーブルデータモデル
@@ -40,30 +41,18 @@ public class PlaylistTableModel extends AbstractTableModel {
         * 空のイベントリストモデル
         */
        public final MidiEventTableModel emptyEventListTableModel = new MidiEventTableModel(emptyTrackListTableModel, null);
-       /**
-        * テーブルモデルの変更を示すイベントが、ファイル名の変更によるものかどうかをチェックします。
-        * @param event テーブルモデルの変更を示すイベント
-        * @return ファイル名の変更による場合true
-        */
-       public static boolean filenameChanged(TableModelEvent event) {
-               int c = event.getColumn();
-               return c == Column.FILENAME.ordinal() || c == TableModelEvent.ALL_COLUMNS ;
-       }
        /** 再生中のシーケンサーの秒位置リスナー */
        private ChangeListener mmssPosition = new ChangeListener() {
                private int value = 0;
                @Override
                public void stateChanged(ChangeEvent event) {
                        Object src = event.getSource();
-                       if( src instanceof MidiSequencerModel ) {
-                               MidiSequencerModel sequencerModel = (MidiSequencerModel)src;
-                               int newValue = sequencerModel.getValue() / 1000;
-                               if(value != newValue) {
-                                       value = newValue;
-                                       int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
-                                       fireTableCellUpdated(rowIndex, Column.POSITION.ordinal());
-                               }
-                       }
+                       if( ! (src instanceof MidiSequencerModel) ) return;
+                       MidiSequencerModel sequencerModel = (MidiSequencerModel)src;
+                       int newValue = sequencerModel.getValue() / 1000;
+                       if(value == newValue) return;
+                       value = newValue;
+                       fireTableCellUpdated(sequencerModel.getSequenceTrackListTableModel(), Column.POSITION);
                }
                @Override
                public String toString() {
@@ -79,38 +68,35 @@ public class PlaylistTableModel extends AbstractTableModel {
                sequencerModel.addChangeListener(mmssPosition);
                sequencerModel.getSequencer().addMetaEventListener(msg->{
                        // EOF(0x2F)が来て曲が終わったら次の曲へ進める
-                       if(msg.getType() == 0x2F) SwingUtilities.invokeLater(()->{
-                               try {
-                                       goNext();
-                               } catch (InvalidMidiDataException e) {
-                                       throw new RuntimeException("Could not play next sequence after end-of-track",e);
-                               }
-                       });
+                       if(msg.getType() == 0x2F) SwingUtilities.invokeLater(()->goNext());
                });
        }
        /**
         * 次の曲へ進みます。
-        *
         * <p>リピートモードの場合は同じ曲をもう一度再生、そうでない場合は次の曲へ進んで再生します。
         * 次の曲がなければ、そこで停止します。いずれの場合も曲の先頭へ戻ります。
         * </p>
-        * @throws InvalidMidiDataException {@link Sequencer#setSequence(Sequence)} を参照
-        * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
+        * @throws IllegalStateException {@link #loadNext(int)} から
+        * {@link InvalidMidiDataException} がスローされた場合
+        * (MIDIシーケンサデバイスが閉じている状態で呼び出されたことが主な原因)
         */
-       private void goNext() throws InvalidMidiDataException {
+       private void goNext() {
                // とりあえず曲の先頭へ戻る
                sequencerModel.getSequencer().setMicrosecondPosition(0);
-               if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1) ) {
-                       // リピートモードのときはもう一度同じ曲を、そうでない場合は次の曲を再生開始
-                       sequencerModel.start();
-               }
-               else {
-                       // 最後の曲が終わったので、停止状態にする
-                       sequencerModel.stop();
-                       // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
-                       // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
-                       int rowIndex = sequenceModelList.indexOf(sequencerModel.getSequenceTrackListTableModel());
-                       fireTableCellUpdated(rowIndex, Column.PLAY.ordinal());
+               try {
+                       if( (Boolean)toggleRepeatAction.getValue(Action.SELECTED_KEY) || loadNext(1) ) {
+                               // リピートモードのときはもう一度同じ曲を、そうでない場合は次の曲を再生開始
+                               sequencerModel.start();
+                       }
+                       else {
+                               // 最後の曲が終わったので、停止状態にする
+                               sequencerModel.stop();
+                               // ここでボタンが停止状態に変わったはずなので、通常であれば再生ボタンが自力で再描画するところだが、
+                               // セルのレンダラーが描く再生ボタンには効かないようなので、セルを突っついて再表示させる。
+                               fireTableCellUpdated(sequencerModel.getSequenceTrackListTableModel(), Column.PLAY);
+                       }
+               } catch (InvalidMidiDataException ex) {
+                       throw new IllegalStateException("Could not play next sequence after end-of-track",ex);
                }
        }
        /**
@@ -287,6 +273,46 @@ public class PlaylistTableModel extends AbstractTableModel {
                }
                public boolean isCellEditable() { return false; }
                public Object getValueOf(SequenceTrackListTableModel sequenceModel) { return ""; }
+               /**
+                * この列が変更されたか調べます。
+                * @param event テーブルモデルの変更を示すイベント
+                * @return この列に変更がある場合true
+                */
+               public boolean isChanged(TableModelEvent event) {
+                       int index = event.getColumn();
+                       return index == ordinal() || index == TableModelEvent.ALL_COLUMNS ;
+               }
+       }
+       /**
+        * 連携中のシーケンサにロードされているシーケンスの行の、指定された列が変更されたか調べます。
+        * ロードされているシーケンスがない場合、変更なしとみなされます。
+        * @param event テーブルモデルの変更を示すイベント
+        * @param column 対象の列(nullを指定すると、どの列が変更されても、その行の変更だけで変更ありとみなされる)
+        * @return 変更がある場合true
+        */
+       public boolean isLoadedSequenceChanged(TableModelEvent event, Column column) {
+               if( column != null && ! column.isChanged(event) ) return false;
+               SequenceTrackListTableModel loadedSequence = sequencerModel.getSequenceTrackListTableModel();
+               return loadedSequence != null && IntStream.rangeClosed(event.getFirstRow(), event.getLastRow())
+                       .anyMatch( index -> index != TableModelEvent.HEADER_ROW && sequenceModelList.get(index) == loadedSequence );
+       }
+       /**
+        * [row, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
+        * @param row 更新されたセルの行
+        * @param column 更新されたセルの列
+        * @see #fireTableCellUpdated(int, int)
+        */
+       public void fireTableCellUpdated(int row, Column column) {
+               fireTableCellUpdated(row, column.ordinal());
+       }
+       /**
+        * [sequence, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
+        * @param sequence 更新されたMIDIシーケンス
+        * @param column 更新されたセルの列
+        * @see #fireTableCellUpdated(int, int)
+        */
+       public void fireTableCellUpdated(SequenceTrackListTableModel sequence, Column column) {
+               fireTableCellUpdated(sequenceModelList.indexOf(sequence), column);
        }
 
        @Override
@@ -317,7 +343,7 @@ public class PlaylistTableModel extends AbstractTableModel {
                case NAME:
                        // シーケンス名の設定または変更
                        if( sequenceModelList.get(row).setName(val.toString()) )
-                               fireTableCellUpdated(row, Column.MODIFIED.ordinal());
+                               fireTableCellUpdated(row, Column.MODIFIED);
                        fireTableCellUpdated(row, column);
                        break;
                case CHARSET:
@@ -326,7 +352,7 @@ public class PlaylistTableModel extends AbstractTableModel {
                        seq.setCharset(Charset.forName(val.toString()));
                        fireTableCellUpdated(row, column);
                        // シーケンス名の表示更新
-                       fireTableCellUpdated(row, Column.NAME.ordinal());
+                       fireTableCellUpdated(row, Column.NAME);
                        // トラック名の表示更新
                        seq.fireTableDataChanged();
                default:
@@ -342,20 +368,52 @@ public class PlaylistTableModel extends AbstractTableModel {
                return (int)(sequenceModelList.stream().mapToLong(m -> m.getMicrosecondLength() / 1000L).sum() / 1000L);
        }
        /**
-        * MIDIシーケンスを追加します。
-        * @param sequence MIDIシーケンス(nullの場合、シーケンスを自動生成して追加)
+        * ファイル名なしでMIDIシーケンスを追加します。
+        * 文字コードは自動的に判別されます(判別に失敗した場合はデフォルトの文字コードが指定されます)。
+        * @param sequence MIDIシーケンス
+        * @return 追加されたシーケンスのインデックス(先頭が 0)
+        */
+       public int add(Sequence sequence) {
+               return add(sequence, (String)null);
+       }
+       /**
+        * ファイル名を指定してMIDIシーケンスを追加します。
+        * 文字コードは自動的に判別されます(判別に失敗した場合はデフォルトの文字コードが指定されます)。
+        * @param sequence MIDIシーケンス
+        * @param filename ファイル名(nullの場合、ファイル名なし)
+        * @return 追加されたシーケンスのインデックス(先頭が 0)
+        */
+       public int add(Sequence sequence, String filename) {
+               Charset charset = MIDISpec.getCharsetOf(sequence);
+               if( charset == null ) charset = Charset.defaultCharset();
+               return add(sequence, charset, filename);
+       }
+       /**
+        * ファイル名なしで、文字コードを指定してMIDIシーケンスを追加します。
+        * @param sequence MIDIシーケンス
+        * @param charset MIDIシーケンス内のテキスト文字コード
+        * @return 追加されたシーケンスのインデックス(先頭が 0)
+        */
+       public int add(Sequence sequence, Charset charset) {
+               return add(sequence, charset, null);
+       }
+       /**
+        * ファイル名、文字コードを指定してMIDIシーケンスを追加します。
+        * @param sequence MIDIシーケンス
         * @param charset MIDIシーケンス内のテキスト文字コード
         * @param filename ファイル名(nullの場合、ファイル名なし)
         * @return 追加されたシーケンスのインデックス(先頭が 0)
         */
        public int add(Sequence sequence, Charset charset, String filename) {
-               if( sequence == null ) {
-                       sequence = (new ChordProgression()).toMidiSequence(charset);
-               }
-               sequenceModelList.add(new SequenceTrackListTableModel(this, sequence, charset, filename));
-               int lastIndex = sequenceModelList.size() - 1;
-               fireTableRowsInserted(lastIndex, lastIndex);
-               return lastIndex;
+               //if( sequence == null ) {
+               //      sequence = (new ChordProgression()).toMidiSequence(charset);
+               //}
+               SequenceTrackListTableModel sequenceModel =
+                               new SequenceTrackListTableModel(this, sequence, charset, filename);
+               int newIndex = sequenceModelList.size();
+               sequenceModelList.add(sequenceModel);
+               fireTableRowsInserted(newIndex, newIndex);
+               return newIndex;
        }
        /**
         * MIDIシーケンスを除去します。除去されたMIDIシーケンスがシーケンサーにロード済みだった場合、アンロードします。
@@ -383,16 +441,13 @@ public class PlaylistTableModel extends AbstractTableModel {
                SequenceTrackListTableModel oldSeq = sequencerModel.getSequenceTrackListTableModel();
                SequenceTrackListTableModel newSeq = (newRowIndex < 0 || sequenceModelList.isEmpty() ? null : sequenceModelList.get(newRowIndex));
                if( ! sequencerModel.setSequenceTrackListTableModel(newSeq) ) return;
-               int columnIndices[] = {
-                       Column.PLAY.ordinal(),
-                       Column.POSITION.ordinal(),
-               };
                if( oldSeq != null ) {
-                       int oldRowIndex = sequenceModelList.indexOf(oldSeq);
-                       for( int columnIndex : columnIndices ) fireTableCellUpdated(oldRowIndex, columnIndex);
+                       fireTableCellUpdated(oldSeq, Column.PLAY);
+                       fireTableCellUpdated(oldSeq, Column.POSITION);
                }
                if( newSeq != null ) {
-                       for( int columnIndex : columnIndices ) fireTableCellUpdated(newRowIndex, columnIndex);
+                       fireTableCellUpdated(newRowIndex, Column.PLAY);
+                       fireTableCellUpdated(newRowIndex, Column.POSITION);
                }
        }
        /**
@@ -414,7 +469,7 @@ public class PlaylistTableModel extends AbstractTableModel {
         * @throws IllegalStateException MIDIシーケンサデバイスが閉じている場合
         */
        public int play(Sequence sequence, Charset charset) throws InvalidMidiDataException {
-               int lastIndex = add(sequence, charset, "");
+               int lastIndex = add(sequence, charset);
                if( ! sequencerModel.getSequencer().isRunning() ) play(lastIndex);
                return lastIndex;
        }
index 91fa6d0..ae3c595 100644 (file)
@@ -5,6 +5,7 @@ import java.io.IOException;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Stream;
 
 import javax.sound.midi.MidiSystem;
 import javax.sound.midi.Sequence;
@@ -47,6 +48,15 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
                }
        }
        /**
+        * [row, column]にあるセルの値が更新されたことを、すべてのリスナーに通知します。
+        * @param row 更新されたセルの行
+        * @param column 更新されたセルの列
+        * @see #fireTableCellUpdated(int, int)
+        */
+       public void fireTableCellUpdated(int row, Column column) {
+               fireTableCellUpdated(row, column.ordinal());
+       }
+       /**
         * このモデルを収容している親のプレイリストを返します。
         */
        public PlaylistTableModel getParent() { return sequenceListTableModel; }
@@ -103,6 +113,13 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
        public int getRowCount() {
                return sequence == null ? 0 : sequence.getTracks().length;
        }
+       /**
+        * トラックが存在しない、空のシーケンスかどうか調べます。
+        * @return トラックが存在しなければtrue
+        */
+       public boolean isEmpty() {
+               return sequence == null || sequence.getTracks().length == 0;
+       }
        @Override
        public int getColumnCount() { return Column.values().length; }
        /**
@@ -193,7 +210,7 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
                        if( ch == trackTableModel.getChannel() ) break;
                        trackTableModel.setChannel(ch);
                        setModified(true);
-                       fireTableCellUpdated(row, Column.EVENTS.ordinal());
+                       fireTableCellUpdated(row, Column.EVENTS);
                        break;
                }
                case TRACK_NAME: trackModelList.get(row).setString((String)val); break;
@@ -222,6 +239,14 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
         */
        public SequenceTickIndex getSequenceTickIndex() { return sequenceTickIndex; }
        /**
+        * トラックから子モデルを生成します。
+        * @param track 対象トラック
+        * @return 対象トラックから生成した子モデル
+        */
+       private MidiEventTableModel createModelOf(Track track) {
+               return new MidiEventTableModel(this, track);
+       }
+       /**
         * MIDIシーケンスを設定します。
         * @param sequence MIDIシーケンス(nullを指定するとトラックリストが空になる)
         */
@@ -246,11 +271,12 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
                fireTimeSignatureChanged();
                //
                // トラックリストを再構築
-               Track tracks[] = sequence.getTracks();
-               for(Track track : tracks) trackModelList.add(new MidiEventTableModel(this, track));
+               Track[] tracks = sequence.getTracks();
+               int newSize = tracks.length;
+               Stream.of(tracks).forEach(track -> trackModelList.add(createModelOf(track)));
                //
                // トラックが挿入されたことを通知
-               fireTableRowsInserted(0, tracks.length-1);
+               fireTableRowsInserted(0, newSize-1);
        }
        /**
         * 拍子が変更されたとき、シーケンスtickインデックスを再作成します。
@@ -289,8 +315,8 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
         * @return 成功したらtrue
         */
        public boolean setName(String name) {
-               if( name.equals(toString()) || sequence == null ) return false;
-               if( ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) ) return false;
+               if( name.equals(toString()) || ! MIDISpec.setNameBytesOf(sequence, name.getBytes(charset)) )
+                       return false;
                setModified(true);
                fireTableDataChanged();
                if( isOnSequencer() )
@@ -303,9 +329,7 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
         * @throws IOException バイト列の出力に失敗した場合
         */
        public byte[] getMIDIdata() throws IOException {
-               if( sequence == null || sequence.getTracks().length == 0 ) {
-                       return null;
-               }
+               if( isEmpty() ) return null;
                try( ByteArrayOutputStream out = new ByteArrayOutputStream() ) {
                        MidiSystem.write(sequence, 1, out);
                        return out.toByteArray();
@@ -329,10 +353,8 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
         * @return トラックモデル(見つからない場合null)
         */
        public MidiEventTableModel getSelectedTrackModel(ListSelectionModel selectionModel) {
-               if( sequence == null || selectionModel.isSelectionEmpty() ) return null;
-               Track tracks[] = sequence.getTracks();
-               if( tracks.length == 0 ) return null;
-               Track t = tracks[selectionModel.getMinSelectionIndex()];
+               if( isEmpty() || selectionModel.isSelectionEmpty() ) return null;
+               Track t = sequence.getTracks()[selectionModel.getMinSelectionIndex()];
                return trackModelList.stream().filter(tm -> tm.getTrack() == t).findFirst().orElse(null);
        }
        /**
@@ -352,9 +374,9 @@ public class SequenceTrackListTableModel extends AbstractTableModel {
         * @return 追加したトラックのインデックス(先頭 0)
         */
        public int createTrack() {
+               int newIndex = getRowCount();
                trackModelList.add(new MidiEventTableModel(this, sequence.createTrack()));
                setModified(true);
-               int newIndex = getRowCount() - 1;
                fireTableRowsInserted(newIndex, newIndex);
                return newIndex;
        }
index 502980a..14d6a72 100644 (file)
@@ -182,11 +182,11 @@ public class MIDISpec {
         */
        public static byte[] getNameBytesOf(Sequence sequence) {
                return Arrays.stream(sequence.getTracks()).map(t->getNameBytesOf(t))
-                       .filter(Objects::nonNull).findFirst().orElse(null);
+                               .filter(Objects::nonNull).findFirst().orElse(null);
        }
        /**
         * シーケンス名のバイト列を設定します。
-        * <p>å\85\88é ­ã\81®ã\83\88ã\83©ã\83\83ã\82¯ã\81«è¨­å®\9aã\81\95ã\82\8cã\81¾ã\81\99ã\80\82設å®\9aã\81«å¤±æ\95\97ã\81\97ã\81\9få ´å\90\88ã\80\81é \86ã\81«æ¬¡ã\81®ã\83\88ã\83©ã\83\83ã\82¯ã\81¸ã\81®è¨­å®\9aã\82\92試ã\81¿ます。
+        * <p>å\85\88é ­ã\81®ã\83\88ã\83©ã\83\83ã\82¯ã\81\8bã\82\89é \86ã\81«è¨­å®\9aã\82\92試ã\81¿ã\80\81æ\88\90å\8a\9fã\81\97ã\81\9fã\81¨ã\81\93ã\82\8dã\81§trueã\82\92è¿\94ã\81\97ます。
         * </p>
         *
         * @param sequence MIDIシーケンス
@@ -194,7 +194,8 @@ public class MIDISpec {
         * @return 成功:true、失敗:false
         */
        public static boolean setNameBytesOf(Sequence sequence, byte[] name) {
-               return Arrays.stream(sequence.getTracks()).anyMatch(t->setNameBytesOf(t,name));
+               return sequence != null && Arrays.stream(sequence.getTracks())
+                               .anyMatch(t->setNameBytesOf(t,name));
        }
        /**
         * 指定されたMIDIシーケンスからメタイベントのテキスト(名前や歌詞など)を検索し、その文字コードを判定します。