source: trunk/src/org/expeditee/items/widgets/JfxBrowser.java@ 963

Last change on this file since 963 was 921, checked in by jts21, 10 years ago

Avoided JfxBrowser build error (caused by having to use internal APIs which can change), by using reflection to get the methods at runtime if they are present

  • Property svn:executable set to *
File size: 43.6 KB
Line 
1/**
2 * JfxBrowser.java
3 * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19package org.expeditee.items.widgets;
20
21/*
22 * JavaFX is not on the default java classpath until Java 8 (but is still included with Java 7), so your IDE will probably complain that the imports below can't be resolved.
23 * In Eclipse hitting'Proceed' when told 'Errors exist in project' should allow you to run Expeditee without any issues (although the JFX Browser widget will not display),
24 * or you can just exclude JfxBrowser, WebParser and JfxbrowserActions from the build path.
25 *
26 * If you are using Ant to build/run, 'ant build' will try to build with JavaFX jar added to the classpath.
27 * If this fails, 'ant build-nojfx' will build with the JfxBrowser, WebParser and JfxbrowserActions excluded from the build path.
28 */
29import java.awt.Point;
30import java.awt.event.KeyListener;
31import java.io.BufferedReader;
32import java.io.IOException;
33import java.io.InputStreamReader;
34import java.lang.reflect.Field;
35import java.lang.reflect.Method;
36
37import javafx.animation.FadeTransition;
38import javafx.application.Platform;
39import javafx.beans.value.ChangeListener;
40import javafx.beans.value.ObservableValue;
41import javafx.concurrent.Worker.State;
42import javafx.embed.swing.JFXPanel;
43import javafx.event.ActionEvent;
44import javafx.event.Event;
45import javafx.event.EventDispatchChain;
46import javafx.event.EventDispatcher;
47import javafx.event.EventHandler;
48import javafx.geometry.Insets;
49import javafx.geometry.Point2D;
50import javafx.geometry.Pos;
51import javafx.geometry.Rectangle2D;
52import javafx.scene.Node;
53import javafx.scene.Scene;
54import javafx.scene.control.Button;
55import javafx.scene.control.Label;
56import javafx.scene.control.ProgressBar;
57import javafx.scene.control.ProgressIndicator;
58import javafx.scene.control.TextField;
59import javafx.scene.control.ToggleButton;
60import javafx.scene.control.Tooltip;
61import javafx.scene.image.Image;
62import javafx.scene.image.ImageView;
63import javafx.scene.input.KeyEvent;
64import javafx.scene.input.MouseButton;
65import javafx.scene.input.MouseEvent;
66import javafx.scene.layout.HBox;
67import javafx.scene.layout.Pane;
68import javafx.scene.layout.Priority;
69import javafx.scene.layout.StackPane;
70import javafx.scene.layout.VBox;
71import javafx.scene.text.Font;
72import javafx.scene.web.WebEngine;
73import javafx.scene.web.WebEvent;
74import javafx.scene.web.WebView;
75import javafx.util.Duration;
76import netscape.javascript.JSObject;
77
78import org.expeditee.gui.DisplayIO;
79import org.expeditee.gui.FrameMouseActions;
80import org.expeditee.gui.FreeItems;
81import org.expeditee.gui.MessageBay;
82import org.expeditee.io.WebParser;
83import org.expeditee.items.Item;
84import org.expeditee.items.Picture;
85import org.expeditee.items.Text;
86import org.expeditee.settings.network.NetworkSettings;
87import org.w3c.dom.NodeList;
88
89import com.sun.javafx.scene.control.skin.TextFieldSkin;
90import com.sun.javafx.scene.text.HitInfo;
91
92/**
93 * Web browser using a JavaFX WebView.
94 *
95 * @author ngw8
96 * @author jts21
97 */
98/**
99 * @author ngw8
100 *
101 */
102public class JfxBrowser extends DataFrameWidget {
103
104 private static final String BACK = "back";
105 private static final String FORWARD = "forward";
106 private static final String REFRESH = "refresh";
107 private static final String CONVERT = "convert";
108 private static final String VIDEO = "video";
109
110 private JFXPanel _panel;
111 private WebView _webView;
112 private WebEngine _webEngine;
113 private Button _forwardButton;
114 private Button _backButton;
115 private Button _stopButton;
116 private Button _goButton;
117 private Button _convertButton;
118 private ToggleButton _readableModeButton;
119 private Label _statusLabel;
120 private FadeTransition _statusFadeIn;
121 private FadeTransition _statusFadeOut;
122
123 private TextField _urlField;
124 private ProgressBar _urlProgressBar;
125 private StackPane _overlay;
126
127 private boolean _parserRunning;
128
129 private MouseButton _buttonDownId = MouseButton.NONE;
130 private MouseEvent _backupEvent = null;
131 private static Field MouseEvent_x, MouseEvent_y;
132
133 static {
134 Platform.setImplicitExit(false);
135
136 Font.loadFont(ClassLoader.getSystemResourceAsStream("org/expeditee/assets/resources/fonts/fontawesome/fontawesome-webfont.ttf"), 12);
137
138 try {
139 MouseEvent_x = MouseEvent.class.getDeclaredField("x");
140 MouseEvent_x.setAccessible(true);
141 MouseEvent_y = MouseEvent.class.getDeclaredField("y");
142 MouseEvent_y.setAccessible(true);
143 } catch (Exception e) {
144 e.printStackTrace();
145 }
146 }
147
148 public JfxBrowser(Text source, final String[] args) {
149 // Initial page is either the page stored in the arguments (if there is one stored) or the homepage
150 super(source, new JFXPanel(), -1, 500, -1, -1, 300, -1);
151
152 _panel = (JFXPanel) _swingComponent;
153
154 // Quick & easy way of having a cancel function for the web parser.
155 // Can't just have a JFX button, as the JFX thread is occupied with running JavaScript so it wouldn't receive the click event straight away
156 _swingComponent.addKeyListener(new KeyListener() {
157
158 @Override
159 public void keyReleased(java.awt.event.KeyEvent e) {
160 if(e.getKeyCode() == java.awt.event.KeyEvent.VK_ESCAPE) {
161 JfxBrowser.this.cancel();
162 }
163 }
164
165 @Override
166 public void keyPressed(java.awt.event.KeyEvent e) {
167 }
168
169 @Override
170 public void keyTyped(java.awt.event.KeyEvent e) {
171 }
172 });
173
174 Platform.runLater(new Runnable() {
175 @Override
176 public void run() {
177 initFx((args != null && args.length > 0) ? args[0] : NetworkSettings.HomePage.get());
178 }
179 });
180 }
181
182 /**
183 * Sets up the browser frame. JFX requires this to be run on a new thread.
184 *
185 * @param url
186 * The URL to be loaded when the browser is created
187 */
188 private void initFx(String url) {
189 try {
190 StackPane mainLayout = new StackPane();
191 mainLayout.setId("jfxbrowser");
192
193 VBox vertical = new VBox();
194 HBox horizontal = new HBox();
195 horizontal.getStyleClass().add("custom-toolbar");
196
197 this._backButton = new Button("\uf060");
198 this._backButton.setTooltip(new Tooltip("Back"));
199 this._backButton.setMinWidth(Button.USE_PREF_SIZE);
200 this._backButton.setMaxHeight(Double.MAX_VALUE);
201 this._backButton.setFocusTraversable(false);
202 this._backButton.getStyleClass().addAll("first", "fa");
203
204 this._backButton.setDisable(true);
205
206 this._forwardButton = new Button("\uf061");
207 this._forwardButton.setTooltip(new Tooltip("Forward"));
208 this._forwardButton.setMinWidth(Button.USE_PREF_SIZE);
209 this._forwardButton.setMaxHeight(Double.MAX_VALUE);
210 this._forwardButton.setFocusTraversable(false);
211 this._forwardButton.getStyleClass().addAll("last", "fa");
212
213 this._urlField = new TextField(url);
214 this._urlField.getStyleClass().add("url-field");
215 this._urlField.setMaxWidth(Double.MAX_VALUE);
216 this._urlField.setFocusTraversable(false);
217
218 this._stopButton = new Button("\uF00D");
219 this._stopButton.setTooltip(new Tooltip("Stop loading the page"));
220 this._stopButton.getStyleClass().addAll("url-button", "url-cancel-button", "fa");
221 this._stopButton.setMinWidth(Button.USE_PREF_SIZE);
222 this._stopButton.setMaxHeight(Double.MAX_VALUE);
223 StackPane.setAlignment(this._stopButton, Pos.CENTER_RIGHT);
224 this._stopButton.setFocusTraversable(false);
225
226 this._goButton = new Button("\uf061");
227 this._goButton.setTooltip(new Tooltip("Load the entered address"));
228 this._goButton.getStyleClass().addAll("url-button", "url-go-button", "fa");
229 this._goButton.setMinWidth(Button.USE_PREF_SIZE);
230 this._goButton.setMaxHeight(Double.MAX_VALUE);
231 StackPane.setAlignment(this._goButton, Pos.CENTER_RIGHT);
232 this._goButton.setFocusTraversable(false);
233
234 this._readableModeButton = new ToggleButton();
235 this._readableModeButton.setMinWidth(Button.USE_PREF_SIZE);
236 this._readableModeButton.setFocusTraversable(false);
237 this._readableModeButton.setTooltip(new Tooltip("Switch to an easy-to-read view of the page"));
238
239 Image readableModeIcon = new Image(ClassLoader.getSystemResourceAsStream("org/expeditee/assets/images/readableModeIcon.png"));
240 this._readableModeButton.setGraphic(new ImageView(readableModeIcon));
241
242 this._convertButton = new Button("Convert");
243 this._convertButton.setMinWidth(Button.USE_PREF_SIZE);
244 this._convertButton.setFocusTraversable(false);
245
246 this._urlProgressBar = new ProgressBar();
247 this._urlProgressBar.getStyleClass().add("url-progress-bar");
248 this._urlProgressBar.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
249
250 // Status label that displays the URL when a link is hovered over
251 this._statusLabel = new Label();
252 this._statusLabel.getStyleClass().addAll("browser-status-label");
253 this._statusLabel.setVisible(false);
254
255 this._statusFadeIn = new FadeTransition();
256 this._statusFadeIn.setDuration(Duration.millis(200));
257 this._statusFadeIn.setNode(this._statusLabel);
258 this._statusFadeIn.setFromValue(0);
259 this._statusFadeIn.setToValue(1);
260 this._statusFadeIn.setCycleCount(1);
261 this._statusFadeIn.setAutoReverse(false);
262
263 this._statusFadeOut = new FadeTransition();
264 this._statusFadeOut.setDuration(Duration.millis(400));
265 this._statusFadeOut.setNode(this._statusLabel);
266 this._statusFadeOut.setFromValue(1);
267 this._statusFadeOut.setToValue(0);
268 this._statusFadeOut.setCycleCount(1);
269 this._statusFadeOut.setAutoReverse(false);
270
271 this._statusFadeOut.setOnFinished(new EventHandler<ActionEvent>() {
272
273 @Override
274 public void handle(ActionEvent arg0) {
275 JfxBrowser.this._statusLabel.setVisible(false);
276 }
277 });
278
279
280 StackPane urlbar = new StackPane();
281 urlbar.getChildren().addAll(_urlProgressBar, this._urlField, this._stopButton, this._goButton);
282
283 horizontal.getChildren().addAll(this._backButton, this._forwardButton, urlbar, this._readableModeButton, this._convertButton);
284
285 HBox.setHgrow(this._backButton, Priority.NEVER);
286 HBox.setHgrow(this._forwardButton, Priority.NEVER);
287 HBox.setHgrow(this._convertButton, Priority.NEVER);
288 HBox.setHgrow(this._readableModeButton, Priority.NEVER);
289 HBox.setHgrow(urlbar, Priority.ALWAYS);
290
291 HBox.setMargin(this._readableModeButton, new Insets(0, 5, 0, 5));
292 HBox.setMargin(this._forwardButton, new Insets(0, 5, 0, 0));
293
294 this._webView = new WebView();
295 this._webView.setMaxWidth(Double.MAX_VALUE);
296 this._webView.setMaxHeight(Double.MAX_VALUE);
297 this._webEngine = this._webView.getEngine();
298
299 this._urlProgressBar.progressProperty().bind(_webEngine.getLoadWorker().progressProperty());
300
301
302 // Pane to hold just the webview. This seems to be the only way to allow the webview to be resized to greater than its parent's
303 // size. This also means that the webview's prefSize must be manually set when the Pane resizes, using the event handlers below
304 Pane browserPane = new Pane();
305 browserPane.getChildren().addAll(_webView, this._statusLabel);
306
307 HBox.setHgrow(browserPane, Priority.ALWAYS);
308 VBox.setVgrow(browserPane, Priority.ALWAYS);
309
310 browserPane.widthProperty().addListener(new ChangeListener<Object>() {
311
312 @Override
313 public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
314 JfxBrowser.this._webView.setPrefWidth((Double) newValue);
315 }
316 });
317
318 browserPane.heightProperty().addListener(new ChangeListener<Object>() {
319
320 @Override
321 public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {
322 JfxBrowser.this._webView.setPrefHeight((Double) newValue);
323 JfxBrowser.this._statusLabel.setTranslateY((Double) newValue - JfxBrowser.this._statusLabel.heightProperty().doubleValue());
324 }
325 });
326
327 vertical.getChildren().addAll(horizontal, browserPane);
328
329 this._overlay = new StackPane();
330 this._overlay.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
331
332 // Class for CSS styling
333 this._overlay.getStyleClass().add("browser-overlay");
334
335 // Don't show the overlay until processing the page
336 this._overlay.setVisible(false);
337
338 Label overlayLabel = new Label("Importing page to Expeditee...");
339
340 ProgressIndicator prog = new ProgressIndicator();
341 prog.setMaxSize(25, 25);
342
343 this._overlay.getChildren().addAll(overlayLabel, prog);
344
345 this._overlay.setAlignment(Pos.CENTER);
346 StackPane.setMargin(overlayLabel, new Insets(-50, 0, 0, 0));
347 StackPane.setMargin(prog, new Insets(50, 0, 0, 0));
348
349 mainLayout.getChildren().addAll(vertical, this._overlay);
350
351 final Scene scene = new Scene(mainLayout);
352
353 final String cssPath = ClassLoader.getSystemResource("org/expeditee/assets/style/jfx.css").toString();
354
355 scene.getStylesheets().add(cssPath);
356
357 this._panel.setScene(scene);
358
359 // Disable right click menu
360 this._webView.setContextMenuEnabled(false);
361
362 // Showing the status label when a link is hovered over
363 this._webEngine.setOnStatusChanged(new EventHandler<WebEvent<String>>() {
364
365 @Override
366 public void handle(WebEvent<String> arg0) {
367 if (arg0.getData() != null && hasValidProtocol(arg0.getData())) {
368 JfxBrowser.this._statusLabel.setText(arg0.getData());
369
370 JfxBrowser.this._statusFadeOut.stop();
371
372 if(JfxBrowser.this._statusLabel.isVisible()) {
373 // Don't play the fade in if the label is already partially visible
374 JfxBrowser.this._statusLabel.setOpacity(1);
375 } else {
376 JfxBrowser.this._statusLabel.setVisible(true);
377 JfxBrowser.this._statusFadeIn.play();
378 }
379 } else {
380 JfxBrowser.this._statusFadeIn.stop();
381
382 JfxBrowser.this._statusFadeOut.play();
383 }
384 }
385 });
386
387
388 final EventDispatcher initial = this._urlField.getEventDispatcher();
389
390 this._urlField.setEventDispatcher(new EventDispatcher() {
391 @Override
392 public Event dispatchEvent(Event e, EventDispatchChain tail) {
393 if (e instanceof MouseEvent) {
394 MouseEvent m = (MouseEvent) e;
395 if (m.getButton() == MouseButton.SECONDARY && m.getEventType() == MouseEvent.MOUSE_RELEASED) {
396 e.consume();
397 JfxBrowser.this._urlField.getOnMouseReleased().handle(m);
398 }
399 }
400 return initial.dispatchEvent(e, tail);
401 }
402 });
403
404 this._backButton.setOnAction(new EventHandler<ActionEvent>() {
405 @Override
406 public void handle(ActionEvent e) {
407 navigateBack();
408 }
409 });
410
411 _forwardButton.setOnAction(new EventHandler<ActionEvent>() {
412 @Override
413 public void handle(ActionEvent e) {
414 navigateForward();
415 }
416 });
417
418 this._stopButton.setOnAction(new EventHandler<ActionEvent>() {
419
420 @Override
421 public void handle(ActionEvent arg0) {
422 JfxBrowser.this._webEngine.getLoadWorker().cancel();
423 }
424 });
425
426 this._goButton.setOnAction(new EventHandler<ActionEvent>() {
427
428 @Override
429 public void handle(ActionEvent arg0) {
430 navigate(JfxBrowser.this._urlField.getText());
431 }
432 });
433
434 this._readableModeButton.setOnAction(new EventHandler<ActionEvent>() {
435
436 @Override
437 public void handle(ActionEvent arg0) {
438 if (arg0.getSource() instanceof ToggleButton) {
439 ToggleButton source = (ToggleButton) arg0.getSource();
440
441 // This seems backwards, but because the button's just been clicked, its state has already changed
442 if(!source.isSelected()) {
443 // Disable readable mode by refreshing the page
444 JfxBrowser.this._webEngine.reload();
445 } else {
446 JfxBrowser.this.enableReadableMode();
447 }
448 }
449 }
450 });
451
452 this._convertButton.setOnAction(new EventHandler<ActionEvent>() {
453 @Override
454 public void handle(ActionEvent e) {
455 getFrameNew();
456 }
457 });
458
459 this._urlField.setOnAction(new EventHandler<ActionEvent>() {
460 @Override
461 public void handle(ActionEvent e) {
462 navigate(JfxBrowser.this._urlField.getText());
463 }
464 });
465
466 this._urlField.setOnKeyTyped(new EventHandler<KeyEvent>() {
467 @Override
468 public void handle(KeyEvent e) {
469 // Hiding the cursor when typing, to be more Expeditee-like
470 DisplayIO.setCursor(org.expeditee.items.Item.HIDDEN_CURSOR);
471 }
472 });
473
474 this._urlField.setOnMouseMoved(new EventHandler<MouseEvent>() {
475 @Override
476 public void handle(MouseEvent e) {
477 JfxBrowser.this._backupEvent = e;
478 // make sure we have focus if the mouse is moving over us
479 if(!JfxBrowser.this._urlField.isFocused()) {
480 JfxBrowser.this._urlField.requestFocus();
481 }
482 // Checking if the user has been typing - if so, move the cursor to the caret position
483 if (DisplayIO.getCursor() == Item.HIDDEN_CURSOR) {
484 DisplayIO.setCursor(org.expeditee.items.Item.TEXT_CURSOR);
485 DisplayIO.setCursorPosition(getCoordFromCaret(JfxBrowser.this._urlField));
486 } else {
487 // Otherwise, move the caret to the cursor location
488 // int x = FrameMouseActions.getX() - JfxBrowser.this.getX(), y = FrameMouseActions.getY() - JfxBrowser.this.getY();
489 JfxBrowser.this._urlField.positionCaret(getCaretFromCoord(JfxBrowser.this._urlField, e));
490 }
491 }
492 });
493
494 this._urlField.setOnMouseEntered(new EventHandler<MouseEvent>() {
495 @Override
496 public void handle(MouseEvent arg0) {
497 JfxBrowser.this._urlField.requestFocus();
498 }
499 });
500
501 this._urlField.setOnMouseExited(new EventHandler<MouseEvent>() {
502 @Override
503 public void handle(MouseEvent arg0) {
504 JfxBrowser.this._webView.requestFocus();
505 }
506 });
507
508 this._urlField.setOnMouseDragged(new EventHandler<MouseEvent>() {
509 @Override
510 public void handle(MouseEvent e) {
511 if (!JfxBrowser.this._urlField.isDisabled()) {
512 if (JfxBrowser.this._buttonDownId == MouseButton.MIDDLE || JfxBrowser.this._buttonDownId == MouseButton.SECONDARY) {
513 if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) {
514 setCaretFromCoord(JfxBrowser.this._urlField, e);
515 }
516 }
517 }
518 }
519 });
520
521 this._urlField.focusedProperty().addListener(new ChangeListener<Boolean>() {
522 @Override
523 public void changed(ObservableValue<? extends Boolean> property, Boolean oldValue, Boolean newValue) {
524 if(newValue.booleanValue()) {
525 DisplayIO.setCursor(org.expeditee.items.Item.TEXT_CURSOR);
526 } else {
527 // Restoring the standard cursor, since it is changed to a text cursor when focus is gained
528 DisplayIO.setCursor(org.expeditee.items.Item.DEFAULT_CURSOR);
529 }
530 }
531 });
532
533 this._urlField.setOnMouseReleased(new EventHandler<MouseEvent>() {
534 @Override
535 public void handle(MouseEvent e) {
536 JfxBrowser.this._buttonDownId = MouseButton.NONE;
537
538 Text item;
539
540 // If nothing is selected, then select all the text so that it will be copied/moved
541 if (JfxBrowser.this._urlField.getSelectedText() == null || JfxBrowser.this._urlField.getSelectedText().length() == 0) {
542 JfxBrowser.this._urlField.selectAll();
543 }
544
545 if (e.getButton() == MouseButton.SECONDARY) {
546 // Right mouse button released, so copy the selection (i.e. don't remove the original)
547 item = DisplayIO.getCurrentFrame().createNewText(JfxBrowser.this._urlField.getSelectedText());
548 FrameMouseActions.pickup(item);
549 } else if (e.getButton() == MouseButton.MIDDLE) {
550 // Middle mouse button released, so copy the selection then remove it from the URL field
551 item = DisplayIO.getCurrentFrame().createNewText(JfxBrowser.this._urlField.getSelectedText());
552 JfxBrowser.this._urlField.setText(
553 JfxBrowser.this._urlField.getText().substring(0, JfxBrowser.this._urlField.getSelection().getStart())
554 + JfxBrowser.this._urlField.getText().substring(JfxBrowser.this._urlField.getSelection().getEnd(),
555 JfxBrowser.this._urlField.getText().length()));
556
557 FrameMouseActions.pickup(item);
558 }
559 }
560 });
561
562 this._urlField.setOnMousePressed(new EventHandler<MouseEvent>() {
563 @Override
564 public void handle(MouseEvent e) {
565 JfxBrowser.this._buttonDownId = e.getButton();
566 }
567 });
568
569 this._webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
570
571 @Override
572 public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
573
574 switch (newState) {
575 case READY: // READY
576 // MessageBay.displayMessage("WebEngine ready");
577 break;
578 case SCHEDULED: // SCHEDULED
579 // MessageBay.displayMessage("Scheduled page load");
580 break;
581 case RUNNING: // RUNNING
582 System.out.println("Loading page!");
583
584 // Updating the URL bar to display the URL of the page being loaded
585 JfxBrowser.this._urlField.setText(JfxBrowser.this._webEngine.getLocation());
586
587 // Removing the style from the progress bar that causes it to hide
588 JfxBrowser.this._urlProgressBar.getStyleClass().remove("completed");
589
590 JfxBrowser.this._stopButton.setVisible(true);
591 JfxBrowser.this._goButton.setVisible(false);
592
593 if (JfxBrowser.this._webEngine.getHistory().getCurrentIndex() + 1 >= JfxBrowser.this._webEngine.getHistory().getEntries().size()) {
594 JfxBrowser.this._forwardButton.setDisable(true);
595 } else {
596 JfxBrowser.this._forwardButton.setDisable(false);
597 }
598
599 // Unless the history is empty (i.e. this is the first page being loaded), enable the back button.
600 // The only time the back button should be disbaled is on the first page load (which this statement deals with)
601 // and if the user has just hit the back button taking them to the first page in the history (dealt with in the
602 // navigateBack method)
603 if (JfxBrowser.this._webEngine.getHistory().getEntries().size() > 0) {
604 JfxBrowser.this._backButton.setDisable(false);
605 }
606
607 JfxBrowser.this._convertButton.setDisable(true);
608 JfxBrowser.this._readableModeButton.setDisable(true);
609
610 break;
611 case SUCCEEDED: // SUCCEEDED
612 MessageBay.displayMessage("Finished loading page");
613 JfxBrowser.this._urlProgressBar.getStyleClass().add("completed");
614
615 if(JfxBrowser.this._readableModeButton.isSelected()) {
616 JfxBrowser.this.enableReadableMode();
617 }
618
619 case CANCELLED: // CANCELLED
620 JfxBrowser.this._convertButton.setDisable(false);
621 JfxBrowser.this._readableModeButton.setDisable(false);
622 JfxBrowser.this._stopButton.setVisible(false);
623 JfxBrowser.this._goButton.setVisible(true);
624 break;
625 case FAILED: // FAILED
626 MessageBay.displayMessage("Failed to load page");
627 JfxBrowser.this._stopButton.setVisible(false);
628 JfxBrowser.this._goButton.setVisible(true);
629 break;
630 }
631 }
632 });
633
634 // Captures mouse click events on webview to enable expeditee like behavior for JavaFX browser.
635 this._webView.setOnMouseClicked(new EventHandler<javafx.scene.input.MouseEvent>() {
636 @Override
637 public void handle(javafx.scene.input.MouseEvent e) {
638 if(e.getButton() == MouseButton.SECONDARY) {
639 // Gets text currently selected in webview
640 String selection = (String) JfxBrowser.this._webEngine.executeScript("window.getSelection().toString()");
641
642 // If no text is selected, see if an image is under the cursor
643 if (selection.length() == 0) {
644 JSObject window = (JSObject) JfxBrowser.this._webEngine.executeScript("window");
645 Object o = JfxBrowser.this._webEngine.executeScript("document.elementFromPoint(" + e.getX() + "," + e.getY() + ");");
646
647 if(o instanceof org.w3c.dom.Node) {
648 org.w3c.dom.Node node = (org.w3c.dom.Node) o;
649 JSObject style = (JSObject) window.call("getComputedStyle", node);
650
651 if(node.getNodeName().toLowerCase().equals("img") ||
652 ((String) style.call("getPropertyValue", "background-image")).startsWith("url")) {
653
654 try {
655 JSObject bounds = (JSObject) ((JSObject) node).call("getBoundingClientRect", new Object[] {});
656 float width = Float.valueOf(bounds.getMember("width").toString());
657 float height = Float.valueOf(bounds.getMember("height").toString());
658
659 Picture pic;
660
661 if (((String) style.call("getPropertyValue", new Object[] { "background-image" })).startsWith("url(")) {
662 pic = WebParser.getBackgroundImageFromNode(node, style, DisplayIO.getCurrentFrame(), null,
663 (float) FrameMouseActions.getX(), (float) FrameMouseActions.getY(), width, height);
664
665 } else {
666 String imgSrc;
667 if(node.getNodeName().toLowerCase().equals("img") &&
668 (imgSrc = ((JSObject) node).getMember("src").toString()) != null) {
669 pic = WebParser.getImageFromUrl(imgSrc, null, DisplayIO.getCurrentFrame(),
670 (float) FrameMouseActions.getX(), (float) FrameMouseActions.getY(), (int) width, null, null, null, null, null, 0, 0);
671 } else {
672 return;
673 }
674 }
675
676 String linkUrl;
677
678 // Check the image and its immediate parent for links
679 if ((node.getNodeName().toLowerCase().equals("a") && (linkUrl = (String) ((JSObject)node).getMember("href")) != null)
680 || (node.getParentNode().getNodeName().toLowerCase().equals("a") && (linkUrl = (String)((JSObject)node.getParentNode()).getMember("href")) != null)) {
681
682 if(hasValidProtocol(linkUrl)) {
683 pic.getSource().setAction("createFrameWithBrowser " + linkUrl);
684 }
685 }
686
687 pic.setXY(FrameMouseActions.getX(), FrameMouseActions.getY());
688 FrameMouseActions.pickup(pic);
689 } catch (Exception e1) {
690 // TODO Auto-generated catch block
691 e1.printStackTrace();
692 }
693
694 } else if(node.getNodeName().toLowerCase().equals("video")) {
695 String src = ((JSObject)node).getMember("src").toString();
696 if(src == null || src.trim().length() == 0) {
697 NodeList children = node.getChildNodes();
698 for(int i = 0; i < children.getLength(); i++) {
699 org.w3c.dom.Node child = children.item(i);
700 if(child.getNodeName().toLowerCase().equals("source")) {
701 src = ((JSObject)child).getMember("src").toString();
702 if(src != null && src.trim().length() > 0) {
703 break;
704 }
705 }
706
707 }
708 if(src == null || src.trim().length() == 0) {
709 return;
710 }
711 }
712 Text t = new Text("@iw: org.expeditee.items.widgets.jfxmedia "
713 + ((JSObject)node).getMember("width")
714 + ((JSObject)node).getMember("height")
715 + ":" + src);
716 t.setParent(DisplayIO.getCurrentFrame());
717 t.setXY(FrameMouseActions.getX(), FrameMouseActions.getY());
718 JfxMedia media = new JfxMedia(t, new String[] { src });
719 FrameMouseActions.pickup(media.getItems());
720
721 } else if(node.getNodeName().toLowerCase().equals("a") && ((JSObject)node).getMember("href") != null) {
722 // If a link is right clicked, copy the text content and give it an action to create
723 // a new frame containing a browser pointing to the linked page
724 Text t = DisplayIO.getCurrentFrame().createNewText(((String) ((JSObject)node).getMember("textContent")).trim());
725 t.addAction("createFrameWithBrowser " + (String) ((JSObject)node).getMember("href"));
726 t.setXY(FrameMouseActions.getX(), FrameMouseActions.getY());
727 FrameMouseActions.pickup(t);
728 }
729 }
730 } else {
731 // Copy text and attach to cursor
732 Text t = DisplayIO.getCurrentFrame().createNewText(selection);
733 t.setXY(FrameMouseActions.getX(), FrameMouseActions.getY());
734 FrameMouseActions.pickup(t);
735 }
736 }
737 }
738 });
739
740 this.navigate(url);
741 } catch (Exception e) {
742 e.printStackTrace();
743 }
744 }
745
746 public void navigate(String url) {
747 final String actualURL;
748
749 // check if protocol is missing
750 if (!hasValidProtocol(url)) {
751 // check if it's a search
752 int firstSpace = url.indexOf(" ");
753 int firstDot = url.indexOf(".");
754 int firstSlash = url.indexOf('/');
755 int firstQuestion = url.indexOf('?');
756 int firstSQ;
757 if(firstSlash == -1) {
758 firstSQ = firstQuestion;
759 } else if(firstQuestion == -1) {
760 firstSQ = firstSlash;
761 } else {
762 firstSQ = -1;
763 }
764 if(firstDot <= 0 || // no '.' or starts with '.' -> search
765 (firstSpace != -1 && firstSpace < firstDot + 1) || // ' ' before '.' -> search
766 (firstSpace != -1 && firstSpace < firstSQ)) { // no '/' or '?' -> search
767 // make it a search
768 actualURL = NetworkSettings.SearchEngine.get() + url;
769 } else {
770 // add the missing protocol
771 actualURL = "http://" + url;
772 }
773 } else {
774 actualURL = url;
775 }
776 System.out.println(actualURL);
777 try {
778 Platform.runLater(new Runnable() {
779 @Override
780 public void run() {
781 try {
782 JfxBrowser.this._webEngine.load(actualURL);
783 } catch (Exception e) {
784 e.printStackTrace();
785 }
786 }
787 });
788 } catch (Exception e) {
789 e.printStackTrace();
790 }
791 }
792
793 /**
794 * Navigates JfxBrowser back through history. If end of history reached the user is notified via the MessageBay.
795 * Max size of history is 100 by default.
796 */
797 public void navigateBack() {
798 try {
799 Platform.runLater(new Runnable() {
800 @Override
801 public void run() {
802 try {
803 JfxBrowser.this._webEngine.getHistory().go(-1);
804
805 // Disable the back button if we're at the start of history
806 if (JfxBrowser.this._webEngine.getHistory().getCurrentIndex() <= 0) {
807 JfxBrowser.this._backButton.setDisable(true);
808 } else {
809 JfxBrowser.this._backButton.setDisable(false);
810 }
811
812 FreeItems.getInstance().clear();
813 } catch (IndexOutOfBoundsException e) {
814 MessageBay.displayMessage("Start of History");
815 }
816 }
817 });
818 } catch (Exception e) {
819 e.printStackTrace();
820 }
821 }
822
823 /**
824 * Navigates JfxBrowser forward through history. If end of history reached the user is notified via the MessageBay.
825 * Max size of history is 100 by default.
826 */
827 public void navigateForward() {
828 try {
829 Platform.runLater(new Runnable() {
830 @Override
831 public void run() {
832 try {
833 JfxBrowser.this._webEngine.getHistory().go(1);
834 FreeItems.getInstance().clear();
835 } catch (IndexOutOfBoundsException e) {
836 MessageBay.displayMessage("End of History");
837 }
838 }
839 });
840 } catch (Exception e) {
841 e.printStackTrace();
842 }
843 }
844
845 /**
846 * Refreshes webview by reloading the page.
847 */
848 public void refresh() {
849 try {
850 Platform.runLater(new Runnable() {
851 @Override
852 public void run() {
853 try {
854 JfxBrowser.this._webEngine.reload();
855 FreeItems.getInstance().clear();
856 MessageBay.displayMessage("Page Reloading");
857 } catch (Exception e) {
858 e.printStackTrace();
859 }
860 }
861 });
862 } catch (Exception e) {
863 e.printStackTrace();
864 }
865
866 }
867
868 /**
869 * Traverses DOM an turns elements into expeditee items.
870 */
871 public void getFrame() {
872 try {
873 WebParser.parsePageSimple(this, _webEngine, _webView, DisplayIO.getCurrentFrame());
874 } catch (Exception e) {
875 e.printStackTrace();
876 }
877 }
878
879 public void getFrameNew() {
880
881 this._parserRunning = true;
882
883 try {
884 // hack to make sure we don't try parsing the page from within the JavaFX thread,
885 // because doing so causes deadlock
886 new Thread(new Runnable() {
887 public void run() {
888 WebParser.parsePageSimple(JfxBrowser.this, JfxBrowser.this._webEngine, JfxBrowser.this._webView, DisplayIO.getCurrentFrame());
889 }
890 }).start();
891 } catch (Exception e) {
892 e.printStackTrace();
893 this._parserRunning = false;
894 }
895 }
896
897 /**
898 * Used to drop text items onto JfxBrowser widget. Does nothing if a text item is not attached to cursor. <br>
899 * "back" -> navigates back a page in browser's session history <br>
900 * "forward" -> navigates forward a page in browser's session history <br>
901 * "refresh" -> reloads current page <br>
902 * "getFrame" -> attempts to parse page into an expeditee frame <br>
903 * url -> all other text is assumed to be a url which browser attempts to navigate to
904 *
905 * @return Whether a JfxBrowser specific event is run.
906 *
907 */
908 @Override
909 public boolean ItemsLeftClickDropped() {
910 Text carried = null;
911 if ((carried = FreeItems.getTextAttachedToCursor()) == null) { // fails if no text is attached to cursor.
912 return false;
913 }
914
915 if (carried.getText().toLowerCase().equals(BACK)) {
916 navigateBack();
917 } else if (carried.getText().toLowerCase().equals(FORWARD)) {
918 navigateForward();
919 } else if (carried.getText().toLowerCase().equals(REFRESH)) {
920 refresh();
921 } else if (carried.getText().toLowerCase().equals(CONVERT)) {
922 getFrame();
923 } else {
924 String text = carried.getText().trim();
925 this.navigate(text);
926 FreeItems.getInstance().clear();
927 }
928
929 return true;
930 }
931
932 /**
933 * Used to enable expeditee like text-widget interaction for middle mouse clicks. Does nothing if a text item is not attached to cursor.
934 * @return false if a text-widget interaction did not occur, true if a text-widget interaction did occur.
935 */
936 @Override
937 public boolean ItemsMiddleClickDropped() {
938 if(ItemsRightClickDropped()) {
939 FreeItems.getInstance().clear(); // removed held text item - like normal expeditee middle click behaviour.
940 return true;
941 }
942 return false;
943 }
944
945 /**
946 * Used to enable expeditee like text-widget interaction for right mouse clicks. Does nothing if a text item is not attached to cursor.
947 * @return false if a text-widget interaction did not occur, true if a text-widget interaction did occur.
948 */
949 @Override
950 public boolean ItemsRightClickDropped() {
951 Text t = null;
952 if((t = FreeItems.getTextAttachedToCursor()) == null) { // fails if no text item is attached to the cursor.
953 return false;
954 }
955
956 final int x = FrameMouseActions.getX() - this.getX(), y = FrameMouseActions.getY() - this.getY();
957 if(!this._urlField.getBoundsInParent().contains(x, y)) {
958 // fails if not clicking on urlField
959 return false;
960 }
961
962 final String insert = t.getText();
963 Platform.runLater(new Runnable() {
964 @Override
965 public void run() {
966 // Inserts text in text item into urlField at the position of the mouse.
967 String s = JfxBrowser.this._urlField.getText();
968 int index = getCaretFromCoord(JfxBrowser.this._urlField, getMouseEventForPosition(JfxBrowser.this._backupEvent, JfxBrowser.this._urlField, x, y));
969 if(index < s.length()) {
970 s = s.substring(0, index) + insert + s.substring(index);
971 } else {
972 s = s + insert;
973 }
974 JfxBrowser.this._urlField.setText(s);
975 }
976 });
977
978 return true;
979 }
980
981 /**
982 * Shows/hides a message reading 'Importing page' over the widget
983 *
984 * @param visible
985 */
986 public void setOverlayVisible(boolean visible) {
987 this._overlay.setVisible(visible);
988 }
989
990 public void setScrollbarsVisible(boolean visible) {
991 if (!visible) {
992 this._webView.getStyleClass().add("scrollbars-hidden");
993 } else {
994 this._webView.getStyleClass().remove("scrollbars-hidden");
995 }
996 }
997
998 /**
999 * Sets the size of the webview element of the widget
1000 *
1001 * @param width
1002 * @param height
1003 */
1004 public void setWebViewSize(double width, double height) {
1005 this._webView.setPrefSize(width, height);
1006 }
1007
1008 /**
1009 * Resizes the webview back to the size of its parent element
1010 */
1011 public void rebindWebViewSize() {
1012 this._webView.getParent().resize(0, 0);
1013 }
1014
1015 @Override
1016 protected String[] getArgs() {
1017 String[] r = null;
1018 if (this._webView != null) {
1019 try {
1020 r = new String[] { this._webEngine.getLocation() };
1021 } catch (Exception e) {
1022 e.printStackTrace();
1023 }
1024 }
1025 return r;
1026 }
1027
1028 private Point getCoordFromCaret(TextField text) {
1029 TextFieldSkin skin = (TextFieldSkin) text.getSkin();
1030
1031 Point2D onScene = text.localToScene(0, 0);
1032
1033 double x = onScene.getX() + JfxBrowser.this.getX();// - org.expeditee.gui.Browser._theBrowser.getOrigin().x;
1034 double y = onScene.getY() + JfxBrowser.this.getY();// - org.expeditee.gui.Browser._theBrowser.getOrigin().y;
1035
1036 Rectangle2D cp = skin.getCharacterBounds(text.getCaretPosition());
1037
1038 return new Point((int) (cp.getMinX() + x), (int) (cp.getMinY() + y));
1039 }
1040
1041 /**
1042 * Get internal JavaFX methods via reflection at runtime.<br>
1043 * These are internal API methods for converting mouse (pixel) position to caret (text) position.
1044 * This is used because JavaFX does not appear to support this functionality in any public API.
1045 * This class solves two problems:
1046 * - If the system we are compiling on does not have the method, it should still compile
1047 * - If the system we are running on does not have the method, it should still run
1048 * (just without the expeditee-like URL bar text selection)
1049 * Unfortunately it will still fail if the internal API ever removes the TextFieldSkin or HitInfo classes,
1050 * But that is unavoidable without a compile-time preprocessor or some greater hacks
1051 */
1052 private static final class kludges {
1053
1054 private static Method getGetIndex() {
1055 try {
1056 return TextFieldSkin.class.getMethod("getIndex", MouseEvent.class);
1057 } catch(Exception e) {
1058 return null;
1059 }
1060 }
1061 private static Method getGetInsertionIndex() {
1062 try {
1063 return HitInfo.class.getMethod("getInsertionIndex");
1064 } catch(Exception e) {
1065 return null;
1066 }
1067 }
1068 private static Method getPositionCaret() {
1069 try {
1070 return TextFieldSkin.class.getMethod("positionCaret", HitInfo.class, boolean.class);
1071 } catch(Exception e) {
1072 return null;
1073 }
1074 }
1075
1076 private static final Method getIndex = getGetIndex();
1077 private static final Method getInsertionIndex = getGetInsertionIndex();
1078 private static final Method positionCaret = getPositionCaret();
1079 private static final boolean enabled = (getIndex != null && getInsertionIndex != null && positionCaret != null);
1080 }
1081
1082 /**
1083 * Attempts to get the caret (text) position for a given pixel (MouseEvent) position
1084 *
1085 * @param text The textfield to find the caret position for
1086 * @param e The MouseEvent containing the coordinates to convert to caret position
1087 *
1088 * @return The caret position if successful, otherwise the current caret position of the TextField.
1089 */
1090 private int getCaretFromCoord(TextField text, MouseEvent e) {
1091 if (kludges.enabled) {
1092 try {
1093 return (int) kludges.getInsertionIndex.invoke(kludges.getIndex.invoke(text.getSkin(), e));
1094 } catch (Exception ex) {
1095 ex.printStackTrace();
1096 }
1097 }
1098 return text.getCaretPosition();
1099 }
1100
1101 /**
1102 * Attempts to set the caret (text) position from a given pixel (MouseEvent) position
1103 *
1104 * @param text The textfield to set the caret position of
1105 * @param e The MouseEvent containing the coordinates to convert to caret position
1106 */
1107 private void setCaretFromCoord(TextField text, MouseEvent e) {
1108 if (kludges.enabled) {
1109 try {
1110 Object skin = text.getSkin();
1111 kludges.positionCaret.invoke(skin, kludges.getIndex.invoke(skin, e), true);
1112 } catch (Exception ex) {
1113 ex.printStackTrace();
1114 }
1115 }
1116 }
1117
1118 /**
1119 * @param src The MouseEvent to clone
1120 * @param node The node the position will be relative to
1121 * @param x The position in Expeditee space
1122 * @param y The position in Expeditee space
1123 * @return A fake MouseEvent for a specific position relative to a Node
1124 */
1125 private MouseEvent getMouseEventForPosition(MouseEvent src, Node node, int x, int y) {
1126 MouseEvent dst = (MouseEvent) ((Event) src).copyFor(null, null);
1127 try {
1128 MouseEvent_x.set(dst, x - node.localToScene(0, 0).getX());
1129 MouseEvent_y.set(dst, y - node.localToScene(0, 0).getY());
1130 } catch (Exception e) {
1131 e.printStackTrace();
1132 }
1133 return dst;
1134 }
1135
1136 private void enableReadableMode() {
1137 String readabilityJs;
1138 String readabilityCss;
1139
1140 readabilityJs = readResourceFile("org/expeditee/assets/scripts/browserreadablemode/readability.min.js");
1141 readabilityCss = readResourceFile("org/expeditee/assets/scripts/browserreadablemode/readability.css");
1142
1143 JSObject window = (JSObject)JfxBrowser.this._webEngine.executeScript("window");
1144 window.setMember("readabilityJs", readabilityJs);
1145 window.setMember("readabilityCss", readabilityCss);
1146
1147 JfxBrowser.this._webEngine.executeScript(""
1148 + "javascript:("
1149 + "function(){ "
1150 + "readStyle = '';"
1151 + "readSize = 'size-medium';"
1152 + "readMargin = 'margin-medium';"
1153 + "_readability_script = document.createElement('SCRIPT');"
1154 + "_readability_script.type = 'text/javascript';"
1155 + "_readability_script.appendChild(document.createTextNode(readabilityJs));"
1156 + "document.head.appendChild(_readability_script);"
1157 + "readability.init();"
1158
1159 // readability.init() removes all css, so have to add the stylesheet after init
1160 + "_readability_css = document.createElement('STYLE');"
1161 + "_readability_css.type='text/css';"
1162 + "_readability_css.appendChild(document.createTextNode(readabilityCss));"
1163 + "document.head.appendChild(_readability_css);"
1164
1165 // Font Awesome CSS from the Bootstrap CDN
1166 + "_fontawesome_css = document.createElement('LINK');"
1167 + "_fontawesome_css.rel = 'stylesheet'; "
1168 + "_fontawesome_css.href = '//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css';"
1169 + "_fontawesome_css.type = 'text/css';"
1170 + "document.head.appendChild(_fontawesome_css);"
1171 + "}"
1172 + ")();"
1173 );
1174 }
1175
1176 /**
1177 * Reads a resource file into a string
1178 * @return The contents of the specified file as a string
1179 */
1180 private static String readResourceFile(String path) {
1181 BufferedReader bufferedReader = null;
1182 StringBuilder stringBuilder = new StringBuilder();
1183
1184 String line;
1185
1186 try {
1187 bufferedReader = new BufferedReader(new InputStreamReader(ClassLoader.getSystemResourceAsStream(path)));
1188
1189 while ((line = bufferedReader.readLine()) != null) {
1190 stringBuilder.append(line + "\n");
1191 }
1192
1193 } catch (IOException e) {
1194 e.printStackTrace();
1195 } finally {
1196 if (bufferedReader != null) {
1197 try {
1198 bufferedReader.close();
1199 } catch (IOException e) {
1200 e.printStackTrace();
1201 }
1202 }
1203 }
1204
1205 return stringBuilder.toString();
1206 }
1207
1208 /**
1209 * Checks if a URL string starts with a protocol that can be loaded by the webview
1210 * @param url URL string to check
1211 * @return
1212 */
1213 private static boolean hasValidProtocol(String url) {
1214 String urlLower = url.toLowerCase();
1215
1216 // check if protocol is present
1217 return (urlLower.startsWith("http://") || url.startsWith("https://") || urlLower.startsWith("ftp://") || urlLower.startsWith("file://"));
1218 }
1219
1220 /**
1221 * @return Whether the parser is running. If this is true then the parser is running,
1222 * however even if it is false, the parser may still be running (but it has been requested to stop)
1223 */
1224 public boolean isParserRunning() {
1225 return this._parserRunning;
1226 }
1227
1228 /**
1229 * Should be called when the web parser has finished converting a page
1230 */
1231 public void parserFinished() {
1232 this._parserRunning = false;
1233 }
1234
1235 /**
1236 * Cancels the current action being performed by the browser, such as loading a page or converting a page
1237 */
1238 public void cancel() {
1239 if(isParserRunning()) {
1240 this._parserRunning = false;
1241 } else {
1242 Platform.runLater(new Runnable() {
1243
1244 @Override
1245 public void run() {
1246 JfxBrowser.this._webEngine.getLoadWorker().cancel();
1247 }
1248 });
1249 }
1250 }
1251}
Note: See TracBrowser for help on using the repository browser.