/*
* Created on Dec 11, 2005
*
* Copyright (c) 2005 Peter Johan Salomonsen (http://www.petersalomonsen.com)
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
* Frinika 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 General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.frinika.sequencer.model;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.Vector;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
import static com.frinika.localization.CurrentLocale.getMessage;
/**
* The EditHistoryContainer monitors all edits on a EditHistoryRecorders. By setting
* a marking pont using the mark method - it will be possible to roll back to
* the state at the marking point at a later stage. It's also possible to roll
* back and roll forward again. This gives undo/redo features to a
* EditHistoryRecorder.
*
* How to use? Simply call the mark method before any action or series of
* actions on the sequence. Set the markstring according to a description of
* what can be undone.
*
* Remember that all actions on a EditHistoryRecordable has to be either add or
* remove - also meaning that if you're just going to change a MidiEvent you
* should always remove it - change it - and then add it to the track again.
*
* If you have undo/redo menuitems reference them with the setMenuItem methods,
* so that they're constantly updated according to the undo/redo marks. You can
* also reuse your undo/redo menuitems in other windows. Do this by adding a the
* undo/redo menuitems from this EditHistoryContainer rather than creating them
* yourself.
*
* @author Peter Johan Salomonsen
*/
public class EditHistoryContainer {
private Vector<EditHistoryAction> editHistory = new Vector<EditHistoryAction>();
private Vector<EditHistoryMark> editHistoryMarks = new Vector<EditHistoryMark>();
private int redoMarkIndex = 0;
private JMenuItem undoMenuItem;
private JMenuItem redoMenuItem;
private Vector<EditHistoryListener> editHistoryListeners = new Vector<EditHistoryListener>();
/**
* Set to false during undo or redo operations so that these operations are not recorded as well.
*/
private boolean recordingEnabled = true;
private int savedPosition = 0;
public EditHistoryContainer() {
// Create default undo and redo menuItems
undoMenuItem = new JMenuItem();
undoMenuItem.setText(getMessage("edithistorycontainer.editmenu.undo"));
undoMenuItem.setEnabled(false);
undoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
undoMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
undo();
}
});
redoMenuItem = new JMenuItem();
redoMenuItem.setText(getMessage("edithistorycontainer.editmenu.redo"));
redoMenuItem.setEnabled(false);
redoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
redoMenuItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
redo();
}
});
}
/**
* Called by the FrinikaTrackWrapper add and remove methods when there are
* changes to the track.
*
* @param edit_history_type
* @param track
* @param event
*/
public void push(EditHistoryRecorder recorder, int edit_history_type, EditHistoryRecordable event) {
if(recordingEnabled)
editHistory.add(new EditHistoryRecordableAction(this,recorder,edit_history_type, event));
}
/**
* Push a generic editHistoryAction onto the recording
* @param editHistoryAction
*/
public void push(EditHistoryAction editHistoryAction)
{
if(recordingEnabled)
editHistory.add(editHistoryAction);
}
/**
* Call the mark method before any action or series of actions on the
* sequence. Set the markstring according to a description of what can be
* undone.
*
* @param markString
*/
public void mark(String markString) {
if(!recordingEnabled)
return;
// System. out.println(" MARK ");
if (redoMarkIndex < editHistoryMarks.size()) {
// If we're pushing a new entry, one should not be able to redo
int startIndex = editHistoryMarks.get(redoMarkIndex)
.getEditHistoryIndex();
int endIndex;
if (redoMarkIndex < editHistoryMarks.size() - 1)
endIndex = editHistoryMarks.get(redoMarkIndex + 1)
.getEditHistoryIndex();
else
endIndex = editHistory.size();
// System. out.println(startIndex+" "+endIndex+"
// "+editHistory.size());
for (int n = startIndex; n < endIndex; n++)
editHistory.remove(editHistory.size() - 1);
for (int n = redoMarkIndex; n < editHistoryMarks.size(); n++)
editHistoryMarks.remove(editHistoryMarks.size() - 1);
}
editHistoryMarks
.add(new EditHistoryMark(editHistory.size(), markString));
redoMarkIndex = editHistoryMarks.size();
updateMenuItems();
// System. out.println(redoMarkIndex+" mark: "+markString);
}
private void notifyEditHistoryListeners(
EditHistoryAction[] editHistoryActionArray) {
// System .out.println(" NOTIFY ");
for (EditHistoryListener editHistoryListener : editHistoryListeners)
editHistoryListener
.fireSequenceDataChanged(editHistoryActionArray);
}
/**
* Clients should call this method when done with a marked action, in order
* to notify listeners for changes.
*
*/
public void notifyEditHistoryListeners() {
Vector<EditHistoryAction> editHistoryActions = new Vector<EditHistoryAction>();
if (redoMarkIndex > 0) {
int lastIndex = editHistoryMarks.get(redoMarkIndex - 1)
.getEditHistoryIndex();
int currentIndex;
if (redoMarkIndex == editHistoryMarks.size())
currentIndex = editHistory.size() - 1;
else
currentIndex = editHistoryMarks.get(redoMarkIndex)
.getEditHistoryIndex() - 1;
for (int n = currentIndex; n >= lastIndex; n--)
editHistoryActions.add(editHistory.get(n));
}
/**
* TODO: PJS: What is this? Yes it can be zero, since you may undo everything... Why shouldn't it?
* if size is zero then nothing has happened ?
*
* PJL: OK if everything can cope with it being zero I commented it out because some stuff was falling over.
* array out of bounds if I recall correctly.
* Maybe fixed now ?
if (editHistoryActions.size() == 0 ) {
try {
throw new Exception(" Should the editHistory really be zero ");
} catch (Exception e) {
e.printStackTrace();
return;
}
}
*/
EditHistoryAction[] editHistoryActionArray = new EditHistoryAction[editHistoryActions
.size()];
editHistoryActions.toArray(editHistoryActionArray);
notifyEditHistoryListeners(editHistoryActionArray);
}
/**
* Add an editHistory listener to this edithistorycontainer
*
* @param editHistoryListener
*/
public void addEditHistoryListener(EditHistoryListener editHistoryListener) {
editHistoryListeners.add(editHistoryListener);
}
/**
* Remove an editHistory listener from this edithistorycontainer
*
* @param editHistoryListener
*/
public void removeEditHistoryListener(
EditHistoryListener editHistoryListener) {
editHistoryListeners.remove(editHistoryListener);
}
/**
* Redo actions up to the next mark
*
*/
public void redo() {
// Check if there's anything to redo
if (redoMarkIndex < editHistoryMarks.size()) {
int startIndex = editHistoryMarks.get(redoMarkIndex)
.getEditHistoryIndex();
int endIndex;
if (redoMarkIndex < editHistoryMarks.size() - 1)
endIndex = editHistoryMarks.get(redoMarkIndex + 1)
.getEditHistoryIndex();
else
endIndex = editHistory.size();
EditHistoryAction[] editHistoryActionArray = new EditHistoryAction[endIndex
- startIndex];
for (int n = startIndex; n < endIndex; n++) {
editHistory.get(n).redo();
editHistoryActionArray[n - startIndex] = editHistory.get(n);
}
redoMarkIndex++;
notifyEditHistoryListeners(editHistoryActionArray);
updateMenuItems();
// System. out.println(redoMarkIndex+" redo ");
}
}
/**
* Get the descriptive string of the actions that will be rolled back on the
* next undo
*
* @return
*/
public String getNextUndoMarkString() {
if (redoMarkIndex > 0)
return editHistoryMarks.get(redoMarkIndex - 1).getMarkString();
else
return null;
}
/**
* Get the descriptive string of the actions that will be redone on the next
* redo
*
* @return
*/
public String getNextRedoMarkString() {
if (redoMarkIndex < editHistoryMarks.size())
return editHistoryMarks.get(redoMarkIndex).getMarkString();
else
return null;
}
/**
* Undo actions back to the previous mark
*
*/
public void undo() {
if (redoMarkIndex > 0) {
int lastIndex = editHistoryMarks.get(redoMarkIndex - 1)
.getEditHistoryIndex();
int currentIndex;
if (redoMarkIndex == editHistoryMarks.size())
currentIndex = editHistory.size() - 1;
else
currentIndex = editHistoryMarks.get(redoMarkIndex)
.getEditHistoryIndex() - 1;
EditHistoryRecordableAction[] editHistoryEntryArray = new EditHistoryRecordableAction[currentIndex
- lastIndex + 1];
for (int n = currentIndex; n >= lastIndex; n--) {
editHistory.get(n).undo();
// Use an inverted editHistoryType when notifying the listeners
if(editHistory.get(n) instanceof EditHistoryRecordableAction)
editHistoryEntryArray[n - lastIndex] = ((EditHistoryRecordableAction)editHistory.get(n))
.getInvertedClone();
}
redoMarkIndex--;
notifyEditHistoryListeners(editHistoryEntryArray);
updateMenuItems();
// System. out.println(redoMarkIndex+" undo ");
}
}
/**
* Update undo/redo menuitems so that they show info according to the
* undo/redo marks. Eventually disable them if there's nothing to undo or
* redo
*
*/
public void updateMenuItems() {
if (undoMenuItem != null) {
String nextUndoMarkString = getNextUndoMarkString();
if (nextUndoMarkString != null) {
undoMenuItem
.setText(getMessage("edithistorycontainer.editmenu.undo")
+ " " + nextUndoMarkString);
undoMenuItem.setEnabled(true);
} else {
undoMenuItem
.setText(getMessage("edithistorycontainer.editmenu.undo"));
undoMenuItem.setEnabled(false);
}
}
if (redoMenuItem != null) {
String nextRedoMarkString = getNextRedoMarkString();
if (nextRedoMarkString != null) {
redoMenuItem
.setText(getMessage("edithistorycontainer.editmenu.redo")
+ " " + nextRedoMarkString);
redoMenuItem.setEnabled(true);
} else {
redoMenuItem
.setText(getMessage("edithistorycontainer.editmenu.redo"));
redoMenuItem.setEnabled(false);
}
}
}
/**
* @return Returns the redoMenuItem.
*/
public JMenuItem getRedoMenuItem() {
return redoMenuItem;
}
/**
* @return Returns the undoMenuItem.
*/
public JMenuItem getUndoMenuItem() {
return undoMenuItem;
}
/**
* Use to give a menuitem reference to the EditHistoryContainer so that it
* can update the menuitem text
*
* @param redoMenuItem
*/
public void setRedoMenuItem(JMenuItem redoMenuItem) {
this.redoMenuItem = redoMenuItem;
}
/**
* Use to give a menuitem reference to the EditHistoryContainer so that it
* can update the menuitem text
*
* @param redoMenuItem
*/
public void setUndoMenuItem(JMenuItem undoMenuItem) {
this.undoMenuItem = undoMenuItem;
}
/**
* Tell if there has been any edits on this history container
* @return
*/
public boolean hasChanges()
{
if(redoMarkIndex==savedPosition )
return false;
else
return true;
}
/**
* @return Returns the recordingEnabled.
*/
public boolean isRecordingEnabled() {
return recordingEnabled;
}
/**
* Use during undo/redo operations so that these are not recorded in the EditHistory
*/
public void disableRecording() {
this.recordingEnabled = false;
}
/**
* Use after undo/redo operations to reenable edithistory recording
*
*/
public void enableRecording() {
this.recordingEnabled = true;
}
/**
* Called by the ProjectContainer when saving the project so that hasChanges() will return false as long as there are no changes after the save
*
*/
public void updateSavedPosition()
{
savedPosition=redoMarkIndex;
notifyEditHistoryListeners();
}
}