1 /* 2 * Copyright (c) 2008, 2014, 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 }