001/** 002 * @author Francesco Kriegel (francesco.kriegel@gmx.de) 003 */ 004package conexp.fx.gui; 005 006/* 007 * #%L 008 * Concept Explorer FX 009 * %% 010 * Copyright (C) 2010 - 2023 Francesco Kriegel 011 * %% 012 * This program is free software: you can redistribute it and/or modify 013 * it under the terms of the GNU General Public License as 014 * published by the Free Software Foundation, either version 3 of the 015 * License, or (at your option) any later version. 016 * 017 * This program is distributed in the hope that it will be useful, 018 * but WITHOUT ANY WARRANTY; without even the implied warranty of 019 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 020 * GNU General Public License for more details. 021 * 022 * You should have received a copy of the GNU General Public 023 * License along with this program. If not, see 024 * <http://www.gnu.org/licenses/gpl-3.0.html>. 025 * #L% 026 */ 027 028import java.awt.Desktop; 029import java.io.File; 030import java.io.IOException; 031import java.net.URI; 032import java.util.Iterator; 033import java.util.concurrent.ExecutorService; 034 035import com.google.common.collect.Collections2; 036import com.google.common.collect.Iterators; 037import com.google.common.collect.Lists; 038 039import conexp.fx.core.builder.Requests; 040import conexp.fx.core.collections.Collections3; 041import conexp.fx.core.collections.Pair; 042import conexp.fx.core.context.MatrixContext; 043import conexp.fx.core.util.FileFormat; 044import conexp.fx.core.xml.StringData; 045import conexp.fx.core.xml.StringListData; 046import conexp.fx.core.xml.XMLFile; 047import conexp.fx.gui.assistent.ConstructAssistent; 048import conexp.fx.gui.assistent.ExportAssistent; 049import conexp.fx.gui.dataset.Dataset; 050import conexp.fx.gui.dataset.DatasetView; 051import conexp.fx.gui.dataset.FCADataset; 052import conexp.fx.gui.dataset.RDFDataset; 053import conexp.fx.gui.dialog.ErrorDialog; 054import conexp.fx.gui.dialog.FXDialog; 055import conexp.fx.gui.dialog.InfoDialog; 056import conexp.fx.gui.task.BlockingExecutor; 057import conexp.fx.gui.task.ExecutorStatusBar; 058import conexp.fx.gui.task.TimeTask; 059import conexp.fx.gui.util.AppUserModelIdUtility; 060import conexp.fx.gui.util.FXControls; 061import conexp.fx.gui.util.Platform2; 062import javafx.application.Application; 063import javafx.application.Platform; 064import javafx.beans.binding.Bindings; 065import javafx.beans.property.ListProperty; 066import javafx.beans.property.ObjectProperty; 067import javafx.beans.property.SimpleListProperty; 068import javafx.beans.property.SimpleObjectProperty; 069import javafx.beans.value.ChangeListener; 070import javafx.beans.value.ObservableValue; 071import javafx.collections.FXCollections; 072import javafx.collections.ListChangeListener; 073import javafx.collections.ObservableList; 074import javafx.geometry.Orientation; 075import javafx.geometry.Rectangle2D; 076import javafx.scene.Scene; 077import javafx.scene.control.Button; 078import javafx.scene.control.Control; 079import javafx.scene.control.Menu; 080import javafx.scene.control.MenuBar; 081import javafx.scene.control.MenuItem; 082import javafx.scene.control.MenuItemBuilder; 083import javafx.scene.control.SelectionMode; 084import javafx.scene.control.SeparatorMenuItem; 085import javafx.scene.control.SplitPane; 086import javafx.scene.control.ToolBar; 087import javafx.scene.control.TreeItem; 088import javafx.scene.control.TreeView; 089import javafx.scene.image.Image; 090import javafx.scene.input.KeyCode; 091import javafx.scene.input.KeyEvent; 092import javafx.scene.layout.AnchorPane; 093import javafx.scene.layout.BorderPane; 094import javafx.scene.layout.HBox; 095import javafx.scene.layout.Priority; 096import javafx.scene.layout.StackPane; 097import javafx.stage.FileChooser; 098import javafx.stage.Screen; 099import javafx.stage.Stage; 100import javafx.stage.StageStyle; 101 102public class ConExpFX extends Application { 103 104 public static ConExpFX instance; 105 106 public final static void main(String[] args) { 107 System.setProperty("file.encoding", "UTF-8"); 108 if (System.getProperty("os.name").toLowerCase().startsWith("windows")) 109 AppUserModelIdUtility.setCurrentProcessExplicitAppUserModelID("conexp-fx"); 110// AquaFx.style(); 111 launch(args); 112 } 113 114 public static final void execute(final TimeTask<?> task) { 115 instance.executor.execute(task); 116 } 117 118 public static final ExecutorService getThreadPool() { 119 return instance.executor.tpe; 120 } 121 122 private final class CFXMenuBar { 123 124 private final MenuBar menuBar = new MenuBar(); 125 private final Menu contextMenu = new Menu("_Context"); 126 private final Menu viewMenu = new Menu("_View"); 127 private final Menu helpMenu = new Menu("?"); 128 129 private CFXMenuBar() { 130 super(); 131 HBox.setHgrow(menuBar, Priority.ALWAYS); 132 buildContextMenu(); 133 buildViewMenu(); 134 buildHelpMenu(); 135 menuBar.getMenus().addAll(contextMenu, viewMenu, helpMenu); 136// menuBar.setUseSystemMenuBar(true); 137 rootPane.setTop(menuBar); 138 } 139 140 private final void buildViewMenu() { 141 viewMenu.getItems().add( 142 FXControls.newMenuItem( 143 "Fullscreen", 144 "image/16x16/new_page.png", 145 e -> primaryStage.setFullScreen(!primaryStage.isFullScreen()))); 146 } 147 148 private final void buildContextMenu() { 149 final MenuItem newMenuItem = 150 FXControls.newMenuItem("New", "image/16x16/new_page.png", e -> new ConstructAssistent().showAndWait()); 151 final MenuItem openMenuItem = FXControls.newMenuItem("Open", "image/16x16/folder.png", e -> showOpenFileDialog()); 152 final MenuItem saveMenuItem = 153 FXControls.newMenuItem("Save", "image/16x16/save.png", true, e -> treeView.getActiveDataset().get().save()); 154 final MenuItem saveAsMenuItem = FXControls 155 .newMenuItem("Save As", "image/16x16/save.png", true, e -> treeView.getActiveDataset().get().saveAs()); 156 final MenuItem exportMenuItem = FXControls.newMenuItem("Export", "image/16x16/briefcase.png", true, e -> { 157 if (treeView.getActiveDataset().get() instanceof FCADataset) 158 new ExportAssistent(primaryStage, (FCADataset<?, ?>) treeView.getActiveDataset().get()).showAndWait(); 159 }); 160 final MenuItem closeMenuItem = 161 FXControls.newMenuItem("Close", "image/16x16/delete.png", true, e -> treeView.closeActiveDataset()); 162 final Menu historyMenu = new Menu("History", FXControls.newImageView("image/16x16/clock.png")); 163 final MenuItem exitMenuItem = FXControls.newMenuItem("Exit", "image/16x16/delete.png", e -> stop()); 164 treeView.getActiveDataset().addListener(new ChangeListener<Dataset>() { 165 166 public final void changed( 167 final ObservableValue<? extends Dataset> observable, 168 final Dataset oldSelectedTab, 169 final Dataset newSelectedTab) { 170 Platform.runLater(() -> { 171 saveMenuItem.disableProperty().unbind(); 172 if (newSelectedTab == null) { 173 saveMenuItem.setDisable(true); 174 saveAsMenuItem.setDisable(true); 175 exportMenuItem.setDisable(true); 176 closeMenuItem.setDisable(true); 177 } else { 178 saveMenuItem.disableProperty().bind( 179 Bindings 180 .createBooleanBinding(() -> !newSelectedTab.unsavedChanges.get(), newSelectedTab.unsavedChanges)); 181 saveAsMenuItem.setDisable(false); 182 exportMenuItem.setDisable(false); 183 closeMenuItem.setDisable(false); 184 } 185 }); 186 } 187 }); 188 historyMenu.disableProperty().bind(fileHistory.emptyProperty()); 189 fileHistory.addListener(new ListChangeListener<File>() { 190 191 @SuppressWarnings("deprecation") 192 public final void onChanged(final ListChangeListener.Change<? extends File> c) { 193 historyMenu.getItems().clear(); 194 historyMenu.getItems().addAll( 195 Collections2.transform( 196 fileHistory, 197 file -> MenuItemBuilder.create().text(file.toString()).onAction(e -> Platform.runLater(() -> { 198 if (file.exists() && file.isFile()) 199 openFile(FileFormat.of(file)); 200 })).build())); 201 } 202 }); 203 contextMenu.getItems().addAll( 204 newMenuItem, 205 openMenuItem, 206 saveMenuItem, 207 saveAsMenuItem, 208 exportMenuItem, 209 closeMenuItem, 210 new SeparatorMenuItem(), 211 historyMenu, 212 new SeparatorMenuItem(), 213 exitMenuItem); 214 215 } 216 217 private final void buildHelpMenu() { 218 if (Desktop.isDesktopSupported()) { 219 final MenuItem helpMenuItem = FXControls.newMenuItem("Help", "image/16x16/help.png", ev -> { 220 try { 221 Desktop.getDesktop().browse(new URI("http://lat.inf.tu-dresden.de/~francesco/conexp-fx/conexp-fx.html")); 222 } catch (Exception e) { 223 new ErrorDialog(primaryStage, e).showAndWait(); 224 } 225 }); 226 helpMenu.getItems().add(helpMenuItem); 227 } 228 final MenuItem infoMenuItem = 229 FXControls.newMenuItem("Info", "image/16x16/info.png", e -> new InfoDialog(ConExpFX.this).showAndWait()); 230 helpMenu.getItems().addAll(infoMenuItem); 231 } 232 } 233 234 public final class DatasetTreeView extends TreeView<Control> { 235 236 private final ObservableList<Dataset> datasets = FXCollections.observableArrayList(); 237 public final ObjectProperty<Dataset> activeDataset = new SimpleObjectProperty<Dataset>(null); 238 private final ToolBar toolBar = new ToolBar(); 239 240 private DatasetTreeView() { 241 super(); 242 final Button newButton = new Button("New", FXControls.newImageView("image/16x16/new_page.png")); 243 newButton.setOnAction(e -> new ConstructAssistent().showAndWait()); 244 final Button openButton = new Button("Open", FXControls.newImageView("image/16x16/folder.png")); 245 openButton.setOnAction(e -> showOpenFileDialog()); 246 toolBar.getItems().addAll(newButton, openButton); 247 this.setRoot(new TreeItem<>()); 248 this.setShowRoot(false); 249 this.selectionModelProperty().get().setSelectionMode(SelectionMode.MULTIPLE); 250 activeDataset.bind(Bindings.createObjectBinding(() -> { 251// final Object foo = instance == null ? 0 : instance; 252// synchronized (foo) { 253 Dataset active = null; 254// final Iterator<TreeItem<Control>> it = getSelectionModel().getSelectedItems().iterator(); 255 final Iterator<TreeItem<Control>> it = Iterators.filter(getSelectionModel().getSelectedItems().iterator(), x -> x != null); 256 if (it.hasNext()) { 257 TreeItem<?> selectedItem = it.next(); 258 if (selectedItem.isLeaf()) 259 selectedItem = selectedItem.getParent(); 260 if (selectedItem instanceof Dataset.DatasetTreeItem) { 261 active = ((Dataset.DatasetTreeItem) selectedItem).getDataset(); 262 while (it.hasNext()) { 263 selectedItem = it.next(); 264 if (selectedItem.isLeaf()) 265 selectedItem = selectedItem.getParent(); 266 if (selectedItem instanceof Dataset.DatasetTreeItem) { 267 if (!active.equals(((Dataset.DatasetTreeItem) selectedItem).getDataset())) { 268 active = null; 269 break; 270 } 271 } 272 } 273 } 274 } 275 return active; 276// } 277 }, this.getSelectionModel().getSelectedItems())); 278 this.getSelectionModel().getSelectedItems().addListener(new ListChangeListener<TreeItem<Control>>() { 279 280 @Override 281 public synchronized void onChanged(ListChangeListener.Change<? extends TreeItem<Control>> c) { 282// synchronized (instance) { 283 // The below code sometimes throws IndexOutOfBoundsExceptions for no apparent reason. 284// while (c.next()) { 285// if (c.wasAdded()) 286// c 287// .getAddedSubList() 288// .stream() 289// .filter(item -> item instanceof DatasetView<?>.DatasetViewTreeItem) 290// .map(item -> (DatasetView<?>.DatasetViewTreeItem) item) 291// .forEach(item -> contentPane.getItems().add(item.getDatasetView().getContentNode())); 292// if (c.wasRemoved()) 293// c 294// .getRemoved() 295// .stream() 296// .filter(item -> item instanceof DatasetView<?>.DatasetViewTreeItem) 297// .map(item -> (DatasetView<?>.DatasetViewTreeItem) item) 298// .forEach(item -> contentPane.getItems().remove(item.getDatasetView().getContentNode())); 299// } 300 // Let's do it as follows instead. 301 contentPane.getItems().clear(); 302 getSelectionModel().getSelectedItems() 303 .stream() 304 .filter(item -> item instanceof DatasetView<?>.DatasetViewTreeItem) 305 .map(item -> (DatasetView<?>.DatasetViewTreeItem) item) 306 .forEach(item -> contentPane.getItems().add(item.getDatasetView().getContentNode())); 307 Platform2.runOnFXThreadAndWaitTryCatch(() -> { 308// synchronized (instance) { 309 final double pos = contentPane.getItems().isEmpty() ? 0d : 1d / (double) contentPane.getItems().size(); 310 for (int i = 0; i < contentPane.getItems().size(); i++) 311 contentPane.setDividerPosition(i, pos * (double) (i + 1)); 312// } 313 }); 314// } 315 } 316 }); 317 datasets.addListener(new ListChangeListener<Dataset>() { 318 319 @Override 320 public void onChanged(ListChangeListener.Change<? extends Dataset> c) { 321// synchronized (instance) { 322 while (c.next()) { 323 if (c.wasAdded()) 324 c.getAddedSubList().forEach(dataset -> dataset.addToTree(DatasetTreeView.this)); 325 if (c.wasRemoved()) 326 c.getRemoved().forEach(dataset -> { 327 dataset.views.forEach(view -> contentPane.getItems().remove(view.getContentNode())); 328 final TreeItem<Control> parentItem = getParentItem(dataset); 329 parentItem 330 .getChildren() 331 .parallelStream() 332 .filter(treeItem -> treeItem instanceof Dataset.DatasetTreeItem) 333 .map(treeItem -> (Dataset.DatasetTreeItem) treeItem) 334 .filter(treeItem -> treeItem.getDataset().equals(dataset)) 335 .findAny() 336 .ifPresent(treeItem -> parentItem.getChildren().remove(treeItem)); 337 }); 338 } 339// } 340 } 341 }); 342 ConExpFX.this.splitPane.getItems().add(new BorderPane(this, toolBar, null, null, null)); 343 } 344 345 public final ObservableList<Dataset> getDatasets() { 346 return datasets; 347 } 348 349 public final ObjectProperty<Dataset> getActiveDataset() { 350 return activeDataset; 351 } 352 353 public final void addDataset(final Dataset dataset) { 354 Platform2.runOnFXThread(() -> { 355// synchronized (instance) { 356 datasets.add(dataset); 357// } 358 }); 359 } 360 361 public final void close(final Dataset dataset) { 362 askForUnsavedChanges(dataset); 363 getSelectionModel().clearSelection(); 364 datasets.remove(dataset); 365 execute( 366 TimeTask.create( 367 dataset, 368 "Closing " + dataset.id.get(), 369 () -> Platform2.runOnFXThread(() -> executor.cancel(dataset)))); 370 } 371 372 public final void closeActiveDataset() { 373 if (activeDataset.isNotNull().get()) 374 close(activeDataset.get()); 375 } 376 377 public final TreeItem<Control> getParentItem(final Dataset dataset) { 378 if (dataset.parent != null) 379 return dataset.parent.treeItem; 380 return getRoot(); 381 } 382 } 383 384 public Stage primaryStage; 385 private final StackPane stackPane = new StackPane(); 386 private final BorderPane rootPane = new BorderPane(); 387 private final AnchorPane overlayPane = new AnchorPane(); 388 private final SplitPane contentPane = new SplitPane(); 389 private final SplitPane splitPane = new SplitPane(); 390 public final DatasetTreeView treeView = new DatasetTreeView(); 391 public final ExecutorStatusBar executorStatusBar = new ExecutorStatusBar(overlayPane); 392 393 public final BlockingExecutor executor = new BlockingExecutor(); 394 public final XMLFile configuration = initConfiguration(); 395 public final ListProperty<File> fileHistory = 396 new SimpleListProperty<File>(FXCollections.observableArrayList()); 397 public File lastDirectory; 398 public final ObservableList<MatrixContext<?, ?>> contexts = FXCollections.observableList( 399 Lists.transform( 400 Collections3.filter(treeView.getDatasets(), dataset -> dataset instanceof FCADataset), 401 dataset -> ((FCADataset<?, ?>) dataset).context)); 402 public final ObservableList<MatrixContext<?, ?>> orders = 403 FXCollections.observableList(Collections3.filter(contexts, context -> context.isHomogen())); 404 405 public final void start(final Stage primaryStage) { 406 ConExpFX.instance = this; 407 Platform.setImplicitExit(true); 408 this.primaryStage = primaryStage; 409 this.primaryStage.initStyle(StageStyle.DECORATED); 410 this.primaryStage.setTitle("Concept Explorer FX"); 411 this.primaryStage.getIcons().add(new Image(ConExpFX.class.getResourceAsStream("image/conexp-fx.png"))); 412 this.primaryStage.setScene(new Scene(rootPane, 1280, 800)); 413 this.primaryStage.addEventHandler(KeyEvent.KEY_PRESSED, e -> { 414 if (e.getCode().equals(KeyCode.F11)) 415 ConExpFX.this.primaryStage.setFullScreen(!ConExpFX.this.primaryStage.isFullScreen()); 416 }); 417 this.primaryStage.setOnCloseRequest(e -> stop()); 418 final Rectangle2D bounds = Screen.getPrimary().getVisualBounds(); 419 this.primaryStage.setX(bounds.getMinX()); 420 this.primaryStage.setY(bounds.getMinY()); 421 this.primaryStage.setWidth(bounds.getWidth()); 422 this.primaryStage.setHeight(bounds.getHeight()); 423// this.primaryStage.setFullScreen(true); 424 this.stackPane.getChildren().addAll(splitPane, overlayPane); 425 this.overlayPane.setMouseTransparent(true); 426 this.rootPane.setCenter(stackPane); 427 this.rootPane.setBottom(executorStatusBar.statusBar); 428// this.rootPane.getStylesheets().add("conexp/fx/gui/style/style.css"); 429 this.splitPane.setOrientation(Orientation.HORIZONTAL); 430 this.splitPane.getItems().add(contentPane); 431 new CFXMenuBar(); 432 this.executorStatusBar.setOnMouseExitedHandler(this.primaryStage.getScene()); 433 this.executorStatusBar.bindTo(executor); 434 this.primaryStage.show(); 435 Platform.runLater(() -> { 436 ConExpFX.this.splitPane.setDividerPositions(new double[] { 437 0.1618d 438 }); 439 readConfiguration(); 440 }); 441 } 442 443 private final XMLFile initConfiguration() { 444 try { 445 File file = new File(System.getProperty("user.home"), ".conexp-fx.xml"); 446 if (!file.exists()) 447 try { 448 XMLFile.createEmptyConfiguration(file); 449 } catch (Exception e) { 450 e.printStackTrace(); 451 System.out.println("Cannot create file " + file.getAbsolutePath()); 452 System.out.println("Creating temporary file instead."); 453 file = new File(File.createTempFile("conexp-fx", "tmp").getParent(), "conexp-fx.xml"); 454 } 455 if (!file.exists()) 456 XMLFile.createEmptyConfiguration(file); 457 System.out.println("configuration file: " + file.getAbsolutePath()); 458 return new XMLFile(file); 459 } catch (IOException e) { 460 e.printStackTrace(); 461 return null; 462 } 463 } 464 465 private final void readConfiguration() { 466 if (configuration.containsKey("file_history")) 467 fileHistory.addAll(Lists.transform(configuration.get("file_history").getStringListValue(), File::new)); 468 if (configuration.containsKey("last_directory") 469 && new File(configuration.get("last_directory").getStringValue()).exists() 470 && new File(configuration.get("last_directory").getStringValue()).isDirectory()) 471 lastDirectory = new File(configuration.get("last_directory").getStringValue()); 472 if (configuration.containsKey("last_opened_files")) 473 for (String last_opened_file : configuration.get("last_opened_files").getStringListValue()) 474 if (new File(last_opened_file).exists() && new File(last_opened_file).isFile()) 475 openFile(FileFormat.of(new File(last_opened_file))); 476 } 477 478 private final void writeConfiguration() throws IOException { 479 if (lastDirectory != null) 480 configuration.put("last_directory", new StringData("last_directory", lastDirectory.toString())); 481 configuration.put("last_opened_files", new StringListData("last_opened_files", "last_opened_file")); 482 for (Dataset d : treeView.datasets) 483 if (d.file != null) 484 configuration.get("last_opened_files").getStringListValue().add(d.file.toString()); 485 configuration 486 .put("file_history", new StringListData("file_history", "file", Lists.transform(fileHistory, File::toString))); 487 configuration.store(); 488 } 489 490 private final void showOpenFileDialog() { 491 final Pair<File, FileFormat> ffile = showOpenFileDialog( 492 "Open Dataset", 493 FileFormat.CXT, 494 FileFormat.CFX, 495 FileFormat.CSVB, 496 FileFormat.NT, 497 FileFormat.CSVT); 498 if (ffile != null) 499 openFile(ffile); 500 } 501 502 public synchronized final Pair<File, FileFormat> 503 showOpenFileDialog(final String title, final FileFormat... fileFormats) { 504 final FileChooser fc = new FileChooser(); 505 fc.setTitle(title); 506 if (lastDirectory != null) 507 fc.setInitialDirectory(lastDirectory); 508 for (FileFormat ff : fileFormats) 509 fc.getExtensionFilters().add(ff.extensionFilter); 510 final File file = fc.showOpenDialog(primaryStage); 511 if (file == null) 512 return null; 513 FileFormat fileFormat = null; 514 for (FileFormat ff : fileFormats) 515 if (fc.getSelectedExtensionFilter().equals(ff.extensionFilter)) { 516 fileFormat = ff; 517 break; 518 } 519 if (fileFormat == null) 520 return null; 521 lastDirectory = file.getParentFile(); 522 return FileFormat.of(file, fileFormat); 523 } 524 525 @SuppressWarnings("incomplete-switch") 526 private void openFile(final Pair<File, FileFormat> ffile) { 527 fileHistory.remove(ffile.first()); 528 fileHistory.add(0, ffile.first()); 529 switch (ffile.second()) { 530 case CFX: 531 treeView.addDataset(new FCADataset<String, String>(null, new Requests.Import.ImportCFX(ffile.first()))); 532 break; 533 case CXT: 534 treeView.addDataset(new FCADataset<String, String>(null, new Requests.Import.ImportCXT(ffile.first()))); 535 break; 536 case CSVB: 537 treeView.addDataset(new FCADataset<String, String>(null, new Requests.Import.ImportCSVB(ffile.first()))); 538 break; 539 case NT: 540 treeView.addDataset(new RDFDataset(ffile.first(), ffile.second())); 541 break; 542 case CSVT: 543 treeView.addDataset(new RDFDataset(ffile.first(), ffile.second())); 544 } 545 } 546 547 private final void askForUnsavedChanges() { 548 treeView.getDatasets().forEach(this::askForUnsavedChanges); 549 } 550 551 private final void askForUnsavedChanges(final Dataset dataset) { 552 if (dataset.unsavedChanges.get() && new FXDialog<Void>( 553 primaryStage, 554 FXDialog.Style.QUESTION, 555 "Unsaved Changes", 556 dataset.id.get() + " has unsaved changes. Do you want to save?", 557 null).showAndWait().result().equals(FXDialog.Answer.YES)) 558 dataset.save(); 559 } 560 561 public final void stop() { 562 askForUnsavedChanges(); 563 try { 564 writeConfiguration(); 565 primaryStage.close(); 566 System.exit(0); 567 } catch (IOException e) { 568 System.err.println("Could not write configuration to " + configuration.getFile()); 569 e.printStackTrace(); 570 primaryStage.close(); 571 System.exit(1); 572 } 573 } 574 575}