source: trunk/gli/src/org/greenstone/gatherer/undo/UndoManager.java@ 4293

Last change on this file since 4293 was 4293, checked in by jmt12, 21 years ago

Initial revision

  • Property svn:keywords set to Author Date Id Revision
File size: 20.9 KB
Line 
1package org.greenstone.gatherer.undo;
2/**
3 *#########################################################################
4 *
5 * A component of the Gatherer application, part of the Greenstone digital
6 * library suite from the New Zealand Digital Library Project at the
7 * University of Waikato, New Zealand.
8 *
9 * <BR><BR>
10 *
11 * Author: John Thompson, Greenstone Digital Library, University of Waikato
12 *
13 * <BR><BR>
14 *
15 * Copyright (C) 1999 New Zealand Digital Library Project
16 *
17 * <BR><BR>
18 *
19 * This program is free software; you can redistribute it and/or modify
20 * it under the terms of the GNU General Public License as published by
21 * the Free Software Foundation; either version 2 of the License, or
22 * (at your option) any later version.
23 *
24 * <BR><BR>
25 *
26 * This program is distributed in the hope that it will be useful,
27 * but WITHOUT ANY WARRANTY; without even the implied warranty of
28 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 * GNU General Public License for more details.
30 *
31 * <BR><BR>
32 *
33 * You should have received a copy of the GNU General Public License
34 * along with this program; if not, write to the Free Software
35 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
36 *########################################################################
37 */
38import java.awt.*;
39import java.awt.datatransfer.Transferable;
40import java.awt.dnd.*;
41import java.awt.event.*;
42import java.awt.geom.AffineTransform;
43import java.io.*;
44import java.util.*;
45import javax.swing.*;
46import javax.swing.tree.*;
47import org.greenstone.gatherer.Gatherer;
48import org.greenstone.gatherer.file.FileJob;
49import org.greenstone.gatherer.file.FileNode;
50import org.greenstone.gatherer.file.FileQueue;
51import org.greenstone.gatherer.file.FileSystemModel;
52import org.greenstone.gatherer.msm.GDMDocument;
53import org.greenstone.gatherer.msm.Metadata;
54import org.greenstone.gatherer.msm.MSMEvent;
55import org.greenstone.gatherer.msm.MSMListenerAdapter;
56import org.greenstone.gatherer.util.ArrayTools;
57import org.greenstone.gatherer.util.DragComponent;
58import org.greenstone.gatherer.util.DragGroup;
59import org.greenstone.gatherer.util.Utility;
60/** 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.
61 * @author John Thompson, Greenstone Digital Library, University of Waikato
62 * @version 2.3c
63 */
64public class UndoManager
65 extends JButton
66 implements ActionListener, DragComponent, DropTargetListener {
67 private ArrayList redo_sources;
68 private ArrayList undo_sources;
69 private boolean ignore = false;
70 private boolean ignore_next = false;
71 /** 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. */
72 private DragGroup group;
73/** In order to make this button a drop target we have to create a DropTarget instance with the button as its target. */
74 private DropTarget drop_target;
75 private FileQueue file_queue;
76 private FileSystemModel model;
77 private GDMDocument obsolete_metadata;
78 /** What sort of action should a drag resemble. Not really used as we override with custom drag icon. */
79 private int drag_action = DnDConstants.ACTION_MOVE;
80 /** The last point the mouse was at. Used to repaint 'spoilt' area. */
81 private Point pt_last = null;
82 /** The area covered by the drag ghost, our custom drag icon. */
83 private Rectangle ra_ghost = new Rectangle();
84 private UndoStack redo;
85 private UndoStack undo;
86 static final public int FILE_COPY = 1;
87 static final public int FILE_DELETE = 2;
88 static final public int FILE_MOVE = 3;
89
90 public UndoManager() {
91 super(Utility.getImage("bin.gif"));
92 this.file_queue = Gatherer.f_man.getQueue();
93 this.drop_target = new DropTarget(this, drag_action, this, true);
94
95 setBackground(Gatherer.config.getColor("coloring.button_background", true));
96 setForeground(Gatherer.config.getColor("coloring.button_foreground", true));
97 setOpaque(true);
98
99 // Creation
100 File recycle_directory = new File(Utility.RECYCLE);
101 if(!recycle_directory.exists()) {
102 recycle_directory.mkdirs();
103 recycle_directory.deleteOnExit();
104 }
105 this.model = new FileSystemModel(new FileNode(recycle_directory, "Undo"));
106 obsolete_metadata = new GDMDocument(); // This GDM is never saved.
107 redo = new UndoStack(false);
108 redo_sources = new ArrayList();
109 undo = new UndoStack(true);
110 undo_sources = new ArrayList();
111 if(Gatherer.debug != null) {
112 showTree();
113 }
114 }
115
116 public void actionPerformed(ActionEvent event) {
117 // Is this an undo event source...
118 if(undo_sources.contains(event.getSource())) {
119 UndoJob undo_job = undo.pop();
120 undo_job.action(true, file_queue);
121 // Now retrieve all other undo jobs with the same job-number and action them
122 while((undo_job = undo.getJob(undo_job.ID())) != null) {
123 undo_job.action(true, file_queue);
124 }
125 }
126 // Or a redo one.
127 else if(redo_sources.contains(event.getSource())) {
128 UndoJob redo_job = redo.pop();
129 redo_job.action(false, file_queue);
130 // Now retrieve all other redo jobs with the same job-number and action them
131 while((redo_job = redo.getJob(redo_job.ID())) != null) {
132 redo_job.action(false, file_queue);
133 }
134 }
135 }
136
137 public void addMetadata(File file, ArrayList metadatum) {
138 for(int i = 0; metadatum != null && i < metadatum.size(); i++) {
139 Metadata metadata = (Metadata) metadatum.get(i);
140 ///ystem.err.println("UndoMetadata: " + file.getAbsolutePath() + " => " + metadata);
141 obsolete_metadata.addMetadata(file.getAbsolutePath(), metadata);
142 }
143 }
144
145 public void addUndo(long id, int type, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode record, boolean undo_event) {
146 if(target_model == null) {
147 target_model = this;
148 }
149 UndoJobAdder job_adder = new UndoJobAdder(id, type, source_model, source_parent, target_model, record, undo_event);
150 SwingUtilities.invokeLater(job_adder);
151 }
152
153 public void clear() {
154 undo.clear();
155 redo.clear();
156 }
157
158 /** 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. */
159 public void clearGhost(){
160 }
161
162 public void destroy() {
163 // Remove all references of this as a listener
164 for(int i = 0; i < redo_sources.size(); i++) {
165 AbstractButton source = (AbstractButton) redo_sources.get(i);
166 source.removeActionListener(this);
167 }
168 for(int j = 0; j < undo_sources.size(); j++) {
169 AbstractButton source = (AbstractButton) undo_sources.get(j);
170 source.removeActionListener(this);
171 }
172 redo_sources = null;
173 undo_sources = null;
174 redo = null;
175 undo = null;
176 obsolete_metadata = null;
177 file_queue = null;
178 }
179
180 /** 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. */
181 public void dragEnter(DropTargetDragEvent event) {
182 //ystem.err.println("Drag entered");
183 group.grabFocus(this);
184 setBackground(Gatherer.config.getColor("coloring.button_selected_background", true));
185 setForeground(Gatherer.config.getColor("coloring.button_selected_foreground", true));
186 }
187
188 /** 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. */
189 public void dragExit(DropTargetEvent event) {
190 //ystem.err.println("Drag exitted");
191 setBackground(Gatherer.config.getColor("coloring.button_background", true));
192 setForeground(Gatherer.config.getColor("coloring.button_foreground", true));
193 }
194
195 /** 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. */
196 public void dragOver(DropTargetDragEvent event) {
197 Graphics2D g2 = (Graphics2D) getGraphics();
198 Point pt = event.getLocation();
199 if(pt_last != null && pt.equals(pt_last)) {
200 return;
201 }
202 pt_last = pt;
203 if(!DragSource.isDragImageSupported()) {
204 // Erase the last ghost image and or cue line
205 paintImmediately(ra_ghost.getBounds());
206 // Remember where you are about to draw the new ghost image
207 ra_ghost.setRect(pt.x - group.mouse_offset.x, pt.y - group.mouse_offset.y, group.image_ghost.getWidth(), group.image_ghost.getHeight());
208 // Draw the ghost image
209 g2.drawImage(group.image_ghost, AffineTransform.getTranslateInstance(ra_ghost.getX(), ra_ghost.getY()), null);
210 }
211 }
212
213 /** 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(). */
214 public void drop(DropTargetDropEvent event) {
215 ignore = true;
216 group.grabFocus(this);
217 setBackground(Gatherer.config.getColor("coloring.button_background", true));
218 setForeground(Gatherer.config.getColor("coloring.button_foreground", true));
219 Transferable transferable = event.getTransferable();
220 try {
221 DragComponent source = group.getSource();
222 TreePath[] selection = group.getSelection();
223 FileNode[] source_nodes = new FileNode[selection.length];
224 for(int i = 0; i < source_nodes.length; i++) {
225 source_nodes[i] = (FileNode) selection[i].getLastPathComponent();
226 }
227 ///ystem.err.println("Dropped files vector contains " + new_files.size() + " files.");
228 event.acceptDrop(drag_action);
229 // Action delete
230 Gatherer.f_man.action(source, source_nodes, this, null);
231 group.setSource(null);
232 group.setSelection(null);
233 }
234 catch(Exception error) {
235 error.printStackTrace();
236 event.rejectDrop();
237 }
238 ignore = false;
239 // Clear up the group.image_ghost
240 paintImmediately(ra_ghost.getBounds());
241 event.getDropTargetContext().dropComplete(true);
242 }
243
244 /** 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. */
245 public void dropActionChanged(DropTargetDragEvent event) {
246 }
247
248 public void fileCopied(long id, DragComponent target_model, FileNode target_parent, FileNode record, boolean undo_event) {
249 ///ystem.err.println("fileCopied(" + id + ", " + target_model + ", " + target_parent + ", " + record + ", " + undo_event + ")");
250 UndoJob job = new UndoJob(id, null, null, target_model, record, FILE_COPY);
251 if(undo_event) {
252 ///ystem.err.println("Add undo job");
253 undo.push(job);
254 }
255 else {
256 ///ystem.err.println("Add redo job");
257 redo.push(job);
258 }
259 }
260
261 public void fileDeleted(long id, DragComponent source_model, FileNode source_parent, FileNode target_parent, FileNode record, boolean undo_event) {
262 UndoJob job = new UndoJob(id, source_model, source_parent, this, record, FILE_DELETE);
263 if(undo_event) {
264 ///ystem.err.println("Add undo job");
265 undo.push(job);
266 }
267 else {
268 ///ystem.err.println("Add redo job");
269 redo.push(job);
270 }
271 }
272
273 public void fileMoved(long id, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode target_parent, FileNode record, boolean undo_event) {
274 ///ystem.err.println("fileMoved(" + id + ", " + source_model + ", " + source_parent + ", " + target_model + ", " + target_parent + ", " + record + ", " + undo_event + ")");
275 UndoJob job = new UndoJob(id, source_model, source_parent, target_model, record, FILE_MOVE);
276 if(undo_event) {
277 ///ystem.err.println("Add undo job");
278 undo.push(job);
279 }
280 else {
281 ///ystem.err.println("Add redo job");
282 redo.push(job);
283 }
284 }
285
286 /** Used to notify this component that it has gained focus by some method other that mouse focus. */
287 public void gainFocus(){
288 }
289
290 public ArrayList getMetadata(File file) {
291 ///ystem.err.println("UndoMetadata: " + file.getAbsolutePath());
292 return obsolete_metadata.getMetadata(file.getAbsolutePath(), true, new ArrayList(), file);
293 }
294
295 /** Any implementation of DragComponent must include this method so that a outsider can get at the underlying tree model behind the component. */
296 public FileSystemModel getTreeModel(){
297 return (FileSystemModel) model;
298 }
299
300 public boolean ignore() {
301 return ignore;
302 }
303
304 /** 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. */
305 public void loseFocus(){
306 }
307
308 public void registerRedoSource(AbstractButton source) {
309 if(!redo_sources.contains(source)) {
310 redo_sources.add(source);
311 source.addActionListener(this);
312 }
313 }
314
315 public void registerUndoSource(AbstractButton source) {
316 if(!undo_sources.contains(source)) {
317 undo_sources.add(source);
318 source.addActionListener(this);
319 }
320 }
321
322 public void setGroup(DragGroup group) {
323 this.group = group;
324 }
325
326 public void undoAll() {
327 FileQueue immediate_queue = new FileQueue(true);
328 UndoJob undo_job = null;
329 while((undo_job = undo.pop()) != null) {
330 undo_job.action(true, immediate_queue);
331 // Now retrieve all other undo jobs with the same job-number and action them
332 while((undo_job = undo.getJob(undo_job.ID())) != null) {
333 undo_job.action(true, immediate_queue);
334 }
335 }
336 immediate_queue.run();
337 // Returns only when all undo actions complete.
338 }
339
340 static final public File generateUniqueFile(FileNode record) {
341 String filename_raw = ArrayTools.objectArrayToString(record.getPath());
342 int hash_code = filename_raw.hashCode();
343 File file = new File(Utility.RECYCLE, String.valueOf(hash_code));
344 int offset = 65;
345 while(file.exists() && offset != 90) {
346 file = new File(Utility.RECYCLE, String.valueOf(hash_code) + (char)offset);
347 offset++;
348 }
349 return file;
350 }
351
352 private void showTree() {
353 JDialog dialog = new JDialog(Gatherer.g_man, "Recycle Bin Model");
354 dialog.setSize(new Dimension(400,300));
355 JPanel content_pane = (JPanel) dialog.getContentPane();
356 JTree tree = new JTree(model);
357 content_pane.setLayout(new BorderLayout());
358 content_pane.add(new JScrollPane(tree), BorderLayout.CENTER);
359 dialog.show();
360 }
361
362 private class UndoJob {
363 private FileNode record = null;
364 private FileNode source_parent = null;
365 private DragComponent source_model = null;
366 private DragComponent target_model = null;
367 private long id = 0;
368 private int type = -1;
369 private TreePath record_path = null;
370 private TreePath source_parent_path = null;
371 /** Undo file action Constructor.
372 * @param id A unique <i>long</i> id number for all actions associated with a certain gesture.
373 * @param source_model The source <strong>DragComponent</strong> where the new record originally came from. If this is <i>null</i> then it is assumed you are interested in using the 'recycle' bin model.
374 * @param source_parent The previous parent <strong>FileNode</strong> of the new record.
375 * @param target_model The target <strong>DragComponent</strong> where the new record is now.
376 * @param record The new <strong>FileNode</strong> itself.
377 * @param type An <i>int</i> indicating the action that has occured, not what undo action is needed.
378 */
379 public UndoJob(long id, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode record, int type) {
380 this.id = id;
381 this.record = record;
382 if(record != null) {
383 this.record_path = new TreePath(record.getPath());
384 }
385 this.source_model = source_model;
386 this.source_parent = source_parent;
387 if(source_parent != null) {
388 this.source_parent_path = new TreePath(source_parent.getPath());
389 }
390 this.target_model = target_model;
391 this.type = type;
392 }
393
394 public void action(boolean is_undo, FileQueue file_queue) {
395 // Retrieve the lastest version of each file record
396 FileNode latest_record = null;
397 FileNode latest_source_parent = null;
398 if(target_model != null && record_path != null) {
399 ///ystem.err.println("Retrieving latest version of record from " + target_model + ".");
400 latest_record = ((FileSystemModel)target_model.getTreeModel()).getNode(record_path);
401 }
402 if(source_model != null && source_parent_path != null) {
403 ///ystem.err.println("Retrieving latest version of source parent.");
404 latest_source_parent = ((FileSystemModel)source_model.getTreeModel()).getNode(source_parent_path);
405 }
406 // Of course if there are no newer versions, stick to the ones we've already got.
407 if(latest_record == null) {
408 ///ystem.err.println("Using original record.");
409 latest_record = record;
410 }
411 if(latest_source_parent == null) {
412 ///ystem.err.println("Using original source parent.");
413 latest_source_parent = source_parent;
414 }
415 // Heres the fraction, too much friction.
416 switch(type) {
417 case FILE_COPY:
418 // To undo a file copy we issue a delete file action on the destination file record.
419 file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.DELETE, !is_undo, true, false);
420 break;
421 case FILE_DELETE:
422 // To undo a file delete we issue a copy file action from our recycle bin.
423 file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.MOVE, !is_undo, true, false);
424 break;
425 case FILE_MOVE:
426 // 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.
427 if(source_model != null && source_parent != null) {
428 // To undo a file move we issue a move file action to return it to where it was.
429 file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.MOVE, !is_undo, true, false);
430 }
431 // Otherwise we perform another delete.
432 else {
433 file_queue.addJob(id, target_model, latest_record, source_model, latest_source_parent, FileJob.DELETE, !is_undo, true, false);
434 }
435 break;
436 default:
437 System.err.println("Unknown code.");
438 }
439 }
440
441 public FileNode getRecord() {
442 return record;
443 }
444
445 public long ID() {
446 return id;
447 }
448 }
449
450 private class UndoJobAdder
451 implements Runnable {
452 private boolean undo_event;
453 private FileNode record;
454 private FileNode source_parent;
455 private DragComponent source_model;
456 private DragComponent target_model;
457 private int type;
458 private long id;
459 public UndoJobAdder(long id, int type, DragComponent source_model, FileNode source_parent, DragComponent target_model, FileNode record, boolean undo_event) {
460 this.id = id;
461 this.record = record;
462 this.source_model = source_model;
463 this.source_parent = source_parent;
464 this.target_model = target_model;
465 this.type = type;
466 this.undo_event = undo_event;
467 }
468 public void run() {
469 UndoJob job = new UndoJob(id, source_model, source_parent, target_model, record, type);
470 if(undo_event) {
471 undo.push(job);
472 }
473 else {
474 redo.push(job);
475 }
476 }
477 }
478
479 private class UndoStack
480 extends LinkedList {
481 private boolean enabled = false;
482 private boolean undo;
483 private int pos = 0;
484 public UndoStack(boolean undo) {
485 this.undo = undo;
486 }
487 public void clear() {
488 super.clear();
489 pos = 0;
490 if(enabled) {
491 setEnabled(false);
492 }
493 }
494 public UndoJob getJob(long id) {
495 UndoJob job = null;
496 while(job == null && pos < size()) {
497 UndoJob temp = (UndoJob) get(pos);
498 if(temp.ID() == id) {
499 job = temp;
500 remove(temp);
501 }
502 else {
503 pos++;
504 }
505 }
506 if(size() == 0) {
507 setEnabled(false);
508 pos = 0;
509 }
510 return job;
511 }
512 public void push(UndoJob job) {
513 addFirst(job);
514 pos = 0;
515 if(!enabled) {
516 setEnabled(true);
517 }
518 }
519 public UndoJob pop() {
520 UndoJob job = null;
521 if(size() > 0) {
522 job = (UndoJob) removeFirst();
523 if(size() == 0 && enabled) {
524 setEnabled(false);
525 pos = 0;
526 }
527 }
528 return job;
529 }
530 public void reset() {
531 pos = 0;
532 }
533 private void setEnabled(boolean state) {
534 ArrayList sources;
535 if(undo) {
536 sources = undo_sources;
537 }
538 else {
539 sources = redo_sources;
540 }
541 for(int i = 0; i < sources.size(); i++) {
542 AbstractButton source = (AbstractButton) sources.get(i);
543 source.setEnabled(state);
544 }
545 enabled = state;
546 }
547 }
548}
Note: See TracBrowser for help on using the repository browser.