/**
* This file is a part of GrantMaster.
* Copyright (C) 2015 Gábor Fehér <feherga@gmail.com>
*
* 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 com.github.gaborfeher.grantmaster.ui;
import com.github.gaborfeher.grantmaster.framework.base.TablePageControllerBase;
import com.github.gaborfeher.grantmaster.framework.base.TabSelectionChangeListener;
import com.github.gaborfeher.grantmaster.framework.utils.DatabaseConnection;
import com.github.gaborfeher.grantmaster.framework.utils.DatabaseSingleton;
import com.github.gaborfeher.grantmaster.framework.utils.MyFileLock;
import com.github.gaborfeher.grantmaster.framework.utils.Utils;
import com.github.gaborfeher.grantmaster.logic.entities.Project;
import com.github.gaborfeher.grantmaster.logic.wrappers.CurrencyWrapper;
import com.github.gaborfeher.grantmaster.logic.wrappers.GlobalBudgetCategoryWrapper;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;
import javafx.application.HostServices;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.stage.FileChooser;
import javafx.stage.Popup;
import javafx.stage.Stage;
import javax.persistence.EntityManager;
import org.slf4j.LoggerFactory;
public class MainPageController implements Initializable {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(MainPageController.class);
/**
* Number of static tabs shown. (The ones that are not open/close-able
* project tabs.
*/
private static final int NUM_SYSTEM_TABS = 6;
private static final int ACTIVATE_TAB_AT_STARTUP = 5;
private static final int ACTIVATE_TAB_AT_THIS_POS_AFTER_OPEN_PROJECT = 0;
@FXML private TextField pathLabel;
@FXML Parent root;
@FXML TabPane mainTabs;
List<Project> projects;
Stage stage;
@FXML ProjectListTabController projectListTabController;
@FXML CurrencyPairsTabController currencyPairsTabController;
@FXML AboutTabController aboutTabController;
private boolean allowCloseDatabase() {
if (DatabaseSingleton.INSTANCE.getUnsavedChange()) {
Alert alert = new Alert(AlertType.NONE);
alert.setTitle(Utils.getString("SaveBeforeCloseTitle"));
alert.setHeaderText(Utils.getString("SaveBeforeCloseQuestion"));
ButtonType saveButtonType = new ButtonType(Utils.getString("SaveBeforeCloseSave"));
ButtonType discardButtonType = new ButtonType(Utils.getString("SaveBeforeCloseClose"));
ButtonType cancelButtonType = new ButtonType(Utils.getString("SaveBeforeCloseCancel"));
alert.getButtonTypes().add(saveButtonType);
alert.getButtonTypes().add(discardButtonType);
alert.getButtonTypes().add(cancelButtonType);
((Button) alert.getDialogPane().lookupButton(cancelButtonType)).setDefaultButton(true);
ButtonType userChoice = alert.showAndWait().get();
if (userChoice == cancelButtonType) {
return false;
} else if (userChoice == discardButtonType) {
return true;
} else if (userChoice == saveButtonType) {
handleSaveButtonAction(null);
return !DatabaseSingleton.INSTANCE.getUnsavedChange();
} else {
logger.warn("allowCloseDatabase(): unknown userChoice: {}", userChoice);
return false;
}
}
return true;
}
public boolean shutdown() {
if (!allowCloseDatabase()) {
return false;
}
DatabaseSingleton.INSTANCE.close();
return true;
}
private void closeProjectTabs() {
mainTabs.getSelectionModel().selectFirst();
mainTabs.getTabs().remove(NUM_SYSTEM_TABS, mainTabs.getTabs().size());
}
private void resetAndRefreshTabs() {
closeProjectTabs();
mainTabs.getSelectionModel().select(ACTIVATE_TAB_AT_THIS_POS_AFTER_OPEN_PROJECT);
TabSelectionChangeListener.activateTab(mainTabs.getTabs().get(ACTIVATE_TAB_AT_THIS_POS_AFTER_OPEN_PROJECT));
}
public void addTab(Tab newTab) throws IOException {
for (int i = NUM_SYSTEM_TABS; i < mainTabs.getTabs().size(); ++i) {
Tab alreadyOpenTab = mainTabs.getTabs().get(i);
if (newTab.getText().equals(alreadyOpenTab.getText())) {
// The requested project is already open.
mainTabs.getSelectionModel().select(alreadyOpenTab);
return;
}
}
mainTabs.getTabs().add(newTab);
mainTabs.getSelectionModel().select(newTab);
}
private TabPane findTabPaneChild(Parent page) {
for (Node child : page.getChildrenUnmodifiable()) {
if (child instanceof TabPane) {
return (TabPane) child;
}
}
return null;
}
private FileChooser getFileChooserForHdbFiles(String title) {
FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter("zip-compressed hsqldb files", "*.hdb"));
fileChooser.setSelectedExtensionFilter(fileChooser.getExtensionFilters().get(0));
fileChooser.setTitle(title);
return fileChooser;
}
private void tryOpenFile(File path) {
DatabaseConnection.Errors errors = new DatabaseConnection.Errors();
DatabaseConnection connection = DatabaseConnection.openDatabase(path, errors);
if (connection != null) {
DatabaseSingleton.INSTANCE.setConnection(connection);
resetAndRefreshTabs();
pathLabel.setText(DatabaseSingleton.INSTANCE.getCurrentlyOpenArchiveFile().getAbsolutePath());
} else {
List<ButtonType> extraButtons = new ArrayList<>();
ButtonType forceBreakLock = new ButtonType(
Utils.getString("DatabaseConnection.ForceBreakLock"));
if (errors.lockingError) {
logger.warn("tryOpenFile('{}'): locking error", path);
extraButtons.add(forceBreakLock);
}
Optional<ButtonType> result = Utils.showListDialog(
"MainPage.OpenDatabase",
"MainPage.OpenDatabaseError",
errors.errorKeys,
extraButtons);
if (errors.lockingError && result.isPresent() && result.get() == forceBreakLock) {
logger.info("tryOpenFile('{}'): breaking lock", path);
MyFileLock.breakLock(path);
tryOpenFile(path); // retry
}
}
}
private void showError(String title, String header, String content) {
Alert alert = new Alert(AlertType.ERROR);
if (title != null) {
alert.setTitle(Utils.getString(title));
}
if (header != null) {
alert.setHeaderText(Utils.getString(header));
}
if (content != null) {
alert.setContentText(Utils.getString(content));
}
alert.showAndWait();
}
@FXML
private void handleOpenButtonAction(ActionEvent event) {
if (!allowCloseDatabase()) {
return;
}
FileChooser fileChooser = getFileChooserForHdbFiles(Utils.getString("MainPage.OpenDatabase"));
File path = fileChooser.showOpenDialog(stage);
if (path == null) {
return;
}
tryOpenFile(path);
}
@FXML
private void handleNewButtonAction(ActionEvent event) {
if (!allowCloseDatabase()) {
return;
}
DatabaseConnection connection = DatabaseConnection.createNewDatabase();
if (connection != null) {
closeProjectTabs();
DatabaseSingleton.INSTANCE.setConnection(connection);
boolean result = DatabaseSingleton.INSTANCE.transaction((EntityManager em) -> {
CurrencyWrapper.createDefaultCurrencies(em);
GlobalBudgetCategoryWrapper.createDefaultBudgetCategories(em);
return true;
});
if (!result) {
DatabaseSingleton.INSTANCE.setConnection(null);
return;
}
resetAndRefreshTabs();
pathLabel.setText(Utils.getString("MainPage.StatusNewDatabase"));
}
}
private File selectFileForSaving() {
FileChooser fileChooser = getFileChooserForHdbFiles(Utils.getString("MainPage.SaveDatabase"));
File selectedFile = fileChooser.showSaveDialog(stage);
if (selectedFile == null) {
return null;
}
if (!selectedFile.getAbsolutePath().endsWith(".hdb")) {
selectedFile = new File(selectedFile.getAbsolutePath() + ".hdb");
// The save dialog normally checks if the file exists, but now we need
// to check again for the .hdb file.
if (selectedFile.exists()) {
Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle(Utils.getString("MainPage.SaveDatabase"));
alert.setHeaderText(Utils.getString("SaveOverrideQuestion") + "\n" + selectedFile.getAbsolutePath());
if (alert.showAndWait().get() != ButtonType.OK) {
return null;
}
}
}
return selectedFile;
}
@FXML
private void handleSaveButtonAction(ActionEvent event) {
if (!DatabaseSingleton.INSTANCE.isConnected()) {
return;
}
boolean success = false;
if (DatabaseSingleton.INSTANCE.getCurrentlyOpenArchiveFile() == null) {
File pathToSave = selectFileForSaving();
if (pathToSave == null) {
return;
}
success = DatabaseSingleton.INSTANCE.saveAsDatabase(pathToSave);
} else {
success = DatabaseSingleton.INSTANCE.saveDatabase();
}
if (!success) {
showError("MainPage.SaveDatabase", "MainPage.SaveDatabaseError", null);
return;
}
pathLabel.setText(DatabaseSingleton.INSTANCE.getCurrentlyOpenArchiveFile().getAbsolutePath());
}
@FXML
private void handleSaveAsButtonAction(ActionEvent event) {
if (!DatabaseSingleton.INSTANCE.isConnected()) {
return;
}
File selectedFile = selectFileForSaving();
if (selectedFile == null) {
return;
}
if (!DatabaseSingleton.INSTANCE.saveAsDatabase(selectedFile)) {
showError("MainPage.SaveDatabase", "MainPage.SaveDatabaseError", null);
return;
}
pathLabel.setText(DatabaseSingleton.INSTANCE.getCurrentlyOpenArchiveFile().getAbsolutePath());
}
@FXML
private void handleExportSheetButtonAction(ActionEvent event) {
FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter("Excel spreadsheets (*.xls)", "*.xls"));
fileChooser.setSelectedExtensionFilter(fileChooser.getExtensionFilters().get(0));
fileChooser.setTitle(Utils.getString("MainPage.ExportToExcel"));
File exportFile = fileChooser.showSaveDialog(stage);
if (exportFile == null) {
return;
}
if (!exportFile.getAbsolutePath().endsWith(".xls")) {
exportFile = new File(exportFile.getAbsolutePath() + ".xls");
if (exportFile.exists()) {
Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle(Utils.getString("MainPage.ExportToExcel"));
alert.setHeaderText(
Utils.getString("SaveOverrideQuestion") +
"\n" +
exportFile.getAbsolutePath());
if (alert.showAndWait().get() != ButtonType.OK) {
exportFile = null;
}
}
}
TablePageControllerBase.exportActiveTabToXls(exportFile);
}
@FXML
private void handleContextHelpButtonAction(ActionEvent event) {
final Popup popup = new Popup();
Tab activeTab = TabSelectionChangeListener.getActiveTab();
if (activeTab == null || activeTab.getUserData() == null) {
return;
}
// The help text is currently stored in userData.
// TODO(gaborfeher): Find a better place.
String helpText = (String) activeTab.getUserData();
Label popupLabel = new Label(helpText);
popupLabel.setStyle("-fx-border-color: black;");
popup.setAutoHide(true);
popup.setAutoFix(true);
// Calculate popup placement coordinates.
Node eventSource = (Node) event.getSource();
Bounds sourceNodeBounds = eventSource.localToScreen(eventSource.getBoundsInLocal());
popup.setX(sourceNodeBounds.getMinX() - 5.0);
popup.setY(sourceNodeBounds.getMaxY() + 5.0);
popup.getContent().addAll(popupLabel);
popup.show(stage);
}
@Override
public void initialize(URL url, ResourceBundle rb) {
projectListTabController.init(this);
currencyPairsTabController.init(this);
mainTabs.getSelectionModel().selectedItemProperty().addListener(new TabSelectionChangeListener());
mainTabs.getSelectionModel().select(ACTIVATE_TAB_AT_STARTUP);
}
void setStage(Stage stage) {
this.stage = stage;
}
void setHostServices(HostServices hostServices) {
aboutTabController.setHostServices(hostServices);
}
}