org.eclipse.team.svn.ui.discovery.wizards.ConnectorDiscoveryWizardMainPage.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.team.svn.ui.discovery.wizards.ConnectorDiscoveryWizardMainPage.java

Source

/*******************************************************************************
 * Copyright (c) 2009 Tasktop Technologies, Polarion Software and others.
 * 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:
 *     Tasktop Technologies - initial API and implementation
 *     David Green
 *     Shawn Minto bug 275513
 *       Steffen Pingel bug 276012 code review, bug 277191 gradient canvas
 *******************************************************************************/
package org.eclipse.team.svn.ui.discovery.wizards;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IBundleGroup;
import org.eclipse.core.runtime.IBundleGroupProvider;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.ColorRegistry;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.DecorationOverlayIcon;
import org.eclipse.jface.viewers.IDecoration;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.ACC;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleControlAdapter;
import org.eclipse.swt.accessibility.AccessibleControlEvent;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Resource;
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.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.team.svn.core.SVNTeamPlugin;
import org.eclipse.team.svn.core.discovery.model.AbstractDiscoverySource;
import org.eclipse.team.svn.core.discovery.model.BundleDiscoveryStrategy;
import org.eclipse.team.svn.core.discovery.model.ConnectorDescriptor;
import org.eclipse.team.svn.core.discovery.model.ConnectorDescriptorKind;
import org.eclipse.team.svn.core.discovery.model.ConnectorDiscovery;
import org.eclipse.team.svn.core.discovery.model.DiscoveryCategory;
import org.eclipse.team.svn.core.discovery.model.DiscoveryConnector;
import org.eclipse.team.svn.core.discovery.model.Icon;
import org.eclipse.team.svn.core.discovery.model.Overview;
import org.eclipse.team.svn.core.discovery.model.RemoteBundleDiscoveryStrategy;
import org.eclipse.team.svn.core.discovery.util.DiscoveryCategoryComparator;
import org.eclipse.team.svn.core.discovery.util.DiscoveryConnectorComparator;
import org.eclipse.team.svn.core.operation.LoggedOperation;
import org.eclipse.team.svn.ui.SVNTeamUIPlugin;
import org.eclipse.team.svn.ui.SVNUIMessages;
import org.eclipse.team.svn.ui.discovery.GradientCanvas;
import org.eclipse.team.svn.ui.discovery.util.DiscoveryUiUtil;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.IFormColors;
import org.eclipse.ui.progress.WorkbenchJob;

/**
 * The main wizard page that allows users to select connectors that they wish to install.
 * 
 * @author David Green
 * @author Igor Burilo
 */
public class ConnectorDiscoveryWizardMainPage extends WizardPage {

    private static String DISCOVERY_PROPERTIES_FILE = "discovery.properties";
    private static String URL_DISCOVERY_PROPERTY = "url";

    private static final String COLOR_WHITE = "white"; //$NON-NLS-1$
    private static final String COLOR_DARK_GRAY = "DarkGray"; //$NON-NLS-1$
    private static final String COLOR_CATEGORY_GRADIENT_START = "category.gradient.start"; //$NON-NLS-1$
    private static final String COLOR_CATEGORY_GRADIENT_END = "category.gradient.end"; //$NON-NLS-1$ 

    private static Boolean useNativeSearchField;

    private final List<ConnectorDescriptor> installableConnectors = new ArrayList<ConnectorDescriptor>();

    private ConnectorDiscovery discovery;

    private Composite body;

    private final List<Resource> disposables = new ArrayList<Resource>();

    private Font h2Font;

    private Font h1Font;

    private Color colorWhite;

    private Text filterText;

    private WorkbenchJob refreshJob;

    private String previousFilterText = ""; //$NON-NLS-1$

    private Pattern filterPattern;

    private Label clearFilterTextControl;

    private Set<String> installedFeatures;

    private Image infoImage;

    private Cursor handCursor;

    private Color colorCategoryGradientStart;

    private Color colorCategoryGradientEnd;

    private Color colorDisabled;

    private ScrolledComposite bodyScrolledComposite;

    public ConnectorDiscoveryWizardMainPage() {
        super(ConnectorDiscoveryWizardMainPage.class.getSimpleName());
        setTitle(SVNUIMessages.ConnectorDiscoveryWizardMainPage_connectorDiscovery);
        // setImageDescriptor(image);
        setDescription(SVNUIMessages.ConnectorDiscoveryWizardMainPage_pageDescription);
        setPageComplete(false);
    }

