/**
* Copyright (c) 2016 Codetrails GmbH.
* 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:
* Andreas Sewe - initial API and implementation.
*/
package org.eclipse.recommenders.internal.news.rcp.toolbar;
import static org.eclipse.recommenders.internal.news.rcp.MessageUtils.getPeriodStartDate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import org.apache.commons.lang3.time.DateUtils;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.e4.core.commands.ECommandService;
import org.eclipse.e4.core.commands.EHandlerService;
import org.eclipse.e4.core.di.annotations.Optional;
import org.eclipse.e4.core.di.extensions.Preference;
import org.eclipse.e4.ui.di.UIEventTopic;
import org.eclipse.e4.ui.model.application.ui.menu.MToolControl;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.recommenders.internal.news.rcp.CommandConstants;
import org.eclipse.recommenders.internal.news.rcp.CommonImages;
import org.eclipse.recommenders.internal.news.rcp.Constants;
import org.eclipse.recommenders.internal.news.rcp.FeedDescriptor;
import org.eclipse.recommenders.internal.news.rcp.MessageUtils.MessageAge;
import org.eclipse.recommenders.internal.news.rcp.NewsRcpPreferences;
import org.eclipse.recommenders.internal.news.rcp.PreferenceConstants;
import org.eclipse.recommenders.internal.news.rcp.TopicConstants;
import org.eclipse.recommenders.internal.news.rcp.l10n.Messages;
import org.eclipse.recommenders.internal.news.rcp.notifications.NotificationBridge;
import org.eclipse.recommenders.internal.news.rcp.poll.PollingManager;
import org.eclipse.recommenders.news.api.NewsItem;
import org.eclipse.recommenders.news.api.poll.INewsPollingService;
import org.eclipse.recommenders.news.api.poll.PollingPolicy;
import org.eclipse.recommenders.news.api.poll.PollingRequest;
import org.eclipse.recommenders.news.api.poll.PollingResult;
import org.eclipse.recommenders.news.api.poll.PollingResult.Status;
import org.eclipse.recommenders.news.api.read.IReadItemsStore;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering;
@SuppressWarnings("restriction")
public class NewsToolControl {
private static final class PlaceholderAction extends Action {
private PlaceholderAction(String label) {
super(label);
setEnabled(false);
}
}
private final class ExecuteCommandAction extends Action {
private final ParameterizedCommand command;
private ExecuteCommandAction(String text, ImageDescriptor image, String commandId,
Map<String, Object> commandParameters) {
super(text, image);
command = commandService.createCommand(commandId, commandParameters);
}
@Override
public void run() {
handlerService.executeHandler(command);
}
@Override
public boolean isEnabled() {
return handlerService.canExecute(command);
}
}
private final Action openContextMenuAction = new Action() {
@Override
public void run() {
if (toolBarManager != null) {
toolBarManager.getContextMenuManager().getMenu().setVisible(true);
}
}
};
private final MToolControl modelElement;
private final NewsRcpPreferences preferences;
private final INewsPollingService pollingService;
private final IReadItemsStore readItemsStore;
private final ECommandService commandService;
private final EHandlerService handlerService;
@Nullable
private ToolBarManager toolBarManager;
@Inject
public NewsToolControl(MToolControl modelElement, NewsRcpPreferences preferences,
INewsPollingService pollingService, IReadItemsStore readItemsStore, ECommandService commandService,
EHandlerService handlerService) {
this.modelElement = modelElement;
this.preferences = preferences;
this.pollingService = pollingService;
this.readItemsStore = readItemsStore;
this.commandService = commandService;
this.handlerService = handlerService;
}
/**
* Causes the singleton {@link NotificationBridge} to be instantiated.
*/
@Inject
public void initialize(NotificationBridge ignored) {
// No op
}
/**
* Causes the singleton {@link PollingManager} to be instantiated.
*/
@Inject
public void initialize(PollingManager ignored) {
// No op
}
@PostConstruct
public void createGui(Composite parent) {
ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT | SWT.HORIZONTAL);
MenuManager contextMenu = new MenuManager();
contextMenu.setRemoveAllWhenShown(true);
contextMenu.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(IMenuManager menu) {
Map<FeedDescriptor, PollingResult> feedContents = retrieveFeedContents(
preferences.getFeedDescriptors());
if (feedContents.isEmpty()) {
PlaceholderAction allFeedsDisabledAction = new PlaceholderAction(
Messages.ACTION_LABEL_ALL_FEEDS_DISABLED);
menu.add(allFeedsDisabledAction);
} else {
for (Entry<FeedDescriptor, PollingResult> entry : sortByName(feedContents.entrySet())) {
MenuManager feedMenu = createFeedMenu(entry.getKey(), entry.getValue());
menu.add(feedMenu);
}
}
menu.add(new Separator());
ExecuteCommandAction pollNowAction = new ExecuteCommandAction(Messages.ACTION_LABEL_POLL_NOW,
CommonImages.REFRESH, CommandConstants.ID_POLL_NEWS_FEEDS,
Collections.<String, Object>emptyMap());
menu.add(pollNowAction);
ExecuteCommandAction markAsReadAction = new ExecuteCommandAction(Messages.ACTION_LABEL_MARK_AS_READ,
null, CommandConstants.ID_READ_NEWS_ITEMS,
ImmutableMap.<String, Object>of(CommandConstants.PARAMETER_READ_NEWS_ITEMS_NEWS_ITEMS,
getAllItems(feedContents.values())));
menu.add(markAsReadAction);
menu.add(new Separator());
ExecuteCommandAction preferencesAction = new ExecuteCommandAction(Messages.ACTION_LABEL_PREFERENCES,
null, CommandConstants.ID_PREFERENCES, ImmutableMap.<String, Object>of(
CommandConstants.PARAMETER_PREFERENCES_PREFERENCE_PAGE_ID, Constants.PREF_PAGE_ID));
menu.add(preferencesAction);
}
private Iterable<Entry<FeedDescriptor, PollingResult>> sortByName(
Set<Entry<FeedDescriptor, PollingResult>> entrySet) {
return Ordering.natural().onResultOf(new Function<Entry<FeedDescriptor, PollingResult>, String>() {
@Override
public String apply(Entry<FeedDescriptor, PollingResult> entry) {
return entry.getKey().getName();
}
}).sortedCopy(entrySet);
}
});
toolBarManager.setContextMenuManager(contextMenu);
refreshUnreadItemsStatus(openContextMenuAction);
toolBarManager.add(openContextMenuAction);
toolBarManager.createControl(parent);
this.toolBarManager = toolBarManager;
}
private MenuManager createFeedMenu(FeedDescriptor feed, PollingResult result) {
List<NewsItem> items = result.getAllNewsItems();
int numberOfUnreadMessages = getNumberOfUnreadMessages(items);
String feedLabel;
if (numberOfUnreadMessages == 0) {
feedLabel = MessageFormat.format(Messages.LABEL_READ_FEED, preserveAtSign(feed.getName()));
} else {
feedLabel = MessageFormat.format(Messages.LABEL_UNREAD_FEED, preserveAtSign(feed.getName()),
numberOfUnreadMessages);
}
MenuManager feedMenu = new MenuManager(feedLabel);
if (result.getStatus().equals(Status.NOT_DOWNLOADED)) {
feedMenu.add(new PlaceholderAction(Messages.FEED_NOT_POLLED_YET));
} else if (result.getAllNewsItems().isEmpty()) {
feedMenu.add(new PlaceholderAction(Messages.FEED_EMPTY));
} else {
List<List<NewsItem>> ageGroups = splitMessagesByAge(items);
List<String> labels = ImmutableList.of(Messages.ACTION_LABEL_TODAY, Messages.ACTION_LABEL_YESTERDAY,
Messages.ACTION_LABEL_THIS_WEEK, Messages.ACTION_LABEL_LAST_WEEK, Messages.ACTION_LABEL_THIS_MONTH,
Messages.ACTION_LABEL_LAST_MONTH, Messages.ACTION_LABEL_THIS_YEAR,
Messages.ACTION_LABEL_OLDER_ENTRIES, Messages.ACTION_LABEL_UNDETERMINED_ENTRIES);
for (int i = 0; i < MessageAge.values().length; i++) {
List<NewsItem> ageGroup = ageGroups.get(i);
if (ageGroup.isEmpty()) {
continue;
}
feedMenu.add(new Separator());
feedMenu.add(new PlaceholderAction(labels.get(i)));
for (NewsItem item : ageGroup) {
String itemLabel;
if (readItemsStore.isRead(item)) {
itemLabel = MessageFormat.format(Messages.LABEL_READ_ITEM, preserveAtSign(item.getTitle()));
} else {
itemLabel = MessageFormat.format(Messages.LABEL_UNREAD_ITEM, preserveAtSign(item.getTitle()));
}
ExecuteCommandAction readNewsItemAction = new ExecuteCommandAction(itemLabel, null,
CommandConstants.ID_READ_NEWS_ITEMS,
ImmutableMap.<String, Object>of(CommandConstants.PARAMETER_READ_NEWS_ITEMS_NEWS_ITEMS,
Collections.singleton(item),
CommandConstants.PARAMETER_READ_NEWS_ITEMS_OPEN_BROWSER, true));
feedMenu.add(readNewsItemAction);
}
}
feedMenu.add(new Separator());
ExecuteCommandAction markAsReadAction = new ExecuteCommandAction(Messages.ACTION_LABEL_MARK_AS_READ, null,
CommandConstants.ID_READ_NEWS_ITEMS,
ImmutableMap.<String, Object>of(CommandConstants.PARAMETER_READ_NEWS_ITEMS_NEWS_ITEMS, items));
feedMenu.add(markAsReadAction);
}
return feedMenu;
}
private int getNumberOfUnreadMessages(Collection<NewsItem> collection) {
int numberOfUnreadMessages = 0;
for (NewsItem item : collection) {
if (!readItemsStore.isRead(item)) {
numberOfUnreadMessages++;
}
}
return numberOfUnreadMessages;
}
/**
* @see org.eclipse.jface.action.IAction#setText(java.lang.String)
* @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=486086">Bug 486086</a>
*/
private static String preserveAtSign(String actionText) {
String atSign = "@"; //$NON-NLS-1$
if (actionText.contains(atSign)) {
return actionText + atSign;
} else {
return actionText;
}
}
@Inject
@Optional
public void handlePollingResults(@UIEventTopic(TopicConstants.POLLING_RESULTS) Collection<PollingResult> ignored) {
refreshUnreadItemsStatus(openContextMenuAction);
}
@Inject
@Optional
public void handleNewsItemsRead(@UIEventTopic(TopicConstants.NEWS_ITEMS_READ) Collection<NewsItem> ignored) {
refreshUnreadItemsStatus(openContextMenuAction);
}
private void refreshUnreadItemsStatus(Action action) {
int numberOfUnreadMessages = getNumberOfUnreadMessages(
getAllItems(retrieveFeedContents(preferences.getFeedDescriptors()).values()));
boolean hasUnreadItems = numberOfUnreadMessages > 0;
action.setImageDescriptor(hasUnreadItems ? CommonImages.RSS_ACTIVE : CommonImages.RSS_INACTIVE);
action.setToolTipText(MessageFormat.format(Messages.ACTION_TOOLTIP_NEWS, numberOfUnreadMessages));
}
@PreDestroy
public void dispose() {
if (toolBarManager != null) {
toolBarManager.dispose();
}
}
private Map<FeedDescriptor, PollingResult> retrieveFeedContents(List<FeedDescriptor> feeds) {
List<PollingRequest> requests = new ArrayList<>(feeds.size());
for (FeedDescriptor feed : feeds) {
if (!feed.isEnabled()) {
continue;
}
requests.add(new PollingRequest(feed.getUri(), PollingPolicy.never()));
}
if (requests.isEmpty()) {
return Collections.emptyMap();
}
Map<FeedDescriptor, PollingResult> feedContents = new HashMap<>();
Collection<PollingResult> results = pollingService.poll(requests, null);
Iterator<FeedDescriptor> feedsIterator = feeds.iterator();
Iterator<PollingResult> resultsIterator = results.iterator();
while (feedsIterator.hasNext() && resultsIterator.hasNext()) {
FeedDescriptor feed = feedsIterator.next();
PollingResult result = resultsIterator.next();
feedContents.put(feed, result);
}
return feedContents;
}
private static List<List<NewsItem>> splitMessagesByAge(List<NewsItem> messages) {
Locale locale = Locale.getDefault();
Calendar calendar = Calendar.getInstance(locale);
List<List<NewsItem>> result = new ArrayList<>();
for (int i = 0; i < MessageAge.values().length; i++) {
List<NewsItem> list = new ArrayList<>();
result.add(list);
}
if (messages == null) {
return result;
}
Date today = DateUtils.truncate(calendar.getTime(), Calendar.DAY_OF_MONTH);
for (NewsItem message : messages) {
for (MessageAge messageAge : MessageAge.values()) {
if (message.getDate().after(getPeriodStartDate(messageAge, today, locale))
|| message.getDate().equals(getPeriodStartDate(messageAge, today, locale))) {
result.get(messageAge.getIndex()).add(message);
break;
}
}
if (message.getDate().before(getPeriodStartDate(MessageAge.OLDER, today, locale))) {
result.get(MessageAge.OLDER.getIndex()).add(message);
}
}
return result;
}
private static Collection<NewsItem> getAllItems(Collection<PollingResult> results) {
Collection<NewsItem> allItems = new ArrayList<>();
for (PollingResult result : results) {
allItems.addAll(result.getAllNewsItems());
}
return allItems;
}
}