/*******************************************************************************
* Copyright (c) 2010, 2016 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
* Patrik Suzzi <psuzzi@gmail.com> - Bug 497348
******************************************************************************/
package org.eclipse.e4.ui.workbench.addons.dndaddon;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.e4.ui.internal.workbench.swt.AbstractPartRenderer;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
import org.eclipse.e4.ui.model.application.ui.basic.MPartStack;
import org.eclipse.e4.ui.model.application.ui.basic.MStackElement;
import org.eclipse.e4.ui.model.application.ui.basic.MWindow;
import org.eclipse.e4.ui.workbench.IPresentationEngine;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
/**
* This agent manage drag and drop when dragging a Tab in Eclipse Part Stacks.
*/
public class StackDropAgent extends DropAgent {
private Rectangle tabArea;
private MPartStack dropStack;
private CTabFolder dropCTF;
/**
* @param manager
*/
public StackDropAgent(DnDManager manager) {
super(manager);
}
@Override
public boolean canDrop(MUIElement dragElement, DnDInfo info) {
// We only except stack elements and whole stacks
if (!(dragElement instanceof MStackElement) && !(dragElement instanceof MPartStack)) {
return false;
}
// We have to be over a stack ourselves
if (!(info.curElement instanceof MPartStack)) {
return false;
}
MPartStack stack = (MPartStack) info.curElement;
if (stack.getTags().contains(IPresentationEngine.STANDALONE)) {
return false;
}
// We only work for CTabFolders
if (!(stack.getWidget() instanceof CTabFolder)) {
return false;
}
// We can't drop stacks onto itself
if (stack == dragElement) {
return false;
}
// You can only drag MParts from window to window
// NOTE: Disabled again due to too many issues, see bug 445305 for details
// if (!(dragElement instanceof MPart)) {
EModelService ms = dndManager.getModelService();
MWindow dragElementWin = ms.getTopLevelWindowFor(dragElement);
MWindow dropWin = ms.getTopLevelWindowFor(stack);
if (dragElementWin != dropWin)
{
return false;
// }
}
// only allow dropping into the the area
Rectangle areaRect = getTabAreaRect((CTabFolder) stack.getWidget());
boolean inArea = areaRect.contains(info.cursorPos);
if (inArea) {
tabArea = areaRect;
dropStack = (MPartStack) info.curElement;
dropCTF = (CTabFolder) dropStack.getWidget();
}
return inArea;
}
private Rectangle getTabAreaRect(CTabFolder theCTF) {
Rectangle ctfBounds = theCTF.getBounds();
ctfBounds.height = theCTF.getTabHeight();
Rectangle displayBounds = Display.getCurrent().map(theCTF.getParent(), null, ctfBounds);
return displayBounds;
}
/**
* Static helper to get visible items without using member variables in this
* stateful Agent.
*
* @param dropCTF
* @return
*/
private static List<CTabItem> getVisibleItems(CTabFolder dropCTF) {
return Stream.of(dropCTF.getItems())
.filter(i -> i.isShowing())
.collect(Collectors.toList());
}
/**
* Static helper to compute the visual rectangles to drop, without using
* member variables in this stateful Agent.
*
* @param dropCTF
* @param visibleItems
* @param tabArea
* @return
*/
private static ArrayList<Rectangle> getItemRects(CTabFolder dropCTF, List<CTabItem> visibleItems,
Rectangle tabArea) {
// for dropping, we consider visible tab only
ArrayList<Rectangle> itemRects = new ArrayList<Rectangle>();
CTabItem item;
if (visibleItems.size() > 0) {
// First rect is from left to the center of the item
item = visibleItems.get(0);
Rectangle itemRect = item.getBounds();
int centerX = itemRect.x + (itemRect.width / 2);
itemRect.width /= 2;
int curX = itemRect.x + itemRect.width;
Rectangle insertRect = dropCTF.getDisplay().map(dropCTF, null, itemRect);
itemRects.add(insertRect);
// Process the other items
for (int i = 1; i < visibleItems.size(); i++) {
item = visibleItems.get(i);
itemRect = item.getBounds();
centerX = itemRect.x + (itemRect.width / 2);
itemRect.width = centerX - curX;
itemRect.x = curX;
curX = centerX;
insertRect = dropCTF.getDisplay().map(dropCTF, null, itemRect);
itemRects.add(insertRect);
}
// Finally, add rectangle from the center of the last element
// to the end
itemRect.x = curX;
itemRect.width = dropCTF.getBounds().width - curX;
insertRect = dropCTF.getDisplay().map(dropCTF, null, itemRect);
itemRects.add(insertRect);
} else {
// Empty stack, whole area is index == 0
itemRects.add(tabArea);
}
//
return itemRects;
}
private ArrayList<Rectangle> computeInsertRects() {
List<CTabItem> visibleItems = getVisibleItems(dropCTF);
return getItemRects(dropCTF, visibleItems, tabArea);
}
private int getDropIndex(DnDInfo info) {
ArrayList<Rectangle> itemRects = computeInsertRects();
if (itemRects == null) {
return -1;
}
for (Rectangle itemRect : itemRects) {
if (itemRect.contains(info.cursorPos)) {
return itemRects.indexOf(itemRect);
}
}
return -1;
}
@Override
public void dragLeave(MUIElement dragElement, DnDInfo info) {
dndManager.clearOverlay();
if (dndManager.getFeedbackStyle() == DnDManager.HOSTED) {
if (dragElement.getParent() != null) {
dndManager.hostElement(dragElement, 16, 10);
}
} else {
dndManager.setHostBounds(null);
}
tabArea = null;
super.dragLeave(dragElement, info);
}
/**
* Tracks movements of mouse on the Stack where the user is dropping the element
*/
@Override
public boolean track(MUIElement dragElement, DnDInfo info) {
if (!tabArea.contains(info.cursorPos) || dropStack == null || !dropStack.isToBeRendered()) {
return false;
}
int dropIndex = getDropIndex(info);
if (dropIndex == -1) {
return true;
}
dndManager.setCursor(Display.getCurrent().getSystemCursor(SWT.CURSOR_HAND));
if (dropStack.getChildren().indexOf(dragElement) == dropIndex){
return true;
}
if (dndManager.getFeedbackStyle() == DnDManager.HOSTED) {
dock(dragElement, dropIndex);
Display.getCurrent().update();
showFrame(dragElement);
} else {
List<CTabItem> visibleItems = getVisibleItems(dropCTF);
if (dropIndex < visibleItems.size()) {
Rectangle itemBounds = visibleItems.get(dropIndex).getBounds();
itemBounds.width = 2;
itemBounds = Display.getCurrent().map(dropCTF, null, itemBounds);
dndManager.frameRect(itemBounds);
} else if (visibleItems.size() > 0) {
Rectangle itemBounds = visibleItems.get(dropIndex - 1).getBounds();
itemBounds.x = itemBounds.x + itemBounds.width;
itemBounds.width = 2;
itemBounds = Display.getCurrent().map(dropCTF, null, itemBounds);
dndManager.frameRect(itemBounds);
} else {
Rectangle fr = new Rectangle(tabArea.x, tabArea.y, tabArea.width, tabArea.height);
fr.width = 2;
dndManager.frameRect(fr);
}
if (dndManager.getFeedbackStyle() == DnDManager.GHOSTED) {
Rectangle ca = dropCTF.getClientArea();
ca = Display.getCurrent().map(dropCTF, null, ca);
dndManager.setHostBounds(ca);
}
}
return true;
}
/**
* @param dragElement
* @param dropIndex
*/
private void dock(MUIElement dragElement, int dropIndex) {
List<CTabItem> vItems = getVisibleItems(dropCTF);
boolean hiddenTabs = (vItems.size() < dropCTF.getChildren().length);
// Adjust the index if necessary
int elementIndex = dropStack.getChildren().indexOf(dragElement);
if (elementIndex != -1 && !(dragElement instanceof MPartStack)) {
// Get the index of this CTF entry
Control dragCtrl = (Control) dragElement.getWidget();
for (CTabItem cti : dropCTF.getItems()) {
if (dragCtrl == cti.getControl()) {
int itemIndex = dropCTF.indexOf(cti);
if (dropIndex > 0 && itemIndex < dropIndex) {
dropIndex--;
}
}
}
}
// 'dropIndex' is now the index of the CTabItem to put ourselves before
// we need to adjust this to be a model index
if (hiddenTabs) {
// some tabs are hidden
int nVisibleItems = vItems.size();
if(dropIndex<nVisibleItems){
CTabItem item = vItems.get(dropIndex);
MUIElement itemModel = (MUIElement) item.getData(AbstractPartRenderer.OWNING_ME);
// if we're going before ourselves its a NO-OP
if (itemModel == dragElement) {
return;
}
dropIndex = itemModel.getParent().getChildren().indexOf(itemModel);
// if the item is dropped at the last position, there is
// no existing item to put ourselves before
// so we'll just go to the end.
} else if (dropIndex == nVisibleItems) {
dropIndex = dropStack.getChildren().size();
}
} else {
// all tabs are visible
int ctfItemCount = dropCTF.getItemCount();
if (dropIndex < ctfItemCount) {
CTabItem item = dropCTF.getItem(dropIndex);
MUIElement itemModel = (MUIElement) item.getData(AbstractPartRenderer.OWNING_ME);
// if we're going before ourselves its a NO-OP
if (itemModel == dragElement) {
return;
}
dropIndex = itemModel.getParent().getChildren().indexOf(itemModel);
// if the item is dropped at the last position, there is
// no existing item to put ourselves before
// so we'll just go to the end.
} else if (dropIndex == ctfItemCount) {
dropIndex = dropStack.getChildren().size();
}
}
if (dragElement instanceof MStackElement) {
if (dragElement.getParent() != null) {
dragElement.getParent().getChildren().remove(dragElement);
}
if (dropIndex >= 0 && dropIndex < dropStack.getChildren().size()) {
dropStack.getChildren().add(dropIndex, (MStackElement) dragElement);
} else {
dropStack.getChildren().add((MStackElement) dragElement);
}
// (Re)active the element being dropped
dropStack.setSelectedElement((MStackElement) dragElement);
} else {
MPartStack stack = (MPartStack) dragElement;
MStackElement curSel = stack.getSelectedElement();
List<MStackElement> kids = stack.getChildren();
// First move over all *non-selected* elements
int selIndex = kids.indexOf(curSel);
boolean curSelProcessed = false;
while (kids.size() > 1) {
// Offset the 'get' to account for skipping 'curSel'
MStackElement kid = curSelProcessed ? kids.get(kids.size() - 2) : kids.get(kids.size() - 1);
if (kid == curSel) {
curSelProcessed = true;
continue;
}
kids.remove(kid);
if (dropIndex >= 0 && dropIndex < dropStack.getChildren().size()) {
dropStack.getChildren().add(dropIndex, kid);
} else {
dropStack.getChildren().add(kid);
}
}
// Finally, move over the selected element
kids.remove(curSel);
dropIndex = dropIndex + selIndex;
if (dropIndex >= 0 && dropIndex < dropStack.getChildren().size()) {
dropStack.getChildren().add(dropIndex, curSel);
} else {
dropStack.getChildren().add(curSel);
}
// (Re)active the element being dropped
dropStack.setSelectedElement(curSel);
}
}
/**
* @param dragElement
*/
private void showFrame(MUIElement dragElement) {
CTabFolder ctf = (CTabFolder) dropStack.getWidget();
CTabItem[] items = ctf.getItems();
CTabItem item = null;
for (CTabItem tabItem : items) {
if (tabItem.getData(AbstractPartRenderer.OWNING_ME) == dragElement) {
item = tabItem;
break;
}
}
Rectangle bounds = item.getBounds();
bounds = Display.getCurrent().map(dropCTF, null, bounds);
Rectangle outerBounds = new Rectangle(bounds.x - 3, bounds.y - 3, bounds.width + 6,
bounds.height + 6);
dndManager.frameRect(outerBounds);
}
@Override
public boolean drop(MUIElement dragElement, DnDInfo info) {
if (dndManager.getFeedbackStyle() != DnDManager.HOSTED) {
int dropIndex = getDropIndex(info);
if (dropIndex != -1) {
MUIElement toActivate = dragElement instanceof MPartStack ? ((MPartStack) dragElement)
.getSelectedElement() : dragElement;
dock(dragElement, dropIndex);
reactivatePart(toActivate);
}
}
return true;
}
}