Package lighthouse

Source Code of lighthouse.MainWindow

package lighthouse;

import com.google.protobuf.ByteString;
import com.subgraph.orchid.TorClient;
import com.subgraph.orchid.TorInitializationListener;
import com.vinumeris.updatefx.UpdateFX;
import com.vinumeris.updatefx.Updater;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import javafx.animation.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.input.DragEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.util.Duration;
import lighthouse.controls.ClickableBitcoinAddress;
import lighthouse.controls.NotificationBarPane;
import lighthouse.controls.ProjectOverviewWidget;
import lighthouse.controls.ProjectView;
import lighthouse.files.AppDirectory;
import lighthouse.files.DiskManager;
import lighthouse.model.BitcoinUIModel;
import lighthouse.protocol.Project;
import lighthouse.subwindows.EditProjectWindow;
import lighthouse.subwindows.SendMoneyController;
import lighthouse.subwindows.UpdateFXWindow;
import lighthouse.subwindows.WalletSettingsController;
import lighthouse.utils.GuiUtils;
import lighthouse.utils.easing.EasingMode;
import lighthouse.utils.easing.ElasticInterpolator;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.utils.MonetaryFormat;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import static javafx.beans.binding.Bindings.when;
import static lighthouse.threading.AffinityExecutor.UI_THREAD;
import static lighthouse.utils.GuiUtils.animatedBind;
import static lighthouse.utils.GuiUtils.platformFiddleChooser;

/**
* Gets created auto-magically by FXMLLoader via reflection. The widget fields are set to the GUI controls they're named
* after. This class handles all the updates and event handling for the main UI.
*/
public class MainWindow {
    private static final Logger log = LoggerFactory.getLogger(MainWindow.class);

    @FXML HBox topBoxLeftArea;
    @FXML Label balance;
    @FXML Button sendMoneyOutBtn, setupWalletBtn, menuBtn;
    @FXML ClickableBitcoinAddress addressControl;
    @FXML HBox balanceArea;
    @FXML VBox projectsVBox;
    @FXML HBox topBox;
    @FXML VBox root;
    @FXML HBox contentHBox;
    @FXML ScrollPane contentScrollPane;
    @FXML ProjectView projectView;
    @FXML StackPane projectViewContainer;
    @FXML VBox overviewVbox;
    @FXML VBox contentStack;
    @FXML Label addProjectIcon;
    @FXML Label networkIndicatorLabel;
    @FXML Button backButton;

    // These are read-only mirrors of sets maintained by the backend. Changes made by LighthouseBackend are propagated
    // into the UI thread and applied there asynchronously, thus it is safe to connect them directly to UI widgets.
    private ObservableList<Project> projects;

    public static BitcoinUIModel bitcoinUIModel = new BitcoinUIModel();
    private NotificationBarPane.Item syncItem;
    private ObservableMap<String, LighthouseBackend.ProjectStateInfo> projectStates;
    // A map indicating the status of checking each project against the network (downloading, found an error, done, etc)
    // This is mirrored into the UI thread from the backend.
    private ObservableMap<Project, LighthouseBackend.CheckStatus> checkStates;

    private SimpleBooleanProperty inProjectView = new SimpleBooleanProperty();

    private static Updater updater;

    enum Views {
        OVERVIEW,
        PROJECT
    }

    // Called by FXMLLoader.
    public void initialize() {
        AwesomeDude.setIcon(sendMoneyOutBtn, AwesomeIcon.SIGN_OUT, "12pt", ContentDisplay.LEFT);
        Tooltip.install(sendMoneyOutBtn, new Tooltip("Send money out of the wallet"));
        AwesomeDude.setIcon(setupWalletBtn, AwesomeIcon.LOCK, "12pt", ContentDisplay.LEFT);
        Tooltip.install(setupWalletBtn, new Tooltip("Make paper backup and encrypt your wallet"));
        AwesomeDude.setIcon(addProjectIcon, AwesomeIcon.FILE_ALT, "50pt; -fx-text-fill: white" /* lame hack */);

        // Slide back button in/out.
        AwesomeDude.setIcon(backButton, AwesomeIcon.ARROW_CIRCLE_LEFT, "30");
        animatedBind(topBoxLeftArea, topBoxLeftArea.translateXProperty(), when(inProjectView).then(0).otherwise(-45),
                Interpolator.EASE_OUT);

        AwesomeDude.setIcon(menuBtn, AwesomeIcon.BARS);

        // Avoid duplicate add errors.
        contentStack.getChildren().remove(projectViewContainer);
        contentStack.getChildren().remove(overviewVbox);

        // Some UI init is done in onBitcoinSetup
        switchView(Views.OVERVIEW);

        // Wait for the backend to start up so we can populate the projects list without seeing laggards drop in
        // from the top, as otherwise the backend could still be loading projects by the time we're done loading
        // the UI.
        if (!Main.instance.waitForInit())
            return// Backend didn't start up e.g. app is already running.

        projects = Main.backend.mirrorProjects(UI_THREAD);
        projectStates = Main.backend.mirrorProjectStates(UI_THREAD);
        checkStates = Main.backend.mirrorCheckStatuses(UI_THREAD);
        for (Project project : projects)
            projectsVBox.getChildren().add(0, buildProjectWidget(project));
        projects.addListener((ListChangeListener<Project>) change -> {
            while (change.next()) {
                if (change.wasReplaced()) {
                    updateExistingProject(change.getFrom(), change.getAddedSubList().get(0), change.getRemoved().get(0));
                } else if (change.wasAdded()) {
                    slideInNewProject(change.getAddedSubList().get(0));
                } else if (change.wasRemoved()) {
                    log.warn("Cannot animate project remove yet: {}", change);
                }
            }
        });
    }

