Skip to content

Commit

Permalink
Fix Issue #378 Null exception when dragging MixConsole while no song…
Browse files Browse the repository at this point in the history
… opened. Implement Issue #375 "Add clone as user track" action. Add MidiMixManager.getExistingMix(Song). Add createEnterExitComponentLayer(). Add CornerLayout.
  • Loading branch information
jjazzboss committed Dec 11, 2023
1 parent 1d1e2eb commit 4db32b3
Show file tree
Hide file tree
Showing 14 changed files with 949 additions and 66 deletions.
Binary file added graphics/CloneTrack-10x10.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added graphics/CloneTrack-12x12.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
346 changes: 346 additions & 0 deletions graphics/CloneTrack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ private MidiMixManager()
{
}


/**
* Get a MidiMix for the specified song in the following order.
* <p>
Expand All @@ -87,7 +88,7 @@ private MidiMixManager()
*/
public MidiMix findMix(Song s) throws MidiUnavailableException
{
LOGGER.log(Level.FINE, "findMix() -- s={0}", s);
LOGGER.log(Level.FINE, "findMix() -- s={0}", s);
// Try to get existing MidiMix in memory
MidiMix mm = mapSongMix.get(s);
if (mm == null)
Expand All @@ -107,8 +108,11 @@ public MidiMix findMix(Song s) throws MidiUnavailableException

} catch (IOException ex)
{
LOGGER.log(Level.WARNING, "findMix(Song) Problem reading mix file: {0} : {1}", new Object[]{mixFile.getAbsolutePath(),
ex.getMessage()});
LOGGER.log(Level.WARNING, "findMix(Song) Problem reading mix file: {0} : {1}", new Object[]
{
mixFile.getAbsolutePath(),
ex.getMessage()
});
}

if (mm != null)
Expand All @@ -119,8 +123,11 @@ public MidiMix findMix(Song s) throws MidiUnavailableException
mm.checkConsistency(s, true);
} catch (SongCreationException ex)
{
LOGGER.log(Level.WARNING, "findMix(Song) song mix file: {0} not consistent with song, ignored. ex={1}", new Object[]{mixFile.getAbsolutePath(),
ex.getMessage()});
LOGGER.log(Level.WARNING, "findMix(Song) song mix file: {0} not consistent with song, ignored. ex={1}", new Object[]
{
mixFile.getAbsolutePath(),
ex.getMessage()
});
mm = null;
}
}
Expand All @@ -139,6 +146,25 @@ public MidiMix findMix(Song s) throws MidiUnavailableException
return mm;
}

/**
* Find a mix which must be existing.
* <p>
* If you're not sure if a MidiMix was already created for the specified song, use findMix(Song) instead.
*
* @param s
* @return Can't be null
* @throws IllegalStateException If no mix found
*/
public MidiMix findExistingMix(Song s)
{
MidiMix mm = mapSongMix.get(s);
if (mm == null)
{
throw new IllegalStateException("No MidiMix found for s=" + s);
}
return mm;
}

