com.maccasoft.composer.MusicEditor.java Source code

Java tutorial

Introduction

Here is the source code for com.maccasoft.composer.MusicEditor.java

Source

/*
 * Copyright (c) 2016 Marco Maccaferri and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Marco Maccaferri - initial API and implementation
 */

package com.maccasoft.composer;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collections;

import org.eclipse.core.databinding.observable.list.IListChangeListener;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.list.ListChangeEvent;
import org.eclipse.core.databinding.observable.list.ListDiffVisitor;
import org.eclipse.jface.databinding.viewers.ObservableListContentProvider;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.ColumnViewerEditor;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
import org.eclipse.jface.viewers.ColumnViewerEditorActivationStrategy;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.nebula.jface.gridviewer.GridTableViewer;
import org.eclipse.nebula.jface.gridviewer.GridViewerColumn;
import org.eclipse.nebula.jface.gridviewer.GridViewerEditor;
import org.eclipse.nebula.jface.gridviewer.internal.CellSelection;
import org.eclipse.nebula.widgets.grid.Grid;
import org.eclipse.nebula.widgets.grid.GridColumn;
import org.eclipse.nebula.widgets.grid.GridColumnGroup;
import org.eclipse.nebula.widgets.grid.GridItem;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Spinner;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;

import com.maccasoft.composer.internal.ImageRegistry;
import com.maccasoft.composer.internal.TextEditingSupport;
import com.maccasoft.composer.model.Instrument;
import com.maccasoft.composer.model.InvalidInstrumentException;
import com.maccasoft.composer.model.Music;
import com.maccasoft.composer.model.Project;
import com.maccasoft.composer.model.ProjectCompiler;
import com.maccasoft.composer.model.ProjectException;
import com.maccasoft.composer.model.Song;
import com.maccasoft.composer.model.SongRow;

import jssc.SerialPort;
import jssc.SerialPortException;

public class MusicEditor {

    Shell shell;
    ComboViewer songsCombo;
    ToolItem edit;
    ToolItem play;
    ToolItem stop;
    ToolItem delete;
    Spinner bpm;
    Spinner rows;
    Spinner octave;
    InstrumentToolBar instrumentToolBar;

    GridTableViewer viewer;

    Project project;
    Song currentSong;

    private SerialPort serialPort;
    private FontMetrics fontMetrics;

    final IListChangeListener listChangeListener = new IListChangeListener() {

        @Override
        public void handleListChange(ListChangeEvent event) {
            event.diff.accept(new ListDiffVisitor() {

                @Override
                public void handleRemove(int index, Object element) {
                    ((SongRow) element).removePropertyChangeListener(propertyChangeListener);
                }

                @Override
                public void handleAdd(int index, Object element) {
                    ((SongRow) element).addPropertyChangeListener(propertyChangeListener);
                }
            });
            Display.getDefault().timerExec(250, rowBackgroundUpdateRunnable);
        }
    };

    final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() {

        @Override
        public void propertyChange(PropertyChangeEvent evt) {

        }
    };

    final IListChangeListener instrumentsListChangeListener = new IListChangeListener() {

        @Override
        public void handleListChange(ListChangeEvent event) {
            viewer.refresh();
        }
    };

