Skip to content

Commit 58c8fbd

Browse files
author
nicolaiparlog
committed
[!] Adapted the WebView hyperlink listening to implement 'ListenerHandle'.
ATTENTION: This revision makes [b6c35e5] compile again.
1 parent 547580a commit 58c8fbd

File tree

5 files changed

+364
-276
lines changed

5 files changed

+364
-276
lines changed

src/demo/java/org/codefx/libfx/control/webview/WebViewHyperlinkListenerDemo.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import javax.swing.event.HyperlinkEvent;
1414
import javax.swing.event.HyperlinkEvent.EventType;
1515

16+
import org.codefx.libfx.listener.ListenerHandle;
17+
1618
/**
1719
* Demonstrates how to use the {@link WebViewHyperlinkListener}.
1820
*/
@@ -110,7 +112,7 @@ private static CheckBox createCancelEventBox() {
110112
private static void manageListener(WebView webView, WebViewHyperlinkListener listener,
111113
BooleanProperty attachedProperty) {
112114
attachedProperty.set(true);
113-
WebViewHyperlinkListenerHandle listenerHandle = WebViews.addHyperlinkListener(webView, listener);
115+
ListenerHandle listenerHandle = WebViews.addHyperlinkListener(webView, listener);
114116

115117
attachedProperty.addListener((obs, wasAttached, isAttached) -> {
116118
if (isAttached) {
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package org.codefx.libfx.control.webview;
2+
3+
import java.util.Optional;
4+
import java.util.function.BiConsumer;
5+
import java.util.function.Consumer;
6+
import java.util.stream.Stream;
7+
8+
import javafx.application.Platform;
9+
import javafx.beans.value.ObservableValue;
10+
import javafx.concurrent.Worker.State;
11+
import javafx.scene.web.WebView;
12+
13+
import javax.swing.event.HyperlinkEvent;
14+
import javax.swing.event.HyperlinkEvent.EventType;
15+
16+
import org.codefx.libfx.concurrent.when.ExecuteAlwaysWhen;
17+
import org.codefx.libfx.concurrent.when.ExecuteWhen;
18+
import org.codefx.libfx.dom.DomEventType;
19+
import org.codefx.libfx.dom.EventTransformer;
20+
import org.w3c.dom.NodeList;
21+
import org.w3c.dom.events.Event;
22+
import org.w3c.dom.events.EventListener;
23+
import org.w3c.dom.events.EventTarget;
24+
25+
/**
26+
* A default implementation of {@link WebViewHyperlinkListenerHandle} which acts on the {@link WebView} and
27+
* {@link WebViewHyperlinkListener} specified during construction.
28+
*/
29+
class DefaultWebViewHyperlinkListenerHandle implements WebViewHyperlinkListenerHandle {
30+
31+
// Inspired by :
32+
// - http://stackoverflow.com/q/17555937 -
33+
// - http://blogs.kiyut.com/tonny/2013/07/30/javafx-webview-addhyperlinklistener/
34+
35+
/*
36+
* Many type names do not allow to easily recognize whether they come from the DOM- or the JavaFX-packages. To make
37+
* it easier, all instances of org.w3c.dom classes carry a 'dom'-prefix.
38+
*/
39+
40+
// #region ATTRIBUTES
41+
42+
/**
43+
* The {@link WebView} to which the {@link #domEventListener} will be attached.
44+
*/
45+
private final WebView webView;
46+
47+
/**
48+
* The {@link WebViewHyperlinkListener} which will be called by {@link #domEventListener} when an event occurs.
49+
*/
50+
private final WebViewHyperlinkListener eventListener;
51+
52+
/**
53+
* The filter for events by their {@link EventType}. If the filter is empty, all events will be processed. Otherwise
54+
* only events of the present type will be processed.
55+
*/
56+
private final Optional<EventType> eventTypeFilter;
57+
58+
/**
59+
* The DOM-{@link EventListener} which will be attached to the {@link #webView}.
60+
*/
61+
private final EventListener domEventListener;
62+
63+
/**
64+
* Transforms DOM {@link Event}s to {@link HyperlinkEvent}s.
65+
*/
66+
private final EventTransformer eventTransformer;
67+
68+
/**
69+
* Executes {@link #attachListenerInApplicationThread()} each time the web view's load worker changes its state to
70+
* {@link State#SUCCEEDED SUCCEEDED}.
71+
* <p>
72+
* The executer is only present while the listener is attached.
73+
*/
74+
private Optional<ExecuteAlwaysWhen<State>> attachWhenLoadSucceeds;
75+
76+
// #end ATTRIBUTES
77+
78+
/**
79+
* Creates a new listener handle for the specified arguments.
80+
*
81+
* @param webView
82+
* the {@link WebView} to which the {@code eventListener} will be attached
83+
* @param eventListener
84+
* the {@link WebViewHyperlinkListener} which will be attached to the {@code webView}
85+
* @param eventTypeFilter
86+
* the filter for events by their {@link EventType}
87+
* @param eventTransformer
88+
* the transformer for DOM {@link Event}s
89+
*/
90+
public DefaultWebViewHyperlinkListenerHandle(
91+
WebView webView, WebViewHyperlinkListener eventListener, Optional<EventType> eventTypeFilter,
92+
EventTransformer eventTransformer) {
93+
94+
this.webView = webView;
95+
this.eventListener = eventListener;
96+
this.eventTypeFilter = eventTypeFilter;
97+
this.eventTransformer = eventTransformer;
98+
99+
domEventListener = this::callHyperlinkListenerWithEvent;
100+
}
101+
102+
// #region ATTACH
103+
104+
@Override
105+
public void attach() {
106+
if (Platform.isFxApplicationThread())
107+
attachInApplicationThreadEachTimeLoadSucceeds();
108+
else
109+
Platform.runLater(() -> attachInApplicationThreadEachTimeLoadSucceeds());
110+
}
111+
112+
/**
113+
* Attaches the {@link #domEventListener} to the {@link #webView} every time the {@code webView} successfully loaded
114+
* a page.
115+
* <p>
116+
* Must be called in JavaFX application thread.
117+
*/
118+
private void attachInApplicationThreadEachTimeLoadSucceeds() {
119+
ObservableValue<State> webWorkerState = webView.getEngine().getLoadWorker().stateProperty();
120+
121+
ExecuteAlwaysWhen<State> attachWhenLoadSucceeds = ExecuteWhen
122+
.on(webWorkerState)
123+
.when(state -> state == State.SUCCEEDED)
124+
.thenAlways(state -> attachListenerInApplicationThread());
125+
this.attachWhenLoadSucceeds = Optional.of(attachWhenLoadSucceeds);
126+
127+
attachWhenLoadSucceeds.executeWhen();
128+
}
129+
130+
/**
131+
* Attaches the {@link #domEventListener} to the {@link #webView}.
132+
* <p>
133+
* Must be called in JavaFX application thread.
134+
*/
135+
private void attachListenerInApplicationThread() {
136+
BiConsumer<EventTarget, String> addListener =
137+
(eventTarget, eventType) -> eventTarget.addEventListener(eventType, domEventListener, false);
138+
onEachLinkForEachManagedEventType(addListener);
139+
}
140+
141+
// #end ATTACH
142+
143+
// #region DETACH
144+
145+
@Override
146+
public void detach() {
147+
if (Platform.isFxApplicationThread())
148+
detachInApplicationThread();
149+
else
150+
Platform.runLater(() -> detachInApplicationThread());
151+
}
152+
153+
/**
154+
* Detaches the {@link #domEventListener} from the {@link #webView} and cancels and resets
155+
* {@link #attachWhenLoadSucceeds}.
156+
* <p>
157+
* Must be called in JavaFX application thread.
158+
*/
159+
private void detachInApplicationThread() {
160+
attachWhenLoadSucceeds.ifPresent(attachWhen -> attachWhen.cancel());
161+
attachWhenLoadSucceeds = Optional.empty();
162+
163+
// it suffices to remove the listener if the worker state is on SUCCEEDED;
164+
// because when the view is currently loading, the canceled 'attachWhen' will not re-add the listener
165+
State webWorkerState = webView.getEngine().getLoadWorker().getState();
166+
if (webWorkerState == State.SUCCEEDED) {
167+
BiConsumer<EventTarget, String> removeListener =
168+
(eventTarget, eventType) -> eventTarget.removeEventListener(eventType, domEventListener, false);
169+
onEachLinkForEachManagedEventType(removeListener);
170+
}
171+
}
172+
173+
// #end DETACH
174+
175+
// #region COMMON MANAGEMENT METHODS
176+
177+
/**
178+
* Executes the specified function on each link in the {@link #webView}'s current document for each
179+
* {@link DomEventType} for which {@link #manageListenerForEventType(DomEventType)} returns true.
180+
* <p>
181+
* Must be called in JavaFX application thread.
182+
*
183+
* @param manageListener
184+
* a {@link BiConsumer} which acts on a link and a DOM event type
185+
*/
186+
private void onEachLinkForEachManagedEventType(BiConsumer<EventTarget, String> manageListener) {
187+
NodeList domNodeList = webView.getEngine().getDocument().getElementsByTagName("a");
188+
for (int i = 0; i < domNodeList.getLength(); i++) {
189+
EventTarget domTarget = (EventTarget) domNodeList.item(i);
190+
onLinkForEachManagedEventType(domTarget, manageListener);
191+
}
192+
}
193+
194+
/**
195+
* Executes the specified function on the specified link for each {@link DomEventType} for which
196+
* {@link #manageListenerForEventType(DomEventType)} returns true.
197+
* <p>
198+
* Must be called in JavaFX application thread.
199+
*
200+
* @param link
201+
* The {@link EventTarget} with which {@code manageListener} will be called
202+
* @param manageListener
203+
* a {@link BiConsumer} which acts on a link and a DOM event type
204+
*/
205+
private void onLinkForEachManagedEventType(EventTarget link, BiConsumer<EventTarget, String> manageListener) {
206+
Consumer<DomEventType> manageListenerForType =
207+
domEventType -> manageListener.accept(link, domEventType.getDomName());
208+
Stream.of(DomEventType.values())
209+
.filter(this::manageListenerForEventType)
210+
.forEach(manageListenerForType);
211+
}
212+
213+
/**
214+
* Indicates whether a listener must be added for the specified DOM event type and the {@link #eventTypeFilter}.
215+
*
216+
* @param domEventType
217+
* the {@link DomEventType} for which a listener might be added
218+
* @return true if the DOM event type has a representation as an hyperlink event type and is not filtered out; false
219+
* otherwise
220+
*/
221+
private boolean manageListenerForEventType(DomEventType domEventType) {
222+
boolean domEventTypeHasRepresentation = domEventType.toHyperlinkEventType().isPresent();
223+
if (!domEventTypeHasRepresentation)
224+
return false;
225+
226+
boolean filterOn = eventTypeFilter.isPresent();
227+
if (!filterOn)
228+
return true;
229+
230+
return domEventType.toHyperlinkEventType().get() == eventTypeFilter.get();
231+
}
232+
233+
// #end COMMON MANAGEMENT METHODS
234+
235+
// #region PROCESS EVENT
236+
237+
/**
238+
* Transforms the specified {@code domEvent} into a {@link HyperlinkEvent} and calls the {@link #eventListener} with
239+
* it.
240+
*
241+
* @param domEvent
242+
* the DOM-{@link Event}
243+
*/
244+
private void callHyperlinkListenerWithEvent(Event domEvent) {
245+
boolean canNotTransformEvent = !eventTransformer.canTransformToHyperlinkEvent(domEvent);
246+
if (canNotTransformEvent)
247+
return;
248+
249+
HyperlinkEvent event = eventTransformer.transformToHyperlinkEvent(domEvent, webView);
250+
boolean cancel = eventListener.hyperlinkUpdate(event);
251+
cancel(domEvent, cancel);
252+
}
253+
254+
/**
255+
* Cancels the specified event if it is cancelable and cancellation is indicated by the specified flag.
256+
*
257+
* @param domEvent
258+
* the DOM-{@link Event} to be canceled
259+
* @param cancel
260+
* indicates whether the event should be canceled
261+
*/
262+
private static void cancel(Event domEvent, boolean cancel) {
263+
if (domEvent.getCancelable() && cancel)
264+
domEvent.preventDefault();
265+
}
266+
267+
// #end PROCESS EVENT
268+
269+
}

0 commit comments

Comments
 (0)