| HyperlinkDemo.java |
1 /****************************************************************************
2 **
3 ** This file is part of yFiles-2.9.
4 **
5 ** yWorks proprietary/confidential. Use is subject to license terms.
6 **
7 ** Redistribution of this file or of an unauthorized byte-code version
8 ** of this file is strictly forbidden.
9 **
10 ** Copyright (c) 2000-2011 by yWorks GmbH, Vor dem Kreuzberg 28,
11 ** 72070 Tuebingen, Germany. All rights reserved.
12 **
13 ***************************************************************************/
14 package demo.view.viewmode;
15
16 import demo.view.DemoBase;
17 import y.base.Node;
18 import y.base.NodeCursor;
19 import y.geom.YRectangle;
20 import y.view.EditMode;
21 import y.view.Graph2D;
22 import y.view.Graph2DTraversal;
23 import y.view.Graph2DView;
24 import y.view.HitInfo;
25 import y.view.HtmlLabelConfiguration;
26 import y.view.Mouse2DEvent;
27 import y.view.NodeLabel;
28 import y.view.NodeRealizer;
29 import y.view.TooltipMode;
30 import y.view.YLabel;
31
32 import java.awt.Cursor;
33 import java.awt.EventQueue;
34 import java.awt.Point;
35 import java.awt.RenderingHints;
36 import java.awt.event.MouseEvent;
37 import java.awt.geom.Point2D;
38 import java.net.URL;
39 import java.util.Locale;
40 import java.util.Map;
41 import javax.swing.JDialog;
42 import javax.swing.JOptionPane;
43 import javax.swing.event.HyperlinkEvent;
44 import javax.swing.event.HyperlinkListener;
45
46 /**
47 * Demonstrates how to use {@link HtmlLabelConfiguration} to trigger and process
48 * hyperlink events with HTML formatted label text.
49 * <p>
50 * When clicking on an external link such as
51 * <blockquote>
52 * <code><a href="http://www.yworks.com/products/yfiles">yFiles for Java</a></code>,
53 * </blockquote>
54 * a dialog is opened that displays the link's destination in response to the
55 * generated hyperlink event.
56 * </p>
57 * <p>
58 * Additionally, a custom protocol <code>graph</code> is used to allow
59 * in-graph navigation. E.g. clicking on
60 * <blockquote>
61 * <code><a href="graph://yfilesforjava">yFiles for Java</a></code>
62 * </blockquote>
63 * will navigate to the first node in the graph that has a corresponding
64 * <blockquote>
65 * <code><a name="yfilesforjava"></a></code>
66 * </blockquote>
67 * declaration in its label text.
68 * </p>
69 * @see <a href="http://docs.yworks.com/yfiles/doc/developers-guide/realizer_related.html#labels_config_hyperlink">Section Realizer-Related Features</a> in the yFiles for Java Developer's Guide
70 */
71 public class HyperlinkDemo extends DemoBase {
72 private static final String HTML_LABEL_CONFIG = "HtmlLabel";
73 private boolean fractionMetricsForSizeCalculationEnabled;
74
75 public HyperlinkDemo() {
76 loadInitialGraph();
77 }
78
79 protected void loadInitialGraph() {
80 loadGraph("resource/HyperlinkDemo.graphml");
81 }
82
83 protected void initialize() {
84 super.initialize();
85
86 // Ensures that text always fits into label bounds independent of zoom level.
87 // Stores the value to be able to reset it when running the demo in the DemoBrowser,
88 // so this setting cannot effect other demos.
89 fractionMetricsForSizeCalculationEnabled = YLabel.isFractionMetricsForSizeCalculationEnabled();
90 YLabel.setFractionMetricsForSizeCalculationEnabled(true);
91 view.getRenderingHints().put(
92 RenderingHints.KEY_FRACTIONALMETRICS,
93 RenderingHints.VALUE_FRACTIONALMETRICS_ON);
94 }
95
96 /**
97 * Cleans up.
98 * This method is called by the demo browser when the demo is stopped or another demo starts.
99 */
100 public void dispose() {
101 YLabel.setFractionMetricsForSizeCalculationEnabled(fractionMetricsForSizeCalculationEnabled);
102 }
103
104 /**
105 * Overwritten to register a label configuration for HTML formatted label
106 * text.
107 */
108 protected void configureDefaultRealizers() {
109 final YLabel.Factory f = NodeLabel.getFactory();
110 final HtmlLabelConfiguration impl = new HtmlLabelConfiguration();
111 final Map impls = f.createDefaultConfigurationMap();
112 impls.put(YLabel.Painter.class, impl);
113 impls.put(YLabel.Layout.class, impl);
114 impls.put(YLabel.BoundsProvider.class, impl);
115 f.addConfiguration(HTML_LABEL_CONFIG, impls);
116
117 super.configureDefaultRealizers();
118 }
119
120 /**
121 * Overwritten to create an edit mode that triggers and processes hyperlink
122 * events for labels.
123 * @return a {@link HyperlinkEditMode} instance.
124 */
125 protected EditMode createEditMode() {
126 return new HyperlinkEditMode();
127 }
128
129 /**
130 * Overwritten to disable tooltips.
131 */
132 protected TooltipMode createTooltipMode() {
133 return null;
134 }
135
136 public static void main( String[] args ) {
137 EventQueue.invokeLater(new Runnable() {
138 public void run() {
139 Locale.setDefault(Locale.ENGLISH);
140 initLnF();
141 (new HyperlinkDemo()).start();
142 }
143 });
144 }
145
146 /**
147 * Triggers hyperlink events for mouse moved and mouse clicked events that
148 * occur for hyperlinks in HTML formatted label text.
149 */
150 private static final class HyperlinkEditMode extends EditMode {
151 HyperlinkEditMode() {
152 allowResizeNodes(false);
153 allowNodeCreation(false);
154 }
155
156 /**
157 * Processes the specified hyperlink event.
158 * @param e the hyperlink event to process.
159 */
160 protected void hyperlinkUpdate( final HyperlinkEvent e ) {
161 (new EventHandler(view)).hyperlinkUpdate(e);
162 }
163
164 /**
165 * Checks whether or not the node click actually occurred on one of the
166 * node's label. If that is the case and the label uses a HTML configuration
167 * for measuring and rendering its content, the configuration's
168 * <code>handleLabelEvent</code> method is used to trigger a corresponding
169 * hyperlink event.
170 * @see HtmlLabelConfiguration#handleLabelEvent(y.view.YLabel, y.view.Mouse2DEvent, javax.swing.event.HyperlinkListener)
171 */
172 protected void nodeClicked(
173 final Graph2D graph,
174 final Node node,
175 final boolean wasSelected,
176 final double x,
177 final double y,
178 final boolean modifierSet
179 ) {
180 final NodeRealizer nr = graph.getRealizer(node);
181 if (nr.labelCount() > 0) {
182 for (int i = nr.labelCount(); i --> 0;) {
183 final NodeLabel nl = nr.getLabel(i);
184 if (nl.contains(x, y)) {
185 if (labelClickedImpl(nl, x, y)) {
186 return;
187 }
188 }
189 }
190 }
191 super.nodeClicked(graph, node, wasSelected, x, y, modifierSet);
192 }
193
194 /**
195 * Triggers hyperlink events for the specified label.
196 */
197 protected void labelClicked(
198 final Graph2D graph,
199 final YLabel label,
200 final boolean wasSelected,
201 final double x, final double y,
202 final boolean modifierSet
203 ) {
204 if (labelClickedImpl(label, x, y)) {
205 return;
206 }
207 super.labelClicked(graph, label, wasSelected, x, y, modifierSet);
208 }
209
210 /**
211 * Synthesizes mouse events that may trigger hyperlink events for the
212 * specified label.
213 * @param label the label that has been clicked upon.
214 * @param x the x-component of the mouse click's <em>world</em> coordinate.
215 * @param y the y-component of the mouse click's <em>world</em> coordinate.
216 * @return <code>true</code> if the click triggered a hyperlink event;
217 * <code>false</code> otherwise.
218 */
219 private boolean labelClickedImpl(
220 final YLabel label,
221 final double x, final double y
222 ) {
223 if (HTML_LABEL_CONFIG.equals(label.getConfiguration())) {
224 final HtmlLabelConfiguration htmlSupport = getHtmlConfiguration();
225 final EventHolder callback = new EventHolder();
226 // a "real" mouse click always results in a pressed, a released, and a
227 // clicked event
228 // because handleLabelEvent relies on the JComponent used for rendering
229 // the label to trigger hyperlink event, the same event sequence
230 // that would occur for a "real" mouse click is used as there is
231 // no way to know whether the JComponent reacts to released or to
232 // clicked events
233 // e.g. using JEditorPane, the default HTMLEditorKit will fire
234 // a hyperlink activated event in response to a mouse clicked event
235 // however, JWebEngine's com.inet.html.InetHtmlEditorKit fires
236 // hyperlink activated events in response to mouse release events
237 htmlSupport.handleLabelEvent(
238 label,
239 createEvent(x, y, lastReleaseEvent, MouseEvent.MOUSE_PRESSED),
240 null);
241 htmlSupport.handleLabelEvent(
242 label,
243 createEvent(x, y, lastReleaseEvent, MouseEvent.MOUSE_RELEASED),
244 callback);
245 htmlSupport.handleLabelEvent(
246 label,
247 createEvent(x, y, lastReleaseEvent, MouseEvent.MOUSE_CLICKED),
248 callback);
249 final HyperlinkEvent e = callback.getEvent();
250 if (e != null) {
251 hyperlinkUpdate(e);
252 return true;
253 }
254 }
255 return false;
256 }
257
258
259 /**
260 * Checks whether or not the mouse was moved over a hyperlink in the
261 * HTML formatted text of a label.
262 * @param x the x-component of the mouse event's world coordinate.
263 * @param y the y-component of the mouse event's world coordinate.
264 */
265 public void mouseMoved( final double x, final double y ) {
266 // first check whether or not the event happened over a label
267 final HitInfo info = view.getHitInfoFactory().createHitInfo(
268 x, y, Graph2DTraversal.NODE_LABELS, true);
269 if (info.hasHitNodeLabels()) {
270 final NodeLabel label = info.getHitNodeLabel();
271 // now check whether the label uses a HTML configuration
272 if (HTML_LABEL_CONFIG.equals(label.getConfiguration())) {
273 final HtmlLabelConfiguration htmlSupport = getHtmlConfiguration();
274 final EventHolder callback = new EventHolder();
275 // finally check whether or not the mouse moved into or out of
276 // a hyperlink
277 htmlSupport.handleLabelEvent(
278 label,
279 createEvent(x, y, lastMoveEvent, MouseEvent.MOUSE_MOVED),
280 callback);
281 final HyperlinkEvent e = callback.getEvent();
282 if (e != null) {
283 if (e.getEventType() == HyperlinkEvent.EventType.ENTERED) {
284 // change the cursor to let the user know that something will
285 // happen if the mouse is clicked at the current location
286 view.setViewCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
287 } else if (e.getEventType() == HyperlinkEvent.EventType.EXITED) {
288 view.setViewCursor(Cursor.getDefaultCursor());
289 }
290 }
291 return;
292 }
293 }
294
295 super.mouseMoved(x, y);
296 }
297
298
299 /**
300 * Creates a <code>Mouse2DEvent</code> for the specified world coordinates
301 * and triggering mouse event.
302 * @param x the x-component of the event's world coordinate.
303 * @param y the y-component of the event's world coordinate.
304 * @param e the triggering mouse event.
305 * @param id the type of <code>Mouse2DEvent</code> to create.
306 * @return a <code>Mouse2DEvent</code> for the specified world coordinates
307 * and triggering mouse event.
308 */
309 private Mouse2DEvent createEvent(
310 final double x, final double y,
311 final MouseEvent e,
312 final int id
313 ) {
314 return new Mouse2DEvent(
315 e.getSource(),
316 this,
317 id,
318 e.getWhen(),
319 e.getModifiersEx(),
320 x,
321 y,
322 e.getButton(),
323 e.getClickCount(),
324 e.isPopupTrigger());
325 }
326
327 private static HtmlLabelConfiguration getHtmlConfiguration() {
328 return (HtmlLabelConfiguration) NodeLabel.getFactory().getImplementation(
329 HTML_LABEL_CONFIG, YLabel.Painter.class);
330 }
331 }
332
333 /**
334 * Caches hyperlink events.
335 */
336 private static class EventHolder implements HyperlinkListener {
337 private HyperlinkEvent event;
338
339 /**
340 * Caches the specified hyperlink event.
341 * @param e the event to cache.
342 */
343 public void hyperlinkUpdate( final HyperlinkEvent e ) {
344 event = e;
345 }
346
347 /**
348 * Returns the cached event.
349 * @return the cached event.
350 */
351 HyperlinkEvent getEvent() {
352 return event;
353 }
354 }
355
356 /**
357 * Processes {@link HtmlLabelConfiguration.LabelHyperlinkEvent}s.
358 */
359 private static class EventHandler implements HyperlinkListener {
360 private final Graph2DView view;
361
362 EventHandler( final Graph2DView view ) {
363 this.view = view;
364 }
365
366 /**
367 * Processes the specified hyperlink event.
368 * @param e the hyperlink event to process.
369 */
370 public void hyperlinkUpdate( final HyperlinkEvent e ) {
371 // determine if it is a label hyperlink event
372 if (e instanceof HtmlLabelConfiguration.LabelHyperlinkEvent) {
373 // determine if the event is triggered from a link that uses the demo's
374 // custom "graph" protocol that can be used to navigate the current
375 // graph
376 if (isGraphNavigationEvent(e)) {
377 navigateTo((HtmlLabelConfiguration.LabelHyperlinkEvent) e);
378 } else {
379 displayExternalLink((HtmlLabelConfiguration.LabelHyperlinkEvent) e);
380 }
381 }
382 }
383
384 /**
385 * Determines whether or not the specified event is a
386 * <em>graph navigation</em> event, that is whether or not the event's link
387 * uses the demo's custom <code>graph</code> protocol.
388 * @param e the event to check.
389 * @return <code>true</code> the event's link uses the demo's custom
390 * <code>graph</code> protocol; <code>false</code> otherwise.
391 */
392 private boolean isGraphNavigationEvent( final HyperlinkEvent e ) {
393 final URL url = e.getURL();
394 if (url == null) {
395 final String desc = e.getDescription();
396 return desc != null && desc.startsWith("graph://");
397 } else {
398 return "graph".equals(url.getProtocol());
399 }
400 }
401
402 /**
403 * Displays a dialog with the specified event's hyperlink destination.
404 * @param e the event whose hyperlink destination has to be displayed.
405 */
406 private void displayExternalLink( final HtmlLabelConfiguration.LabelHyperlinkEvent e ) {
407 final YLabel label = e.getLabel();
408 final YRectangle lbox = label.getBox();
409 final Point l = view.getLocationOnScreen();
410 final int vx = l.x + view.toViewCoordX(lbox.getX());
411 final int vy = l.y + view.toViewCoordY(lbox.getY() + lbox.getHeight());
412
413 final String title = "External Link";
414 final String message =
415 title +
416 "\nHref: " + e.getDescription();
417 final JOptionPane jop = new JOptionPane(message, JOptionPane.INFORMATION_MESSAGE);
418 final JDialog jd = jop.createDialog(view, title);
419 jd.setLocation(vx, vy);
420 jd.setVisible(true);
421 }
422
423 /**
424 * Navigates to the node that is referenced in the specified event's
425 * hyperlink destination.
426 * @param e a hyperlink event whose hyperlink uses the demo's customs
427 * <code>graph</code> protocol.
428 */
429 private void navigateTo( final HtmlLabelConfiguration.LabelHyperlinkEvent e ) {
430 final String destination;
431 final URL url = e.getURL();
432 if (url == null) {
433 destination = e.getDescription().substring(8);
434 } else {
435 destination = url.getPath();
436 }
437
438 // search for a node that has an anchor which corresponds to the
439 // desired destination
440 final Graph2D g = view.getGraph2D();
441 for (NodeCursor nc = g.nodes(); nc.ok(); nc.next()) {
442 final NodeRealizer nr = g.getRealizer(nc.node());
443 if (nr.labelCount() > 0) {
444 final String s = nr.getLabelText();
445 if (s.indexOf("<a name=\"" + destination + "\">") > -1) {
446 navigateTo(nr);
447 break;
448 }
449 }
450 }
451 }
452
453 /**
454 * Focuses the specified node context in the demo's graph view.
455 * @param realizer the node context.
456 */
457 private void navigateTo( final NodeRealizer realizer ) {
458 view.setViewCursor(Cursor.getDefaultCursor());
459 final double z = view.getZoom();
460 final double cx = realizer.getCenterX();
461 final double cy = realizer.getCenterY();
462 view.focusView(z, new Point2D.Double(cx, cy), true);
463 }
464 }
465 }
466