    private void switchView(Views view) {
        switch (view) {
            case OVERVIEW:
                contentStack.getChildren().remove(projectViewContainer);
                contentStack.getChildren().add(overviewVbox);
                projectView.updateForVisibility(false, null);
                inProjectView.set(false);
                break;
            case PROJECT:
                contentStack.getChildren().remove(overviewVbox);
                contentStack.getChildren().add(projectViewContainer);
                projectView.updateForVisibility(true, checkStates);
                inProjectView.set(true);
                break;
            default: throw new IllegalStateException();
        }
    }

    private void switchToProject(Project next) {
        log.info("Switching to project: {}", next.getTitle());
        projectView.project.set(next);
        switchView(Views.PROJECT);
    }

    // Triggered by the project disk model being adjusted.
    private void updateExistingProject(int index, Project newProject, Project prevProject) {
        projectsVBox.getChildren().set(projectsVBox.getChildren().size() - 2 - index, buildProjectWidget(newProject));
        if (inProjectView.get() && projectView.getProject().equals(prevProject)) {
            projectView.setProject(newProject);
        }
    }

    // Triggered by the project disk model being adjusted.
    private void slideInNewProject(Project project) {
        if (contentScrollPane.getVvalue() != contentScrollPane.getVmin()) {
            // Need to scroll to the top before dropping the project widget in.
            scrollToTop().setOnFinished(ev -> slideInNewProject(project));
            return;
        }
        ProjectOverviewWidget projectWidget = buildProjectWidget(project);

        // Hack: Add at the end for the size calculation, then we'll move it to the start after the next frame.
        projectWidget.setVisible(false);
        projectsVBox.getChildren().add(projectWidget);

        // Slide in from above.
        Platform.runLater(() -> {
            double amount = projectWidget.getHeight();
            amount += projectsVBox.getSpacing();
            contentHBox.setTranslateY(-amount);
            TranslateTransition transition = new TranslateTransition(Duration.millis(1500), contentHBox);
            transition.setFromY(-amount);
            transition.setToY(0);
            transition.setInterpolator(new ElasticInterpolator(EasingMode.EASE_OUT));
            transition.setDelay(Duration.millis(1000));
            transition.play();
            // Re-position at the start.
            projectsVBox.getChildren().remove(projectWidget);
            projectsVBox.getChildren().add(0, projectWidget);
            projectWidget.setVisible(true);
        });
    }

