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 static com.javafx.experiments.scheduleapp.ConferenceScheduleApp.*; 35 import com.javafx.experiments.scheduleapp.TouchClickedEventAvoider; 36 import com.javafx.experiments.scheduleapp.Page; 37 import static com.javafx.experiments.scheduleapp.Theme.*; 38 import com.javafx.experiments.scheduleapp.control.EventPopoverPage; 39 import com.javafx.experiments.scheduleapp.control.Popover; 40 import com.javafx.experiments.scheduleapp.control.SearchBox; 41 import com.javafx.experiments.scheduleapp.data.DataService; 42 import com.javafx.experiments.scheduleapp.model.Event; 43 import com.javafx.experiments.scheduleapp.model.Session; 44 import com.javafx.experiments.scheduleapp.model.SessionTime; 45 import com.javafx.experiments.scheduleapp.model.SessionType; 46 import com.javafx.experiments.scheduleapp.model.Track; 47 import java.text.DateFormat; 48 import java.text.SimpleDateFormat; 49 import java.util.ArrayList; 50 import java.util.Calendar; 51 import java.util.Comparator; 52 import java.util.Date; 53 import java.util.HashMap; 54 import java.util.LinkedList; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.Set; 58 import java.util.concurrent.ExecutorService; 59 import java.util.concurrent.Executors; 60 import java.util.concurrent.ThreadFactory; 61 import javafx.beans.InvalidationListener; 62 import javafx.beans.Observable; 63 import javafx.beans.property.BooleanProperty; 64 import javafx.beans.property.SimpleBooleanProperty; 65 import javafx.beans.value.ChangeListener; 66 import javafx.beans.value.ObservableValue; 67 import javafx.collections.FXCollections; 68 import javafx.collections.ListChangeListener; 69 import javafx.collections.ListChangeListener.Change; 70 import javafx.collections.ObservableList; 71 import javafx.concurrent.Task; 72 import javafx.concurrent.WorkerStateEvent; 73 import javafx.event.EventHandler; 74 import javafx.geometry.VPos; 75 import javafx.scene.Node; 76 import javafx.scene.control.Button; 77 import javafx.scene.control.ListCell; 78 import javafx.scene.control.ListView; 79 import javafx.scene.control.Skin; 80 import javafx.scene.input.MouseEvent; 81 import javafx.scene.layout.HBox; 82 import javafx.scene.layout.Priority; 83 import javafx.scene.layout.Region; 84 import javafx.scene.paint.Color; 85 import javafx.scene.shape.Rectangle; 86 import javafx.scene.text.Font; 87 import javafx.scene.text.FontWeight; 88 import javafx.scene.text.Text; 89 import javafx.util.Callback; 90 91 public class CatalogPage extends Page implements Callback<ListView<CatalogPage.Row>, ListCell<CatalogPage.Row>>, Runnable, ChangeListener<String> { 92 private static DateFormat TIME_FORMAT = new SimpleDateFormat("hh:mma"); 93 private static DateFormat DAY_FORMAT = new SimpleDateFormat("EEEE"); 94 private static final int TIME_COLUMN_WIDTH = 100; 95 private static final int SLOT_WIDTH = 200; 96 private static final int SLOT_HEIGHT = 85; 97 private static final int SLOT_BAR_WIDTH = 5; 98 private static final int SLOT_BAR_GAP = 5; 99 private static final int SLOT_GAP = 5; 100 private static final int SLOT_TEXT_WRAP = SLOT_WIDTH - SLOT_BAR_WIDTH - SLOT_BAR_GAP - SLOT_GAP; 101 private static final Font TITLE_FONT = BASE_FONT; 102 private static final Color TITLE_COLOR = DARK_GREY; 103 private static final Font SPEAKERS_FONT = Font.font(DEFAULT_FONT_NAME, FontWeight.BOLD, 12); 104 private static final Color SPEAKERS_COLOR = Color.web("#5f5f5f"); 105 private static final Font TIME_FONT = LARGE_FONT; 106 private static final Font DAY_FONT = BASE_FONT; 107 private static final int MAX_TITLE_CHARS = 55; 108 private static final int MAX_SPEAKER_CHARS = 50; 109 110 /** 111 * The ExecutorService used for running our filter tasks. We could have just created 112 * a new thread each time, but there really isn't a need for it. In addition, by having 113 * a single thread executor, we can be sure that no two tasks are operating at the 114 * same time, which determinism makes it easier for us to efficiently handle updating 115 * the state of the UI thread during filtering. 116 */ 117 private static ExecutorService FILTER_EXECUTOR = Executors.newSingleThreadExecutor(new ThreadFactory() { 118 @Override public Thread newThread(Runnable r) { 119 Thread th = new Thread(r); 120 th.setDaemon(true); 121 th.setName("Catalog Filter Thread"); 122 return th; 123 } 124 }); 125 126 /** 127 * The ListView of all of the sessions at the conference. Each row is 128 * comprised first of a Date, followed by Sessions that are associated 129 * with the Date / time. 130 */ 131 private final ListView<Row> list; 132 133 /** 134 * This button will pop up the session filter popover. The session filter differs 135 * from the filter bar in that it allows you to filter out entire 136 * categories of issues, whereas the "search bar" (really a filter bar) is 137 * based on the text in the sessions (summary, title, speakers). 138 */ 139 private final Button sessionFilterButton; 140 141 /** 142 * The session filter is used to store the results of the session filter pane, 143 * and can filter out sessions passed to it. 144 */ 145 private SessionFilterCriteria sessionFilter = SessionFilterCriteria.EMPTY; 146 147 /** 148 * The data items used in the ListView. 149 */ 150 private final ObservableList<Row> rows = FXCollections.observableArrayList(); 151 152 /** 153 * A list of Rows that have been filtered out of the <code>rows</code>. That is, 154 * these rows are no longer in the list view. 155 */ 156 private final List<Row> filtered = new LinkedList<Row>(); 157 158 /** 159 * The search box, used for filtering. 160 */ 161 private final SearchBox searchBox = new SearchBox(); 162 163 /** 164 * The background task which actually does the filtering. This is moved into a 165 * background task so as not to cause delays in the GUI. 166 */ 167 private Task<Runnable> filterTask = null; 168 169 private final HBox searchBar = new HBox(10); 170 171 private final Popover sessionFilterPopover = new Popover(); 172 private final SearchFilterPopoverPage filterPopoverPage; 173 private final Popover popover; 174 175 /** 176 * Creats a new catalog filterPopoverPage. 177 * 178 * @param dataService The dataservice to use. 179 */ 180 public CatalogPage(final Popover popover, final DataService dataService) { 181 super("Content Catalog", dataService); 182 this.popover = popover; 183 sessionFilterPopover.getStyleClass().add("session-popover"); 184 sessionFilterPopover.setPrefWidth(440); 185 // set pick on bounds to false as we don't want to capture mouse events 186 // that are meant for the top tabs even though those tabs are in our bounds 187 // because of the filter button 188 setPickOnBounds(false); 189 // create list 190 list = new ListView<Row>(){ 191 { 192 getStyleClass().setAll("twitter-list-view"); 193 skinClassNameProperty().set("javafx.scene.control.skin.ListViewSkin"); 194 setCellFactory(CatalogPage.this); 195 } 196 }; 197 198 if (IS_BEAGLE) { 199 new TouchClickedEventAvoider(list); 200 } 201 202 // create filter button 203 this.filterPopoverPage = new SearchFilterPopoverPage(dataService, this); 204 sessionFilterButton = new Button(); 205 sessionFilterButton.setId("session-filter-button"); 206 sessionFilterButton.getStyleClass().clear(); 207 sessionFilterButton.setPrefSize(69, 31); 208 sessionFilterButton.setOnMouseClicked(new EventHandler<MouseEvent>() { 209 @Override public void handle(MouseEvent e) { 210 if (sessionFilterPopover.isVisible()) { 211 sessionFilterPopover.hide(); 212 } else { 213 sessionFilterPopover.pushPage(filterPopoverPage); 214 sessionFilterPopover.show(); 215 } 216 } 217 }); 218 // create search bar 219 HBox.setHgrow(searchBox, Priority.ALWAYS); 220 searchBar.getChildren().addAll(searchBox, sessionFilterButton); 221 searchBox.textProperty().addListener(this); 222 searchBox.setFocusTraversable(false); 223 // add children 224 getChildren().setAll(searchBar, list, sessionFilterPopover); 225 // set list to use rows as model 226 list.setItems(rows); 227 // populate filterPopoverPage with initial data 228 buildRows(); 229 // listen for when session time data changes and rebuild rows 230 dataService.getStartTimes().addListener(new ListChangeListener<Date>() { 231 @Override public void onChanged(Change<? extends Date> c) { 232 buildRows(); 233 filter(); 234 } 235 }); 236 } 237 238 private void buildRows() { 239 final List<Row> items = new ArrayList<>(200); 240 for (final Date startTime : dataService.getStartTimes()) { 241 final List<Session> s = dataService.getSessionsAtTimeSlot(startTime); 242 final List<SessionView> views = new ArrayList<>(s.size()); 243 for (Session session : s) { 244 final SessionView view = new SessionView(); 245 view.session = session; 246 views.add(view); 247 } 248 final Row row = new Row(startTime, views); 249 items.add(row); 250 } 251 rows.setAll(items); 252 } 253 254 @Override public void reset() { 255 // Cause the session filter to clear out 256 sessionFilterPopover.hide(); 257 filterPopoverPage.reset(); 258 searchBox.setText(""); 259 list.scrollTo(0); 260 } 261 262 /** 263 * Called by the search box whenever the text of the search box has changed. 264 * If there is a filterTask already being executed, then we will cancel it. 265 * We then create a new filterTask which will process all of the rows and 266 * then update the UI with the results of the filter operation. 267 */ 268 @Override public void changed(ObservableValue<? extends String> ov, String oldValue, String newValue) { 269 sessionFilter = SessionFilterCriteria.withText(searchBox.getText().toLowerCase(), sessionFilter); 270 filter(); 271 } 272 273 @Override public void run() { 274 sessionFilter = SessionFilterCriteria.withText(sessionFilter.getText(), filterPopoverPage.getSessionFilterCriteria()); 275 filter(); 276 } 277 278 private void filter() { 279 // If there is an existing filter task running, then we need to cancel it. 280 if (filterTask != null && filterTask.isRunning()) { 281 filterTask.cancel(); 282 } 283 284 // We will not set lastFilter until we have successfully completed a filter operation 285 final SessionFilterCriteria criteria = sessionFilter; 286 // The rows that we're going to search through. 287 final List<Row> allRows = new ArrayList<Row>(); 288 allRows.addAll(rows); 289 allRows.addAll(filtered); 290 291 // Create a new filterTask 292 filterTask = new Task<Runnable>() { 293 @Override protected Runnable call() throws Exception { 294 // A map of sessions which need to move from being filtered to being unfiltered 295 final Map<Row, List<SessionView>> sessionsToRestore = new HashMap<Row, List<SessionView>>(); 296 // A map of sessions which need to move to being filtered 297 final Map<Row, List<SessionView>> sessionsToFilter = new HashMap<Row, List<SessionView>>(); 298 // The filtering criteria saves as local variables 299 final String text = criteria.getText(); 300 final Set<Track> tracks = criteria.getTracks(); 301 final Set<SessionType> types = criteria.getSessionTypes(); 302 // For each row we needed to consider, inspect all of its session views and 303 // filter them appropriately. 304 for (Row row : allRows) { 305 for (SessionView view : row.sessions) { 306 // If the task has been canceled, then bail. This is done on each iteration 307 // so that we do as little work after the thread has been canceled a possible 308 if (isCancelled()) return null; 309 // If the view is presently filtered, but should no longer be filtered, 310 // then add it to the sessionsToRestore map. 311 final Session s = view.session; 312 final Track t = s.getTrack(); 313 final SessionType st = s.getSessionType(); 314 315 final boolean shouldKeep = (s.getTitle().toLowerCase().contains(text) || 316 s.getSpeakersDisplay().toLowerCase().contains(text) || 317 s.getSummary().toLowerCase().contains(text) || 318 s.getAbbreviation().toLowerCase().contains(text)) && 319 (!(t != null && tracks.contains(t)) && 320 !(st != null && types.contains(st))); 321 322 if (view.filtered.get() && shouldKeep) { 323 // We do not need to filter this one any more 324 List<SessionView> views = sessionsToRestore.get(row); 325 if (views == null) { 326 views = new ArrayList<SessionView>(); 327 sessionsToRestore.put(row, views); 328 } 329 views.add(view); 330 // If the view is presently not being filtered, but should be, then add 331 // it to the sessionsToFilter map. 332 } else if (!view.filtered.get() && !shouldKeep) { 333 // We need to filter this one 334 List<SessionView> views = sessionsToFilter.get(row); 335 if (views == null) { 336 views = new ArrayList<SessionView>(); 337 sessionsToFilter.put(row, views); 338 } 339 views.add(view); 340 } 341 } 342 } 343 344 return new Runnable() { 345 @Override public void run() { 346 // Hide all sessions that have been filtered. 347 for (Map.Entry<Row, List<SessionView>> entry : sessionsToFilter.entrySet()) { 348 final Row row = entry.getKey(); 349 final List<SessionView> views = entry.getValue(); 350 for (SessionView view : views) { 351 assert view.filtered.get() == false; 352 assert row.numVisible > 0; 353 354 row.numVisible--; 355 view.filtered.set(true); 356 357 if (row.numVisible == 0) { 358 filtered.add(row); 359 rows.remove(row); 360 } 361 } 362 } 363 364 // Restore all the sessions that have been restored. 365 for (Map.Entry<Row, List<SessionView>> entry : sessionsToRestore.entrySet()) { 366 final Row row = entry.getKey(); 367 final List<SessionView> views = entry.getValue(); 368 for (SessionView view : views) { 369 assert view.filtered.get() == true; 370 assert row.numVisible >= 0; 371 372 if (row.numVisible == 0) { 373 filtered.remove(row); 374 rows.add(row); 375 } 376 377 row.numVisible++; 378 view.filtered.set(false); 379 } 380 } 381 382 // Resort the rows so that the dates are all correct 383 FXCollections.sort(rows, new Comparator<Row>() { 384 @Override public int compare(Row o1, Row o2) { 385 return o1.date.compareTo(o2.date); 386 } 387 }); 388 } 389 }; 390 } 391 }; 392 filterTask.setOnSucceeded(new EventHandler<WorkerStateEvent>() { 393 @Override public void handle(WorkerStateEvent event) { 394 assert !filterTask.isCancelled(); 395 Runnable r = filterTask.getValue(); 396 if (r != null) { 397 filterTask.getValue().run(); 398 } 399 } 400 }); 401 filterTask.setOnFailed(new EventHandler<WorkerStateEvent>() { 402 @Override public void handle(WorkerStateEvent event) { 403 // Debugging 404 event.getSource().getException().printStackTrace(); 405 } 406 }); 407 408 // Execute the filterTask on this single-threaded Executor 409 FILTER_EXECUTOR.submit(filterTask); 410 } 411 412 @Override protected void layoutChildren() { 413 final int w = (int)getWidth() - 24; 414 final int h = (int)getHeight() - 24; 415 416 sessionFilterPopover.autosize(); 417 sessionFilterPopover.setLayoutX(w - sessionFilterPopover.prefWidth(-1) + 17); 418 sessionFilterPopover.setLayoutY(58); 419 searchBar.resize(w,30); 420 searchBar.setLayoutX(12); 421 searchBar.setLayoutY(12); 422 423 list.resize(w,h - 42); 424 list.setLayoutX(12); 425 list.setLayoutY(53); 426 } 427 428 /** 429 * Part of the "View-Model", the row represents a row in the list view. 430 * Each row is made up of a Date and a number of "SessionView" objects. 431 * Each SessionView is a "View-Model" for a Session. 432 */ 433 public class Row { 434 final Date date; 435 final List<SessionView> sessions; 436 int numVisible; 437 438 Row(Date date, List<SessionView> sessions) { 439 this.date = date; 440 this.sessions = sessions; 441 for (SessionView v : sessions) { 442 if (!v.filtered.get()) numVisible++; 443 } 444 } 445 } 446 447 /** 448 * The "View-Model" for a Session. This contains a single observable Boolean property 449 * called "filtered", and a reference to the session that this view represents. 450 */ 451 class SessionView { 452 BooleanProperty filtered = new SimpleBooleanProperty(this, "filtered", false); 453 Session session; 454 } 455 456 @Override public ListCell<Row> call(ListView<Row> listView) { 457 return new TimeSlotListCell(); 458 } 459 460 static int counter = 0; 461 private class TimeSlotListCell extends ListCell<Row> implements Skin<TimeSlotListCell> { 462 private Text timeText = new Text(); 463 private Text dayText = new Text(); 464 private List<Tile> tiles = new ArrayList<Tile>(); 465 private String name = "TimeSlotListCell-" + counter++; 466 467 @Override public String toString() { return name; } 468 469 private TimeSlotListCell() { 470 super(); 471 setSkin(this); 472 getStyleClass().clear(); 473 timeText.setFont(TIME_FONT); 474 timeText.setFill(BLUE); 475 dayText.setFont(DAY_FONT); 476 dayText.setFill(BLUE); 477 getChildren().add(dayText); 478 } 479 480 @Override protected double computePrefWidth(double height) { 481 return 330; 482 } 483 484 // TODO ListView is broken -- it doesn't support content bias! Workaround: return the width as the pref width 485 @Override protected double computePrefHeight(double width) { 486 final Row row = getItem(); 487 if (row == null || row.numVisible == 0) return (getIndex() == 0 ? 5 : 0) + SLOT_HEIGHT + 5; 488 489 // We have a row and it has sessions. We need to figure out how many tiles 490 // can be placed in the session layout area per row (right of the time column). 491 // That will tell us how many rows of sessions we will have, which then gives us 492 // a pref height that we can return. 493 if (width == -1) width = getWidth(); 494 // TODO normally insets need to be taken into account but we're ignoring them 495 final double sessionAreaWidth = width - TIME_COLUMN_WIDTH; 496 final int tilesPerRow = (int) (sessionAreaWidth / (SLOT_WIDTH + SLOT_GAP)); 497 if (tilesPerRow == 0) { 498 return SLOT_HEIGHT + (getIndex() == 0 ? 10 : 5); 499 } 500 final int numRows = (int) Math.ceil(row.numVisible / (double) tilesPerRow); 501 // TODO did I get this right? 502 return (getIndex() == 0 ? 5 : 0) + (numRows > 0 ? (numRows * (SLOT_HEIGHT + SLOT_GAP)) : 5); 503 } 504 505 @Override protected void layoutChildren() { 506 final int top = (getIndex() == 0 ? 5 : 0); 507 timeText.relocate(5, top + 5); 508 dayText.relocate(5, top + 30); 509 510 final Row row = getItem(); 511 final int numTiles = row == null ? 0 : row.numVisible; 512 final double width = getWidth(); 513 final double sessionAreaWidth = width - TIME_COLUMN_WIDTH; 514 final int tilesPerRow = (int) (sessionAreaWidth / (SLOT_WIDTH + SLOT_GAP)); 515 final int numRows = tilesPerRow == 0 ? 0 : (int) Math.ceil(numTiles / (double) tilesPerRow); 516 517 int tileIndex = 0; 518 for (int r=0; r<numRows; r++) { 519 for (int c=0; c<tilesPerRow && tileIndex < tiles.size();) { 520 Tile tile = tiles.get(tileIndex++); 521 if (tile.isVisible()) { 522 double x = c * (SLOT_WIDTH + SLOT_GAP) + TIME_COLUMN_WIDTH; 523 double y = r * (SLOT_HEIGHT + SLOT_GAP) + 5; 524 tile.resizeRelocate(x, y, SLOT_WIDTH, SLOT_HEIGHT); 525 c++; 526 } 527 } 528 } 529 } 530 531 // CELL METHODS 532 @Override protected void updateItem(Row item, boolean empty) { 533 super.updateItem(item, empty); 534 535 if (!empty) { 536 List<SessionView> views = item.sessions; 537 538 // Unlink all tiles 539 for (int i=0; i<tiles.size(); i++) { 540 Tile tile = tiles.get(i); 541 tile.unlink(); 542 } 543 544 // Add any missing tiles 545 for (int i=tiles.size(); i<views.size(); i++) { 546 Tile tile = new Tile(this); 547 tiles.add(tile); 548 getChildren().add(tile); 549 } 550 551 // Update all tiles 552 for (int i=0; i<views.size(); i++) { 553 SessionView view = views.get(i); 554 Tile tile = tiles.get(i); 555 tile.update(view); 556 } 557 558 // update date text 559 if (item.date == null) { 560 timeText.setVisible(false); 561 dayText.setVisible(false); 562 } else { 563 final Date date = item.date; 564 timeText.setText(TIME_FORMAT.format(date)); 565 dayText.setText(DAY_FORMAT.format(date).toUpperCase()); 566 timeText.setVisible(true); 567 dayText.setVisible(true); 568 } 569 } else { 570 timeText.setVisible(false); 571 dayText.setVisible(false); 572 for (Tile tile : tiles) { 573 tile.unlink(); 574 } 575 } 576 } 577 578 // SKIN METHODS 579 @Override public TimeSlotListCell getSkinnable() { return this; } 580 @Override public Node getNode() { return timeText; } 581 @Override public void dispose() {} 582 } 583 584 class Tile extends Region implements EventHandler<MouseEvent>, InvalidationListener { 585 private Text title; 586 private Text speaker; 587 private Rectangle bar; 588 private SessionView view; // Doubly-linked between Tile & SessionView. 589 private TimeSlotListCell cell; 590 591 Tile(TimeSlotListCell cell) { 592 this.cell = cell; 593 title = new Text(); 594 title.setWrappingWidth(SLOT_TEXT_WRAP); 595 title.setFont(TITLE_FONT); 596 title.setFill(TITLE_COLOR); 597 title.setTextOrigin(VPos.TOP); 598 speaker = new Text(); 599 speaker.setFont(SPEAKERS_FONT); 600 speaker.setFill(SPEAKERS_COLOR); 601 speaker.setTextOrigin(VPos.TOP); 602 bar = new Rectangle(SLOT_BAR_WIDTH, 0); 603 setOnMouseClicked(this); 604 getChildren().addAll(bar, title, speaker); 605 } 606 607 void update(SessionView view) { 608 this.view = view; 609 view.filtered.addListener(this); 610 Session session = view.session; 611 String s = session.getAbbreviation() + " :: " + session.getTitle(); 612 if (s.length() > MAX_TITLE_CHARS) s = s.substring(0, MAX_TITLE_CHARS).trim() + "..."; 613 title.setText(s); 614 s = session.getSpeakersDisplay(); 615 if (s.length() > MAX_SPEAKER_CHARS) s = s.substring(0, MAX_SPEAKER_CHARS).trim() + "..."; 616 speaker.setText(s); 617 bar.setFill(Color.web(session.getTrack().getColor())); 618 setVisible(!view.filtered.get()); 619 } 620 621 @Override public void invalidated(Observable observable) { 622 setVisible(!this.view.filtered.get()); 623 cell.requestLayout(); 624 } 625 626 void unlink() { 627 if (this.view != null) { 628 this.view.filtered.removeListener(this); 629 } 630 this.view = null; 631 setVisible(false); 632 } 633 634 @Override protected double computePrefWidth(double height) { 635 return SLOT_WIDTH; 636 } 637 638 @Override protected double computePrefHeight(double width) { 639 return SLOT_HEIGHT; 640 } 641 642 @Override protected void layoutChildren() { 643 // I'm ignoring the insets 644 bar.setHeight(getHeight()); 645 646 final double wrappingWidth = getWidth() - SLOT_BAR_WIDTH - SLOT_BAR_GAP; 647 title.setWrappingWidth(wrappingWidth); 648 speaker.setWrappingWidth(wrappingWidth); 649 650 title.setX(SLOT_BAR_WIDTH + SLOT_BAR_GAP); 651 speaker.setX(SLOT_BAR_WIDTH + SLOT_BAR_GAP); 652 speaker.setY(title.prefHeight(wrappingWidth)); 653 } 654 655 @Override public void handle(MouseEvent mouseEvent) { 656 // find SessionTime 657 if (view == null || view.session == null) return; 658 Session session = view.session; 659 long startTime = cell.getItem().date.getTime(); 660 SessionTime sessionTime = null; 661 for(SessionTime st : session.getSessionTimes()) { 662 if (startTime == st.getStart().getTime()) { 663 sessionTime = st; 664 break; 665 } 666 } 667 // get/create event 668 Event event = sessionTime.getEvent(); 669 if (event == null) { 670 Calendar calendar = Calendar.getInstance(); 671 calendar.setTime(sessionTime.getStart()); 672 calendar.add(Calendar.MINUTE, sessionTime.getLength()); 673 event = new Event(session, sessionTime.getStart(), calendar.getTime(), sessionTime); 674 } 675 // show popover 676 EventPopoverPage page = new EventPopoverPage(dataService, event, false); 677 popover.clearPages(); 678 popover.pushPage(page); 679 popover.show(); 680 } 681 } 682 }