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.ArrayList; 37 import java.util.Calendar; 38 import java.util.Iterator; 39 import java.util.List; 40 import java.util.regex.Pattern; 41 import javafx.animation.Interpolator; 42 import javafx.animation.KeyFrame; 43 import javafx.animation.KeyValue; 44 import javafx.animation.Timeline; 45 import javafx.beans.InvalidationListener; 46 import javafx.beans.Observable; 47 import javafx.beans.binding.DoubleBinding; 48 import javafx.beans.property.SimpleDoubleProperty; 49 import javafx.beans.value.ChangeListener; 50 import javafx.beans.value.ObservableValue; 51 import javafx.collections.ObservableList; 52 import javafx.event.ActionEvent; 53 import javafx.event.Event; 54 import javafx.event.EventHandler; 55 import javafx.geometry.HPos; 56 import javafx.geometry.Orientation; 57 import javafx.geometry.VPos; 58 import javafx.scene.Node; 59 import javafx.scene.control.ContentDisplay; 60 import javafx.scene.control.Label; 61 import javafx.scene.control.ListCell; 62 import javafx.scene.control.ListView; 63 import javafx.scene.control.Skin; 64 import javafx.scene.image.Image; 65 import javafx.scene.image.ImageView; 66 import javafx.scene.input.MouseEvent; 67 import javafx.scene.layout.ColumnConstraints; 68 import javafx.scene.layout.GridPane; 69 import javafx.scene.layout.Priority; 70 import javafx.scene.layout.Region; 71 import javafx.scene.layout.VBox; 72 import javafx.scene.paint.Color; 73 import javafx.scene.shape.Rectangle; 74 import javafx.scene.text.Text; 75 import javafx.stage.Window; 76 import javafx.util.Callback; 77 import javafx.util.Duration; 78 import com.javafx.experiments.scheduleapp.TouchClickedEventAvoider; 79 import com.javafx.experiments.scheduleapp.Page; 80 import com.javafx.experiments.scheduleapp.control.EventPopoverPage; 81 import com.javafx.experiments.scheduleapp.control.Popover; 82 import com.javafx.experiments.scheduleapp.control.ResizableWrappingText; 83 import com.javafx.experiments.scheduleapp.control.SearchBox; 84 import com.javafx.experiments.scheduleapp.data.DataService; 85 import com.javafx.experiments.scheduleapp.model.Session; 86 import com.javafx.experiments.scheduleapp.model.SessionTime; 87 import com.javafx.experiments.scheduleapp.model.Speaker; 88 import com.sun.javafx.scene.control.skin.FXVK; 89 90 import static com.javafx.experiments.scheduleapp.ConferenceScheduleApp.*; 91 import static com.javafx.experiments.scheduleapp.Theme.*; 92 import java.awt.image.BufferedImage; 93 import java.io.IOException; 94 import java.net.URL; 95 import java.util.HashMap; 96 import java.util.Map; 97 import java.util.logging.Level; 98 import java.util.logging.Logger; 99 import javafx.embed.swing.SwingFXUtils; 100 import javafx.scene.image.WritableImage; 101 import javax.imageio.ImageIO; 102 103 /** 104 * Page showing searchable list of all speakers at the conference 105 */ 106 public class SpeakersPage extends Page implements ChangeListener<String> { 107 private static DateFormat DATE_TIME_FORMAT = new SimpleDateFormat("hh:mma EEE"); 108 private static final Pattern HYPERLINK_PATTERN = Pattern.compile("http(s)?://\\S+"); 109 private static final int PIC_SIZE = 48; 110 private static final int GAP = 6; 111 private static final int IMG_GAP = 12; 112 private static final int MIN_HEIGHT = GAP + PIC_SIZE + 10 + GAP; 113 private static final int TEXT_LEFT = GAP+PIC_SIZE+IMG_GAP; 114 115 private final SpeakerList speakersList = new SpeakerList(); 116 private final SearchBox searchBox = new SearchBox(); 117 private final Map<Speaker,Image> speakerImageCache = new HashMap<>(); 118 119 /** 120 * index of currently expanded cell. We need to keep tack of the cell that 121 * is expanded so that when you scroll away from it and its cell is reused 122 * then scroll back we can put the cell back in the expanded state. 123 */ 124 private int expandedCellIndex = -1; 125 /** 126 * the currently expanded cell or null if no cell is expanded. This is used 127 * to make sure only once cell is expanded at a time. So when we expand a 128 * new cell we can collapse this one. 129 */ 130 private SpeakerListCell expandedCell = null; 131 132 private Popover popover; 133 134 public SpeakersPage(Popover popover, DataService dataService) { 135 super("Speakers", dataService); 136 this.popover = popover; 137 if (IS_BEAGLE) { 138 new TouchClickedEventAvoider(speakersList); 139 } 140 speakersList.setItems(dataService.getSpeakers()); 141 getChildren().setAll(speakersList,searchBox); 142 searchBox.textProperty().addListener(this); 143 144 // HORRIFIC!! The problem is that on Beagle right now, we're not seeing the 145 // virtual keyboard layer getting hidden. This is likely a bug in the window 146 // hardware layer support. This code will just move the keyboard out of the way. 147 searchBox.focusedProperty().addListener(new InvalidationListener() { 148 @Override public void invalidated(Observable observable) { 149 Iterator<Window> itr = Window.impl_getWindows(); 150 while (itr.hasNext()) { 151 Window win = itr.next(); 152 Object obj = win.getScene().getRoot().lookup(".fxvk"); 153 if (obj instanceof FXVK) { 154 FXVK keyboard = (FXVK) obj; 155 System.err.println("Found virtual keyboard"); 156 if (searchBox.isFocused()) { 157 keyboard.setVisible(true); 158 keyboard.setTranslateX(0); 159 } else { 160 keyboard.setVisible(false); 161 keyboard.setTranslateX(2000); 162 } 163 } 164 } 165 } 166 }); 167 } 168 169 @Override public void reset() { 170 searchBox.setText(""); 171 speakersList.scrollTo(0); 172 } 173 174 @Override protected void layoutChildren() { 175 final int w = (int)getWidth() - 24; 176 final int h = (int)getHeight() - 24; 177 searchBox.resize(w,30); 178 searchBox.setLayoutX(12); 179 searchBox.setLayoutY(12); 180 speakersList.resize(w,h - 42); 181 speakersList.setLayoutX(12); 182 speakersList.setLayoutY(53); 183 } 184 185 /** 186 * Handle text searching 187 */ 188 @Override public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) { 189 final ObservableList<Speaker> items = speakersList.getItems(); 190 long start = System.currentTimeMillis(); 191 if (newValue == null || newValue.length() == 0) { 192 items.setAll(dataService.getSpeakers()); 193 } else { 194 final List<Speaker> speakers = dataService.getSpeakers(); 195 final ArrayList<Speaker> results = new ArrayList(speakers.size()); 196 final char[] search = newValue.toLowerCase().toCharArray(); 197 for(int i=0; i < speakers.size(); i++) { 198 final Speaker s = speakers.get(i); 199 final String first = s.firstName; 200 boolean match = true; 201 final int max = first.length() < search.length ? first.length() : search.length; 202 for (int c=0; c < max; c++) { 203 if(Character.toLowerCase(first.charAt(c)) != search[c]) { 204 match = false; 205 break; 206 } 207 } 208 if (match == false) { 209 match = true; 210 final String last = s.lastName; 211 final int maxl = last.length() < search.length ? last.length() : search.length; 212 for (int c=0; c < maxl; c++) { 213 if(Character.toLowerCase(last.charAt(c)) != search[c]) { 214 match = false; 215 break; 216 } 217 } 218 } 219 if (match) results.add(s); 220 } 221 System.out.println("Setting data..."); 222 long t0 = System.currentTimeMillis(); 223 items.setAll(results); 224 //ObservableList<Speaker> empty = FXCollections.observableArrayList(); 225 //speakersList.setItems(empty); 226 //items.clear(); 227 //speakersList.setItems(FXCollections.observableArrayList(results)); 228 long t1 = System.currentTimeMillis(); 229 System.out.println("Done: " + (t1 - t0)); 230 231 } 232 long end = System.currentTimeMillis(); 233 System.out.println("search took = "+(end-start)); 234 // clear expanded cells 235 expandedCellIndex = -1; 236 expandedCell = null; 237 } 238 239 240 /** 241 * Helper method used to update a session row in speakers extended info to 242 * show or not show a star for when it added to sessions to attend. 243 * 244 * @param sessionTitle The label to add/remove star from 245 * @param show true if the star should be added to the label, false if it 246 * should be removed. 247 */ 248 private static void updateStar(Label sessionTitle, boolean show) { 249 if(show) { 250 ImageView star = new ImageView(STAR); 251 sessionTitle.setGraphic(star); 252 sessionTitle.setContentDisplay(ContentDisplay.RIGHT); 253 } else { 254 sessionTitle.setGraphic(null); 255 } 256 } 257 258 /** 259 * Custom list for speakers, will all standard CSS removed an using our 260 * custom SpeakerListCell. 261 */ 262 private class SpeakerList extends ListView<Speaker> implements Callback<ListView<Speaker>, ListCell<Speaker>>{ 263 public SpeakerList(){ 264 getStyleClass().setAll("twitter-list-view"); 265 // skinClassNameProperty().set("com.sun.javafx.scene.control.skin.ListViewSkin"); 266 setSkin(new com.sun.javafx.scene.control.skin.ListViewSkin(this)); 267 setCellFactory(this); 268 // hack workaround for cell sizing 269 Node node = lookup(".clipped-container"); 270 if (node != null) node.setManaged(true); 271 } 272 273 @Override public ListCell<Speaker> call(ListView<Speaker> p) { 274 return new SpeakerListCell(); 275 } 276 } 277 278 /** 279 * The main body of the speaker list cell. This is separate from the cell 280 * so that it can be cached and not need to be updated while the cells clip 281 * is changing during expansion. 282 */ 283 private final static class SpeakerListCellBody extends Region { 284 private final Text name = new Text(); 285 private final Text company = new Text(); 286 private final ImageView image = new ImageView(); 287 private final ImageView shadow = new ImageView(SHADOW_PIC); 288 private final Rectangle imageBorder = new Rectangle(PIC_SIZE+6, PIC_SIZE+6,Color.WHITE); 289 private final Rectangle dividerLine = new Rectangle(1, 1); 290 291 public SpeakerListCellBody() { 292 name.setFont(LARGE_FONT); 293 name.setFill(DARK_GREY); 294 name.setTextOrigin(VPos.CENTER); 295 company.setFont(LARGE_LIGHT_FONT); 296 company.setFill(GRAY); 297 company.setTextOrigin(VPos.CENTER); 298 dividerLine.setFill(Color.web("#ddd")); 299 dividerLine.setSmooth(false); 300 shadow.setFitWidth(PIC_SIZE+6); 301 image.setFitWidth(PIC_SIZE); 302 image.setFitHeight(PIC_SIZE); 303 getChildren().addAll(name,company,dividerLine,shadow,imageBorder,image); 304 } 305 306 @Override protected void layoutChildren() { 307 final int w = (int)getWidth(); 308 final int h = (int)getHeight(); 309 final int centerY = (int)(MIN_HEIGHT/2d); 310 dividerLine.setWidth(w); 311 image.setLayoutX(GAP); 312 image.setLayoutY(GAP+3); 313 imageBorder.setLayoutX(GAP-3); 314 imageBorder.setLayoutY(GAP); 315 shadow.setLayoutX(GAP-3); 316 shadow.setLayoutY(GAP+PIC_SIZE+6); 317 name.setLayoutX(TEXT_LEFT); 318 name.setLayoutY(centerY-13); 319 company.setLayoutX(TEXT_LEFT); 320 company.setLayoutY(centerY+13); 321 } 322 } 323 324 /** 325 * The extended info section of the speaker list cell. This is separate from 326 * the cell so that it can be cached and not need to be updated while the 327 * cells clip is changing during expansion. 328 */ 329 private final static class SpeakerListCellExtended extends GridPane { 330 private final VBox sessionsList = new VBox(); 331 private final Text jobTitleText = new Text("TITLE"); 332 private final ResizableWrappingText jobTitle = new ResizableWrappingText(); 333 private final Text bioText = new Text("BIO"); 334 private final ResizableWrappingText bio = new ResizableWrappingText(); 335 336 public SpeakerListCellExtended() { 337 // create extended content 338 setVgap(12); 339 getColumnConstraints().setAll(new ColumnConstraints(TEXT_LEFT-GAP)); 340 jobTitleText.setFont(SMALL_FONT); 341 jobTitleText.setFill(BLUE); 342 GridPane.setConstraints(jobTitleText, 0, 0,1,1, HPos.LEFT, VPos.TOP); 343 jobTitle.setFont(LIGHT_FONT); 344 jobTitle.setFill(DARK_GREY); 345 GridPane.setConstraints(jobTitle, 1, 0); 346 bioText.setFont(SMALL_FONT); 347 bioText.setFill(BLUE); 348 GridPane.setConstraints(bioText, 0, 1,1,1, HPos.LEFT, VPos.TOP); 349 bio.setFont(LIGHT_FONT); 350 bio.setFill(DARK_GREY); 351 GridPane.setConstraints(bio, 1, 1); 352 Text sessionsText = new Text("SESSIONS"); 353 sessionsText.setFont(SMALL_FONT); 354 sessionsText.setFill(BLUE); 355 GridPane.setConstraints(sessionsText, 0, 2,1,1, HPos.LEFT, VPos.TOP); 356 sessionsList.getStyleClass().setAll("speaker-session-list"); 357 sessionsList.setFillWidth(true); 358 GridPane.setConstraints(sessionsList, 1, 2,1,1, HPos.LEFT, VPos.TOP, Priority.ALWAYS, Priority.NEVER); 359 360 getChildren().addAll(jobTitleText, jobTitle, bioText, bio, sessionsText,sessionsList); 361 } 362 } 363 364 /** 365 * Custom list cell for the speakers list. It uses a pattern to avoid 366 * standard list cell skin to have minimal overhead. 367 */ 368 private class SpeakerListCell extends ListCell<Speaker> implements Skin<SpeakerListCell>, EventHandler { 369 private final SpeakerListCellBody body = new SpeakerListCellBody(); 370 private final SpeakerListCellExtended expandedContent = new SpeakerListCellExtended(); 371 private final ImageView arrow = new ImageView(RIGHT_ARROW); 372 private int cellIndex; 373 private final Rectangle clip = new Rectangle(); 374 private final SimpleDoubleProperty expansion = new SimpleDoubleProperty(0) { 375 @Override protected void invalidated() { 376 super.invalidated(); 377 requestLayout(); 378 } 379 }; 380 381 private SpeakerListCell() { 382 super(); 383 // we don't need any of the labeled functionality of the default cell skin, so we replace skin with our own 384 // in this case using this same class as it saves memory. This skin is very simple its just a HBox container 385 setSkin(this); 386 getStyleClass().clear(); 387 arrow.rotateProperty().bind(new DoubleBinding() { 388 { bind(expansion); } 389 @Override protected double computeValue() { 390 return 90 - (180*expansion.get()); 391 } 392 }); 393 clip.setSmooth(false); 394 setClip(clip); 395 getChildren().addAll(arrow); 396 setOnMouseClicked(this); 397 setPickOnBounds(true); 398 } 399 400 @Override public Orientation getContentBias() { 401 return Orientation.HORIZONTAL; 402 } 403 404 @Override public void resize(double width, double height) { 405 super.resize(width, height); 406 clip.setWidth(width); 407 clip.setHeight(height); 408 } 409 410 private Timeline expansionTimeline = null; 411 412 @Override public void handle(Event t) { 413 final double e = this.expansion.get(); 414 expandCollapse(this.expansion.get() < 1); 415 } 416 417 private void expandCollapse(boolean expand) { 418 if (expansionTimeline != null) expansionTimeline.stop(); 419 if (expand) { 420 if (expandedCell != null) expandedCell.expandCollapse(false); 421 expandedCellIndex = getIndex(); 422 expandedCell = this; 423 expansionTimeline = new Timeline( 424 new KeyFrame(Duration.millis(200), new KeyValue(expansion, 1, Interpolator.EASE_BOTH)) 425 ); 426 expansionTimeline.play(); 427 } else { 428 expansionTimeline = new Timeline( 429 new KeyFrame(Duration.millis(200), new KeyValue(expansion, 0, Interpolator.EASE_BOTH)) 430 ); 431 expansionTimeline.setOnFinished(new EventHandler<ActionEvent>() { 432 @Override public void handle(ActionEvent t) { 433 if (getIndex() == expandedCellIndex) { 434 expandedCellIndex = -1; 435 expandedCell = null; 436 } 437 } 438 }); 439 expansionTimeline.play(); 440 } 441 } 442 443 @Override protected double computePrefWidth(double height) { 444 return 100; 445 } 446 447 @Override protected double computePrefHeight(double width) { 448 width = speakersList.getWidth(); 449 double headerHeight = MIN_HEIGHT; 450 final double e = this.expansion.get(); 451 if (e == 0) { 452 return (int) (headerHeight + 0.5d); 453 } else { 454 final int textLeft = GAP+PIC_SIZE+IMG_GAP; 455 final double expandedContentHeight = expandedContent.prefHeight(width - textLeft - GAP); 456 return (int) (headerHeight + (expandedContentHeight * e) + GAP + 0.5d); 457 } 458 } 459 460 @Override protected void layoutChildren() { 461 final int w = (int)getWidth(); 462 final int h = (int)getHeight(); 463 final int centerY = (int)(MIN_HEIGHT/2d); 464 body.resize(w, MIN_HEIGHT); 465 arrow.setLayoutX(w-GAP-7); 466 arrow.setLayoutY(centerY-4); 467 if (this.expansion.get() > 0) { 468 if (expandedContent.getParent() == null) { 469 getChildren().add(expandedContent); 470 } 471 expandedContent.setLayoutX(GAP); 472 expandedContent.setLayoutY(MIN_HEIGHT); 473 final int expandedContentWidth = w - GAP - GAP; 474 final int expandedContentHeight = (int)expandedContent.prefHeight(expandedContentWidth); 475 expandedContent.resize(expandedContentWidth, expandedContentHeight); 476 } else { 477 getChildren().remove(expandedContent); 478 } 479 } 480 481 @Override protected Node impl_pickNodeLocal(double localX, double localY) { 482 if (contains(localX, localY)) { 483 if (this.expansion.get() > 0) { 484 final Node superPick = super.impl_pickNodeLocal(localX, localY); 485 if (superPick != null) return superPick; 486 } 487 return this; 488 } 489 return null; 490 } 491 492 // CELL METHODS 493 @Override protected void updateItem(Speaker speaker, boolean empty) { 494 super.updateItem(speaker,empty); 495 final ObservableList<Node> children = getChildren(); 496 body.dividerLine.setVisible(getIndex() != 0); 497 if (speaker == null) { // empty item 498 for (Node child: children) child.setVisible(false); 499 } else { 500 body.name.setText(speaker.getFullName() + (speaker.isRockStar()?" (Rock Star)":"")); 501 body.company.setText(speaker.getCompany()); 502 final String bioStr = speaker.getBio(); 503 if (bioStr == null || bioStr.length() == 0) { 504 expandedContent.bioText.setVisible(false); 505 expandedContent.bioText.setManaged(false); 506 expandedContent.bio.setVisible(false); 507 expandedContent.bio.setManaged(false); 508 } else { 509 expandedContent.bioText.setVisible(true); 510 expandedContent.bioText.setManaged(true); 511 expandedContent.bio.setVisible(true); 512 expandedContent.bio.setManaged(true); 513 expandedContent.bio.setText(speaker.getBio()); 514 } 515 final String jobTitleStr = speaker.getJobTitle(); 516 if (jobTitleStr == null || jobTitleStr.length() == 0) { 517 expandedContent.jobTitleText.setVisible(false); 518 expandedContent.jobTitleText.setManaged(false); 519 expandedContent.jobTitle.setVisible(false); 520 expandedContent.jobTitle.setManaged(false); 521 } else { 522 expandedContent.jobTitleText.setVisible(true); 523 expandedContent.jobTitleText.setManaged(true); 524 expandedContent.jobTitle.setVisible(true); 525 expandedContent.jobTitle.setManaged(true); 526 expandedContent.jobTitle.setText(speaker.getJobTitle()); 527 } 528 if (speaker.getImageUrl() != null) { 529 Image img = speakerImageCache.get(speaker); 530 if (img == null) { 531 img = new Image(speaker.getImageUrl(),PIC_SIZE, PIC_SIZE,false,true, true); 532 speakerImageCache.put(speaker,img); 533 } 534 body.image.setImage(img); 535 } else { 536 body.image.setImage(DUKE_48); 537 } 538 if (getIndex() == expandedCellIndex) { 539 expansion.set(1); 540 } else if (expansion.get() == 1) { 541 expansion.set(0); 542 } 543 arrow.setVisible(true); 544 body.setVisible(true); 545 expandedContent.sessionsList.getChildren().clear(); 546 boolean first = true; 547 for(final Session session: speaker.getSessions()) { 548 for (final SessionTime sessionTime: session.getSessionTimes()) { 549 Label title = new Label(session.getTitle()+" @"+DATE_TIME_FORMAT.format(sessionTime.getStart())); 550 title.setFont(BASE_FONT); 551 title.setTextFill(DARK_GREY); 552 title.setPrefHeight(44); 553 title.setMaxWidth(Double.MAX_VALUE); 554 title.setOnMouseClicked(new SessionClickHandler(session, sessionTime, title)); 555 updateStar(title, sessionTime.getEvent() != null); 556 expandedContent.sessionsList.getChildren().add(title); 557 if (!first) { 558 title.getStyleClass().add("session-list-item"); 559 } 560 first = false; 561 } 562 563 } 564 } 565 } 566 567 // SKIN METHODS 568 @Override public SpeakerListCell getSkinnable() { return this; } 569 @Override public Node getNode() { return body; } 570 @Override public void dispose() {} 571 } 572 573 private class SessionClickHandler implements EventHandler<MouseEvent>, Runnable { 574 private final Session session; 575 private final SessionTime sessionTime; 576 private final Label sessionTitle; 577 578 public SessionClickHandler(Session session, SessionTime sessionTime, Label sessionTitle) { 579 this.session = session; 580 this.sessionTime = sessionTime; 581 this.sessionTitle = sessionTitle; 582 } 583 584 @Override public void handle(MouseEvent t) { 585 t.consume(); 586 // get/create event 587 com.javafx.experiments.scheduleapp.model.Event event = sessionTime.getEvent(); 588 if (event == null) { 589 Calendar calendar = Calendar.getInstance(); 590 calendar.setTime(sessionTime.getStart()); 591 calendar.add(Calendar.MINUTE, sessionTime.getLength()); 592 event = new com.javafx.experiments.scheduleapp.model.Event(session, sessionTime.getStart(), calendar.getTime(), sessionTime); 593 } 594 // show popover 595 EventPopoverPage page = new EventPopoverPage(dataService, event, false); 596 popover.clearPages(); 597 popover.pushPage(page); 598 popover.show(); 599 } 600 601 @Override public void run() { 602 updateStar(sessionTitle, sessionTime.getEvent() != null); 603 } 604 605 } 606 }