package org.greenstone.gatherer.undo; /** *######################################################################### * * A component of the Gatherer application, part of the Greenstone digital * library suite from the New Zealand Digital Library Project at the * University of Waikato, New Zealand. * *

* * Author: John Thompson, Greenstone Digital Library, University of Waikato * *

* * Copyright (C) 1999 New Zealand Digital Library Project * *

* * This program 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. * *

* * This program 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 this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. *######################################################################## */ import java.awt.*; import java.awt.datatransfer.Transferable; import java.awt.dnd.*; import java.awt.event.*; import java.awt.geom.AffineTransform; import java.io.*; import java.util.*; import javax.swing.*; import javax.swing.tree.*; import org.greenstone.gatherer.Gatherer; import org.greenstone.gatherer.file.FileJob; import org.greenstone.gatherer.file.FileNode; import org.greenstone.gatherer.file.FileQueue; import org.greenstone.gatherer.file.FileSystemModel; import org.greenstone.gatherer.msm.GDMDocument; import org.greenstone.gatherer.msm.Metadata; import org.greenstone.gatherer.msm.MSMEvent; import org.greenstone.gatherer.msm.MSMListenerAdapter; import org.greenstone.gatherer.util.ArrayTools; import org.greenstone.gatherer.util.DragComponent; import org.greenstone.gatherer.util.DragGroup; import org.greenstone.gatherer.util.Utility; /** Manages the rather complex task of undoing and redoing actions. It does this by storing two queues, undo and redo, containing undo jobs which encapsulate all the information needed to undo or redo an action. It is also important to note that the manage creates a temporary directory, and then places any deleted files in this directory, and saves their associated metadata with them, for the life of the session. This is to facilitate the restoring of deleted files. * @author John Thompson, Greenstone Digital Library, University of Waikato * @version 2.3c */ public class UndoManager extends JButton implements ActionListener, DragComponent, DropTargetListener { private ArrayList redo_sources; private ArrayList undo_sources; private boolean ignore = false; private boolean ignore_next = false; /** The group encompasses all of the objects you plan to drag and drop within, and ensures that only one has focus (as clearly identified by the colour of the selection field or, in this particular case, the background) and that actions only occur between components in the same group. */ private DragGroup group; /** In order to make this button a drop target we have to create a DropTarget instance with the button as its target. */ private DropTarget drop_target; private FileQueue file_queue; private FileSystemModel model; private GDMDocument obsolete_metadata; /** What sort of action should a drag resemble. Not really used as we override with custom drag icon. */ private int drag_action = DnDConstants.ACTION_MOVE; /** The last point the mouse was at. Used to repaint 'spoilt' area. */ private Point pt_last = null; /** The area covered by the drag ghost, our custom drag icon. */ private Rectangle ra_ghost = new Rectangle(); private UndoStack redo; private UndoStack undo; static final public int FILE_COPY = 1; static final public int FILE_DELETE = 2; static final public int FILE_MOVE = 3; public UndoManager() { super(Utility.getImage("bin.gif")); this.file_queue = Gatherer.f_man.getQueue(); this.drop_target = new DropTarget(this, drag_action, this, true); setBackground(Gatherer.config.getColor("coloring.button_background", true)); setForeground(Gatherer.config.getColor("coloring.button_foreground", true)); setOpaque(true); // Creation File recycle_directory = new File(Utility.RECYCLE); if(!recycle_directory.exists()) { recycle_directory.mkdirs(); recycle_directory.deleteOnExit(); } this.model = new FileSystemModel(new FileNode(recycle_directory, "Undo")); obsolete_metadata = new GDMDocument(); // This GDM is never saved. redo = new UndoStack(false); redo_sources = new ArrayList(); undo = new UndoStack(true); undo_sources = new ArrayList(); if(Gatherer.debug != null) { showTree(); } } public void actionPerformed(ActionEvent event) { // Is this an undo event source... if(undo_sources.contains(event.getSource())) { UndoJob undo_job = undo.pop(); undo_job.action(true, file_queue); // Now retrieve all other undo jobs with the same job-number and action them while((undo_job = undo.getJob(undo_job.ID())) != null) { undo_job.action(true, file_queue); } } // Or a redo one. else if(redo_sources.contains(event.getSource())) { UndoJob redo_job = redo.pop(); redo_job.action(false, file_queue); // Now retrieve all other redo jobs with the same job-number and action them while((redo_job = redo.getJob(redo_job.ID())) != null) { redo_job.action(false, file_queue); } } } public void addMetadata(File file, ArrayList metadatum) { for(int i = 0; metadatum != null && i < metadatum.size(); i++) { Metadata metadata = (Metadata) metadatum.get(i); ///ystem.err.println("UndoMetadata: " + file.getAbsolutePath() + " => " + metadata); obsolete_metadata.addMetadata(file.getAbsolutePath(), metadata); } } public void addUndo(long id, int type, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode record, boolean undo_event) { if(target_model == null) { target_model = this; } UndoJobAdder job_adder = new UndoJobAdder(id, type, source_model, source_parent, target_model, record, undo_event); SwingUtilities.invokeLater(job_adder); } public void clear() { undo.clear(); redo.clear(); } /** In order for the appearance to be consistant, given we may be in the situation where the pointer has left our focus but the ghost remains, this method allows other members of the GGroup to tell this component to repair the 'spoilt' region left by its ghost. */ public void clearGhost(){ } public void destroy() { // Remove all references of this as a listener for(int i = 0; i < redo_sources.size(); i++) { AbstractButton source = (AbstractButton) redo_sources.get(i); source.removeActionListener(this); } for(int j = 0; j < undo_sources.size(); j++) { AbstractButton source = (AbstractButton) undo_sources.get(j); source.removeActionListener(this); } redo_sources = null; undo_sources = null; redo = null; undo = null; obsolete_metadata = null; file_queue = null; } /** Any implementation of DropTargetListener must include this method so we can be notified when the drag focus enters this component. We want to provide some sort of indication whether the current component is an acceptable drop target as well as indicating focus. */ public void dragEnter(DropTargetDragEvent event) { //ystem.err.println("Drag entered"); group.grabFocus(this); setBackground(Gatherer.config.getColor("coloring.button_selected_background", true)); setForeground(Gatherer.config.getColor("coloring.button_selected_foreground", true)); } /** Any implementation of DropTargetListener must include this method so we can be notified when the drag focus leaves this component. We need to indicate that we have lost focus. */ public void dragExit(DropTargetEvent event) { //ystem.err.println("Drag exitted"); setBackground(Gatherer.config.getColor("coloring.button_background", true)); setForeground(Gatherer.config.getColor("coloring.button_foreground", true)); } /** Any implementation of DropTargetListener must include this method so we can be notified when the drag moves in this component. This is where we repaint our ghost icon at the tip of the mouse pointer. */ public void dragOver(DropTargetDragEvent event) { Graphics2D g2 = (Graphics2D) getGraphics(); Point pt = event.getLocation(); if(pt_last != null && pt.equals(pt_last)) { return; } pt_last = pt; if(!DragSource.isDragImageSupported()) { // Erase the last ghost image and or cue line paintImmediately(ra_ghost.getBounds()); // Remember where you are about to draw the new ghost image ra_ghost.setRect(pt.x - group.mouse_offset.x, pt.y - group.mouse_offset.y, group.image_ghost.getWidth(), group.image_ghost.getHeight()); // Draw the ghost image g2.drawImage(group.image_ghost, AffineTransform.getTranslateInstance(ra_ghost.getX(), ra_ghost.getY()), null); } } /** Any implementation of DropTargetListener must include this method so we can be notified when the drag ends, ie the transferable is dropped. This in turn triggers a series of add events preceded by a pre() and followed by a post(). */ public void drop(DropTargetDropEvent event) { ignore = true; group.grabFocus(this); setBackground(Gatherer.config.getColor("coloring.button_background", true)); setForeground(Gatherer.config.getColor("coloring.button_foreground", true)); Transferable transferable = event.getTransferable(); try { DragComponent source = group.getSource(); TreePath[] selection = group.getSelection(); FileNode[] source_nodes = new FileNode[selection.length]; for(int i = 0; i < source_nodes.length; i++) { source_nodes[i] = (FileNode) selection[i].getLastPathComponent(); } ///ystem.err.println("Dropped files vector contains " + new_files.size() + " files."); event.acceptDrop(drag_action); // Action delete Gatherer.f_man.action(source, source_nodes, this, null); group.setSource(null); group.setSelection(null); } catch(Exception error) { error.printStackTrace(); event.rejectDrop(); } ignore = false; // Clear up the group.image_ghost paintImmediately(ra_ghost.getBounds()); event.getDropTargetContext().dropComplete(true); } /** Any implementation of DropTargetListener must include this method so we can be notified when the action to be taken upon drop changes. We never change so we don't do anything here. */ public void dropActionChanged(DropTargetDragEvent event) { } public void fileCopied(long id, DragComponent target_model, FileNode target_parent, FileNode record, boolean undo_event) { ///ystem.err.println("fileCopied(" + id + ", " + target_model + ", " + target_parent + ", " + record + ", " + undo_event + ")"); UndoJob job = new UndoJob(id, null, null, target_model, record, FILE_COPY); if(undo_event) { ///ystem.err.println("Add undo job"); undo.push(job); } else { ///ystem.err.println("Add redo job"); redo.push(job); } } public void fileDeleted(long id, DragComponent source_model, FileNode source_parent, FileNode target_parent, FileNode record, boolean undo_event) { UndoJob job = new UndoJob(id, source_model, source_parent, this, record, FILE_DELETE); if(undo_event) { ///ystem.err.println("Add undo job"); undo.push(job); } else { ///ystem.err.println("Add redo job"); redo.push(job); } } public void fileMoved(long id, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode target_parent, FileNode record, boolean undo_event) { ///ystem.err.println("fileMoved(" + id + ", " + source_model + ", " + source_parent + ", " + target_model + ", " + target_parent + ", " + record + ", " + undo_event + ")"); UndoJob job = new UndoJob(id, source_model, source_parent, target_model, record, FILE_MOVE); if(undo_event) { ///ystem.err.println("Add undo job"); undo.push(job); } else { ///ystem.err.println("Add redo job"); redo.push(job); } } /** Used to notify this component that it has gained focus by some method other that mouse focus. */ public void gainFocus(){ } public ArrayList getMetadata(File file) { ///ystem.err.println("UndoMetadata: " + file.getAbsolutePath()); return obsolete_metadata.getMetadata(file.getAbsolutePath(), true, new ArrayList(), file); } /** Any implementation of DragComponent must include this method so that a outsider can get at the underlying tree model behind the component. */ public FileSystemModel getTreeModel(){ return (FileSystemModel) model; } public boolean ignore() { return ignore; } /** This method is used to inform this component when it loses focus by means other than a drag mouse event, and should indicate this somehow. */ public void loseFocus(){ } public void registerRedoSource(AbstractButton source) { if(!redo_sources.contains(source)) { redo_sources.add(source); source.addActionListener(this); } } public void registerUndoSource(AbstractButton source) { if(!undo_sources.contains(source)) { undo_sources.add(source); source.addActionListener(this); } } public void setGroup(DragGroup group) { this.group = group; } public void undoAll() { FileQueue immediate_queue = new FileQueue(true); UndoJob undo_job = null; while((undo_job = undo.pop()) != null) { undo_job.action(true, immediate_queue); // Now retrieve all other undo jobs with the same job-number and action them while((undo_job = undo.getJob(undo_job.ID())) != null) { undo_job.action(true, immediate_queue); } } immediate_queue.run(); // Returns only when all undo actions complete. } static final public File generateUniqueFile(FileNode record) { String filename_raw = ArrayTools.objectArrayToString(record.getPath()); int hash_code = filename_raw.hashCode(); File file = new File(Utility.RECYCLE, String.valueOf(hash_code)); int offset = 65; while(file.exists() && offset != 90) { file = new File(Utility.RECYCLE, String.valueOf(hash_code) + (char)offset); offset++; } return file; } private void showTree() { JDialog dialog = new JDialog(Gatherer.g_man, "Recycle Bin Model"); dialog.setSize(new Dimension(400,300)); JPanel content_pane = (JPanel) dialog.getContentPane(); JTree tree = new JTree(model); content_pane.setLayout(new BorderLayout()); content_pane.add(new JScrollPane(tree), BorderLayout.CENTER); dialog.show(); } private class UndoJob { private FileNode record = null; private FileNode source_parent = null; private DragComponent source_model = null; private DragComponent target_model = null; private long id = 0; private int type = -1; private TreePath record_path = null; private TreePath source_parent_path = null; /** Undo file action Constructor. * @param id A unique long id number for all actions associated with a certain gesture. * @param source_model The source DragComponent where the new record originally came from. If this is null then it is assumed you are interested in using the 'recycle' bin model. * @param source_parent The previous parent FileNode of the new record. * @param target_model The target DragComponent where the new record is now. * @param record The new FileNode itself. * @param type An int indicating the action that has occured, not what undo action is needed. */ public UndoJob(long id, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode record, int type) { this.id = id; this.record = record; if(record != null) { this.record_path = new TreePath(record.getPath()); } this.source_model = source_model; this.source_parent = source_parent; if(source_parent != null) { this.source_parent_path = new TreePath(source_parent.getPath()); } this.target_model = target_model; this.type = type; } public void action(boolean is_undo, FileQueue file_queue) { // Retrieve the lastest version of each file record FileNode latest_record = null; FileNode latest_source_parent = null; if(target_model != null && record_path != null) { ///ystem.err.println("Retrieving latest version of record from " + target_model + "."); latest_record = ((FileSystemModel)target_model.getTreeModel()).getNode(record_path); } if(source_model != null && source_parent_path != null) { ///ystem.err.println("Retrieving latest version of source parent."); latest_source_parent = ((FileSystemModel)source_model.getTreeModel()).getNode(source_parent_path); } // Of course if there are no newer versions, stick to the ones we've already got. if(latest_record == null) { ///ystem.err.println("Using original record."); latest_record = record; } if(latest_source_parent == null) { ///ystem.err.println("Using original source parent."); latest_source_parent = source_parent; } // Heres the fraction, too much friction. switch(type) { case FILE_COPY: // To undo a file copy we issue a delete file action on the destination file record. file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.DELETE, !is_undo, true, false); break; case FILE_DELETE: // To undo a file delete we issue a copy file action from our recycle bin. file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.MOVE, !is_undo, true, false); break; case FILE_MOVE: // This may be a legitimate move, or may be a side effect of an undelete. If the formed source model and parent will be non-null. if(source_model != null && source_parent != null) { // To undo a file move we issue a move file action to return it to where it was. file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.MOVE, !is_undo, true, false); } // Otherwise we perform another delete. else { file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.DELETE, !is_undo, true, false); } break; default: System.err.println("Unknown code."); } } public FileNode getRecord() { return record; } public long ID() { return id; } } private class UndoJobAdder implements Runnable { private boolean undo_event; private FileNode record; private FileNode source_parent; private DragComponent source_model; private DragComponent target_model; private int type; private long id; public UndoJobAdder(long id, int type, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode record, boolean undo_event) { this.id = id; this.record = record; this.source_model = source_model; this.source_parent = source_parent; this.target_model = target_model; this.type = type; this.undo_event = undo_event; } public void run() { UndoJob job = new UndoJob(id, source_model, source_parent, target_model, record, type); if(undo_event) { undo.push(job); } else { redo.push(job); } } } private class UndoStack extends LinkedList { private boolean enabled = false; private boolean undo; private int pos = 0; public UndoStack(boolean undo) { this.undo = undo; } public void clear() { super.clear(); pos = 0; if(enabled) { setEnabled(false); } } public UndoJob getJob(long id) { UndoJob job = null; while(job == null && pos < size()) { UndoJob temp = (UndoJob) get(pos); if(temp.ID() == id) { job = temp; remove(temp); } else { pos++; } } if(size() == 0) { setEnabled(false); pos = 0; } return job; } public void push(UndoJob job) { addFirst(job); pos = 0; if(!enabled) { setEnabled(true); } } public UndoJob pop() { UndoJob job = null; if(size() > 0) { job = (UndoJob) removeFirst(); if(size() == 0 && enabled) { setEnabled(false); pos = 0; } } return job; } public void reset() { pos = 0; } private void setEnabled(boolean state) { ArrayList sources; if(undo) { sources = undo_sources; } else { sources = redo_sources; } for(int i = 0; i < sources.size(); i++) { AbstractButton source = (AbstractButton) sources.get(i); source.setEnabled(state); } enabled = state; } } }