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             CASPIAN_STYLESHEET_URL = caspianCssFile.exists() ? 
 113                     caspianCssFile.toURI().toURL().toExternalForm() :
 114                     com.sun.javafx.scene.control.skin.ButtonSkin.class.getResource("caspian/caspian.css").toExternalForm();
 115             File modenaCssFile = new File("../../../modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css");
 116             if (!modenaCssFile.exists()) {
 117                 modenaCssFile = new File("rt/modules/controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css");
 118                 System.out.println("modenaCssFile = " + modenaCssFile);
 119                 System.out.println("modenaCssFile = " + modenaCssFile.getAbsolutePath());
 120             }
 121             MODENA_STYLESHEET_URL = modenaCssFile.exists() ? 
 122                     modenaCssFile.toURI().toURL().toExternalForm() : 
 123                     com.sun.javafx.scene.control.skin.ButtonSkin.class.getResource("modena/modena.css").toExternalForm();
 124             MODENA_STYLESHEET_BASE = MODENA_STYLESHEET_URL.substring(0,MODENA_STYLESHEET_URL.lastIndexOf('/')+1);
 125             CASPIAN_STYLESHEET_BASE = CASPIAN_STYLESHEET_URL.substring(0,CASPIAN_STYLESHEET_URL.lastIndexOf('/')+1);
 126             MODENA_EMBEDDED_STYLESHEET_URL = MODENA_STYLESHEET_BASE + "modena-embedded-performance.css";
 127             System.out.println("MODENA_EMBEDDED_STYLESHEET_URL = " + MODENA_EMBEDDED_STYLESHEET_URL);
 128         } catch (MalformedURLException ex) {
 129             Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 130         }
 131     }
 132     
 133     private BorderPane outerRoot;
 134     private BorderPane root;
 135     private SamplePageNavigation samplePageNavigation;
 136     private SamplePage samplePage;
 137     private Node mosaic;
 138     private Node heightTest;
 139     private SimpleWindowPage simpleWindows;
 140     private Node combinationsTest;
 141     private Node customerTest;
 142     private Stage mainStage;
 143     private Color backgroundColor;
 144     private Color baseColor;
 145     private Color accentColor;
 146     private String fontName = null;
 147     private int fontSize = 13;
 148     private String styleSheetContent = "";
 149     private String styleSheetBase = "";
 150     private ToggleButton modenaButton,retinaButton,rtlButton,embeddedPerformanceButton;
 151     private TabPane contentTabs;
 152     private boolean test = false;
 153     private boolean embeddedPerformanceMode = false;
 154     private final EventHandler<ActionEvent> rebuild = event -> Platform.runLater(() -> {
 155         updateUserAgentStyleSheet();
 156         rebuildUI(modenaButton.isSelected(), retinaButton.isSelected(),
 157                   contentTabs.getSelectionModel().getSelectedIndex(),
 158                   samplePageNavigation.getCurrentSection());
 159     });
 160     
 161     private static Modena instance;
 162 
 163     public static Modena getInstance() {
 164         return instance;
 165     }
 166     
 167     public Map<String, Node> getContent() {
 168         return samplePage.getContent();
 169     }
 170 
 171     public void setRetinaMode(boolean retinaMode) {
 172         if (retinaMode) {
 173             contentTabs.getTransforms().setAll(new Scale(2,2));
 174         } else {
 175             contentTabs.getTransforms().setAll(new Scale(1,1));
 176         }
 177         contentTabs.requestLayout();
 178     }
 179     
 180     public void restart() {
 181         mainStage.close();
 182         root = null;
 183         accentColor = null;
 184         baseColor = null;
 185         backgroundColor = null;
 186         fontName = null;
 187         fontSize = 13;
 188         try {
 189             start(new Stage());
 190         } catch (Exception ex) {
 191             throw new RuntimeException("Failed to start another Modena window", ex);
 192         }
 193     }
 194     
 195     @Override public void start(Stage stage) throws Exception {
 196         if (getParameters().getRaw().contains(TEST)) {
 197             test = true;
 198         }
 199         mainStage = stage;
 200         // set user agent stylesheet
 201         updateUserAgentStyleSheet(true);
 202         // build Menu Bar
 203         outerRoot = new BorderPane();
 204         outerRoot.setTop(buildMenuBar());
 205         outerRoot.setCenter(root);
 206         // build UI
 207         rebuildUI(true,false,0, null);
 208         // show UI
 209         Scene scene = new Scene(outerRoot, 1024, 768);
 210         scene.getStylesheets().add(testAppCssUrl);
 211         stage.setScene(scene);
 212         stage.setTitle("Modena");
 213 //        stage.setIconified(test); // TODO: Blocked by http://javafx-jira.kenai.com/browse/JMY-203
 214         stage.show(); // see SamplePage.java:110 comment on how test fails without having stage shown
 215         instance = this;
 216     }
 217     
 218     private MenuBar buildMenuBar() {
 219         MenuBar menuBar = new MenuBar();
 220         menuBar.setUseSystemMenuBar(true);
 221         Menu fontSizeMenu = new Menu("Font");
 222         ToggleGroup tg = new ToggleGroup();
 223         fontSizeMenu.getItems().addAll(
 224             buildFontRadioMenuItem("System Default", null, 0, tg),
 225             buildFontRadioMenuItem("Mac (13px)", "Lucida Grande", 13, tg),
 226             buildFontRadioMenuItem("Windows 100% (12px)", "Segoe UI", 12, tg),
 227             buildFontRadioMenuItem("Windows 125% (15px)", "Segoe UI", 15, tg),
 228             buildFontRadioMenuItem("Windows 150% (18px)", "Segoe UI", 18, tg),
 229             buildFontRadioMenuItem("Linux (13px)", "Lucida Sans", 13, tg),
 230             buildFontRadioMenuItem("Embedded Touch (22px)", "Arial", 22, tg),
 231             buildFontRadioMenuItem("Embedded Small (9px)", "Arial", 9, tg)
 232         );
 233         menuBar.getMenus().add(fontSizeMenu);
 234         return menuBar;
 235     }
 236     
 237     private void updateUserAgentStyleSheet() {
 238         updateUserAgentStyleSheet(modenaButton.isSelected());
 239     }
 240     
 241     private void updateUserAgentStyleSheet(boolean modena) {
 242         final SamplePage.Section scrolledSection = samplePageNavigation==null? null : samplePageNavigation.getCurrentSection();
 243         styleSheetContent = modena ?
 244                 loadUrl(MODENA_STYLESHEET_URL) :
 245                 loadUrl(CASPIAN_STYLESHEET_URL);
 246         if (!modena &&
 247             (baseColor == null || baseColor == Color.TRANSPARENT) &&
 248             (backgroundColor == null || backgroundColor == Color.TRANSPARENT) &&
 249             (accentColor == null || accentColor == Color.TRANSPARENT) &&
 250             (fontName == null)) {
 251             // no customizations
 252             System.out.println("USING NO CUSTIMIZATIONS TO CSS, stylesheet = "+(modena?"modena":"caspian"));
 253 
 254             // load theme
 255             setUserAgentStylesheet("internal:stylesheet"+Math.random()+".css");
 256             if (root != null) root.requestLayout();
 257             // restore scrolled section
 258             Platform.runLater(() -> samplePageNavigation.setCurrentSection(scrolledSection));
 259             return;
 260         }
 261         if (modena && embeddedPerformanceMode) styleSheetContent += loadUrl(MODENA_EMBEDDED_STYLESHEET_URL);
 262         styleSheetBase = modena ? MODENA_STYLESHEET_BASE : CASPIAN_STYLESHEET_BASE;
 263         styleSheetContent += "\n.root {\n";
 264         System.out.println("baseColor = "+baseColor);
 265         System.out.println("accentColor = " + accentColor);
 266         System.out.println("backgroundColor = " + backgroundColor);
 267         if (baseColor != null && baseColor != Color.TRANSPARENT) {
 268             styleSheetContent += "    -fx-base:" + colorToRGBA(baseColor) + ";\n";
 269         }
 270         if (backgroundColor != null && backgroundColor != Color.TRANSPARENT) {
 271             styleSheetContent += "    -fx-background:" + colorToRGBA(backgroundColor) + ";\n";
 272         }
 273         if (accentColor != null && accentColor != Color.TRANSPARENT) {
 274             styleSheetContent += "    -fx-accent:" + colorToRGBA(accentColor) + ";\n";
 275         }
 276         if (fontName != null) {
 277             styleSheetContent += "    -fx-font:"+fontSize+"px \""+fontName+"\";\n";
 278         }
 279         styleSheetContent += "}\n";
 280         
 281         // set white background for caspian
 282         if (!modena) {
 283             styleSheetContent += ".needs-background {\n-fx-background-color: white;\n}";
 284         }
 285             
 286         // load theme
 287         setUserAgentStylesheet("internal:stylesheet"+Math.random()+".css");
 288         
 289         if (root != null) root.requestLayout();
 290 
 291         // restore scrolled section
 292         Platform.runLater(() -> samplePageNavigation.setCurrentSection(scrolledSection));
 293     }
 294     
 295     private void rebuildUI(boolean modena, boolean retina, int selectedTab, final SamplePage.Section scrolledSection) {
 296         try {
 297             if (root == null) {
 298                 root = new BorderPane();
 299                 outerRoot.setCenter(root);
 300             } else {
 301                 // clear out old UI
 302                 root.setTop(null);
 303                 root.setCenter(null);
 304             }
 305             // Create sample page and nav
 306             samplePageNavigation = new SamplePageNavigation();
 307             samplePage = samplePageNavigation.getSamplePage();
 308             // Create Content Area
 309             contentTabs = new TabPane();
 310             Tab tab1 = new Tab("All Controls");
 311             tab1.setContent(samplePageNavigation);
 312             Tab tab2 = new Tab("UI Mosaic");
 313             tab2.setContent(new ScrollPane(mosaic = (Node)FXMLLoader.load(Modena.class.getResource("ui-mosaic.fxml"))));
 314 
 315             Tab tab3 = new Tab("Alignment Test");
 316             tab3.setContent(new ScrollPane(heightTest =
 317                     (Node)FXMLLoader.load(Modena.class.getResource("SameHeightTest.fxml"))));
 318 
 319             Tab tab4 = new Tab("Simple Windows");
 320             tab4.setContent(new ScrollPane(simpleWindows = new SimpleWindowPage()));
 321 
 322             Tab tab5 = new Tab("Combinations");
 323             tab5.setContent(new ScrollPane(combinationsTest =
 324                     (Node)FXMLLoader.load(Modena.class.getResource("CombinationTest.fxml"))));
 325 
 326             // Customer example from bug report http://javafx-jira.kenai.com/browse/DTL-5561
 327             Tab tab6 = new Tab("Customer Example");
 328             tab6.setContent(new ScrollPane(customerTest =
 329                     (Node)FXMLLoader.load(Modena.class.getResource("ScottSelvia.fxml"))));
 330 
 331             contentTabs.getTabs().addAll(tab1, tab2, tab3, tab4, tab5, tab6);
 332             contentTabs.getSelectionModel().select(selectedTab);
 333             samplePage.setMouseTransparent(test);
 334             // height test set selection for 
 335             Platform.runLater(() -> {
 336                 for (Node n: heightTest.lookupAll(".choice-box")) {
 337                     ((ChoiceBox)n).getSelectionModel().selectFirst();
 338                 }
 339                 for (Node n: heightTest.lookupAll(".combo-box")) {
 340                     ((ComboBox)n).getSelectionModel().selectFirst();
 341                 }
 342             });
 343             // Create Toolbar
 344             retinaButton = new ToggleButton("@2x");
 345             retinaButton.setSelected(retina);
 346             retinaButton.setOnAction(event -> {
 347                 ToggleButton btn = (ToggleButton)event.getSource();
 348                 setRetinaMode(btn.isSelected());
 349             });
 350             ToggleGroup themesToggleGroup = new ToggleGroup();
 351             modenaButton = new ToggleButton("Modena");
 352             modenaButton.setToggleGroup(themesToggleGroup);
 353             modenaButton.setSelected(modena);
 354             modenaButton.setOnAction(rebuild);
 355             modenaButton.getStyleClass().add("left-pill:");
 356             ToggleButton caspianButton = new ToggleButton("Caspian");
 357             caspianButton.setToggleGroup(themesToggleGroup);
 358             caspianButton.setSelected(!modena);
 359             caspianButton.setOnAction(rebuild);
 360             caspianButton.getStyleClass().add("right-pill");
 361             Button reloadButton = new Button("", new ImageView(new Image(Modena.class.getResource("reload_12x14.png").toString())));
 362             reloadButton.setOnAction(rebuild);
 363 
 364             rtlButton = new ToggleButton("RTL");
 365             rtlButton.setOnAction(event -> root.setNodeOrientation(rtlButton.isSelected() ?
 366                     NodeOrientation.RIGHT_TO_LEFT : NodeOrientation.LEFT_TO_RIGHT));
 367 
 368             embeddedPerformanceButton = new ToggleButton("EP");
 369             embeddedPerformanceButton.setSelected(embeddedPerformanceMode);
 370             embeddedPerformanceButton.setTooltip(new Tooltip("Apply Embedded Performance extra stylesheet"));
 371             embeddedPerformanceButton.setOnAction(event -> {
 372                 embeddedPerformanceMode = embeddedPerformanceButton.isSelected();
 373                 rebuild.handle(event);
 374             });
 375 
 376             Button saveButton = new Button("Save...");
 377             saveButton.setOnAction(saveBtnHandler);
 378 
 379             Button restartButton = new Button("Restart");
 380             retinaButton.setOnAction(event -> restart());
 381 
 382             ToolBar toolBar = new ToolBar(new HBox(modenaButton, caspianButton), reloadButton, rtlButton,
 383                     embeddedPerformanceButton, new Separator(), retinaButton,
 384                     new Label("Base:"),
 385                     createBaseColorPicker(),
 386                     new Label("Background:"),
 387                     createBackgroundColorPicker(),
 388                     new Label("Accent:"),
 389                     createAccentColorPicker(),
 390                     new Separator(), saveButton, restartButton
 391                     );
 392             toolBar.setId("TestAppToolbar");
 393             // Create content group used for scaleing @2x
 394             final Pane contentGroup = new Pane() {
 395                 @Override protected void layoutChildren() {
 396                     double scale = contentTabs.getTransforms().isEmpty() ? 1 : ((Scale)contentTabs.getTransforms().get(0)).getX();
 397                     contentTabs.resizeRelocate(0,0,getWidth()/scale, getHeight()/scale);
 398                 }
 399             };
 400             contentGroup.getChildren().add(contentTabs);
 401             // populate root
 402             root.setTop(toolBar);
 403             root.setCenter(contentGroup);
 404             
 405             samplePage.getStyleClass().add("needs-background");
 406             mosaic.getStyleClass().add("needs-background");
 407             heightTest.getStyleClass().add("needs-background");
 408             combinationsTest.getStyleClass().add("needs-background");
 409             customerTest.getStyleClass().add("needs-background");
 410             simpleWindows.setModena(modena);
 411             // apply retina scale
 412             if (retina) {
 413                 contentTabs.getTransforms().setAll(new Scale(2,2));
 414             }
 415             root.applyCss();
 416             // update state
 417             Platform.runLater(() -> {
 418                 // move focus out of the way
 419                 modenaButton.requestFocus();
 420                 samplePageNavigation.setCurrentSection(scrolledSection);
 421             });
 422         } catch (IOException ex) {
 423             Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 424         }
 425     }
 426 
 427     public RadioMenuItem buildFontRadioMenuItem(String name, final String in_fontName, final int in_fontSize, ToggleGroup tg) {
 428         RadioMenuItem rmItem = new RadioMenuItem(name);
 429         rmItem.setOnAction(event -> setFont(in_fontName, in_fontSize));
 430         rmItem.setStyle("-fx-font: " + in_fontSize + "px \"" + in_fontName + "\";");
 431         rmItem.setToggleGroup(tg);
 432         return rmItem;
 433     }
 434     
 435     public void setFont(String in_fontName, int in_fontSize) {
 436         System.out.println("===================================================================");
 437         System.out.println("==   SETTING FONT TO "+in_fontName+" "+in_fontSize+"px");
 438         System.out.println("===================================================================");
 439         fontName = in_fontName;
 440         fontSize = in_fontSize;
 441         updateUserAgentStyleSheet();
 442     }
 443     
 444     private ColorPicker createBaseColorPicker() {
 445         ColorPicker colorPicker = new ColorPicker(Color.TRANSPARENT);
 446         colorPicker.getCustomColors().addAll(
 447                 Color.TRANSPARENT,
 448                 Color.web("#f3622d"),
 449                 Color.web("#fba71b"),
 450                 Color.web("#57b757"),
 451                 Color.web("#41a9c9"),
 452                 Color.web("#888"),
 453                 Color.RED,
 454                 Color.ORANGE,
 455                 Color.YELLOW,
 456                 Color.GREEN,
 457                 Color.CYAN,
 458                 Color.BLUE,
 459                 Color.PURPLE,
 460                 Color.MAGENTA,
 461                 Color.BLACK
 462         );
 463         colorPicker.valueProperty().addListener((observable, oldValue, c) -> setBaseColor(c));
 464         return colorPicker;
 465     }
 466     
 467     public void setBaseColor(Color c) {
 468         if (c == null) {
 469             baseColor = null;
 470         } else {
 471             baseColor = c;
 472         }
 473         updateUserAgentStyleSheet();
 474     }
 475     
 476     private ColorPicker createBackgroundColorPicker() {
 477         ColorPicker colorPicker = new ColorPicker(Color.TRANSPARENT);
 478         colorPicker.getCustomColors().addAll(
 479                 Color.TRANSPARENT,
 480                 Color.web("#f3622d"),
 481                 Color.web("#fba71b"),
 482                 Color.web("#57b757"),
 483                 Color.web("#41a9c9"),
 484                 Color.web("#888"),
 485                 Color.RED,
 486                 Color.ORANGE,
 487                 Color.YELLOW,
 488                 Color.GREEN,
 489                 Color.CYAN,
 490                 Color.BLUE,
 491                 Color.PURPLE,
 492                 Color.MAGENTA,
 493                 Color.BLACK
 494         );
 495         colorPicker.valueProperty().addListener((observable, oldValue, c) -> {
 496             if (c == null) {
 497                 backgroundColor = null;
 498             } else {
 499                 backgroundColor = c;
 500             }
 501             updateUserAgentStyleSheet();
 502         });
 503         return colorPicker;
 504     }
 505     
 506     private ColorPicker createAccentColorPicker() {
 507         ColorPicker colorPicker = new ColorPicker(Color.web("#0096C9"));
 508         colorPicker.getCustomColors().addAll(
 509                 Color.TRANSPARENT,
 510                 Color.web("#0096C9"),
 511                 Color.web("#4fb6d6"),
 512                 Color.web("#f3622d"),
 513                 Color.web("#fba71b"),
 514                 Color.web("#57b757"),
 515                 Color.web("#41a9c9"),
 516                 Color.web("#888"),
 517                 Color.RED,
 518                 Color.ORANGE,
 519                 Color.YELLOW,
 520                 Color.GREEN,
 521                 Color.CYAN,
 522                 Color.BLUE,
 523                 Color.PURPLE,
 524                 Color.MAGENTA,
 525                 Color.BLACK
 526         );
 527         colorPicker.valueProperty().addListener((observable, oldValue, c) -> setAccentColor(c));
 528         return colorPicker;
 529     }
 530 
 531     public void setAccentColor(Color c) {
 532         if (c == null) {
 533             accentColor = null;
 534         } else {
 535             accentColor = c;
 536         }
 537         updateUserAgentStyleSheet();
 538     }
 539     
 540     private EventHandler<ActionEvent> saveBtnHandler = event -> {
 541         FileChooser fc = new FileChooser();
 542         fc.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG", "*.png"));
 543         File file = fc.showSaveDialog(mainStage);
 544         if (file != null) {
 545             try {
 546                 samplePage.getStyleClass().add("root");
 547                 int width = (int)(samplePage.getLayoutBounds().getWidth()+0.5d);
 548                 int height = (int)(samplePage.getLayoutBounds().getHeight()+0.5d);
 549                 BufferedImage imgBuffer = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
 550                 Graphics2D g2 = imgBuffer.createGraphics();
 551                 for (int y=0; y<height; y+=2048) {
 552                     SnapshotParameters snapshotParameters = new SnapshotParameters();
 553                     int remainingHeight = Math.min(2048, height - y);
 554                     snapshotParameters.setViewport(new Rectangle2D(0,y,width,remainingHeight));
 555                     WritableImage img = samplePage.snapshot(snapshotParameters, null);
 556                     g2.drawImage(SwingFXUtils.fromFXImage(img,null),0,y,null);
 557                 }
 558                 g2.dispose();
 559                 ImageIO.write(imgBuffer, "PNG", file);
 560                 System.out.println("Written image: "+file.getAbsolutePath());
 561             } catch (IOException ex) {
 562                 Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 563             }
 564         }
 565     };
 566     
 567     public static void main(String[] args) {
 568         launch(args);
 569     }
 570     
 571     /** Utility method to load a URL into a string */
 572     private static String loadUrl(String url) {
 573         StringBuilder sb = new StringBuilder();
 574         try {
 575             BufferedReader br = new BufferedReader(new InputStreamReader(new URL(url).openStream()));
 576             String line;
 577             while ((line = br.readLine()) != null) {
 578                 sb.append(line);
 579                 sb.append('\n');
 580             }
 581         } catch (IOException ex) {
 582             Logger.getLogger(Modena.class.getName()).log(Level.SEVERE, null, ex);
 583         }
 584         return sb.toString();
 585     }
 586     
 587     // =========================================================================
 588     // URL Handler to create magic "internal:stylesheet.css" url for our css string buffer
 589     {
 590         URL.setURLStreamHandlerFactory(new StringURLStreamHandlerFactory());
 591     }
 592 
 593     private String colorToRGBA(Color color) {
 594         // Older version didn't care about opacity
 595 //        return String.format((Locale) null, "#%02x%02x%02x", 
 596 //                Math.round(color.getRed() * 255), 
 597 //                Math.round(color.getGreen() * 255), 
 598 //                Math.round(color.getBlue() * 255));
 599         return String.format((Locale) null, "rgba(%d, %d, %d, %f)", 
 600             (int) Math.round(color.getRed() * 255), 
 601             (int) Math.round(color.getGreen() * 255), 
 602             (int) Math.round(color.getBlue() * 255),
 603             color.getOpacity());
 604     }
 605 
 606     /**
 607      * Simple URLConnection that always returns the content of the cssBuffer
 608      */
 609     private class StringURLConnection extends URLConnection {
 610         public StringURLConnection(URL url){
 611             super(url);
 612         }
 613         
 614         @Override public void connect() throws IOException {}
 615 
 616         @Override public InputStream getInputStream() throws IOException {
 617             return new ByteArrayInputStream(styleSheetContent.getBytes("UTF-8"));
 618         }
 619     }
 620     
 621     private class StringURLStreamHandlerFactory implements URLStreamHandlerFactory {
 622         URLStreamHandler streamHandler = new URLStreamHandler(){
 623             @Override protected URLConnection openConnection(URL url) throws IOException {
 624                 if (url.toString().toLowerCase().endsWith(".css")) {
 625                     return new StringURLConnection(url);
 626                 } else {
 627                     return new URL(styleSheetBase+url.getFile()).openConnection();
 628                 }
 629             }
 630         };
 631         @Override public URLStreamHandler createURLStreamHandler(String protocol) {
 632             if ("internal".equals(protocol)) {
 633                 return streamHandler;
 634             }
 635             return null;
 636         }
 637     }
 638 }