/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.command.explorer; import com.google.common.annotations.VisibleForTesting; import com.google.gwt.core.client.Callback; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.google.gwt.user.client.ui.IsWidget; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.web.bindery.event.shared.EventBus; import org.eclipse.che.api.promises.client.Operation; import org.eclipse.che.api.promises.client.OperationException; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.ide.DelayedTask; import org.eclipse.che.ide.api.app.AppContext; import org.eclipse.che.ide.api.command.CommandAddedEvent; import org.eclipse.che.ide.api.command.CommandGoal; import org.eclipse.che.ide.api.command.CommandGoalRegistry; import org.eclipse.che.ide.api.command.CommandImpl; import org.eclipse.che.ide.api.command.CommandImpl.ApplicableContext; import org.eclipse.che.ide.api.command.CommandManager; import org.eclipse.che.ide.api.command.CommandRemovedEvent; import org.eclipse.che.ide.api.command.CommandType; import org.eclipse.che.ide.api.command.CommandUpdatedEvent; import org.eclipse.che.ide.api.component.WsAgentComponent; import org.eclipse.che.ide.api.dialogs.DialogFactory; import org.eclipse.che.ide.api.editor.EditorAgent; import org.eclipse.che.ide.api.notification.NotificationManager; import org.eclipse.che.ide.api.parts.WorkspaceAgent; import org.eclipse.che.ide.api.parts.base.BasePresenter; import org.eclipse.che.ide.command.CommandResources; import org.eclipse.che.ide.command.CommandUtils; import org.eclipse.che.ide.command.node.NodeFactory; import org.eclipse.che.ide.command.type.chooser.CommandTypeChooser; import org.eclipse.che.providers.DynaObject; import org.vectomatic.dom.svg.ui.SVGResource; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.eclipse.che.ide.api.constraints.Constraints.LAST; import static org.eclipse.che.ide.api.notification.StatusNotification.DisplayMode.EMERGE_MODE; import static org.eclipse.che.ide.api.notification.StatusNotification.Status.FAIL; import static org.eclipse.che.ide.api.parts.PartStackType.NAVIGATION; /** Presenter for Commands Explorer. */ @DynaObject @Singleton public class CommandsExplorerPresenter extends BasePresenter implements CommandsExplorerView.ActionDelegate, WsAgentComponent { private final CommandsExplorerView view; private final CommandResources resources; private final WorkspaceAgent workspaceAgent; private final CommandManager commandManager; private final NotificationManager notificationManager; private final CommandTypeChooser commandTypeChooser; private final ExplorerMessages messages; private final RefreshViewTask refreshViewTask; private final DialogFactory dialogFactory; private final NodeFactory nodeFactory; private final EditorAgent editorAgent; private final AppContext appContext; private final EventBus eventBus; @Inject public CommandsExplorerPresenter(CommandsExplorerView view, CommandResources commandResources, WorkspaceAgent workspaceAgent, CommandManager commandManager, NotificationManager notificationManager, CommandTypeChooser commandTypeChooser, ExplorerMessages messages, RefreshViewTask refreshViewTask, DialogFactory dialogFactory, NodeFactory nodeFactory, EditorAgent editorAgent, AppContext appContext, EventBus eventBus) { this.view = view; this.resources = commandResources; this.workspaceAgent = workspaceAgent; this.commandManager = commandManager; this.notificationManager = notificationManager; this.commandTypeChooser = commandTypeChooser; this.messages = messages; this.refreshViewTask = refreshViewTask; this.dialogFactory = dialogFactory; this.nodeFactory = nodeFactory; this.editorAgent = editorAgent; this.appContext = appContext; this.eventBus = eventBus; view.setDelegate(this); } @Override public void start(Callback<WsAgentComponent, Exception> callback) { callback.onSuccess(this); if (partStack == null || !partStack.containsPart(this)) { workspaceAgent.openPart(this, NAVIGATION, LAST); } refreshView(); eventBus.addHandler(CommandAddedEvent.getType(), e -> refreshViewAndSelectCommand(e.getCommand())); eventBus.addHandler(CommandRemovedEvent.getType(), e -> refreshView()); eventBus.addHandler(CommandUpdatedEvent.getType(), e -> refreshView()); } @Override public void go(AcceptsOneWidget container) { refreshView(); container.setWidget(getView()); } @Override public String getTitle() { return messages.partTitle(); } @Override public IsWidget getView() { return view; } @Nullable @Override public String getTitleToolTip() { return messages.partTooltip(); } @Nullable @Override public SVGResource getTitleImage() { return resources.explorerPart(); } @Override public void onCommandAdd(int left, int top) { commandTypeChooser.show(left, top).then(createCommand(getDefaultContext())); } /** Returns the default {@link ApplicableContext} for the new command. */ private ApplicableContext getDefaultContext() { final ApplicableContext context = new ApplicableContext(); if (appContext.getProjects().length > 0) { context.setWorkspaceApplicable(false); Arrays.stream(appContext.getProjects()) .forEach(p -> context.addProject(p.getPath())); } return context; } /** Returns an operation which creates a command with the given context. */ private Operation<CommandType> createCommand(ApplicableContext context) { return selectedCommandType -> { final CommandGoal selectedGoal = view.getSelectedGoal(); if (selectedGoal == null) { return; } commandManager.createCommand(selectedGoal.getId(), selectedCommandType.getId(), context) .then(command -> { refreshViewAndSelectCommand(command); editorAgent.openEditor(nodeFactory.newCommandFileNode(command)); }) .catchError(showErrorNotification(messages.unableCreate())); }; } @Override public void onCommandDuplicate(CommandImpl command) { commandManager.createCommand(command) .then(this::refreshViewAndSelectCommand) .catchError(showErrorNotification(messages.unableDuplicate())); } @Override public void onCommandRemove(CommandImpl command) { dialogFactory.createConfirmDialog(messages.removeCommandConfirmationTitle(), messages.removeCommandConfirmationMessage(command.getName()), () -> commandManager.removeCommand(command.getName()) .catchError(showErrorNotification(messages.unableRemove())), null).show(); } /** Returns an operation which shows an error notification with the given title. */ private Operation<PromiseError> showErrorNotification(String title) { return err -> { notificationManager.notify(title, err.getMessage(), FAIL, EMERGE_MODE); throw new OperationException(err.getMessage()); }; } /** Refresh view and preserve current selection. */ private void refreshView() { refreshViewAndSelectCommand(null); } private void refreshViewAndSelectCommand(CommandImpl command) { refreshViewTask.delayAndSelectCommand(command); } /** * {@link DelayedTask} for refreshing the view and optionally selecting the specified command. * <p>Tree widget in the view works asynchronously using events * and it needs some time to be fully rendered. * So successive refreshing view must be called with some delay. */ // since GIN can't instantiate inner classes // made it nested in order to allow injection @VisibleForTesting static class RefreshViewTask extends DelayedTask { // 300 milliseconds should be enough to fully refreshing the tree private static final int DELAY_MILLIS = 300; private final CommandsExplorerView view; private final CommandGoalRegistry goalRegistry; private final CommandManager commandManager; private final CommandUtils commandUtils; private CommandImpl commandToSelect; @Inject public RefreshViewTask(CommandsExplorerView view, CommandGoalRegistry goalRegistry, CommandManager commandManager, CommandUtils commandUtils) { this.view = view; this.goalRegistry = goalRegistry; this.commandManager = commandManager; this.commandUtils = commandUtils; } @Override public void onExecute() { refreshView(); if (commandToSelect != null) { // wait some time while tree in the view will be fully refreshed new Timer() { @Override public void run() { view.selectCommand(commandToSelect); } }.schedule(DELAY_MILLIS); } } void delayAndSelectCommand(@Nullable CommandImpl command) { if (command != null) { commandToSelect = command; } delay(DELAY_MILLIS); } private void refreshView() { final Map<CommandGoal, List<CommandImpl>> commandsByGoals = new HashMap<>(); // all predefined command goals must be shown in the view // so populate map by all registered command goals for (CommandGoal goal : goalRegistry.getAllPredefinedGoals()) { commandsByGoals.put(goal, new ArrayList<>()); } commandsByGoals.putAll(commandUtils.groupCommandsByGoal(commandManager.getCommands())); view.setCommands(commandsByGoals); } } }