/*******************************************************************************
* Copyright (c) 2010 Tasktop Technologies 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
* Yatta Solutions - bug 432803: public API, bug 413871: performance
*******************************************************************************/
package org.eclipse.epp.internal.mpc.ui.wizards;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.concurrent.Callable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.epp.internal.mpc.core.MarketplaceClientCore;
import org.eclipse.epp.internal.mpc.core.service.AbstractDataStorageService.NotAuthorizedException;
import org.eclipse.epp.internal.mpc.core.util.URLUtil;
import org.eclipse.epp.internal.mpc.ui.MarketplaceClientUi;
import org.eclipse.epp.internal.mpc.ui.MarketplaceClientUiPlugin;
import org.eclipse.epp.internal.mpc.ui.catalog.MarketplaceCatalogSource;
import org.eclipse.epp.internal.mpc.ui.catalog.MarketplaceNodeCatalogItem;
import org.eclipse.epp.internal.mpc.ui.wizards.MarketplaceViewer.ContentType;
import org.eclipse.epp.mpc.core.model.INode;
import org.eclipse.epp.mpc.core.service.IUserFavoritesService;
import org.eclipse.epp.mpc.ui.Operation;
import org.eclipse.equinox.internal.p2.discovery.model.CatalogItem;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.layout.RowLayoutFactory;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.operation.ModalContext;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TypedEvent;
import org.eclipse.swt.graphics.Point;
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.Label;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.userstorage.util.ConflictException;
/**
* @author Steffen Pingel
* @author David Green
* @author Carsten Reckord
*/
public class DiscoveryItem<T extends CatalogItem> extends AbstractMarketplaceDiscoveryItem<T>
implements PropertyChangeListener {
private static final String FAVORITED_BUTTON_STATE_DATA = "favorited"; //$NON-NLS-1$
private static final int BUTTONBAR_MARGIN_TOP = 8;
public static final String WIDGET_ID_INSTALLS = "installs"; //$NON-NLS-1$
public static final String WIDGET_ID_TAGS = "tags"; //$NON-NLS-1$
public static final String WIDGET_ID_RATING = "rating"; //$NON-NLS-1$
public static final String WIDGET_ID_SHARE = "share"; //$NON-NLS-1$
public static final String WIDGET_ID_LEARNMORE = "learn more"; //$NON-NLS-1$
public static final String WIDGET_ID_OVERVIEW = "overview"; //$NON-NLS-1$
public static final String WIDGET_ID_ALREADY_INSTALLED = "already installed"; //$NON-NLS-1$
public static final String WIDGET_ID_ACTION = "action"; //$NON-NLS-1$
private ItemButtonController buttonController;
private StyledText installInfoLink;
private ShareSolutionLink shareSolutionLink;
private Button favoriteButton;
private SelectionListener toggleFavoritesListener;
public DiscoveryItem(Composite parent, int style, MarketplaceDiscoveryResources resources,
IMarketplaceWebBrowser browser,
final T connector, MarketplaceViewer viewer) {
super(parent, style, resources, browser, connector, viewer);
connector.addPropertyChangeListener(this);
this.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent e) {
connector.removePropertyChangeListener(DiscoveryItem.this);
}
});
}
@Override
protected void createContent() {
toggleFavoritesListener = new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
toggleFavorite();
}
};
super.createContent();
}
@Override
protected void createInstallButtons(Composite parent) {
Composite composite = new Composite(parent, SWT.NONE); // prevent the button from changing the layout of the title
GridDataFactory.fillDefaults().indent(0, BUTTONBAR_MARGIN_TOP).align(SWT.TRAIL, SWT.FILL).applyTo(composite);
int numColumns = 1;
boolean installed = connector.isInstalled();
if (installed && getViewer().getContentType() != ContentType.INSTALLED
&& getViewer().getContentType() != ContentType.SELECTION) {
Button alreadyInstalledButton = new Button(composite, SWT.PUSH | SWT.BOLD);
setWidgetId(alreadyInstalledButton, WIDGET_ID_ALREADY_INSTALLED);
alreadyInstalledButton.setText(Messages.DiscoveryItem_AlreadyInstalled);
alreadyInstalledButton.setFont(JFaceResources.getFontRegistry().getItalic("")); //$NON-NLS-1$
Point preferredSize = alreadyInstalledButton.computeSize(SWT.DEFAULT, SWT.DEFAULT);
int preferredWidth = preferredSize.x + 10;//Give a bit of extra padding for italic font
GridDataFactory.swtDefaults()
.align(SWT.TRAIL, SWT.CENTER)
.minSize(preferredWidth, SWT.DEFAULT)
.hint(preferredWidth, SWT.DEFAULT)
.grab(false, true)
.applyTo(alreadyInstalledButton);
alreadyInstalledButton.addSelectionListener(new SelectionListener() {
public void widgetSelected(SelectionEvent e) {
//show installed tab
getViewer().setContentType(ContentType.INSTALLED);
//then scroll to item
getViewer().reveal(DiscoveryItem.this);
}
public void widgetDefaultSelected(SelectionEvent e) {
widgetSelected(e);
}
});
} else if (hasInstallMetadata()) {
DropDownButton dropDown = new DropDownButton(composite, SWT.PUSH);
Button button = dropDown.getButton();
setWidgetId(button, WIDGET_ID_ACTION);
Point preferredSize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT);
int preferredWidth = preferredSize.x + 10;//Give a bit of extra padding for bold or italic font
GridDataFactory.swtDefaults()
.align(SWT.TRAIL, SWT.CENTER)
.minSize(preferredWidth, SWT.DEFAULT)
.grab(false, true)
.applyTo(button);
buttonController = new ItemButtonController(getViewer(), this, dropDown);
} else if (browser != null) {
installInfoLink = StyledTextHelper.createStyledTextLabel(composite);
setWidgetId(installInfoLink, WIDGET_ID_LEARNMORE);
installInfoLink.setToolTipText(Messages.DiscoveryItem_installInstructionsTooltip);
StyledTextHelper.appendLink(installInfoLink, Messages.DiscoveryItem_installInstructions,
Messages.DiscoveryItem_installInstructions, SWT.BOLD);
new LinkListener() {
@Override
protected void selected(Object href, TypedEvent e) {
browser.openUrl(getCatalogItemNode().getUrl());
}
}.register(installInfoLink);
GridDataFactory.swtDefaults().align(SWT.TRAIL, SWT.CENTER).grab(false, true).applyTo(installInfoLink);
} else {
Label placeholder = new Label(composite, SWT.NONE);
placeholder.setText(" "); //$NON-NLS-1$
GridDataFactory.swtDefaults().align(SWT.TRAIL, SWT.CENTER).grab(false, true).applyTo(placeholder);
}
GridLayoutFactory.fillDefaults()
.numColumns(numColumns)
.margins(0, 0)
.extendedMargins(0, 5, 0, 0)
.spacing(5, 0)
.applyTo(composite);
}
@Override
protected void createInstallInfo(Composite parent) {
Composite composite = new Composite(parent, SWT.NULL); // prevent the button from changing the layout of the title
GridDataFactory.fillDefaults()
.indent(DESCRIPTION_MARGIN_LEFT, BUTTONBAR_MARGIN_TOP)
.grab(true, false)
.align(SWT.BEGINNING, SWT.CENTER)
.applyTo(composite);
RowLayoutFactory.fillDefaults().type(SWT.HORIZONTAL).pack(true).applyTo(composite);
Integer installsTotal = null;
Integer installsRecent = null;
if (connector.getData() instanceof INode) {
INode node = (INode) connector.getData();
installsTotal = node.getInstallsTotal();
installsRecent = node.getInstallsRecent();
}
if (installsTotal != null || installsRecent != null) {
StyledText installInfo = new StyledText(composite, SWT.READ_ONLY | SWT.SINGLE);
setWidgetId(installInfo, WIDGET_ID_INSTALLS);
String totalText = installsTotal == null ? Messages.DiscoveryItem_Unknown_Installs : MessageFormat.format(
Messages.DiscoveryItem_Compact_Number, installsTotal.intValue(), installsTotal * 0.001,
installsTotal * 0.000001);
String recentText = installsRecent == null ? Messages.DiscoveryItem_Unknown_Installs
: MessageFormat.format("{0, number}", //$NON-NLS-1$
installsRecent.intValue());
String installInfoText = NLS.bind(Messages.DiscoveryItem_Installs, totalText, recentText);
int formatTotalsStart = installInfoText.indexOf(totalText);
if (formatTotalsStart == -1) {
installInfo.append(installInfoText);
} else {
if (formatTotalsStart > 0) {
installInfo.append(installInfoText.substring(0, formatTotalsStart));
}
StyledTextHelper.appendStyled(installInfo, totalText, new StyleRange(0, 0, null, null, SWT.BOLD));
installInfo.append(installInfoText.substring(formatTotalsStart + totalText.length()));
}
} else {
if (shareSolutionLink != null) {
shareSolutionLink.setShowText(true);
}
}
}
@Override
protected void createSocialButtons(Composite parent) {
Integer favorited = getFavoriteCount();
if (favorited == null || getCatalogItemUrl() == null) {
Label spacer = new Label(this, SWT.NONE);
spacer.setText(" ");//$NON-NLS-1$
GridDataFactory.fillDefaults().indent(0, BUTTONBAR_MARGIN_TOP).align(SWT.CENTER, SWT.FILL).applyTo(spacer);
} else {
createFavoriteButton(parent);
}
if (getCatalogItemUrl() != null) {
shareSolutionLink = new ShareSolutionLink(parent, connector);
Control shareControl = shareSolutionLink.getControl();
GridDataFactory.fillDefaults()
.indent(DESCRIPTION_MARGIN_LEFT, BUTTONBAR_MARGIN_TOP)
.align(SWT.BEGINNING, SWT.FILL)
.applyTo(shareControl);
} else {
Label spacer = new Label(this, SWT.NONE);
spacer.setText(" ");//$NON-NLS-1$
GridDataFactory.fillDefaults().indent(0, BUTTONBAR_MARGIN_TOP).align(SWT.CENTER, SWT.FILL).applyTo(spacer);
}
}
private Integer getFavoriteCount() {
if (connector.getData() instanceof INode) {
INode node = (INode) connector.getData();
IUserFavoritesService userFavoritesService = getUserFavoritesService();
if (userFavoritesService != null) {
return userFavoritesService.getFavoriteCount(node);
}
return node.getFavorited();
}
return null;
}
private void createFavoriteButton(Composite parent) {
favoriteButton = new Button(parent, SWT.PUSH);
setWidgetId(favoriteButton, WIDGET_ID_RATING);
refreshFavoriteButton();
//Make width more or less fixed
int width = SWT.DEFAULT;
{
favoriteButton.setText("999"); //$NON-NLS-1$
Point pSize = favoriteButton.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
width = pSize.x;
}
refreshFavoriteCount();
Point pSize = favoriteButton.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
width = Math.max(width, pSize.x);
final String ratingDescription = NLS.bind(Messages.DiscoveryItem_Favorited_Times, favoriteButton.getText());
favoriteButton.setToolTipText(ratingDescription);
favoriteButton.getAccessible().addAccessibleListener(new AccessibleAdapter() {
@Override
public void getName(AccessibleEvent e) {
e.result = ratingDescription;
}
});
GridDataFactory.fillDefaults()
.indent(0, BUTTONBAR_MARGIN_TOP)
.hint(Math.min(width, MAX_IMAGE_WIDTH), SWT.DEFAULT)
.align(SWT.CENTER, SWT.FILL)
.applyTo(favoriteButton);
}
private void refreshFavoriteButton() {
if (favoriteButton == null || this.isDisposed() || favoriteButton.isDisposed()) {
return;
}
if (Display.getCurrent() != this.getDisplay()) {
this.getDisplay().asyncExec(new Runnable() {
public void run() {
refreshFavoriteButton();
}
});
return;
}
boolean favorited = isFavorited();
Object lastFavorited = favoriteButton.getData(FAVORITED_BUTTON_STATE_DATA);
if (lastFavorited == null || (favorited != Boolean.TRUE.equals(lastFavorited))) {
favoriteButton.setData(FAVORITED_BUTTON_STATE_DATA, lastFavorited);
String imageId = favorited ? MarketplaceClientUiPlugin.ITEM_ICON_STAR_SELECTED
: MarketplaceClientUiPlugin.ITEM_ICON_STAR;
favoriteButton.setImage(MarketplaceClientUiPlugin.getInstance().getImageRegistry().get(imageId));
IUserFavoritesService userFavoritesService = getUserFavoritesService();
favoriteButton.setEnabled(userFavoritesService != null);
favoriteButton.removeSelectionListener(toggleFavoritesListener);
if (userFavoritesService != null) {
favoriteButton.addSelectionListener(toggleFavoritesListener);
}
}
refreshFavoriteCount();
}
private void refreshFavoriteCount() {
Integer favoriteCount = getFavoriteCount();
String favoriteCountText;
if (favoriteCount == null) {
favoriteCountText = ""; //$NON-NLS-1$
} else {
favoriteCountText = favoriteCount.toString();
}
favoriteButton.setText(favoriteCountText);
}
private boolean isFavorited() {
MarketplaceNodeCatalogItem nodeConnector = (MarketplaceNodeCatalogItem) connector;
Boolean favorited = nodeConnector.getUserFavorite();
return Boolean.TRUE.equals(favorited);
}
private void setFavorited(boolean newFavorited) {
boolean oldFavorited = isFavorited();
if (oldFavorited != newFavorited) {
//FIXME we should type the connector to MarketplaceNodeCatalogItem
MarketplaceNodeCatalogItem nodeConnector = (MarketplaceNodeCatalogItem) connector;
nodeConnector.setUserFavorite(newFavorited);
if (!newFavorited && getViewer().getContentType() == ContentType.FAVORITES) {
getViewer().getCatalog().removeItem(connector);
getViewer().refresh();
} else {
refreshFavoriteButton();
}
}
}
private IUserFavoritesService getUserFavoritesService() {
MarketplaceCatalogSource source = (MarketplaceCatalogSource) this.getData().getSource();
IUserFavoritesService userFavoritesService = source.getMarketplaceService().getUserFavoritesService();
return userFavoritesService;
}
private void toggleFavorite() {
final INode node = this.getCatalogItemNode();
final IUserFavoritesService userFavoritesService = getUserFavoritesService();
if (node != null && userFavoritesService != null) {
final boolean newFavorited = !isFavorited();
final Throwable[] error = new Throwable[] { null };
BusyIndicator.showWhile(getDisplay(), new Runnable() {
public void run() {
try {
ModalContext.run(new IRunnableWithProgress() {
public void run(final IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
try {
userFavoritesService.getStorageService().runWithLogin(new Callable<Void>() {
public Void call() throws Exception {
userFavoritesService.setFavorite(node, newFavorited, monitor);
return null;
}
});
} catch (Exception e) {
error[0] = e;
}
}
}, true, new NullProgressMonitor(), getDisplay());
} catch (InvocationTargetException e) {
error[0] = e.getCause();
} catch (InterruptedException e) {
error[0] = e;
}
}
});
Throwable e = error[0];
if (e != null) {
if (e instanceof NotAuthorizedException) {
// authentication was cancelled
return;
} else if (e instanceof ConflictException) {
// silently ignored - service already tried to resolve this
return;
} else {
IStatus status = MarketplaceClientCore.computeStatus(e,
NLS.bind(Messages.DiscoveryItem_FavoriteActionFailed, this.getNameLabelText()));
MarketplaceClientUi.handle(status, StatusManager.SHOW | StatusManager.BLOCK | StatusManager.LOG);
return;
}
}
setFavorited(newFavorited);
}
}
private INode getCatalogItemNode() {
Object data = connector.getData();
if (data instanceof INode) {
INode node = (INode) data;
return node;
}
return null;
}
private String getCatalogItemUrl() {
INode node = getCatalogItemNode();
return node == null ? null : node.getUrl();
}
private boolean hasInstallMetadata() {
if (!connector.getInstallableUnits().isEmpty() && connector.getSiteUrl() != null) {
try {
URLUtil.toURI(connector.getSiteUrl());
return true;
} catch (Exception ex) {
//ignore
}
}
return false;
}
/**
* @deprecated use {@link #maybeModifySelection(Operation)}
*/
@Deprecated
protected boolean maybeModifySelection(org.eclipse.epp.internal.mpc.ui.wizards.Operation operation) {
return maybeModifySelection(operation.getOperation());
}
protected boolean maybeModifySelection(Operation operation) {
getViewer().modifySelection(connector, operation);
return true;
}
@Override
public boolean isSelected() {
return getData().isSelected();
}
/**
* @deprecated use {@link #getSelectedOperation()} instead
*/
@Deprecated
public org.eclipse.epp.internal.mpc.ui.wizards.Operation getOperation() {
return org.eclipse.epp.internal.mpc.ui.wizards.Operation.map(getSelectedOperation());
}
public Operation getSelectedOperation() {
return getViewer().getSelectionModel().getSelectedOperation(getData());
}
public void propertyChange(PropertyChangeEvent evt) {
if (!isDisposed()) {
getDisplay().asyncExec(new Runnable() {
public void run() {
if (!isDisposed()) {
refresh(true);
}
}
});
}
}
@Override
protected void refresh() {
refresh(true);
}
protected void refresh(boolean updateState) {
super.refresh();
if (updateState && buttonController != null) {
buttonController.refresh();
}
refreshFavoriteButton();
}
@Override
protected MarketplaceViewer getViewer() {
return (MarketplaceViewer) super.getViewer();
}
@Override
protected void searchForProvider(String searchTerm) {
getViewer().search(searchTerm);
}
@Override
protected void searchForTag(String tag) {
getViewer().doQueryForTag(tag);
}
}