1 /*
   2  * Copyright (c) 2008, 2013, 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 com.javafx.experiments.scheduleapp.pages;
  33 
  34 import java.text.DateFormat;
  35 import java.text.SimpleDateFormat;
  36 import java.util.Calendar;
  37 import java.util.TimeZone;
  38 import javafx.beans.value.ChangeListener;
  39 import javafx.beans.value.ObservableValue;
  40 import javafx.collections.ObservableList;
  41 import javafx.event.EventHandler;
  42 import javafx.geometry.Bounds;
  43 import javafx.geometry.Insets;
  44 import javafx.geometry.Pos;
  45 import javafx.geometry.VPos;
  46 import javafx.scene.Group;
  47 import javafx.scene.Node;
  48 import javafx.scene.control.Button;
  49 import javafx.scene.control.ListCell;
  50 import javafx.scene.control.ListView;
  51 import javafx.scene.control.Skin;
  52 import javafx.scene.image.Image;
  53 import javafx.scene.image.ImageView;
  54 import javafx.scene.input.MouseEvent;
  55 import javafx.scene.layout.Region;
  56 import javafx.scene.layout.VBox;
  57 import javafx.scene.paint.Color;
  58 import javafx.scene.shape.Line;
  59 import javafx.scene.shape.Polyline;
  60 import javafx.scene.shape.Rectangle;
  61 import javafx.scene.text.Font;
  62 import javafx.scene.text.FontWeight;
  63 import javafx.scene.text.Text;
  64 import javafx.util.Callback;
  65 import com.javafx.experiments.scheduleapp.TouchClickedEventAvoider;
  66 import com.javafx.experiments.scheduleapp.ConferenceScheduleApp;
  67 import com.javafx.experiments.scheduleapp.Page;
  68 import com.javafx.experiments.scheduleapp.control.EventPopoverPage;
  69 import com.javafx.experiments.scheduleapp.control.Popover;
  70 import com.javafx.experiments.scheduleapp.data.DataService;
  71 import com.javafx.experiments.scheduleapp.model.Event;
  72 import com.javafx.experiments.scheduleapp.model.Session;
  73 import com.javafx.experiments.scheduleapp.model.Track;
  74 import com.sun.javafx.scene.control.skin.ListViewSkin;
  75 
  76 import static com.javafx.experiments.scheduleapp.ConferenceScheduleApp.*;
  77 import static com.javafx.experiments.scheduleapp.Theme.*;
  78 
  79 public class TimelinePage extends Page implements Callback<ListView<Event>, ListCell<Event>> {
  80     private static final DateFormat TIME_FORMAT = new SimpleDateFormat("hh:mma");
  81     private static final DateFormat DAY_FORMAT = new SimpleDateFormat("EEEE MMMM d");
  82     private static final Image DOT_IMAGE = new Image(ConferenceScheduleApp.class.getResource("images/timeline-dot.png").toExternalForm());
  83     private static final Image PRESENTATION_IMAGE = new Image(ConferenceScheduleApp.class.getResource("images/timeline-presentation.png").toExternalForm());
  84     private static final Image TOP_FADE_IMAGE = new Image(ConferenceScheduleApp.class.getResource("images/timeline-top-fade.png").toExternalForm());
  85     private static final Image BOTTOM_FADE_IMAGE = new Image(ConferenceScheduleApp.class.getResource("images/timeline-bottom-fade.png").toExternalForm());
  86     private static final Calendar TEMP_CALENDAR_1 = Calendar.getInstance();
  87     private static final Calendar TEMP_CALENDAR_2 = Calendar.getInstance();
  88     private static final int TOP = 20;
  89     private static final int BOTTOM = 20;
  90     private static final int LABEL_HEIGHT = 18;
  91     private static final int BUBBLE_PADDING = 5;
  92     private static final Font TIME_FONT = LARGE_FONT;
  93     private static final Font TITLE_FONT = BASE_FONT;
  94     private static final Color TITLE_COLOR = DARK_GREY;
  95     private static final Font SPEAKERS_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.BOLD, 12);
  96     private static final Color SPEAKERS_COLOR = Color.web("#5f5f5f");
  97     private static final Font SUMMARY_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.NORMAL, 11);
  98     private static final Color SUMMARY_COLOR = DARK_GREY;
  99     private static Image TOOTH_IMAGE = new Image(ConferenceScheduleApp.class.getResource("images/timeline-bubble-tooth.png").toExternalForm());
 100     
 101     static {
 102         TIME_FORMAT.setTimeZone(TimeZone.getTimeZone("PST"));
 103     }
 104     
 105     private ListView<Event> timelineListView;
 106     private TimelineListViewSkin timelineListViewSkin;
 107     private DayLabel pastLabel = new DayLabel(BLUE, null);
 108     private DayLabel futureLabel = new DayLabel(BLUE, null);
 109     private Rectangle line = new Rectangle(2,10,BLUE);
 110     private Polyline topZigZag = new Polyline(0,0, 0,5, 10,10, -10,15, 0,20);
 111     private Polyline bottomZigZag = new Polyline(0,0, 0,-5, -10,-10, 10,-15, 0,-20);
 112     private ImageView topFade = new ImageView(TOP_FADE_IMAGE);
 113     private ImageView bottomFade = new ImageView(BOTTOM_FADE_IMAGE);
 114     private Button nowButton = new Button();
 115     private long currentTime = -1;
 116     private final VBox notLoggedInContent = new VBox(20);
 117     private Popover popover;
 118 
 119     public TimelinePage(final Popover popover, final DataService dataService) {
 120         super("My Timeline", dataService);
 121         this.popover = popover;
 122         timelineListView = new ListView<Event>() {
 123             {
 124                 getStyleClass().clear();
 125                 setId("timeline-list-view");
 126                 setSkin(timelineListViewSkin = new TimelineListViewSkin(this));
 127                 setCellFactory(TimelinePage.this);
 128                 setItems(dataService.getEvents());
 129             }
 130         };
 131         if (IS_BEAGLE) {
 132             new TouchClickedEventAvoider(timelineListView);
 133         }
 134         topZigZag.setStroke(BLUE);
 135         topZigZag.setStrokeWidth(2);
 136         bottomZigZag.setStroke(BLUE);
 137         bottomZigZag.setStrokeWidth(2);
 138         nowButton.setId("now-button");
 139         nowButton.getStyleClass().clear();
 140         nowButton.setOnMouseClicked(new EventHandler<MouseEvent>() {
 141             @Override public void handle(MouseEvent t) {  
 142                 // update current time
 143                 currentTime = dataService.getNow();
 144                 // find first item starting after the current time
 145                 final ObservableList<Event> items = timelineListView.getItems();
 146                 int itemToScrollTo = -1;
 147                 for(int i=0; i< items.size(); i++) {
 148                     if (items.get(i).getStart().getTime() > currentTime) {
 149                        itemToScrollTo = i;
 150                         break;
 151                     }
 152                 }
 153                 if (itemToScrollTo == -1) itemToScrollTo = items.size()-1; // time must be after all items
 154                 timelineListView.scrollTo(itemToScrollTo);
 155             }
 156         });
 157         
 158         ImageView loggedInMessage = new ImageView(new Image(ConferenceScheduleApp.class.getResource("images/need-to-be-logged-in.png").toExternalForm()));
 159         Button loginBtn = new Button("Login");
 160         loginBtn.getStyleClass().setAll("large-light-blue-button");
 161         loginBtn.setOnMouseClicked(new EventHandler<MouseEvent>() {
 162             @Override public void handle(MouseEvent event) {
 163                 ConferenceScheduleApp.getInstance().showLoginScreen();
 164             }
 165         });
 166         notLoggedInContent.setAlignment(Pos.CENTER);
 167         notLoggedInContent.getChildren().addAll(loggedInMessage, loginBtn);
 168         
 169         getChildren().setAll(line,topZigZag,bottomZigZag,pastLabel,futureLabel,timelineListView, topFade, bottomFade, nowButton, notLoggedInContent);
 170         
 171         ConferenceScheduleApp.getInstance().getSessionManagement().isGuestProperty().addListener(new ChangeListener<Boolean>() {
 172             @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 173                 setIsGuest(newValue);
 174             }
 175         });
 176         setIsGuest(true);
 177     }
 178 
 179     @Override public void reset() {
 180         timelineListView.scrollTo(0);
 181     }
 182 
 183     private void setIsGuest(boolean isGuest) {
 184         if (isGuest) {
 185             timelineListView.setVisible(false);
 186             line.setVisible(false);
 187             topZigZag.setVisible(false);
 188             bottomZigZag.setVisible(false);
 189             pastLabel.setVisible(false);
 190             futureLabel.setVisible(false);
 191             nowButton.setVisible(false);
 192             topFade.setVisible(false);
 193             bottomFade.setVisible(false);
 194             notLoggedInContent.setVisible(true);
 195         } else {
 196             timelineListView.setVisible(true);
 197             line.setVisible(true);
 198             topZigZag.setVisible(true);
 199             bottomZigZag.setVisible(true);
 200             pastLabel.setVisible(true);
 201             futureLabel.setVisible(true);
 202             nowButton.setVisible(true);
 203             topFade.setVisible(true);
 204             bottomFade.setVisible(true);
 205             notLoggedInContent.setVisible(false);
 206         }
 207     }
 208 
 209     @Override protected void layoutChildren() {
 210         final double w = getWidth();
 211         final int center = (int)(w/2d);
 212         final double h = getHeight();
 213         pastLabel.setLayoutX(center);
 214         pastLabel.setLayoutY(TOP);
 215         futureLabel.setLayoutX(center);
 216         futureLabel.setLayoutY(h - BOTTOM - LABEL_HEIGHT);
 217         line.setLayoutX(center-1);
 218         line.setLayoutY(TOP + LABEL_HEIGHT + 20);
 219         line.setHeight(h - TOP - BOTTOM - LABEL_HEIGHT - LABEL_HEIGHT - 40);
 220         line.setSmooth(false);
 221         topZigZag.setLayoutX(center);
 222         topZigZag.setLayoutY(TOP + LABEL_HEIGHT);
 223         bottomZigZag.setLayoutX(center);
 224         bottomZigZag.setLayoutY(h - BOTTOM - LABEL_HEIGHT);
 225         timelineListView.resizeRelocate(0, TOP+LABEL_HEIGHT+20, w, h - TOP - BOTTOM - LABEL_HEIGHT - LABEL_HEIGHT - 40);
 226         notLoggedInContent.resizeRelocate(0, TOP+LABEL_HEIGHT+20, w, h - TOP - BOTTOM - LABEL_HEIGHT - LABEL_HEIGHT - 40);
 227         topFade.setLayoutY(TOP+LABEL_HEIGHT+20);
 228         topFade.setFitWidth(w);
 229         bottomFade.setLayoutY(h - BOTTOM - LABEL_HEIGHT - 34);
 230         bottomFade.setFitWidth(w);
 231         
 232         nowButton.resize(138,48);
 233         nowButton.setLayoutX(w-5-138);
 234         nowButton.setLayoutY(h-5-48);
 235     }
 236 
 237     @Override public ListCell<Event> call(ListView<Event> listView) {
 238         return new TimelineListCell();
 239     }
 240     
 241     private class TimelineListViewSkin extends ListViewSkin<Event> implements Runnable {
 242         private TimelineListCell prevFirstCell = null, prevLastCell = null;
 243         public TimelineListViewSkin(ListView<Event> listView) {
 244             super(listView);
 245             flow.setCellChangeNotificationListener(this);
 246         }
 247         
 248         @Override public void run() {
 249             TimelineListCell firstCell = (TimelineListCell)flow.getFirstVisibleCell();
 250             if (firstCell != prevFirstCell && firstCell != null) {
 251                 Event firstVisibleEvent = firstCell.getItem();
 252                 pastLabel.setText(DAY_FORMAT.format(firstVisibleEvent.getStart()).toUpperCase());
 253             }
 254             TimelineListCell lastCell = (TimelineListCell)flow.getLastVisibleCell();
 255             if (lastCell != prevLastCell && lastCell != null) {
 256                 Event lastVisibleEvent = lastCell.getItem();
 257                 if (lastVisibleEvent == null) lastVisibleEvent = timelineListView.getItems().get(timelineListView.getItems().size()-1);
 258                 futureLabel.setText(DAY_FORMAT.format(lastVisibleEvent.getStart()).toUpperCase());
 259             }
 260             prevFirstCell = firstCell;
 261             prevLastCell = lastCell;
 262         }
 263     }
 264     
 265     /**
 266      * Special Label node with a rounded rectangle background and a center-top origin.
 267      */
 268     private class DayLabel extends Group {
 269         private final Rectangle backgroundRect = new Rectangle();
 270         private final Text text = new Text();
 271         
 272         public DayLabel(Color color, String content) {
 273             text.setTextOrigin(VPos.TOP);
 274             text.setLayoutY(4);
 275             text.setFont(TITLE_FONT);
 276             text.setFill(Color.WHITE);
 277             backgroundRect.setFill(color);
 278             backgroundRect.setArcWidth(10);
 279             backgroundRect.setArcHeight(10);
 280             getChildren().addAll(backgroundRect, text);
 281             if (content != null) setText(content);
 282         }
 283         
 284         public final void setText(String newText) {
 285             text.setText(newText);
 286             final Bounds textBounds = text.getBoundsInParent();
 287             final int textWidth = (int)(textBounds.getWidth()+0.5);
 288             final int textHeight = (int)(textBounds.getHeight()+0.5);
 289             text.setLayoutX(-textWidth/2);
 290             backgroundRect.setLayoutX((-textWidth/2) - 5);
 291             backgroundRect.setWidth(textWidth + 10);
 292             backgroundRect.setHeight(textHeight + 6);
 293         }
 294     }
 295     
 296     private class TimelineListCell extends ListCell<Event> implements Skin<TimelineListCell> {
 297         private EventBubble bubble = new EventBubble();
 298         private Text timeText = new Text();
 299         private ImageView dot = new ImageView(DOT_IMAGE);
 300         private ImageView icon = new ImageView(PRESENTATION_IMAGE);
 301         private DayLabel dayLabel = new DayLabel(BLUE,null);
 302         private DayLabel nowLabel = new DayLabel(PINK,"NOW");
 303         private Line nowLine = new Line();
 304         private boolean isOnRight = false;
 305         private boolean isFirstOfDay = false;
 306         private boolean isJustAfterNow = false;
 307         private boolean isFirst = false;
 308         private boolean isLast = false;
 309         
 310         private TimelineListCell() {
 311             super();
 312             // we don't need any of the labeled functionality of the default cell skin, so we replace skin with our own
 313             // in this case using this same class as it saves memory. This skin is very simple its just a HBox container
 314             setSkin(this);
 315             getStyleClass().clear();
 316             timeText.setFont(TIME_FONT);
 317             timeText.setFill(BLUE);
 318             timeText.setTextOrigin(VPos.CENTER);
 319             nowLine.setStroke(PINK);
 320             nowLine.setStrokeWidth(2);
 321             nowLine.getStrokeDashArray().setAll(5d,8d);
 322             getChildren().addAll(timeText, dot, icon, dayLabel, nowLine, nowLabel);
 323         }
 324 
 325         @Override protected double computePrefWidth(double height) {
 326             return 100;
 327         }
 328 
 329         @Override protected double computePrefHeight(double width) {
 330             final Insets insets = getInsets();
 331             final double bubblePref = bubble.prefHeight((timelineListView.getWidth()/2)-30) + (isFirstOfDay?40:0) + (isJustAfterNow?40:0) + (isFirst||isLast?10:0);
 332             return insets.getTop() + bubblePref + insets.getBottom();
 333         }
 334 
 335         @Override protected void layoutChildren() {
 336             final int top = (isFirstOfDay?40:0) + (isJustAfterNow?40:0) + (isFirst?10:0);
 337             final double w = getWidth();
 338             final double h = getHeight() - top - (isLast?10:0);
 339             final int centerX = (int)(w/2d);
 340             final int centerY = (int)(h/2d) + top;
 341 //            final int dayLabelWidth = (int)dayLabel.prefWidth(-1);
 342 //            dayLabel.resizeRelocate(centerX - (int)(dayLabelWidth/2), 10, dayLabelWidth, LABEL_HEIGHT);
 343             if (isFirstOfDay) {
 344                 dayLabel.setLayoutX(centerX);
 345                 dayLabel.setLayoutY(10);
 346             }
 347             if (isJustAfterNow) {
 348                 final int nowY = isFirstOfDay?50:10;
 349                 nowLabel.setLayoutX(centerX);
 350                 nowLabel.setLayoutY(nowY);
 351                 final int nowCenterY = 1 + nowY + (int)(nowLabel.getLayoutBounds().getHeight()/2d);
 352                 nowLine.setStartX(12);
 353                 nowLine.setStartY(nowCenterY);
 354                 nowLine.setEndX(w-18);
 355                 nowLine.setEndY(nowCenterY);
 356             }
 357             dot.relocate(
 358                     (int)(centerX - (dot.getLayoutBounds().getWidth()/2)), 
 359                     (int)(centerY - (dot.getLayoutBounds().getHeight()/2)));
 360             final Bounds iconBounds = icon.getLayoutBounds();
 361             if (isOnRight) {
 362                 bubble.resizeRelocate(centerX+20, top, w-centerX-30, h);
 363                 icon.relocate(
 364                         centerX - iconBounds.getWidth() - 12, 
 365                         (int)(centerY - (iconBounds.getHeight()/2)));
 366                 timeText.relocate(
 367                         (int)(centerX - iconBounds.getWidth() - 10 -timeText.getBoundsInParent().getWidth()-10), 
 368                         (int)(centerY-(timeText.getBoundsInParent().getHeight()/2)));
 369             } else {
 370                 bubble.resizeRelocate(10, top, w-centerX-30, h);
 371                 icon.relocate(
 372                         centerX + 12, 
 373                         (int)(centerY - (iconBounds.getHeight()/2)));
 374                 timeText.relocate(
 375                         (int)(centerX + 10 + iconBounds.getWidth() + 10), 
 376                         (int)(centerY-(timeText.getBoundsInParent().getHeight()/2)));
 377             }
 378         }
 379         
 380         // CELL METHODS
 381         @Override protected void updateItem(Event item, boolean empty) {
 382             if (getItem() != item || empty) {
 383                 // let super do its work
 384                 super.updateItem(item, empty);
 385                 // calculate if we are on right or left
 386                 final int index = getIndex();
 387                 isFirst = index == 0;
 388                 isLast = index == (timelineListView.getItems().size()-1);
 389                 isOnRight = (index % 2) == 0;
 390                 bubble.setIsOnRight(isOnRight);
 391                 // update text and bubble
 392     //            System.out.println("updating cell ["+cellIndex+"] to type "+(item==null?"null":item.rowType)+"  from "+oldRowType);
 393                 if (empty) { // empty item
 394                     timeText.setText(null);
 395                     bubble.setVisible(false);
 396                     dayLabel.setVisible(false);
 397                     nowLabel.setVisible(false);
 398                     nowLine.setVisible(false);
 399                     dot.setVisible(false);
 400                     icon.setVisible(false);
 401                     isFirstOfDay = false;
 402                     isJustAfterNow = false;
 403                 } else {
 404                     final Event prevItem = (index <= 0) ? null : timelineListView.getItems().get(index-1);
 405                     // see if we are first of day
 406                     if(index <= 0) {
 407                         isFirstOfDay = false;
 408                     } else {
 409                         TEMP_CALENDAR_1.setTime(prevItem.getStart());
 410                         TEMP_CALENDAR_2.setTime(item.getStart());
 411                         isFirstOfDay = TEMP_CALENDAR_1.get(Calendar.DAY_OF_YEAR) < TEMP_CALENDAR_2.get(Calendar.DAY_OF_YEAR);
 412                     }
 413                     if (isFirstOfDay) {
 414                         dayLabel.setText(DAY_FORMAT.format(item.getStart()).toUpperCase());
 415                     }
 416                     dayLabel.setVisible(isFirstOfDay);
 417 
 418                     // see if need to show now label
 419                     currentTime = dataService.getNow();
 420 
 421                     if(index <= 0) {
 422                         isJustAfterNow = currentTime < item.getStart().getTime();
 423                     } else {
 424                         isJustAfterNow = currentTime < item.getEnd().getTime() && currentTime > prevItem.getEnd().getTime();
 425                     }
 426                     nowLabel.setVisible(isJustAfterNow);
 427                     nowLine.setVisible(isJustAfterNow);
 428 
 429                     timeText.setText(TIME_FORMAT.format(item.getStart()));
 430                     bubble.setVisible(true);
 431                     bubble.setEvent(item);
 432                     dot.setVisible(true);
 433                     icon.setVisible(true);
 434                     requestLayout();
 435                 }
 436             }
 437         }
 438 
 439         // SKIN METHODS
 440         @Override public TimelineListCell getSkinnable() { return this; }
 441         @Override public Node getNode() { return bubble; }
 442         @Override public void dispose() {}
 443     }
 444     
 445     private class EventBubble extends Region {
 446         private Text titleText = new Text();
 447         private Text speakersText = new Text();
 448         private Text summaryText = new Text();
 449         private ImageView tooth = new ImageView(TOOTH_IMAGE);
 450         private Rectangle trackColor = new Rectangle(5,5,10, 10);
 451         private boolean isOnRight = false;
 452         private Event event;
 453 
 454         public EventBubble() {
 455             getStyleClass().add("timelinev2-bubble");
 456             getChildren().addAll(titleText,speakersText,summaryText, trackColor, tooth);
 457             titleText.setFont(TITLE_FONT);
 458             titleText.setFill(TITLE_COLOR);
 459             titleText.setTextOrigin(VPos.TOP);
 460             speakersText.setFont(SPEAKERS_FONT);
 461             speakersText.setFill(SPEAKERS_COLOR);
 462             speakersText.setTextOrigin(VPos.TOP);
 463             summaryText.setFont(BASE_FONT);
 464             summaryText.setFill(DARK_GREY);
 465             summaryText.setTextOrigin(VPos.TOP);
 466             setPickOnBounds(true);
 467             setOnMouseClicked(new EventHandler<MouseEvent>() {
 468                 @Override public void handle(MouseEvent t) {
 469                     if (event != null) {
 470                         EventPopoverPage page = new EventPopoverPage(dataService, event, false);
 471                         popover.clearPages();
 472                         popover.pushPage(page);
 473                         popover.show();
 474                     }
 475                 }
 476             });
 477         }
 478 
 479         public void setIsOnRight(boolean isOnRight) {
 480             this.isOnRight = isOnRight;
 481         }
 482 
 483         @Override protected double computePrefWidth(double height) {
 484             return 100;
 485         }
 486 
 487         @Override protected double computePrefHeight(double width) {
 488             int textWrapWidth = (int) width - 20 - BUBBLE_PADDING;
 489             
 490             // TEMPORARY CODE - textWrapWidth should be the same number that is computed in layoutChildren()
 491             // but for some reason, it oscillates back and forth by 1 pixel causing wrapping to occur.
 492             // The fix is to bump it up by one pixel and avoid the work
 493             textWrapWidth++;
 494             
 495             titleText.setWrappingWidth(textWrapWidth);
 496             speakersText.setWrappingWidth(textWrapWidth);
 497             summaryText.setWrappingWidth(textWrapWidth);
 498             return (int)(BUBBLE_PADDING + titleText.getBoundsInParent().getHeight() + 
 499                          BUBBLE_PADDING + speakersText.getBoundsInParent().getHeight() + 
 500                          BUBBLE_PADDING + summaryText.getBoundsInParent().getHeight() + 
 501                          BUBBLE_PADDING + 0.5);
 502         }
 503 
 504         @Override protected void layoutChildren() {
 505             final int w = (int)getWidth();
 506             final int h = (int)getHeight();
 507             // layout tooth
 508             if (isOnRight) {
 509                 tooth.setScaleX(1);
 510                 tooth.relocate(
 511                         -tooth.getLayoutBounds().getWidth()+1, 
 512                         (h - tooth.getLayoutBounds().getHeight())/2);
 513             } else {
 514                 tooth.setScaleX(-1);
 515                 tooth.relocate(
 516                         w-1, 
 517                         (h - tooth.getLayoutBounds().getHeight())/2);
 518             }
 519             // layout color bar
 520             trackColor.setHeight(h-10);
 521             // layout text
 522             final int textWrapWidth = w - 20 - BUBBLE_PADDING;
 523             titleText.setWrappingWidth(textWrapWidth);
 524             speakersText.setWrappingWidth(textWrapWidth);
 525             summaryText.setWrappingWidth(textWrapWidth);
 526             titleText.relocate(20, BUBBLE_PADDING);
 527             final int titleHeight = (int)(titleText.getBoundsInParent().getHeight()+0.5);
 528             speakersText.relocate(20, BUBBLE_PADDING + titleHeight + BUBBLE_PADDING);
 529             final int speakersHeight = (int)(speakersText.getBoundsInParent().getHeight()+0.5);
 530             summaryText.relocate(20, BUBBLE_PADDING + titleHeight + BUBBLE_PADDING + speakersHeight + BUBBLE_PADDING);
 531         }
 532         
 533         public void setEvent(Event event) {
 534             this.event = event;
 535             Session session = event.getSession();
 536             if (session != null) {
 537                 titleText.setText(session.getAbbreviation()+" :: "+session.getTitle());
 538                 speakersText.setText(session.getSpeakersDisplay());
 539                 summaryText.setText(event.getSessionTime().getRoom().toString());
 540                 Track track = session.getTrack();
 541                 trackColor.setFill(track==null? Color.gray(0.9) : Color.web(track.getColor()));
 542                 speakersText.setVisible(true);
 543             } else if (event != null) {
 544                 titleText.setText(event.getTitle());
 545                 if (event.getAttendees() != null) {
 546                     String attendees = "";
 547                     for (String attendee: event.getAttendees()) {
 548                         if (attendees.length() != 0) attendees += ", ";
 549                         attendees += attendee;
 550                     }
 551                     speakersText.setText(attendees);
 552                     speakersText.setVisible(true);
 553                 } else {
 554                     speakersText.setVisible(false);
 555                 }
 556                 summaryText.setText(event.getLocation()==null?"":event.getLocation());
 557                 trackColor.setFill(Color.BLACK);
 558             } else {
 559                 titleText.setText("");
 560                 summaryText.setText("");
 561             }
 562             requestLayout();
 563         }
 564     }
 565 }