All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.binjr.core.controllers.MainViewController Maven / Gradle / Ivy

There is a newer version: 3.20.1
Show newest version
/*
 *    Copyright 2016-2021 Frederic Thevenet
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package eu.binjr.core.controllers;

import eu.binjr.common.function.CheckedLambdas;
import eu.binjr.common.javafx.bindings.BindingManager;
import eu.binjr.common.javafx.controls.*;
import eu.binjr.common.logging.Logger;
import eu.binjr.common.text.StringUtils;
import eu.binjr.core.appearance.StageAppearanceManager;
import eu.binjr.core.data.adapters.*;
import eu.binjr.core.data.async.AsyncTaskManager;
import eu.binjr.core.data.exceptions.CannotInitializeDataAdapterException;
import eu.binjr.core.data.exceptions.CannotLoadWorksheetException;
import eu.binjr.core.data.exceptions.DataAdapterException;
import eu.binjr.core.data.exceptions.NoAdapterFoundException;
import eu.binjr.core.data.workspace.*;
import eu.binjr.core.dialogs.Dialogs;
import eu.binjr.core.preferences.AppEnvironment;
import eu.binjr.core.preferences.UserHistory;
import eu.binjr.core.preferences.UserPreferences;
import eu.binjr.core.update.UpdateManager;
import jakarta.xml.bind.JAXBException;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.*;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.TextAlignment;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Callback;
import javafx.util.Duration;
import org.controlsfx.control.MaskerPane;
import org.eclipse.fx.ui.controls.tree.FilterableTreeItem;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;

/**
 * The controller class for the main view
 *
 * @author Frederic Thevenet
 */
public class MainViewController implements Initializable {
    static final int SETTINGS_PANE_DISTANCE = 250;
    private static final Logger logger = Logger.create(MainViewController.class);
    private static final String[] BINJR_FILE_PATTERN = new String[]{"*.bjr", "*.xml"};
    private static final double SEARCH_BAR_PANE_DISTANCE = 40;
    private static final PseudoClass HOVER_PSEUDO_CLASS = PseudoClass.getPseudoClass("hover");
    private static final DataFormat GENERIC_BINDING_FORMAT = new DataFormat(SourceBinding.MIME_TYPE);
    private static final DataFormat TIME_SERIES_BINDING_FORMAT = new DataFormat(TimeSeriesBinding.MIME_TYPE);
    private static final DataFormat TEXT_FILES_BINDING_FORMAT = new DataFormat(TextFilesBinding.MIME_TYPE);
    private static final DataFormat LOG_FILES_BINDING_FORMAT = new DataFormat(LogFilesBinding.MIME_TYPE);

    private final Map seriesControllers = new WeakHashMap<>();
    private final Map sourcesAdapters = new WeakHashMap<>();
    private final BooleanProperty searchBarVisible = new SimpleBooleanProperty(false);
    private final BooleanProperty searchBarHidden = new SimpleBooleanProperty(!searchBarVisible.get());
    private final BooleanProperty treeItemDragAndDropInProgress = new SimpleBooleanProperty(false);
    private BooleanBinding noWorksheetPresent;
    private BooleanBinding noSourcePresent;

    @FXML
    private MenuItem restoreClosedWorksheetMenu;
    @FXML
    private AnchorPane sourcePane;
    @FXML
    private MenuItem hideSourcePaneMenu;
    @FXML
    private StackPane newWorksheetDropTarget;
    @FXML
    private DrawerPane commandBar;
    @FXML
    private AnchorPane root;
    @FXML
    private Label addWorksheetLabel;
    @FXML
    private MaskerPane sourceMaskerPane;
    @FXML
    private MaskerPane worksheetMaskerPane;
    @FXML
    private Pane searchBarRoot;
    @FXML
    private TextField searchField;
    @FXML
    private Button searchButton;
    @FXML
    private Button hideSearchBarButton;
    @FXML
    private ToggleButton searchCaseSensitiveToggle;
    @FXML
    private StackPane sourceArea;
    List> searchResultSet;
    int currentSearchHit = -1;
    private Workspace workspace;
    @FXML
    private MenuButton worksheetMenu;
    private Optional associatedFile = Optional.empty();
    @FXML
    private Accordion sourcesPane;
    @FXML
    private TearableTabPane tearableTabPane;
    @FXML
    private MenuItem saveMenuItem;
    @FXML
    private Menu openRecentMenu;
    @FXML
    private SplitPane contentView;
    @FXML
    private StackPane settingsPane;
    @FXML
    private StackPane worksheetArea;
    @FXML
    private Menu addSourceMenu;
    @FXML
    private StackPane curtains;
    @FXML
    private Label addSourceLabel;

