// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.collide.client.workspace;
import com.google.collide.client.AppContext;
import com.google.collide.client.communication.FrontendApi.ApiCallback;
import com.google.collide.client.ui.tree.SelectionModel;
import com.google.collide.client.util.PathUtil;
import com.google.collide.client.workspace.FileTreeUiController.DragDropListener;
import com.google.collide.dto.EmptyMessage;
import com.google.collide.dto.Mutation;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.dto.WorkspaceTreeUpdate;
import com.google.collide.dto.client.DtoClientImpls.DirInfoImpl;
import com.google.collide.json.client.JsoArray;
import com.google.common.annotations.VisibleForTesting;
import elemental.html.DragEvent;
/**
* A controller to manage the in-tree drag and drop.
* <p>
* Dragging started outside file tree is not handled here.
*/
public class FileTreeNodeMoveController implements WorkspaceReadOnlyChangedEvent.Handler {
// TODO: Change to custom format (such as
// "application/collide-nodes-move-started") once Chrome supports it.
private static final String MOVE_START_INDICATOR_FORMAT = "text/plain";
private static final String MOVE_START_INDICATOR = "NODES_MOVE_STARTED";
private final FileTreeUiController fileTreeUiController;
private boolean isReadOnly;
private final JsoArray<FileTreeNode> nodesToMove = JsoArray.create();
private final AppContext appContext;
private final FileTreeModel fileTreeModel;
public FileTreeNodeMoveController(AppContext appContext,
FileTreeUiController fileTreeUiController, FileTreeModel fileTreeModel) {
this.appContext = appContext;
this.fileTreeUiController = fileTreeUiController;
this.fileTreeModel = fileTreeModel;
attachEventHandlers();
}
private void attachEventHandlers() {
if (fileTreeUiController == null) {
return;
}
fileTreeUiController.setFileTreeNodeMoveListener(new DragDropListener() {
@Override
public void onDragDrop(FileTreeNode node, DragEvent event) {
event.getDataTransfer().clearData(MOVE_START_INDICATOR_FORMAT);
if (isReadOnly || !wasDragInTree(event)) {
return;
}
handleMove(node);
nodesToMove.clear();
}
@Override
public void onDragStart(FileTreeNode node, DragEvent event) {
// Save the selected nodes. Users may drop them in tree.
saveSelectedNodesOrParam(node);
// TODO: once Chrome supports dataTransfer.addElement, add
// nodesToMove to provide move feedback.
event.getDataTransfer().setData(MOVE_START_INDICATOR_FORMAT, MOVE_START_INDICATOR);
}
});
}
@Override
public void onWorkspaceReadOnlyChanged(WorkspaceReadOnlyChangedEvent event) {
isReadOnly = event.isReadOnly();
}
public static boolean wasDragInTree(DragEvent event) {
return MOVE_START_INDICATOR.equals(
event.getDataTransfer().getData(MOVE_START_INDICATOR_FORMAT));
}
private void saveSelectedNodesOrParam(FileTreeNode node) {
nodesToMove.clear();
SelectionModel<FileTreeNode> selectionModel =
fileTreeUiController.getTree().getSelectionModel();
JsoArray<FileTreeNode> selectedNodes = selectionModel.getSelectedNodes();
if (selectedNodes.contains(node)) {
// Drag is starting from one of the selected nodes.
// We move all selected nodes.
nodesToMove.addAll(selectedNodes);
} else {
// Drag is starting outside all selected nodes.
// We only move the drag-start-node.
nodesToMove.add(node);
}
}
/**
* For test only.
*/
@VisibleForTesting
void setNodesToMove(JsoArray<FileTreeNode> nodesToMove) {
this.nodesToMove.clear();
this.nodesToMove.addAll(nodesToMove);
}
@VisibleForTesting
boolean isMoveAllowed(FileTreeNode parentDirData) {
// File should not be moved to its original place, i.e., /a/b/1.js ==> under
// /a/b
// Folder should not be moved to its original place and to under itself or
// under any of its subfolders, i.e., /a/b/c ==> under /a/b, under /a/b/c,
// under /a/b/c/d.
for (int i = 0, n = nodesToMove.size(); i < n; i++) {
FileTreeNode nodeToMove = nodesToMove.get(i);
if (nodeToMove.isFile()) {
if (nodeToMove.getParent().getNodePath().equals(parentDirData.getNodePath())) {
return false;
}
} else {
// is folder.
// source folder won't be root, so it has parent.
if (nodeToMove.getParent().getNodePath().equals(parentDirData.getNodePath())) {
return false;
}
if (nodeToMove.getNodePath().containsPath(parentDirData.getNodePath())) {
return false;
}
}
}
return true;
}
private void handleMove(FileTreeNode parentDirData) {
if (nodesToMove.isEmpty()) {
return;
}
// Check each selected node to make sure it can be moved.
if (!isMoveAllowed(parentDirData)) {
return;
}
WorkspaceTreeUpdate msg = fileTreeModel.makeEmptyTreeUpdate();
for (int i = 0, n = nodesToMove.size(); i < n; i++) {
FileTreeNode nodeToMove = nodesToMove.get(i);
PathUtil targetPath = new PathUtil.Builder().addPath(parentDirData.getNodePath())
.addPathComponent(FileTreeUtils.allocateName(
parentDirData.<DirInfoImpl>cast(), nodeToMove.getName())).build();
msg.getMutations().add(FileTreeUtils.makeMutation(Mutation.Type.MOVE,
nodeToMove.getNodePath(), targetPath, nodeToMove.isDirectory(),
nodeToMove.getFileEditSessionKey()));
}
appContext.getFrontendApi().MUTATE_WORKSPACE_TREE.send(
msg, new ApiCallback<EmptyMessage>() {
@Override
public void onMessageReceived(EmptyMessage message) {
// Do nothing. We lean on the multicasted MOVE to have the action
// update our local model.
}
@Override
public void onFail(FailureReason reason) {
// Do nothing.
}
});
}
public void cleanup() {
fileTreeUiController.setFileTreeNodeMoveListener(null);
}
}