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