// 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.code;
import com.google.collide.client.document.DocumentManager;
import com.google.collide.client.document.DocumentMetadata;
import com.google.collide.client.document.DocumentManager.GetDocumentCallback;
import com.google.collide.client.history.RootPlace;
import com.google.collide.client.ui.tree.TreeNodeElement;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.PathUtil;
import com.google.collide.client.workspace.FileTreeModel;
import com.google.collide.client.workspace.FileTreeNode;
import com.google.collide.client.workspace.FileTreeUiController;
import com.google.collide.client.workspace.WorkspacePlace;
import com.google.collide.client.workspace.FileTreeModel.NodeRequestCallback;
import com.google.collide.dto.FileContents;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.json.client.JsoArray;
import com.google.collide.shared.document.Document;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
/**
* Handler for file selection that drives file contents and the file tree selection.
*
*/
public class FileSelectionController implements GetDocumentCallback {
/**
* Event that broadcasts that a file's contents have been received over the network and we have
* determined if it is editable.
*
*/
public static class FileOpenedEvent extends GwtEvent<FileOpenedEvent.Handler> {
public interface Handler extends EventHandler {
public void onFileOpened(boolean isEditable, PathUtil filePath);
}
public static final Type<Handler> TYPE = new Type<Handler>();
private final boolean isFileEditable;
private final PathUtil filePath;
public FileOpenedEvent(boolean isEditable, PathUtil filePath) {
this.isFileEditable = isEditable;
this.filePath = filePath;
}
@Override
protected void dispatch(Handler handler) {
handler.onFileOpened(isFileEditable, filePath);
}
public boolean isEditable() {
return isFileEditable;
}
public PathUtil getFilePath() {
return filePath;
}
@Override
public com.google.gwt.event.shared.GwtEvent.Type<Handler> getAssociatedType() {
return TYPE;
}
}
private FileSelectedPlace.NavigationEvent mostRecentNavigationEvent;
private boolean isSelectedFileEditable = false;
private final FileTreeUiController treeUiController;
private final FileTreeModel fileTreeModel;
private final EditableContentArea contentArea;
private final EditorBundle editorBundle;
private final UneditableDisplay uneditableDisplay;
private final DocumentManager documentManager;
public FileSelectionController(DocumentManager documentManager,
EditorBundle editorBundle,
UneditableDisplay uneditableDisplay,
FileTreeModel fileTreeModel,
FileTreeUiController treeUiController,
EditableContentArea contentArea) {
this.documentManager = documentManager;
this.editorBundle = editorBundle;
this.uneditableDisplay = uneditableDisplay;
this.fileTreeModel = fileTreeModel;
this.treeUiController = treeUiController;
this.contentArea = contentArea;
}
/**
* Deselects the currently selected file, if one is selected.
*/
public void deselectFile() {
mostRecentNavigationEvent = null;
treeUiController.clearSelectedNodes();
}
public void selectFile(FileSelectedPlace.NavigationEvent navigationEvent) {
// The root is a special case no-op.
if (!navigationEvent.getPath().equals(PathUtil.WORKSPACE_ROOT)) {
doSelectTreeNode(navigationEvent);
}
}
public boolean isSelectedFileEditable() {
return isSelectedFileEditable;
}
FileTreeModel getFileTreeModel() {
return fileTreeModel;
}
private void doSelectTreeNode(final FileSelectedPlace.NavigationEvent navigationEvent) {
mostRecentNavigationEvent = navigationEvent;
fileTreeModel.requestWorkspaceNode(navigationEvent.getPath(), new NodeRequestCallback() {
@Override
public void onNodeAvailable(FileTreeNode node) {
// If we have since navigated away, exit early and don't select the
// file.
if (mostRecentNavigationEvent != navigationEvent
|| !mostRecentNavigationEvent.isActiveLeaf()) {
return;
}
// Expand the tree to reveal the node if it happens to be hidden (in the
// case of deep linking)
TreeNodeElement<FileTreeNode> renderedElement = node.getRenderedTreeNode();
if (renderedElement == null
|| !renderedElement.isActive(treeUiController.getTree().getResources().treeCss())) {
// Select the node without dispatching the node selection action.
treeUiController.autoExpandAndSelectNode(node, false);
}
}
@Override
public void onNodeUnavailable() {
// This can happen if a history event is dispatched for a path not
// found in our file tree.
// TODO: Throw up some UI to show that no such file exists.
// For now simply ignore the selection.
}
@Override
public void onError(FailureReason reason) {
// Already logged by the FileTreeModel.
}
});
documentManager.getDocument(navigationEvent.getPath(), this);
}
@Override
public void onDocumentReceived(Document document) {
PathUtil path = DocumentMetadata.getPath(document);
if (mostRecentNavigationEvent == null || !mostRecentNavigationEvent.isActiveLeaf()
|| !path.equals(mostRecentNavigationEvent.getPath())) {
// User selected another file or navigated away since this request, ignore
return;
}
openDocument(document, path);
WorkspacePlace.PLACE.fireEvent(new FileOpenedEvent(true, path));
}
private void openDocument(final Document document, PathUtil path) {
isSelectedFileEditable = true;
contentArea.setContent(editorBundle);
// Note that we dont use the name from the incoming FileContents. They
// should generally be the same. But in the case of a rename or a move, they
// might not be. Those changes get propagated separately, so we stick with
// the client's view of the world wrt to naming.
editorBundle.setDocument(document, path, DocumentMetadata.getFileEditSessionKey(document));
if (mostRecentNavigationEvent.getLineNo()
!= FileSelectedPlace.NavigationEvent.IGNORE_LINE_NUMBER) {
/*
* TODO: This scheduled deferred is so we set scroll AFTER the selection
* restorer. After demo, I'll create a better API on editor for components that want to set
* initial selection/scroll.
*/
final FileSelectedPlace.NavigationEvent savedNavigationEvent = mostRecentNavigationEvent;
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
if (mostRecentNavigationEvent == savedNavigationEvent) {
editorBundle.getEditor().scrollTo(mostRecentNavigationEvent.getLineNo(),
Math.max(0, mostRecentNavigationEvent.getColumn()));
}
}
});
}
// Set the tab title to the current open file
Elements.setCollideTitle(path.getBaseName());
editorBundle.getEditor().getFocusManager().focus();
// Save the list of open documents.
// TODO: Send a list of files when we support tabs.
JsoArray<String> openFiles = JsoArray.create();
openFiles.add(path.getPathString());
}
@Override
public void onUneditableFileContentsReceived(FileContents uneditableFile) {
showDisplayOnly(uneditableFile);
WorkspacePlace.PLACE.fireEvent(
new FileOpenedEvent(false, new PathUtil(uneditableFile.getPath())));
}
/**
* Changes the display for an uneditable file.
*/
private void showDisplayOnly(FileContents uneditableFile) {
PathUtil filePath = new PathUtil(uneditableFile.getPath());
if (!filePath.equals(mostRecentNavigationEvent.getPath())) {
// User selected another file since this request, ignore
return;
}
isSelectedFileEditable = false;
// Set the tab title to the current open file
Elements.setCollideTitle(filePath.getBaseName());
contentArea.setContent(uneditableDisplay);
uneditableDisplay.displayUneditableFileContents(uneditableFile);
editorBundle.getBreadcrumbs().setPath(filePath);
}
@Override
public void onFileNotFoundReceived() {
// TODO: pretty file not found message
RootPlace.PLACE.fireChildPlaceNavigation(
WorkspacePlace.PLACE.createNavigationEvent());
}
}