diff --git a/history.md b/history.md index 248817e..2ea74c1 100644 --- a/history.md +++ b/history.md @@ -1,6 +1,13 @@ ### Version History +#### v0.1.27 +- Added package `msmEditingTools` with class `MsmEditingTools` to create context menu popups when right-clicking the MSM tree. +- Right now there is only one such context menu for the MSM root node that offers to apply every `sequencingMap` to the affected MSM and MPM maps. + - The `sequencingMaps` are then deleted from the MSM as they are no longer valid. + - It is recommended to operate this functionality BEFORE making any entries in the graphical score as they will get lost in the process. + + #### v0.1.26 - Dependency updates - WebLaF Core v1.2.13 to v1.2.14 diff --git a/src/mpmToolbox/Main.java b/src/mpmToolbox/Main.java index ae4b608..8bc8320 100644 --- a/src/mpmToolbox/Main.java +++ b/src/mpmToolbox/Main.java @@ -13,7 +13,7 @@ * @author Axel Berndt */ public class Main { - public static final String version = "0.1.26"; + public static final String version = "0.1.27"; public static void main(String[] args) { // read the application settings from file diff --git a/src/mpmToolbox/gui/mpmEditingTools/MpmEditingTools.java b/src/mpmToolbox/gui/mpmEditingTools/MpmEditingTools.java index a8a8c69..d560810 100644 --- a/src/mpmToolbox/gui/mpmEditingTools/MpmEditingTools.java +++ b/src/mpmToolbox/gui/mpmEditingTools/MpmEditingTools.java @@ -24,8 +24,6 @@ import mpmToolbox.gui.mpmTree.MpmStyleCollection; import mpmToolbox.gui.mpmTree.MpmTree; import mpmToolbox.gui.mpmTree.MpmTreeNode; -import mpmToolbox.gui.msmTree.MsmTree; -import mpmToolbox.gui.msmTree.MsmTreeNode; import mpmToolbox.projectData.score.Score; import mpmToolbox.projectData.score.ScoreNode; import mpmToolbox.projectData.score.ScorePage; @@ -878,22 +876,6 @@ public static WebPopupMenu makeScoreContextMenu(@NotNull MpmTreeNode mpmTreeNode return menu; } - /** - * This creates the context menu in the ScoreDisplayPanel when an MSM object is right-clicked. - * @return - */ - public static WebPopupMenu makeScoreContextMenu(@NotNull MsmTreeNode msmTreeNode, @NotNull MsmTree msmTree, @NotNull ScorePage scorePage) { - WebMenuItem deleteFromScore = new WebMenuItem("Remove from Score"); - deleteFromScore.addActionListener(actionEvent -> { - scorePage.removeEntry((Element) msmTreeNode.getUserObject()); // remove the note from the score page graph structure - msmTree.updateNode(msmTreeNode); // update the MsmTree - }); - - WebPopupMenu menu = new WebPopupMenu(); - menu.add(deleteFromScore); - return menu; - } - /** * changes in the performance should also be visible in the alignment that is currently shown in the audio frame * @param performance the performance where the changes were made diff --git a/src/mpmToolbox/gui/mpmTree/MpmTree.java b/src/mpmToolbox/gui/mpmTree/MpmTree.java index b147a93..320dc4f 100644 --- a/src/mpmToolbox/gui/mpmTree/MpmTree.java +++ b/src/mpmToolbox/gui/mpmTree/MpmTree.java @@ -213,7 +213,6 @@ public void mouseClicked(MouseEvent mouseEvent) { */ @Override public void mousePressed(MouseEvent mouseEvent) { - } /** @@ -222,7 +221,6 @@ public void mousePressed(MouseEvent mouseEvent) { */ @Override public void mouseReleased(MouseEvent mouseEvent) { - } /** @@ -231,7 +229,6 @@ public void mouseReleased(MouseEvent mouseEvent) { */ @Override public void mouseEntered(MouseEvent mouseEvent) { - } /** @@ -240,7 +237,6 @@ public void mouseEntered(MouseEvent mouseEvent) { */ @Override public void mouseExited(MouseEvent mouseEvent) { - } @Override diff --git a/src/mpmToolbox/gui/msmEditingTools/MsmEditingTools.java b/src/mpmToolbox/gui/msmEditingTools/MsmEditingTools.java new file mode 100644 index 0000000..8ee35c6 --- /dev/null +++ b/src/mpmToolbox/gui/msmEditingTools/MsmEditingTools.java @@ -0,0 +1,167 @@ +package mpmToolbox.gui.msmEditingTools; + +import com.alee.api.annotations.NotNull; +import com.alee.laf.menu.WebMenuItem; +import com.alee.laf.menu.WebPopupMenu; +import meico.mei.Helper; +import meico.mpm.elements.Part; +import meico.mpm.elements.Performance; +import meico.mpm.elements.maps.ArticulationMap; +import meico.mpm.elements.maps.GenericMap; +import meico.msm.Msm; +import mpmToolbox.gui.mpmTree.MpmTree; +import mpmToolbox.gui.msmTree.MsmTree; +import mpmToolbox.gui.msmTree.MsmTreeNode; +import mpmToolbox.projectData.score.ScorePage; +import nu.xom.Element; +import nu.xom.Elements; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Helper class for MsmTreeNode. It generates the context menu that appears when right clicking + * in the MSM tree and all functionality required for it. + * @author Axel Berndt + */public class MsmEditingTools { + public static WebPopupMenu makeMsmTreeContextMenu(@NotNull MsmTreeNode forThisNode, @NotNull MsmTree inThisMsmTree) { + MsmTreeNode self = forThisNode; + MsmTree msmTree = inThisMsmTree; + +// System.out.println("Creating popup menu for " + self.name); + WebPopupMenu menu = new WebPopupMenu(); + + switch (self.getType()) { + case msm: + // resolve all sequencingMaps + WebMenuItem expandRepetitions = new WebMenuItem("Resolve all sequencingMaps"); + expandRepetitions.setToolTipText("Creates \"through-composed\" MSM and performances.
Deletes all sequencingMaps afterwards. If you plan to use
this functionality, do it before making entries in the score!"); + expandRepetitions.addActionListener(actionEvent -> MsmEditingTools.resolveAllSequencingMaps(msmTree)); + menu.add(expandRepetitions); + break; + + case attribute: + break; + + case global: + break; + + case part: + break; + + case header: + break; + + case dated: + break; + + case score: + break; + + case sequencingMap: + break; + + case note: + break; + + case rest: + break; + + case lyrics: + break; + + case element: + default: + break; + } + + return menu; + + } + + /** + * Invoke this method to immediately open the editor dialog of MSM nodes ... at least those that have + * an editor dialog and are leaf nodes in the MSM tree. Non-leaf nodes expand on double click and that + * is what this method is meant to be used for, open the editor on double click. + * @param forThisNode + * @param inThisMsmTree + */ + public static void quickOpenEditor(@NotNull MsmTreeNode forThisNode, @NotNull MsmTree inThisMsmTree) { + MsmTreeNode self = forThisNode; + MsmTree msmTree = inThisMsmTree; + + switch (self.getType()) { + // TODO: any editing of the MSM data? + default: + break; + } + } + + /** + * This creates the context menu in the ScoreDisplayPanel when an MSM object is right-clicked. + * @return + */ + public static WebPopupMenu makeScoreContextMenu(@NotNull MsmTreeNode msmTreeNode, @NotNull MsmTree msmTree, @NotNull ScorePage scorePage) { + WebMenuItem deleteFromScore = new WebMenuItem("Remove from Score"); + deleteFromScore.addActionListener(actionEvent -> { + scorePage.removeEntry((Element) msmTreeNode.getUserObject()); // remove the note from the score page graph structure + msmTree.updateNode(msmTreeNode); // update the MsmTree + }); + + WebPopupMenu menu = new WebPopupMenu(); + menu.add(deleteFromScore); + return menu; + } + + /** + * apply all sequencingMaps to the MSM tree and the MPM performances, remove the sequencingMaps from the MSM + * @param msmTree + */ + private static void resolveAllSequencingMaps(@NotNull MsmTree msmTree) { + Msm msm = msmTree.getProjectPane().getMsm(); + MpmTree mpmTree = msmTree.getProjectPane().getMpmTree(); + + ArrayList articulationMaps = new ArrayList<>(); + + // apply the sequencingMaps to MPM data first + Element globalSequencingMap = msm.getRootElement().getFirstChildElement("global").getFirstChildElement("dated").getFirstChildElement("sequencingMap"); + for (Performance performance : mpmTree.getProjectPane().getMpm().getAllPerformances()) { + if (globalSequencingMap != null) { + HashMap maps = performance.getGlobal().getDated().getAllMaps(); + for (GenericMap map : maps.values()) { + map.applySequencingMap(globalSequencingMap); + if (map instanceof ArticulationMap) // in articulationMaps the elements have notid attribute that has to be updated after resolving the sequencingmaps in MSM + articulationMaps.add(map); // so keep the articulationMaps for later reference + } + } + Elements msmParts = msm.getParts(); + ArrayList mpmParts = performance.getAllParts(); + for (int pa=0; pa < performance.size(); ++pa) { + Element msmPart = msmParts.get(pa); + Element sequencingMap = msmPart.getFirstChildElement("dated").getFirstChildElement("sequencingMap"); + if (sequencingMap == null) { + sequencingMap = globalSequencingMap; + if (sequencingMap == null) + continue; + } + for (GenericMap map : mpmParts.get(pa).getDated().getAllMaps().values()) { + map.applySequencingMap(sequencingMap); + if (map instanceof ArticulationMap) // in articulationMaps the elements have notid attribute that has to be updated after resolving the sequencingmaps in MSM + articulationMaps.add(map); // so keep the articulationMaps for later reference + } + } + } + + // apply the sequencingMaps to MSM data, this will also delete the sequencingMaps + HashMap repetitionIDs = msm.resolveSequencingMaps(); + + // update the articulationMap's elements' noteid attributes + for (GenericMap map : articulationMaps) + Helper.updateMpmNoteidsAfterResolvingRepetitions(map, repetitionIDs); + + // reload the nodes whose content changed + msmTree.reloadNode(msmTree.getRootNode()); + mpmTree.reloadNode(mpmTree.getRootNode()); + msmTree.getProjectPane().getScore().cleanupDeadNodes(); + } +} diff --git a/src/mpmToolbox/gui/msmTree/MsmTree.java b/src/mpmToolbox/gui/msmTree/MsmTree.java index 53ec883..8f84add 100644 --- a/src/mpmToolbox/gui/msmTree/MsmTree.java +++ b/src/mpmToolbox/gui/msmTree/MsmTree.java @@ -13,10 +13,15 @@ import nu.xom.Element; import nu.xom.Node; +import javax.swing.*; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Enumeration; @@ -24,7 +29,7 @@ * A custom WebAsncTree for MSM data. * @author Axel Berndt */ -public class MsmTree extends WebExTree implements TreeSelectionListener/*, MouseListener*/ { +public class MsmTree extends WebExTree implements MouseListener, TreeSelectionListener, TreeModelListener { @NotNull private final ProjectPane projectPane; // a link to the parent project pane to access its data, midi player etc. private WebDockableFrame dockableFrame = null; // a WebDockableFrame instance that displays this MSM tree, to be used in class ProjectPane @@ -44,8 +49,9 @@ public MsmTree(@NotNull ProjectPane projectPane) { // msmTree.setCellEditor(new MsmTreeCellEditor()); // msmTree.setStyleId(StyleId.treeTransparent); + this.addMouseListener(this); this.addTreeSelectionListener(this); -// this.addMouseListener(this); + this.treeModel.addTreeModelListener(this); } // /** @@ -206,4 +212,77 @@ public WebDockableFrame getDockableFrame() { return this.dockableFrame; } + + /** + * When the user clicked in the tree, perform this action. + * @param mouseEvent + */ + @Override + public void mouseClicked(MouseEvent mouseEvent) { + // right click opens the context menu of the clicked node + if (SwingUtilities.isRightMouseButton(mouseEvent)) { // if right click + MsmTreeNode node = this.getNodeForRow(this.getClosestRowForLocation(mouseEvent.getX(), mouseEvent.getY())); // get the node that has been clicked + node.getContextMenu(this).show(this, mouseEvent.getX() - 25, mouseEvent.getY()); // trigger its context menu + return; + } + if (SwingUtilities.isLeftMouseButton(mouseEvent)) { // if left click + if (mouseEvent.getClickCount() > 1) { // if double (or more) click -> open editor dialog + MsmTreeNode node = this.getSelectedNode(); // get the node that has been double-clicked + node.openEditorDialog(this); + } + } + } + + /** + * Perform this action when mouse button is pressed. + * @param mouseEvent + */ + @Override + public void mousePressed(MouseEvent mouseEvent) { + } + + /** + * Perform this action when mouse button is released. + * @param mouseEvent + */ + @Override + public void mouseReleased(MouseEvent mouseEvent) { + } + + /** + * Perform this action when the mouse cursor enters the MpmTree widget. + * @param mouseEvent + */ + @Override + public void mouseEntered(MouseEvent mouseEvent) { + } + + /** + * perform this action when the mouse cursor exits the MpmTree widget. + * @param mouseEvent + */ + @Override + public void mouseExited(MouseEvent mouseEvent) { + } + + @Override + public void treeNodesChanged(TreeModelEvent treeModelEvent) { + } + + @Override + public void treeNodesInserted(TreeModelEvent treeModelEvent) { + } + + @Override + public void treeNodesRemoved(TreeModelEvent treeModelEvent) { + } + + /** + * if anything in the tree structure changed the score display gets updated + * @param treeModelEvent + */ + @Override + public void treeStructureChanged(TreeModelEvent treeModelEvent) { + this.projectPane.repaintScoreDisplay(); // repaint the score display so a selected MpmTreeNode gets highlighted and when switching to another performance we get to see its overlay + } } diff --git a/src/mpmToolbox/gui/msmTree/MsmTreeNode.java b/src/mpmToolbox/gui/msmTree/MsmTreeNode.java index bb66c76..1620ccb 100644 --- a/src/mpmToolbox/gui/msmTree/MsmTreeNode.java +++ b/src/mpmToolbox/gui/msmTree/MsmTreeNode.java @@ -3,11 +3,13 @@ import com.alee.api.annotations.NotNull; import com.alee.api.ui.TextBridge; import com.alee.extended.tree.WebExTree; +import com.alee.laf.menu.WebPopupMenu; import com.alee.laf.tree.TreeNodeParameters; import com.alee.laf.tree.UniqueNode; import meico.mei.Helper; import meico.midi.EventMaker; import meico.midi.MidiPlayer; +import mpmToolbox.gui.msmEditingTools.MsmEditingTools; import mpmToolbox.projectData.ProjectData; import nu.xom.Attribute; import nu.xom.Element; @@ -18,8 +20,6 @@ import javax.sound.midi.Track; import javax.swing.*; -import static mpmToolbox.gui.msmTree.MsmTreeNode.XmlNodeType.root; - /** * Instances of this class represent MSM data (Element, Attribute or Node) in a WebAsyncTree * @author Axel Berndt @@ -64,7 +64,7 @@ public MsmTreeNode(@NotNull final Element xml, @NotNull ProjectData project) { // determine type switch (xml.getLocalName()) { case "msm": - this.type = root; + this.type = XmlNodeType.msm; break; case "global": this.type = XmlNodeType.global; @@ -72,12 +72,18 @@ public MsmTreeNode(@NotNull final Element xml, @NotNull ProjectData project) { case "part": this.type = XmlNodeType.part; break; + case "header": + this.type = XmlNodeType.header; + break; case "dated": - this.type = XmlNodeType.score; + this.type = XmlNodeType.dated; break; case "score": this.type = XmlNodeType.score; break; + case "sequencingMap": + this.type = XmlNodeType.sequencingMap; + break; case "note": this.type = XmlNodeType.note; break; @@ -119,8 +125,8 @@ public MsmTreeNode(@NotNull final Attribute attribute, @NotNull ProjectData proj */ private void generateMyName() { switch (this.type) { - case root: - this.name = "</> " + ((Element)this.getUserObject()).getLocalName() + ""; + case msm: + this.name = "</> msm"; break; case part: this.name = ((Element)this.getUserObject()).getLocalName().concat(" " + ((Element)this.getUserObject()).getAttributeValue("number")).concat(" " + ((Element)this.getUserObject()).getAttributeValue("name")); @@ -128,6 +134,9 @@ private void generateMyName() { case score: this.name = " " + ((Element)this.getUserObject()).getLocalName() + ""; break; + case sequencingMap: + this.name = "sequencingMap"; + break; case note: { int ppq = this.project.getMsm().getPPQ(); double duration = Double.parseDouble(((Element)this.getUserObject()).getAttributeValue("duration")); @@ -167,10 +176,15 @@ private void generateMyName() { case lyrics: this.name = ((Element)this.getUserObject()).getLocalName() + " \"" + ((Element)this.getUserObject()).getValue() + "\""; break; + case element: + break; case attribute: this.name = "@ " + ((Attribute)this.getUserObject()).getLocalName() + " " + this.getUserObject().getValue() + ""; // this.name = "@ " + ((Attribute)this.getUserObject()).getLocalName() + ": " + this.getUserObject().getValue(); break; + case global: + case dated: + case header: default: this.name = ((Element)this.getUserObject()).getLocalName(); } @@ -184,6 +198,23 @@ protected void update() { this.generateMyName(); } + /** + * This method creates the context menu when the node is right-clicked. + * @param msmTree the MsmTree instance that this node belongs to + */ + public WebPopupMenu getContextMenu(@NotNull MsmTree msmTree) { + return MsmEditingTools.makeMsmTreeContextMenu(this, msmTree); + } + + /** + * This method will trigger the editor dialog for the node, provided it has one. + * It is meant to be invoked by a double click event in class MsmTree. + * @param msmTree + */ + public void openEditorDialog(@NotNull MsmTree msmTree) { + MsmEditingTools.quickOpenEditor(this, msmTree); + } + /** * Access the XML object behind this node. * @return @@ -296,7 +327,7 @@ public void play(MidiPlayer midiPlayer) { */ public Element getPart() { switch (this.getType()) { - case root: + case msm: case global: return null; default: { @@ -312,15 +343,17 @@ public Element getPart() { * an enumeration of the node types */ public enum XmlNodeType { - root, // a root node - element, // an element - attribute, // an attribute - global, // a global element - part, // a part element - dated, // a dated element - score, // a score element - note, // a note element - rest, // a rest element - lyrics // a lyrics element + msm, // an msm node + element, // an element + attribute, // an attribute + global, // a global element + part, // a part element + header, // a header element + dated, // a dated element + score, // a score element + sequencingMap, // a sequencingMap element + note, // a note element + rest, // a rest element + lyrics // a lyrics element } } diff --git a/src/mpmToolbox/gui/score/ScoreDisplayPanel.java b/src/mpmToolbox/gui/score/ScoreDisplayPanel.java index d9ec6d0..ef7f586 100644 --- a/src/mpmToolbox/gui/score/ScoreDisplayPanel.java +++ b/src/mpmToolbox/gui/score/ScoreDisplayPanel.java @@ -12,6 +12,7 @@ import mpmToolbox.gui.mpmEditingTools.PlaceAndCreateContextMenu; import mpmToolbox.gui.mpmTree.MpmTree; import mpmToolbox.gui.mpmTree.MpmTreeNode; +import mpmToolbox.gui.msmEditingTools.MsmEditingTools; import mpmToolbox.gui.msmTree.MsmTree; import mpmToolbox.gui.msmTree.MsmTreeNode; import mpmToolbox.projectData.score.Score; @@ -571,7 +572,7 @@ private void dragOrSelectGesture(MouseEvent mouseEvent) { case MouseEvent.BUTTON1: // left click break; case MouseEvent.BUTTON3: // right click = context menu - WebPopupMenu menu = MpmEditingTools.makeScoreContextMenu(msmTreeNode, msmTree, scorePage); + WebPopupMenu menu = MsmEditingTools.makeScoreContextMenu(msmTreeNode, msmTree, scorePage); menu.show(this, mouseEvent.getX() - 25, mouseEvent.getY()); break; } @@ -814,7 +815,7 @@ public void mouseReleased(MouseEvent mouseEvent) { if (msmTreeNode != null) { // if something has been selected msmTree.setSelectedNode(msmTreeNode); // select the node in the msm tree msmTree.scrollPathToVisible(msmTreeNode.getTreePath()); // scroll the tree so the node is visible - WebPopupMenu menu = MpmEditingTools.makeScoreContextMenu(msmTreeNode, msmTree, scorePage); + WebPopupMenu menu = MsmEditingTools.makeScoreContextMenu(msmTreeNode, msmTree, scorePage); menu.show(this, mouseEvent.getX() - 25, mouseEvent.getY()); } break; diff --git a/src/mpmToolbox/projectData/score/Score.java b/src/mpmToolbox/projectData/score/Score.java index 6aa3064..b7c7ecc 100644 --- a/src/mpmToolbox/projectData/score/Score.java +++ b/src/mpmToolbox/projectData/score/Score.java @@ -273,10 +273,11 @@ public int cleanupDeadNodes() { for (ScorePage page : this.pages) { ArrayList toBeRemoved = new ArrayList<>(); for (Element element : page.getAllEntries().keySet()) { // for each element that is linked in this score page - if (element.getLocalName().equals("note")) // notes cannot be removed since we are not allowed to edit the MSM - continue; + if (element.getLocalName().equals("note") // if we have a note element + && (element.getDocument() == this.parentProject.getMsm().getDocument())) // if it is still linked in the MSM document + continue; // we keep it if ((this.parentProject.getMpm() != null) // if we have an MPM document - && (element.getDocument() == this.parentProject.getMpm().getDocument())) // if the element is still linked in the MPM document + && (element.getDocument() == this.parentProject.getMpm().getDocument())) // if the element is still linked in the MPM document continue; // we keep it toBeRemoved.add(element); // otherwise it is dd and should be removed from the score }