    /**
     * Initializes a new instance of the {@link MainViewController} class.
     */
    public MainViewController() {
        super();
        this.workspace = new Workspace();
    }

    public static Optional treeItemsAsChartList(Collection> treeItems, Node dlgRoot) {
        var charts = new ArrayList();
        var totalBindings = 0;
        for (var treeItem : treeItems) {
            for (var t : TreeViewUtils.splitAboveLeaves(treeItem, true)) {
                var chart = new BindingsHierarchy();
                var binding = t.getValue();
                chart.setName(binding);
                for (var b : TreeViewUtils.flattenLeaves(t)) {
                    chart.getBindings().add(b);
                    totalBindings++;
                }
                charts.add(chart);
            }
        }
        if (totalBindings >= UserPreferences.getInstance().maxSeriesPerChartBeforeWarning.get().intValue()) {
            if (Dialogs.confirmDialog(dlgRoot,
                    "This action will add " + totalBindings + " series on a single worksheet.",
                    "Are you sure you want to proceed?"
            ) != ButtonType.YES) {
                return Optional.empty();
            }
        }
        return Optional.of(charts.toArray(BindingsHierarchy[]::new));
    }

    @FXML
    private void worksheetAreaOnDragOver(DragEvent event) {
        Dragboard db = event.getDragboard();
        if (db.hasContent(GENERIC_BINDING_FORMAT) ||
                db.hasContent(TIME_SERIES_BINDING_FORMAT) ||
                db.hasContent(TEXT_FILES_BINDING_FORMAT) ||
                db.hasContent(LOG_FILES_BINDING_FORMAT)) {
            event.acceptTransferModes(TransferMode.COPY);
            event.consume();
        }
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        assert root != null : "fx:id\"root\" was not injected!";
        assert tearableTabPane != null : "fx:id\"tearableTabPane\" was not injected!";
        assert sourcesPane != null : "fx:id\"sourceTabPane\" was not injected!";
        assert saveMenuItem != null : "fx:id\"saveMenuItem\" was not injected!";
        assert openRecentMenu != null : "fx:id\"openRecentMenu\" was not injected!";
        assert contentView != null : "fx:id\"contentView\" was not injected!";
        noWorksheetPresent = Bindings.size(tearableTabPane.getTabs()).isEqualTo(0);
        noSourcePresent = Bindings.size(sourcesPane.getPanes()).isEqualTo(0);

        contentView.getDividers().stream().findFirst().ifPresent(divider -> {
            divider.setPosition(getWorkspace().getDividerPosition());
            getWorkspace().getBindingManager().bind(getWorkspace().dividerPositionProperty(), divider.positionProperty());
        });
        sourcesPane.mouseTransparentProperty().bind(noSourcePresent);
        addSourceLabel.visibleProperty().bind(noSourcePresent);
        workspace.sourcePaneVisibleProperty().addListener((observable, oldValue, newValue) -> toggleSourcePaneVisibilty(newValue));
        workspace.presentationModeProperty().addListener((observable, oldValue, newValue) -> {
            for (var w : workspace.getWorksheets()) {
                w.setEditModeEnabled(!newValue);
            }
            workspace.setSourcePaneVisible(!newValue);
        });
        for (var w : workspace.getWorksheets()) {
            w.setEditModeEnabled(!workspace.isPresentationMode());
        }
        workspace.setSourcePaneVisible(!workspace.isPresentationMode());
        toggleSourcePaneVisibilty(workspace.isSourcePaneVisible());
        sourcesPane.expandedPaneProperty().addListener(
                (ObservableValue observable, TitledPane oldPane, TitledPane newPane) -> {
                    if (UserPreferences.getInstance().preventFoldingAllSourcePanes.get()) {
                        boolean expandRequiered = true;
                        for (TitledPane pane : sourcesPane.getPanes()) {
                            if (pane.isExpanded()) {
                                expandRequiered = false;
                            }
                        }
                        if ((expandRequiered) && (oldPane != null)) {
                            Platform.runLater(() -> sourcesPane.setExpandedPane(oldPane));
                        }
                    }
                });
        addWorksheetLabel.visibleProperty().bind(noWorksheetPresent);
        tearableTabPane.setDetachedStageStyle(AppEnvironment.getInstance().getWindowsStyle());
        tearableTabPane.setNewTabFactory(this::worksheetTabFactory);
        tearableTabPane.getGlobalTabs().addListener((ListChangeListener) this::onWorksheetTabChanged);
        tearableTabPane.setTearable(true);
        tearableTabPane.setOnOpenNewWindow(event -> {
            Stage stage = (Stage) event.getSource();
            stage.setTitle(AppEnvironment.APP_NAME);
            registerStageKeyEvents(stage);

            StackPane dropZone = new StackPane(ToolButtonBuilder.makeIconNode(Pos.CENTER, "new-tab-icon"));
            dropZone.getStyleClass().add("drop-zone");
            dropZone.setOnDragDropped(this::handleDragDroppedOnWorksheetArea);
            dropZone.setOnDragOver(this::worksheetAreaOnDragOver);
            dropZone.setOnDragExited(this::handleOnDragExitedNewWorksheet);
            dropZone.setOnDragEntered(this::handleOnDragEnteredNewWorksheet);
            var newPaneDropZone = new StackPane(dropZone);
            newPaneDropZone.getStyleClass().add("chart-viewport-parent");
            AnchorPane.setTopAnchor(newPaneDropZone, 0.0);
            AnchorPane.setLeftAnchor(newPaneDropZone, 0.0);
            AnchorPane.setRightAnchor(newPaneDropZone, 0.0);
            newPaneDropZone.setPrefHeight(34);
            newPaneDropZone.setMaxHeight(34);
            newPaneDropZone.managedProperty().bind(treeItemDragAndDropInProgressProperty());
            newPaneDropZone.visibleProperty().bind(treeItemDragAndDropInProgressProperty());
            ((Pane) stage.getScene().getRoot()).getChildren().add(newPaneDropZone);
            StageAppearanceManager.getInstance().register(stage);
        });
        tearableTabPane.setOnClosingWindow(event -> {
            StageAppearanceManager.getInstance().unregister((Stage) event.getSource());
            unregisterStageKeyEvents((Stage) event.getSource());
        });
        sourcesPane.getPanes().addListener(this::onSourceTabChanged);
        sourcesPane.addEventFilter(KeyEvent.KEY_PRESSED, (e -> {
            if (e.getCode() == KeyCode.F && e.isControlDown()) {
                handleShowSearchBar(null);
            }
        }));
        saveMenuItem.disableProperty().bind(workspace.dirtyProperty().not());
        commandBar.setSibling(contentView);
        searchField.textProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                invalidateSearchResults();
                findNext();
            }
        });
        searchCaseSensitiveToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
            invalidateSearchResults();
            findNext();
        });
        searchBarVisible.addListener((observable, oldValue, newValue) -> {
            if (newValue) {
                searchField.requestFocus();
                if (searchBarHidden.getValue()) {
                    slidePanel(1, Duration.millis(0));
                    searchBarHidden.setValue(false);
                }
            } else {
                if (!searchBarHidden.getValue()) {
                    slidePanel(-1, Duration.millis(0));
                    searchBarHidden.setValue(true);
                }
            }
        });
        this.addSourceMenu.getItems().addAll(populateSourceMenu());
        this.addSourceMenu.setOnShowing(event -> addSourceMenu.getItems().setAll(populateSourceMenu()));
        newWorksheetDropTarget.managedProperty()
                .bind(tearableTabPane.emptyProperty().not().and(treeItemDragAndDropInProgressProperty()));
        newWorksheetDropTarget.visibleProperty()
                .bind(tearableTabPane.emptyProperty().not().and(treeItemDragAndDropInProgressProperty()));
        this.restoreClosedWorksheetMenu.disableProperty().bind(workspace.closedWorksheetQueueEmptyProperty());
        Platform.runLater(this::runAfterInitialize);
    }

    protected void runAfterInitialize() {
        UserPreferences userPrefs = UserPreferences.getInstance();
        Stage stage = Dialogs.getStage(root);
        stage.titleProperty().bind(Bindings.createStringBinding(
                () -> String.format("%s%s - %s",
                        (workspace.isDirty() ? "*" : ""),
                        workspace.pathProperty().getValue().toString(),
                        AppEnvironment.APP_NAME
                ),
                workspace.pathProperty(),
                workspace.dirtyProperty())
        );
        stage.setOnCloseRequest(event -> {
            if (!confirmAndClearWorkspace()) {
                event.consume();
            } else {
                saveWindowPositionAndQuit();
            }
        });
        registerStageKeyEvents(stage);
        if (associatedFile.isPresent()) {
            logger.debug(() -> "Opening associated file " + associatedFile.get());
            loadWorkspace(new File(associatedFile.get()));
        } else if (userPrefs.loadLastWorkspaceOnStartup.get()) {
            UserHistory.getInstance().mostRecentWorkspaces.peek().ifPresent(latestWorkspacePath -> {
                File latestWorkspace = latestWorkspacePath.toFile();
                if (latestWorkspace.exists()) {
                    loadWorkspace(latestWorkspace);
                } else {
                    logger.warn("Cannot reopen workspace " + latestWorkspace.getPath() + ": file does not exists");
                }
            });
        }
        if (userPrefs.checkForUpdateOnStartUp.get()) {
            UpdateManager.getInstance().asyncCheckForUpdate(
                    release -> UpdateManager.getInstance().showUpdateAvailableNotification(release, root), null, null
            );
        }
    }

    private void registerStageKeyEvents(Stage stage) {
        BindingManager manager = new BindingManager();
        stage.setUserData(manager);
        stage.addEventFilter(KeyEvent.KEY_PRESSED, manager.registerHandler(e -> {
            logger.trace(() -> "KEY_PRESSED event trapped, keycode=" + e.getCode());
            if (e.getCode() == KeyCode.F12) {
                AppEnvironment.getInstance().setDebugMode(!AppEnvironment.getInstance().isDebugMode());
            }
            if (e.getCode() == KeyCode.F5 || (e.getCode() == KeyCode.R && e.isControlDown())) {
                getSelectedWorksheetController().ifPresent(WorksheetController::refresh);
            }
            if (e.getCode() == KeyCode.M && e.isControlDown()) {
                handleTogglePresentationMode();
            }
            if (e.getCode() == KeyCode.P && e.isControlDown()) {
                getSelectedWorksheetController().ifPresent(WorksheetController::saveSnapshot);
            }
            if (e.getCode() == KeyCode.T && e.isControlDown() && !e.isShiftDown()) {
                editWorksheet(new XYChartsWorksheet());
            }
            if (e.isControlDown() && (e.getCode() == KeyCode.W || e.getCode() == KeyCode.F4)) {
                closeWorksheetTab((EditableTab) tearableTabPane.getSelectedTab());
            }
            if (e.getCode() == KeyCode.LEFT && e.isAltDown()) {
                getSelectedWorksheetController().ifPresent(WorksheetController::navigateBackward);
            }
            if (e.getCode() == KeyCode.RIGHT && e.isAltDown()) {
                getSelectedWorksheetController().ifPresent(WorksheetController::navigateForward);
            }
            if (e.getCode() == KeyCode.T && e.isShiftDown() && e.isControlDown()) {
                restoreLatestClosedWorksheet();
            }
        }));
        stage.addEventFilter(KeyEvent.KEY_PRESSED, manager.registerHandler(e -> handleControlKey(e, true)));
        stage.addEventFilter(KeyEvent.KEY_RELEASED, manager.registerHandler(e -> handleControlKey(e, false)));
        manager.attachListener(stage.focusedProperty(), (observable, oldValue, newValue) -> {
            //main stage lost focus -> invalidates shift or ctrl pressed
            UserPreferences.getInstance().shiftPressed.set(false);
            UserPreferences.getInstance().ctrlPressed.set(false);
        });
    }

    private void unregisterStageKeyEvents(Stage stage) {
        BindingManager manager = (BindingManager) stage.getUserData();
        if (manager != null) {
            manager.close();
        }
    }

    //region UI handlers
    @FXML
    protected void handleAboutAction(ActionEvent event) throws IOException {
        Dialog dialog = new Dialog<>();
        dialog.initStyle(StageStyle.DECORATED);
        dialog.setTitle("About " + AppEnvironment.APP_NAME);
        dialog.setDialogPane(FXMLLoader.load(getClass().getResource("/eu/binjr/views/AboutBoxView.fxml")));
        dialog.initOwner(Dialogs.getStage(root));
        dialog.getDialogPane().getStylesheets().add(getClass().getResource(StageAppearanceManager.getFontFamilyCssPath()).toExternalForm());
        dialog.showAndWait();
    }

    @FXML
    protected void handleQuitAction(ActionEvent event) {
        if (confirmAndClearWorkspace()) {
            saveWindowPositionAndQuit();
        }
    }

    @FXML
    protected void handlePreferencesAction(ActionEvent actionEvent) {
        try {
            TranslateTransition openNav = new TranslateTransition(new Duration(350), settingsPane);
            openNav.setToX(SETTINGS_PANE_DISTANCE);
            openNav.play();
            commandBar.expand();
        } catch (Exception ex) {
            Dialogs.notifyException("Failed to display preference dialog", ex, root);
        }
    }

    @FXML
    protected void handleExpandCommandBar(ActionEvent actionEvent) {
        commandBar.toggle();
    }

    @FXML
    protected void handleAddNewWorksheet(Event event) {
        editWorksheet(new XYChartsWorksheet());
    }

    @FXML
    protected void handleAddSource(Event event) {
        Node sourceNode = (Node) event.getSource();
        ContextMenu sourceMenu = new ContextMenu();
        sourceMenu.getItems().addAll(populateSourceMenu());
        sourceMenu.show(sourceNode, Side.BOTTOM, 0, 0);
    }

    @FXML
    protected void handleHelpAction(ActionEvent event) {
        openUrlInBrowser(AppEnvironment.HTTP_BINJR_WIKI);
    }

    @FXML
    private void handleShortcutsAction(ActionEvent actionEvent) {
        openUrlInBrowser(AppEnvironment.HTTP_BINJR_SHORTCUTS);
    }

    @FXML
    protected void handleViewOnGitHub(ActionEvent event) {
        openUrlInBrowser(AppEnvironment.HTTP_GITHUB_REPO);
    }

    @FXML
    protected void handleBinjrWebsite(ActionEvent actionEvent) {
        openUrlInBrowser(AppEnvironment.HTTP_WWW_BINJR_EU);
    }

    private void openUrlInBrowser(String url) {
        try {
            Dialogs.launchUrlInExternalBrowser(url);
        } catch (IOException | URISyntaxException e) {
            logger.error("Failed to launch url in browser: " + url);
            logger.debug("Exception stack", e);
        }
    }

    @FXML
    protected void handleNewWorkspace(ActionEvent event) {
        confirmAndClearWorkspace();
    }

    @FXML
    protected void handleOpenWorkspace(ActionEvent event) {
        openWorkspaceFromFile();
    }

    @FXML
    protected void handleShowSearchBar(ActionEvent actionEvent) {
        this.searchBarVisible.setValue(true);
    }

    @FXML
    protected void handleHidePanel(ActionEvent actionEvent) {
        this.searchField.clear();
        this.searchBarVisible.setValue(false);
    }

    @FXML
    protected void handleFindNextInTreeView(ActionEvent actionEvent) {
        findNext();
    }

    @FXML
    protected void handleSaveWorkspace(ActionEvent event) {
        saveWorkspace();
    }

    @FXML
    protected void handleSaveAsWorkspace(ActionEvent event) {
        saveWorkspaceAs();
    }

    //endregion

    @FXML
    protected void handleDisplayChartProperties(ActionEvent actionEvent) {
        getSelectedWorksheetController().ifPresent(WorksheetController::toggleShowPropertiesPane);
    }

    @FXML
    protected void populateOpenRecentMenu(Event event) {
        Menu menu = (Menu) event.getSource();
        Collection recentPath = UserHistory.getInstance().mostRecentWorkspaces.getAll();
        if (!recentPath.isEmpty()) {
            menu.getItems().setAll(recentPath.stream().map(s -> {
                MenuItem m = new MenuItem(s.toString());
                m.setMnemonicParsing(false);
                m.setOnAction(e -> loadWorkspace(new File(((MenuItem) e.getSource()).getText())));
                return m;
            }).collect(Collectors.toList()));
        } else {
            MenuItem none = new MenuItem("none");
            none.setDisable(true);
            menu.getItems().setAll(none);
        }
    }

    private TitledPane newSourcePane(Source source) {
        TitledPane newPane = new TitledPane();
        Label label = new Label();
        source.getBindingManager().bind(label.textProperty(), source.nameProperty());
        GridPane titleRegion = new GridPane();
        titleRegion.setHgap(5);
        titleRegion.getColumnConstraints().add(
                new ColumnConstraints(65, USE_COMPUTED_SIZE, USE_COMPUTED_SIZE, Priority.ALWAYS, HPos.LEFT, true));
        titleRegion.getColumnConstraints().add(
                new ColumnConstraints(65, USE_COMPUTED_SIZE, USE_COMPUTED_SIZE, Priority.NEVER, HPos.RIGHT, false));
        source.getBindingManager().bind(titleRegion.minWidthProperty(), newPane.widthProperty().subtract(30));
        source.getBindingManager().bind(titleRegion.maxWidthProperty(), newPane.widthProperty().subtract(30));

        // *** Toolbar ***
        HBox toolbar = new HBox();
        toolbar.getStyleClass().add("title-pane-tool-bar");
        toolbar.setAlignment(Pos.CENTER);

        Button closeButton = new ToolButtonBuilder




© 2015 - 2024 Weber Informatics LLC | Privacy Policy