    final Runnable rowBackgroundUpdateRunnable = new Runnable() {

        @Override
        public void run() {
            Grid grid = (Grid) viewer.getControl();
            if (grid.isDisposed()) {
                return;
            }

            Color bg = Display.getDefault().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND);

            GridItem[] gridItems = grid.getItems();
            for (int i = 0; i < gridItems.length; i++) {
                gridItems[i].setBackground((i & 1) == 0 ? null : bg);
            }
        }
    };

    public MusicEditor(Composite parent) {
        shell = parent.getShell();

        Composite container = new Composite(parent, SWT.NONE);
        container.setLayout(new GridLayout(1, false));
        container.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));

        GC gc = new GC(container);
        fontMetrics = gc.getFontMetrics();
        gc.dispose();

        createHeader(container);
        createMusicViewer(container);

        container.addDisposeListener(new DisposeListener() {

            @Override
            public void widgetDisposed(DisposeEvent e) {
                try {
                    if (serialPort.isOpened()) {
                        serialPort.closePort();
                    }
                } catch (SerialPortException ex) {
                    ex.printStackTrace();
                }
            }
        });
    }

    void updateViewFromProject() {
        songsCombo.setInput(project.getObservableSongs());
        if (project.getSongSize() != 0) {
            songsCombo.setSelection(new StructuredSelection(project.getSong(0)));
            rows.setEnabled(true);
        } else {
            rows.setEnabled(false);
        }
        viewer.getControl().setFocus();
    }

    void updateSongView() {
        edit.setEnabled(currentSong != null);
        play.setEnabled(currentSong != null);
        stop.setEnabled(currentSong != null);
        delete.setEnabled(currentSong != null && project.getSongs().size() > 1);
        bpm.setSelection(currentSong.getBpm());
        rows.setSelection(currentSong.getObservableRows().size());
        viewer.setInput(currentSong.getObservableRows());
    }

    void createHeader(Composite parent) {
        Composite composite = new Composite(parent, SWT.NONE);
        GridLayout gridLayout = new GridLayout(12, false);
        gridLayout.marginWidth = gridLayout.marginHeight = 0;
        composite.setLayout(gridLayout);
        composite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

        Label label = new Label(composite, SWT.NONE);
        label.setText("Song");

        songsCombo = new ComboViewer(composite, SWT.DROP_DOWN | SWT.READ_ONLY | SWT.NO_FOCUS);
        songsCombo.getCombo().setVisibleItemCount(20);
        songsCombo.getCombo()
                .setLayoutData(new GridData(Dialog.convertWidthInCharsToPixels(fontMetrics, 30), SWT.DEFAULT));
        songsCombo.setContentProvider(new ObservableListContentProvider());
        songsCombo.setLabelProvider(new LabelProvider());

        ToolBar toolBar = new ToolBar(composite, SWT.FLAT | SWT.NO_FOCUS);

        edit = new ToolItem(toolBar, SWT.PUSH);
        edit.setImage(ImageRegistry.getImageFromResources("application_edit.png"));
        edit.setToolTipText("Rename");
        edit.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                IInputValidator validator = new IInputValidator() {

                    @Override
                    public String isValid(String newText) {
                        if (newText.length() == 0) {
                            return "";
                        }
                        for (Song song : project.getSongs()) {
                            if (song != currentSong && newText.equalsIgnoreCase(song.getName())) {
                                return "A song with the same title already exists";
                            }
                        }
                        return null;
                    }
                };
                InputDialog dlg = new InputDialog(shell, "Rename Song", "Title:", currentSong.getName(), validator);
                if (dlg.open() == InputDialog.OK) {
                    currentSong.setName(dlg.getValue());
                    songsCombo.refresh();
                }
            }
        });

        new ToolItem(toolBar, SWT.SEPARATOR);

        play = new ToolItem(toolBar, SWT.PUSH);
        play.setImage(ImageRegistry.getImageFromResources("control_play_blue.png"));
        play.setToolTipText("Play");
        play.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                Runnable playThread = new Runnable() {

                    @Override
                    public void run() {
                        try {
                            ProjectCompiler compiler = new ProjectCompiler(project);
                            Music music = compiler.build(currentSong);
                            byte[] data = music.toArray();
                            try {
                                serialPort.writeInt('P');
                                serialPort.writeInt(data.length & 0xFF);
                                serialPort.writeInt((data.length >> 8) & 0xFF);
                                serialPort.writeBytes(data);
                            } catch (SerialPortException e) {
                                e.printStackTrace();
                            }
                        } catch (final ProjectException ex) {
                            Display.getDefault().asyncExec(new Runnable() {

                                @Override
                                public void run() {
                                    if (shell.isDisposed()) {
                                        return;
                                    }
                                    MessageDialog.openError(shell, Main.APP_TITLE,
                                            "An error occurred while compiling song:\r\n\r\n" + ex.getMessage());
                                    focusOnErrorCell(ex);
                                }
                            });
                        }
                    }
                };
                new Thread(playThread).start();
            }
        });

        stop = new ToolItem(toolBar, SWT.PUSH);
        stop.setImage(ImageRegistry.getImageFromResources("control_stop_blue.png"));
        stop.setToolTipText("Stop");
        stop.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                try {
                    serialPort.writeInt('0');
                } catch (SerialPortException ex) {
                    ex.printStackTrace();
                }
            }
        });

        new ToolItem(toolBar, SWT.SEPARATOR);

        delete = new ToolItem(toolBar, SWT.PUSH);
        delete.setImage(ImageRegistry.getImageFromResources("application_delete.png"));
        delete.setToolTipText("Delete song");
        delete.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                IStructuredSelection selection = songsCombo.getStructuredSelection();
                if (selection.isEmpty()) {
                    return;
                }
                if (!MessageDialog.openConfirm(shell, Main.APP_TITLE, "You really want to delete this song?")) {
                    return;
                }
                int index = project.getSongs().indexOf(selection.getFirstElement());
                if (index > 0) {
                    songsCombo.setSelection(new StructuredSelection(project.getSong(index - 1)));
                } else {
                    songsCombo.setSelection(new StructuredSelection(project.getSong(index + 1)));
                }
                project.getObservableSongs().remove(selection.getFirstElement());
                delete.setEnabled(currentSong != null && project.getSongs().size() > 1);
            }
        });

        label = new Label(composite, SWT.NONE);
        label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

        label = new Label(composite, SWT.NONE);
        label.setText("BPM");

        bpm = new Spinner(composite, SWT.BORDER | SWT.NO_FOCUS);
        bpm.setValues(120, 0, 9999, 0, 1, 1);
        bpm.setLayoutData(new GridData(Dialog.convertWidthInCharsToPixels(fontMetrics, 5), SWT.DEFAULT));
        bpm.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                if (currentSong != null) {
                    currentSong.setBpm(bpm.getSelection());
                }
            }
        });

        label = new Label(composite, SWT.NONE);
        label.setText("Rows");

        rows = new Spinner(composite, SWT.BORDER | SWT.NO_FOCUS);
        rows.setValues(0, 0, 9999, 0, 1, 1);
        rows.setLayoutData(new GridData(Dialog.convertWidthInCharsToPixels(fontMetrics, 5), SWT.DEFAULT));
        rows.setEnabled(false);
        rows.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                if (currentSong == null) {
                    return;
                }
                int totalRows = rows.getSelection();
                while (currentSong.getObservableRows().size() > totalRows) {
                    currentSong.getObservableRows().remove(currentSong.getObservableRows().size() - 1);
                }
                while (currentSong.getObservableRows().size() < totalRows) {
                    currentSong.getObservableRows().add(new SongRow());
                }
            }
        });

        label = new Label(composite, SWT.NONE);
        label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

        label = new Label(composite, SWT.NONE);
        label.setText("Octave");

        octave = new Spinner(composite, SWT.BORDER | SWT.NO_FOCUS);
        octave.setValues(3, 1, 9, 0, 1, 1);
        octave.setLayoutData(new GridData(Dialog.convertWidthInCharsToPixels(fontMetrics, 3), SWT.DEFAULT));
        octave.addSelectionListener(new SelectionAdapter() {

            @Override
            public void widgetSelected(SelectionEvent e) {
                viewer.getControl().setFocus();
            }
        });

        instrumentToolBar = new InstrumentToolBar(composite);

        songsCombo.addSelectionChangedListener(new ISelectionChangedListener() {

            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                IStructuredSelection selection = (IStructuredSelection) event.getSelection();
                currentSong = (Song) selection.getFirstElement();
                updateSongView();
            }
        });
    }

    void createMusicViewer(Composite parent) {
        Group group = new Group(parent, SWT.NONE);
        group.setText("Pattern");
        group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        group.setLayout(new GridLayout(1, false));

        final Font font;
        if ("win32".equals(SWT.getPlatform())) {
            font = new Font(Display.getDefault(), "Courier New", 10, SWT.NONE);
        } else {
            font = new Font(Display.getDefault(), "mono", 10, SWT.NONE);
        }

        GC gc = new GC(group);
        gc.setFont(font);
        FontMetrics fontMetrics = gc.getFontMetrics();
        gc.dispose();

        Grid table = new Grid(group, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
        table.setHeaderVisible(true);
        table.setLinesVisible(false);
        table.setRowHeaderVisible(true);
        table.setItemHeaderWidth(Dialog.convertWidthInCharsToPixels(fontMetrics, 6));
        table.setCellSelectionEnabled(true);
        table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        table.setFont(font);

        table.addDisposeListener(new DisposeListener() {

            @Override
            public void widgetDisposed(DisposeEvent e) {
                font.dispose();
            }
        });

        viewer = new GridTableViewer(table);

        ObservableListContentProvider contentProvider = new ObservableListContentProvider() {

            @Override
            public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
                if (oldInput != null) {
                    ((IObservableList) oldInput).removeListChangeListener(listChangeListener);
                }
                if (newInput != null) {
                    ((IObservableList) newInput).addListChangeListener(listChangeListener);
                }
                Display.getDefault().timerExec(0, rowBackgroundUpdateRunnable);
                super.inputChanged(viewer, oldInput, newInput);
            }
        };
        viewer.setContentProvider(contentProvider);

        viewer.setRowHeaderLabelProvider(new CellLabelProvider() {

            @Override
            public void update(ViewerCell cell) {
                int index = ((IObservableList) viewer.getInput()).indexOf(cell.getElement());
                cell.setText(String.format("%02X", index));
            }
        });

        for (int ch = 0; ch < Project.channelLabels.length; ch++) {
            final int channel = ch;

            GridColumnGroup columnGroup = new GridColumnGroup(table, SWT.NONE);
            columnGroup.setText(Project.channelLabels[ch]);

            GridColumn column = new GridColumn(columnGroup, SWT.CENTER);
            column.setText("Note");
            column.setWidth(Dialog.convertWidthInCharsToPixels(fontMetrics, 6));
            GridViewerColumn viewerColumn = new GridViewerColumn(viewer, column);
            viewerColumn.setLabelProvider(new CellLabelProvider() {

                @Override
                public void update(ViewerCell cell) {
                    SongRow element = (SongRow) cell.getElement();
                    cell.setText(element.getNote(channel));
                }
            });
            viewerColumn.setEditingSupport(new TextEditingSupport(viewer) {

                @Override
                protected Object getValue(Object element) {
                    return ((SongRow) element).getNote(channel);
                }

                @Override
                protected void setValue(Object element, Object value) {
                    ((SongRow) element).setNote(channel, value.toString());
                    viewer.update(element, null);
                }
            });

            column = new GridColumn(columnGroup, SWT.CENTER);
            column.setText("Ins.");
            column.setWidth(Dialog.convertWidthInCharsToPixels(fontMetrics, 6));
            viewerColumn = new GridViewerColumn(viewer, column);
            viewerColumn.setLabelProvider(new CellLabelProvider() {

                @Override
                public void update(ViewerCell cell) {
                    SongRow element = (SongRow) cell.getElement();
                    String value = element.getInstrument(channel);
                    cell.setText(value);
                    if (project.getInstrument(value) == null) {
                        cell.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
                    } else {
                        cell.setForeground(null);
                    }
                }
            });
            viewerColumn.setEditingSupport(new TextEditingSupport(viewer) {

                @Override
                protected Object getValue(Object element) {
                    return ((SongRow) element).getInstrument(channel);
                }

                @Override
                protected void setValue(Object element, Object value) {
                    ((SongRow) element).setInstrument(channel, value.toString());
                    viewer.update(element, null);
                }
            });

            column = new GridColumn(columnGroup, SWT.CENTER);
            column.setText("Fx1");
            column.setWidth(Dialog.convertWidthInCharsToPixels(fontMetrics, 6));
            viewerColumn = new GridViewerColumn(viewer, column);
            viewerColumn.setLabelProvider(new CellLabelProvider() {

                @Override
                public void update(ViewerCell cell) {
                    SongRow element = (SongRow) cell.getElement();
                    String value = element.getFx1(channel);
                    cell.setText(value);
                    if (value.startsWith("TR") && project.getInstrument(element.getInstrument(channel)) == null) {
                        cell.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
                    } else {
                        cell.setForeground(null);
                    }
                }
            });
            viewerColumn.setEditingSupport(new TextEditingSupport(viewer) {

                @Override
                protected Object getValue(Object element) {
                    return ((SongRow) element).getFx1(channel);
                }

                @Override
                protected void setValue(Object element, Object value) {
                    ((SongRow) element).setFx1(channel, value.toString());
                    viewer.update(element, null);
                }
            });

            column = new GridColumn(columnGroup, SWT.CENTER);
            column.setText("Fx2");
            column.setWidth(Dialog.convertWidthInCharsToPixels(fontMetrics, 6));
            viewerColumn = new GridViewerColumn(viewer, column);
            viewerColumn.setLabelProvider(new CellLabelProvider() {

                @Override
                public void update(ViewerCell cell) {
                    SongRow element = (SongRow) cell.getElement();
                    String value = element.getFx2(channel);
                    cell.setText(value);
                    if (value.startsWith("TR") && project.getInstrument(element.getInstrument(channel)) == null) {
                        cell.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
                    } else {
                        cell.setForeground(null);
                    }
                }
            });
            viewerColumn.setEditingSupport(new TextEditingSupport(viewer) {

                @Override
                protected Object getValue(Object element) {
                    return ((SongRow) element).getFx2(channel);
                }

                @Override
                protected void setValue(Object element, Object value) {
                    ((SongRow) element).setFx2(channel, value.toString());
                    viewer.update(element, null);
                }
            });
        }

        table.addKeyListener(new NoteKeyListener(viewer) {

            @Override
            protected int getOctave() {
                return octave.getSelection();
            }

            @Override
            protected String getInstrument() {
                IStructuredSelection selection = instrumentToolBar.getStructuredSelection();
                String id = project.getInstrumentId((Instrument) selection.getFirstElement());
                return id != null ? id : super.getInstrument();
            }
        });

        ColumnViewerEditorActivationStrategy activationStrategy = new ColumnViewerEditorActivationStrategy(viewer) {

            @Override
            protected boolean isEditorActivationEvent(ColumnViewerEditorActivationEvent event) {
                return event.eventType == ColumnViewerEditorActivationEvent.TRAVERSAL
                        || event.eventType == ColumnViewerEditorActivationEvent.MOUSE_DOUBLE_CLICK_SELECTION
                        || (event.eventType == ColumnViewerEditorActivationEvent.KEY_PRESSED
                                && event.character == SWT.CR)
                        || event.eventType == ColumnViewerEditorActivationEvent.PROGRAMMATIC;
            }
        };
        activationStrategy.setEnableEditorActivationWithKeyboard(true);

        GridViewerEditor.create(viewer, activationStrategy, ColumnViewerEditor.TABBING_HORIZONTAL
                | ColumnViewerEditor.KEYBOARD_ACTIVATION | GridViewerEditor.SELECTION_FOLLOWS_EDITOR);
    }

    public void setProject(Project project) {
        if (this.project != null) {
            this.project.getObservableInstruments().removeListChangeListener(instrumentsListChangeListener);
        }
        this.project = project;
        this.project.getObservableInstruments().addListChangeListener(instrumentsListChangeListener);
        instrumentToolBar.setProject(project);
        updateViewFromProject();
    }

    public Project getProject() {
        return project;
    }

    public Song getCurrentSong() {
        return currentSong;
    }

    public SerialPort getSerialPort() {
        return serialPort;
    }

    public void setSerialPort(String portName) {
        try {
            if (serialPort != null && serialPort.isOpened()) {
                serialPort.closePort();
            }
            serialPort = new SerialPort(portName);
            serialPort.openPort();
            serialPort.setParams(SerialPort.BAUDRATE_115200, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
                    SerialPort.PARITY_NONE, false, false);
            instrumentToolBar.setSerialPort(serialPort);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void addSongSelectionChangedListener(ISelectionChangedListener l) {
        songsCombo.addSelectionChangedListener(l);
    }

    public void removeSongSelectionChangedListener(ISelectionChangedListener l) {
        songsCombo.removeSelectionChangedListener(l);
    }

    public void focusOnErrorCell(ProjectException e) {
        int columnIndex = e.getChannelIndex() * 4;
        if (e instanceof InvalidInstrumentException) {
            columnIndex += 1;
        }
        viewer.setSelection(new StructuredSelection(e.getRow()), true);
        CellSelection selection = new CellSelection(Collections.singletonList(e.getRow()),
                Collections.singletonList(Collections.singletonList(columnIndex)), e.getRow(), null);
        viewer.setSelection(selection, true);
    }
}