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

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

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