/******************************************************************************* * Copyright (c) 2005 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.bpel.common.ui.editmodel; import java.util.ArrayList; import java.util.Arrays; import java.util.EventObject; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.bpel.common.ui.CommonUIPlugin; import org.eclipse.bpel.common.ui.Messages; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IStatus; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.gef.commands.Command; import org.eclipse.gef.commands.CommandStack; import org.eclipse.gef.commands.CommandStackListener; import org.eclipse.gef.commands.CompoundCommand; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWorkbenchWindow; /** * Implements the CommandStack API, adds extra notifications, * supports validateEdit and update the resource's dirty state. * * WDG: TODO: the GEF CommandStack has been extended with extra notifications. * However, how are we supposed to use it since everything is still private? */ public class EditModelCommandStack extends CommandStack { protected int saveLocation = 0; protected int fCurrentLocation = 0; protected Set<Resource> dirtyUntilSave = new HashSet<Resource>(); protected List<Context> fContexts = new ArrayList<Context>(30); /** * Brand new shiny EditModelCommandStack. */ public EditModelCommandStack() { super(); } /** * @see org.eclipse.gef.commands.CommandStack#getUndoCommand() */ @Override public Command getUndoCommand() { if (fCurrentLocation < 1) { return null; } return fContexts.get(fCurrentLocation-1).fCommand; } /** * @see org.eclipse.gef.commands.CommandStack#canUndo() */ @Override public boolean canUndo() { Command c = getUndoCommand(); return (c != null) && c.canUndo(); } /** * @see org.eclipse.gef.commands.CommandStack#getRedoCommand() */ @Override public Command getRedoCommand() { if (fCurrentLocation >= fContexts.size()) return null; return fContexts.get(fCurrentLocation).fCommand; } /** * @see org.eclipse.gef.commands.CommandStack#canRedo() */ @Override public boolean canRedo() { return (getRedoCommand() != null); } /** * @see org.eclipse.gef.commands.CommandStack#execute(org.eclipse.gef.commands.Command) */ @Override public void execute (Command command) { SharedCommandStackChangedEvent event = notifyListeners(SharedCommandStackListener.EVENT_START_EXECUTE); if (!event.doit) return; if(getUndoCommand() instanceof PlaceHolderCommand) { // This should never happen because the EditModelCommandFramework should // remove the placeholder during the notifyListeners() call above. throw new IllegalStateException(); } if (command == null) return; if (!validateEdit(command)) return; if (!command.canExecute()) return; drop(fCurrentLocation, fContexts.size()); // Paranoia check if(getUndoCommand() instanceof PlaceHolderCommand) { // This should never happen because the EditModelCommandFramework should // remove the placeholder during the notifyListeners() call above. throw new IllegalStateException(); } command.execute(); if(getUndoCommand() instanceof PlaceHolderCommand) { // This should never happen because the EditModelCommandFramework should // remove the placeholder during the notifyListeners() call above. throw new IllegalStateException(); } int limit = getUndoLimit(); if (limit > 0) while (fCurrentLocation >= limit) { if (saveLocation == 0) saveLocation = -1; drop(0); notifyListeners(SharedCommandStackListener.EVENT_DROP_LAST_UNDO_STACK_ENTRY); } if(getUndoCommand() instanceof PlaceHolderCommand) { // This should never happen because the EditModelCommandFramework should // remove the placeholder during the notifyListeners() call above. throw new IllegalStateException(); } Resource[] resources = getModifiedResources(command); if ((resources.length > 0) || (command instanceof PlaceHolderCommand)) { Context c = new Context(command, resources); fContexts.add(c); fCurrentLocation = fContexts.size(); // mark resources as dirty/clean as appropriate. c.setModifiedFlags(true); //System.out.println("execute - markModified. currentLocation="+currentLocation+", saveLocation="+saveLocation); } notifyListeners(SharedCommandStackListener.EVENT_FINISH_EXECUTE); } /* * call VCM validateEdit; * open the dialog to ask the user if he/she wants to procede * in case the file is readonly; * return true if the command should be executed otherwise returns false; */ protected boolean validateEdit(Command command) { Resource[] resources = getResources(command); if(resources.length == 0) return true; boolean disposeShell = false; Shell shell; IWorkbenchWindow win = CommonUIPlugin.getDefault().getWorkbench().getActiveWorkbenchWindow(); if (win != null) { shell = win.getShell(); } else { disposeShell = true; shell = new Shell(); } try { IFile[] files = new IFile[resources.length]; StringBuffer filesString = new StringBuffer(); for (int i = 0; i < resources.length; i++) { Resource resource = resources[i]; files[i] = EditModel.getIFileForURI(resource.getURI()); filesString.append(files[i].getName()); if(i < resources.length - 1) filesString.append(", "); //$NON-NLS-1$ } IStatus stat = ResourcesPlugin.getWorkspace().validateEdit(files, shell); if (stat.getSeverity() == IStatus.CANCEL) { return false; } else if (!stat.isOK()) { String[] buttons = { IDialogConstants.OK_LABEL }; // String msg; if(files.length == 1) msg = NLS.bind(Messages.EditModelCommandStack_validateEdit_message0, (new String[]{filesString.toString(),stat.getMessage()})); else msg = NLS.bind(Messages.EditModelCommandStack_validateEdit_message1, (new String[]{filesString.toString(),stat.getMessage()})); MessageDialog dialog = new MessageDialog( shell, Messages.EditModelCommandStack_validateEdit_title, null, // accept the default windowing system icon msg, MessageDialog.WARNING, buttons, 0); dialog.open(); return false; } } finally { if(disposeShell) shell.dispose(); } return true; } /** * @see org.eclipse.gef.commands.CommandStack#dispose() */ @Override public void dispose() { drop(0,fContexts.size()); } /** * @see org.eclipse.gef.commands.CommandStack#flush() */ @Override public void flush() { SharedCommandStackChangedEvent event = notifyListeners(SharedCommandStackListener.EVENT_START_FLUSH); if (!event.doit) return; drop(0, fContexts.size()); fContexts.clear(); saveLocation = -1; fCurrentLocation = 0; // TODO: should we mark all resources as clean? notifyListeners(SharedCommandStackListener.EVENT_FINISH_FLUSH); } /** * @see org.eclipse.gef.commands.CommandStack#getCommands() */ @Override public Object[] getCommands() { Object[] commands = new Object[fContexts.size()]; for (int i = 0; i < fContexts.size(); i++) { commands[i] = fContexts.get(i).fCommand; } return commands; } /** * @see org.eclipse.gef.commands.CommandStack#isDirty() */ @Override public boolean isDirty() { //System.out.println("isDirty: C="+currentLocation+" S="+saveLocation+" dus="+dirtyUntilSave.size()); return (fCurrentLocation != saveLocation) || !dirtyUntilSave.isEmpty(); } /** * @see org.eclipse.gef.commands.CommandStack#markSaveLocation() */ @Override public void markSaveLocation() { // // mark all the resources we know about as clean! // for (int i = 0; i<contexts.size(); i++) { // Context c = (Context)contexts.get(i); // c.setModifiedFlags(false); // } // // that includes ones that fell off the bottom of the undo stack. // for (Iterator it = dirtyUntilSave.iterator(); it.hasNext(); ) { // setResourceModified((Resource)it.next(), false); // } dirtyUntilSave.clear(); saveLocation = fCurrentLocation; notifyListeners(SharedCommandStackListener.EVENT_MARK_SAVED); } /** * @see org.eclipse.gef.commands.CommandStack#undo() */ @Override public void undo() { SharedCommandStackChangedEvent event = notifyListeners(SharedCommandStackListener.EVENT_START_UNDO); if (!event.doit) return; if (!canUndo()) return; Context c = fContexts.get(fCurrentLocation-1); c.fCommand.undo(); fCurrentLocation--; // mark resources as dirty/clean as appropriate. if (fCurrentLocation < saveLocation) { // moving away from save location --> resources can only become dirty c.setModifiedFlags(true); // System.out.println("undo - markModified. currentLocation="+currentLocation+", saveLocation="+saveLocation); } else { // moving towards save location --> resources can only become clean updateModifiedFlags(); } notifyListeners(); notifyListeners(SharedCommandStackListener.EVENT_FINISH_UNDO); } /** * @see org.eclipse.gef.commands.CommandStack#redo() */ @Override public void redo() { SharedCommandStackChangedEvent event = notifyListeners(SharedCommandStackListener.EVENT_START_REDO); if (!event.doit) return; if (!canRedo()) return; Context c = fContexts.get(fCurrentLocation); c.fCommand.redo(); fCurrentLocation++; // mark resources as dirty/clean as appropriate. if (fCurrentLocation > saveLocation) { // moving away from save location --> resources can only become dirty c.setModifiedFlags(true); //System.out.println("redo - markModified. currentLocation="+currentLocation+", saveLocation="+saveLocation); } else { // moving towards save location --> resources can only become clean updateModifiedFlags(); } if(getUndoCommand() instanceof PlaceHolderCommand) { // This should never happen throw new IllegalStateException(); } notifyListeners(); notifyListeners(SharedCommandStackListener.EVENT_FINISH_REDO); } /** * Sends notification to all {@link CommandStackListener}s. */ protected SharedCommandStackChangedEvent notifyListeners(int property) { SharedCommandStackChangedEvent event = new SharedCommandStackChangedEvent(this); event.property = property; for (Object next : listeners) { CommandStackListener csl = (CommandStackListener) next; csl.commandStackChanged(event); } return event; } /* * Helper to remove a command from any point in the stack. */ protected void drop (int pos) { //System.out.println(" (drop "+pos+") C="+currentLocation+" S="+saveLocation); if ((pos < 0) || pos >= fContexts.size()) { throw new IllegalArgumentException(); } Context c = fContexts.get(pos); int a = Math.min(saveLocation, fCurrentLocation); int b = Math.max(saveLocation, fCurrentLocation); if ((a <= pos) && (pos < b)) { // we're dropping something between current and save point. dirtyUntilSave.addAll(Arrays.asList(c.fResources)); //System.out.println("dus = "+dirtyUntilSave); } c.fCommand.dispose(); fContexts.remove(pos); if (fCurrentLocation > pos) { fCurrentLocation--; } if (saveLocation > pos) { saveLocation--; } } /* * Helper to remove a range of commands from anywhere in the stack. */ protected void drop(int from, int to) { if (to < from) { int a=to; to=from; from=a; } //System.out.println("drop: "+to+".."+from); while (to > from) { drop(from); to--; } } /* * Helper to mark a resource as clean or dirty (mostly for ease of debugging) */ protected static void setResourceModified(Resource r, boolean modified) { if (r.isModified() != modified) { //System.out.println("> "+modified+" : "+r); r.setModified(modified); } } /* * Helper to calculate which resources should be marked as dirty */ protected void updateModifiedFlags() { //System.out.println("calculateModifiedState()"); Set<Resource> cleanResources = new HashSet<Resource>(); // for starters, treat everything as clean for (Context c : fContexts) { cleanResources.addAll(Arrays.asList(c.fResources)); } // mark things that fell off the bottom of the undo stack as dirty for (Resource resource : dirtyUntilSave) { cleanResources.remove(resource); setResourceModified(resource, true); } // mark things modified between saveLocation and currentLocation as dirty int a = Math.min(fCurrentLocation, saveLocation); int b = Math.max(fCurrentLocation, saveLocation); for (int i = Math.max(a,0); i<b; i++) { Context c = fContexts.get(i); cleanResources.removeAll(Arrays.asList(c.fResources)); c.setModifiedFlags(true); } // mark anything we still consider clean as clean for (Resource resource : cleanResources) { setResourceModified(resource, false); } } /** * SharedCommandStackListener * */ public static interface SharedCommandStackListener extends CommandStackListener { public static final int EVENT_START_EXECUTE = 1; public static final int EVENT_FINISH_EXECUTE = 2; public static final int EVENT_START_UNDO = 3; public static final int EVENT_FINISH_UNDO = 4; public static final int EVENT_START_REDO = 5; public static final int EVENT_FINISH_REDO = 6; public static final int EVENT_START_FLUSH = 7; public static final int EVENT_FINISH_FLUSH = 8; public static final int EVENT_START_MARK_SAVED = 9; public static final int EVENT_FINISH_MARK_SAVED = 10; public static final int EVENT_DROP_LAST_UNDO_STACK_ENTRY = 11; public static final int EVENT_MARK_SAVED = 12; } public static class SharedCommandStackChangedEvent extends EventObject { int property; public boolean doit = true; SharedCommandStackChangedEvent(Object source) { super(source); } public EditModelCommandStack getStack() { return (EditModelCommandStack)getSource(); } public int getProperty() { return property; } } protected static Resource[] EMPTY_RESOURCE_ARRAY = new Resource[0]; // TODO: should this be in a utility class? Can it be made extensible? public static Resource[] getResources(Command command) { if (command instanceof IEditModelCommand) { return ((IEditModelCommand)command).getResources(); } if (command instanceof CompoundCommand) { CompoundCommand ccmd = (CompoundCommand) command; Set<Resource> set = new HashSet<Resource>(); for (Object n : ccmd.getChildren() ) { for (Resource r : getResources((Command)n)) { set.add(r); } } if (set.isEmpty()) { return EMPTY_RESOURCE_ARRAY; } return set.toArray( EMPTY_RESOURCE_ARRAY ); } throw new IllegalArgumentException(); } // TODO: should this be in a utility class? Can it be made extensible? public static Resource[] getModifiedResources(Command command) { if (command instanceof IEditModelCommand) { return ((IEditModelCommand)command).getModifiedResources(); } if (command instanceof CompoundCommand) { CompoundCommand ccmd = (CompoundCommand) command; Set<Resource> set = new HashSet<Resource>(); for (Object n : ccmd.getChildren() ) { for (Resource r : getModifiedResources((Command)n)) { set.add(r); } } if (set.isEmpty()) { return EMPTY_RESOURCE_ARRAY; } return set.toArray( EMPTY_RESOURCE_ARRAY ); } throw new IllegalArgumentException(); } protected static class Context { public Command fCommand; public Resource[] fResources; public Context(Command command, Resource[] resources) { this.fCommand = command; this.fResources = resources; } public void setModifiedFlags (boolean value) { for (Resource r : fResources) { setResourceModified(r, value); } } } }