|
1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. |
|
2 // Use of this source code is governed by a BSD-style license that can be |
|
3 // found in the LICENSE file. |
|
4 |
|
5 package org.chromium.debug.ui; |
|
6 |
|
7 import java.util.ArrayList; |
|
8 import java.util.Arrays; |
|
9 import java.util.Collections; |
|
10 import java.util.Comparator; |
|
11 import java.util.HashMap; |
|
12 import java.util.LinkedHashMap; |
|
13 import java.util.LinkedHashSet; |
|
14 import java.util.List; |
|
15 import java.util.Map; |
|
16 import java.util.Set; |
|
17 |
|
18 import org.eclipse.jface.dialogs.IMessageProvider; |
|
19 import org.eclipse.swt.events.SelectionListener; |
|
20 import org.eclipse.swt.widgets.Button; |
|
21 import org.eclipse.swt.widgets.Combo; |
|
22 import org.eclipse.swt.widgets.Shell; |
|
23 |
|
24 /** |
|
25 * A small set of utility classes that help programming dialog window logic. |
|
26 */ |
|
27 public class DialogUtils { |
|
28 /* |
|
29 * Part 1. Update graph. |
|
30 * |
|
31 * All logical elements of dialog and dependencies between them are modeled as DAG. |
|
32 * The data flows from vertices that corresponds to various input fields through |
|
33 * some transformations to the terminal vertices that are in-dialog helper messages and OK button. |
|
34 * |
|
35 * A linear data flow (no forks, no merges) is suitable for data transformation and |
|
36 * is programmed manually. Forks and merges are hard to dispatch manually and are managed by |
|
37 * class Updater. |
|
38 * |
|
39 * Updater knows about "source" vertices that may have several outgoing edges and about |
|
40 * "consumer" vertices that may have several incoming edges. Based on source vertex changes |
|
41 * updater updates consumer vertices in topological order. |
|
42 */ |
|
43 |
|
44 /** |
|
45 * Represents source vertex for Updater. Technically updater uses this interface only as a flag |
|
46 * interface, because the only methods it uses are {@link Object#equals}/Object{@link #hashCode}. |
|
47 */ |
|
48 public interface ValueSource<T> { |
|
49 /** |
|
50 * Method is not used by updater, for convenience only. |
|
51 * @return current value of the vertex |
|
52 */ |
|
53 T getValue(); |
|
54 } |
|
55 |
|
56 /** |
|
57 * Represents consumer vertex for Updater. Each instance should be explicitly registered in |
|
58 * Updater. |
|
59 */ |
|
60 public interface ValueConsumer { |
|
61 /** |
|
62 * Called by updater when some linked sources have changed and it's time this vertex |
|
63 * got updated. {@link Updater#reportChanged} may be called if some {@line ValueSource}s have |
|
64 * changed during this update (but this should not break topological order of the graph). |
|
65 */ |
|
66 void update(Updater updater); |
|
67 } |
|
68 |
|
69 /** |
|
70 * Helps to conduct update for vertices in a value graph. |
|
71 * Technically Updater does not see a real graph, because it doesn't support vertices |
|
72 * that are simultaneously source and consumer. Programmer helps manage other edges manually by |
|
73 * calling {@link #reportChanged} method. |
|
74 */ |
|
75 public static class Updater { |
|
76 private final LinkedHashMap<ValueConsumer, Boolean> needsUpdateMap = |
|
77 new LinkedHashMap<ValueConsumer, Boolean>(); |
|
78 private final Map<ValueSource<?>, List<ValueConsumer>> reversedDependenciesMap = |
|
79 new HashMap<ValueSource<?>, List<ValueConsumer>>(); |
|
80 private boolean alreadyUpdating = false; |
|
81 |
|
82 public void addConsumer(ValueConsumer value, ValueSource<?> ... dependencies) { |
|
83 addConsumer(value, Arrays.asList(dependencies)); |
|
84 } |
|
85 |
|
86 /** |
|
87 * Registers a consumer vertex with all its dependencies. |
|
88 */ |
|
89 public void addConsumer(ValueConsumer value, List<? extends ValueSource<?>> dependencies) { |
|
90 Boolean res = needsUpdateMap.put(value, Boolean.FALSE); |
|
91 if (res != null) { |
|
92 throw new IllegalArgumentException("Already added"); //$NON-NLS-1$ |
|
93 } |
|
94 for (ValueSource<?> dep : dependencies) { |
|
95 List<ValueConsumer> reversedDeps = reversedDependenciesMap.get(dep); |
|
96 if (reversedDeps == null) { |
|
97 reversedDeps = new ArrayList<ValueConsumer>(2); |
|
98 reversedDependenciesMap.put(dep, reversedDeps); |
|
99 } |
|
100 reversedDeps.add(value); |
|
101 } |
|
102 } |
|
103 |
|
104 /** |
|
105 * Reports about sources that have been changed and plans future update of consumers. This |
|
106 * method may be called at any time (it is not thread-safe though). |
|
107 */ |
|
108 public void reportChanged(ValueSource<?> source) { |
|
109 List<ValueConsumer> reversedDeps = reversedDependenciesMap.get(source); |
|
110 if (reversedDeps != null) { |
|
111 for (ValueConsumer consumer : reversedDeps) { |
|
112 needsUpdateMap.put(consumer, Boolean.TRUE); |
|
113 } |
|
114 } |
|
115 } |
|
116 |
|
117 /** |
|
118 * Performs update of all vertices that need it. If some sources are reported changed |
|
119 * during the run of this method, their consumers are also updated. |
|
120 */ |
|
121 public void update() { |
|
122 if (alreadyUpdating) { |
|
123 return; |
|
124 } |
|
125 alreadyUpdating = true; |
|
126 try { |
|
127 updateImpl(); |
|
128 } finally { |
|
129 alreadyUpdating = false; |
|
130 } |
|
131 } |
|
132 |
|
133 private void updateImpl() { |
|
134 boolean hasChanges = true; |
|
135 while (hasChanges) { |
|
136 hasChanges = false; |
|
137 for (Map.Entry<ValueConsumer, Boolean> en : needsUpdateMap.entrySet()) { |
|
138 if (en.getValue() == Boolean.TRUE) { |
|
139 en.setValue(Boolean.FALSE); |
|
140 ValueConsumer currentValue = en.getKey(); |
|
141 currentValue.update(this); |
|
142 } |
|
143 } |
|
144 } |
|
145 } |
|
146 |
|
147 /** |
|
148 * Updates all consumer vertices in graph. |
|
149 */ |
|
150 public void updateAll() { |
|
151 for (Map.Entry<?, Boolean> en : needsUpdateMap.entrySet()) { |
|
152 en.setValue(Boolean.TRUE); |
|
153 } |
|
154 update(); |
|
155 } |
|
156 } |
|
157 |
|
158 /** |
|
159 * A basic implementation of object that is both consumer and source. Updater will treat |
|
160 * as 2 separate objects. |
|
161 */ |
|
162 public static abstract class ValueProcessor<T> implements ValueConsumer, ValueSource<T> { |
|
163 private T currentValue = null; |
|
164 public T getValue() { |
|
165 return currentValue; |
|
166 } |
|
167 protected void setCurrentValue(T currentValue) { |
|
168 this.currentValue = currentValue; |
|
169 } |
|
170 } |
|
171 |
|
172 /* |
|
173 * Part 2. Optional data type etc |
|
174 * |
|
175 * Since dialog should deal with error user entry, the typical data type is either value or error. |
|
176 * This is implemented as Optional interface. Most of data transformations should work only |
|
177 * when all inputs are non-error and generate error in return otherwise. This is implemented in |
|
178 * ExpressionProcessor. |
|
179 */ |
|
180 |
|
181 |
|
182 /** |
|
183 * A primitive approach to "optional" algebraic type. This type is T + Set<Message>. |
|
184 */ |
|
185 public interface Optional<V> { |
|
186 V getNormal(); |
|
187 boolean isNormal(); |
|
188 Set<? extends Message> errorMessages(); |
|
189 } |
|
190 |
|
191 public static <V> Optional<V> createOptional(final V value) { |
|
192 return new Optional<V>() { |
|
193 public Set<Message> errorMessages() { |
|
194 return Collections.emptySet(); |
|
195 } |
|
196 public V getNormal() { |
|
197 return value; |
|
198 } |
|
199 public boolean isNormal() { |
|
200 return true; |
|
201 } |
|
202 }; |
|
203 } |
|
204 |
|
205 public static <V> Optional<V> createErrorOptional(Message message) { |
|
206 return createErrorOptional(Collections.singleton(message)); |
|
207 } |
|
208 |
|
209 public static <V> Optional<V> createErrorOptional(final Set<? extends Message> messages) { |
|
210 return new Optional<V>() { |
|
211 public Set<? extends Message> errorMessages() { |
|
212 return messages; |
|
213 } |
|
214 public V getNormal() { |
|
215 throw new UnsupportedOperationException(); |
|
216 } |
|
217 public boolean isNormal() { |
|
218 return false; |
|
219 } |
|
220 }; |
|
221 } |
|
222 |
|
223 /** |
|
224 * A user interface message for dialog window. It has text and priority that helps choosing |
|
225 * the most important message it there are many of them. |
|
226 */ |
|
227 public static class Message { |
|
228 private final String text; |
|
229 private final MessagePriority priority; |
|
230 public Message(String text, MessagePriority priority) { |
|
231 this.text = text; |
|
232 this.priority = priority; |
|
233 } |
|
234 public String getText() { |
|
235 return text; |
|
236 } |
|
237 public MessagePriority getPriority() { |
|
238 return priority; |
|
239 } |
|
240 } |
|
241 |
|
242 /** |
|
243 * Priority of a user interface message. |
|
244 * Constants are listed from most important to least important. |
|
245 */ |
|
246 public enum MessagePriority { |
|
247 BLOCKING_PROBLEM(IMessageProvider.ERROR), |
|
248 BLOCKING_INFO(IMessageProvider.NONE), |
|
249 WARNING(IMessageProvider.WARNING); |
|
250 |
|
251 private final int messageProviderType; |
|
252 private MessagePriority(int messageProviderType) { |
|
253 this.messageProviderType = messageProviderType; |
|
254 } |
|
255 public int getMessageProviderType() { |
|
256 return messageProviderType; |
|
257 } |
|
258 } |
|
259 |
|
260 /** |
|
261 * A base class for the source-consumer pair that accepts several values as a consumer, |
|
262 * performs a calculation over them and gives it away the result via source interface. |
|
263 * Some sources may be of Optional type. If some of sources have error value the corresponding |
|
264 * error value is returned automatically. |
|
265 * <p> |
|
266 * The implementation should override a single method {@link #calculateNormal}. |
|
267 */ |
|
268 public static abstract class ExpressionProcessor<T> extends ValueProcessor<Optional<T>> { |
|
269 private final List<ValueSource<? extends Optional<?>>> optionalSources; |
|
270 public ExpressionProcessor(List<ValueSource<? extends Optional<?>>> optionalSources) { |
|
271 this.optionalSources = optionalSources; |
|
272 } |
|
273 |
|
274 protected abstract Optional<T> calculateNormal(); |
|
275 |
|
276 private Optional<T> calculateNewValue() { |
|
277 Set<Message> errors = new LinkedHashSet<Message>(0); |
|
278 for (ValueSource<? extends Optional<?>> source : optionalSources) { |
|
279 if (!source.getValue().isNormal()) { |
|
280 errors.addAll(source.getValue().errorMessages()); |
|
281 } |
|
282 } |
|
283 if (errors.isEmpty()) { |
|
284 return calculateNormal(); |
|
285 } else { |
|
286 return createErrorOptional(errors); |
|
287 } |
|
288 } |
|
289 public void update(Updater updater) { |
|
290 Optional<T> result = calculateNewValue(); |
|
291 Optional<T> oldValue = getValue(); |
|
292 setCurrentValue(result); |
|
293 if (!result.equals(oldValue)) { |
|
294 updater.reportChanged(this); |
|
295 } |
|
296 } |
|
297 } |
|
298 |
|
299 /* |
|
300 * Part 3. Various utils. |
|
301 */ |
|
302 |
|
303 /** |
|
304 * A general-purpose implementation of OK button vertex. It works as a consumer of |
|
305 * 1 result value and several warning sources. From its sources it decides whether |
|
306 * OK button should be enabled and also provides dialog messages (errors, warnings, infos). |
|
307 */ |
|
308 public static class OkButtonControl implements ValueConsumer { |
|
309 private final ValueSource<? extends Optional<?>> resultSource; |
|
310 private final List<? extends ValueSource<String>> warningSources; |
|
311 private final DialogElements dialogElements; |
|
312 |
|
313 public OkButtonControl(ValueSource<? extends Optional<?>> resultSource, |
|
314 List<? extends ValueSource<String>> warningSources, DialogElements dialogElements) { |
|
315 this.resultSource = resultSource; |
|
316 this.warningSources = warningSources; |
|
317 this.dialogElements = dialogElements; |
|
318 } |
|
319 |
|
320 /** |
|
321 * Returns a list of dependencies for updater -- a convenience method. |
|
322 */ |
|
323 public List<? extends ValueSource<?>> getDependencies() { |
|
324 ArrayList<ValueSource<?>> result = new ArrayList<ValueSource<?>>(); |
|
325 result.add(resultSource); |
|
326 result.addAll(warningSources); |
|
327 return result; |
|
328 } |
|
329 |
|
330 public void update(Updater updater) { |
|
331 Optional<?> result = resultSource.getValue(); |
|
332 List<Message> messages = new ArrayList<Message>(); |
|
333 for (ValueSource<String> warningSource : warningSources) { |
|
334 if (warningSource.getValue() != null) { |
|
335 messages.add(new Message(warningSource.getValue(), MessagePriority.WARNING)); |
|
336 } |
|
337 } |
|
338 boolean enabled; |
|
339 if (result.isNormal()) { |
|
340 enabled = true; |
|
341 } else { |
|
342 enabled = false; |
|
343 messages.addAll(result.errorMessages()); |
|
344 } |
|
345 dialogElements.getOkButton().setEnabled(enabled); |
|
346 String errorMessage; |
|
347 int type; |
|
348 if (messages.isEmpty()) { |
|
349 errorMessage = null; |
|
350 type = IMessageProvider.NONE; |
|
351 } else { |
|
352 Message visibleMessage = Collections.max(messages, messageComparatorBySeverity); |
|
353 errorMessage = visibleMessage.getText(); |
|
354 type = visibleMessage.getPriority().getMessageProviderType(); |
|
355 } |
|
356 dialogElements.setMessage(errorMessage, type); |
|
357 } |
|
358 |
|
359 private static final Comparator<Message> messageComparatorBySeverity = |
|
360 new Comparator<Message>() { |
|
361 public int compare(Message o1, Message o2) { |
|
362 int ordinal1 = o1.getPriority().ordinal(); |
|
363 int ordinal2 = o2.getPriority().ordinal(); |
|
364 if (ordinal1 < ordinal2) { |
|
365 return +1; |
|
366 } else if (ordinal1 == ordinal2) { |
|
367 return 0; |
|
368 } else { |
|
369 return -1; |
|
370 } |
|
371 } |
|
372 }; |
|
373 } |
|
374 |
|
375 /** |
|
376 * A basic interface to elements of the dialog window from dialog logic part. The user may extend |
|
377 * this interface with more elements. |
|
378 */ |
|
379 public interface DialogElements { |
|
380 Shell getShell(); |
|
381 Button getOkButton(); |
|
382 void setMessage(String message, int type); |
|
383 } |
|
384 |
|
385 /** |
|
386 * A wrapper around Combo that provides logic-level data-oriented access to the control. |
|
387 * This is not a simply convenience wrapper, because Combo itself does not keep a real data, |
|
388 * but only its string representation. |
|
389 */ |
|
390 public static abstract class ComboWrapper<E> { |
|
391 private final Combo combo; |
|
392 public ComboWrapper(Combo combo) { |
|
393 this.combo = combo; |
|
394 } |
|
395 public Combo getCombo() { |
|
396 return combo; |
|
397 } |
|
398 public void addSelectionListener(SelectionListener listener) { |
|
399 combo.addSelectionListener(listener); |
|
400 } |
|
401 public abstract E getSelected(); |
|
402 public abstract void setSelected(E element); |
|
403 } |
|
404 } |