    public void createControl(Composite parent) {
        createRefreshJob();

        Composite container = new Composite(parent, SWT.NULL);
        container.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                refreshJob.cancel();
            }
        });
        container.setLayout(new GridLayout(1, false));
        //      
        { // header
            Composite header = new Composite(container, SWT.NULL);
            GridLayoutFactory.fillDefaults().applyTo(header);
            GridDataFactory.fillDefaults().grab(true, false).applyTo(header);

            //          TODO: refresh button?
            if (getWizard().isShowConnectorDescriptorKindFilter()
                    || getWizard().isShowConnectorDescriptorTextFilter()) {
                Composite filterContainer = new Composite(header, SWT.NULL);
                GridDataFactory.fillDefaults().grab(true, false).applyTo(filterContainer);

                int numColumns = 1; // 1 for label
                if (getWizard().isShowConnectorDescriptorKindFilter()) {
                    numColumns += ConnectorDescriptorKind.values().length;
                }
                if (getWizard().isShowConnectorDescriptorTextFilter()) {
                    ++numColumns;
                }
                GridLayoutFactory.fillDefaults().numColumns(numColumns).applyTo(filterContainer);
                Label label = new Label(filterContainer, SWT.NULL);
                label.setText(SVNUIMessages.ConnectorDiscoveryWizardMainPage_filterLabel);

                if (getWizard().isShowConnectorDescriptorTextFilter()) {
                    Composite textFilterContainer;
                    boolean nativeSearch = useNativeSearchField(header);
                    if (nativeSearch) {
                        textFilterContainer = new Composite(filterContainer, SWT.NULL);
                    } else {
                        textFilterContainer = new Composite(filterContainer, SWT.BORDER);
                        textFilterContainer
                                .setBackground(header.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
                    }
                    GridDataFactory.fillDefaults().grab(true, false).applyTo(textFilterContainer);
                    GridLayoutFactory.fillDefaults().numColumns(2).applyTo(textFilterContainer);

                    if (nativeSearch) {
                        filterText = new Text(textFilterContainer,
                                SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL);
                    } else {
                        filterText = new Text(textFilterContainer, SWT.SINGLE);
                    }

                    filterText.addModifyListener(new ModifyListener() {
                        public void modifyText(ModifyEvent e) {
                            filterTextChanged();
                        }
                    });
                    if (nativeSearch) {
                        filterText.addSelectionListener(new SelectionAdapter() {
                            @Override
                            public void widgetDefaultSelected(SelectionEvent e) {
                                if (e.detail == SWT.ICON_CANCEL) {
                                    clearFilterText();
                                }
                            }
                        });
                        GridDataFactory.fillDefaults().grab(true, false).span(2, 1).applyTo(filterText);
                    } else {
                        GridDataFactory.fillDefaults().grab(true, false).applyTo(filterText);
                        clearFilterTextControl = createClearFilterTextControl(textFilterContainer, filterText);
                        clearFilterTextControl.setVisible(false);
                    }
                }

                if (getWizard().isShowConnectorDescriptorKindFilter()) { // filter buttons

                    for (final ConnectorDescriptorKind kind : ConnectorDescriptorKind.values()) {
                        final Button checkbox = new Button(filterContainer, SWT.CHECK);
                        checkbox.setSelection(getWizard().isVisible(kind));
                        checkbox.setText(getFilterLabel(kind));
                        checkbox.addSelectionListener(new SelectionListener() {
                            public void widgetSelected(SelectionEvent e) {
                                boolean selection = checkbox.getSelection();
                                getWizard().setVisibility(kind, selection);
                                connectorDescriptorKindVisibilityUpdated();
                            }

                            public void widgetDefaultSelected(SelectionEvent e) {
                                widgetSelected(e);
                            }
                        });
                    }
                }

            }

        }
        { // container
            body = new Composite(container, SWT.NULL);
            GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 480).applyTo(body);
        }
        Dialog.applyDialogFont(container);
        setControl(container);

        //Setting context help
        PlatformUI.getWorkbench().getHelpSystem().setHelp(parent,
                "org.eclipse.team.svn.help.connectorDiscoveryWizContext"); //$NON-NLS-1$
    }

    private static boolean useNativeSearchField(Composite composite) {
        if (useNativeSearchField == null) {
            useNativeSearchField = Boolean.FALSE;
            Text testText = null;
            try {
                testText = new Text(composite, SWT.SEARCH | SWT.ICON_CANCEL);
                useNativeSearchField = new Boolean((testText.getStyle() & SWT.ICON_CANCEL) != 0);
            } finally {
                if (testText != null) {
                    testText.dispose();
                }
            }

        }
        return useNativeSearchField;
    }

    private void createRefreshJob() {
        refreshJob = new WorkbenchJob("filter") { //$NON-NLS-1$

            @Override
            public IStatus runInUIThread(IProgressMonitor monitor) {
                if (filterText.isDisposed()) {
                    return Status.CANCEL_STATUS;
                }
                String text = filterText.getText();
                text = text.trim();

                if (!previousFilterText.equals(text)) {
                    previousFilterText = text;
                    filterPattern = createPattern(previousFilterText);
                    if (clearFilterTextControl != null) {
                        clearFilterTextControl.setVisible(filterPattern != null);
                    }
                    createBodyContents();
                }
                return Status.OK_STATUS;
            }
        };
        refreshJob.setSystem(true);
    }

    protected Pattern createPattern(String filterText) {
        if (filterText == null || filterText.length() == 0) {
            return null;
        }
        String regex = filterText;
        regex.replace("\\", "\\\\").replace("?", ".").replace("*", ".*?"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
        return Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
    }

    private Label createClearFilterTextControl(Composite filterContainer, final Text filterText) {
        final Image inactiveImage = SVNTeamUIPlugin.instance()
                .getImageDescriptor("icons/wizards/find-clear-disabled.gif").createImage();
        final Image activeImage = SVNTeamUIPlugin.instance().getImageDescriptor("icons/wizards/find-clear.gif")
                .createImage();
        final Image pressedImage = new Image(filterContainer.getDisplay(), activeImage, SWT.IMAGE_GRAY);

        final Label clearButton = new Label(filterContainer, SWT.NONE);
        clearButton.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
        clearButton.setImage(inactiveImage);
        clearButton.setToolTipText(SVNUIMessages.ConnectorDiscoveryWizardMainPage_clearButton_toolTip);
        clearButton.setBackground(filterContainer.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
        clearButton.addMouseListener(new MouseAdapter() {
            private MouseMoveListener fMoveListener;

            @Override
            public void mouseDown(MouseEvent e) {
                clearButton.setImage(pressedImage);
                fMoveListener = new MouseMoveListener() {
                    private boolean fMouseInButton = true;

                    public void mouseMove(MouseEvent e) {
                        boolean mouseInButton = isMouseInButton(e);
                        if (mouseInButton != fMouseInButton) {
                            fMouseInButton = mouseInButton;
                            clearButton.setImage(mouseInButton ? pressedImage : inactiveImage);
                        }
                    }
                };
                clearButton.addMouseMoveListener(fMoveListener);
            }

            @Override
            public void mouseUp(MouseEvent e) {
                if (fMoveListener != null) {
                    clearButton.removeMouseMoveListener(fMoveListener);
                    fMoveListener = null;
                    boolean mouseInButton = isMouseInButton(e);
                    clearButton.setImage(mouseInButton ? activeImage : inactiveImage);
                    if (mouseInButton) {
                        clearFilterText();
                        filterText.setFocus();
                    }
                }
            }

            private boolean isMouseInButton(MouseEvent e) {
                Point buttonSize = clearButton.getSize();
                return 0 <= e.x && e.x < buttonSize.x && 0 <= e.y && e.y < buttonSize.y;
            }
        });
        clearButton.addMouseTrackListener(new MouseTrackListener() {
            public void mouseEnter(MouseEvent e) {
                clearButton.setImage(activeImage);
            }

            public void mouseExit(MouseEvent e) {
                clearButton.setImage(inactiveImage);
            }

            public void mouseHover(MouseEvent e) {
            }
        });
        clearButton.addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent e) {
                inactiveImage.dispose();
                activeImage.dispose();
                pressedImage.dispose();
            }
        });
        clearButton.getAccessible().addAccessibleListener(new AccessibleAdapter() {
            @Override
            public void getName(AccessibleEvent e) {
                e.result = SVNUIMessages.ConnectorDiscoveryWizardMainPage_clearButton_accessibleListener;
            }
        });
        clearButton.getAccessible().addAccessibleControlListener(new AccessibleControlAdapter() {
            @Override
            public void getRole(AccessibleControlEvent e) {
                e.detail = ACC.ROLE_PUSHBUTTON;
            }
        });
        return clearButton;
    }

    @Override
    public ConnectorDiscoveryWizard getWizard() {
        return (ConnectorDiscoveryWizard) super.getWizard();
    }

    private void clearFilterText() {
        filterText.setText(""); //$NON-NLS-1$
        filterTextChanged();
    }

    private void filterTextChanged() {
        refreshJob.cancel();
        refreshJob.schedule(200L);
    }

    private String getFilterLabel(ConnectorDescriptorKind kind) {
        switch (kind) {
        case DOCUMENT:
            return SVNUIMessages.ConnectorDiscoveryWizardMainPage_filter_documents;
        case TASK:
            return SVNUIMessages.ConnectorDiscoveryWizardMainPage_filter_tasks;
        case VCS:
            return SVNUIMessages.ConnectorDiscoveryWizardMainPage_filter_vcs;
        default:
            throw new IllegalStateException(kind.name());
        }
    }

    /**
     * cause the UI to respond to a change in visibility filters
     * 
     * @see #setVisibility(ConnectorDescriptorKind, boolean)
     */
    public void connectorDescriptorKindVisibilityUpdated() {
        createBodyContents();
    }

    @Override
    public void dispose() {
        super.dispose();
        for (Resource resource : disposables) {
            resource.dispose();
        }
        clearDisposables();
        if (discovery != null) {
            discovery.dispose();
        }
    }

    private void clearDisposables() {
        disposables.clear();
        h1Font = null;
        h2Font = null;
        infoImage = null;
        handCursor = null;
        colorCategoryGradientStart = null;
        colorCategoryGradientEnd = null;
    }

    public void createBodyContents() {
        // remove any existing contents
        for (Control child : body.getChildren()) {
            child.dispose();
        }
        clearDisposables();
        initializeCursors();
        initializeImages();
        initializeFonts();
        initializeColors();

        GridLayoutFactory.fillDefaults().applyTo(body);

        bodyScrolledComposite = new ScrolledComposite(body, SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER);

        configureLook(bodyScrolledComposite, colorWhite);
        GridDataFactory.fillDefaults().grab(true, true).applyTo(bodyScrolledComposite);

        // FIXME 3.2 does white work for any desktop theme, e.g. an inverse theme?
        final Composite scrolledContents = new Composite(bodyScrolledComposite, SWT.NONE);
        configureLook(scrolledContents, colorWhite);
        scrolledContents.setRedraw(false);
        try {
            createDiscoveryContents(scrolledContents);
        } finally {
            scrolledContents.layout(true);
            scrolledContents.setRedraw(true);
        }
        Point size = scrolledContents.computeSize(body.getSize().x, SWT.DEFAULT, true);
        scrolledContents.setSize(size);

        bodyScrolledComposite.setExpandHorizontal(true);
        bodyScrolledComposite.setMinWidth(100);
        bodyScrolledComposite.setExpandVertical(true);
        bodyScrolledComposite.setMinHeight(1);

        bodyScrolledComposite.addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                // XXX small offset in case list has a scroll bar
                Point size = scrolledContents.computeSize(body.getSize().x - 20, SWT.DEFAULT, true);
                scrolledContents.setSize(size);
                bodyScrolledComposite.setMinHeight(size.y);
            }
        });

        bodyScrolledComposite.setContent(scrolledContents);

        Dialog.applyDialogFont(body);
        // we've changed it so it needs to know
        body.layout(true);
    }

    private void initializeCursors() {
        if (handCursor == null) {
            handCursor = new Cursor(getShell().getDisplay(), SWT.CURSOR_HAND);
            disposables.add(handCursor);
        }
    }

    private void initializeImages() {
        if (infoImage == null) {
            infoImage = SVNTeamUIPlugin.instance().getImageDescriptor("icons/discovery/message_info.gif")
                    .createImage();
            disposables.add(infoImage);
        }
    }

    private void initializeColors() {
        if (colorWhite == null) {
            ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
            if (!colorRegistry.hasValueFor(COLOR_WHITE)) {
                colorRegistry.put(COLOR_WHITE, new RGB(255, 255, 255));
            }
            colorWhite = colorRegistry.get(COLOR_WHITE);
        }
        if (colorDisabled == null) {
            ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
            if (!colorRegistry.hasValueFor(COLOR_DARK_GRAY)) {
                colorRegistry.put(COLOR_DARK_GRAY, new RGB(0x69, 0x69, 0x69));
            }
            colorDisabled = colorRegistry.get(COLOR_DARK_GRAY);
        }

        if (colorCategoryGradientStart == null) {
            ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
            if (!colorRegistry.hasValueFor(COLOR_CATEGORY_GRADIENT_START)) {
                colorRegistry.put(COLOR_CATEGORY_GRADIENT_START, new RGB(240, 240, 240));
            }
            colorCategoryGradientStart = colorRegistry.get(COLOR_CATEGORY_GRADIENT_START);
        }

        if (colorCategoryGradientEnd == null) {
            ColorRegistry colorRegistry = JFaceResources.getColorRegistry();
            if (!colorRegistry.hasValueFor(COLOR_CATEGORY_GRADIENT_END)) {
                colorRegistry.put(COLOR_CATEGORY_GRADIENT_END, new RGB(220, 220, 220));
            }
            colorCategoryGradientEnd = colorRegistry.get(COLOR_CATEGORY_GRADIENT_END);
        }
    }

    private void initializeFonts() {
        // create a level-2 heading font
        if (h2Font == null) {
            Font baseFont = JFaceResources.getDialogFont();
            FontData[] fontData = baseFont.getFontData();
            for (FontData data : fontData) {
                data.setStyle(data.getStyle() | SWT.BOLD);
                data.height = data.height * 1.25f;
            }
            h2Font = new Font(Display.getCurrent(), fontData);
            disposables.add(h2Font);
        }
        // create a level-1 heading font
        if (h1Font == null) {
            Font baseFont = JFaceResources.getDialogFont();
            FontData[] fontData = baseFont.getFontData();
            for (FontData data : fontData) {
                data.setStyle(data.getStyle() | SWT.BOLD);
                data.height = data.height * 1.35f;
            }
            h1Font = new Font(Display.getCurrent(), fontData);
            disposables.add(h1Font);
        }
    }

    private class ConnectorDescriptorItemUi implements PropertyChangeListener, Runnable {
        private final DiscoveryConnector connector;

        private final Button checkbox;

        private final Label iconLabel;

        private final Label nameLabel;

        private ToolItem infoButton;

        private final Label providerLabel;

        private final Label description;

        private final Composite checkboxContainer;

        private final Composite connectorContainer;

        private final Display display;

        private Image iconImage;

        private Image warningIconImage;

        public ConnectorDescriptorItemUi(DiscoveryConnector connector, Composite categoryChildrenContainer,
                Color background) {
            display = categoryChildrenContainer.getDisplay();
            this.connector = connector;
            connector.addPropertyChangeListener(this);

            connectorContainer = new Composite(categoryChildrenContainer, SWT.NULL);

            configureLook(connectorContainer, background);
            GridDataFactory.fillDefaults().grab(true, false).applyTo(connectorContainer);
            GridLayout layout = new GridLayout(4, false);
            layout.marginLeft = 7;
            layout.marginTop = 2;
            layout.marginBottom = 2;
            connectorContainer.setLayout(layout);

            checkboxContainer = new Composite(connectorContainer, SWT.NULL);
            configureLook(checkboxContainer, background);
            GridDataFactory.swtDefaults().align(SWT.CENTER, SWT.BEGINNING).span(1, 2).applyTo(checkboxContainer);
            GridLayoutFactory.fillDefaults().spacing(1, 1).numColumns(2).applyTo(checkboxContainer);

            checkbox = new Button(checkboxContainer, SWT.CHECK);
            checkbox.setText(" "); //$NON-NLS-1$
            // help UI tests
            checkbox.setData("connectorId", connector.getInstallableUnits()); //$NON-NLS-1$
            configureLook(checkbox, background);
            checkbox.setSelection(installableConnectors.contains(connector));
            checkbox.addFocusListener(new FocusAdapter() {
                @Override
                public void focusGained(FocusEvent e) {
                    bodyScrolledComposite.showControl(connectorContainer);
                }
            });

            GridDataFactory.swtDefaults().align(SWT.CENTER, SWT.CENTER).applyTo(checkbox);

            iconLabel = new Label(checkboxContainer, SWT.NULL);
            configureLook(iconLabel, background);
            GridDataFactory.swtDefaults().align(SWT.CENTER, SWT.CENTER).applyTo(iconLabel);

            if (connector.getIcon() != null) {
                iconImage = computeIconImage(connector.getSource(), connector.getIcon(), 32, false);
                if (iconImage != null) {
                    iconLabel.setImage(iconImage);
                }
            }

            nameLabel = new Label(connectorContainer, SWT.NULL);
            configureLook(nameLabel, background);
            GridDataFactory.fillDefaults().grab(true, false).applyTo(nameLabel);
            nameLabel.setFont(h2Font);
            nameLabel.setText(connector.getName());

            providerLabel = new Label(connectorContainer, SWT.NULL);
            configureLook(providerLabel, background);
            GridDataFactory.fillDefaults().align(SWT.END, SWT.CENTER).applyTo(providerLabel);
            providerLabel.setText(
                    SVNUIMessages.format(SVNUIMessages.ConnectorDiscoveryWizardMainPage_provider_and_license,
                            new Object[] { connector.getProvider(), connector.getLicense() }));

            if (hasTooltip(connector)) {
                ToolBar toolBar = new ToolBar(connectorContainer, SWT.FLAT);
                toolBar.setBackground(background);

                infoButton = new ToolItem(toolBar, SWT.PUSH);
                infoButton.setImage(infoImage);
                infoButton.setToolTipText(SVNUIMessages.ConnectorDiscoveryWizardMainPage_tooltip_showOverview);
                hookTooltip(toolBar, infoButton, connectorContainer, nameLabel, connector.getSource(),
                        connector.getOverview());
                GridDataFactory.fillDefaults().align(SWT.END, SWT.CENTER).applyTo(toolBar);
            } else {
                new Label(connectorContainer, SWT.NULL).setText(" "); //$NON-NLS-1$
            }

            description = new Label(connectorContainer, SWT.NULL | SWT.WRAP);
            configureLook(description, background);

            GridDataFactory.fillDefaults().grab(true, false).span(3, 1).hint(100, SWT.DEFAULT).applyTo(description);
            String descriptionText = connector.getDescription();
            int maxDescriptionLength = 162;
            if (descriptionText.length() > maxDescriptionLength) {
                descriptionText = descriptionText.substring(0, maxDescriptionLength);
            }
            description.setText(descriptionText.replaceAll("(\\r\\n)|\\n|\\r", " ")); //$NON-NLS-1$ //$NON-NLS-2$

            // always disabled color to make it less prominent
            providerLabel.setForeground(colorDisabled);

            checkbox.addSelectionListener(new SelectionListener() {
                public void widgetDefaultSelected(SelectionEvent e) {
                    widgetSelected(e);
                }

                public void widgetSelected(SelectionEvent e) {
                    boolean selected = checkbox.getSelection();
                    maybeModifySelection(selected);
                }
            });
            MouseListener connectorItemMouseListener = new MouseAdapter() {
                @Override
                public void mouseUp(MouseEvent e) {
                    boolean selected = !checkbox.getSelection();
                    if (maybeModifySelection(selected)) {
                        checkbox.setSelection(selected);
                    }
                }
            };
            checkboxContainer.addMouseListener(connectorItemMouseListener);
            connectorContainer.addMouseListener(connectorItemMouseListener);
            iconLabel.addMouseListener(connectorItemMouseListener);
            nameLabel.addMouseListener(connectorItemMouseListener);
            providerLabel.addMouseListener(connectorItemMouseListener);
            description.addMouseListener(connectorItemMouseListener);
        }

        protected boolean maybeModifySelection(boolean selected) {
            if (selected) {
                if (connector.getAvailable() == null) {
                    return false;
                }
                if (!connector.getAvailable()) {
                    MessageDialog.openWarning(getShell(),
                            SVNUIMessages.ConnectorDiscoveryWizardMainPage_warningTitleConnectorUnavailable,
                            SVNUIMessages.format(
                                    SVNUIMessages.ConnectorDiscoveryWizardMainPage_warningMessageConnectorUnavailable,
                                    connector.getName()));
                    return false;
                }
            }
            ConnectorDiscoveryWizardMainPage.this.modifySelection(connector, selected);
            return true;
        }

        public void updateAvailability() {
            boolean enabled = connector.getAvailable() != null && connector.getAvailable();

            checkbox.setEnabled(enabled);
            nameLabel.setEnabled(enabled);
            providerLabel.setEnabled(enabled);
            description.setEnabled(enabled);
            Color foreground;
            if (enabled) {
                foreground = connectorContainer.getForeground();
            } else {
                foreground = colorDisabled;
            }
            nameLabel.setForeground(foreground);
            description.setForeground(foreground);

            if (iconImage != null) {
                boolean unavailable = !enabled && connector.getAvailable() != null;
                if (unavailable) {
                    if (warningIconImage == null) {
                        warningIconImage = new DecorationOverlayIcon(iconImage, SVNTeamUIPlugin.instance()
                                .getImageDescriptor("icons/discovery/message_warning.gif"), IDecoration.TOP_LEFT)
                                        .createImage();
                        disposables.add(warningIconImage);
                    }
                    iconLabel.setImage(warningIconImage);
                } else if (warningIconImage != null) {
                    iconLabel.setImage(iconImage);
                }
            }
        }

        public void propertyChange(PropertyChangeEvent evt) {
            display.asyncExec(this);
        }

        public void run() {
            if (!connectorContainer.isDisposed()) {
                updateAvailability();
            }
        }
    }

    private void createDiscoveryContents(Composite container) {

        Color background = container.getBackground();

        if (discovery == null || isEmpty(discovery)) {
            GridLayoutFactory.fillDefaults().margins(5, 5).applyTo(container);

            boolean atLeastOneKindFiltered = false;
            for (ConnectorDescriptorKind kind : ConnectorDescriptorKind.values()) {
                if (!getWizard().isVisible(kind)) {
                    atLeastOneKindFiltered = true;
                    break;
                }
            }
            Control helpTextControl;
            if (filterPattern != null) {
                Link link = new Link(container, SWT.WRAP);

                link.setFont(container.getFont());
                link.setText(SVNUIMessages.ConnectorDiscoveryWizardMainPage_noMatchingItems_withFilterText);
                link.addListener(SWT.Selection, new Listener() {
                    public void handleEvent(Event event) {
                        clearFilterText();
                        filterText.setFocus();
                    }
                });
                helpTextControl = link;
            } else {
                Label helpText = new Label(container, SWT.WRAP);
                helpText.setFont(container.getFont());
                if (atLeastOneKindFiltered) {
                    helpText.setText(SVNUIMessages.ConnectorDiscoveryWizardMainPage_noMatchingItems_filteredType);
                } else {
                    helpText.setText(SVNUIMessages.ConnectorDiscoveryWizardMainPage_noMatchingItems_noFilter);
                }
                helpTextControl = helpText;
            }
            configureLook(helpTextControl, background);
            GridDataFactory.fillDefaults().grab(true, false).hint(100, SWT.DEFAULT).applyTo(helpTextControl);
        } else {
            GridLayoutFactory.fillDefaults().numColumns(2).spacing(0, 0).applyTo(container);

            List<DiscoveryCategory> categories = new ArrayList<DiscoveryCategory>(discovery.getCategories());
            Collections.sort(categories, new DiscoveryCategoryComparator());

            Composite categoryChildrenContainer = null;
            for (DiscoveryCategory category : categories) {
                if (isEmpty(category)) {
                    // don't add empty categories
                    continue;
                }
                { // category header
                    final GradientCanvas categoryHeaderContainer = new GradientCanvas(container, SWT.NONE);
                    categoryHeaderContainer.setSeparatorVisible(true);
                    categoryHeaderContainer.setSeparatorAlignment(SWT.TOP);
                    categoryHeaderContainer.setBackgroundGradient(
                            new Color[] { colorCategoryGradientStart, colorCategoryGradientEnd }, new int[] { 100 },
                            true);
                    categoryHeaderContainer.putColor(IFormColors.H_BOTTOM_KEYLINE1, colorCategoryGradientStart);
                    categoryHeaderContainer.putColor(IFormColors.H_BOTTOM_KEYLINE2, colorCategoryGradientEnd);

                    GridDataFactory.fillDefaults().span(2, 1).applyTo(categoryHeaderContainer);
                    GridLayoutFactory.fillDefaults().numColumns(3).margins(5, 5).equalWidth(false)
                            .applyTo(categoryHeaderContainer);

                    Label iconLabel = new Label(categoryHeaderContainer, SWT.NULL);
                    if (category.getIcon() != null) {
                        Image image = computeIconImage(category.getSource(), category.getIcon(), 48, true);
                        if (image != null) {
                            iconLabel.setImage(image);
                        }
                    }
                    iconLabel.setBackground(null);
                    GridDataFactory.swtDefaults().align(SWT.CENTER, SWT.BEGINNING).span(1, 2).applyTo(iconLabel);

                    Label nameLabel = new Label(categoryHeaderContainer, SWT.NULL);
                    nameLabel.setFont(h1Font);
                    nameLabel.setText(category.getName());
                    nameLabel.setBackground(null);

                    GridDataFactory.fillDefaults().grab(true, false).applyTo(nameLabel);
                    if (hasTooltip(category)) {
                        ToolBar toolBar = new ToolBar(categoryHeaderContainer, SWT.FLAT);
                        toolBar.setBackground(null);
                        ToolItem infoButton = new ToolItem(toolBar, SWT.PUSH);
                        infoButton.setImage(infoImage);
                        infoButton.setToolTipText(
                                SVNUIMessages.ConnectorDiscoveryWizardMainPage_tooltip_showOverview);
                        hookTooltip(toolBar, infoButton, categoryHeaderContainer, nameLabel, category.getSource(),
                                category.getOverview());
                        GridDataFactory.fillDefaults().align(SWT.END, SWT.CENTER).applyTo(toolBar);
                    } else {
                        new Label(categoryHeaderContainer, SWT.NULL).setText(" "); //$NON-NLS-1$
                    }
                    Label description = new Label(categoryHeaderContainer, SWT.WRAP);
                    GridDataFactory.fillDefaults().grab(true, false).span(2, 1).hint(100, SWT.DEFAULT)
                            .applyTo(description);
                    description.setBackground(null);
                    description.setText(category.getDescription());
                }

                categoryChildrenContainer = new Composite(container, SWT.NULL);
                configureLook(categoryChildrenContainer, background);
                GridDataFactory.fillDefaults().span(2, 1).grab(true, false).applyTo(categoryChildrenContainer);
                GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(categoryChildrenContainer);

                int numChildren = 0;
                List<DiscoveryConnector> connectors = new ArrayList<DiscoveryConnector>(category.getConnectors());
                Collections.sort(connectors, new DiscoveryConnectorComparator(category));
                for (final DiscoveryConnector connector : connectors) {
                    if (isFiltered(connector)) {
                        continue;
                    }

                    if (++numChildren > 1) {
                        // a separator between connector descriptors
                        Composite border = new Composite(categoryChildrenContainer, SWT.NULL);
                        GridDataFactory.fillDefaults().grab(true, false).hint(SWT.DEFAULT, 1).applyTo(border);
                        GridLayoutFactory.fillDefaults().applyTo(border);
                        border.addPaintListener(new ConnectorBorderPaintListener());
                    }

                    ConnectorDescriptorItemUi itemUi = new ConnectorDescriptorItemUi(connector,
                            categoryChildrenContainer, background);
                    itemUi.updateAvailability();
                }
            }
            // last one gets a border
            Composite border = new Composite(categoryChildrenContainer, SWT.NULL);
            GridDataFactory.fillDefaults().grab(true, false).hint(SWT.DEFAULT, 1).applyTo(border);
            GridLayoutFactory.fillDefaults().applyTo(border);
            border.addPaintListener(new ConnectorBorderPaintListener());
        }
        container.layout(true);
        container.redraw();
    }

    private void configureLook(Control control, Color background) {
        control.setBackground(background);
    }

    private void hookTooltip(final Control tooltipControl, final ToolItem tipActivator, final Control exitControl,
            final Control titleControl, AbstractDiscoverySource source, Overview overview) {
        final OverviewToolTip toolTip = new OverviewToolTip(tooltipControl, source, overview);
        Listener listener = new Listener() {
            public void handleEvent(Event event) {
                switch (event.type) {
                case SWT.Dispose:
                case SWT.MouseWheel:
                    toolTip.hide();
                    break;
                }

            }
        };
        tipActivator.addListener(SWT.Dispose, listener);
        tipActivator.addListener(SWT.MouseWheel, listener);
        tipActivator.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                Point titleAbsLocation = titleControl.getParent().toDisplay(titleControl.getLocation());
                Point containerAbsLocation = tooltipControl.getParent().toDisplay(tooltipControl.getLocation());
                Rectangle bounds = titleControl.getBounds();
                int relativeX = titleAbsLocation.x - containerAbsLocation.x;
                int relativeY = titleAbsLocation.y - containerAbsLocation.y;

                relativeY += bounds.height + 3;
                toolTip.show(new Point(relativeX, relativeY));
            }
        });
        Listener exitListener = new Listener() {
            public void handleEvent(Event event) {
                switch (event.type) {
                case SWT.MouseWheel:
                    toolTip.hide();
                    break;
                case SWT.MouseExit:
                    /*
                     * Check if the mouse exit happened because we move over the
                     * tooltip
                     */
                    Rectangle containerBounds = exitControl.getBounds();
                    Point displayLocation = exitControl.getParent().toDisplay(containerBounds.x, containerBounds.y);
                    containerBounds.x = displayLocation.x;
                    containerBounds.y = displayLocation.y;
                    if (containerBounds.contains(Display.getCurrent().getCursorLocation())) {
                        break;
                    }
                    toolTip.hide();
                    break;
                }
            }
        };
        hookRecursively(exitControl, exitListener);
    }

    private void hookRecursively(Control control, Listener listener) {
        control.addListener(SWT.Dispose, listener);
        control.addListener(SWT.MouseHover, listener);
        control.addListener(SWT.MouseMove, listener);
        control.addListener(SWT.MouseExit, listener);
        control.addListener(SWT.MouseDown, listener);
        control.addListener(SWT.MouseWheel, listener);
        if (control instanceof Composite) {
            for (Control child : ((Composite) control).getChildren()) {
                hookRecursively(child, listener);
            }
        }
    }

    private boolean hasTooltip(final DiscoveryConnector connector) {
        return connector.getOverview() != null && connector.getOverview().getSummary() != null
                && connector.getOverview().getSummary().length() > 0;
    }

    private boolean hasTooltip(final DiscoveryCategory category) {
        return category.getOverview() != null && category.getOverview().getSummary() != null
                && category.getOverview().getSummary().length() > 0;
    }

    /**
     * indicate if there is nothing to display in the UI, given the current state of
     * {@link ConnectorDiscoveryWizard#isVisible(ConnectorDescriptorKind) filters}.
     */
    private boolean isEmpty(ConnectorDiscovery discovery) {
        for (DiscoveryCategory category : discovery.getCategories()) {
            if (!isEmpty(category)) {
                return false;
            }
        }
        return true;
    }

    /**
     * indicate if the category has nothing to display in the UI, given the current state of
     * {@link ConnectorDiscoveryWizard#isVisible(ConnectorDescriptorKind) filters}.
     */
    private boolean isEmpty(DiscoveryCategory category) {
        if (category.getConnectors().isEmpty()) {
            return true;
        }
        for (ConnectorDescriptor descriptor : category.getConnectors()) {
            if (!isFiltered(descriptor)) {
                return false;
            }
        }
        return true;
    }

    private boolean isFiltered(ConnectorDescriptor descriptor) {
        boolean kindFiltered = true;
        for (ConnectorDescriptorKind kind : descriptor.getKind()) {
            if (getWizard().isVisible(kind)) {
                kindFiltered = false;
                break;
            }
        }
        if (kindFiltered) {
            return true;
        }
        if (installedFeatures != null && installedFeatures.contains(descriptor.getInstallableUnits())) {
            // always filter installed features per bug 275777
            return true;
        }
        if (filterPattern != null) {
            if (!(filterMatches(descriptor.getName()) || filterMatches(descriptor.getDescription())
                    || filterMatches(descriptor.getProvider()) || filterMatches(descriptor.getLicense()))) {
                return true;
            }
        }
        return false;
    }

    private boolean filterMatches(String text) {
        return text != null && filterPattern.matcher(text).find();
    }

    private Image computeIconImage(AbstractDiscoverySource discoverySource, Icon icon, int dimension,
            boolean fallback) {
        String imagePath;
        switch (dimension) {
        case 64:
            imagePath = icon.getImage64();
            if (imagePath != null || !fallback) {
                break;
            }
        case 48:
            imagePath = icon.getImage48();
            if (imagePath != null || !fallback) {
                break;
            }
        case 32:
            imagePath = icon.getImage32();
            break;
        default:
            throw new IllegalArgumentException();
        }
        if (imagePath != null && imagePath.length() > 0) {
            URL resource = discoverySource.getResource(imagePath);
            if (resource != null) {
                ImageDescriptor descriptor = ImageDescriptor.createFromURL(resource);
                Image image = descriptor.createImage();
                if (image != null) {
                    disposables.add(image);
                    return image;
                }
            }
        }
        return null;
    }

    private void maybeUpdateDiscovery() {
        if (!getControl().isDisposed() && isCurrentPage() && discovery == null) {
            final Dictionary<Object, Object> environment = getWizard().getEnvironment();
            boolean wasCancelled = false;
            try {
                getContainer().run(true, true, new IRunnableWithProgress() {
                    public void run(IProgressMonitor monitor)
                            throws InvocationTargetException, InterruptedException {

                        if (ConnectorDiscoveryWizardMainPage.this.installedFeatures == null) {
                            Set<String> installedFeatures = new HashSet<String>();
                            IBundleGroupProvider[] bundleGroupProviders = Platform.getBundleGroupProviders();
                            for (IBundleGroupProvider provider : bundleGroupProviders) {
                                if (monitor.isCanceled()) {
                                    throw new InterruptedException();
                                }
                                IBundleGroup[] bundleGroups = provider.getBundleGroups();
                                for (IBundleGroup group : bundleGroups) {
                                    installedFeatures.add(group.getIdentifier());
                                }
                            }
                            ConnectorDiscoveryWizardMainPage.this.installedFeatures = installedFeatures;
                        }

                        ConnectorDiscovery connectorDiscovery = new ConnectorDiscovery();
                        connectorDiscovery.getDiscoveryStrategies().add(new BundleDiscoveryStrategy());

                        //retrieve discovery url from properties file and call remote discovery strategy
                        URL discoveryFileUrl = FileLocator.find(SVNTeamUIPlugin.instance().getBundle(),
                                new Path(ConnectorDiscoveryWizardMainPage.DISCOVERY_PROPERTIES_FILE), null);
                        if (discoveryFileUrl != null) {
                            InputStream in = null;
                            try {
                                Properties props = new Properties();
                                in = discoveryFileUrl.openStream();
                                props.load(in);
                                String discoveryUrl = props
                                        .getProperty(ConnectorDiscoveryWizardMainPage.URL_DISCOVERY_PROPERTY);
                                RemoteBundleDiscoveryStrategy remoteDiscoveryStrategy = new RemoteBundleDiscoveryStrategy();
                                remoteDiscoveryStrategy.setDiscoveryUrl(discoveryUrl);
                                connectorDiscovery.getDiscoveryStrategies().add(remoteDiscoveryStrategy);
                            } catch (IOException ie) {
                                LoggedOperation.reportError(this.getClass().getName(), ie);
                            } finally {
                                if (in != null) {
                                    try {
                                        in.close();
                                    } catch (IOException e) {
                                        //ignore
                                    }
                                }
                            }
                        }

                        connectorDiscovery.setEnvironment(environment);
                        connectorDiscovery.setVerifyUpdateSiteAvailability(false);
                        try {
                            connectorDiscovery.performDiscovery(monitor);
                        } catch (CoreException e) {
                            throw new InvocationTargetException(e);
                        } finally {
                            ConnectorDiscoveryWizardMainPage.this.discovery = connectorDiscovery;
                        }
                        if (monitor.isCanceled()) {
                            throw new InterruptedException();
                        }
                    }
                });
            } catch (InvocationTargetException e) {
                IStatus status = computeStatus(e,
                        SVNUIMessages.ConnectorDiscoveryWizardMainPage_unexpectedException);
                DiscoveryUiUtil.logAndDisplayStatus(getShell(),
                        SVNUIMessages.ConnectorDiscoveryWizardMainPage_errorTitle, status);
            } catch (InterruptedException e) {
                // cancelled by user so nothing to do here.
                wasCancelled = true;
            }
            if (discovery != null) {
                discoveryUpdated(wasCancelled);
                if (!discovery.getConnectors().isEmpty()) {
                    try {
                        getContainer().run(true, true, new IRunnableWithProgress() {
                            public void run(IProgressMonitor monitor)
                                    throws InvocationTargetException, InterruptedException {
                                discovery.verifySiteAvailability(monitor);
                            }
                        });
                    } catch (InvocationTargetException e) {
                        IStatus status = computeStatus(e,
                                SVNUIMessages.ConnectorDiscoveryWizardMainPage_unexpectedException);
                        DiscoveryUiUtil.logAndDisplayStatus(getShell(),
                                SVNUIMessages.ConnectorDiscoveryWizardMainPage_errorTitle, status);
                    } catch (InterruptedException e) {
                        // cancelled by user so nothing to do here.
                        wasCancelled = true;
                    }
                }
                // createBodyContents() shouldn't be necessary but for some reason checkboxes don't 
                // regain their enabled state
                createBodyContents();
            }
            // help UI tests
            body.setData("discoveryComplete", "true"); //$NON-NLS-1$//$NON-NLS-2$
        }
    }

    private IStatus computeStatus(InvocationTargetException e, String message) {
        Throwable cause = e.getCause();
        IStatus statusCause;
        if (cause instanceof CoreException) {
            statusCause = ((CoreException) cause).getStatus();
        } else {
            statusCause = new Status(IStatus.ERROR, SVNTeamPlugin.NATURE_ID, cause.getMessage(), cause);
        }
        if (statusCause.getMessage() != null) {
            message = SVNUIMessages.format(SVNUIMessages.ConnectorDiscoveryWizardMainPage_message_with_cause,
                    new Object[] { message, statusCause.getMessage() });
        }
        IStatus status = new MultiStatus(SVNTeamPlugin.NATURE_ID, 0, new IStatus[] { statusCause }, message, cause);
        return status;
    }

    @Override
    public void setVisible(boolean visible) {
        super.setVisible(visible);
        if (visible && discovery == null) {
            Display.getCurrent().asyncExec(new Runnable() {
                public void run() {
                    maybeUpdateDiscovery();
                }
            });
        }
    }

    private void discoveryUpdated(boolean wasCancelled) {
        createBodyContents();
        if (discovery != null && !wasCancelled) {
            int categoryWithConnectorCount = 0;
            for (DiscoveryCategory category : discovery.getCategories()) {
                categoryWithConnectorCount += category.getConnectors().size();
            }
            if (categoryWithConnectorCount == 0) {
                // nothing was discovered: notify the user
                MessageDialog.openWarning(getShell(),
                        SVNUIMessages.ConnectorDiscoveryWizardMainPage_noConnectorsFound,
                        SVNUIMessages.ConnectorDiscoveryWizardMainPage_noConnectorsFound_description);
            }
        }
    }

    public List<ConnectorDescriptor> getInstallableConnectors() {
        return installableConnectors;
    }

    private void modifySelection(final DiscoveryConnector connector, boolean selected) {
        connector.setSelected(selected);

        if (selected) {
            installableConnectors.add(connector);
        } else {
            installableConnectors.remove(connector);
        }
        setPageComplete(!installableConnectors.isEmpty());
    }

    public class ConnectorBorderPaintListener implements PaintListener {
        public void paintControl(PaintEvent e) {
            Composite composite = (Composite) e.widget;
            Rectangle bounds = composite.getBounds();
            GC gc = e.gc;
            gc.setLineStyle(SWT.LINE_DOT);
            gc.drawLine(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y);
        }
    }

}