001package conexp.fx.gui.context; 002 003/* 004 * #%L 005 * Concept Explorer FX 006 * %% 007 * Copyright (C) 2010 - 2019 Francesco Kriegel 008 * %% 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as 011 * published by the Free Software Foundation, either version 3 of the 012 * License, or (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public 020 * License along with this program. If not, see 021 * <http://www.gnu.org/licenses/gpl-3.0.html>. 022 * #L% 023 */ 024import java.util.Collection; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.Timer; 028import java.util.TimerTask; 029import java.util.concurrent.ConcurrentHashMap; 030 031import org.semanticweb.owlapi.model.OWLClassExpression; 032 033import com.google.common.base.Function; 034import com.google.common.collect.Collections2; 035 036import conexp.fx.core.collections.Pair; 037import conexp.fx.core.collections.relation.RelationEvent; 038import conexp.fx.core.collections.relation.RelationEventHandler; 039import conexp.fx.core.context.Concept; 040import conexp.fx.core.context.MatrixContext; 041import conexp.fx.core.context.MatrixContext.Incidence; 042import conexp.fx.core.util.Constants; 043import conexp.fx.core.util.IdGenerator; 044import conexp.fx.core.util.OWLUtil; 045import conexp.fx.gui.ConExpFX; 046import conexp.fx.gui.cellpane.Cell; 047import conexp.fx.gui.cellpane.CellPane; 048import conexp.fx.gui.cellpane.InteractionMode; 049import conexp.fx.gui.dataset.FCADataset; 050import conexp.fx.gui.graph.ConceptGraph; 051import conexp.fx.gui.util.ColorScheme; 052import conexp.fx.gui.util.LaTeX; 053import conexp.fx.gui.util.Platform2; 054import de.tudresden.inf.tcs.fcalib.Implication; 055import javafx.animation.Interpolator; 056import javafx.animation.KeyFrame; 057import javafx.animation.KeyValue; 058import javafx.animation.Timeline; 059import javafx.application.Platform; 060import javafx.beans.binding.DoubleBinding; 061import javafx.beans.binding.IntegerBinding; 062import javafx.beans.binding.StringBinding; 063import javafx.beans.property.BooleanProperty; 064import javafx.beans.property.DoubleProperty; 065import javafx.beans.property.IntegerProperty; 066import javafx.beans.property.SimpleBooleanProperty; 067import javafx.beans.property.SimpleDoubleProperty; 068import javafx.beans.property.SimpleIntegerProperty; 069import javafx.beans.value.ChangeListener; 070import javafx.beans.value.ObservableValue; 071import javafx.collections.MapChangeListener; 072import javafx.event.ActionEvent; 073import javafx.event.EventHandler; 074import javafx.geometry.Bounds; 075import javafx.geometry.Insets; 076import javafx.geometry.Pos; 077import javafx.scene.Node; 078import javafx.scene.control.Button; 079import javafx.scene.control.ButtonBuilder; 080import javafx.scene.control.ContextMenu; 081import javafx.scene.control.MenuItem; 082import javafx.scene.control.ScrollBar; 083import javafx.scene.control.Slider; 084import javafx.scene.control.SliderBuilder; 085import javafx.scene.control.TextField; 086import javafx.scene.control.TextFieldBuilder; 087import javafx.scene.control.ToggleButton; 088import javafx.scene.control.ToolBar; 089import javafx.scene.image.Image; 090import javafx.scene.image.ImageView; 091import javafx.scene.image.ImageViewBuilder; 092import javafx.scene.input.KeyEvent; 093import javafx.scene.input.MouseEvent; 094import javafx.scene.layout.BorderPane; 095import javafx.scene.layout.ColumnConstraints; 096import javafx.scene.layout.GridPane; 097import javafx.scene.layout.HBox; 098import javafx.scene.layout.RowConstraints; 099import javafx.scene.paint.Color; 100import javafx.scene.text.TextAlignment; 101import javafx.util.Duration; 102 103public class MatrixContextWidget<G, M> extends BorderPane { 104 105 public final class RowHeaderPane extends CellPane<RowHeaderPane, RowHeaderCell> { 106 107 private RowHeaderPane(final boolean interactive) { 108 super("DomainPane", InteractionMode.ROWS, interactive); 109 this.rowHeightDefault.bind(MatrixContextWidget.this.cellSizeDefault); 110 this.columnWidthDefault.bind(MatrixContextWidget.this.rowHeaderSizeDefault); 111 this.zoomFactor.bind(MatrixContextWidget.this.zoomFactor); 112 this.textSizeDefault.bind(MatrixContextWidget.this.textSizeDefault); 113 this.animate.bind(MatrixContextWidget.this.animate); 114 this.autoSizeRows.set(false); 115 this.autoSizeColumns.set(true); 116 this.maxColumns.set(1); 117 this.maxRows.set(context.rowHeads().size()); 118// final RelationEventHandler<G, M> eventHandler = new RelationEventHandler<G, M>() { 119// 120// public final void handle(final RelationEvent<G, M> event) { 121// Platform2.runOnFXThread(new Runnable() { 122// 123// public void run() { 124//// System.out.println("updating rows"); 125// maxRows.set(context.rowHeads().size()); 126//// updateContent(); 127// } 128// }); 129// } 130// }; 131// context.addEventHandler(eventHandler, RelationEvent.ROWS); 132// context.addEventHandler(eventHandler, RelationEvent.ROWS_ADDED); 133// context.addEventHandler(eventHandler, RelationEvent.ROWS_REMOVED); 134 context.addEventHandler( 135 __ -> Platform2.runOnFXThread(() -> maxRows.set(context.rowHeads().size())), 136 RelationEvent.ROWS); 137 if (dataset != null) 138 this.interactionPane.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() { 139 140 public final void handle(final MouseEvent event) { 141 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 142 } 143 }); 144 zoomFactor.addListener((__, ___, ____) -> updateContent()); 145 } 146 147 protected final RowHeaderCell createCell(final int row, final int column) { 148 return new RowHeaderCell(row); 149 } 150 151 public final void highlightConcept(final Collection<Integer> domainIndices) { 152 highlight(domainIndices, null); 153 } 154 155 protected final Map<G, Node> decorations = new ConcurrentHashMap<>(); 156 157 public final void addDecoration(final G row, final Node decoration) { 158 decorations.put(row, decoration); 159 } 160 } 161 162 public final class ColHeaderPane extends CellPane<ColHeaderPane, ColHeaderCell> { 163 164 private ColHeaderPane(final boolean interactive) { 165 super("CodomainPane", InteractionMode.COLUMNS, interactive); 166 this.rowHeightDefault.bind(MatrixContextWidget.this.colHeaderSizeDefault); 167 this.columnWidthDefault.bind(MatrixContextWidget.this.cellSizeDefault); 168 this.zoomFactor.bind(MatrixContextWidget.this.zoomFactor); 169 this.textSizeDefault.bind(MatrixContextWidget.this.textSizeDefault); 170 this.animate.bind(MatrixContextWidget.this.animate); 171 this.autoSizeRows.set(true); 172 this.autoSizeColumns.set(false); 173 this.maxRows.set(1); 174 this.maxColumns.set(context.colHeads().size()); 175 final RelationEventHandler<G, M> eventHandler = new RelationEventHandler<G, M>() { 176 177 public final void handle(final RelationEvent<G, M> event) { 178 Platform2.runOnFXThread(new Runnable() { 179 180 public void run() { 181// System.out.println("updating columns"); 182 maxColumns.set(context.colHeads().size()); 183// updateContent(); 184 } 185 }); 186 } 187 }; 188 context.addEventHandler(eventHandler, RelationEvent.COLUMNS); 189// context.addEventHandler(eventHandler, RelationEvent.COLUMNS_ADDED); 190// context.addEventHandler(eventHandler, RelationEvent.COLUMNS_REMOVED); 191 if (dataset != null) 192 this.interactionPane.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() { 193 194 public final void handle(final MouseEvent event) { 195 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 196 } 197 }); 198 zoomFactor.addListener((__, ___, ____) -> updateContent()); 199 } 200 201 protected final ColHeaderCell createCell(final int row, final int column) { 202 return new ColHeaderCell(column); 203 } 204 205 public final void highlightConcept(final Collection<Integer> codomainIndices) { 206 highlight(null, codomainIndices); 207 } 208 } 209 210 public final class ContextPane extends CellPane<ContextPane, ContextCell> { 211 212 private ContextPane(final boolean interactive) { 213 super("FormalContextPane", InteractionMode.ROWS_AND_COLUMNS, interactive); 214 this.textSizeDefault.bind(MatrixContextWidget.this.incidenceSizeDefault); 215 this.zoomFactor.bind(MatrixContextWidget.this.zoomFactor); 216 this.bind(rowHeaderPane, InteractionMode.ROWS); 217 this.bind(colHeaderPane, InteractionMode.COLUMNS); 218 if (dataset != null) 219 this.rowMap.addListener(new MapChangeListener<Integer, Integer>() { 220 221 public final void onChanged( 222 final javafx.collections.MapChangeListener.Change<? extends Integer, ? extends Integer> change) { 223 dataset.unsavedChanges.set(true); 224 } 225 }); 226 if (dataset != null) 227 this.columnMap.addListener(new MapChangeListener<Integer, Integer>() { 228 229 public final void onChanged( 230 final javafx.collections.MapChangeListener.Change<? extends Integer, ? extends Integer> change) { 231 dataset.unsavedChanges.set(true); 232 } 233 }); 234 if (dataset != null) 235 this.interactionPane.addEventHandler(MouseEvent.MOUSE_EXITED, new EventHandler<MouseEvent>() { 236 237 public final void handle(final MouseEvent event) { 238 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 239 } 240 }); 241 zoomFactor.addListener((__, ___, ____) -> updateContent()); 242 } 243 244 protected final ContextCell createCell(final int row, final int column) { 245 return new ContextCell(row, column); 246 } 247 } 248 249 public final class RowHeaderCell extends Cell<RowHeaderCell, RowHeaderPane> { 250 251 private ImageView view = ImageViewBuilder.create().build(); 252 253 private RowHeaderCell(final int row) { 254 super(rowHeaderPane, row, 0, Pos.CENTER_RIGHT, TextAlignment.RIGHT, false, null, false); 255 this.contentPane.get().getChildren().add(view); 256 this.contentPane.get().text.setOpacity(0); 257 if (cellPane.interactive) { 258 final ContextMenu contextMenu = new ContextMenu(); 259 final MenuItem editItem = new MenuItem("Edit"); 260 final MenuItem removeItem = new MenuItem("Remove"); 261 final MenuItem selectItem = new MenuItem("Select"); 262 final MenuItem insertItem = new MenuItem("Insert"); 263 if (dataset != null && dataset.editable) 264 contextMenu.getItems().addAll(editItem, removeItem, selectItem, insertItem); 265 else 266 contextMenu.getItems().addAll(removeItem, selectItem); 267 insertItem.setOnAction(__ -> { 268 ((FCADataset<String, String>) dataset).addObject( 269 (dataset.context.isHomogen() ? "Element " : "Object ") + IdGenerator.getNextId(dataset), 270 contentCoordinates.get().x()); 271 rowHeaderPane.rowOpacityMap.keySet().stream().sorted().filter(i -> i >= contentCoordinates.get().x()).forEach( 272 i -> rowHeaderPane.rowOpacityMap.put(i + 1, rowHeaderPane.rowOpacityMap.remove(i))); 273 }); 274 editItem.setOnAction(__ -> { 275 if (MatrixContextWidget.this.dataset.editable) { 276 final G object = context.rowHeads().get(contentCoordinates.get().x()); 277 final TextField textField = TextFieldBuilder.create().text((String) object).build(); 278 textField.addEventHandler(KeyEvent.KEY_RELEASED, event -> { 279 switch (event.getCode()) { 280 case ENTER: 281 dataset.renameObject(object, (G) textField.getText().trim()); 282 case ESCAPE: 283// interactionPane.get().getChildren().remove(textField); 284 contentPane.get().getChildren().remove(textField); 285 contentPane.get().getChildren().add(view); 286 } 287 }); 288// interactionPane.get().getChildren().add(textField); 289 contentPane.get().getChildren().remove(view); 290 contentPane.get().getChildren().add(textField); 291 textField.focusedProperty().addListener( 292 (observable, oldValue, newValue) -> new Timer().schedule(new TimerTask() { 293 294 public final void run() { 295 Platform.runLater(() -> textField.selectAll()); 296 } 297 }, 20)); 298 textField.requestFocus(); 299 } else { 300 System.out.println("no instance of MatrixContextWidget"); 301 } 302 }); 303 removeItem.setOnAction(__ -> { 304 dataset.removeObject(context.rowHeads().get(contentCoordinates.get().x())); 305 rowHeaderPane.rowOpacityMap.keySet().stream().sorted().filter(i -> i > contentCoordinates.get().x()).forEach( 306 i -> rowHeaderPane.rowOpacityMap.put(i - 1, rowHeaderPane.rowOpacityMap.remove(i))); 307 }); 308 selectItem.setOnAction(__ -> select()); 309 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { 310 switch (event.getButton()) { 311 case PRIMARY: 312 select(); 313 break; 314 case SECONDARY: 315 contextMenu.show(interactionPane.getValue(), event.getScreenX(), event.getScreenY()); 316 } 317 }); 318 } 319 if (dataset != null) 320 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_ENTERED, event -> { 321 if (contextPane.highlight.get()) 322 dataset.conceptGraph.highlight( 323 true, 324 dataset.conceptGraph.highlightRequests.object(context.rowHeads().get(contentCoordinates.get().x()))); 325 }); 326 if (cellPane.autoSizeRows.get() || cellPane.autoSizeColumns.get()) 327 view.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() { 328 329 @Override 330 public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) { 331 final double decorationWidth = 332 cellPane.decorations.containsKey(context.rowHeads().get(contentCoordinates.get().x())) 333 ? cellPane.decorations 334 .get(context.rowHeads().get(contentCoordinates.get().x())) 335 .layoutBoundsProperty() 336 .get() 337 .getWidth() 338 : 0d; 339 final double width = newValue.getWidth() + decorationWidth; 340 if (width > cellPane.maximalTextWidth.get()) 341 cellPane.maximalTextWidth.set(width); 342 } 343 }); 344 context.addEventHandler(event -> Platform2.runOnFXThread(RowHeaderCell.this::updateContent), RelationEvent.ROWS); 345 updateContent(); 346 } 347 348 private final void select() { 349 synchronized (rowHeaderPane.rowOpacityMap) { 350 if (rowHeaderPane.rowOpacityMap.containsKey(contentCoordinates.get().x())) { 351 if (dataset != null) 352 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 353 rowHeaderPane.rowOpacityMap.remove(contentCoordinates.get().x()); 354 if (dataset != null) 355 dataset.selectObject(context.rowHeads().get(contentCoordinates.get().x())); 356 } else { 357 if (dataset != null) 358 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 359 rowHeaderPane.rowOpacityMap.put(contentCoordinates.get().x(), Constants.HIDE_OPACITY); 360 if (dataset != null) 361 dataset.ignoreObject(context.rowHeads().get(contentCoordinates.get().x())); 362 } 363 } 364// if (rowHeaderPane.rowOpacityMap.containsKey(contentCoordinates.get().x())) { 365// rowHeaderPane.rowOpacityMap.remove(contentCoordinates.get().x()); 366//// final G object = context.getDomain().get(contentCoordinates.get().x()); 367//// context.selectObject(object); 368// } else { 369// rowHeaderPane.rowOpacityMap.put(contentCoordinates.get().x(), Constants.HIDE_OPACITY); 370//// final G object = context.getDomain().get(contentCoordinates.get().x()); 371//// context.deselectObject(object); 372// } 373 } 374 375 public final void updateContent() { 376 try { 377 final String string = context.rowHeads().get(contentCoordinates.get().x()).toString(); 378 textContent.set(string); 379 view.setImage(LaTeX.toFXImage(string, (float) (16d * zoomFactor.get()))); 380 if (cellPane.decorations.containsKey(context.rowHeads().get(contentCoordinates.get().x()))) { 381 interactionPane.get().getChildren().removeAll(cellPane.decorations.values()); 382 interactionPane 383 .get() 384 .setRight(cellPane.decorations.get(context.rowHeads().get(contentCoordinates.get().x()))); 385 } 386 } catch (IndexOutOfBoundsException __) {} 387 } 388 } 389 390 public final class ColHeaderCell extends Cell<ColHeaderCell, ColHeaderPane> { 391 392 private ImageView view = ImageViewBuilder.create().build(); 393 394 private ColHeaderCell(final int column) { 395 super(colHeaderPane, 0, column, Pos.CENTER_LEFT, TextAlignment.LEFT, true, null, false); 396 this.contentPane.get().getChildren().add(view); 397 this.contentPane.get().text.setOpacity(0); 398 if (cellPane.interactive) { 399 final ContextMenu contextMenu = new ContextMenu(); 400 final MenuItem editItem = new MenuItem("Edit"); 401 final MenuItem removeItem = new MenuItem("Remove"); 402 final MenuItem selectItem = new MenuItem("Select"); 403 final MenuItem insertItem = new MenuItem("Insert"); 404 if (dataset != null && dataset.editable) 405 contextMenu.getItems().addAll(editItem, removeItem, selectItem, insertItem); 406 else 407 contextMenu.getItems().addAll(removeItem, selectItem); 408 insertItem.setOnAction(__ -> { 409 ((FCADataset<String, String>) dataset).addAttribute( 410 (dataset.context.isHomogen() ? "Element " : "Attribute ") + IdGenerator.getNextId(dataset), 411 contentCoordinates.get().y()); 412 colHeaderPane.columnOpacityMap 413 .keySet() 414 .stream() 415 .sorted() 416 .filter(i -> i >= contentCoordinates.get().y()) 417 .forEach(i -> colHeaderPane.columnOpacityMap.put(i + 1, colHeaderPane.columnOpacityMap.remove(i))); 418 }); 419 editItem.setOnAction(event -> { 420 if (MatrixContextWidget.this.dataset.editable) { 421 final M attribute = context.colHeads().get(contentCoordinates.get().y()); 422 final TextField textField = TextFieldBuilder.create().text((String) attribute).build(); 423 textField.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> { 424 switch (keyEvent.getCode()) { 425 case ENTER: 426 dataset.renameAttribute(attribute, (M) textField.getText().trim()); 427 case ESCAPE: 428// interactionPane.get().getChildren().remove(textField); 429 contentPane.get().getChildren().remove(textField); 430 contentPane.get().getChildren().add(view); 431 } 432 }); 433// textField.rotateProperty().set(-90); 434 textField.setMinSize(colHeaderPane.rowHeight.get(), cellSize.get()); 435 textField.setMaxSize(colHeaderPane.rowHeight.get(), cellSize.get()); 436// interactionPane.get().getChildren().add(textField); 437 contentPane.get().getChildren().remove(view); 438 contentPane.get().getChildren().add(textField); 439 textField.focusedProperty().addListener( 440 (observable, oldValue, newValue) -> new Timer().schedule(new TimerTask() { 441 442 public final void run() { 443 Platform.runLater(() -> textField.selectAll()); 444 } 445 }, 20)); 446 textField.requestFocus(); 447 } 448 }); 449 removeItem.setOnAction(event -> { 450 dataset.removeAttribute(context.colHeads().get(contentCoordinates.get().y())); 451 colHeaderPane.columnOpacityMap.remove(contentCoordinates.get().y()); 452 colHeaderPane.columnOpacityMap 453 .keySet() 454 .stream() 455 .sorted() 456 .filter(i -> i > contentCoordinates.get().y()) 457 .forEach(i -> colHeaderPane.columnOpacityMap.put(i - 1, colHeaderPane.columnOpacityMap.remove(i))); 458 }); 459 selectItem.setOnAction(event -> select()); 460// if (!tab.fca.context.selectedAttributes().contains(tab.fca.context.colHeads().get(contentCoordinates.get().y()))) 461// colHeaderPane.columnOpacityMap.put(contentCoordinates.get().y(), Constants.HIDE_OPACITY); 462 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { 463 switch (event.getButton()) { 464 case PRIMARY: 465 select(); 466 break; 467 case SECONDARY: 468 contextMenu.show(interactionPane.getValue(), event.getScreenX(), event.getScreenY()); 469 } 470 }); 471 } 472 if (dataset != null) 473 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_ENTERED, event -> { 474 if (contextPane.highlight.get()) { 475 final M m = context.colHeads().get(contentCoordinates.get().y()); 476 if (context.selectedAttributes().contains(m)) 477 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.attribute(m)); 478 } 479 }); 480 if (cellPane.autoSizeRows.get() || cellPane.autoSizeColumns.get()) 481 view.layoutBoundsProperty().addListener(new ChangeListener<Bounds>() { 482 483 @Override 484 public void changed(ObservableValue<? extends Bounds> observable, Bounds oldValue, Bounds newValue) { 485 final double width = newValue.getWidth(); 486 if (width > cellPane.maximalTextWidth.get()) 487 cellPane.maximalTextWidth.set(width); 488 } 489 }); 490 context 491 .addEventHandler(event -> Platform2.runOnFXThread(ColHeaderCell.this::updateContent), RelationEvent.COLUMNS); 492 updateContent(); 493 } 494 495 private void select() { 496 synchronized (colHeaderPane.columnOpacityMap) { 497 if (colHeaderPane.columnOpacityMap.containsKey(contentCoordinates.get().y())) { 498 if (dataset != null) 499 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 500 colHeaderPane.columnOpacityMap.remove(contentCoordinates.get().y()); 501 if (dataset != null) 502 dataset.selectAttribute(context.colHeads().get(contentCoordinates.get().y())); 503 } else { 504 if (dataset != null) 505 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 506 colHeaderPane.columnOpacityMap.put(contentCoordinates.get().y(), Constants.HIDE_OPACITY); 507 if (dataset != null) 508 dataset.ignoreAttribute(context.colHeads().get(contentCoordinates.get().y())); 509 } 510 } 511 } 512 513 public final void updateContent() { 514 try { 515 final M m = context.colHeads().get(contentCoordinates.get().y()); 516 final String string = 517 m instanceof OWLClassExpression ? OWLUtil.toString((OWLClassExpression) m) : m.toString(); 518 textContent.set(string); 519 view.setImage(LaTeX.toFXImage(string, (float) (16d * zoomFactor.get()))); 520 } catch (IndexOutOfBoundsException __) {} 521 } 522 } 523 524 public final class ContextCell extends Cell<ContextCell, ContextPane> { 525 526 private ContextCell(final int row, final int column) { 527 super(contextPane, row, column, Pos.CENTER, TextAlignment.CENTER, false, null, false); 528 if (dataset != null) 529 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() { 530 531 public final void handle(final MouseEvent event) { 532 final G g = context.rowHeads().get(contentCoordinates.get().x()); 533 final M m = context.colHeads().get(contentCoordinates.get().y()); 534 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 535 dataset.flip(g, m); 536 dataset.unsavedChanges.set(true); 537 } 538 }); 539 else 540 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { 541 final G g = context.rowHeads().get(contentCoordinates.get().x()); 542 final M m = context.colHeads().get(contentCoordinates.get().y()); 543 if (context.contains(g, m)) 544 context.remove(g, m); 545 else 546 context.add(g, m); 547 }); 548 if (dataset != null) 549 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>() { 550 551 public final void handle(final MouseEvent event) { 552 if (contextPane.highlight.get()) { 553 final G g = context.rowHeads().get(contentCoordinates.get().x()); 554 final M m = context.colHeads().get(contentCoordinates.get().y()); 555 if (context.selectedAttributes().contains(m)) 556 if (textContent.get().equals(Constants.CROSS_CHARACTER)) 557 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.incidence(g, m)); 558 else if (textContent.get().equals(Constants.DOWN_ARROW_CHARACTER)) 559 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.downArrow(g, m)); 560 else if (textContent.get().equals(Constants.UP_ARROW_CHARACTER)) 561 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.upArrow(g, m)); 562 else if (textContent.get().equals(Constants.BOTH_ARROW_CHARACTER)) 563 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.bothArrow(g, m)); 564 else if (textContent.get().equals(Constants.NO_CROSS_CHARACTER)) 565 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.nonIncidence(g, m)); 566 } 567 } 568 }); 569 context.addEventHandler(event -> updateContent(), RelationEvent.ANY); 570// RelationEvent.ENTRIES_ADDED, 571// RelationEvent.ENTRIES_REMOVED, 572// RelationEvent.ALL_CHANGED, 573// RelationEvent.SELECTION_CHANGED); 574 updateContent(); 575 } 576 577 @SuppressWarnings("incomplete-switch") 578 public final void updateContent() { 579 try { 580 final G g = context.rowHeads().get(contentCoordinates.get().x()); 581 final M m = context.colHeads().get(contentCoordinates.get().y()); 582 if (context.selectedAttributes().contains(m) && context.selectedObjects().contains(g)) { 583 final Pair<Incidence, Incidence> p = context.selection.getValue(g, m, showArrows.get(), showPaths.get()); 584 final Incidence first = p.first(); 585 final Incidence second = p.second(); 586 ContextCell.this.textContent.set( 587 second != null && first == Incidence.NO_CROSS ? Constants.NO_CROSS_CHARACTER_BOLD : first.toString()); 588 switch (first) { 589 case BOTH_ARROW: 590 ContextCell.this.contentPane.get().text.setRotate(-45d); 591 break; 592 case DOWN_ARROW: 593 case UP_ARROW: 594 case CROSS: 595 case NO_CROSS: 596 ContextCell.this.contentPane.get().text.setRotate(0d); 597 break; 598 } 599 if (second == null) 600 ContextCell.this.contentPane.get().text.setFill(Color.BLACK); 601 else 602 switch (second) { 603 case BOTH_PATH: 604 ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_INTERVAL); 605 break; 606 case DOWN_PATH: 607 ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_LOWER); 608 break; 609 case UP_PATH: 610 ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_UPPER); 611 break; 612 } 613 } else { 614 ContextCell.this.textContent.set(context.getValue(g, m, false).first().toString()); 615 ContextCell.this.contentPane.get().text.setRotate(0d); 616 } 617 } catch (IndexOutOfBoundsException __) {} 618 } 619 } 620 621 protected final FCADataset<G, M> dataset; 622 protected final MatrixContext<G, M> context; 623 protected final GridPane centerPane = new GridPane(); 624 public final RowHeaderPane rowHeaderPane; 625 public final ColHeaderPane colHeaderPane; 626 public final ContextPane contextPane; 627 protected final ScrollBar rowScrollBar; 628 protected final ScrollBar colScrollBar; 629// public final ListSpinner<Integer> zoomSpinner =new ListSpinner<Integer>(-4, 4, 1); 630 public final Slider zoomSlider = SliderBuilder 631 .create() 632 .min(-4d) 633 .max(4d) 634 .value(0d) 635 // .showTickMarks(true) 636 // .minorTickCount(17) 637 // .majorTickUnit(1d) 638 // .snapToTicks(true) 639 .blockIncrement(0.25d) 640 .build(); 641 public final DoubleProperty zoomFactor = new SimpleDoubleProperty(0.01d); 642 public final IntegerProperty rowHeaderSizeDefault = new SimpleIntegerProperty(40); 643 public final IntegerProperty colHeaderSizeDefault = new SimpleIntegerProperty(40); 644 public final IntegerProperty cellSizeDefault = new SimpleIntegerProperty(20); 645 public final IntegerProperty textSizeDefault = new SimpleIntegerProperty(16); 646 public final IntegerProperty incidenceSizeDefault = new SimpleIntegerProperty(20); 647 public final IntegerBinding cellSize = new IntegerBinding() { 648 649 { 650 super.bind(zoomFactor, cellSizeDefault); 651 } 652 653 protected int computeValue() { 654 return (int) (zoomFactor.get() 655 * cellSizeDefault.doubleValue()); 656 }; 657 }; 658 public final IntegerBinding textSize = new IntegerBinding() { 659 660 { 661 super.bind(zoomFactor, textSizeDefault); 662 } 663 664 protected int computeValue() { 665 return (int) (zoomFactor.get() 666 * textSizeDefault.doubleValue()); 667 }; 668 }; 669 public final IntegerBinding incidenceSize = new IntegerBinding() { 670 671 { 672 super.bind(zoomFactor, textSizeDefault); 673 } 674 675 protected int computeValue() { 676 return (int) (zoomFactor.get() 677 * incidenceSizeDefault.doubleValue()); 678 }; 679 }; 680 public final BooleanProperty animate = new SimpleBooleanProperty(false); 681 public final BooleanProperty showArrows = new SimpleBooleanProperty(); 682 public final BooleanProperty showPaths = new SimpleBooleanProperty(); 683 public final ToggleButton highlightToggleButton = new ToggleButton(); 684 public final DoubleBinding height; 685 686 public MatrixContextWidget(final FCADataset<G, M> fcaInstance) { 687 this(fcaInstance, true); 688 } 689 690 /** 691 * @param dataset 692 * @param withToolbar 693 * @param orContext 694 * may only be set if fcaInstance is null, otherwise unexpected behaviour may occur. 695 */ 696 @SafeVarargs 697 public MatrixContextWidget( 698 final FCADataset<G, M> dataset, 699 final boolean withToolbar, 700 final MatrixContext<G, M>... orContext) { 701 this(dataset, withToolbar, true, orContext); 702 } 703 704 @SafeVarargs 705 public MatrixContextWidget( 706 final FCADataset<G, M> dataset, 707 final boolean withToolbar, 708 final boolean interactive, 709 final MatrixContext<G, M>... orContext) { 710 super(); 711 this.dataset = dataset; 712 if (dataset == null && orContext[0] != null) 713 this.context = orContext[0]; 714 else 715 this.context = dataset.context; 716 this.rowHeaderPane = new RowHeaderPane(interactive); 717 this.colHeaderPane = new ColHeaderPane(interactive); 718 this.contextPane = new ContextPane(interactive); 719 this.rowScrollBar = contextPane.getRowScrollBar(); 720 this.colScrollBar = contextPane.getColumnScrollBar(); 721 centerPane.setHgap(4); 722 centerPane.setVgap(4); 723 rowHeaderPane.colorScheme.setValue(ColorScheme.JAVA_FX); 724 colHeaderPane.colorScheme.setValue(ColorScheme.JAVA_FX); 725 contextPane.colorScheme.setValue(ColorScheme.JAVA_FX); 726 final RowConstraints firstRowConstraints = new RowConstraints(); 727 firstRowConstraints.minHeightProperty().bind(colHeaderPane.rowHeight); 728 firstRowConstraints.maxHeightProperty().bind(colHeaderPane.rowHeight); 729 final RowConstraints secondRowConstraints = new RowConstraints(); 730 secondRowConstraints.minHeightProperty().bind(cellSize); 731 secondRowConstraints.prefHeightProperty().bind(new IntegerBinding() { 732 733 { 734 super.bind(rowHeaderPane.maxRows, cellSize); 735 } 736 737 protected final int computeValue() { 738 return context.rowHeads().size() * cellSize.get(); 739 } 740 }); 741 final RowConstraints thirdRowConstraints = new RowConstraints(); 742 thirdRowConstraints.minHeightProperty().bind(cellSize); 743 thirdRowConstraints.maxHeightProperty().bind(cellSize); 744 centerPane.getRowConstraints().addAll(firstRowConstraints, secondRowConstraints, thirdRowConstraints); 745 final ColumnConstraints firstColumnConstraints = new ColumnConstraints(); 746 firstColumnConstraints.minWidthProperty().bind(rowHeaderPane.columnWidth); 747 firstColumnConstraints.maxWidthProperty().bind(rowHeaderPane.columnWidth); 748 final ColumnConstraints secondColumnConstraints = new ColumnConstraints(); 749 secondColumnConstraints.minWidthProperty().bind(cellSize); 750 secondColumnConstraints.maxWidthProperty().bind(new IntegerBinding() { 751 752 { 753 super.bind(colHeaderPane.maxColumns, cellSize); 754 } 755 756 protected final int computeValue() { 757 return context.colHeads().size() * cellSize.get(); 758 } 759 }); 760 final ColumnConstraints thirdColumnConstraints = new ColumnConstraints(); 761 thirdColumnConstraints.minWidthProperty().bind(cellSize); 762 thirdColumnConstraints.maxWidthProperty().bind(cellSize); 763 centerPane.getColumnConstraints().addAll(firstColumnConstraints, secondColumnConstraints, thirdColumnConstraints); 764 centerPane.add(contextPane.getContentAndInteractionStackPane(), 1, 1); 765 centerPane.add(rowHeaderPane.getContentAndInteractionStackPane(), 0, 1); 766 centerPane.add(colHeaderPane.getContentAndInteractionStackPane(), 1, 0); 767 centerPane.add(rowScrollBar, 2, 1); 768 centerPane.add(colScrollBar, 1, 2); 769 this.setCenter(centerPane); 770 if (withToolbar) 771 createToolBar(); 772 else 773 zoomFactor.set(Math.pow(2d, 0)); 774 final ChangeListener<Boolean> updateContentListener = new ChangeListener<Boolean>() { 775 776 public final void 777 changed(final ObservableValue<? extends Boolean> observable, final Boolean oldValue, final Boolean newValue) { 778 contextPane.updateContent(); 779 } 780 }; 781 showArrows.addListener(updateContentListener); 782 showPaths.addListener(updateContentListener); 783 height = new DoubleBinding() { 784 785 { 786 bind( 787 colHeaderPane.heightProperty(), 788 contextPane.heightProperty(), 789 colHeaderPane.rowHeight, 790 colHeaderPane.visibleRows, 791 contextPane.rowHeight, 792 contextPane.visibleRows); 793 } 794 795 @Override 796 protected double computeValue() { 797// System.out.println(colHeaderPane.rowHeight.get()); 798// System.out.println(colHeaderPane.visibleRows.get()); 799// System.out.println(contextPane.rowHeight.get()); 800// System.out.println(contextPane.visibleRows.get()); 801 final int h = colHeaderPane.rowHeight.get() * colHeaderPane.visibleRows.get() 802 + contextPane.rowHeight.get() * contextPane.visibleRows.get(); 803// System.out.println(h); 804 return h; 805 } 806 }; 807 rowHeaderPane.toFront(); 808 colHeaderPane.toFront(); 809 final Timeline t = new Timeline(); 810 t.getKeyFrames().add(new KeyFrame( 811 Duration.millis(1000), 812 // new EventHandler<ActionEvent>() { 813 // 814 // public final void handle(final ActionEvent event) { 815 // final Timeline s = new Timeline(); 816 // s.getKeyFrames().add( 817 // new KeyFrame(Duration.millis(1000), new KeyValue(zoomSlider.valueProperty(), 0.9d, Interpolator.EASE_OUT))); 818 // Platform.runLater(new Runnable() { 819 // 820 // 821 // public final void run() { 822 // s.play(); 823 // } 824 // }); 825 // } 826 // }, 827 new KeyValue(zoomSlider.valueProperty(), 0d, Interpolator.EASE_IN))); 828 Platform.runLater(new Runnable() { 829 830 public void run() { 831 t.play(); 832 } 833 }); 834 if (dataset != null && dataset.editable) { 835 836 final Button domainButton = ButtonBuilder 837 .create() 838 // .text(conExpTab.fca.context.isHomogen() ? "New Element" : "New Object") 839 .onAction(new EventHandler<ActionEvent>() { 840 841 public void handle(ActionEvent event) { 842 ((FCADataset<String, String>) dataset).addObject( 843 (dataset.context.isHomogen() ? "Element " : "Object ") + IdGenerator.getNextId(dataset), 844 -1); 845 } 846 }) 847 .build(); 848 final ImageView view = 849 ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/add.png"))).build(); 850 view.scaleXProperty().bind(zoomFactor); 851 view.scaleYProperty().bind(zoomFactor); 852 domainButton.setGraphic(view); 853 domainButton.minWidthProperty().bind(rowHeaderPane.columnWidth); 854 domainButton.maxWidthProperty().bind(rowHeaderPane.columnWidth); 855 domainButton.minHeightProperty().bind(cellSize); 856 domainButton.maxHeightProperty().bind(cellSize); 857 domainButton.styleProperty().bind(new StringBinding() { 858 859 { 860 super.bind(textSize); 861 } 862 863 @Override 864 protected String computeValue() { 865 return "-fx-padding: 0; -fx-font-size: " + textSize.get() + ";"; 866 } 867 }); 868 centerPane.add(domainButton, 0, 2); 869 final Button codomainButton = ButtonBuilder 870 .create() 871 // .text(conExpTab.fca.context.isHomogen() ? "New Element" : "New Attribute") 872 .onAction(new EventHandler<ActionEvent>() { 873 874 public void handle(ActionEvent event) { 875 ((FCADataset<String, String>) dataset).addAttribute( 876 (dataset.context.isHomogen() ? "Element " : "Attribute ") + IdGenerator.getNextId(dataset), 877 -1); 878 } 879 }) 880 .build(); 881 final ImageView view2 = 882 ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/add.png"))).build(); 883 view2.scaleXProperty().bind(zoomFactor); 884 view2.scaleYProperty().bind(zoomFactor); 885 codomainButton.setGraphic(view2); 886 codomainButton.rotateProperty().set(-90); 887 codomainButton.minWidthProperty().bind(colHeaderPane.rowHeight); 888 codomainButton.maxWidthProperty().bind(colHeaderPane.rowHeight); 889 codomainButton.minHeightProperty().bind(cellSize); 890 codomainButton.maxHeightProperty().bind(cellSize); 891 codomainButton.translateXProperty().bind(new DoubleBinding() { 892 893 { 894 super.bind(colHeaderPane.rowHeight, cellSize); 895 } 896 897 @Override 898 protected double computeValue() { 899 return -(colHeaderPane.rowHeight.get() - cellSize.get()) / 2d; 900 } 901 }); 902 codomainButton.styleProperty().bind(new StringBinding() { 903 904 { 905 super.bind(textSize); 906 } 907 908 @Override 909 protected String computeValue() { 910 return "-fx-padding: 0; -fx-font-size: " + textSize.get() + ";"; 911 } 912 }); 913 centerPane.add(codomainButton, 2, 0); 914 } 915 } 916 917 private final void createToolBar() { 918 zoomFactor.bind(new DoubleBinding() { 919 920 { 921 bind(zoomSlider.valueProperty()); 922 } 923 924 protected double computeValue() { 925 return Math.pow(2d, zoomSlider.valueProperty().get()); 926 } 927 }); 928 final ToggleButton arrowsToggleButton = 929 new ToggleButton(Constants.DOWN_ARROW_CHARACTER + Constants.UP_ARROW_CHARACTER); 930 arrowsToggleButton.setSelected(false); 931 arrowsToggleButton.setMinHeight(24); 932 showArrows.bind(arrowsToggleButton.selectedProperty()); 933 final ToggleButton pathsToggleButton = 934 new ToggleButton(Constants.DOWN_ARROW_CHARACTER + Constants.DOWN_ARROW_CHARACTER); 935 pathsToggleButton.setDisable(true); 936 pathsToggleButton.setSelected(false); 937 pathsToggleButton.setMinHeight(24); 938 showPaths.bind(pathsToggleButton.selectedProperty()); 939 arrowsToggleButton.addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() { 940 941 @Override 942 public final void handle(final ActionEvent event) { 943 if (showArrows.get()) { 944 pathsToggleButton.setDisable(false); 945 } else { 946 pathsToggleButton.setSelected(false); 947 pathsToggleButton.setDisable(true); 948 } 949 } 950 }); 951 highlightToggleButton.setSelected(false); 952 highlightToggleButton.setMinHeight(24); 953 highlightToggleButton.setGraphic( 954 ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/flag.png"))).build()); 955 rowHeaderPane.highlight.bind(highlightToggleButton.selectedProperty()); 956 colHeaderPane.highlight.bind(highlightToggleButton.selectedProperty()); 957 contextPane.highlight.bind(highlightToggleButton.selectedProperty()); 958 arrowsToggleButton.setStyle("-fx-background-radius: 5 0 0 5, 5 0 0 5, 4 0 0 4, 3 0 0 3;"); 959 pathsToggleButton.setStyle("-fx-background-radius: 0, 0, 0, 0"); 960 highlightToggleButton.setStyle("-fx-background-radius: 0 5 5 0, 0 5 5 0, 0 4 4 0, 0 3 3 0;"); 961 HBox showBox = new HBox(); 962 showBox.setPadding(new Insets(0d)); 963 showBox.getChildren().addAll(arrowsToggleButton, pathsToggleButton, highlightToggleButton); 964 final ToolBar toolBar = new ToolBar(); 965 toolBar.getItems().addAll(zoomSlider, showBox); 966 this.setTop(toolBar); 967 toolBar.toFront(); 968 } 969 970 protected final void update() { 971 rowHeaderPane.updateContent(); 972 colHeaderPane.updateContent(); 973 contextPane.updateContent(); 974 } 975 976 public final void dehighlight() { 977 rowHeaderPane.dehighlight(); 978 colHeaderPane.dehighlight(); 979 contextPane.dehighlight(); 980 contextPane.highlightConcept.set(false); 981 } 982 983 public final void highlight(final Concept<G, M> concept) { 984 final Collection<Integer> domainIndices = context.rowHeads().indicesOf(concept.extent(), true); 985 final Collection<Integer> codomainIndices = context.colHeads().indicesOf(concept.intent(), true); 986 contextPane.highlightConcept.set(true); 987 rowHeaderPane.highlightConcept(Collections2.transform(domainIndices, new Function<Integer, Integer>() { 988 989 public final Integer apply(final Integer index) { 990 for (Entry<Integer, Integer> entry : contextPane.rowMap.get().entrySet()) 991 if (entry.getValue().equals(index)) 992 return entry.getKey(); 993 return index; 994 } 995 })); 996 colHeaderPane.highlightConcept(Collections2.transform(codomainIndices, new Function<Integer, Integer>() { 997 998 public final Integer apply(final Integer index) { 999 for (Entry<Integer, Integer> entry : contextPane.columnMap.get().entrySet()) 1000 if (entry.getValue().equals(index)) 1001 return entry.getKey(); 1002 return index; 1003 } 1004 })); 1005 } 1006 1007 public final void highlightImplication(final Implication<M> implication) { 1008 1009 } 1010 1011 public final void addRowDecoration(final G row, final Node decoration) { 1012 rowHeaderPane.addDecoration(row, decoration); 1013 } 1014}