ca.wumbo.doommanager.client.controller.file.DoomFileController.java Source code

Java tutorial

Introduction

Here is the source code for ca.wumbo.doommanager.client.controller.file.DoomFileController.java

Source

/*
 * DoomManager
 * Copyright (C) 2014  Chris K
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package ca.wumbo.doommanager.client.controller.file;

import javax.annotation.PostConstruct;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import ca.wumbo.doommanager.client.controller.CoreController;
import ca.wumbo.doommanager.client.util.Controllable;
import ca.wumbo.doommanager.client.util.EntryControllable;
import ca.wumbo.doommanager.client.util.Resources;
import ca.wumbo.doommanager.client.util.SelfInjectableController;
import ca.wumbo.doommanager.file.entry.Entry;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableCell;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.util.Callback;

/**
 * The controller for the Doom file viewer window.
 */
public class DoomFileController extends SelfInjectableController implements Controllable {

    @Autowired
    private CoreController coreController;

    @Autowired
    private Resources resources;

    @Value("${doomfile.controller.fxmlpath}")
    private String fxmlPath;

    //=========================================================================

    @FXML
    private BorderPane rootBorderPane;

    @FXML
    private SplitPane splitPane;

    @FXML
    private BorderPane leftBorderPane;

    @FXML
    private TreeTableView<Entry> entryTreeTable;

    @FXML
    private TreeTableColumn<Entry, Image> statusColumn;

    @FXML
    private TreeTableColumn<Entry, Entry> nameColumn;

    @FXML
    private TreeTableColumn<Entry, String> sizeColumn;

    @FXML
    private TreeTableColumn<Entry, String> typeColumn;

    @FXML
    private BorderPane rightBorderPane;

    //=========================================================================

    /**
     * The logger for this class.
     */
    private static final Logger log = LogManager.getLogger(DoomFileController.class);

    /**
     * Only to be instantiated by Spring.
     */
    private DoomFileController() {
    }

