| HyperlinkDemo.java |
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><a href="http://www.yworks.com/products/yfiles">yFiles for Java</a></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><a href="graph://yfilesforjava">yFiles for Java</a></code>
76 * </blockquote>
77 * will navigate to the first node in the graph that has a corresponding
78 * <blockquote>
79 * <code><a name="yfilesforjava"></a></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