001package conexp.fx.gui.context; 002 003/* 004 * #%L 005 * Concept Explorer FX 006 * %% 007 * Copyright (C) 2010 - 2023 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 031//import 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; 044//import 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) : 518 m.toString(); 519 textContent.set(string); 520 view.setImage(LaTeX.toFXImage(string, (float) (16d * zoomFactor.get()))); 521 } catch (IndexOutOfBoundsException __) {} 522 } 523 } 524 525 public final class ContextCell extends Cell<ContextCell, ContextPane> { 526 527 private ContextCell(final int row, final int column) { 528 super(contextPane, row, column, Pos.CENTER, TextAlignment.CENTER, false, null, false); 529 if (dataset != null) 530 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() { 531 532 public final void handle(final MouseEvent event) { 533 final G g = context.rowHeads().get(contentCoordinates.get().x()); 534 final M m = context.colHeads().get(contentCoordinates.get().y()); 535 dataset.conceptGraph.highlight(false, dataset.conceptGraph.highlightRequests.dehighlight()); 536 dataset.flip(g, m); 537 dataset.unsavedChanges.set(true); 538 } 539 }); 540 else 541 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_CLICKED, event -> { 542 final G g = context.rowHeads().get(contentCoordinates.get().x()); 543 final M m = context.colHeads().get(contentCoordinates.get().y()); 544 if (context.contains(g, m)) 545 context.remove(g, m); 546 else 547 context.add(g, m); 548 }); 549 if (dataset != null) 550 this.interactionPane.get().addEventHandler(MouseEvent.MOUSE_ENTERED, new EventHandler<MouseEvent>() { 551 552 public final void handle(final MouseEvent event) { 553 if (contextPane.highlight.get()) { 554 final G g = context.rowHeads().get(contentCoordinates.get().x()); 555 final M m = context.colHeads().get(contentCoordinates.get().y()); 556 if (context.selectedAttributes().contains(m)) 557 if (textContent.get().equals(Constants.CROSS_CHARACTER)) 558 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.incidence(g, m)); 559 else if (textContent.get().equals(Constants.DOWN_ARROW_CHARACTER)) 560 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.downArrow(g, m)); 561 else if (textContent.get().equals(Constants.UP_ARROW_CHARACTER)) 562 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.upArrow(g, m)); 563 else if (textContent.get().equals(Constants.BOTH_ARROW_CHARACTER)) 564 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.bothArrow(g, m)); 565 else if (textContent.get().equals(Constants.NO_CROSS_CHARACTER)) 566 dataset.conceptGraph.highlight(true, dataset.conceptGraph.highlightRequests.nonIncidence(g, m)); 567 } 568 } 569 }); 570 context.addEventHandler(event -> updateContent(), RelationEvent.ANY); 571// RelationEvent.ENTRIES_ADDED, 572// RelationEvent.ENTRIES_REMOVED, 573// RelationEvent.ALL_CHANGED, 574// RelationEvent.SELECTION_CHANGED); 575 updateContent(); 576 } 577 578 @SuppressWarnings("incomplete-switch") 579 public final void updateContent() { 580 try { 581 final G g = context.rowHeads().get(contentCoordinates.get().x()); 582 final M m = context.colHeads().get(contentCoordinates.get().y()); 583 if (context.selectedAttributes().contains(m) && context.selectedObjects().contains(g)) { 584 final Pair<Incidence, Incidence> p = context.selection.getValue(g, m, showArrows.get(), showPaths.get()); 585 final Incidence first = p.first(); 586 final Incidence second = p.second(); 587 ContextCell.this.textContent.set( 588 second != null && first == Incidence.NO_CROSS ? Constants.NO_CROSS_CHARACTER_BOLD : first.toString()); 589 switch (first) { 590 case BOTH_ARROW: 591 ContextCell.this.contentPane.get().text.setRotate(-45d); 592 break; 593 case DOWN_ARROW: 594 case UP_ARROW: 595 case CROSS: 596 case NO_CROSS: 597 ContextCell.this.contentPane.get().text.setRotate(0d); 598 break; 599 } 600 if (second == null) 601 ContextCell.this.contentPane.get().text.setFill(Color.BLACK); 602 else 603 switch (second) { 604 case BOTH_PATH: 605 ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_INTERVAL); 606 break; 607 case DOWN_PATH: 608 ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_LOWER); 609 break; 610 case UP_PATH: 611 ContextCell.this.contentPane.get().text.setFill(ConceptGraph.COLOR_UPPER); 612 break; 613 } 614 } else { 615 ContextCell.this.textContent.set(context.getValue(g, m, false).first().toString()); 616 ContextCell.this.contentPane.get().text.setRotate(0d); 617 } 618 } catch (IndexOutOfBoundsException __) {} 619 } 620 } 621 622 protected final FCADataset<G, M> dataset; 623 protected final MatrixContext<G, M> context; 624 protected final GridPane centerPane = new GridPane(); 625 public final RowHeaderPane rowHeaderPane; 626 public final ColHeaderPane colHeaderPane; 627 public final ContextPane contextPane; 628 protected final ScrollBar rowScrollBar; 629 protected final ScrollBar colScrollBar; 630// public final ListSpinner<Integer> zoomSpinner =new ListSpinner<Integer>(-4, 4, 1); 631 public final Slider zoomSlider = SliderBuilder 632 .create() 633 .min(-4d) 634 .max(4d) 635 .value(0d) 636 // .showTickMarks(true) 637 // .minorTickCount(17) 638 // .majorTickUnit(1d) 639 // .snapToTicks(true) 640 .blockIncrement(0.25d) 641 .build(); 642 public final DoubleProperty zoomFactor = new SimpleDoubleProperty(0.01d); 643 public final IntegerProperty rowHeaderSizeDefault = new SimpleIntegerProperty(40); 644 public final IntegerProperty colHeaderSizeDefault = new SimpleIntegerProperty(40); 645 public final IntegerProperty cellSizeDefault = new SimpleIntegerProperty(20); 646 public final IntegerProperty textSizeDefault = new SimpleIntegerProperty(16); 647 public final IntegerProperty incidenceSizeDefault = new SimpleIntegerProperty(20); 648 public final IntegerBinding cellSize = new IntegerBinding() { 649 650 { 651 super.bind(zoomFactor, cellSizeDefault); 652 } 653 654 protected int computeValue() { 655 return (int) (zoomFactor.get() 656 * cellSizeDefault.doubleValue()); 657 }; 658 }; 659 public final IntegerBinding textSize = new IntegerBinding() { 660 661 { 662 super.bind(zoomFactor, textSizeDefault); 663 } 664 665 protected int computeValue() { 666 return (int) (zoomFactor.get() 667 * textSizeDefault.doubleValue()); 668 }; 669 }; 670 public final IntegerBinding incidenceSize = new IntegerBinding() { 671 672 { 673 super.bind(zoomFactor, textSizeDefault); 674 } 675 676 protected int computeValue() { 677 return (int) (zoomFactor.get() 678 * incidenceSizeDefault.doubleValue()); 679 }; 680 }; 681 public final BooleanProperty animate = new SimpleBooleanProperty(false); 682 public final BooleanProperty showArrows = new SimpleBooleanProperty(); 683 public final BooleanProperty showPaths = new SimpleBooleanProperty(); 684 public final ToggleButton highlightToggleButton = new ToggleButton(); 685 public final DoubleBinding height; 686 687 public MatrixContextWidget(final FCADataset<G, M> fcaInstance) { 688 this(fcaInstance, true); 689 } 690 691 /** 692 * @param dataset 693 * @param withToolbar 694 * @param orContext 695 * may only be set if fcaInstance is null, otherwise unexpected behaviour may occur. 696 */ 697 @SafeVarargs 698 public MatrixContextWidget( 699 final FCADataset<G, M> dataset, 700 final boolean withToolbar, 701 final MatrixContext<G, M>... orContext) { 702 this(dataset, withToolbar, true, orContext); 703 } 704 705 @SafeVarargs 706 public MatrixContextWidget( 707 final FCADataset<G, M> dataset, 708 final boolean withToolbar, 709 final boolean interactive, 710 final MatrixContext<G, M>... orContext) { 711 super(); 712 this.dataset = dataset; 713 if (dataset == null && orContext[0] != null) 714 this.context = orContext[0]; 715 else 716 this.context = dataset.context; 717 this.rowHeaderPane = new RowHeaderPane(interactive); 718 this.colHeaderPane = new ColHeaderPane(interactive); 719 this.contextPane = new ContextPane(interactive); 720 this.rowScrollBar = contextPane.getRowScrollBar(); 721 this.colScrollBar = contextPane.getColumnScrollBar(); 722 centerPane.setHgap(4); 723 centerPane.setVgap(4); 724 rowHeaderPane.colorScheme.setValue(ColorScheme.JAVA_FX); 725 colHeaderPane.colorScheme.setValue(ColorScheme.JAVA_FX); 726 contextPane.colorScheme.setValue(ColorScheme.JAVA_FX); 727 final RowConstraints firstRowConstraints = new RowConstraints(); 728 firstRowConstraints.minHeightProperty().bind(colHeaderPane.rowHeight); 729 firstRowConstraints.maxHeightProperty().bind(colHeaderPane.rowHeight); 730 final RowConstraints secondRowConstraints = new RowConstraints(); 731 secondRowConstraints.minHeightProperty().bind(cellSize); 732 secondRowConstraints.prefHeightProperty().bind(new IntegerBinding() { 733 734 { 735 super.bind(rowHeaderPane.maxRows, cellSize); 736 } 737 738 protected final int computeValue() { 739 return context.rowHeads().size() * cellSize.get(); 740 } 741 }); 742 final RowConstraints thirdRowConstraints = new RowConstraints(); 743 thirdRowConstraints.minHeightProperty().bind(cellSize); 744 thirdRowConstraints.maxHeightProperty().bind(cellSize); 745 centerPane.getRowConstraints().addAll(firstRowConstraints, secondRowConstraints, thirdRowConstraints); 746 final ColumnConstraints firstColumnConstraints = new ColumnConstraints(); 747 firstColumnConstraints.minWidthProperty().bind(rowHeaderPane.columnWidth); 748 firstColumnConstraints.maxWidthProperty().bind(rowHeaderPane.columnWidth); 749 final ColumnConstraints secondColumnConstraints = new ColumnConstraints(); 750 secondColumnConstraints.minWidthProperty().bind(cellSize); 751 secondColumnConstraints.maxWidthProperty().bind(new IntegerBinding() { 752 753 { 754 super.bind(colHeaderPane.maxColumns, cellSize); 755 } 756 757 protected final int computeValue() { 758 return context.colHeads().size() * cellSize.get(); 759 } 760 }); 761 final ColumnConstraints thirdColumnConstraints = new ColumnConstraints(); 762 thirdColumnConstraints.minWidthProperty().bind(cellSize); 763 thirdColumnConstraints.maxWidthProperty().bind(cellSize); 764 centerPane.getColumnConstraints().addAll(firstColumnConstraints, secondColumnConstraints, thirdColumnConstraints); 765 centerPane.add(contextPane.getContentAndInteractionStackPane(), 1, 1); 766 centerPane.add(rowHeaderPane.getContentAndInteractionStackPane(), 0, 1); 767 centerPane.add(colHeaderPane.getContentAndInteractionStackPane(), 1, 0); 768 centerPane.add(rowScrollBar, 2, 1); 769 centerPane.add(colScrollBar, 1, 2); 770 this.setCenter(centerPane); 771 if (withToolbar) 772 createToolBar(); 773 else 774 zoomFactor.set(Math.pow(2d, 0)); 775 final ChangeListener<Boolean> updateContentListener = new ChangeListener<Boolean>() { 776 777 public final void 778 changed(final ObservableValue<? extends Boolean> observable, final Boolean oldValue, final Boolean newValue) { 779 contextPane.updateContent(); 780 } 781 }; 782 showArrows.addListener(updateContentListener); 783 showPaths.addListener(updateContentListener); 784 height = new DoubleBinding() { 785 786 { 787 bind( 788 colHeaderPane.heightProperty(), 789 contextPane.heightProperty(), 790 colHeaderPane.rowHeight, 791 colHeaderPane.visibleRows, 792 contextPane.rowHeight, 793 contextPane.visibleRows); 794 } 795 796 @Override 797 protected double computeValue() { 798// System.out.println(colHeaderPane.rowHeight.get()); 799// System.out.println(colHeaderPane.visibleRows.get()); 800// System.out.println(contextPane.rowHeight.get()); 801// System.out.println(contextPane.visibleRows.get()); 802 final int h = colHeaderPane.rowHeight.get() * colHeaderPane.visibleRows.get() 803 + contextPane.rowHeight.get() * contextPane.visibleRows.get(); 804// System.out.println(h); 805 return h; 806 } 807 }; 808 rowHeaderPane.toFront(); 809 colHeaderPane.toFront(); 810 final Timeline t = new Timeline(); 811 t.getKeyFrames().add(new KeyFrame( 812 Duration.millis(1000), 813 // new EventHandler<ActionEvent>() { 814 // 815 // public final void handle(final ActionEvent event) { 816 // final Timeline s = new Timeline(); 817 // s.getKeyFrames().add( 818 // new KeyFrame(Duration.millis(1000), new KeyValue(zoomSlider.valueProperty(), 0.9d, Interpolator.EASE_OUT))); 819 // Platform.runLater(new Runnable() { 820 // 821 // 822 // public final void run() { 823 // s.play(); 824 // } 825 // }); 826 // } 827 // }, 828 new KeyValue(zoomSlider.valueProperty(), 0d, Interpolator.EASE_IN))); 829 Platform.runLater(new Runnable() { 830 831 public void run() { 832 t.play(); 833 } 834 }); 835 if (dataset != null && dataset.editable) { 836 837 final Button domainButton = ButtonBuilder 838 .create() 839 // .text(conExpTab.fca.context.isHomogen() ? "New Element" : "New Object") 840 .onAction(new EventHandler<ActionEvent>() { 841 842 public void handle(ActionEvent event) { 843 ((FCADataset<String, String>) dataset).addObject( 844 (dataset.context.isHomogen() ? "Element " : "Object ") + IdGenerator.getNextId(dataset), 845 -1); 846 } 847 }) 848 .build(); 849 final ImageView view = 850 ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/add.png"))).build(); 851 view.scaleXProperty().bind(zoomFactor); 852 view.scaleYProperty().bind(zoomFactor); 853 domainButton.setGraphic(view); 854 domainButton.minWidthProperty().bind(rowHeaderPane.columnWidth); 855 domainButton.maxWidthProperty().bind(rowHeaderPane.columnWidth); 856 domainButton.minHeightProperty().bind(cellSize); 857 domainButton.maxHeightProperty().bind(cellSize); 858 domainButton.styleProperty().bind(new StringBinding() { 859 860 { 861 super.bind(textSize); 862 } 863 864 @Override 865 protected String computeValue() { 866 return "-fx-padding: 0; -fx-font-size: " + textSize.get() + ";"; 867 } 868 }); 869 centerPane.add(domainButton, 0, 2); 870 final Button codomainButton = ButtonBuilder 871 .create() 872 // .text(conExpTab.fca.context.isHomogen() ? "New Element" : "New Attribute") 873 .onAction(new EventHandler<ActionEvent>() { 874 875 public void handle(ActionEvent event) { 876 ((FCADataset<String, String>) dataset).addAttribute( 877 (dataset.context.isHomogen() ? "Element " : "Attribute ") + IdGenerator.getNextId(dataset), 878 -1); 879 } 880 }) 881 .build(); 882 final ImageView view2 = 883 ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/add.png"))).build(); 884 view2.scaleXProperty().bind(zoomFactor); 885 view2.scaleYProperty().bind(zoomFactor); 886 codomainButton.setGraphic(view2); 887 codomainButton.rotateProperty().set(-90); 888 codomainButton.minWidthProperty().bind(colHeaderPane.rowHeight); 889 codomainButton.maxWidthProperty().bind(colHeaderPane.rowHeight); 890 codomainButton.minHeightProperty().bind(cellSize); 891 codomainButton.maxHeightProperty().bind(cellSize); 892 codomainButton.translateXProperty().bind(new DoubleBinding() { 893 894 { 895 super.bind(colHeaderPane.rowHeight, cellSize); 896 } 897 898 @Override 899 protected double computeValue() { 900 return -(colHeaderPane.rowHeight.get() - cellSize.get()) / 2d; 901 } 902 }); 903 codomainButton.styleProperty().bind(new StringBinding() { 904 905 { 906 super.bind(textSize); 907 } 908 909 @Override 910 protected String computeValue() { 911 return "-fx-padding: 0; -fx-font-size: " + textSize.get() + ";"; 912 } 913 }); 914 centerPane.add(codomainButton, 2, 0); 915 } 916 } 917 918 private final void createToolBar() { 919 zoomFactor.bind(new DoubleBinding() { 920 921 { 922 bind(zoomSlider.valueProperty()); 923 } 924 925 protected double computeValue() { 926 return Math.pow(2d, zoomSlider.valueProperty().get()); 927 } 928 }); 929 final ToggleButton arrowsToggleButton = 930 new ToggleButton(Constants.DOWN_ARROW_CHARACTER + Constants.UP_ARROW_CHARACTER); 931 arrowsToggleButton.setSelected(false); 932 arrowsToggleButton.setMinHeight(24); 933 showArrows.bind(arrowsToggleButton.selectedProperty()); 934 final ToggleButton pathsToggleButton = 935 new ToggleButton(Constants.DOWN_ARROW_CHARACTER + Constants.DOWN_ARROW_CHARACTER); 936 pathsToggleButton.setDisable(true); 937 pathsToggleButton.setSelected(false); 938 pathsToggleButton.setMinHeight(24); 939 showPaths.bind(pathsToggleButton.selectedProperty()); 940 arrowsToggleButton.addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() { 941 942 @Override 943 public final void handle(final ActionEvent event) { 944 if (showArrows.get()) { 945 pathsToggleButton.setDisable(false); 946 } else { 947 pathsToggleButton.setSelected(false); 948 pathsToggleButton.setDisable(true); 949 } 950 } 951 }); 952 highlightToggleButton.setSelected(false); 953 highlightToggleButton.setMinHeight(24); 954 highlightToggleButton.setGraphic( 955 ImageViewBuilder.create().image(new Image(ConExpFX.class.getResourceAsStream("image/16x16/flag.png"))).build()); 956 rowHeaderPane.highlight.bind(highlightToggleButton.selectedProperty()); 957 colHeaderPane.highlight.bind(highlightToggleButton.selectedProperty()); 958 contextPane.highlight.bind(highlightToggleButton.selectedProperty()); 959 arrowsToggleButton.setStyle("-fx-background-radius: 5 0 0 5, 5 0 0 5, 4 0 0 4, 3 0 0 3;"); 960 pathsToggleButton.setStyle("-fx-background-radius: 0, 0, 0, 0"); 961 highlightToggleButton.setStyle("-fx-background-radius: 0 5 5 0, 0 5 5 0, 0 4 4 0, 0 3 3 0;"); 962 HBox showBox = new HBox(); 963 showBox.setPadding(new Insets(0d)); 964 showBox.getChildren().addAll(arrowsToggleButton, pathsToggleButton, highlightToggleButton); 965 final ToolBar toolBar = new ToolBar(); 966 toolBar.getItems().addAll(zoomSlider, showBox); 967 this.setTop(toolBar); 968 toolBar.toFront(); 969 } 970 971 protected final void update() { 972 rowHeaderPane.updateContent(); 973 colHeaderPane.updateContent(); 974 contextPane.updateContent(); 975 } 976 977 public final void dehighlight() { 978 rowHeaderPane.dehighlight(); 979 colHeaderPane.dehighlight(); 980 contextPane.dehighlight(); 981 contextPane.highlightConcept.set(false); 982 } 983 984 public final void highlight(final Concept<G, M> concept) { 985 final Collection<Integer> domainIndices = context.rowHeads().indicesOf(concept.extent(), true); 986 final Collection<Integer> codomainIndices = context.colHeads().indicesOf(concept.intent(), true); 987 contextPane.highlightConcept.set(true); 988 rowHeaderPane.highlightConcept(Collections2.transform(domainIndices, new Function<Integer, Integer>() { 989 990 public final Integer apply(final Integer index) { 991 for (Entry<Integer, Integer> entry : contextPane.rowMap.get().entrySet()) 992 if (entry.getValue().equals(index)) 993 return entry.getKey(); 994 return index; 995 } 996 })); 997 colHeaderPane.highlightConcept(Collections2.transform(codomainIndices, new Function<Integer, Integer>() { 998 999 public final Integer apply(final Integer index) { 1000 for (Entry<Integer, Integer> entry : contextPane.columnMap.get().entrySet()) 1001 if (entry.getValue().equals(index)) 1002 return entry.getKey(); 1003 return index; 1004 } 1005 })); 1006 } 1007 1008 public final void highlightImplication(final Implication<M> implication) { 1009 1010 } 1011 1012 public final void addRowDecoration(final G row, final Node decoration) { 1013 rowHeaderPane.addDecoration(row, decoration); 1014 } 1015}