/**
* Try to get a MidiMix for the specified Rhythm in the following order:
* <p>
Expand All @@ -151,7 +177,7 @@ public MidiMix findMix(Song s) throws MidiUnavailableException
*/
public MidiMix findMix(Rhythm r)
{
LOGGER.log(Level.FINE, "findMix() -- r={0}", r);
LOGGER.log(Level.FINE, "findMix() -- r={0}", r);
MidiMix mm = null;
File mixFile = r instanceof AdaptedRhythm ? null : FileDirectoryManager.getInstance().getRhythmMixFile(r.getName(), r.getFile());
if (mixFile != null && mixFile.canRead())
Expand All @@ -162,8 +188,11 @@ public MidiMix findMix(Rhythm r)
StatusDisplayer.getDefault().setStatusText(ResUtil.getString(getClass(), "LoadedRhythmMix", mixFile.getAbsolutePath()));
} catch (IOException ex)
{
LOGGER.log(Level.SEVERE, "findMix(rhythm) Problem reading mix file: {0} : {1}. Creating a new mix instead.", new Object[]{mixFile.getAbsolutePath(),
ex.getMessage()});
LOGGER.log(Level.SEVERE, "findMix(rhythm) Problem reading mix file: {0} : {1}. Creating a new mix instead.", new Object[]
{
mixFile.getAbsolutePath(),
ex.getMessage()
});
}
}
if (mm == null)
Expand All @@ -186,7 +215,7 @@ public MidiMix findMix(Rhythm r)
public MidiMix createMix(Song sg) throws MidiUnavailableException
{

LOGGER.log(Level.FINE, "createMix() -- sg={0}", sg);
LOGGER.log(Level.FINE, "createMix() -- sg={0}", sg);
MidiMix mm = new MidiMix(sg);


Expand All @@ -210,8 +239,8 @@ public MidiMix createMix(Song sg) throws MidiUnavailableException
/**
* Create a MidiMix for the specified rhythm.
* <p>
* Create one InstrumentMix per rhythm voice, using rhythm voice's preferred instrument and settings, and preferred channel
* (except if several voices share the same preferred channel)
* Create one InstrumentMix per rhythm voice, using rhythm voice's preferred instrument and settings, and preferred channel (except if several voices share
* the same preferred channel)
* .<p>
*
* @param r If r is
Expand All @@ -230,7 +259,7 @@ public MidiMix createMix(Rhythm r)

RhythmVoiceInstrumentProvider p = RhythmVoiceInstrumentProvider.getProvider();
Instrument ins = p.findInstrument(rv);
assert ins != null : "rv=" + rv;
assert ins != null : "rv=" + rv;
int channel = rv.getPreferredChannel();

if (mm.getInstrumentMix(channel) != null)
Expand All @@ -243,7 +272,7 @@ public MidiMix createMix(Rhythm r)
channel = mm.findFreeChannel(rv.isDrums());
if (channel == -1)
{
throw new IllegalStateException("No Midi channel available in MidiMix. r=" + r + " rhythmVoices=" + r.getRhythmVoices());
throw new IllegalStateException("No Midi channel available in MidiMix. r=" + r + " rhythmVoices=" + r.getRhythmVoices());
}
}

Expand All @@ -268,7 +297,7 @@ public void propertyChange(PropertyChangeEvent e)
if (e.getSource() instanceof Song)
{
Song song = (Song) e.getSource();
assert mapSongMix.get(song) != null : "song=" + song + " mapSongMix=" + mapSongMix;
assert mapSongMix.get(song) != null : "song=" + song + " mapSongMix=" + mapSongMix;
if (e.getPropertyName().equals(Song.PROP_CLOSED))
{
unregisterSong(song);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
package org.jjazz.mixconsole;

import org.jjazz.midimix.api.UserRhythmVoice;
import org.jjazz.rhythm.api.RhythmVoice;

/**
* The user actions on a MixChannelPanel that can not be handled by the MixChannelPanel itself.
Expand Down Expand Up @@ -52,6 +53,13 @@ public interface MixChannelPanelController
*/
void removeUserPhrase(UserRhythmVoice userRhythmVoice);

/**
* Clone a rhythm track in a new user track.
*
* @param rhythmVoice
*/
void cloneRhythmTrackAsUserTrack(RhythmVoice rhythmVoice);

/**
* User wants to edit settings of our MixChannelPanel.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.google.common.base.Preconditions;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jjazz.backgroundsongmusicbuilder.api.ActiveSongMusicBuilder;
import org.jjazz.instrumentchooser.spi.InstrumentChooserDialog;
import org.jjazz.midi.api.DrumKit;
import org.jjazz.midi.api.Instrument;
Expand All @@ -36,11 +37,13 @@
import org.jjazz.rhythm.api.RhythmVoice;
import org.jjazz.midimix.api.MidiMix;
import org.jjazz.midimix.api.UserRhythmVoice;
import org.jjazz.mixconsole.actions.AddUserTrack;
import org.jjazz.musiccontrol.api.PlaybackSettings;
import org.jjazz.outputsynth.api.OutputSynthManager;
import org.jjazz.phrase.api.Phrase;
import org.jjazz.rhythmmusicgeneration.api.MusicGenerationQueue;
import org.jjazz.song.api.Song;
import org.jjazz.songeditormanager.api.SongEditorManager;
import org.jjazz.mixconsole.actions.AddUserTrack;
import org.jjazz.undomanager.api.JJazzUndoManager;
import org.jjazz.undomanager.api.JJazzUndoManagerFinder;
import org.jjazz.utilities.api.ResUtil;
Expand Down Expand Up @@ -149,6 +152,50 @@ public void editChannelName(int channel, String newName)
um.endCEdit(undoText);
}

@Override
public void cloneRhythmTrackAsUserTrack(RhythmVoice rv)
{
// Find a name not already used
var usedNames = song.getUserPhraseNames();
String basename = rv.getName() + "-";
int index = 1;
while (usedNames.contains(basename + index))
{
index++;
}
String name = basename + index;


// Retrieve the phrase
var asmb = ActiveSongMusicBuilder.getInstance();
var result = asmb.getLastResult();
Phrase p = result.mapRvPhrases().get(rv);
if (p == null)
{
String msg = "Unexpected error: no phrase found for " + rv.getName() + ". Maybe retry later?";
NotifyDescriptor d = new NotifyDescriptor.Message(msg, NotifyDescriptor.ERROR_MESSAGE);
DialogDisplayer.getDefault().notify(d);
return;
}
Phrase p2 = new Phrase(p.getChannel(), p.isDrums());
p2.add(p);


// Add the user track
if (AddUserTrack.performAddUserPhrase(song, name, p2))
{
// Copy InstrumentMix from the original rhythm track and mute original track
var userRv = midiMix.getUserRhythmVoice(name);
var userRvChannel = midiMix.getChannel(userRv);
var rvInsMix = midiMix.getInstrumentMix(rv);
rvInsMix.setMute(true);
var userInsMix = new InstrumentMix(rvInsMix);
userInsMix.setMute(false);
midiMix.setInstrumentMix(userRvChannel, userRv, userInsMix);
}

}

@Override
public void editUserPhrase(UserRhythmVoice userRhythmVoice)
{
Expand Down Expand Up @@ -247,12 +294,10 @@ public void editPreviousInstrument(int channel)
insMix.setInstrument(ins);
}



// ----------------------------------------------------------------------------
// Private methods
// ----------------------------------------------------------------------------

private String buildSettingsTitle(int channel)
{
StringBuilder title = new StringBuilder(ResUtil.getString(getClass(), "MixChannelPanelControllerImpl.DialogTitle", channel + 1));
Expand All @@ -267,4 +312,5 @@ private String buildSettingsTitle(int channel)
return title.toString();
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright @2019 Jerome Lelasseux. All rights reserved.
*
* This file is part of the JJazzLab software.
*
* JJazzLab is free software: you can redistribute it and/or modify
* it under the terms of the Lesser GNU General Public License (LGPLv3)
* as published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* JJazzLab is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with JJazzLab. If not, see <https://www.gnu.org/licenses/>
*
* Contributor(s):
*/
package org.jjazz.mixconsole;

import java.awt.BorderLayout;
import java.util.function.Consumer;
import java.util.logging.Logger;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
import org.jjazz.midimix.api.MidiMix;
import org.jjazz.rhythm.api.RhythmVoice;
import org.jjazz.song.api.Song;
import org.jjazz.flatcomponents.api.FlatButton;
import org.jjazz.uiutilities.api.CornerLayout;
import org.jjazz.uiutilities.api.UIUtilities;
import org.jjazz.utilities.api.ResUtil;

/**
* Add "clone as user track" button which appears only when hovering component.
*/
public class PhraseViewerPanelRhythm extends PhraseViewerPanel
{
private static final Icon ICON_CLONE_AS_USER_TRACK = new ImageIcon(PhraseViewerPanelRhythm.class.getResource("resources/CloneTrack-10x10.png"));
private final JPanel supportPanel; // Needed to use JLayer
private final FlatButton fbtn_clone;
private boolean buttonShown;
private static final Logger LOGGER = Logger.getLogger(PhraseViewerPanelRhythm.class.getSimpleName());

public PhraseViewerPanelRhythm(Song song, MidiMix mMix, MixChannelPanelController controller, RhythmVoice rv)
{
super(song, mMix, controller, rv);

// Prepare the button
fbtn_clone = new FlatButton();
fbtn_clone.setIcon(ICON_CLONE_AS_USER_TRACK);
fbtn_clone.addActionListener(ae -> getController().cloneRhythmTrackAsUserTrack(getRhythmVoice()));
fbtn_clone.setToolTipText(ResUtil.getString(getClass(), "PhraseViewerPanel.CloneAsUserTrackToolip"));


// Prepare for the JLayer
// We can't directly associate a JLayer to "this" JPanel object, so we add a support panel on which JLayer will be used
supportPanel = new JPanel();
supportPanel.setLayout(new CornerLayout(BUTTONS_PADDING)); // Reuse layout of parent class
supportPanel.setOpaque(false); // So we can see the PhraseViewerPanel background
var enterExitConsumer = new Consumer<Boolean>()
{
@Override
public void accept(Boolean b)
{
// LOGGER.log(Level.SEVERE, "this={0} accept() b={1} buttonShown={2}", new Object[]
// {
// PhraseViewerPanelRhythm.this, b, buttonShown
// });
if (b && !buttonShown)
{
buttonShown = true;
supportPanel.add(fbtn_clone, CornerLayout.NORTH_EAST);
supportPanel.revalidate();
supportPanel.repaint();

} else if (!b && buttonShown)
{
buttonShown = false;
supportPanel.remove(fbtn_clone);
supportPanel.revalidate();
supportPanel.repaint();
}
}
};
var layer = UIUtilities.createEnterExitComponentLayer(supportPanel, enterExitConsumer);


// Now add the JLayer so that it takes all the place
setLayout(new BorderLayout());
add(layer, BorderLayout.CENTER);
}


// ----------------------------------------------------------------------------
// Private methods
// ----------------------------------------------------------------------------
// ----------------------------------------------------------------------------
// Inner classes
// ----------------------------------------------------------------------------
}
Loading

0 comments on commit 4db32b3

Please # to comment.