Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: a9f4193b4e8525890494d1c939fd502d4fdfaf27 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/****************************************************************************
 * Copyright (c) 2017, 2018 Remain Software
 *
 * 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:
 *    Wim Jongman <wim.jongman@remainsoftware.com> - initial API and implementation
 *****************************************************************************/
package org.eclipse.tips.core;

import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.tips.core.internal.FinalTip;
import org.eclipse.tips.core.internal.LogUtil;

/**
 * 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 {

	/**
	 * Ready property.
	 */
	public static final String PROP_READY = "PR"; //$NON-NLS-1$

	private ITipManager fTipManager;
	private int fTipIndex;
	private List<Tip> fTips = new ArrayList<>();
	private Tip fCurrentTip;
	private boolean fReady;
	private PropertyChangeSupport fChangeSupport = new PropertyChangeSupport(this);
	private Tip fFinalTip = new FinalTip(getID());
	private String fExpression;

	/**
	 * A predicate that tests if a tip is valid based on its read state and the
	 * requirement to only serve read tips. Subclasses may replace this predicate if
	 * they want to add some additional tests.
	 */
	private Predicate<Tip> fUnreadTipPredicate = pTip -> {
		if (getManager().mustServeReadTips()) {
			return true;
		}
		return !getManager().isRead(pTip);
	};

	/**
	 * 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();

	/**
	 * @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() {
		List<Tip> list = getTips(fUnreadTipPredicate);
		if (list.isEmpty()) {
			return setCurrentTip(fFinalTip);
		}
		if (getManager().mustServeReadTips() && 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(fUnreadTipPredicate);
		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;
	}

	/**
	 * A convenience method to get the list of tips based on the read status of the
	 * tip and the requirement to serve unread or all tips.
	 *
	 * @return the list of tips based on the description above
	 * @see ITipManager#mustServeReadTips()
	 * @see ITipManager#isRead(Tip)
	 */
	public List<Tip> getTips() {
		return getTips(fUnreadTipPredicate);
	}

	/**
	 * Get a list of tips filtered by the passed predicate.
	 *
	 * @param predicate a {@link Predicate} targeting a Tip object or null to return all tips
	 * @return an unmodifiable list of tips.
	 */
	public synchronized List<Tip> getTips(Predicate<Tip> predicate) {
		if (predicate != null) {
			return Collections.unmodifiableList(fTips //
					.stream() //
					.filter(tip -> predicate.test(tip)) //
					.sorted(Comparator.comparing(Tip::getCreationDate).reversed()) //
					.collect(Collectors.toList()));
		}
		return Collections.unmodifiableList(fTips);
	}

	/**
	 * 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().isOpen()) {
//			return this;
//		}
		getManager().log(LogUtil.info(Messages.TipProvider_0));
		doSetTips(tips, true);
		fReady = true;
		fChangeSupport.firePropertyChange(PROP_READY, false, true);
		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;
		fChangeSupport.firePropertyChange(PROP_READY, false, true);
		return this;
	}

	private synchronized void doSetTips(List<Tip> tips, boolean replace) {
		if (replace) {
			fTips.clear();
		}
		fTips.addAll(tips);
	}

	/**
	 * Gets the change support so that interested parties can subscribe to the
	 * property change events of this provider.
	 *
	 * @return the {@link PropertyChangeSupport}
	 */
	public PropertyChangeSupport getChangeSupport() {
		return fChangeSupport;
	}

	/**
	 * 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>
	 *  &lt;with
	 *     variable="activeWorkbenchWindow.activePerspective"&gt;
	 *         &lt;equals value="org.eclipse.jdt.ui.JavaPerspective"&gt;&lt;/equals&gt;
	 *  &lt;/with&gt;
	 * </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;
	}
}

Back to the top