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

Last change on this file since 898 was 898, checked in by ngw8, 10 years ago

Added an extremely basic 'stop' function to the web parser, so conversions can be cancelled. There's no button, as the JFX thread is occupied with running Javascript most of the time (which has to be done on the JFX thread), meaning it's blocked from receiving clicks. Instead it's using a Swing event handler, listening for the escape key being released anywhere over the widget. (the escape now also stops loading the page)

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