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