    @FXML
    private void initialize() {
        // Keep the left window the same size when resizing/maximizing.
        SplitPane.setResizableWithParent(leftBorderPane, false);

        // Allow selection of multiple cells.
        entryTreeTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        // Make the cells update accordingly.
        nameColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().entryProperty());
        sizeColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().dataLengthStringProperty());
        typeColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().entryTypeProperty());

        // Resize the splitter to a reasonable position.
        // Since we can't use Platform.runlater() to do this, we have to use a listener that removes itself.
        // Note: The old way it was done was to pass the tabPane's width to the function after this is initialized.
        InvalidationListener invalidationListener = new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                setSplitterPosition((int) splitPane.getWidth());
                splitPane.widthProperty().removeListener(this); // Remove itself after the size is set.
            }
        };
        splitPane.widthProperty().addListener(invalidationListener);

        // Handle item selection. This will clean up our GUI and add/remove panes on the right of the splitter.
        entryTreeTable.getSelectionModel().selectedItemProperty().addListener((obsValue, oldValue, newValue) -> {
            updateGUIFromEntrySelection(oldValue, newValue);
        });

        // Support right clicking menus on the rows, source: https://gist.github.com/james-d/7758918
        entryTreeTable.setRowFactory(new Callback<TreeTableView<Entry>, TreeTableRow<Entry>>() {
            @Override
            public TreeTableRow<Entry> call(TreeTableView<Entry> tableView) {
                // Create the row to return.
                TreeTableRow<Entry> row = new TreeTableRow<>();
                ContextMenu contextMenu = new ContextMenu();

                // Whenever this row gets a new item (or is updated), rebuild the right click menu.
                row.itemProperty().addListener((observableValue, oldValue, newValue) -> {
                    // TODO - Dynamically generate a new context menu - contextMenu.getItems().add(new MenuItem());
                });

                // Set context menu on row, but use a binding to make it only show for non-empty rows:
                row.contextMenuProperty()
                        .bind(Bindings.when(row.emptyProperty()).then((ContextMenu) null).otherwise(contextMenu));

                return row;
            }
        });

        // Instead of assigning graphics to each node, only do it for the cells.
        // This should help reduce object creation by having it only required for the visible rows.
        nameColumn.setCellFactory(new Callback<TreeTableColumn<Entry, Entry>, TreeTableCell<Entry, Entry>>() {
            @Override
            public TreeTableCell<Entry, Entry> call(TreeTableColumn<Entry, Entry> param) {
                return new TreeTableCell<Entry, Entry>() {
                    @Override
                    protected void updateItem(Entry item, boolean empty) {
                        super.updateItem(item, empty);
                        if (!empty && item != null) {
                            setText(item.getName());
                            Image img = resources.getImage(item.getClass().getSimpleName().toLowerCase());
                            setGraphic(new ImageView(img));
                        } else {
                            setText(null);
                            setGraphic(null);
                        }
                    }
                };
            }
        });
    }

    /**
     * Loads the FXML data and injects it into this object. Should be called by
     * Spring right after the constructor and dependencies are linked. To 
     * reduce code duplication, this functionality was moved to a containing
     * class.
     * 
     * @throws NullPointerException
     *       If the FXML path is null.
     * 
     * @throws RuntimeException
     *       If the FXML file is missing or corrupt.
     */
    @PostConstruct
    public void loadFXML() {
        super.loadFXML(fxmlPath);
    }

    /**
     * This is to be called when the user clicks on a new row, which means the
     * GUI should be updated by disposing of whatever is on the right (if it's
     * possible), and adding in the new view.
     * 
     * @param oldTreeItem
     *       The old tree item (null is allowed).
     * 
     * @param newTreeItem
     *       The new tree item (null is allowed).
     */
    private void updateGUIFromEntrySelection(TreeItem<Entry> oldTreeItem, TreeItem<Entry> newTreeItem) {
        // We only care if a click causes a change in entries.
        if (oldTreeItem != newTreeItem) {
            // Check if we haven't saved yet, and if not... prompt the user.
            // TODO

            // If we're changing to a new entry, time to load a new Pane.
            if (newTreeItem != null) {
                // Load the new Node based on what the entry is, and place it on the right.
                EntryControllable entryControllable = newTreeItem.getValue().getNewController();
                entryControllable.setEntry(newTreeItem.getValue());

                // Clean up the right pane to assist the garbage collector.
                // TODO

                // Insert the root pane at the center.
                rightBorderPane.setCenter(entryControllable.getRootPane());
            } else {
                // If we're going to null, we should clear last element placed in the pane with a blank pane.
                rightBorderPane.setCenter(new BorderPane());
            }
        }
    }

    /**
     * Setting the splitter position properly can be annoying due to the API
     * being unreliable with Platform.runLater(), therefore the only current
     * solution to make this work is to have this method called later on.
     * 
     * @param widthContainer
     *       The full width of the container (what should be the width of the
     *       splitpane node).
     */
    public void setSplitterPosition(int widthContainer) {
        log.trace("Setting splitter position relative to width {}.", widthContainer);

        double estimatedTableWidth = entryTreeTable.getColumns().size() * 2.0; // 1 pixel border on each side. 
        estimatedTableWidth += 16.0; // Scrollpane scroller width is approximately 16 pixels wide.

        // Add up the individual column sizes.
        for (TreeTableColumn<Entry, ?> col : entryTreeTable.getColumns())
            estimatedTableWidth += col.getWidth();

        // Clamp between [0.0, 1.0], and set it.
        double splitIndex = Math.min(Math.max(0.0, estimatedTableWidth / widthContainer), 1.0);
        splitPane.setDividerPositions(splitIndex);
    }

    @Override
    public Pane getRootPane() {
        return rootBorderPane;
    }

    public TreeTableView<Entry> getEntryTreeTable() {
        return entryTreeTable;
    }
}