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