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 
  33 package ensemble;
  34 
  35 import javafx.beans.property.ReadOnlyStringProperty;
  36 import javafx.beans.property.ReadOnlyStringWrapper;
  37 import javafx.beans.value.ChangeListener;
  38 import javafx.beans.value.ObservableValue;
  39 import javafx.css.PseudoClass;
  40 import javafx.event.ActionEvent;
  41 import javafx.geometry.Pos;
  42 import javafx.scene.Node;
  43 import javafx.scene.control.Button;
  44 import javafx.scene.control.ContentDisplay;
  45 import javafx.scene.control.ListCell;
  46 import javafx.scene.control.ListView;
  47 import javafx.scene.control.Pagination;
  48 import javafx.scene.control.Skin;
  49 import javafx.scene.image.Image;
  50 import javafx.scene.image.ImageView;
  51 import javafx.scene.layout.HBox;
  52 import javafx.scene.text.Text;
  53 import javafx.util.Callback;
  54 import java.lang.ref.Reference;
  55 import java.lang.ref.WeakReference;
  56 import java.util.ArrayList;
  57 import java.util.Arrays;
  58 import java.util.List;
  59 import java.util.Map;
  60 import java.util.WeakHashMap;
  61 import ensemble.generated.Samples;
  62 import static ensemble.EnsembleApp.SHOW_HIGHLIGHTS;
  63 
  64 /**
  65  * The home page for ensemble.
  66  */
  67 public class HomePage extends ListView<HomePage.HomePageRow> implements Callback<ListView<HomePage.HomePageRow>, ListCell<HomePage.HomePageRow>>, ChangeListener<Number>, Page{
  68     private static final int ITEM_WIDTH = 216;
  69     private static final int ITEM_HEIGHT = 162;
  70     private static final int ITEM_GAP = 32;
  71     private static final int MIN_MARGIN = 20;
  72     private static enum RowType{Highlights,Title,Samples};
  73     private int numberOfColumns = -1;
  74     private final HomePageRow HIGHLIGHTS_ROW = new HomePageRow(RowType.Highlights,null,null);
  75     private final PageBrowser pageBrowser;
  76     private final ReadOnlyStringProperty titleProperty = new ReadOnlyStringWrapper();
  77 
  78     public HomePage(PageBrowser pageBrowser) {
  79         this.pageBrowser = pageBrowser;
  80         setId("HomePage");
  81         // don't use any of the standard ListView CSS
  82         getStyleClass().clear();
  83         // listen for when the list views width changes and recalculate number of columns
  84         widthProperty().addListener(this);
  85         // set our custom cell factory
  86         setCellFactory(this);
  87     }
  88 
  89     @Override public ReadOnlyStringProperty titleProperty() {
  90         return titleProperty;
  91     }
  92 
  93     @Override public String getTitle() {
  94         return titleProperty.get();
  95     }
  96 
  97     @Override public String getUrl() {
  98         return PageBrowser.HOME_URL;
  99     }
 100 
 101     @Override public Node getNode() {
 102         return this;
 103     }
 104 
 105     /* Called when the ListView's width changes */
 106     @Override public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidth) {
 107         // calculate new number of columns that will fit
 108         double width = newWidth.doubleValue();
 109         width -= 60;
 110         final int newNumOfColumns = Math.max(1, (int) (width / (ITEM_WIDTH + ITEM_GAP)));
 111         // our size may have changed, so see if we need to rebuild items list
 112         if (numberOfColumns != newNumOfColumns) {
 113             numberOfColumns = newNumOfColumns;
 114             rebuild();
 115         }
 116     }
 117 
 118     @Override public ListCell<HomePageRow> call(ListView<HomePageRow> homePageRowListView) {
 119         return new HomeListCell();
 120     }
 121 
 122     // Called to rebuild the list's items based on the current number of columns
 123     private void rebuild() {
 124         // build new list of titles and samples
 125         List<HomePageRow> newItems = new ArrayList<>();
 126         // add Highlights to top
 127         if (SHOW_HIGHLIGHTS) {
 128             newItems.add(HIGHLIGHTS_ROW);
 129         }
 130         // add any samples directly in root category
 131         addSampleRows(newItems,Samples.ROOT.samples);
 132         // add samples for all sub categories
 133         for(SampleCategory category: Samples.ROOT.subCategories) {
 134             // add title row
 135             newItems.add(new HomePageRow(RowType.Title,category.name,null));
 136             // add samples
 137             addSampleRows(newItems,category.samplesAll);
 138         }
 139         // replace the lists items
 140         getItems().setAll(newItems);
 141     }
 142 
 143     /**
 144      * Add samples rows to the items list for all SampleInfo's in samples array
 145      *
 146      * @param items The list of rows to add too
 147      * @param samples The SampleInfo's to create rows for
 148      */
 149     private void addSampleRows(List<HomePageRow> items, SampleInfo[] samples) {
 150         if(samples == null) return;
 151         for(int row = 0; row < Math.ceil((double) samples.length / numberOfColumns); row++) {
 152             int sampleIndex = row*numberOfColumns;
 153             SampleInfo[] sampleInfos = Arrays.copyOfRange(samples,sampleIndex, Math.min(sampleIndex+numberOfColumns,samples.length));
 154             items.add(new HomePageRow(RowType.Samples, null, sampleInfos));
 155         }
 156     }
 157 
 158     private Reference<Pagination> paginationCache;
 159     private ImageView highlightRibbon;
 160     private Map<String, SectionRibbon> ribbonsCache = new WeakHashMap<>();
 161     private Map<SampleInfo, Button> buttonCache = new WeakHashMap<>();
 162 
 163     private static int cellCount = 0;
 164     private static final PseudoClass TITLE_PSEUDO_CLASS = PseudoClass.getPseudoClass(RowType.Title.toString());
 165 
 166     private class HomeListCell extends ListCell<HomePageRow> implements Callback<Integer,Node>,  Skin<HomeListCell> {
 167         private static final double HIGHLIGHTS_HEIGHT = 430;
 168         private static final double RIBBON_HEIGHT = 60;
 169         private static final double DEFAULT_HEIGHT = 230;
 170         private static final double DEFAULT_WIDTH = 100;
 171         private double height = DEFAULT_HEIGHT;
 172         int cellIndex;
 173         private RowType oldRowType = null;
 174         private HBox box = new HBox(ITEM_GAP);
 175         private HomeListCell() {
 176             super();
 177             getStyleClass().clear();
 178             cellIndex = cellCount++;
 179             box.getStyleClass().add("home-page-cell");
 180             // we don't need any of the labeled functionality of the default cell skin, so we replace skin with our own
 181             // in this case using this same class as it saves memory. This skin is very simple its just a HBox container
 182             setSkin(this);
 183         }
 184 
 185         @Override protected double computeMaxHeight(double d) {
 186             return height;
 187         }
 188 
 189         @Override protected double computePrefHeight(double d) {
 190             return height;
 191         }
 192 
 193         @Override protected double computeMinHeight(double d) {
 194             return height;
 195         }
 196 
 197         @Override protected double computeMaxWidth(double height) {
 198             return Double.MAX_VALUE;
 199         }
 200 
 201         @Override protected double computePrefWidth(double height) {
 202             return DEFAULT_WIDTH;
 203         }
 204 
 205         @Override protected double computeMinWidth(double height) {
 206             return DEFAULT_WIDTH;
 207         }
 208 
 209         // CELL METHODS
 210         @Override protected void updateItem(HomePageRow item, boolean empty) {
 211             super.updateItem(item, empty);
 212             box.pseudoClassStateChanged(TITLE_PSEUDO_CLASS,item !=null && item.rowType == RowType.Title);
 213             if (item == null) {
 214                 oldRowType = null;
 215                 box.getChildren().clear();
 216                 height = DEFAULT_HEIGHT;
 217             } else {
 218                 switch (item.rowType) {
 219                     case Highlights:
 220                         if (oldRowType != RowType.Highlights) {
 221                             height = HIGHLIGHTS_HEIGHT;
 222                             Pagination pagination = paginationCache == null ? null 
 223                                     : paginationCache.get();
 224                             if (pagination == null) {
 225                                 pagination = new Pagination(Samples.HIGHLIGHTS.length);
 226                                 pagination.getStyleClass().add(Pagination.STYLE_CLASS_BULLET);
 227                                 pagination.setMaxWidth(USE_PREF_SIZE);
 228                                 pagination.setMaxHeight(USE_PREF_SIZE);
 229                                 pagination.setPageFactory(this);
 230                                 paginationCache = new WeakReference<>(pagination);
 231                             }
 232                             if (highlightRibbon == null) {
 233                                 highlightRibbon = new ImageView(new Image(getClass().getResource("images/highlights-ribbon.png").toExternalForm()));
 234                                 highlightRibbon.setManaged(false);
 235                                 highlightRibbon.layoutXProperty().bind(pagination.layoutXProperty().add(5));
 236                                 highlightRibbon.layoutYProperty().bind(pagination.layoutYProperty().add(5));
 237                             }
 238                             box.setAlignment(Pos.CENTER);
 239                             box.getChildren().setAll(pagination, highlightRibbon);
 240                         }
 241                         break;
 242                     case Title:
 243                         height = RIBBON_HEIGHT;
 244                         SectionRibbon ribbon = ribbonsCache.get(item.title);
 245                         if (ribbon == null) {
 246                             ribbon = new SectionRibbon(item.title.toUpperCase());
 247                             ribbonsCache.put(item.title, ribbon);
 248                         }
 249                         box.getChildren().setAll(ribbon);
 250                         box.setAlignment(Pos.CENTER);
 251                         break;
 252                     case Samples:
 253                         height = DEFAULT_HEIGHT;
 254                         box.setAlignment(Pos.CENTER);
 255                         box.getChildren().clear();
 256                         for (int i = 0; i < item.samples.length; i++) {
 257                             final SampleInfo sample = item.samples[i];
 258                             Button sampleButton = buttonCache.get(sample);
 259                             if (sampleButton == null) {
 260                                 sampleButton = new Button();
 261                                 sampleButton.getStyleClass().setAll("sample-button");
 262                                 sampleButton.setContentDisplay(ContentDisplay.TOP);
 263                                 sampleButton.setText(sample.name);
 264                                 sampleButton.setGraphic(sample.getMediumPreview());
 265                                 sampleButton.setOnAction((ActionEvent actionEvent) -> {
 266                                     pageBrowser.goToSample(sample);
 267                                 });
 268                                 buttonCache.put(sample, sampleButton);
 269                             }
 270                             if (sampleButton.getParent() != null) {
 271                                 ((HBox) sampleButton.getParent()).getChildren().remove(sampleButton);
 272                             }
 273                             box.getChildren().add(sampleButton);
 274                         }
 275                         break;
 276                 }
 277                 oldRowType = item.rowType;
 278             }
 279         }
 280 
 281         // SKIN METHODS
 282         @Override public HomeListCell getSkinnable() { return this; }
 283         @Override public Node getNode() { return box; }
 284         @Override public void dispose() {}
 285         // CALLBACK METHODS
 286         @Override public Node call(final Integer highlightIndex) {
 287             Button sampleButton = new Button();
 288             sampleButton.getStyleClass().setAll("sample-button");
 289             sampleButton.setGraphic(Samples.HIGHLIGHTS[highlightIndex].getLargePreview());
 290             sampleButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
 291             sampleButton.setOnAction((ActionEvent actionEvent) -> {
 292 //                System.out.println("Clicked " + Samples.HIGHLIGHTS[highlightIndex].name);
 293                 pageBrowser.goToSample(Samples.HIGHLIGHTS[highlightIndex]);
 294             });
 295             return sampleButton;
 296         }
 297     }
 298 
 299     public static class HomePageRow {
 300         public final RowType rowType;
 301         public final String title;
 302         public final SampleInfo[] samples;
 303 
 304         private HomePageRow(RowType rowType, String title, SampleInfo[] samples) {
 305             this.rowType = rowType;
 306             this.title = title;
 307             this.samples = samples;
 308         }
 309 
 310         @Override
 311         public String toString() {
 312             return "HomePageRow{" + "rowType=" + rowType + ", title=" + title + ", samples=" + samples + '}';
 313         }
 314     }
 315 
 316     private static class SectionRibbon extends Text {
 317         public SectionRibbon(String text) {
 318             super(text);
 319             getStyleClass().add("section-ribbon-text");
 320         }
 321     }
 322 //    private static class SectionRibbon extends Region {
 323 //        private Text textNode = new Text();
 324 //        public SectionRibbon(String text) {
 325 //            textNode.setText(text);
 326 //            textNode.getStyleClass().add("section-ribbon-text");
 327 //            getStyleClass().add("section-ribbon");
 328 //            setPrefHeight(50);
 329 //            setMaxWidth(USE_PREF_SIZE);
 330 //    //        textNode.setEffect(RIBBON_EFFECT);
 331 //            getChildren().add(textNode);
 332 //        }
 333 //
 334 //        public void setText(String text) {
 335 //            textNode.setText(text);
 336 //        }
 337 //
 338 //        @Override protected void layoutChildren() {
 339 //            final Bounds textBounds = textNode.getBoundsInParent();
 340 //            System.out.println("textBounds = " + textBounds);
 341 //            System.out.println("getWidth() = " + getWidth());
 342 //            textNode.relocate(0,
 343 //                    snapPosition((getHeight() - textBounds.getHeight()) / 2) - 3);
 344 //        }
 345 //    }
 346 }