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

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

Given we aren't even using undo anymore, supressed the dialog window that opened up if you ran gli with -debug

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