    private ProjectOverviewWidget buildProjectWidget(Project project) {
        SimpleObjectProperty<LighthouseBackend.ProjectState> state = new SimpleObjectProperty<>(getProjectState(project));
        projectStates.addListener((javafx.beans.InvalidationListener) x -> state.set(getProjectState(project)));
        ProjectOverviewWidget projectWidget = new ProjectOverviewWidget(project,
                Main.backend.makeTotalPledgedProperty(project, UI_THREAD),
                state);
        projectWidget.onCheckStatusChanged(checkStates.get(project));
        checkStates.addListener((MapChangeListener<Project, LighthouseBackend.CheckStatus>) change -> {
            if (change.getKey().equals(project))
                projectWidget.onCheckStatusChanged(change.wasAdded() ? change.getValueAdded() : null);
        });
        projectWidget.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> switchToProject(project));
        return projectWidget;
    }

    private LighthouseBackend.ProjectState getProjectState(Project project) {
        LighthouseBackend.ProjectStateInfo info = projectStates.get(project.getID());
        return info == null ? LighthouseBackend.ProjectState.OPEN : info.state;
    }

    @FXML
    public void addProjectClicked(ActionEvent event) {
        EditProjectWindow.openForCreate();
    }

    @FXML
    public void importClicked(ActionEvent event) {
        FileChooser chooser = new FileChooser();
        chooser.setTitle("Select a bitcoin project file to import");
        chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Project/contract files", "*" + DiskManager.PROJECT_FILE_EXTENSION));
                platformFiddleChooser(chooser);
        File file = chooser.showOpenDialog(Main.instance.mainStage);
        if (file == null)
            return;
        log.info("Import clicked: {}", file);
        importProject(file);
    }

    @FXML
    public void backToOverview(ActionEvent event) {
        switchView(Views.OVERVIEW);
    }

    @FXML
    public void dragOver(DragEvent event) {
        boolean accept = false;
        if (event.getGestureSource() != null)
            return;   // Coming from us.
        for (File file : event.getDragboard().getFiles()) {
            if (file.toString().endsWith(DiskManager.PROJECT_FILE_EXTENSION) || file.toString().endsWith(DiskManager.PLEDGE_FILE_EXTENSION)) {
                accept = true;
                break;
            }
        }
        if (accept)
            event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
    }

    @FXML
    public void dragDropped(DragEvent event) {
        log.info("Drop: {}", event);
        for (File file : event.getDragboard().getFiles()) {
            if (file.toString().endsWith(DiskManager.PROJECT_FILE_EXTENSION)) {
                importProject(file);
            } else if (file.toString().endsWith(DiskManager.PLEDGE_FILE_EXTENSION)) {
                try {
                    Sha256Hash hash = Sha256Hash.hashFileContents(file);
                    Files.copy(file.toPath(), AppDirectory.dir().resolve(hash + DiskManager.PLEDGE_FILE_EXTENSION));
                } catch (IOException e) {
                    GuiUtils.informationalAlert("Import failed",
                            "Could not copy the dropped pledge into the Lighthouse application directory: " + e);
                }
            } else
                log.error("Unknown file type dropped: should not happen: " + file);
        }
    }


    public static void importProject(File file) {
        importProject(file.toPath());
    }

    public static void importProject(Path file) {
        try {
            Main.backend.importProjectFrom(file);
        } catch (IOException e) {
            GuiUtils.informationalAlert("Failed to import project",
                    "Could not read project file: " + e.getLocalizedMessage());
        }
    }

    private static boolean firstTime = true;
    public void onBitcoinSetup() {
        bitcoinUIModel.setWallet(Main.wallet);
        addressControl.addressProperty().bind(bitcoinUIModel.addressProperty());
        balance.textProperty().bind(EasyBind.map(bitcoinUIModel.balanceProperty(), coin -> MonetaryFormat.BTC.noCode().format(coin).toString()));
        // Don't let the user click send money when the wallet is empty.
        sendMoneyOutBtn.disableProperty().bind(bitcoinUIModel.balanceProperty().isEqualTo(Coin.ZERO));

        if (Main.params != MainNetParams.get()) {
            networkIndicatorLabel.setVisible(true);
            if (Main.params == TestNet3Params.get())
                networkIndicatorLabel.setText("testnet");
            else if (Main.params == RegTestParams.get())
                networkIndicatorLabel.setText("regtest");
            else
                networkIndicatorLabel.setText("?");
        }

        // Don't do startup processing if the UI is being hot reloaded ...
        if (firstTime) {
            firstTime = false;
            // NotificationBarPane is set up by this point, so we can do things that need to show notifications.
            setupBitcoinSyncNotification();
            doOnlineUpdateCheck();
            maybeShowReleaseNotes();
        }
    }

    private static final String LAST_VER_TAG = "com.vinumeris.lighthouse.lastVer";
    private void maybeShowReleaseNotes() {
        // Show release notes when we've upgraded to a new version (hard coded), but only if this is the first run
        // after the upgrade.
        ByteString bytes = Main.wallet.maybeGetTag(LAST_VER_TAG);
        if (bytes != null) {
            int lastVer = Integer.parseInt(bytes.toStringUtf8());
            if (Main.VERSION > lastVer) {
                log.info("Was upgraded from v{} to v{}!", lastVer, Main.VERSION);

                //
                // No release notes currently.
                //
            }
        }
        Main.wallet.setTag(LAST_VER_TAG, ByteString.copyFromUtf8("" + Main.VERSION));
    }

    private void setupBitcoinSyncNotification() {
        TorClient torClient = Main.bitcoin.peerGroup().getTorClient();
        if (torClient != null) {
            SimpleDoubleProperty torProgress = new SimpleDoubleProperty(-1);
            String torMsg = "Initialising Tor";
            syncItem = Main.instance.notificationBar.displayNewItem(torMsg, torProgress);
            torClient.addInitializationListener(new TorInitializationListener() {
                @Override
                public void initializationProgress(String message, int percent) {
                    Platform.runLater(() -> {
                        syncItem.label.set(torMsg + ": " + message);
                        torProgress.set(percent / 100.0);
                    });
                }

                @Override
                public void initializationCompleted() {
                    Platform.runLater(() -> {
                        syncItem.cancel();
                        showBitcoinSyncMessage();
                    });
                }
            });
        } else {
            showBitcoinSyncMessage();
        }
        bitcoinUIModel.syncProgressProperty().addListener(x -> {
            if (bitcoinUIModel.syncProgressProperty().get() >= 1.0) {
                if (syncItem != null) {
                    // Hack around JFX progress animator leak bug.
                    GuiUtils.runOnGuiThreadAfter(500, () -> {
                        syncItem.cancel();
                        syncItem = null;
                    });
                }
            } else if (syncItem == null) {
                showBitcoinSyncMessage();
            }
        });
    }

    private void doOnlineUpdateCheck() {
        updater = new Updater(Main.instance.updatesURL, Main.APP_NAME, Main.VERSION, AppDirectory.dir(),
                UpdateFX.findCodePath(Main.class), Main.UPDATE_SIGNING_KEYS, Main.UPDATE_SIGNING_THRESHOLD);

        if (!Main.instance.updatesURL.equals(Main.UPDATES_BASE_URL))
            updater.setOverrideURLs(true);    // For testing.

        // Only bother to show the user a notification if we're actually going to download an update.
        updater.progressProperty().addListener(new InvalidationListener() {
            private boolean shown = false;

            @Override
            public void invalidated(Observable x) {
                if (shown) return;
                NotificationBarPane.Item downloadingItem = Main.instance.notificationBar.displayNewItem(
                        "Downloading software update", updater.progressProperty());
                updater.setOnSucceeded(ev -> {
                    Button restartButton = new Button("Restart");
                    restartButton.setOnAction(ev2 -> Main.restart());
                    NotificationBarPane.Item newItem = Main.instance.notificationBar.createItem(
                            "Please restart the app to upgrade to the new version.", restartButton);
                    Main.instance.notificationBar.items.replaceAll(item -> item == downloadingItem ? newItem : item);
                });
                updater.setOnFailed(ev -> {
                    downloadingItem.cancel();
                    log.error("Online update check failed", updater.getException());
                    // At this point the user has seen that we're trying to download something so tell them if it went
                    // wrong.
                    if (Main.params != RegTestParams.get())
                        GuiUtils.informationalAlert("Online update failed",
                                "An error was encountered whilst attempting to download or apply an online update: %s",
                                updater.getException());
                });
                shown = true;
            }
        });
        // Don't bother the user if update check failed: assume some temporary server error that can be fixed silently.
        updater.setOnFailed(ev -> log.error("Online update check failed", updater.getException()));
        Thread thread = new Thread(updater, "Online update check");
        thread.setDaemon(true);
        thread.start();
    }

    private void showBitcoinSyncMessage() {
        syncItem = Main.instance.notificationBar.displayNewItem("Synchronising with the Bitcoin network", bitcoinUIModel.syncProgressProperty());
    }

    private Animation scrollToTop() {
        Animation animation = new Timeline(
                new KeyFrame(GuiUtils.UI_ANIMATION_TIME,
                        new KeyValue(contentScrollPane.vvalueProperty(), contentScrollPane.getVmin(), Interpolator.EASE_BOTH)
                )
        );
        animation.play();
        return animation;
    }

    @FXML
    public void menuClicked(ActionEvent event) {
        // For now just skip straight to the only menu item: the update control panel.
        UpdateFXWindow.open(updater);
    }

    //region Generic Bitcoin wallet related code
    @FXML
    public void sendMoneyOut(ActionEvent event) {
        SendMoneyController.open();
    }

    @FXML
    public void setupWalletClicked(ActionEvent event) {
        Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("subwindows/wallet_settings.fxml", "Wallet settings");
        screen.controller.initialize(null);
    }
    //endregion
}
TOP

Related Classes of lighthouse.MainWindow

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.