1 /*
   2  * Copyright (c) 2008, 2015, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package modena;
  33 
  34 import java.awt.Graphics;
  35 import java.awt.Graphics2D;
  36 import java.awt.image.BufferedImage;
  37 import java.io.BufferedReader;
  38 import java.io.ByteArrayInputStream;
  39 import java.io.File;
  40 import java.io.IOException;
  41 import java.io.InputStream;
  42 import java.io.InputStreamReader;
  43 import java.net.MalformedURLException;
  44 import java.net.URL;
  45 import java.net.URLConnection;
  46 import java.net.URLStreamHandler;
  47 import java.net.URLStreamHandlerFactory;
  48 import java.util.Locale;
  49 import java.util.Map;
  50 import java.util.logging.Level;
  51 import java.util.logging.Logger;
  52 
  53 import javafx.application.Application;
  54 import javafx.application.Platform;
  55 import javafx.embed.swing.SwingFXUtils;
  56 import javafx.event.ActionEvent;
  57 import javafx.event.EventHandler;
  58 import javafx.fxml.FXMLLoader;
  59 import javafx.geometry.NodeOrientation;
  60 import javafx.geometry.Rectangle2D;
  61 import javafx.scene.Node;
  62 import javafx.scene.Scene;
  63 import javafx.scene.SnapshotParameters;
  64 import javafx.scene.control.Button;
  65 import javafx.scene.control.ChoiceBox;
  66 import javafx.scene.control.ColorPicker;
  67 import javafx.scene.control.ComboBox;
  68 import javafx.scene.control.Label;
  69 import javafx.scene.control.Menu;
  70 import javafx.scene.control.MenuBar;
  71 import javafx.scene.control.RadioMenuItem;
  72 import javafx.scene.control.ScrollPane;
  73 import javafx.scene.control.Separator;
  74 import javafx.scene.control.Tab;
  75 import javafx.scene.control.TabPane;
  76 import javafx.scene.control.ToggleButton;
  77 import javafx.scene.control.ToggleGroup;
  78 import javafx.scene.control.ToolBar;
  79 import javafx.scene.control.Tooltip;
  80 import javafx.scene.image.Image;
  81 import javafx.scene.image.ImageView;
  82 import javafx.scene.image.WritableImage;
  83 import javafx.scene.layout.BorderPane;
  84 import javafx.scene.layout.HBox;
  85 import javafx.scene.layout.Pane;
  86 import javafx.scene.paint.Color;
  87 import javafx.scene.transform.Scale;
  88 import javafx.stage.FileChooser;
  89 import javafx.stage.Stage;
  90 
  91 import javax.imageio.ImageIO;
  92 
  93 public class Modena extends Application {
  94     public static final String TEST = "test";
  95     static {
  96         System.getProperties().put("javafx.pseudoClassOverrideEnabled", "true");
  97     }
  98     private static final String testAppCssUrl = Modena.class.getResource("TestApp.css").toExternalForm();
  99     private static String MODENA_STYLESHEET_URL;
 100     private static String MODENA_EMBEDDED_STYLESHEET_URL;
 101     private static String MODENA_STYLESHEET_BASE;
 102     private static String CASPIAN_STYLESHEET_URL;
 103     private static String CASPIAN_STYLESHEET_BASE;
 104     static {
 105         try {
 106             // these are not supported ways to find the platform themes and may 
 107             // change release to release. Just used here for testing.
 108             File caspianCssFile = new File("../../../modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/caspian/caspian.css");
 109             if (!caspianCssFile.exists()) {
 110                 caspianCssFile = new File("rt/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/caspian/caspian.css");
 111             }
 112             String jfxrtPath = Application.class.getProtectionDomain().getCodeSource().getLocation().getPath();
 113             CASPIAN_STYLESHEET_URL = caspianCssFile.exists() ?
 114                     caspianCssFile.toURI().toURL().toExternalForm() : "jar:file:" + jfxrtPath +
 115                     "!/com/sun/javafx/scene/control/skin/caspian/caspian.css";
 116             File modenaCssFile = new File("../../../modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css");
 117             if (!modenaCssFile.exists()) {
 118                 modenaCssFile = new File("rt/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css");
 119                 System.out.println("modenaCssFile = " + modenaCssFile);
 120                 System.out.println("modenaCssFile = " + modenaCssFile.getAbsolutePath());
 121             }
 122             MODENA_STYLESHEET_URL = modenaCssFile.exists() ? 
 123                     modenaCssFile.toURI().toURL().toExternalForm() : "jar:file:" + jfxrtPath +
 124                     "!/com/sun/javafx/scene/control/skin/modena/modena.css";
 125             MODENA_STYLESHEET_BASE = MODENA_STYLESHEET_URL.substring(0,MODENA_STYLESHEET_URL.lastIndexOf('/')+1);
 126             CASPIAN_STYLESHEET_BASE = CASPIAN_STYLESHEET_URL.substring(0,CASPIAN_STYLESHEET_URL.lastIndexOf('/')+1);
 127             MODENA_EMBEDDED_STYLESHEET_URL = MODENA_STYLESHEET_BASE + "modena-embedded-performance.css";
 128             System.out.println("MODENA_EMBEDDED_STYLESHEET_URL = " + MODENA_EMBEDDED_STYLESHEET_URL);
 129         } catch (MalformedURLException ex) {
 130             Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 131         }
 132     }
 133     
 134     private BorderPane outerRoot;
 135     private BorderPane root;
 136     private SamplePageNavigation samplePageNavigation;
 137     private SamplePage samplePage;
 138     private Node mosaic;
 139     private Node heightTest;
 140     private SimpleWindowPage simpleWindows;
 141     private Node combinationsTest;
 142     private Node customerTest;
 143     private Stage mainStage;
 144     private Color backgroundColor;
 145     private Color baseColor;
 146     private Color accentColor;
 147     private String fontName = null;
 148     private int fontSize = 13;
 149     private String styleSheetContent = "";
 150     private String styleSheetBase = "";
 151     private ToggleButton modenaButton,retinaButton,rtlButton,embeddedPerformanceButton;
 152     private TabPane contentTabs;
 153     private boolean test = false;
 154     private boolean embeddedPerformanceMode = false;
 155     private final EventHandler<ActionEvent> rebuild = event -> Platform.runLater(() -> {
 156         updateUserAgentStyleSheet();
 157         rebuildUI(modenaButton.isSelected(), retinaButton.isSelected(),
 158                   contentTabs.getSelectionModel().getSelectedIndex(),
 159                   samplePageNavigation.getCurrentSection());
 160     });
 161     
 162     private static Modena instance;
 163 
 164     public static Modena getInstance() {
 165         return instance;
 166     }
 167     
 168     public Map<String, Node> getContent() {
 169         return samplePage.getContent();
 170     }
 171 
 172     public void setRetinaMode(boolean retinaMode) {
 173         if (retinaMode) {
 174             contentTabs.getTransforms().setAll(new Scale(2,2));
 175         } else {
 176             contentTabs.getTransforms().setAll(new Scale(1,1));
 177         }
 178         contentTabs.requestLayout();
 179     }
 180     
 181     public void restart() {
 182         mainStage.close();
 183         root = null;
 184         accentColor = null;
 185         baseColor = null;
 186         backgroundColor = null;
 187         fontName = null;
 188         fontSize = 13;
 189         try {
 190             start(new Stage());
 191         } catch (Exception ex) {
 192             throw new RuntimeException("Failed to start another Modena window", ex);
 193         }
 194     }
 195     
 196     @Override public void start(Stage stage) throws Exception {
 197         if (getParameters().getRaw().contains(TEST)) {
 198             test = true;
 199         }
 200         mainStage = stage;
 201         // set user agent stylesheet
 202         updateUserAgentStyleSheet(true);
 203         // build Menu Bar
 204         outerRoot = new BorderPane();
 205         outerRoot.setTop(buildMenuBar());
 206         outerRoot.setCenter(root);
 207         // build UI
 208         rebuildUI(true,false,0, null);
 209         // show UI
 210         Scene scene = new Scene(outerRoot, 1024, 768);
 211         scene.getStylesheets().add(testAppCssUrl);
 212         stage.setScene(scene);
 213         stage.setTitle("Modena");
 214 //        stage.setIconified(test); // TODO: Blocked by http://javafx-jira.kenai.com/browse/JMY-203
 215         stage.show(); // see SamplePage.java:110 comment on how test fails without having stage shown
 216         instance = this;
 217     }
 218     
 219     private MenuBar buildMenuBar() {
 220         MenuBar menuBar = new MenuBar();
 221         menuBar.setUseSystemMenuBar(true);
 222         Menu fontSizeMenu = new Menu("Font");
 223         ToggleGroup tg = new ToggleGroup();
 224         fontSizeMenu.getItems().addAll(
 225             buildFontRadioMenuItem("System Default", null, 0, tg),
 226             buildFontRadioMenuItem("Mac (13px)", "Lucida Grande", 13, tg),
 227             buildFontRadioMenuItem("Windows 100% (12px)", "Segoe UI", 12, tg),
 228             buildFontRadioMenuItem("Windows 125% (15px)", "Segoe UI", 15, tg),
 229             buildFontRadioMenuItem("Windows 150% (18px)", "Segoe UI", 18, tg),
 230             buildFontRadioMenuItem("Linux (13px)", "Lucida Sans", 13, tg),
 231             buildFontRadioMenuItem("Embedded Touch (22px)", "Arial", 22, tg),
 232             buildFontRadioMenuItem("Embedded Small (9px)", "Arial", 9, tg)
 233         );
 234         menuBar.getMenus().add(fontSizeMenu);
 235         return menuBar;
 236     }
 237     
 238     private void updateUserAgentStyleSheet() {
 239         updateUserAgentStyleSheet(modenaButton.isSelected());
 240     }
 241     
 242     private void updateUserAgentStyleSheet(boolean modena) {
 243         final SamplePage.Section scrolledSection = samplePageNavigation==null? null : samplePageNavigation.getCurrentSection();
 244         styleSheetContent = modena ?
 245                 loadUrl(MODENA_STYLESHEET_URL) :
 246                 loadUrl(CASPIAN_STYLESHEET_URL);
 247         if (!modena &&
 248             (baseColor == null || baseColor == Color.TRANSPARENT) &&
 249             (backgroundColor == null || backgroundColor == Color.TRANSPARENT) &&
 250             (accentColor == null || accentColor == Color.TRANSPARENT) &&
 251             (fontName == null)) {
 252             // no customizations
 253             System.out.println("USING NO CUSTIMIZATIONS TO CSS, stylesheet = "+(modena?"modena":"caspian"));
 254 
 255             // load theme
 256             setUserAgentStylesheet("internal:stylesheet"+Math.random()+".css");
 257             if (root != null) root.requestLayout();
 258             // restore scrolled section
 259             Platform.runLater(() -> samplePageNavigation.setCurrentSection(scrolledSection));
 260             return;
 261         }
 262         if (modena && embeddedPerformanceMode) styleSheetContent += loadUrl(MODENA_EMBEDDED_STYLESHEET_URL);
 263         styleSheetBase = modena ? MODENA_STYLESHEET_BASE : CASPIAN_STYLESHEET_BASE;
 264         styleSheetContent += "\n.root {\n";
 265         System.out.println("baseColor = "+baseColor);
 266         System.out.println("accentColor = " + accentColor);
 267         System.out.println("backgroundColor = " + backgroundColor);
 268         if (baseColor != null && baseColor != Color.TRANSPARENT) {
 269             styleSheetContent += "    -fx-base:" + colorToRGBA(baseColor) + ";\n";
 270         }
 271         if (backgroundColor != null && backgroundColor != Color.TRANSPARENT) {
 272             styleSheetContent += "    -fx-background:" + colorToRGBA(backgroundColor) + ";\n";
 273         }
 274         if (accentColor != null && accentColor != Color.TRANSPARENT) {
 275             styleSheetContent += "    -fx-accent:" + colorToRGBA(accentColor) + ";\n";
 276         }
 277         if (fontName != null) {
 278             styleSheetContent += "    -fx-font:"+fontSize+"px \""+fontName+"\";\n";
 279         }
 280         styleSheetContent += "}\n";
 281         
 282         // set white background for caspian
 283         if (!modena) {
 284             styleSheetContent += ".needs-background {\n-fx-background-color: white;\n}";
 285         }
 286             
 287         // load theme
 288         setUserAgentStylesheet("internal:stylesheet"+Math.random()+".css");
 289         
 290         if (root != null) root.requestLayout();
 291 
 292         // restore scrolled section
 293         Platform.runLater(() -> samplePageNavigation.setCurrentSection(scrolledSection));
 294     }
 295     
 296     private void rebuildUI(boolean modena, boolean retina, int selectedTab, final SamplePage.Section scrolledSection) {
 297         try {
 298             if (root == null) {
 299                 root = new BorderPane();
 300                 outerRoot.setCenter(root);
 301             } else {
 302                 // clear out old UI
 303                 root.setTop(null);
 304                 root.setCenter(null);
 305             }
 306             // Create sample page and nav
 307             samplePageNavigation = new SamplePageNavigation();
 308             samplePage = samplePageNavigation.getSamplePage();
 309             // Create Content Area
 310             contentTabs = new TabPane();
 311             Tab tab1 = new Tab("All Controls");
 312             tab1.setContent(samplePageNavigation);
 313             Tab tab2 = new Tab("UI Mosaic");
 314             tab2.setContent(new ScrollPane(mosaic = (Node)FXMLLoader.load(Modena.class.getResource("ui-mosaic.fxml"))));
 315 
 316             Tab tab3 = new Tab("Alignment Test");
 317             tab3.setContent(new ScrollPane(heightTest =
 318                     (Node)FXMLLoader.load(Modena.class.getResource("SameHeightTest.fxml"))));
 319 
 320             Tab tab4 = new Tab("Simple Windows");
 321             tab4.setContent(new ScrollPane(simpleWindows = new SimpleWindowPage()));
 322 
 323             Tab tab5 = new Tab("Combinations");
 324             tab5.setContent(new ScrollPane(combinationsTest =
 325                     (Node)FXMLLoader.load(Modena.class.getResource("CombinationTest.fxml"))));
 326 
 327             // Customer example from bug report http://javafx-jira.kenai.com/browse/DTL-5561
 328             Tab tab6 = new Tab("Customer Example");
 329             tab6.setContent(new ScrollPane(customerTest =
 330                     (Node)FXMLLoader.load(Modena.class.getResource("ScottSelvia.fxml"))));
 331 
 332             contentTabs.getTabs().addAll(tab1, tab2, tab3, tab4, tab5, tab6);
 333             contentTabs.getSelectionModel().select(selectedTab);
 334             samplePage.setMouseTransparent(test);
 335             // height test set selection for 
 336             Platform.runLater(() -> {
 337                 for (Node n: heightTest.lookupAll(".choice-box")) {
 338                     ((ChoiceBox)n).getSelectionModel().selectFirst();
 339                 }
 340                 for (Node n: heightTest.lookupAll(".combo-box")) {
 341                     ((ComboBox)n).getSelectionModel().selectFirst();
 342                 }
 343             });
 344             // Create Toolbar
 345             retinaButton = new ToggleButton("@2x");
 346             retinaButton.setSelected(retina);
 347             retinaButton.setOnAction(event -> {
 348                 ToggleButton btn = (ToggleButton)event.getSource();
 349                 setRetinaMode(btn.isSelected());
 350             });
 351             ToggleGroup themesToggleGroup = new ToggleGroup();
 352             modenaButton = new ToggleButton("Modena");
 353             modenaButton.setToggleGroup(themesToggleGroup);
 354             modenaButton.setSelected(modena);
 355             modenaButton.setOnAction(rebuild);
 356             modenaButton.getStyleClass().add("left-pill:");
 357             ToggleButton caspianButton = new ToggleButton("Caspian");
 358             caspianButton.setToggleGroup(themesToggleGroup);
 359             caspianButton.setSelected(!modena);
 360             caspianButton.setOnAction(rebuild);
 361             caspianButton.getStyleClass().add("right-pill");
 362             Button reloadButton = new Button("", new ImageView(new Image(Modena.class.getResource("reload_12x14.png").toString())));
 363             reloadButton.setOnAction(rebuild);
 364 
 365             rtlButton = new ToggleButton("RTL");
 366             rtlButton.setOnAction(event -> root.setNodeOrientation(rtlButton.isSelected() ?
 367                     NodeOrientation.RIGHT_TO_LEFT : NodeOrientation.LEFT_TO_RIGHT));
 368 
 369             embeddedPerformanceButton = new ToggleButton("EP");
 370             embeddedPerformanceButton.setSelected(embeddedPerformanceMode);
 371             embeddedPerformanceButton.setTooltip(new Tooltip("Apply Embedded Performance extra stylesheet"));
 372             embeddedPerformanceButton.setOnAction(event -> {
 373                 embeddedPerformanceMode = embeddedPerformanceButton.isSelected();
 374                 rebuild.handle(event);
 375             });
 376 
 377             Button saveButton = new Button("Save...");
 378             saveButton.setOnAction(saveBtnHandler);
 379 
 380             Button restartButton = new Button("Restart");
 381             retinaButton.setOnAction(event -> restart());
 382 
 383             ToolBar toolBar = new ToolBar(new HBox(modenaButton, caspianButton), reloadButton, rtlButton,
 384                     embeddedPerformanceButton, new Separator(), retinaButton,
 385                     new Label("Base:"),
 386                     createBaseColorPicker(),
 387                     new Label("Background:"),
 388                     createBackgroundColorPicker(),
 389                     new Label("Accent:"),
 390                     createAccentColorPicker(),
 391                     new Separator(), saveButton, restartButton
 392                     );
 393             toolBar.setId("TestAppToolbar");
 394             // Create content group used for scaleing @2x
 395             final Pane contentGroup = new Pane() {
 396                 @Override protected void layoutChildren() {
 397                     double scale = contentTabs.getTransforms().isEmpty() ? 1 : ((Scale)contentTabs.getTransforms().get(0)).getX();
 398                     contentTabs.resizeRelocate(0,0,getWidth()/scale, getHeight()/scale);
 399                 }
 400             };
 401             contentGroup.getChildren().add(contentTabs);
 402             // populate root
 403             root.setTop(toolBar);
 404             root.setCenter(contentGroup);
 405             
 406             samplePage.getStyleClass().add("needs-background");
 407             mosaic.getStyleClass().add("needs-background");
 408             heightTest.getStyleClass().add("needs-background");
 409             combinationsTest.getStyleClass().add("needs-background");
 410             customerTest.getStyleClass().add("needs-background");
 411             simpleWindows.setModena(modena);
 412             // apply retina scale
 413             if (retina) {
 414                 contentTabs.getTransforms().setAll(new Scale(2,2));
 415             }
 416             root.applyCss();
 417             // update state
 418             Platform.runLater(() -> {
 419                 // move focus out of the way
 420                 modenaButton.requestFocus();
 421                 samplePageNavigation.setCurrentSection(scrolledSection);
 422             });
 423         } catch (IOException ex) {
 424             Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 425         }
 426     }
 427 
 428     public RadioMenuItem buildFontRadioMenuItem(String name, final String in_fontName, final int in_fontSize, ToggleGroup tg) {
 429         RadioMenuItem rmItem = new RadioMenuItem(name);
 430         rmItem.setOnAction(event -> setFont(in_fontName, in_fontSize));
 431         rmItem.setStyle("-fx-font: " + in_fontSize + "px \"" + in_fontName + "\";");
 432         rmItem.setToggleGroup(tg);
 433         return rmItem;
 434     }
 435     
 436     public void setFont(String in_fontName, int in_fontSize) {
 437         System.out.println("===================================================================");
 438         System.out.println("==   SETTING FONT TO "+in_fontName+" "+in_fontSize+"px");
 439         System.out.println("===================================================================");
 440         fontName = in_fontName;
 441         fontSize = in_fontSize;
 442         updateUserAgentStyleSheet();
 443     }
 444     
 445     private ColorPicker createBaseColorPicker() {
 446         ColorPicker colorPicker = new ColorPicker(Color.TRANSPARENT);
 447         colorPicker.getCustomColors().addAll(
 448                 Color.TRANSPARENT,
 449                 Color.web("#f3622d"),
 450                 Color.web("#fba71b"),
 451                 Color.web("#57b757"),
 452                 Color.web("#41a9c9"),
 453                 Color.web("#888"),
 454                 Color.RED,
 455                 Color.ORANGE,
 456                 Color.YELLOW,
 457                 Color.GREEN,
 458                 Color.CYAN,
 459                 Color.BLUE,
 460                 Color.PURPLE,
 461                 Color.MAGENTA,
 462                 Color.BLACK
 463         );
 464         colorPicker.valueProperty().addListener((observable, oldValue, c) -> setBaseColor(c));
 465         return colorPicker;
 466     }
 467     
 468     public void setBaseColor(Color c) {
 469         if (c == null) {
 470             baseColor = null;
 471         } else {
 472             baseColor = c;
 473         }
 474         updateUserAgentStyleSheet();
 475     }
 476     
 477     private ColorPicker createBackgroundColorPicker() {
 478         ColorPicker colorPicker = new ColorPicker(Color.TRANSPARENT);
 479         colorPicker.getCustomColors().addAll(
 480                 Color.TRANSPARENT,
 481                 Color.web("#f3622d"),
 482                 Color.web("#fba71b"),
 483                 Color.web("#57b757"),
 484                 Color.web("#41a9c9"),
 485                 Color.web("#888"),
 486                 Color.RED,
 487                 Color.ORANGE,
 488                 Color.YELLOW,
 489                 Color.GREEN,
 490                 Color.CYAN,
 491                 Color.BLUE,
 492                 Color.PURPLE,
 493                 Color.MAGENTA,
 494                 Color.BLACK
 495         );
 496         colorPicker.valueProperty().addListener((observable, oldValue, c) -> {
 497             if (c == null) {
 498                 backgroundColor = null;
 499             } else {
 500                 backgroundColor = c;
 501             }
 502             updateUserAgentStyleSheet();
 503         });
 504         return colorPicker;
 505     }
 506     
 507     private ColorPicker createAccentColorPicker() {
 508         ColorPicker colorPicker = new ColorPicker(Color.web("#0096C9"));
 509         colorPicker.getCustomColors().addAll(
 510                 Color.TRANSPARENT,
 511                 Color.web("#0096C9"),
 512                 Color.web("#4fb6d6"),
 513                 Color.web("#f3622d"),
 514                 Color.web("#fba71b"),
 515                 Color.web("#57b757"),
 516                 Color.web("#41a9c9"),
 517                 Color.web("#888"),
 518                 Color.RED,
 519                 Color.ORANGE,
 520                 Color.YELLOW,
 521                 Color.GREEN,
 522                 Color.CYAN,
 523                 Color.BLUE,
 524                 Color.PURPLE,
 525                 Color.MAGENTA,
 526                 Color.BLACK
 527         );
 528         colorPicker.valueProperty().addListener((observable, oldValue, c) -> setAccentColor(c));
 529         return colorPicker;
 530     }
 531 
 532     public void setAccentColor(Color c) {
 533         if (c == null) {
 534             accentColor = null;
 535         } else {
 536             accentColor = c;
 537         }
 538         updateUserAgentStyleSheet();
 539     }
 540     
 541     private EventHandler<ActionEvent> saveBtnHandler = event -> {
 542         FileChooser fc = new FileChooser();
 543         fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG", "*.png"));
 544         File file = fc.showSaveDialog(mainStage);
 545         if (file != null) {
 546             try {
 547                 samplePage.getStyleClass().add("root");
 548                 int width = (int)(samplePage.getLayoutBounds().getWidth()+0.5d);
 549                 int height = (int)(samplePage.getLayoutBounds().getHeight()+0.5d);
 550                 BufferedImage imgBuffer = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
 551                 Graphics2D g2 = imgBuffer.createGraphics();
 552                 for (int y=0; y<height; y+=2048) {
 553                     SnapshotParameters snapshotParameters = new SnapshotParameters();
 554                     int remainingHeight = Math.min(2048, height - y);
 555                     snapshotParameters.setViewport(new Rectangle2D(0,y,width,remainingHeight));
 556                     WritableImage img = samplePage.snapshot(snapshotParameters, null);
 557                     g2.drawImage(SwingFXUtils.fromFXImage(img,null),0,y,null);
 558                 }
 559                 g2.dispose();
 560                 ImageIO.write(imgBuffer, "PNG", file);
 561                 System.out.println("Written image: "+file.getAbsolutePath());
 562             } catch (IOException ex) {
 563                 Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 564             }
 565         }
 566     };
 567     
 568     public static void main(String[] args) {
 569         launch(args);
 570     }
 571     
 572     /** Utility method to load a URL into a string */
 573     private static String loadUrl(String url) {
 574         StringBuilder sb = new StringBuilder();
 575         try {
 576             BufferedReader br = new BufferedReader(new InputStreamReader(new URL(url).openStream()));
 577             String line;
 578             while ((line = br.readLine()) != null) {
 579                 sb.append(line);
 580                 sb.append('\n');
 581             }
 582         } catch (IOException ex) {
 583             Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 584         }
 585         return sb.toString();
 586     }
 587     
 588     // =========================================================================
 589     // URL Handler to create magic "internal:stylesheet.css" url for our css string buffer
 590     {
 591         URL.setURLStreamHandlerFactory(new StringURLStreamHandlerFactory());
 592     }
 593 
 594     private String colorToRGBA(Color color) {
 595         // Older version didn't care about opacity
 596 //        return String.format((Locale) null, "#%02x%02x%02x", 
 597 //                Math.round(color.getRed() * 255), 
 598 //                Math.round(color.getGreen() * 255), 
 599 //                Math.round(color.getBlue() * 255));
 600         return String.format((Locale) null, "rgba(%d, %d, %d, %f)", 
 601             (int) Math.round(color.getRed() * 255), 
 602             (int) Math.round(color.getGreen() * 255), 
 603             (int) Math.round(color.getBlue() * 255),
 604             color.getOpacity());
 605     }
 606 
 607     /**
 608      * Simple URLConnection that always returns the content of the cssBuffer
 609      */
 610     private class StringURLConnection extends URLConnection {
 611         public StringURLConnection(URL url){
 612             super(url);
 613         }
 614         
 615         @Override public void connect() throws IOException {}
 616 
 617         @Override public InputStream getInputStream() throws IOException {
 618             return new ByteArrayInputStream(styleSheetContent.getBytes("UTF-8"));
 619         }
 620     }
 621     
 622     private class StringURLStreamHandlerFactory implements URLStreamHandlerFactory {
 623         URLStreamHandler streamHandler = new URLStreamHandler(){
 624             @Override protected URLConnection openConnection(URL url) throws IOException {
 625                 if (url.toString().toLowerCase().endsWith(".css")) {
 626                     return new StringURLConnection(url);
 627                 } else {
 628                     return new URL(styleSheetBase+url.getFile()).openConnection();
 629                 }
 630             }
 631         };
 632         @Override public URLStreamHandler createURLStreamHandler(String protocol) {
 633             if ("internal".equals(protocol)) {
 634                 return streamHandler;
 635             }
 636             return null;
 637         }
 638     }
 639 }