Some Eclipse Foundation services are deprecated, or will be soon. Please ensure you've read this important communication.
Bug 317932 - Command (handler) and bindings that supports moving items up or down, with optional copying
Summary: Command (handler) and bindings that supports moving items up or down, with op...
Status: NEW
Alias: None
Product: EMF
Classification: Modeling
Component: Edit (show other bugs)
Version: unspecified   Edit
Hardware: Macintosh Mac OS X - Carbon (unsup.)
: P3 enhancement (vote)
Target Milestone: ---   Edit
Assignee: Ed Merks CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2010-06-25 04:40 EDT by Hallvard Traetteberg CLA
Modified: 2018-01-22 12:17 EST (History)
1 user (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Hallvard Traetteberg CLA 2010-06-25 04:40:15 EDT
In Eclipse's text editor, I use the command and keybindings for moving and copying lines of code all the time. For editing and restructuring EMF data, I sometimes open the ecore/xmi file in a text editor and use the same commands. Now I've implemented a similar set of commands for the EMF editors.

Below is an implementation of a command (handler) that supports moving items up or down, with optional copying. At the bottom, you'll find markup to insert into plugin.xml, to utilize the command handler.

/*******************************************************************************
 * Copyright (c) 2010 Hallvard Traetteberg.
 * 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:
 *     Hallvard Traetteberg - initial API and implementation

******************************************************************************/
package org.eclipse.e4.emf.javascript.ui;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandlerListener;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.MoveCommand;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;

public class NudgeEObjectCommandHandler extends AbstractHandler {

    public NudgeEObjectCommandHandler() {
        super();
    }
    
    public void dispose() {
    }

    private EObject getSelection(IWorkbenchWindow wbw) {
        return (wbw != null && wbw.getActivePage() != null ? getSelection(wbw.getActivePage().getSelection()) : null);
    }
    private EditingDomain getEditingDomainProvider(IWorkbenchWindow wbw) {
        if (wbw == null || wbw.getActivePage() == null) {
            return null;
        }
        IWorkbenchPart part = wbw.getActivePage().getActivePart();
        return (part instanceof IEditingDomainProvider ? ((IEditingDomainProvider) part).getEditingDomain() : null);
    }

    private EObject getSelection(ISelection selection) {
        if (selection instanceof IStructuredSelection && ((IStructuredSelection)selection).getFirstElement() instanceof EObject) {
            return (EObject) (((IStructuredSelection) selection).getFirstElement());
        }
        return null;
    }

    private final String movementDeltaParameterId = "org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta";
    private final String shouldCopyParameterId = "org.eclipse.e4.emf.ui.NudgeEObjectCommand.shouldCopy";
    
    public Object execute(ExecutionEvent event) throws ExecutionException {
        EObject eObject = getSelection(HandlerUtil.getActiveWorkbenchWindow(event));
        EReference containmentFeature = eObject.eClass().eContainmentFeature();
        if (containmentFeature != null && containmentFeature.isMany()) {
            EList<EObject> siblings = (EList<EObject>) eObject.eContainer().eGet(containmentFeature);
            int pos = siblings.indexOf(eObject);
            String movementDelta = event.getParameter(movementDeltaParameterId);
            boolean shouldCopy = "true".equals(event.getParameter(shouldCopyParameterId));
            int d = 0;
            if (movementDelta != null) {
                try {
                    d = Integer.parseInt(movementDelta);
                } catch (NumberFormatException e) {
                }
            }
            int newPos = pos + d;
            if (newPos < 0) {
                newPos = 0;
            } else if (newPos >= siblings.size()) {
                newPos = siblings.size() - 1;
            }
            if (shouldCopy) {
                eObject = EcoreUtil.copy(eObject);
            }
            if (shouldCopy || newPos != pos) {
                EditingDomain editingDomain = getEditingDomainProvider(HandlerUtil.getActiveWorkbenchWindow(event));
                Command command = (shouldCopy ? new AddCommand(editingDomain, siblings, eObject, newPos) : new MoveCommand(editingDomain, siblings, eObject, newPos));
                if (editingDomain != null) {
                    editingDomain.getCommandStack().execute(command);
                } else {
                    command.execute();
                }
            }
        }
        return eObject;
    }

    public boolean isEnabled() {
        EObject eObject = getSelection(PlatformUI.getWorkbench().getActiveWorkbenchWindow());
        return (eObject != null);
    }

    public boolean isHandled() {
        return true;
    }

    public void addHandlerListener(IHandlerListener handlerListener) {
    }
    public void removeHandlerListener(IHandlerListener handlerListener) {
    }
}

  <extension point="org.eclipse.ui.commands">
      <command
            id="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
            name="Move EObject (up or down)">
            <commandParameter

id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta"
                  name="movementDelta"
                  optional="true">
            </commandParameter>
            <commandParameter
                  id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.shouldCopy"
                  name="shouldCopy"
                  optional="true">
            </commandParameter>
      </command>
   </extension>

   <extension point="org.eclipse.ui.handlers">
      <handler

class="org.eclipse.e4.emf.javascript.ui.NudgeEObjectCommandHandler"
            commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand">
      </handler>
   </extension>

    <extension point="org.eclipse.ui.bindings">
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M3+PAGE_UP"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="0x80000000"/>
      </key>
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M1+M3+PAGE_UP"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="0x80000000"/>
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.shouldCopy" value="true"/>
      </key>
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M3+ARROW_UP"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="-1"/>
      </key>
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M1+M3+ARROW_UP"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="-1"/>
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.shouldCopy" value="true"/>
      </key>
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M3+ARROW_DOWN"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="1"/>
      </key>
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M1+M3+ARROW_DOWN"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="1"/>
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.shouldCopy" value="true"/>
      </key>
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M3+PAGE_DOWN"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="0x7fffffff"/>
      </key>
      <key commandId="org.eclipse.e4.emf.ui.NudgeEObjectCommand"
           contextId="org.eclipse.ui.contexts.window"
           sequence="M1+M3+PAGE_DOWN"
           schemeId="org.eclipse.ui.defaultAcceleratorConfiguration">
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta" value="0x7fffffff"/>
           <parameter id="org.eclipse.e4.emf.ui.NudgeEObjectCommand.shouldCopy" value="true"/>
      </key>
   </extension>
Comment 1 Ed Merks CLA 2010-06-25 11:28:30 EDT
Hallvard,

Just glancing at this, I wonder about this line:

eObject.eClass().eContainmentFeature()

Doesn't this always return EcorePackage.Literals.EPACKAGE_ECLASSIFIERS?

I wonder if we did this more generally using the editing domain's getParent and using the index in the parent's getChildren, without specifying a feature at all, if the same thing would work for any tree structure induced by the item providers, i.e., it could even move wrapped objects and objects within non-containment references.  After all, ItemProviderAdapter.factorMoveCommand would deduce a feature and would try to move the object to right place in the overall index in the full list of children.
Comment 2 Hallvard Traetteberg CLA 2010-06-28 10:15:01 EDT
(In reply to comment #1)
> Hallvard,
> 
> Just glancing at this, I wonder about this line:
> eObject.eClass().eContainmentFeature()
> Doesn't this always return EcorePackage.Literals.EPACKAGE_ECLASSIFIERS?

You're right! I guess it should only be eObject.eContainmentFeature(). ebject.eClass().eContainmentFeature() worked because I only tested it on EClasses in the Ecore editor.

> I wonder if we did this more generally using the editing domain's getParent and
> using the index in the parent's getChildren, without specifying a feature at
> all, if the same thing would work for any tree structure induced by the item
> providers, i.e., it could even move wrapped objects and objects within
> non-containment references.  After all, ItemProviderAdapter.factorMoveCommand
> would deduce a feature and would try to move the object to right place in the
> overall index in the full list of children.

So the idea would be to let the ItemProvider, which provides the actual tree view of the underlying EObjects and ELists, decide how to move/copy the selection?

Here's a second attempt, based on this suggestion. I'm sure it can be improved, but I hope it's close to what you're suggesting.

/*******************************************************************************
 * Copyright (c) 2010 Hallvard Traetteberg.
 * 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:
 *     Hallvard Traetteberg - initial API and implementation
 ******************************************************************************/
package org.eclipse.e4.emf.javascript.ui;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.IHandlerListener;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.notify.AdapterFactory;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.MoveCommand;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.emf.edit.provider.ITreeItemContentProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.handlers.HandlerUtil;

public class NudgeEObjectCommandHandler extends AbstractHandler {

	public NudgeEObjectCommandHandler() {
		super();
	}
	
	public void dispose() {
	}

	private EObject getSelection(IWorkbenchWindow wbw) {
		return (wbw != null && wbw.getActivePage() != null ? getSelection(wbw.getActivePage().getSelection()) : null);
	}
	private EditingDomain getEditingDomainProvider(IWorkbenchWindow wbw) {
		if (wbw == null || wbw.getActivePage() == null) {
			return null;
		}
		IWorkbenchPart part = wbw.getActivePage().getActivePart();
		return (part instanceof IEditingDomainProvider ? ((IEditingDomainProvider) part).getEditingDomain() : null);
	}

	private EObject getSelection(ISelection selection) {
		if (selection instanceof IStructuredSelection && ((IStructuredSelection)selection).getFirstElement() instanceof EObject) {
			return (EObject) (((IStructuredSelection) selection).getFirstElement());
		}
		return null;
	}

	private final String movementDeltaParameterId = "org.eclipse.e4.emf.ui.NudgeEObjectCommand.movementDelta";
	private final String shouldCopyParameterId = "org.eclipse.e4.emf.ui.NudgeEObjectCommand.shouldCopy";
	
	public Object execute(ExecutionEvent event) throws ExecutionException {
		Command command = createCommand(event);
		if (command != null && command.canExecute()) {
			EditingDomain editingDomain = getEditingDomainProvider(HandlerUtil.getActiveWorkbenchWindow(event));
			if (editingDomain != null) {
				editingDomain.getCommandStack().execute(command);
			} else {
				command.execute();
			}
			return command.getAffectedObjects();
		}
		return null;
	}

	public Command createCommand(ExecutionEvent event) {
		// first, get the selection
		Object object = getSelection(HandlerUtil.getActiveWorkbenchWindow(event));
		if (object == null) {
			return null;
		}
		
		// second, find the parent and siblings, and siblings size (the siblings are needed for finding the position)
		Object parent = null;
		Collection<?> siblings = null;
		EditingDomain editingDomain = getEditingDomainProvider(HandlerUtil.getActiveWorkbenchWindow(event));
		if (editingDomain instanceof AdapterFactoryEditingDomain) {
			AdapterFactory adapterFactory = ((AdapterFactoryEditingDomain) editingDomain).getAdapterFactory();
			parent = ((ITreeItemContentProvider) adapterFactory.adapt(object, ITreeItemContentProvider.class)).getParent(object);
			siblings = ((ITreeItemContentProvider) adapterFactory.adapt(parent, ITreeItemContentProvider.class)).getChildren(parent);
		} else if (object instanceof EObject) {
			EReference containmentFeature = ((EObject) object).eContainmentFeature();		
			if (containmentFeature != null && containmentFeature.isMany()) {
				EObject container = ((EObject) object).eContainer();
				parent = container;
				siblings = (Collection<?>) container.eGet(containmentFeature);
			}
		}
		if (parent == null || siblings == null) {
			return null;
		}
		

		// third, find existing position
		int pos = -1;
		if (siblings instanceof List<?>) {
			pos = ((List) siblings).indexOf(object);
		} else if (siblings instanceof Collection<?>) {
			Iterator<?> it = ((Collection<?>) siblings).iterator();
			for (int i = 0; it.hasNext(); i++) {
				if (it.next() == object) {
					pos = i;
					break;
				}
			}
		}
		if (pos < 0) {
			return null;
		}

		int movementDelta = 0;
		try {
			movementDelta = Integer.parseInt(event.getParameter(movementDeltaParameterId));
		} catch (NumberFormatException e) {
		} catch (NullPointerException e) {
		}
		boolean shouldCopy = "true".equals(event.getParameter(shouldCopyParameterId));

		// fourth, find new position
		int newPos = pos + movementDelta;
		if (newPos < 0) {
			newPos = 0;
		} else if (newPos >= siblings.size()) {
			newPos = siblings.size() - 1;
		}
		
		// finally, compute command
		Command command = null;
		if (shouldCopy && object instanceof EObject) {
			command = AddCommand.create(editingDomain, parent, null, EcoreUtil.copy((EObject) object), newPos);
		} else if (newPos != pos) {
			command = MoveCommand.create(editingDomain, parent, null, object, newPos);
		}
		return command;
	}

	public boolean isEnabled() {
		EObject eObject = getSelection(PlatformUI.getWorkbench().getActiveWorkbenchWindow());
		return (eObject != null);
	}

	public boolean isHandled() {
		return true;
	}

	public void addHandlerListener(IHandlerListener handlerListener) {
	}
	public void removeHandlerListener(IHandlerListener handlerListener) {
	}
}