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>&lt;a href="http://www.yworks.com/products/yfiles"&gt;yFiles for Java&lt;/a&gt;</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>&lt;a href="graph://yfilesforjava"&gt;yFiles for Java&lt;/a&gt;</code>
62   * </blockquote>
63   * will navigate to the first node in the graph that has a corresponding
64   * <blockquote>
65   * <code>&lt;a name="yfilesforjava"&gt;&lt;/a&gt;</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