/******************************************************************************* * Copyright (C) 2017 Thomas Wolf <thomas.wolf@paranor.ch> * * 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 *******************************************************************************/ package org.eclipse.egit.ui.internal.dialogs; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Set; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.internal.ActionUtils; import org.eclipse.egit.ui.internal.UIIcons; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.egit.ui.internal.components.TitleAndImageDialog; import org.eclipse.egit.ui.internal.gerrit.GerritDialogSettings; import org.eclipse.egit.ui.internal.push.RefSpecDialog; import org.eclipse.egit.ui.internal.push.RefSpecWizard; import org.eclipse.egit.ui.internal.repository.SelectUriWizard; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.viewers.ArrayContentProvider; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.window.Window; import org.eclipse.jface.wizard.WizardDialog; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.URIish; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; 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.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.ISharedImages; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.actions.ActionFactory; /** * Common super class for fetch and push remote configuration dialogs. The * dialog has, besides the usual OK and CANCEL buttons, three more buttons: * {@link #DRY_RUN} to do a dry-run of the operation, {@link #SAVE_ONLY} to save * the {@link RemoteConfig} without performing the operation, and * {@link #REVERT} to undo all configuration changes and re-load the * {@link RemoteConfig} from the {@link Repository}. The OK button saves the * {@link RemoteConfig} <em>and</em> performs the operation. * <p> * Provides UI for the common URI and changing it, and also for adding, * changing, and deleting {@link RefSpec}s. Can be extended via * {@link #createAdditionalUriArea(Composite)} to provide more UI components. * </p> */ public abstract class AbstractConfigureRemoteDialog extends TitleAndImageDialog { /** Button ID for the dry-run button. */ protected static final int DRY_RUN = 98; /** Button ID for the "save only" button. */ protected static final int SAVE_ONLY = 97; /** Button ID for the revert button. */ protected static final int REVERT = 96; private final boolean isPush; private final Repository repository; private RemoteConfig config; private final boolean showBranchInfo; // UI components /** A {@link Text} field for the URI. */ protected Text commonUriText; /** A {@link TableViewer} showing {@link RefSpec}s. */ protected TableViewer specViewer; /** An {@link IAction} that allows changing the common URI. */ protected IAction changeCommonUriAction; /** An {@link IAction} that allows deleting the common URI. */ protected IAction deleteCommonUriAction; /** * An {@link IAction} to add a new {@link RefSpec} to the * {@link #specViewer}. */ protected IAction addRefSpecAction; /** * An {@link IAction} to change an existing {@link RefSpec} in the * {@link #specViewer}. */ protected IAction changeRefSpecAction; /** * An {@link IAction} opening an advanced {@link RefSpec} dialog. */ protected IAction addRefSpecAdvancedAction; /** * Create a new {@link AbstractConfigureRemoteDialog}. * * @param parent * SWT {@link Shell} to parent the dialog on * @param repository * the remote belongs to * @param config * of the remote to be configured * @param showBranchInfo * whether to show additional branch information * @param isPush * whether this dialog is for configuring push */ protected AbstractConfigureRemoteDialog(Shell parent, Repository repository, RemoteConfig config, boolean showBranchInfo, boolean isPush) { super(parent, isPush ? UIIcons.WIZBAN_PUSH : UIIcons.WIZBAN_FETCH); setHelpAvailable(false); setShellStyle(getShellStyle() | SWT.SHELL_TRIM); this.repository = repository; this.config = config; this.showBranchInfo = showBranchInfo; this.isPush = isPush; } /** * Retrieves the {@link Repository} for which the remote is to be * configured. * * @return the {@link Repository} */ protected Repository getRepository() { return repository; } /** * Retrieves the {@link RemoteConfig} as configured currently. * * @return the {@link RemoteConfig} */ protected RemoteConfig getConfig() { return config; } /** * Performs a dry-run of the operation. * * @param monitor * for progress reporting and cancellation, never {@code null} */ protected abstract void dryRun(IProgressMonitor monitor); /** * Performs the operation for real. Invoked in the UI thread; lengthy * operations should be performed in a background job. */ protected abstract void performOperation(); /** * Creates the OK button. * * @param parent * {@link Composite} containing the buttons */ protected abstract void createOkButton(Composite parent); /** * Asks the user for a new {@link RefSpec} to be added to the current * {@link RemoteConfig}. * * @return the new {@link RefSpec}, or {@code null} if none. */ protected abstract RefSpec getNewRefSpec(); @Override protected Control createDialogArea(Composite parent) { final Composite main = new Composite(parent, SWT.NONE); GridLayoutFactory.fillDefaults().applyTo(main); GridDataFactory.fillDefaults().grab(true, true) .minSize(SWT.DEFAULT, SWT.DEFAULT).applyTo(main); if (showBranchInfo) { Composite branchArea = new Composite(main, SWT.NONE); GridLayoutFactory.swtDefaults().numColumns(2).equalWidth(false) .applyTo(branchArea); GridDataFactory.fillDefaults().grab(true, false) .applyTo(branchArea); Label branchLabel = new Label(branchArea, SWT.NONE); branchLabel.setText(UIText.AbstractConfigureRemoteDialog_BranchLabel); String branch; try { branch = getRepository().getBranch(); } catch (IOException e) { branch = null; } if (branch == null || ObjectId.isId(branch)) { branch = UIText.AbstractConfigureRemoteDialog_DetachedHeadMessage; } Text branchText = new Text(branchArea, SWT.BORDER | SWT.READ_ONLY); GridDataFactory.fillDefaults().grab(true, false) .applyTo(branchText); branchText.setText(branch); addDefaultOriginWarning(main); } final Composite sameUriDetails = new Composite(main, SWT.NONE); GridLayoutFactory.fillDefaults().numColumns(4).equalWidth(false) .applyTo(sameUriDetails); GridDataFactory.fillDefaults().grab(true, false) .applyTo(sameUriDetails); Label commonUriLabel = new Label(sameUriDetails, SWT.NONE); commonUriLabel.setText(UIText.AbstractConfigureRemoteDialog_UriLabel); commonUriText = new Text(sameUriDetails, SWT.BORDER | SWT.READ_ONLY); GridDataFactory.fillDefaults().grab(true, false).applyTo(commonUriText); changeCommonUriAction = new Action( UIText.AbstractConfigureRemoteDialog_ChangeUriLabel) { @Override public void run() { SelectUriWizard wiz; if (!commonUriText.getText().isEmpty()) { wiz = new SelectUriWizard(true, commonUriText.getText()); } else { wiz = new SelectUriWizard(true); } if (new WizardDialog(getShell(), wiz).open() == Window.OK) { if (!commonUriText.getText().isEmpty()) { try { getConfig().removeURI( new URIish(commonUriText.getText())); } catch (URISyntaxException ex) { Activator.handleError(ex.getMessage(), ex, true); } } getConfig().addURI(wiz.getUri()); updateControls(); } } }; deleteCommonUriAction = new Action( UIText.AbstractConfigureRemoteDialog_DeleteUriLabel) { @Override public void run() { getConfig().removeURI(getConfig().getURIs().get(0)); updateControls(); } }; createActionButton(sameUriDetails, SWT.PUSH, changeCommonUriAction); createActionButton(sameUriDetails, SWT.PUSH, deleteCommonUriAction) .setEnabled(false); commonUriText.addModifyListener(event -> deleteCommonUriAction .setEnabled(!commonUriText.getText().isEmpty())); createAdditionalUriArea(main); final Group refSpecGroup = new Group(main, SWT.SHADOW_ETCHED_IN); GridDataFactory.fillDefaults().grab(true, true) .minSize(SWT.DEFAULT, SWT.DEFAULT).applyTo(refSpecGroup); refSpecGroup.setText(UIText.AbstractConfigureRemoteDialog_RefMappingGroup); GridLayoutFactory.fillDefaults().numColumns(2).applyTo(refSpecGroup); specViewer = new TableViewer(refSpecGroup, SWT.BORDER | SWT.MULTI); specViewer.setContentProvider(ArrayContentProvider.getInstance()); GridDataFactory.fillDefaults().hint(SWT.DEFAULT, 150) .minSize(SWT.DEFAULT, 30).grab(true, true) .applyTo(specViewer.getTable()); addRefSpecAction = new Action( UIText.AbstractConfigureRemoteDialog_AddRefSpecLabel) { @Override public void run() { doAddRefSpec(); } }; changeRefSpecAction = new Action( UIText.AbstractConfigureRemoteDialog_ChangeRefSpecLabel) { @Override public void run() { doChangeRefSpec(); } }; addRefSpecAdvancedAction = new Action( UIText.AbstractConfigureRemoteDialog_EditAdvancedLabel) { @Override public void run() { doAdvanced(); } }; IAction deleteRefSpecAction = ActionUtils.createGlobalAction( ActionFactory.DELETE, () -> doDeleteRefSpecs()); IAction copyRefSpecAction = ActionUtils .createGlobalAction(ActionFactory.COPY, () -> doCopy()); IAction pasteRefSpecAction = ActionUtils .createGlobalAction(ActionFactory.PASTE, () -> doPaste()); IAction selectAllRefSpecsAction = ActionUtils.createGlobalAction( ActionFactory.SELECT_ALL, () -> { specViewer.getTable().selectAll(); // selectAll doesn't fire a "selection changed" event specViewer.setSelection(specViewer.getSelection()); }); Composite buttonArea = new Composite(refSpecGroup, SWT.NONE); GridLayoutFactory.fillDefaults().applyTo(buttonArea); GridDataFactory.fillDefaults().grab(false, true) .minSize(SWT.DEFAULT, SWT.DEFAULT).applyTo(buttonArea); createActionButton(buttonArea, SWT.PUSH, addRefSpecAction); createActionButton(buttonArea, SWT.PUSH, changeRefSpecAction); createActionButton(buttonArea, SWT.PUSH, deleteRefSpecAction); createActionButton(buttonArea, SWT.PUSH, copyRefSpecAction); createActionButton(buttonArea, SWT.PUSH, pasteRefSpecAction); createActionButton(buttonArea, SWT.PUSH, addRefSpecAdvancedAction); MenuManager contextMenu = new MenuManager(); contextMenu.setRemoveAllWhenShown(true); contextMenu.addMenuListener(manager -> { specViewer.getTable().setFocus(); if (addRefSpecAction.isEnabled()) { manager.add(addRefSpecAction); } if (changeRefSpecAction.isEnabled()) { manager.add(changeRefSpecAction); } if (deleteRefSpecAction.isEnabled()) { manager.add(deleteRefSpecAction); } manager.add(new Separator()); manager.add(copyRefSpecAction); manager.add(pasteRefSpecAction); manager.add(selectAllRefSpecsAction); }); specViewer.getTable() .setMenu(contextMenu.createContextMenu(specViewer.getTable())); ActionUtils.setGlobalActions(specViewer.getTable(), deleteRefSpecAction, copyRefSpecAction, pasteRefSpecAction, selectAllRefSpecsAction); specViewer.addSelectionChangedListener(new ISelectionChangedListener() { @Override public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection sel = (IStructuredSelection) specViewer .getSelection(); copyRefSpecAction.setEnabled(sel.size() == 1); changeRefSpecAction.setEnabled(sel.size() == 1); deleteRefSpecAction.setEnabled(!sel.isEmpty()); selectAllRefSpecsAction.setEnabled(specViewer.getTable() .getItemCount() > 0 && sel.size() != specViewer.getTable().getItemCount()); } }); // Initial action enablement (no selection in the specViewer): copyRefSpecAction.setEnabled(false); changeRefSpecAction.setEnabled(false); deleteRefSpecAction.setEnabled(false); applyDialogFont(main); return main; } /** * Add a warning about this remote being used by other branches * * @param parent */ private void addDefaultOriginWarning(Composite parent) { List<String> otherBranches = new ArrayList<>(); String currentBranch; try { currentBranch = getRepository().getBranch(); } catch (IOException e) { // just don't show this warning return; } String currentRemote = getConfig().getName(); Config repositoryConfig = getRepository().getConfig(); Set<String> branches = repositoryConfig .getSubsections(ConfigConstants.CONFIG_BRANCH_SECTION); for (String branch : branches) { if (branch.equals(currentBranch)) { continue; } String remote = repositoryConfig.getString( ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_REMOTE); if ((remote == null && currentRemote.equals(Constants.DEFAULT_REMOTE_NAME)) || (remote != null && remote.equals(currentRemote))) { otherBranches.add(branch); } } if (otherBranches.isEmpty()) { return; } Composite warningAboutOrigin = new Composite(parent, SWT.NONE); GridLayoutFactory.fillDefaults().numColumns(2) .applyTo(warningAboutOrigin); Label warningLabel = new Label(warningAboutOrigin, SWT.NONE); warningLabel.setImage(PlatformUI.getWorkbench().getSharedImages() .getImage(ISharedImages.IMG_OBJS_WARN_TSK)); Text warningText = new Text(warningAboutOrigin, SWT.READ_ONLY); warningText.setText(NLS.bind( UIText.AbstractConfigureRemoteDialog_ReusedRemoteWarning, getConfig().getName(), Integer.valueOf(otherBranches.size()))); warningText.setToolTipText(otherBranches.toString()); GridDataFactory.fillDefaults().grab(true, false).applyTo(warningLabel); } private void addRefSpec(RefSpec spec) { if (isPush) { getConfig().addPushRefSpec(spec); } else { getConfig().addFetchRefSpec(spec); } } private void removeRefSpec(RefSpec spec) { if (isPush) { getConfig().removePushRefSpec(spec); } else { getConfig().removeFetchRefSpec(spec); } } /** * Hook method to create an additional area between the URI and the * {@link RefSpec} viewer. This default implementation does nothing. * * @param parent * {@link Composite} the additional area shall use as parent * @return the {@link Control}, or {@code null} if none */ protected Control createAdditionalUriArea(Composite parent) { return null; } /** * Validate and enable/disable controls depending on the current state. */ protected abstract void updateControls(); @Override protected final void createButtonsForButtonBar(Composite parent) { createOkButton(parent); createButton(parent, SAVE_ONLY, UIText.AbstractConfigureRemoteDialog_SaveButton, false); createButton(parent, DRY_RUN, UIText.AbstractConfigureRemoteDialog_DryRunButton, false); createButton(parent, REVERT, UIText.AbstractConfigureRemoteDialog_RevertButton, false); createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); } @Override protected final void buttonPressed(int buttonId) { switch (buttonId) { case DRY_RUN: try { new ProgressMonitorDialog(getShell()).run(true, true, (monitor) -> dryRun(monitor)); } catch (InvocationTargetException e) { Activator.showError(e.getMessage(), e); } catch (InterruptedException e1) { // Ignore cancellation here } return; case REVERT: try { config = new RemoteConfig(repository.getConfig(), config.getName()); updateControls(); } catch (URISyntaxException e) { Activator.handleError(e.getMessage(), e, true); } return; case OK: case SAVE_ONLY: StoredConfig repoConfig = getRepository().getConfig(); boolean saved = false; try { config.update(repoConfig); repoConfig.save(); saved = true; } catch (IOException e) { Activator.handleError(e.getMessage(), e, true); } if (saved) { GerritDialogSettings.updateRemoteConfig(repository, config); } if (buttonId == OK) { performOperation(); } okPressed(); return; default: break; } super.buttonPressed(buttonId); } private void doPaste() { Clipboard clipboard = new Clipboard(getShell().getDisplay()); try { String content = (String) clipboard .getContents(TextTransfer.getInstance()); if (content == null) { MessageDialog.openConfirm(getShell(), UIText.AbstractConfigureRemoteDialog_EmptyClipboardDialogTitle, UIText.AbstractConfigureRemoteDialog_EmptyClipboardDialogMessage); } try { RefSpec spec = new RefSpec(content); Ref source; try { // TODO better checks for wild-cards and such source = getRepository().findRef(isPush ? spec.getSource() : spec.getDestination()); } catch (IOException e) { source = null; } if (source != null || MessageDialog.openQuestion(getShell(), UIText.AbstractConfigureRemoteDialog_InvalidRefDialogTitle, NLS.bind( UIText.AbstractConfigureRemoteDialog_InvalidRefDialogMessage, spec.toString()))) { addRefSpec(spec); } updateControls(); } catch (IllegalArgumentException e) { MessageDialog.openError(getShell(), UIText.AbstractConfigureRemoteDialog_NoRefSpecDialogTitle, UIText.AbstractConfigureRemoteDialog_NoRefSpecDialogMessage); } } finally { clipboard.dispose(); } } private void doCopy() { String toCopy = ((IStructuredSelection) specViewer.getSelection()) .getFirstElement().toString(); Clipboard clipboard = new Clipboard(getShell().getDisplay()); try { clipboard.setContents(new String[] { toCopy }, new TextTransfer[] { TextTransfer.getInstance() }); } finally { clipboard.dispose(); } } private void doAddRefSpec() { RefSpec spec = getNewRefSpec(); if (spec != null) { addRefSpec(spec); updateControls(); } } private void doChangeRefSpec() { RefSpec oldSpec = (RefSpec) ((IStructuredSelection) specViewer .getSelection()).getFirstElement(); RefSpecDialog dlg = new RefSpecDialog(getShell(), getRepository(), getConfig(), oldSpec, isPush); if (dlg.open() == Window.OK) { removeRefSpec(oldSpec); addRefSpec(dlg.getSpec()); } updateControls(); } private void doDeleteRefSpecs() { for (Object spec : ((IStructuredSelection) specViewer.getSelection()) .toArray()) { removeRefSpec((RefSpec) spec); } updateControls(); } private void doAdvanced() { RefSpecWizard wizard = new RefSpecWizard(getRepository(), getConfig(), isPush); if (new WizardDialog(getShell(), wizard).open() == Window.OK) { updateControls(); } } private Button createActionButton(Composite parent, int style, IAction action) { Button button = new Button(parent, style); button.setText(action.getText()); button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { action.run(); } }); IPropertyChangeListener listener = event -> { if (IAction.ENABLED.equals(event.getProperty())) { if (!button.isDisposed()) { if (Display.getCurrent() == null) { button.getShell().getDisplay().syncExec(() -> { if (!button.isDisposed()) { button.setEnabled(action.isEnabled()); } }); } else { button.setEnabled(action.isEnabled()); } } } }; button.addDisposeListener( event -> action.removePropertyChangeListener(listener)); action.addPropertyChangeListener(listener); GridDataFactory.fillDefaults().applyTo(button); return button; } }