blob: c383e3e26bad334818bdab7ab66e3cc87441efbe [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2016 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Jan-Ove Weichel <janove.weichel@vogella.com> - Bugs 411578, 486842, 487673
* Lars Vogel <Lars.Vogel@vogella.com> - Bug 492918
*******************************************************************************/
package org.eclipse.ui.internal.ide;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.eclipse.core.runtime.IProduct;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.util.Geometry;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.osgi.util.TextProcessor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Link;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Monitor;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.forms.events.ExpansionAdapter;
import org.eclipse.ui.forms.events.ExpansionEvent;
import org.eclipse.ui.forms.widgets.ExpandableComposite;
import org.eclipse.ui.forms.widgets.Form;
import org.eclipse.ui.forms.widgets.FormToolkit;
/**
* A dialog that prompts for a directory to use as a workspace.
*/
public class ChooseWorkspaceDialog extends TitleAreaDialog {
private static final String DIALOG_SETTINGS_SECTION = "ChooseWorkspaceDialogSettings"; //$NON-NLS-1$
private ChooseWorkspaceData launchData;
private Combo text;
private boolean suppressAskAgain = false;
private boolean centerOnMonitor = false;
private Map<String, Link> recentWorkspacesLinks;
private Form recentWorkspacesForm;
/**
* Create a modal dialog on the arugment shell, using and updating the
* argument data object.
* @param parentShell the parent shell for this dialog
* @param launchData the launch data from past launches
*
* @param suppressAskAgain
* true means the dialog will not have a "don't ask again" button
* @param centerOnMonitor indicates whether the dialog should be centered on
* the monitor or according to it's parent if there is one
*/
public ChooseWorkspaceDialog(Shell parentShell,
ChooseWorkspaceData launchData, boolean suppressAskAgain, boolean centerOnMonitor) {
super(parentShell);
this.launchData = launchData;
this.suppressAskAgain = suppressAskAgain;
this.centerOnMonitor = centerOnMonitor;
}
/**
* Show the dialog to the user (if needed). When this method finishes,
* #getSelection will return the workspace that should be used (whether it
* was just selected by the user or some previous default has been used.
* The parameter can be used to override the users preference. For example,
* this is important in cases where the default selection is already in use
* and the user is forced to choose a different one.
*
* @param force
* true if the dialog should be opened regardless of the value of
* the show dialog checkbox
*/
public void prompt(boolean force) {
if (force || launchData.getShowDialog()) {
open();
// Bug 70576: Dialog gets dismissed via ESC and via the window's
// close box. Make sure the launch doesn't continue with the default
// workspace.
if (getReturnCode() == CANCEL) {
launchData.workspaceSelected(null);
}
return;
}
String[] recent = launchData.getRecentWorkspaces();
// If the selection dialog was not used then the workspace to use is either the
// most recent selection or the initialDefault (if there is no history).
String workspace = null;
if (recent != null && recent.length > 0) {
workspace = recent[0];
}
if (workspace == null || workspace.length() == 0) {
workspace = launchData.getInitialDefault();
}
launchData.workspaceSelected(TextProcessor.deprocess(workspace));
}
/**
* Creates and returns the contents of the upper part of this dialog (above
* the button bar).
* <p>
* The <code>Dialog</code> implementation of this framework method creates
* and returns a new <code>Composite</code> with no margins and spacing.
* </p>
*
* @param parent the parent composite to contain the dialog area
* @return the dialog area control
*/
@Override
protected Control createDialogArea(Composite parent) {
String productName = getWindowTitle();
Composite composite = (Composite) super.createDialogArea(parent);
setTitle(IDEWorkbenchMessages.ChooseWorkspaceDialog_dialogTitle);
setMessage(NLS.bind(IDEWorkbenchMessages.ChooseWorkspaceDialog_dialogMessage, productName));
// bug 59934: load title image for sizing, but set it non-visible so the
// white background is displayed
if (getTitleImageLabel() != null) {
getTitleImageLabel().setVisible(false);
}
// Should only create the Recent Workspaces Composite if Recent
// workspaces exist
boolean createRecentWorkspacesComposite = false;
if (launchData.getRecentWorkspaces()[0] != null) {
createRecentWorkspacesComposite = true;
}
createWorkspaceBrowseRow(composite);
if (!suppressAskAgain) {
createShowDialogButton(composite);
}
if (createRecentWorkspacesComposite) {
createRecentWorkspacesComposite(composite);
}
Dialog.applyDialogFont(composite);
return composite;
}
@Override
protected void createButtonsForButtonBar(Composite parent) {
// create OK and Cancel buttons by default
createButton(parent, IDialogConstants.OK_ID, IDEWorkbenchMessages.ChooseWorkspaceDialog_launchLabel, true);
createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
}
/**
* Returns the title that the dialog (or splash) should have.
*
* @return the window title
* @since 3.4
*/
public static String getWindowTitle() {
String productName = null;
IProduct product = Platform.getProduct();
if (product != null) {
productName = product.getName();
}
if (productName == null) {
productName = IDEWorkbenchMessages.ChooseWorkspaceDialog_defaultProductName;
}
return productName;
}
@Override
protected void configureShell(Shell shell) {
super.configureShell(shell);
shell.setText(NLS.bind(IDEWorkbenchMessages.ChooseWorkspaceDialog_dialogName, getWindowTitle()));
shell.addTraverseListener(e -> {
// Bug 462707: [WorkbenchLauncher] dialog not closed on ESC.
// The dialog doesn't always have a parent, so
// Shell#traverseEscape() doesn't always close it for free.
if (e.detail == SWT.TRAVERSE_ESCAPE) {
e.detail = SWT.TRAVERSE_NONE;
cancelPressed();
}
});
}
/**
* Notifies that the ok button of this dialog has been pressed.
* <p>
* The <code>Dialog</code> implementation of this framework method sets
* this dialog's return code to <code>Window.OK</code>
* and closes the dialog. Subclasses may override.
* </p>
*/
@Override
protected void okPressed() {
workspaceSelected(getWorkspaceLocation());
}
/**
* Set the selected workspace to the given String and close the dialog
*
* @param workspace
*/
private void workspaceSelected(String workspace) {
launchData.workspaceSelected(TextProcessor.deprocess(workspace));
super.okPressed();
}
/**
* Removes the workspace from RecentWorkspaces
*
* @param workspace
*/
private void removeWorkspaceFromLauncher(String workspace) {
// Remove Workspace from Properties
List<String> recentWorkpaces = new ArrayList<>(Arrays.asList(launchData.getRecentWorkspaces()));
recentWorkpaces.remove(workspace);
launchData.setRecentWorkspaces(recentWorkpaces.toArray(new String[0]));
launchData.writePersistedData();
// Remove Workspace Composite
recentWorkspacesLinks.get(workspace).dispose();
recentWorkspacesLinks.remove(workspace);
if (recentWorkspacesLinks.isEmpty()) {
recentWorkspacesForm.dispose();
}
getShell().layout();
initializeBounds();
// Remove Workspace from combobox
text.remove(workspace);
if (text.getText().equals(workspace) || text.getText().isEmpty()) {
text.setText(TextProcessor
.process((text.getItemCount() > 0 ? text.getItem(0) : launchData.getInitialDefault())));
}
}
/**
* Get the workspace location from the widget.
* @return String
*/
protected String getWorkspaceLocation() {
return text.getText();
}
@Override
protected void cancelPressed() {
launchData.workspaceSelected(null);
super.cancelPressed();
}
/**
* The Recent Workspaces area of the dialog is only shown if Recent
* Workspaces are defined. It provides a faster way to launch a specific
* Workspace
*/
private void createRecentWorkspacesComposite(final Composite composite) {
FormToolkit toolkit = new FormToolkit(composite.getDisplay());
composite.addDisposeListener(c -> toolkit.dispose());
recentWorkspacesForm = toolkit.createForm(composite);
recentWorkspacesForm.setBackground(composite.getBackground());
recentWorkspacesForm.getBody().setLayout(new GridLayout());
ExpandableComposite recentWorkspacesExpandable = toolkit.createExpandableComposite(recentWorkspacesForm.getBody(),
ExpandableComposite.TWISTIE);
recentWorkspacesForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
recentWorkspacesExpandable.setBackground(composite.getBackground());
recentWorkspacesExpandable.setText(IDEWorkbenchMessages.ChooseWorkspaceDialog_recentWorkspaces);
recentWorkspacesExpandable.setExpanded(launchData.isShowRecentWorkspaces());
recentWorkspacesExpandable.addExpansionListener(new ExpansionAdapter() {
@Override
public void expansionStateChanged(ExpansionEvent e) {
launchData.setShowRecentWorkspaces(((ExpandableComposite) e.getSource()).isExpanded());
Point size = getInitialSize();
Shell shell = getShell();
shell.setBounds(getConstrainedShellBounds(
new Rectangle(shell.getLocation().x, shell.getLocation().y, size.x, size.y)));
}
});
Composite panel = new Composite(recentWorkspacesExpandable, SWT.NONE);
recentWorkspacesExpandable.setClient(panel);
RowLayout layout = new RowLayout(SWT.VERTICAL);
layout.marginLeft = 14;
layout.spacing = 6;
panel.setLayout(layout);
recentWorkspacesLinks = new HashMap<>(launchData.getRecentWorkspaces().length);
Map<String, String> uniqueWorkspaceNames = createUniqueWorkspaceNameMap();
List<String> recentWorkspacesList = Arrays.asList(launchData.getRecentWorkspaces()).stream()
.filter(s -> s != null && !s.isEmpty()).collect(Collectors.toList());
List<Entry<String, String>> sortedList = uniqueWorkspaceNames.entrySet().stream().sorted((e1, e2) -> Integer
.compare(recentWorkspacesList.indexOf(e1.getValue()), recentWorkspacesList.indexOf(e2.getValue())))
.collect(Collectors.toList());
for (Entry<String, String> uniqueWorkspaceEntry : sortedList) {
final String recentWorkspace = uniqueWorkspaceEntry.getValue();
Link link = new Link(panel, SWT.WRAP);
link.setLayoutData(new RowData(SWT.DEFAULT, SWT.DEFAULT));
link.setText("<a>" + uniqueWorkspaceEntry.getKey() + "</a>"); //$NON-NLS-1$ //$NON-NLS-2$
link.setToolTipText(recentWorkspace);
link.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
workspaceSelected(recentWorkspace);
}
});
recentWorkspacesLinks.put(recentWorkspace, link);
Menu menu = new Menu(link);
MenuItem forgetItem = new MenuItem(menu, SWT.PUSH);
forgetItem.setText(IDEWorkbenchMessages.ChooseWorkspaceDialog_removeWorkspaceSelection);
forgetItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
removeWorkspaceFromLauncher(recentWorkspace);
}
});
link.setMenu(menu);
}
}
/**
* Creates a map with unique WorkspaceNames for the
* RecentWorkspacesComposite. The values are full absolute paths of recently
* used workspaces, the keys are unique segments somehow made from the
* values.
*/
private Map<String, String> createUniqueWorkspaceNameMap() {
final String fileSeparator = File.separator;
Map<String, String> uniqueWorkspaceNameMap = new HashMap<>();
// Convert workspace paths to arrays of single path segments
List<String[]> splittedWorkspaceNames = Arrays.asList(launchData.getRecentWorkspaces()).stream()
.filter(s -> s != null && !s.isEmpty()).map(s -> s.split(Pattern.quote(fileSeparator)))
.collect(Collectors.toList());
// create and collect unique workspace keys produced from arrays,
// try to generate unique keys starting with the last segment of the
// workspace path, increasing number of segments if no unique names
// could be generated,
// loop until all array values are removed from array list
for (int i = 1; !splittedWorkspaceNames.isEmpty(); i++) {
final int c = i;
// Function which flattens arrays to (hopefully unique) keys
Function<String[], String> stringArraytoName = s -> String.join(fileSeparator,
s.length < c ? s : Arrays.copyOfRange(s, s.length - c, s.length));
// list of found unique keys
List<String> uniqueNames = splittedWorkspaceNames.stream().map(stringArraytoName)
.collect(Collectors.groupingBy(s -> s, Collectors.counting())).entrySet().stream()
.filter(e -> e.getValue() == 1).map(e -> e.getKey()).collect(Collectors.toList());
// remove paths for which we have found unique keys
splittedWorkspaceNames.removeIf(a -> {
String joined = stringArraytoName.apply(a);
if (uniqueNames.contains(joined)) {
uniqueWorkspaceNameMap.put(joined, String.join(fileSeparator, a));
return true;
}
return false;
});
}
return uniqueWorkspaceNameMap;
}
/**
* The main area of the dialog is just a row with the current selection
* information and a drop-down of the most recently used workspaces.
*/
private void createWorkspaceBrowseRow(Composite parent) {
Composite panel = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout(3, false);
layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING);
panel.setLayout(layout);
panel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
panel.setFont(parent.getFont());
CLabel label = new CLabel(panel, SWT.NONE);
label.setText(IDEWorkbenchMessages.ChooseWorkspaceDialog_workspaceEntryLabel);
label.setMargins(0, 0, 2, 0);
text = new Combo(panel, SWT.BORDER | SWT.LEAD | SWT.DROP_DOWN);
new DirectoryProposalContentAssist().apply(text);
text.setFocus();
text.setLayoutData(new GridData(400, SWT.DEFAULT));
text.addModifyListener(e -> {
Button okButton = getButton(Window.OK);
if(okButton != null && !okButton.isDisposed()) {
boolean nonWhitespaceFound = false;
String characters = getWorkspaceLocation();
for (int i = 0; !nonWhitespaceFound
&& i < characters.length(); i++) {
if (!Character.isWhitespace(characters.charAt(i))) {
nonWhitespaceFound = true;
}
}
okButton.setEnabled(nonWhitespaceFound);
}
});
setInitialTextValues(text);
Button browseButton = new Button(panel, SWT.PUSH);
browseButton.setText(IDEWorkbenchMessages.ChooseWorkspaceDialog_browseLabel);
setButtonLayoutData(browseButton);
GridData data = (GridData) browseButton.getLayoutData();
data.horizontalAlignment = GridData.HORIZONTAL_ALIGN_END;
browseButton.setLayoutData(data);
browseButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
DirectoryDialog dialog = new DirectoryDialog(getShell(), SWT.SHEET);
dialog.setText(IDEWorkbenchMessages.ChooseWorkspaceDialog_directoryBrowserTitle);
dialog.setMessage(IDEWorkbenchMessages.ChooseWorkspaceDialog_directoryBrowserMessage);
dialog.setFilterPath(getInitialBrowsePath());
String dir = dialog.open();
if (dir != null) {
text.setText(TextProcessor.process(dir));
}
}
});
}
/**
* Return a string containing the path that is closest to the current
* selection in the text widget. This starts with the current value and
* works toward the root until there is a directory for which File.exists
* returns true. Return the current working dir if the text box does not
* contain a valid path.
*
* @return closest parent that exists or an empty string
*/
private String getInitialBrowsePath() {
File dir = new File(getWorkspaceLocation());
while (dir != null && !dir.exists()) {
dir = dir.getParentFile();
}
return dir != null ? dir.getAbsolutePath() : System
.getProperty("user.dir"); //$NON-NLS-1$
}
/*
* see org.eclipse.jface.Window.getInitialLocation()
*/
@Override
protected Point getInitialLocation(Point initialSize) {
Composite parent = getShell().getParent();
if (!centerOnMonitor || parent == null) {
return super.getInitialLocation(initialSize);
}
Monitor monitor = parent.getMonitor();
Rectangle monitorBounds = monitor.getClientArea();
Point centerPoint = Geometry.centerPoint(monitorBounds);
return new Point(centerPoint.x - (initialSize.x / 2), Math.max(
monitorBounds.y, Math.min(centerPoint.y
- (initialSize.y * 2 / 3), monitorBounds.y
+ monitorBounds.height - initialSize.y)));
}
/**
* The show dialog button allows the user to choose to neven be nagged again.
*/
private void createShowDialogButton(Composite parent) {
Composite panel = new Composite(parent, SWT.NONE);
panel.setFont(parent.getFont());
GridLayout layout = new GridLayout(1, false);
layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
panel.setLayout(layout);
GridData data = new GridData(GridData.FILL_BOTH);
data.verticalAlignment = GridData.END;
panel.setLayoutData(data);
Button button = new Button(panel, SWT.CHECK);
button.setText(IDEWorkbenchMessages.ChooseWorkspaceDialog_useDefaultMessage);
button.setSelection(!launchData.getShowDialog());
button.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
launchData.toggleShowDialog();
}
});
}
private void setInitialTextValues(Combo text) {
for (String recentWorkspace : launchData.getRecentWorkspaces()) {
if (recentWorkspace != null) {
text.add(recentWorkspace);
}
}
text.setText(TextProcessor.process((text.getItemCount() > 0 ? text
.getItem(0) : launchData.getInitialDefault())));
}
@Override
protected IDialogSettings getDialogBoundsSettings() {
// If we were explicitly instructed to center on the monitor, then
// do not provide any settings for retrieving a different location or, worse,
// saving the centered location.
if (centerOnMonitor) {
return null;
}
IDialogSettings settings = IDEWorkbenchPlugin.getDefault().getDialogSettings();
IDialogSettings section = settings.getSection(DIALOG_SETTINGS_SECTION);
if (section == null) {
section = settings.addNewSection(DIALOG_SETTINGS_SECTION);
}
return section;
}
public Combo getCombo() {
return text;
}
}