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}