| /**************************************************************************** |
| * Copyright (c) 2017, 2018 Remain Software |
| * 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: |
| * Wim Jongman <wim.jongman@remainsoftware.com> - initial API and implementation |
| *****************************************************************************/ |
| package org.eclipse.tips.core; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.tips.core.internal.FinalTip; |
| |
| /** |
| * Class to provide tips to the tip framework. It is the job of this provider to |
| * manage its tips. Examples of managing tips are: |
| * |
| * <ul> |
| * <li>Loading tips from the internet</li> |
| * <li>Serve next, previous and current tip on request</li> |
| * </ul> |
| * |
| * After the TipProvider is instantiated by the {@link ITipManager}, the |
| * TipManager will insert itself by calling {@link #setManager(ITipManager)}. |
| * Then the TipManager will asynchronous call this providers' |
| * {@link #loadNewTips(IProgressMonitor)} method. The job of the load() method |
| * is to do long work like fetching new tips from the internet and storing them |
| * locally. There is no defined method on how tips should be stored locally, |
| * implementers are free to do what is needed. |
| * |
| * The constructor must return fast, meaning that tips may not be fetched from |
| * the Internet in the constructor. This should be done in the |
| * {@link #loadNewTips(IProgressMonitor)} method. |
| * |
| * To indicate that this provider is ready to serve tips, it should call the |
| * {@link #setTips(List)} method which then sets its <code>ready</code> flag. |
| * |
| */ |
| public abstract class TipProvider { |
| |
| private ITipManager fTipManager; |
| private int fTipIndex; |
| protected List<Tip> fTips = new ArrayList<>(); |
| private Tip fCurrentTip; |
| private boolean fReady; |
| private TipProviderListenerManager fListenerManager = new TipProviderListenerManager(); |
| private Tip fFinalTip = new FinalTip(getID()); |
| private String fExpression; |
| |
| /** |
| * The zero argument constructor must be able to instantiate the TipProvider. |
| * This method may also be used to quickly set the available tips by calling the |
| * {@link #setTips(List)} method. The constructor may not be used to load tips |
| * from the internet. Use the {@link #loadNewTips(IProgressMonitor)} method for |
| * this purpose. |
| * |
| * @see #loadNewTips(IProgressMonitor) |
| * @see #setTips(List) |
| */ |
| public TipProvider() { |
| } |
| |
| /** |
| * Provides the opportunity to release all held resources. |
| */ |
| public abstract void dispose(); |
| |
| /** |
| * @return the short description of this provider. |
| */ |
| public abstract String getDescription(); |
| |
| /** |
| * @return the ID of this provider |
| */ |
| public abstract String getID(); |
| |
| /** |
| * The image used by the UI for low resolution |
| * |
| * @return a 48x48 {@link TipImage} |
| */ |
| public abstract TipImage getImage(); |
| |
| /** |
| * Get a list of tips. The default implementation returns tips based on the |
| * following conditions: <br> |
| * <dl> |
| * <dt><code>pFilter</code> is false</dt> |
| * <dd>Return all read and unread tips.</dd> |
| * <dt><code>pFilter</code> is true</dt> |
| * <dd>Return read and unread tips if the tipManager may serve unread tips, |
| * otherwise return only unread tips.</dd> |
| * </dl> |
| * <p> |
| * Subclasses may override (calling super(false) to fetch the list) if they want |
| * to serve or sort the list of tips in a different way. |
| * |
| * @param filter |
| * false or true, see description above. |
| * @return an unmodifiable list of tips. |
| */ |
| public synchronized List<Tip> getTips(boolean filter) { |
| if (filter) { |
| return Collections.unmodifiableList(fTips // |
| .stream() // |
| .filter(tip -> getManager().mustServeReadTips() || !getManager().isRead(tip)) // |
| .sorted(Comparator.comparing(Tip::getCreationDate).reversed()) // |
| .collect(Collectors.toList())); |
| } |
| return Collections.unmodifiableList(fTips); |
| } |
| |
| /** |
| * @return the {@link Tip} that was last returned by {@link #getNextTip()} or |
| * {@link #getPreviousTip()} |
| */ |
| public synchronized Tip getCurrentTip() { |
| if (fCurrentTip == null) { |
| return getNextTip(); |
| } |
| return fCurrentTip; |
| } |
| |
| /** |
| * The next {@link Tip} is returned based on the read status of the Tip and the |
| * fact if already read tips must be served or not which is known by the |
| * {@link ITipManager}: ({@link ITipManager#mustServeReadTips()}). |
| * |
| * @return the next {@link Tip} |
| * @see #getPreviousTip() |
| * @see #getCurrentTip() |
| */ |
| public synchronized Tip getNextTip() { |
| boolean unreadOnly = !getManager().mustServeReadTips(); |
| List<Tip> list = getTips(unreadOnly); |
| if (list.isEmpty()) { |
| return setCurrentTip(fFinalTip); |
| } |
| if (!unreadOnly && fCurrentTip != null) { |
| fTipIndex++; |
| } else if (fCurrentTip != null && getManager().isRead(fCurrentTip)) { |
| fTipIndex++; |
| } |
| if (fTipIndex >= list.size()) { |
| fTipIndex = 0; |
| } |
| return setCurrentTip(list.get(fTipIndex)); |
| } |
| |
| /** |
| * @return the previous {@link Tip} |
| * @see #getNextTip() |
| * @see #getCurrentTip() |
| */ |
| public Tip getPreviousTip() { |
| List<Tip> list = getTips(!getManager().mustServeReadTips()); |
| if (list.isEmpty()) { |
| return setCurrentTip(fFinalTip); |
| } |
| fTipIndex--; |
| if (fTipIndex < 0) { |
| fTipIndex = list.size() - 1; |
| } |
| return setCurrentTip(list.get(fTipIndex)); |
| } |
| |
| /** |
| * @return the {@link ITipManager} of this provider, never null. |
| */ |
| public synchronized ITipManager getManager() { |
| return fTipManager; |
| } |
| |
| /** |
| * @return true if the provider is ready to deliver tips |
| */ |
| public final boolean isReady() { |
| return fReady; |
| } |
| |
| /** |
| * Is called asynchronously during startup of the TipManager to gather new tips. |
| * |
| * The provider is not available to the UI unless it has called it's |
| * {@link #setTips(List)} method. It is therefore possible that the provider is |
| * not immediately visible in the tip UI but will be added later. |
| * <p> |
| * If you run out of tips and you feel that you should load more tips on your |
| * own then you can also asynchronously call this method. A good place would be |
| * to override {@link #getTips(boolean)}, check if the supply of tips is |
| * sufficient and then call this method asynchronously. |
| * <p> |
| * One strategy is to do a long running fetch in this method and then store the |
| * tips locally. On the next run of the TipManager, the fetched tips can be |
| * served from the constructor (i.e. by calling {@link #setTips(List)}), making |
| * them available immediately |
| * |
| * @param monitor |
| * The monitor to report back progress. |
| * @return the status in case you want to report problems. |
| * @see TipProvider#setTips(List) |
| * @see TipProvider#isReady() |
| */ |
| public abstract IStatus loadNewTips(IProgressMonitor monitor); |
| |
| private synchronized Tip setCurrentTip(Tip pTip) { |
| fCurrentTip = pTip; |
| return fCurrentTip; |
| } |
| |
| /** |
| * Sets the TipManager. You should probably not call this method directly. This |
| * method is normally called after the provider is instantiated by the |
| * {@link ITipManager}. If you create the provider yourself you should register |
| * the provider with {@link ITipManager#register(TipProvider)} which in turn |
| * will call this method. Subclasses may override but must not forget to call |
| * super in order to save the {@link ITipManager}. |
| * |
| * @param tipManager |
| * the {@link ITipManager} |
| * @return this |
| */ |
| public synchronized TipProvider setManager(ITipManager tipManager) { |
| fTipManager = tipManager; |
| return this; |
| } |
| |
| /** |
| * Sets the tips for this provider, replacing the current set of tips, and sets |
| * the <code>ready</code> flag to true. This method is typically called from the |
| * constructor of the {@link TipProvider} but may also be called from the |
| * asynchronous {@link #loadNewTips(IProgressMonitor)} method. |
| * |
| * @param tips |
| * a list of {@link Tip} objects |
| * @return this |
| * @see #addTips(List) |
| * @see #isReady() |
| * @see #loadNewTips(IProgressMonitor) |
| */ |
| public TipProvider setTips(List<Tip> tips) { |
| if(getManager().isDisposed()) { |
| return this; |
| } |
| doSetTips(tips, true); |
| fReady = true; |
| fListenerManager.notifyListeners(TipProviderListener.EVENT_READY, this); |
| return this; |
| } |
| |
| /** |
| * Adds the passed tips to the set of tips this provider already has sets the |
| * <code>ready</code> flag to true. This method is typically called from the |
| * constructor of the {@link TipProvider} but may also be called from the |
| * asynchronous {@link #loadNewTips(IProgressMonitor)} method. |
| * |
| * @param tips |
| * a list of {@link Tip} objects |
| * @return this |
| * @see #setTips(List) |
| * @see #isReady() |
| * @see #loadNewTips(IProgressMonitor) |
| */ |
| public TipProvider addTips(List<Tip> tips) { |
| doSetTips(tips, false); |
| fReady = true; |
| fListenerManager.notifyListeners(TipProviderListener.EVENT_READY, this); |
| return this; |
| } |
| |
| private synchronized void doSetTips(List<Tip> tips, boolean replace) { |
| if (replace) { |
| fTips.clear(); |
| } |
| fTips.addAll(tips); |
| } |
| |
| /** |
| * Gets the listener manager so that interested parties can subscribe to the |
| * events of this provider. |
| * |
| * @return the {@link TipProviderListenerManager} |
| */ |
| public TipProviderListenerManager getListenerManager() { |
| return fListenerManager; |
| } |
| |
| /** |
| * Returns an expression that is used by the {@link ITipManager} to determine |
| * the priority of this provider. The expression can be used to advice the |
| * TipManager when the tips of this provider deserve priority. The Eclipse IDE |
| * TipManager uses the core expression from the o.e.core.runtime bundle. |
| * Example: The expression |
| * |
| * <pre> |
| * <with |
| * variable="activeWorkbenchWindow.activePerspective"> |
| * <equals value="org.eclipse.jdt.ui.JavaPerspective"></equals> |
| * </with> |
| * </pre> |
| * |
| * will give the provider priority when the java perspective is active in the |
| * IDE |
| * |
| * @return the expression which can be empty or null. |
| */ |
| public String getExpression() { |
| return fExpression; |
| } |
| |
| /** |
| * Sets the expression to determine the priority of the provider. |
| * |
| * @param expression |
| * the expression, may be null. |
| * |
| * @see #getExpression() |
| */ |
| public void setExpression(String expression) { |
| fExpression = expression; |
| } |
| } |