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}