com.aptana.git.ui.internal.actions.CommitDialog.java Source code

Java tutorial

Introduction

Here is the source code for com.aptana.git.ui.internal.actions.CommitDialog.java

Source

/**
 * Aptana Studio
 * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the GNU Public License (GPL) v3 (with exceptions).
 * Please see the license.html included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package com.aptana.git.ui.internal.actions;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.ContributionItem;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.StatusDialog;
import org.eclipse.jface.util.LocalSelectionTransfer;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TableDropTargetEffect;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.Scrollable;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.ActiveShellExpression;
import org.eclipse.ui.IWorkbenchActionConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.contexts.IContextActivation;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;

import com.aptana.core.logging.IdeLog;
import com.aptana.core.util.StringUtil;
import com.aptana.git.core.IDebugScopes;
import com.aptana.git.core.model.ChangedFile;
import com.aptana.git.core.model.GitRepository;
import com.aptana.git.ui.DiffFormatter;
import com.aptana.git.ui.GitUIPlugin;

class CommitDialog extends StatusDialog {
    /**
     * Context specific to this dialog.
     */
    private static final String COMMIT_DIALOG_CONTEXT_ID = "com.aptana.git.ui.context.dialog.commit"; //$NON-NLS-1$

    /**
     * Command specific to this dialog to perform a commit (just like hitting Ok/Commit button), so we can bind a
     * keybinding to perform this.
     */
    private static final String PERFORM_COMMIT_COMMAND_ID = "com.aptana.git.ui.command.commit.dialog"; //$NON-NLS-1$

    private static final String CHANGED_FILE_DATA_KEY = "changedFile"; //$NON-NLS-1$

    private GitRepository gitRepository;
    private Text commitMessage;
    private String fMessage;
    private Table unstagedTable;
    private Table stagedTable;
    private Control draggingFromTable;

    private Image newFileImage;
    private Image deletedFileImage;
    private Image emptyFileImage;
    private Scrollable diffArea;
    private ChangedFile fLastDiffFile;

    private StagingButtons unstageButtons;
    private StagingButtons stageButtons;

    private IContextActivation contextActivation;

    private IHandlerActivation commitHandler;

    protected CommitDialog(Shell parentShell, GitRepository gitRepository) {
        super(parentShell);
        Assert.isNotNull(gitRepository, "Must have a non-null git repository!"); //$NON-NLS-1$
        this.gitRepository = gitRepository;
        newFileImage = GitUIPlugin.getImage("icons/obj16/new_file.png"); //$NON-NLS-1$
        deletedFileImage = GitUIPlugin.getImage("icons/obj16/deleted_file.png"); //$NON-NLS-1$
        emptyFileImage = GitUIPlugin.getImage("icons/obj16/empty_file.png"); //$NON-NLS-1$
        fLastDiffFile = null;
    }

    @Override
    protected IDialogSettings getDialogBoundsSettings() {
        IDialogSettings compareSettings = GitUIPlugin.getDefault().getDialogSettings();
        String sectionName = this.getClass().getName();
        IDialogSettings dialogSettings = compareSettings.getSection(sectionName);
        if (dialogSettings == null) {
            dialogSettings = compareSettings.addNewSection(sectionName);
        }
        return dialogSettings;
    }

    @Override
    protected Control createDialogArea(Composite parent) {
        Composite container = (Composite) super.createDialogArea(parent);
        parent.getShell()
                .setText(MessageFormat.format(Messages.CommitDialog_Changes, this.gitRepository.currentBranch()));

        container.setLayout(new GridLayout(1, true));

        createDiffArea(container);

        SashForm sashForm = new SashForm(container, SWT.HORIZONTAL);
        // Make it fill grid, so when we resize it still does...
        sashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        sashForm.setLayout(new FillLayout());

        createUnstagedFileArea(sashForm);
        createCommitMessageArea(sashForm);
        createStagedFileArea(sashForm);

        sashForm.setSashWidth(5);
        sashForm.setWeights(new int[] { 35, 30, 35 });

        validate();
        PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
            public void run() {
                packTable(stagedTable);
                packTable(unstagedTable);
                // Select the first item in staged if there is one
                if (unstagedTable.getItemCount() > 0) {
                    unstagedTable.select(0);
                    ChangedFile file = getChangedFile(unstagedTable.getItem(0));
                    updateDiff(false, file);
                    stageButtons.updateSelectionButton();
                } else if (stagedTable.getItemCount() > 0) {
                    stagedTable.select(0);
                    ChangedFile file = getChangedFile(stagedTable.getItem(0));
                    updateDiff(true, file);
                    unstageButtons.updateSelectionButton();
                }
            }
        });
        return container;
    }

    @Override
    public void create() {
        super.create();
        // forces initial validation when the dialog first comes up
        validate();
    }

    /**
     * Override the default implementation to get a bigger commit dialog on large monitors.
     * 
     * @see org.eclipse.jface.dialogs.Dialog#getInitialSize()
     */
    protected Point getInitialSize() {
        IDialogSettings dialogSettings = getDialogBoundsSettings();
        try {
            dialogSettings.getInt("DIALOG_WIDTH"); //$NON-NLS-1$
        } catch (NumberFormatException e) {
            // The dialog settings are empty, so we need to compute the initial
            // size according to the size of the monitor. Large monitors will get a bigger commit dialog.
            Composite parent = getShell().getParent();
            Monitor monitor = getShell().getDisplay().getPrimaryMonitor();
            if (parent != null) {
                monitor = parent.getMonitor();
            }
            Rectangle monitorBounds = monitor.getClientArea();
            return new Point((int) (0.618 * monitorBounds.width), (int) (0.618 * monitorBounds.height));
        }
        return super.getInitialSize();
    }

    private void createDiffArea(Composite container) {
        try {
            diffArea = new Browser(container, SWT.BORDER);
        } catch (SWTError e) {
            // most likely cause is that browser stuff isn't set up on Linux. We provide a warning message in validate()
            diffArea = new Text(container, SWT.BORDER | SWT.MULTI);
        }
        GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
        data.heightHint = 300;
        diffArea.setLayoutData(data);
        setDiffText(Messages.CommitDialog_NoFileSelected);
    }

    private void setDiffText(String msg) {
        if (diffArea instanceof Browser) {
            ((Browser) diffArea).setText(msg);
        } else {
            ((Text) diffArea).setText(msg);
        }
    }

    private void createUnstagedFileArea(SashForm sashForm) {
        createFileArea(sashForm, false);
    }

    private void createStagedFileArea(SashForm sashForm) {
        createFileArea(sashForm, true);
    }

    private void createFileArea(SashForm sashForm, boolean staged) {
        Composite composite = new Composite(sashForm, SWT.NONE);
        composite.setLayout(new GridLayout(2, false));
        StagingButtons buttons = null;
        if (staged) {
            buttons = new StagingButtons(composite, Messages.CommitDialog_UnstageAllMarker,
                    Messages.CommitDialog_UnstageAll, Messages.CommitDialog_UnstageSelectedMarker,
                    Messages.CommitDialog_UnstageSelected);
            createTableComposite(composite, staged);
            buttons.setTable(stagedTable, staged);
            unstageButtons = buttons;
        } else {
            createTableComposite(composite, staged);
            buttons = new StagingButtons(composite, Messages.CommitDialog_StageAllMarker,
                    Messages.CommitDialog_StageAll, Messages.CommitDialog_StageSelectedMarker,
                    Messages.CommitDialog_StageSelected);
            buttons.setTable(unstagedTable, staged);
            stageButtons = buttons;
        }
    }

    // Creates a table with a title label on top of it.
    private Composite createTableComposite(Composite parent, boolean staged) {
        Composite tableComp = new Composite(parent, SWT.NONE);
        GridLayout layout = new GridLayout(1, false);
        layout.horizontalSpacing = 1;
        layout.marginWidth = 1;
        tableComp.setLayout(layout);
        GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
        data.heightHint = 200;
        data.widthHint = 250;
        tableComp.setLayoutData(data);
        Label label = new Label(tableComp, SWT.NONE);
        Table table = null;
        if (staged) {
            label.setText(Messages.CommitDialog_StagedChanges);
            table = createTable(tableComp, true);
        } else {
            label.setText(Messages.CommitDialog_UnstagedChanges);
            table = createTable(tableComp, false);
        }
        if (staged) {
            stagedTable = table;
        } else {
            unstagedTable = table;
        }
        return tableComp;
    }

    private void createCommitMessageArea(SashForm sashForm) {
        Composite msgComp = new Composite(sashForm, SWT.NONE);
        GridLayout layout = new GridLayout(1, true);
        layout.horizontalSpacing = 0;
        layout.marginWidth = 0;
        msgComp.setLayout(layout);
        Label messageLabel = new Label(msgComp, SWT.NONE);
        messageLabel.setText(Messages.CommitDialog_MessageLabel);
        commitMessage = new Text(msgComp, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.WRAP);
        commitMessage.setText(this.gitRepository.getPrepopulatedCommitMessage());
        commitMessage.addKeyListener(new KeyListener() {

            public void keyReleased(KeyEvent e) {
                validate();
            }

            public void keyPressed(KeyEvent e) {
            }
        });
        GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
        data.heightHint = 200;
        data.widthHint = 300;
        commitMessage.setLayoutData(data);

        IContextService service = (IContextService) PlatformUI.getWorkbench().getService(IContextService.class);
        contextActivation = service.activateContext(COMMIT_DIALOG_CONTEXT_ID);

        IHandlerService handlerService = (IHandlerService) PlatformUI.getWorkbench()
                .getService(IHandlerService.class);
        IHandler handler = new AbstractHandler() {
            public Object execute(ExecutionEvent event) {
                okPressed();
                return null;
            }
        };
        commitHandler = handlerService.activateHandler(PERFORM_COMMIT_COMMAND_ID, handler,
                new ActiveShellExpression(getShell()));
    }

    @Override
    public boolean close() {
        if (contextActivation != null) {
            IContextService service = (IContextService) PlatformUI.getWorkbench().getService(IContextService.class);
            service.deactivateContext(contextActivation);
            contextActivation = null;
        }
        if (commitHandler != null) {
            IHandlerService service = (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class);
            service.deactivateHandler(commitHandler);
            commitHandler.getHandler().dispose();
            commitHandler = null;
        }
        return super.close();
    }

    private Table createTable(Composite composite, final boolean staged) {
        Table table = new Table(composite, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.FULL_SELECTION);
        table.setLinesVisible(true);
        table.setHeaderVisible(true);
        GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
        data.heightHint = 200;
        data.widthHint = 250;
        table.setLayoutData(data);
        String[] titles = { " ", Messages.CommitDialog_PathColumnLabel }; //$NON-NLS-1$
        int[] widths = new int[] { 20, 250 };
        for (int i = 0; i < titles.length; i++) {
            TableColumn column = new TableColumn(table, SWT.NONE);
            column.setText(titles[i]);
            column.setWidth(widths[i]);
        }
        List<ChangedFile> changedFiles = gitRepository.index().changedFiles();
        Collections.sort(changedFiles);
        for (ChangedFile file : changedFiles) {
            boolean match = false;
            if (staged && file.hasStagedChanges()) {
                match = true;
            } else if (!staged && file.hasUnstagedChanges()) {
                match = true;
            }

            if (match) {
                createTableItem(table, file, false);
            }
        }

        // Drag and Drop
        // FIXME If user drags and drops while we're still crunching on last drag/drop then we end up hanging
        // Seems to be related to manipulating the table here before we receive the index changed callback
        Transfer[] types = new Transfer[] { LocalSelectionTransfer.getTransfer() };

        // Drag Source
        DragSource source = new DragSource(table, DND.DROP_MOVE);
        source.setTransfer(types);
        source.addDragListener(new DragSourceAdapter() {
            public void dragStart(DragSourceEvent event) {
                DragSource ds = (DragSource) event.widget;
                draggingFromTable = ds.getControl();

                LocalSelectionTransfer.getTransfer()
                        .setSelection(new StructuredSelection(((Table) draggingFromTable).getSelection()));
                LocalSelectionTransfer.getTransfer().setSelectionSetTime(event.time & 0xFFFFFFFFL);
            }

            public void dragSetData(DragSourceEvent event) {
                // do nothing
            }
        });

        // Create the drop target
        DropTarget target = new DropTarget(table, DND.DROP_MOVE);
        target.setTransfer(types);
        if (table.getItemCount() == 0)
            target.setDropTargetEffect(null);
        target.addDropListener(new DropTargetAdapter() {
            public void dropAccept(DropTargetEvent event) {
                DropTarget dp = (DropTarget) event.widget;
                if (dp.getControl() == draggingFromTable) {
                    event.detail = DND.DROP_NONE;
                }
            }

            public void dragEnter(DropTargetEvent event) {
                // Allow dropping text only
                for (int i = 0, n = event.dataTypes.length; i < n; i++) {
                    if (LocalSelectionTransfer.getTransfer().isSupportedType(event.dataTypes[i])) {
                        event.currentDataType = event.dataTypes[i];
                    }
                }
                event.operations = DND.DROP_MOVE;
            }

            public void dragOver(DropTargetEvent event) {
                event.feedback = DND.FEEDBACK_SCROLL;
            }

            @SuppressWarnings("unchecked")
            public void drop(DropTargetEvent event) {
                if (!LocalSelectionTransfer.getTransfer().isSupportedType(event.currentDataType))
                    return;
                // Get the dropped data
                IStructuredSelection selection = (IStructuredSelection) event.data;
                moveItems(!staged, ((List<TableItem>) selection.toList()).toArray(new TableItem[selection.size()]));
            }
        });

        table.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                super.widgetSelected(e);
                if (e.item == null)
                    return;
                TableItem item = (TableItem) e.item;
                updateDiff(staged, getChangedFile(item));
            }
        });
        // Allow double-clicking to toggle staged/unstaged
        table.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {
                if (e.getSource() == null)
                    return;
                Table table = (Table) e.getSource();
                Point point = new Point(e.x, e.y);
                TableItem item = table.getItem(point);
                if (item == null) {
                    return;
                }
                // did user click on file image? If so, toggle staged/unstage
                Rectangle imageBounds = item.getBounds(0);
                if (imageBounds.contains(point)) {
                    moveItems(staged, new TableItem[] { item });
                }
            }

            @Override
            public void mouseDoubleClick(MouseEvent e) {
                if (e.getSource() == null)
                    return;
                Table table = (Table) e.getSource();
                TableItem[] selected = table.getSelection();
                moveItems(staged, selected);
            }
        });
        // Custom drawing so we can truncate filepaths in middle...
        table.addListener(SWT.EraseItem, new Listener() {

            public void handleEvent(Event event) {
                // Only draw the text custom
                if (event.index != 1)
                    return;

                event.detail &= ~SWT.FOREGROUND;
            }
        });
        table.addListener(SWT.PaintItem, new Listener() {

            public void handleEvent(Event event) {
                // Only draw the text custom
                if (event.index != 1)
                    return;
                TableItem item = (TableItem) event.item;
                String text = item.getText(event.index);

                // Truncate middle of string
                Table theTable = (Table) event.widget;
                int width = theTable.getColumn(event.index).getWidth();
                Point p = event.gc.stringExtent(text); // is text wider than available width?
                if (p.x > width) {
                    // chop string in half and drop a few characters
                    int middle = text.length() / 2;
                    String beginning = text.substring(0, middle - 1);
                    String end = text.substring(middle + 2, text.length());
                    // Now repeatedly chop off one char from each end until we fit
                    // TODO Chop each side separately? it'd take more loops, but text would fit tighter when uneven
                    // lengths work better..
                    while (event.gc.stringExtent(beginning + "..." + end).x > width) //$NON-NLS-1$
                    {
                        if (beginning.length() > 0) {
                            beginning = beginning.substring(0, beginning.length() - 1);
                        } else {
                            break;
                        }
                        if (end.length() > 0) {
                            end = end.substring(1);
                        } else {
                            break;
                        }
                    }
                    text = beginning + "..." + end; //$NON-NLS-1$
                }
                event.gc.drawText(text, event.x, event.y, true);

                event.detail &= ~SWT.FOREGROUND;
            }
        });

        if (!staged) {
            final Table myTable = table;
            MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
            menuMgr.setRemoveAllWhenShown(true);
            menuMgr.addMenuListener(new IMenuListener() {
                public void menuAboutToShow(IMenuManager manager) {
                    TableItem[] selected = myTable.getSelection();
                    List<IResource> files = new ArrayList<IResource>();
                    final List<ChangedFile> changedFiles = new ArrayList<ChangedFile>();
                    for (TableItem item : selected) {
                        ChangedFile file = getChangedFile(item);
                        if (file != null) {
                            changedFiles.add(file);
                            IFile iFile = gitRepository.getFileForChangedFile(file);
                            if (iFile != null) {
                                files.add(iFile);
                            }
                        }
                    }

                    ContributionItem ci = new ContributionItem() {
                        public void fill(Menu menu, int index) {
                            MenuItem item = new MenuItem(menu, SWT.NONE);
                            item.setText(Messages.CommitDialog_RevertLabel);
                            // need to remove the file(s) from staged table once action runs
                            item.addSelectionListener(new SelectionAdapter() {
                                @Override
                                public void widgetSelected(SelectionEvent e) {
                                    // need to make a copy because operation will actually change input files.
                                    final List<ChangedFile> copy = new ArrayList<ChangedFile>(changedFiles);
                                    for (ChangedFile cf : changedFiles) {
                                        copy.add(new ChangedFile(cf));
                                    }

                                    gitRepository.index().discardChangesForFiles(changedFiles);

                                    PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {

                                        public void run() {
                                            // If this file was shown in diff area, we need to blank the diff area!
                                            if (fLastDiffFile != null) {
                                                for (ChangedFile file : copy) {
                                                    if (file != null && file.equals(fLastDiffFile)) {
                                                        updateDiff(null, Messages.CommitDialog_NoFileSelected);
                                                    }
                                                }
                                            }
                                            removeDraggedFilesFromSource(unstagedTable, copy);
                                        }
                                    });
                                }
                            });
                        }
                    };
                    manager.add(ci);
                    // Other plug-ins can contribute there actions here
                    manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
                }
            });
            Menu menu = menuMgr.createContextMenu(table);
            table.setMenu(menu);
        }

        return table;
    }

    private synchronized void unstageFiles(final Collection<ChangedFile> files) {
        // TODO Add a listener to the repo on creation and have toggleStageStatus get invoked with diff!
        // Temporarily disable the tables
        stagedTable.setEnabled(false);
        unstagedTable.setEnabled(false);
        // make a copy so we can erase from original table correctly since their flags get changed by operation
        final List<ChangedFile> copy = new ArrayList<ChangedFile>(files);
        Collections.copy(copy, new ArrayList<ChangedFile>(files));
        IStatus status = gitRepository.index().unstageFiles(files);
        if (status.isOK()) {
            getParentShell().getDisplay().asyncExec(new Runnable() {

                public void run() {
                    toggleStageStatus(copy, false);
                    stagedTable.setEnabled(true);
                    unstagedTable.setEnabled(true);
                }
            });
        } else {
            stagedTable.setEnabled(true);
            unstagedTable.setEnabled(true);
        }
    }

    private synchronized void stageFiles(final Collection<ChangedFile> files) {
        // Temporarily disable the tables
        stagedTable.setEnabled(false);
        unstagedTable.setEnabled(false);
        // make a copy so we can erase from original table correctly since their flags get changed by operation
        final List<ChangedFile> copy = new ArrayList<ChangedFile>(files);
        Collections.copy(copy, new ArrayList<ChangedFile>(files));
        IStatus status = gitRepository.index().stageFiles(files);
        if (status.isOK()) {
            getParentShell().getDisplay().asyncExec(new Runnable() {

                public void run() {
                    toggleStageStatus(copy, true);
                    stagedTable.setEnabled(true);
                    unstagedTable.setEnabled(true);
                }
            });
        } else {
            stagedTable.setEnabled(true);
            unstagedTable.setEnabled(true);
        }
    }

    private void toggleStageStatus(Collection<ChangedFile> files, boolean stage) {
        Table to = stagedTable;
        Table from = unstagedTable;
        if (!stage) {
            from = stagedTable;
            to = unstagedTable;
        }
        to.setRedraw(false);
        for (ChangedFile changedFile : files) {
            createTableItem(to, changedFile, true); // add it to our new table
        }
        packTable(to);
        to.setRedraw(true);
        // to.redraw();
        removeDraggedFilesFromSource(from, files);
        workaroundEmptyTableDropEffectBug(from);
        validate();
    }

    /**
     * Update the diff area.
     * 
     * @param staged
     * @param file
     * @see #updateDiff(ChangedFile, String)
     */
    private void updateDiff(final boolean staged, ChangedFile file) {
        if (file == null) {
            return;
        }
        boolean isBrowser = (diffArea instanceof Browser);

        if (isBrowser && gitRepository.index().hasBinaryAttributes(file)
                && !file.getStatus().equals(ChangedFile.Status.DELETED)) {
            // Special code to draw the image if the binary file is an image
            String[] imageExtensions = new String[] { ".png", ".gif", ".jpeg", ".jpg", ".ico" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$
            for (String extension : imageExtensions) {
                if (file.getPath().endsWith(extension)) {
                    IPath fullPath = gitRepository.workingDirectory().append(file.getPath());
                    updateDiff(file, "<img src=\"" + fullPath.toOSString() + "\" />"); //$NON-NLS-1$ //$NON-NLS-2$
                    return;
                }
            }
        }
        // Don't recalc if it's the same file as we are already showing
        if (fLastDiffFile != null && file.equals(fLastDiffFile)) {
            return;
        }

        String diff = gitRepository.index().diffForFile(file, staged, 3);
        if (isBrowser) {
            try {
                diff = DiffFormatter.toHTML(file.getPath(), diff);
            } catch (Throwable t) {
                IdeLog.logError(GitUIPlugin.getDefault(), "Failed to turn diff into HTML", t, IDebugScopes.DEBUG); //$NON-NLS-1$
            }
        }
        updateDiff(file, diff);
    }

    /**
     * Update the diff area.
     * 
     * @param file
     * @param diff
     * @see #updateDiff(boolean, String)
     */
    private void updateDiff(ChangedFile file, String diff) {
        if (diffArea != null && !diffArea.isDisposed()) {
            setDiffText(diff);
            fLastDiffFile = file;
        }
    }

    /**
     * Creates a table item for a ChangedFile in Git
     * 
     * @param table
     * @param file
     */
    private void createTableItem(Table table, ChangedFile file, boolean sort) {
        TableItem item = null;
        if (sort) {
            // insert into sorted table
            TableItem[] items = table.getItems();
            int index = 0;
            for (TableItem existing : items) {
                String path = existing.getText(1);
                if (file.getPath().compareTo(path) < 0) {
                    break;
                }
                index++;
            }
            item = new TableItem(table, SWT.NONE, index);
        } else {
            // Just insert at end
            item = new TableItem(table, SWT.NONE);
        }
        Image image = emptyFileImage;
        // String text = Messages.CommitDialog_modified;
        if (file.getStatus() == ChangedFile.Status.DELETED) {
            image = deletedFileImage;
            // text = Messages.CommitDialog_deleted;
        } else if (file.getStatus() == ChangedFile.Status.NEW) {
            image = newFileImage;
            // text = Messages.CommitDialog_new;
        }
        // item.setText(0, text);
        item.setImage(0, image);
        item.setText(1, file.getPath());
        item.setData(CHANGED_FILE_DATA_KEY, file);
    }

    private void packTable(Table table) {
        // pack first column (image)
        table.getColumn(0).pack();
        // Make the second column take all the available width!
        int totalWidth = table.getClientArea().width;
        if (totalWidth > 0) {
            totalWidth -= table.getColumn(0).getWidth();
            table.getColumn(1).setWidth(totalWidth);
        }
    }

    private void validate() {
        if (commitMessage.getText().length() < 1) {
            updateStatus(
                    new Status(IStatus.ERROR, GitUIPlugin.getPluginId(), Messages.CommitDialog_EnterMessage_Error));
            return;
        }
        if (stagedTable.getItemCount() == 0) {
            updateStatus(new Status(IStatus.ERROR, GitUIPlugin.getPluginId(),
                    Messages.CommitDialog_StageFilesFirst_Error));
            return;
        }
        if (gitRepository.hasUnresolvedMergeConflicts()) {
            updateStatus(
                    new Status(IStatus.ERROR, GitUIPlugin.getPluginId(), Messages.CommitDialog_CannotMerge_Error));
            return;
        }
        fMessage = commitMessage.getText();

        if (!(diffArea instanceof Browser)) {
            updateStatus(new Status(IStatus.WARNING, GitUIPlugin.getPluginId(),
                    Messages.CommitDialog_BrowserWidgetFailedMsg));
        } else {
            updateStatus(Status.OK_STATUS);
        }
    }

    // TODO Change way dialog is composed to push buttons into commit message area like GitX?

    @Override
    protected Button createButton(Composite parent, int id, String label, boolean defaultButton) {
        if (id == IDialogConstants.OK_ID) {
            label = Messages.CommitDialog_CommitButton_Label;
        } else if (id == IDialogConstants.CANCEL_ID) {
            label = Messages.CommitDialog_CloseButton_Label;
        }
        return super.createButton(parent, id, label, defaultButton);
    }

    @Override
    protected void okPressed() {
        // if there are still unstaged changes don't set return code and close.
        if (unstagedTable.getItemCount() > 0) {
            // disable the buttons until commit is done
            getButton(IDialogConstants.CANCEL_ID).setEnabled(false);
            getButton(IDialogConstants.OK_ID).setEnabled(false);
            boolean success = gitRepository.index().commit(getCommitMessage());
            if (success) {
                // commit worked, wipe commit message and staged files in table
                commitMessage.setText(StringUtil.EMPTY);
                stagedTable.removeAll();
                // TODO Show some sort of success message?
            }
            // TODO What if the commit failed for some reason?!
            // Re-enable buttons
            getButton(IDialogConstants.CANCEL_ID).setEnabled(true);
            getButton(IDialogConstants.OK_ID).setEnabled(true);
        } else {
            super.okPressed();
        }
    }

    public String getCommitMessage() {
        return fMessage;
    }

    /**
     * Find the table items we just dragged over and remove them from the source table
     * 
     * @param sourceTable
     * @param draggedFiles
     */
    private void removeDraggedFilesFromSource(Table sourceTable, Collection<ChangedFile> draggedFiles) {
        if (draggedFiles == null || draggedFiles.isEmpty()) {
            return;
        }
        TableItem[] items = sourceTable.getItems();
        if (draggedFiles.size() == items.length) {
            // shortcut for moving all
            sourceTable.setRedraw(false);
            sourceTable.removeAll();
            packTable(sourceTable);
            sourceTable.setRedraw(true);
            return;
        }

        List<Integer> toRemove = new ArrayList<Integer>();
        for (int i = 0; i < items.length; i++) {
            TableItem item = items[i];
            if (draggedFiles.contains(getChangedFile(item))) {
                toRemove.add(i);
            }
        }
        int[] primitive = new int[toRemove.size()];
        int x = 0;
        for (Integer object : toRemove) {
            primitive[x++] = object.intValue();
        }
        sourceTable.setRedraw(false);
        sourceTable.remove(primitive);
        packTable(sourceTable);
        sourceTable.setRedraw(true);
        sourceTable.redraw();
    }

    private ChangedFile getChangedFile(TableItem item) {
        return (ChangedFile) item.getData(CHANGED_FILE_DATA_KEY);
    }

    /**
     * HACK Workaround bug where drag and drop breaks horribly on a table when it's empty
     * 
     * @param sourceDragTable
     */
    private void workaroundEmptyTableDropEffectBug(Table sourceDragTable) {
        DropTarget dtarget = (DropTarget) sourceDragTable.getData(DND.DROP_TARGET_KEY);
        if (dtarget == null)
            return;
        if (sourceDragTable.getItemCount() == 0) {
            dtarget.setDropTargetEffect(null);
        } else if (dtarget.getDropTargetEffect() == null) {
            dtarget.setDropTargetEffect(new TableDropTargetEffect(sourceDragTable));
        }
    }

    @Override
    protected boolean isResizable() {
        return true;
    }

    /*
     * Stage or Un-Stage the selected items (files).
     * @param staged
     * @param selected
     */
    private void moveItems(final boolean staged, TableItem... selected) {
        Set<ChangedFile> selectedFiles = new HashSet<ChangedFile>();
        for (TableItem item : selected) {
            ChangedFile file = getChangedFile(item);
            if (file == null)
                continue;
            selectedFiles.add(file);
        }
        if (selectedFiles.isEmpty())
            return;

        // Actually stage or unstage the files
        if (staged) {
            unstageFiles(selectedFiles);
        } else {
            stageFiles(selectedFiles);
        }
    }

    /*
     * This class will create buttons next to the tables in order to allow staging and un-staging operations.
     */
    private class StagingButtons implements SelectionListener {
        private String doAllLabel;
        private String doAllTooltip;
        private String doSelectionLabel;
        private String doSelectionTooltip;
        private Table table;
        private boolean staged;
        private Button doSelectionBt;
        private Button doAllBt;

        /**
         * Constructs a new StagingButtons instance.
         * 
         * @param parent
         * @param doAllLabel
         * @param doAllTooltip
         * @param doSelectionLabel
         * @param doSelectionTooltip
         */
        protected StagingButtons(Composite parent, String doAllLabel, String doAllTooltip, String doSelectionLabel,
                String doSelectionTooltip) {
            this.doAllLabel = doAllLabel;
            this.doAllTooltip = doAllTooltip;
            this.doSelectionLabel = doSelectionLabel;
            this.doSelectionTooltip = doSelectionTooltip;
            createComponent(parent);
        }

        /**
         * Set the table (staged or un-staged) that this component controls.
         * 
         * @param table
         * @param staged
         */
        protected void setTable(Table table, boolean staged) {
            this.table = table;
            this.staged = staged;
            table.addSelectionListener(this);
            updateSelectionButton();
        }

        protected void createComponent(Composite parent) {
            Composite comp = new Composite(parent, SWT.NONE);
            GridLayout layout = new GridLayout(1, true);
            layout.horizontalSpacing = 0;
            layout.marginWidth = 1;
            comp.setLayout(layout);

            doAllBt = new Button(comp, SWT.PUSH | SWT.FLAT);
            doAllBt.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
            doAllBt.setText(doAllLabel);
            doAllBt.setToolTipText(doAllTooltip);
            doAllBt.addSelectionListener(this);
            doSelectionBt = new Button(comp, SWT.PUSH | SWT.FLAT);
            doSelectionBt.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
            doSelectionBt.setText(doSelectionLabel);
            doSelectionBt.setToolTipText(doSelectionTooltip);
            doSelectionBt.addSelectionListener(this);

            // minimize the width of this component
            Point doAllSize = doAllBt.computeSize(SWT.DEFAULT, SWT.DEFAULT);
            Point doSelectionSize = doSelectionBt.computeSize(SWT.DEFAULT, SWT.DEFAULT);
            Point bigger = null;
            if (doAllSize.x > doSelectionSize.x) {
                bigger = doAllSize;
            } else {
                bigger = doSelectionSize;
            }
            GridData data = new GridData(SWT.CENTER, SWT.CENTER, false, false);
            data.widthHint = bigger.x + 10;
            comp.setLayoutData(data);
        }

        public void widgetDefaultSelected(SelectionEvent e) {
        }

        public void widgetSelected(SelectionEvent e) {
            Object source = e.getSource();
            if (source instanceof Table) {
                updateSelectionButton();
            } else if (source == doAllBt) {
                if (table != null) {
                    moveItems(staged, table.getItems());
                    updateSelectionDiff(-1);
                    updateSelectionButton();
                }
            } else if (source == doSelectionBt) {
                if (table != null) {
                    int selectionIndex = table.getSelectionIndex();
                    TableItem[] selection = table.getSelection();
                    moveItems(staged, selection);
                    updateSelectionDiff(selectionIndex);
                    updateSelectionButton();
                }
            }
        }

        // Enable the doSelectionBt when the table has a selection
        private void updateSelectionButton() {
            doSelectionBt.setEnabled(table.getSelectionCount() > 0);
        }

        private void updateSelectionDiff(int previousSelectionIndex) {
            // Select the next file in line (if exists)
            if (table.getItemCount() > 0 && previousSelectionIndex > -1) {
                if (table.getItemCount() > previousSelectionIndex) {
                    table.select(previousSelectionIndex);
                } else {
                    table.select(table.getItemCount() - 1);
                }
                ChangedFile file = getChangedFile(table.getSelection()[0]);
                updateDiff(staged, file);
            } else {
                updateDiff(null, Messages.CommitDialog_NoFileSelected);
            }
        }
    }
}