/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* 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 org.community.intellij.plugins.communitycase.checkout.branches;
import com.intellij.ide.DataManager;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.IconLoader;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.CustomStatusBarWidget;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.openapi.wm.impl.status.TextPanel;
import com.intellij.ui.awt.RelativePoint;
import org.community.intellij.plugins.communitycase.Util;
import org.community.intellij.plugins.communitycase.Vcs;
import org.community.intellij.plugins.communitycase.commands.Command;
import org.community.intellij.plugins.communitycase.commands.HandlerUtil;
import org.community.intellij.plugins.communitycase.commands.LineHandler;
import org.community.intellij.plugins.communitycase.ui.UiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
/**
* The branches widget
*/
public class BranchesWidget extends TextPanel implements CustomStatusBarWidget {
/**
* The arrows icon
*/
private static final Icon ARROWS_ICON = IconLoader.getIcon("/ide/statusbar_arrows.png");
/**
* The logger
*/
private static final Logger LOG = Logger.getInstance("#"+BranchesWidget.class.getName());
/**
* The ID of the widget
*/
public static final String ID = "org.community.intellij.plugins.communitycase.BranchConfigurations";
/**
* The listener
*/
final BranchConfigurationsListener myConfigurationsListener;
/**
* The project
*/
final Project myProject;
/**
* The status bar
*/
private final StatusBar myStatusBar;
/**
* The configurations instance
*/
private final BranchConfigurations myConfigurations;
/**
* The selectable configurations. Null if invalidated or non-initialized
*/
private AnAction[] mySelectableConfigurations;
/**
* The selectable configurations. Null if invalidated or non-initialized
*/
private AnAction[] mySelectableWithChangesConfigurations;
/**
* The candidate remote configurations. Null if invalidated or non-initialized
*/
private AnAction[] myRemoveConfigurations;
/**
* If true, the popup is enabled
*/
private boolean myPopupEnabled = false;
/**
* The action group for pop up
*/
private DefaultActionGroup myPopupActionGroup;
/**
* The current popup
*/
private ListPopup myPopup;
/**
* The key for the last popup, used to determine if disposed popup is the actually the last pop up.
*/
private Object myPopupKey;
/**
* The default foreground color
*/
private final Color myDefaultForeground;
/**
* The constructor
*
* @param project the project instance
* @param statusBar the status bar
* @param configurations the configuration settings
*/
public BranchesWidget(Project project, StatusBar statusBar, BranchConfigurations configurations) {
myProject = project;
myStatusBar = statusBar;
setBorder(WidgetBorder.INSTANCE);
myConfigurations = configurations;
myDefaultForeground = getForeground();
myConfigurationsListener = new MyBranchConfigurationsListener();
myConfigurations.addConfigurationListener(myConfigurationsListener);
Disposer.register(myConfigurations, this);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
showPopup();
}
});
updateLabel();
}
/**
* Create and install widget
*
* @param project the context project
* @param configurations the configurations to use
* @return the action that uninstalls widget
*/
static Runnable install(final Project project, final BranchConfigurations configurations) {
final Ref<BranchesWidget> widget = new Ref<BranchesWidget>();
com.intellij.util.ui.UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
StatusBar statusBar = WindowManager.getInstance().getStatusBar(project);
if (statusBar != null) {
final BranchesWidget w = new BranchesWidget(project, statusBar, configurations);
statusBar.addWidget(w, "after " + (SystemInfo.isMac ? "Encoding" : "InsertOverwrite"), project);
widget.set(w);
}
}
});
return new Runnable() {
@Override
public void run() {
if (widget.get() != null) {
com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
Disposer.dispose(widget.get());
}
});
}
}
};
}
/**
* {@inheritDoc}
*/
@Override
public JComponent getComponent() {
return this;
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public String ID() {
return ID;
}
/**
* {@inheritDoc}
*/
@Override
public WidgetPresentation getPresentation(@NotNull PlatformType type) {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public void install(@NotNull StatusBar statusBar) {
}
/**
* {@inheritDoc}
*/
@Override
public void dispose() {
myConfigurations.removeConfigurationListener(myConfigurationsListener);
}
/**
* @return get or create remotes group
*/
private AnAction[] getRemotes() {
assert myPopupEnabled : "pop should be enabled";
if (myRemoveConfigurations == null) {
ArrayList<AnAction> rc = new ArrayList<AnAction>();
for (final String c : myConfigurations.getRemotesCandidates()) {
rc.add(new DumbAwareAction(escapeActionText(c)) {
@Override
public void actionPerformed(AnActionEvent e) {
myConfigurations.startCheckout(null, c, false);
}
});
}
rc.add(new RefreshRemotesAction());
myRemoveConfigurations = rc.toArray(new AnAction[rc.size()]);
}
return myRemoveConfigurations;
}
/**
* Show popup is if it is not shown.
*/
void showPopup() {
if (!myPopupEnabled) {
return;
}
if (myPopup != null) {
myPopup.cancel();
}
final DataContext parent = DataManager.getInstance().getDataContext((Component)myStatusBar);
final DataContext dataContext = SimpleDataContext.getSimpleContext(PlatformDataKeys.PROJECT.getName(), myProject, parent);
final Object key = new Object();
myPopupKey = key;
myPopup = JBPopupFactory.getInstance()
.createActionGroupPopup(null, getPopupActionGroup(), dataContext, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true,
new Runnable() {
@Override
public void run() {
if (key == myPopupKey) {
myPopup = null;
}
}
}, 20);
final Dimension dimension = myPopup.getContent().getPreferredSize();
final Point at = new Point(0, -dimension.height);
myPopup.show(new RelativePoint(getComponent(), at));
}
/**
* Ensure that action for checking out configurations are crated
*
* @return get or create selectable configuration group
*/
private AnAction[] ensureSelectableCreated() {
assert myPopupEnabled : "pop should be enabled";
if (mySelectableConfigurations == null || mySelectableWithChangesConfigurations == null) {
BranchConfiguration current;
try {
current = myConfigurations.getCurrentConfiguration();
}
catch (VcsException e) {
LOG.error("Unexpected error at this point", e);
mySelectableConfigurations = new AnAction[0];
mySelectableWithChangesConfigurations = new AnAction[0];
return mySelectableConfigurations;
}
String name = current == null ? "" : current.getName();
mySelectableConfigurations = checkoutActions(name, true);
mySelectableWithChangesConfigurations = checkoutActions(name, false);
}
return mySelectableConfigurations;
}
/**
* Checkout actions
*
* @param name the excluded name
* @param quick true, if quick checkout actions
* @return an array of actions for the configurations
*/
private AnAction[] checkoutActions(String name, final boolean quick) {
ArrayList<AnAction> rc = new ArrayList<AnAction>();
for (final String c : myConfigurations.getConfigurationNames()) {
if (name.equals(c)) {
// skip current config
continue;
}
rc.add(new DumbAwareAction(escapeActionText(c)) {
@Override
public void actionPerformed(AnActionEvent e) {
try {
final BranchConfiguration toCheckout = myConfigurations.getConfiguration(c);
if (toCheckout == null) {
throw new VcsException("The configuration " + c + " cannot be found.");
}
myConfigurations.startCheckout(toCheckout, null, quick);
}
catch (VcsException e1) {
UiUtil.showOperationError(myProject, e1, "Unable to load: " + c);
}
}
});
}
return rc.toArray(new AnAction[rc.size()]);
}
/**
* @return the action group for popup
*/
ActionGroup getPopupActionGroup() {
if (myPopupActionGroup == null) {
myPopupActionGroup = new DefaultActionGroup(null, false);
myPopupActionGroup.addAction(new DumbAwareAction("Manage Configurations ...") {
@Override
public void actionPerformed(AnActionEvent e) {
ManageConfigurationsDialog.showDialog(myProject, myConfigurations);
}
});
myPopupActionGroup.addAction(new DumbAwareAction("Modify Current Configuration ...") {
@Override
public void actionPerformed(AnActionEvent e) {
try {
final BranchConfiguration current = myConfigurations.getCurrentConfiguration();
myConfigurations.startCheckout(current, null, false);
}
catch (VcsException e1) {
LOG.error("The current configuration must exists at this point", e1);
}
}
});
myPopupActionGroup.addAction(new DumbAwareAction("New Configuration ...") {
@Override
public void actionPerformed(AnActionEvent e) {
myConfigurations.startCheckout(null, null, false);
}
});
myPopupActionGroup.add(new MyRemotesActionGroup());
myPopupActionGroup.add(new MySelectableWithChangesActionGroup());
myPopupActionGroup.addSeparator("Branch Configurations");
myPopupActionGroup.add(new MySelectableActionGroup());
}
return myPopupActionGroup;
}
/**
* Escape action text (underscores)
*
* @param t the text to escape
* @return escaped text
*/
private static String escapeActionText(String t) {
return t.replaceAll("_", "__");
}
/**
* Update label on the widget
*/
private void updateLabel() {
cancelPopup();
final BranchConfigurations.SpecialStatus status = myConfigurations.getSpecialStatus();
String text;
myPopupEnabled = false;
Color color = Color.RED;
String tooltip;
switch (status) {
case CHECKOUT_IN_PROGRESS:
text = "Checkout...";
color = Color.BLUE;
tooltip = "A checkout operation is in progress.";
break;
case MERGING:
text = "Merging...";
tooltip = "Merge is in progress in some vcs roots.";
break;
case REBASING:
tooltip = "Rebase is in progress in some vcs roots.";
text = "Rebasing...";
break;
case NON_:
tooltip = "No valid vcs roots are configured for the project.";
text = "Non- project";
break;
case NORMAL:
BranchConfiguration current;
try {
current = myConfigurations.getCurrentConfiguration();
}
catch (VcsException e) {
current = null;
}
if (current == null) {
tooltip = "The branch configurations are not yet detected.";
text = "Detecting...";
}
else {
myPopupEnabled = true;
text = current.getName();
tooltip = "<html>The branch configuration <b>" + text + "</b> is selected.<br/>Click to select other configuration.</html>";
color = myDefaultForeground;
}
break;
default:
tooltip = "Unknown status: " + status;
text = "Unknown status: " + status;
}
if (!SystemInfo.isMac) {
setForeground(color);
}
setToolTipText(tooltip);
setText(text);
invalidate();
}
@Override
protected void paintComponent(@NotNull final Graphics g) {
super.paintComponent(g);
if (getText() != null && myPopupEnabled) {
final Rectangle r = getBounds();
final Insets insets = getInsets();
ARROWS_ICON
.paintIcon(this, g, r.width - insets.right - ARROWS_ICON.getIconWidth() - 2, r.height / 2 - ARROWS_ICON.getIconHeight() / 2);
}
}
@Override
public Dimension getPreferredSize() {
final Dimension preferredSize = super.getPreferredSize();
return new Dimension(preferredSize.width + ARROWS_ICON.getIconWidth() + 4, preferredSize.height);
}
@Override
protected String getTextForPreferredSize() {
return getText();
}
/**
* Cancel popup if it is shown
*/
private void cancelPopup() {
if (myPopup != null) {
myPopup.cancel();
}
}
/**
* Remotes action group
*/
class MySelectableActionGroup extends ActionGroup {
/**
* The constructor
*/
public MySelectableActionGroup() {
super(null, false);
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public AnAction[] getChildren(@Nullable AnActionEvent e) {
return ensureSelectableCreated();
}
}
/**
* Remotes action group
*/
class MySelectableWithChangesActionGroup extends ActionGroup {
/**
* The constructor
*/
public MySelectableWithChangesActionGroup() {
super("Check out with Selected Changes...", true);
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public AnAction[] getChildren(@Nullable AnActionEvent e) {
ensureSelectableCreated();
return mySelectableWithChangesConfigurations;
}
}
/**
* Remotes action group
*/
class MyRemotesActionGroup extends ActionGroup {
/**
* The constructor
*/
public MyRemotesActionGroup() {
super("Remotes", true);
}
/**
* {@inheritDoc}
*/
@NotNull
@Override
public AnAction[] getChildren(@Nullable AnActionEvent e) {
return getRemotes();
}
}
/**
* The configuration listener
*/
class MyBranchConfigurationsListener implements BranchConfigurationsListener {
/**
* {@inheritDoc}
*/
@Override
public void configurationsChanged() {
com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
mySelectableConfigurations = null;
mySelectableWithChangesConfigurations = null;
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void specialStatusChanged() {
refreshLabel();
}
private void refreshLabel() {
com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
updateLabel();
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void currentConfigurationChanged() {
com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
mySelectableConfigurations = null;
mySelectableWithChangesConfigurations = null;
updateLabel();
}
});
}
/**
* {@inheritDoc}
*/
@Override
public void referencesChanged() {
com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
myRemoveConfigurations = null;
updateLabel();
}
});
}
}
/**
* Refresh remotes
*/
class RefreshRemotesAction extends DumbAwareAction {
/**
* The constructor
*/
public RefreshRemotesAction() {
super("Refresh Remotes...", "Fetch all references for vcs roots", IconLoader.findIcon("/vcs/refresh.png"));
}
/**
* {@inheritDoc}
*/
@Override
public void actionPerformed(AnActionEvent e) {
final String title = "Refreshing remotes";
ProgressManager.getInstance().run(new Task.Backgroundable(myProject, title, false) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
final ArrayList<VcsException> exceptions = new ArrayList<VcsException>();
try {
for (VirtualFile root : Util.getRoots(myProject, Vcs.getInstance(myProject))) {
LineHandler h = new LineHandler(myProject, root, Command.FETCH);
h.addParameters("--all", "-v");
final Collection<VcsException> e = HandlerUtil
.doSynchronouslyWithExceptions(h, indicator, "Fetching all for " + Util.relativePath(myProject.getBaseDir(), root));
exceptions.addAll(e);
}
}
catch (VcsException e1) {
exceptions.add(e1);
}
com.intellij.util.ui.UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
if (!exceptions.isEmpty()) {
UiUtil.showTabErrors(myProject, title, exceptions);
ToolWindowManager.getInstance(myProject).notifyByBalloon(
ChangesViewContentManager.TOOLWINDOW_ID, MessageType.ERROR, "Refreshing remotes failed.");
} else {
ToolWindowManager.getInstance(myProject).notifyByBalloon(
ChangesViewContentManager.TOOLWINDOW_ID, MessageType.INFO, "Refreshing remotes complete.");
}
myRemoveConfigurations = null;
cancelPopup();
}
});
}
});
}
}
}